diff --git a/CHANGELOG.md b/CHANGELOG.md index 0da76c7d..650f6f6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,12 @@ # Changelog -## Latest Version - v12.0.8 (07/23/24) +## Latest Version - v12.0.9 (08/14/24) +### Enhancements: +- [PAX] Portico - Added support for HSA/FSA +- [MEET-IN-THE-CLOUD][UPA] - Add new mapping response fields for "/devices" endpoint + +## v12.0.8 (07/23/24) ### Bug Fixes: - [GP-API] Fix re-sign in after token expiration diff --git a/metadata.xml b/metadata.xml index 6d177324..8b54e8c6 100644 --- a/metadata.xml +++ b/metadata.xml @@ -1,3 +1,3 @@ - 12.0.8 + 12.0.9 \ No newline at end of file diff --git a/src/Terminals/Builders/TerminalAuthBuilder.php b/src/Terminals/Builders/TerminalAuthBuilder.php index 1e481ecf..c035d78e 100644 --- a/src/Terminals/Builders/TerminalAuthBuilder.php +++ b/src/Terminals/Builders/TerminalAuthBuilder.php @@ -59,6 +59,10 @@ class TerminalAuthBuilder extends TerminalBuilder public $tokenValue; + /** + * + * @var AutoSubstantiation + */ public $autoSubstantiation; public ?string $terminalRefNumber; diff --git a/src/Terminals/PAX/PaxController.php b/src/Terminals/PAX/PaxController.php index 9ff7e72e..e5bc92c4 100644 --- a/src/Terminals/PAX/PaxController.php +++ b/src/Terminals/PAX/PaxController.php @@ -173,6 +173,7 @@ public function processTransaction(TerminalAuthBuilder $builder) : TerminalRespo $trace->referenceNumber = $requestId; $trace->invoiceNumber = $builder->invoiceNumber; + if (!empty($builder->clientTransactionId)) { $trace->clientTransactionId = $builder->clientTransactionId; } @@ -214,6 +215,7 @@ public function processTransaction(TerminalAuthBuilder $builder) : TerminalRespo $avs->address = $builder->address->streetAddress1; $avs->zipCode = $builder->address->postalCode; } + $commercial->customerCode = $builder->customerCode; $commercial->poNumber = $builder->poNumber; $commercial->taxExempt = $builder->taxExempt; @@ -230,6 +232,9 @@ public function processTransaction(TerminalAuthBuilder $builder) : TerminalRespo if (empty($builder->gratuity)) { $extData->details[PaxExtData::TIP_REQUEST] = 1; } + + if (!empty($builder->autoSubstantiation)) + $extData->details[PaxExtData::PASS_THROUGH_DATA] = $builder->autoSubstantiation; $transactionType = $this->mapTransactionType($builder->transactionType, $builder->requestMultiUseToken); switch ($builder->paymentMethodType) { diff --git a/src/Terminals/PAX/SubGroups/ExtDataSubGroup.php b/src/Terminals/PAX/SubGroups/ExtDataSubGroup.php index 3eacb3ff..07e51470 100644 --- a/src/Terminals/PAX/SubGroups/ExtDataSubGroup.php +++ b/src/Terminals/PAX/SubGroups/ExtDataSubGroup.php @@ -2,22 +2,101 @@ namespace GlobalPayments\Api\Terminals\PAX\SubGroups; +use GlobalPayments\Api\Entities\AutoSubstantiation; use GlobalPayments\Api\Terminals\Abstractions\IRequestSubGroup; use GlobalPayments\Api\Terminals\Enums\ControlCodes; class ExtDataSubGroup implements IRequestSubGroup { + /** + * + * @var array + */ + public array $details; - public $details; - - public function getElementString() + /** + * + * @return string + */ + public function getElementString(): string { $message = ''; if (!empty($this->details)) { foreach ($this->details as $key => $val) { + if (is_a($val, 'GlobalPayments\Api\Entities\AutoSubstantiation')) { + $message .= sprintf( + "%s=%s", + $key, + $this->autoSubHelper($val) + ); + continue; + } $message .= sprintf("%s=%s%s", $key, $val, chr(ControlCodes::US)); } } return rtrim($message, chr(ControlCodes::US)); } + + /** + * + * @param AutoSubstantiation $info + * @return string + */ + private function autoSubHelper(AutoSubstantiation $info): string + { + $string = sprintf("%s%s", 'FSA', chr(ControlCodes::COLON)); + $string .= sprintf( + "%s%s%s|", + 'HealthCare', + chr(ControlCodes::COMMA), + $info->amounts["TOTAL_HEALTHCARE_AMT"] * 100 + ); + + if ($info->amounts["SUBTOTAL_PRESCRIPTION_AMT"] > 0) { + $string .= sprintf( + "%s%s%s|", + 'Rx', + chr(ControlCodes::COMMA), + $info->amounts["SUBTOTAL_PRESCRIPTION_AMT"] * 100 + ); + } + + if ($info->amounts["SUBTOTAL_VISION__OPTICAL_AMT"] > 0) { + $string .= sprintf( + "%s%s%s|", + 'Vision', + chr(ControlCodes::COMMA), + $info->amounts["SUBTOTAL_VISION__OPTICAL_AMT"] * 100 + ); + } + + if ($info->amounts["SUBTOTAL_DENTAL_AMT"] > 0) { + $string .= sprintf( + "%s%s%s|", + 'Dental', + chr(ControlCodes::COMMA), + $info->amounts["SUBTOTAL_DENTAL_AMT"] * 100 + ); + } + + if ($info->amounts["SUBTOTAL_CLINIC_OR_OTHER_AMT"] > 0) { + $string .= sprintf( + "%s%s%s|", + 'Clinical', + chr(ControlCodes::COMMA), + $info->amounts["SUBTOTAL_CLINIC_OR_OTHER_AMT"] * 100 + ); + } + + if ($info->amounts["SUBTOTAL_COPAY_AMT"] > 0) { + $string .= sprintf( + "%s%s%s|", + 'CoPay', + chr(ControlCodes::COMMA), + $info->amounts["SUBTOTAL_COPAY_AMT"] * 100 + ); + } + + return $string; + } } diff --git a/src/Terminals/UPA/Responses/TransactionResponse.php b/src/Terminals/UPA/Responses/TransactionResponse.php index 88ad1692..35e72141 100644 --- a/src/Terminals/UPA/Responses/TransactionResponse.php +++ b/src/Terminals/UPA/Responses/TransactionResponse.php @@ -7,6 +7,52 @@ class TransactionResponse extends UpaResponseHandler implements IBatchCloseResponse { + public string $responseId; + + public string $responseDateTime; + + public string $gatewayResponseCode; + + public string $gatewayResponseMessage; + + public string $avsResultCode; + + public string $avsResultText; + public float $totalAmount; + public float $authorizedAmount; + public string $CpcInd; + + public string $cardType; + public string $cardGroup; + public string $fallback; + + public string $qpsQualified; + public string $storeAndForward; + + public string $invoiceNumber; + public string $merchantId; + public string $cardBrandTransId; + public string $batchId; + public string $batchSeqNbr; + public string $pinVerified; + public string $applicationPAN; + public string $transactionSequenceCounter; + public string $additionalTerminalCapabilities; + public string $unpredictableNumber; + public string $applicationTransactionCounter; + public string $terminalType; + public string $terminalCapabilities; + public string $terminalCountryCode; + public string $issuerApplicationData; + public string $otherAmount; + public string $amountAuthorized; + public string $transactionTSI; + public string $transactionDate; + public string $transactionCurrencyCode; + public string $dedicatedDF; + public string $applicationAIP; + public string $applicationIdentifier; + public function __construct($jsonResponse) { $this->parseResponse($jsonResponse); @@ -15,31 +61,27 @@ public function __construct($jsonResponse) public function parseResponse($jsonResponse) { if ($this->isGpApiResponse($jsonResponse)) { - if ( - !empty($jsonResponse['action']['result_code']) && - $jsonResponse['action']['result_code'] === 'SUCCESS' - ) { - $this->deviceResponseCode = '00'; - } - $this->status = $jsonResponse['status'] ?? null; $this->transactionId = $jsonResponse['id'] ?? null; $this->deviceResponseText = $jsonResponse['status'] ?? null; + $secondDataNode = $jsonResponse['response']['data'] ?? null; + $cmdResult = $jsonResponse['response']['cmdResult'] ?? null; } else { - if (!empty($jsonResponse['data']['cmdResult'])) { - $this->checkResponse($jsonResponse['data']['cmdResult']); - - if ($jsonResponse['data']['cmdResult']['result'] === 'Success') { - $this->deviceResponseCode = '00'; - } - } + $cmdResult = $jsonResponse['data']['cmdResult'] ?? null; + $secondDataNode = $jsonResponse['data']['data'] ?? null; + } - if (!empty($jsonResponse['data']['data'])) { - $responseMapping = $this->getResponseMapping(); - foreach ($jsonResponse['data']['data'] as $responseData) { - if (is_array($responseData)) { - foreach ($responseData as $key => $value) { - $propertyName = !empty($responseMapping[$key]) ? $responseMapping[$key] : $key; + if (!empty($cmdResult)) { + $this->checkResponse($cmdResult); + $this->deviceResponseCode = ($cmdResult['result'] === 'Success' ? '00' : null); + } + if (!empty($secondDataNode)) { + $responseMapping = $this->getResponseMapping(); + foreach ($secondDataNode as $responseData) { + if (is_array($responseData)) { + foreach ($responseData as $key => $value) { + $propertyName = !empty($responseMapping[$key]) ? $responseMapping[$key] : $key; + if (property_exists($this, $propertyName)) { $this->{$propertyName} = $value; } } @@ -94,6 +136,7 @@ public function getResponseMapping() 'taxDue' => 'taxDue', 'tipDue' => 'tipDue', 'cardBrandTransId' => 'cardBrandTransId', + 'batchSeqNbr' => 'batchSeqNbr', //payment 'cardHolderName' => 'cardHolderName', @@ -108,6 +151,7 @@ public function getResponseMapping() 'storeAndForward' => 'storeAndForward', 'clerkId' => 'clerkId', 'invoiceNbr' => 'invoiceNumber', + 'expiryDate' => 'expirationDate', //EMV '4F' => 'applicationIdentifier', diff --git a/test/Integration/Gateways/PorticoConnector/Certifications/EcommerceTest.php b/test/Integration/Gateways/PorticoConnector/Certifications/EcommerceTest.php index 26d98923..d34c1362 100644 --- a/test/Integration/Gateways/PorticoConnector/Certifications/EcommerceTest.php +++ b/test/Integration/Gateways/PorticoConnector/Certifications/EcommerceTest.php @@ -1475,6 +1475,8 @@ public function test038FraudPreventionReturn() /// BALANCE INQUIRY + + public function test037BalanceInquiryGsb() { if (false === $this->usePrepaid) { diff --git a/test/Integration/Gateways/Terminals/PAX/PaxCreditTests.php b/test/Integration/Gateways/Terminals/PAX/PaxCreditTests.php index 8d4a30c5..6807f544 100644 --- a/test/Integration/Gateways/Terminals/PAX/PaxCreditTests.php +++ b/test/Integration/Gateways/Terminals/PAX/PaxCreditTests.php @@ -3,6 +3,7 @@ namespace GlobalPayments\Api\Tests\Integration\Gateways\Terminals\PAX; use GlobalPayments\Api\Entities\Address; +use GlobalPayments\Api\Entities\AutoSubstantiation; use GlobalPayments\Api\PaymentMethods\CreditCardData; use GlobalPayments\Api\Services\DeviceService; use GlobalPayments\Api\Terminals\ConnectionConfig; @@ -15,10 +16,6 @@ class PaxCreditTests extends TestCase { - /** - * - * @var PaxInterface - */ private $device; protected $card; protected $address; @@ -470,4 +467,40 @@ public function testTipNoPropmpt() : void $this->assertNotNull($response); $this->assertEquals("00", $response->responseCode); } + + /** + * + * @return void + * @throws InvalidArgumentException + * @throws ExpectationFailedException + */ + public function testAutoSubstantiation(): void + { + // Test card: 4393-4212-3456-1236 + // Exp Date: 12-29 + + $address = new Address(); + $address->streetAddress1 = '123 Main St.'; + $address->postalCode = '12345'; + + $card = new CreditCardData(); + $card->number = "4393421234561236"; + $card->expMonth = "12"; + $card->expYear = "29"; + $card->cvn = 123; + + $autosubstantiation = new AutoSubstantiation(); + $autosubstantiation->setDentalSubTotal(50); + $autosubstantiation->setVisionSubTotal(10.75); + + $response = $this->device->sale(60.75) + ->withPaymentMethod($card) + ->withAddress($address) + ->withAutoSubstantiation($autosubstantiation) + ->withAllowDuplicates(true) + ->execute(); + + $this->assertNotNull($response); + $this->assertEquals("60.75", $response->transactionAmount); + } } diff --git a/test/Integration/Gateways/Terminals/UPA/UpaMicTests.php b/test/Integration/Gateways/Terminals/UPA/UpaMicTests.php index 52a33408..45b6a1b0 100644 --- a/test/Integration/Gateways/Terminals/UPA/UpaMicTests.php +++ b/test/Integration/Gateways/Terminals/UPA/UpaMicTests.php @@ -5,12 +5,16 @@ use GlobalPayments\Api\Entities\Enums\Channel; use GlobalPayments\Api\Entities\Exceptions\ApiException; use GlobalPayments\Api\Entities\Exceptions\BuilderException; +use GlobalPayments\Api\Entities\Exceptions\GatewayException; use GlobalPayments\Api\Entities\GpApi\AccessTokenInfo; use GlobalPayments\Api\Services\DeviceService; use GlobalPayments\Api\Terminals\Abstractions\IDeviceInterface; use GlobalPayments\Api\Terminals\ConnectionConfig; use GlobalPayments\Api\Terminals\Enums\ConnectionModes; use GlobalPayments\Api\Terminals\Enums\DeviceType; +use GlobalPayments\Api\Terminals\UPA\Entities\CancelParameters; +use GlobalPayments\Api\Terminals\UPA\Responses\TransactionResponse; +use GlobalPayments\Api\Terminals\UPA\Responses\UpaReportHandler; use GlobalPayments\Api\Tests\Data\BaseGpApiTestConfig; use GlobalPayments\Api\Tests\Integration\Gateways\Terminals\RequestIdProvider; use GlobalPayments\Api\Utils\GenerationUtils; @@ -63,73 +67,61 @@ public function testCreditSale() $this->assertNotNull($response); $this->assertEquals('00', $response->deviceResponseCode); - $this->assertEquals('INITIATED', $response->deviceResponseText); + $this->assertEquals('COMPLETE', $response->deviceResponseText); } public function testCreditSaleWithTerminalRefNumber() { - $response = $this->device->sale(10) - ->withTerminalRefNumber(GenerationUtils::getGuid()) - ->withEcrId('13') - ->execute(); - - $this->assertNotNull($response); - $this->assertEquals('00', $response->deviceResponseCode); - $this->assertEquals('INITIATED', $response->deviceResponseText); + try { + $this->device->sale(10) + ->withTerminalRefNumber(GenerationUtils::getGuid()) + ->withEcrId('13') + ->execute(); + } catch (GatewayException $e) { + $this->assertStringContainsString('[tranNo]-UNKNOWN FIELD', $e->getMessage()); + } } public function testLineItem() { $this->device->ecrId = '12'; + $response = $this->device->lineItem("Line Item #1", "10.00"); $this->assertNotNull($response); $this->assertEquals('00', $response->deviceResponseCode); - $this->assertEquals('INITIATED', $response->deviceResponseText); - } - - public function testCreditAuth() - { - $response = $this->device->authorize(10) - ->withEcrId("10") - ->withTerminalRefNumber('1234') - ->execute(); + $response = $this->device->lineItem("Line Item #2", "11.00"); $this->assertNotNull($response); $this->assertEquals('00', $response->deviceResponseCode); - $this->assertEquals('INITIATED', $response->deviceResponseText); - } - - public function testCreditAuthAndCapture() - { - $response = $this->device->authorize(10) - ->withEcrId("13") - ->withTerminalRefNumber('1234') - ->execute(); + $response = $this->device->lineItem("Line Item #3", "12.00"); $this->assertNotNull($response); $this->assertEquals('00', $response->deviceResponseCode); - $this->assertEquals('INITIATED', $response->deviceResponseText); - - $response = $this->device->capture(10) - ->withEcrId("12") - ->withTransactionId($response->transactionId) - ->execute(); $this->assertNotNull($response); $this->assertEquals('00', $response->deviceResponseCode); - $this->assertEquals('INITIATED', $response->deviceResponseText); + $this->assertEquals('COMPLETE', $response->deviceResponseText); + + sleep(5); + + $cancelParams = new CancelParameters(); + $cancelParams->displayOption = "1"; + $cancelResponse = $this->device->cancel($cancelParams); + + $this->assertNotNull($cancelResponse); + $this->assertEquals('00', $cancelResponse->deviceResponseCode); + $this->assertEquals('COMPLETE', $cancelResponse->deviceResponseText); } - public function testCreditCapture_RandomId() + public function testCreditAuth() { - $response = $this->device->capture(10) - ->withEcrId('13') - ->withTransactionId(GenerationUtils::getGuid()) - ->execute(); - - $this->assertNotNull($response); - $this->assertEquals('00', $response->deviceResponseCode); - $this->assertEquals('INITIATED', $response->deviceResponseText); + try { + $this->device->authorize(10) + ->withEcrId("10") + ->execute(); + } catch (GatewayException $e) { + $this->assertStringContainsString('TRANSACTION CANCELLED COMMAND NOT SUPPORTED', $e->getMessage()); + } } public function testCreditRefund() @@ -140,7 +132,7 @@ public function testCreditRefund() $this->assertNotNull($response); $this->assertEquals('00', $response->deviceResponseCode); - $this->assertEquals('INITIATED', $response->deviceResponseText); + $this->assertEquals('COMPLETE', $response->deviceResponseText); } public function testCreditVerify() @@ -151,7 +143,7 @@ public function testCreditVerify() $this->assertNotNull($response); $this->assertEquals('00', $response->deviceResponseCode); - $this->assertEquals('INITIATED', $response->deviceResponseText); + $this->assertEquals('COMPLETE', $response->deviceResponseText); } public function testCreditVoid() @@ -162,9 +154,11 @@ public function testCreditVoid() $this->assertNotNull($response); $this->assertEquals('00', $response->deviceResponseCode); - $this->assertEquals('INITIATED', $response->deviceResponseText); + $this->assertEquals('COMPLETE', $response->deviceResponseText); $this->assertNotNull($response->transactionId); + sleep(15); + $response = $this->device->void() ->withEcrId('13') ->withTransactionId($response->transactionId) @@ -172,7 +166,7 @@ public function testCreditVoid() $this->assertNotNull($response); $this->assertEquals('00', $response->deviceResponseCode); - $this->assertEquals('INITIATED', $response->deviceResponseText); + $this->assertEquals('COMPLETE', $response->deviceResponseText); } public function testCreditSale_WithoutAmount() @@ -190,11 +184,11 @@ public function testCreditSale_WithoutAmount() } } - public function testCreditAuth_WithoutAmount() + public function testCreditRefund_WithoutAmount() { $exceptionCaught = false; try { - $this->device->authorize() + $this->device->refund() ->withEcrId('13') ->execute(); } catch (BuilderException $e) { @@ -205,43 +199,65 @@ public function testCreditAuth_WithoutAmount() } } - public function testCreditCapture_WithoutTransactionId() + public function testEndOfDay() { - $exceptionCaught = false; - try { - $this->device->capture(10) - ->withEcrId('13') - ->execute(); - } catch (BuilderException $e) { - $exceptionCaught = true; - $this->assertEquals('transactionId cannot be null for this transaction type.', $e->getMessage()); - } finally { - $this->assertTrue($exceptionCaught); - } + $this->device->ecrId = '13'; + /** @var TransactionResponse $response */ + $response = $this->device->endOfDay(); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->deviceResponseText); + $this->assertNotEmpty($response->batchId); } - public function testCreditRefund_WithoutAmount() + public function testCancel() { - $exceptionCaught = false; + $cancelParams = new CancelParameters(); + $cancelParams->displayOption = "1"; + + $cancelResponse = $this->device->cancel($cancelParams); + + $this->assertNotNull($cancelResponse); + $this->assertEquals('00', $cancelResponse->deviceResponseCode); + $this->assertEquals('COMPLETE', $cancelResponse->deviceResponseText); + } + + public function testCreditTipAdjust() + { + $response = $this->device->sale(10) + ->execute(); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + + sleep(3); + try { - $this->device->refund() - ->withEcrId('13') + $this->device->tipAdjust(1.05) + ->withTerminalRefNumber($response->terminalRefNumber) ->execute(); - } catch (BuilderException $e) { - $exceptionCaught = true; - $this->assertEquals('amount cannot be null for this transaction type.', $e->getMessage()); - } finally { - $this->assertTrue($exceptionCaught); + } catch (GatewayException $e) { + $this->assertStringContainsString('TRANSACTION CANCELLED TIP NOT SUPPORTED', $e->getMessage()); } } - public function testEndOfDay() + public function testGetOpenTabDetails() { - $this->device->ecrId = '13'; - $response = $this->device->endOfDay(); + /** @var UpaReportHandler $response */ + $response = $this->device->getOpenTabDetails(); + + $this->assertNotNull($response); + $this->assertEquals("00", $response->deviceResponseCode); + $this->assertNotNull($response->reportRecords); + } + + public function testReboot() + { + $response = $this->device->reboot(); $this->assertNotNull($response); $this->assertEquals('00', $response->deviceResponseCode); - $this->assertEquals('INITIATED', $response->deviceResponseText); + sleep(60); } } \ No newline at end of file