From fcaa1f28eb22a90eac5816e8fe927490e1af5448 Mon Sep 17 00:00:00 2001 From: securesubmit-buildmaster Date: Thu, 14 Nov 2024 08:41:00 -0500 Subject: [PATCH] OctopusDeploy release: 13.0.5 --- CHANGELOG.md | 9 +- metadata.xml | 2 +- src/Gateways/GpApiConnector.php | 2 +- src/Mapping/GpApiMapping.php | 245 +++++----- .../Abstractions/IDeviceInterface.php | 3 +- .../Builders/TerminalSearchBuilder.php | 1 + src/Terminals/DeviceInterface.php | 3 +- src/Terminals/DeviceResponse.php | 3 +- ...onType.php => TransactionTypeResponse.php} | 2 +- .../Responses/DiamondCloudResponse.php | 4 +- src/Terminals/Entities/UDData.php | 4 +- src/Terminals/Enums/BatchReportType.php | 11 + src/Terminals/Enums/DisplayOption.php | 4 +- .../UPA/Entities/Enums/UpaSearchCriteria.php | 1 + src/Terminals/UPA/Responses/BatchList.php | 30 +- .../UPA/Responses/BatchReportResponse.php | 9 +- .../UPA/Responses/SignatureResponse.php | 6 +- .../UPA/Responses/TerminalSetupResponse.php | 5 +- .../UPA/Responses/TransactionResponse.php | 1 - .../UPA/Responses/UpaBatchReport.php | 2 - .../UPA/Responses/UpaResponseHandler.php | 18 +- .../UPA/SubGroups/RequestParamFields.php | 27 +- .../SubGroups/RequestTransactionFields.php | 2 +- src/Terminals/UPA/UpaController.php | 1 + src/Terminals/UPA/UpaInterface.php | 19 +- src/Utils/ArrayUtils.php | 9 +- .../CreditCardNotPresentTest.php | 11 +- .../GpApiConnector/GpApiDigitalWalletTest.php | 7 +- .../Gateways/Terminals/UPA/UpaMicTests.php | 422 +++++++++++++++++- .../Terminals/UPA/samples/download.png | Bin 25878 -> 10837 bytes 30 files changed, 669 insertions(+), 194 deletions(-) rename src/Terminals/Diamond/Entities/Enums/{TransactionType.php => TransactionTypeResponse.php} (88%) create mode 100644 src/Terminals/Enums/BatchReportType.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0da47089..48a6c8f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,14 @@ # Changelog -## Latest Version - v13.0.4 (11/06/24) +## Latest Version - v13.0.5 (11/14/24) +### Enhancements: +- [GP-API] - Add new mapping fields on digital wallet transaction response: masked_number_last4, brand, brand_reference +- [MITC UPA] - Add new commands: getAppInfo, getParam, setTimeZone,clearDataLake, reset, returnToIdle, getDeviceConfig, + print, scan, getDebugInfo, setDebugLevel, getDebugLevel, getSignatureFile, communicationCheck, logon, + findBatches, getBatchDetails, getBatchReport, displayMessage + +## v13.0.4 (11/07/24) ### Enhancements: - [Portico] Added 'GatewayTxnId' value to GatewayException message when available diff --git a/metadata.xml b/metadata.xml index 2f724235..c0c28278 100644 --- a/metadata.xml +++ b/metadata.xml @@ -1,3 +1,3 @@ - 13.0.4 + 13.0.5 \ No newline at end of file diff --git a/src/Gateways/GpApiConnector.php b/src/Gateways/GpApiConnector.php index 30a7fb76..9704f90e 100644 --- a/src/Gateways/GpApiConnector.php +++ b/src/Gateways/GpApiConnector.php @@ -177,7 +177,7 @@ public function processBoardingUser(PayFacBuilder $builder): User } $response = $this->executeProcess($builder); - return GpApiMapping::mapMerchantsEndpointResponse($response, $builder->userReference); + return GpApiMapping::mapMerchantsEndpointResponse($response); } public function processPayFac(PayFacBuilder $builder) diff --git a/src/Mapping/GpApiMapping.php b/src/Mapping/GpApiMapping.php index af1e2525..83c31d8f 100644 --- a/src/Mapping/GpApiMapping.php +++ b/src/Mapping/GpApiMapping.php @@ -2,6 +2,8 @@ namespace GlobalPayments\Api\Mapping; +use DateTime; +use Exception; use GlobalPayments\Api\Entities\Address; use GlobalPayments\Api\Entities\AddressCollection; use GlobalPayments\Api\Entities\AlternativePaymentResponse; @@ -29,6 +31,7 @@ use GlobalPayments\Api\Entities\Enums\TransactionStatus; use GlobalPayments\Api\Entities\Enums\UserType; use GlobalPayments\Api\Entities\Exceptions\ApiException; +use GlobalPayments\Api\Entities\Exceptions\ArgumentException; use GlobalPayments\Api\Entities\FileList; use GlobalPayments\Api\Entities\FileProcessor; use GlobalPayments\Api\Entities\FileUploaded; @@ -38,7 +41,6 @@ use GlobalPayments\Api\Entities\GpApi\PagedResult; use GlobalPayments\Api\Entities\PayerDetails; use GlobalPayments\Api\Entities\PayByLinkResponse; -use GlobalPayments\Api\Entities\PayFac\UserReference; use GlobalPayments\Api\Entities\PaymentMethodList; use GlobalPayments\Api\Entities\Person; use GlobalPayments\Api\Entities\PersonList; @@ -92,8 +94,9 @@ class GpApiMapping * * @param Object $response * @return Transaction + * @throws Exception */ - public static function mapResponse($response) + public static function mapResponse(object $response): Transaction { $transaction = new Transaction(); @@ -176,7 +179,8 @@ public static function mapResponse($response) } return $transaction; } - private static function mapBatchCloseResponse(BatchSummary &$batchSummary, $response) : void + + private static function mapBatchCloseResponse(BatchSummary &$batchSummary, $response): void { $batchSummary->id = $response->id; $batchSummary->processedDeviceId = $response->device_reference ?? null; @@ -190,7 +194,7 @@ private static function mapBatchCloseResponse(BatchSummary &$batchSummary, $resp } } - private static function extractBatchTotals($response) : BatchTotals + private static function extractBatchTotals($response): BatchTotals { $batchTotals = new BatchTotals(); $batchTotals->salesCount = $response->sales->count ?? null; @@ -212,6 +216,10 @@ private static function extractBatchTotals($response) : BatchTotals return $batchTotals; } + + /** + * @throws Exception + */ private static function mapTransferFundsAccountDetails(&$transaction, $transfersResponse): void { $transfers = new TransferFundsAccountCollection(); @@ -219,7 +227,7 @@ private static function mapTransferFundsAccountDetails(&$transaction, $transfers $transfer = new FundsAccountDetails(); $transfer->id = $transferResponse->id; $transfer->timeCreated = !empty($transferResponse->time_created) ? - new \DateTime($transferResponse->time_created) : ''; + new DateTime($transferResponse->time_created) : ''; $transfer->amount = !empty($transferResponse->amount) ? StringUtils::toAmount($transferResponse->amount) : null; $transfer->reference = $transferResponse->reference ?? null; @@ -230,7 +238,7 @@ private static function mapTransferFundsAccountDetails(&$transaction, $transfers $transaction->transfersFundsAccount = $transfers; } - private static function mapPaymentMethodTransactionDetails(Transaction &$transaction, $paymentMethodResponse) + private static function mapPaymentMethodTransactionDetails(Transaction &$transaction, $paymentMethodResponse): void { $cardIssuerResponse = new CardIssuerResponse(); $cardIssuerResponse->result = $paymentMethodResponse->result ?? null; @@ -241,26 +249,24 @@ private static function mapPaymentMethodTransactionDetails(Transaction &$transac $paymentMethodResponse->fingerprint : null; $transaction->fingerprintIndicator = !empty($paymentMethodResponse->fingerprint_presence_indicator) ? $paymentMethodResponse->fingerprint_presence_indicator : null; - if (!empty($paymentMethodResponse->card)) { - $card = $paymentMethodResponse->card; - $transaction->cardDetails = self::mapCardDetails($card); - - $transaction->cardLast4 = !empty($card->masked_number_last4) ? - $card->masked_number_last4 : null; - $transaction->cardType = !empty($card->brand) ? $card->brand : null; - $transaction->cvnResponseCode = !empty($card->cvv) ? $card->cvv : null; - $transaction->cvnResponseMessage = !empty($card->cvv_result) ? $card->cvv_result : null; - $transaction->cardBrandTransactionId = !empty($card->brand_reference) ? - $card->brand_reference : null; - $transaction->avsResponseCode = !empty($card->avs_postal_code_result) ? - $card->avs_postal_code_result : null; - $transaction->avsAddressResponse = !empty($card->avs_address_result) ? $card->avs_address_result : null; - $transaction->avsResponseMessage = !empty($card->avs_action) ? $card->avs_action : null; - $transaction->authorizationCode = $card->authcode ?? null; - if (!empty($card->provider)) { - self::mapCardIssuerResponse($cardIssuerResponse, $card->provider); + $paymentMethodObj = $paymentMethodResponse->card ?? ($paymentMethodResponse->digital_wallet ?? null); + if (!empty($paymentMethodObj)) { + $transaction->cardDetails = self::mapCardDetails($paymentMethodObj); + + $transaction->cardLast4 = $paymentMethodObj->masked_number_last4 ?? null; + $transaction->cardType = $paymentMethodObj->brand ?? null; + $transaction->cvnResponseCode = $paymentMethodObj->cvv ?? null; + $transaction->cvnResponseMessage = $paymentMethodObj->cvv_result ?? null; + $transaction->cardBrandTransactionId = $paymentMethodObj->brand_reference ?? null; + $transaction->avsResponseCode = $paymentMethodObj->avs_postal_code_result ?? null; + $transaction->avsAddressResponse = $paymentMethodObj->avs_address_result ?? null; + $transaction->avsResponseMessage = $paymentMethodObj->avs_action ?? null; + $transaction->authorizationCode = $paymentMethodObj->authcode ?? null; + if (!empty($paymentMethodObj->provider)) { + self::mapCardIssuerResponse($cardIssuerResponse, $paymentMethodObj->provider); } } + $transaction->cardIssuerResponse = $cardIssuerResponse; if (!empty($paymentMethodResponse->apm) && $paymentMethodResponse->apm->provider == strtolower(PaymentProvider::OPEN_BANKING) @@ -300,8 +306,8 @@ private static function mapPayerDetails($payer, $shippingAddress = null): PayerD $payerDetails->email = $payer->email ?? null; if (!empty($payer->billing_address)) { $billingAddress = $payer->billing_address; - $payerDetails->firstName = $billingAddress->first_name ?? null; - $payerDetails->lastName = $billingAddress->last_name ?? null; + $payerDetails->firstName = $billingAddress->first_name ?? null; + $payerDetails->lastName = $billingAddress->last_name ?? null; $payerDetails->billingAddress = self::mapAddressObject( $billingAddress, AddressType::BILLING @@ -333,7 +339,7 @@ private static function mapFraudManagement($fraudResponse): FraudManagementRespo return $fraudFilterResponse; } - private static function mapFraudResponseResult($fraudResponseResult) + private static function mapFraudResponseResult($fraudResponseResult): string { switch ($fraudResponseResult) { case 'PENDING_REVIEW': @@ -353,7 +359,7 @@ private static function mapFraudResponseResult($fraudResponseResult) } } - private static function mapDccInfo($response) + private static function mapDccInfo($response): DccRateData { $dccRateData = new DccRateData(); $dccRateData->cardHolderCurrency = !empty($response->payer_currency) ? $response->payer_currency : null; @@ -369,7 +375,7 @@ private static function mapDccInfo($response) $dccRateData->commissionPercentage = !empty($response->commission_percentage) ? $response->commission_percentage : null; $dccRateData->exchangeRateSourceTimestamp = !empty($response->exchange_rate_time_created) ? - $response->exchange_rate_time_created: null; + $response->exchange_rate_time_created : null; $dccRateData->dccId = !empty($response->id) ? $response->id : null; return $dccRateData; @@ -378,8 +384,11 @@ private static function mapDccInfo($response) /** * @param $response * @param $reportType + * @return PayByLinkSummary|TransactionSummary|PagedResult|MerchantAccountSummary|DisputeSummary|ActionSummary|StoredPaymentMethodSummary|DepositSummary|DisputeDocument + * @throws ApiException + * @throws Exception */ - public static function mapReportResponse($response, $reportType) + public static function mapReportResponse($response, $reportType): PayByLinkSummary|TransactionSummary|PagedResult|MerchantAccountSummary|DisputeSummary|ActionSummary|StoredPaymentMethodSummary|DepositSummary|DisputeDocument { switch ($reportType) { case ReportType::TRANSACTION_DETAIL: @@ -398,7 +407,7 @@ public static function mapReportResponse($response, $reportType) case ReportType::FIND_DEPOSITS_PAGED: $report = self::setPagingInfo($response); foreach ($response->deposits as $deposit) { - array_push($report->result, self::mapDepositSummary($deposit)); + $report->result[] = self::mapDepositSummary($deposit); } break; case ReportType::DOCUMENT_DISPUTE_DETAIL: @@ -414,13 +423,13 @@ public static function mapReportResponse($response, $reportType) case ReportType::FIND_SETTLEMENT_DISPUTES_PAGED: $report = self::setPagingInfo($response); foreach ($response->disputes as $dispute) { - array_push($report->result, self::mapDisputeSummary($dispute)); + $report->result[] = self::mapDisputeSummary($dispute); } break; case ReportType::FIND_STORED_PAYMENT_METHODS_PAGED: $report = self::setPagingInfo($response); foreach ($response->payment_methods as $spm) { - array_push($report->result, self::mapStoredPaymentMethodSummary($spm)); + $report->result[] = self::mapStoredPaymentMethodSummary($spm); } break; case ReportType::STORED_PAYMENT_METHOD_DETAIL: @@ -432,7 +441,7 @@ public static function mapReportResponse($response, $reportType) case ReportType::FIND_ACTIONS_PAGED: $report = self::setPagingInfo($response); foreach ($response->actions as $action) { - array_push($report->result, self::mapActionsSummary($action)); + $report->result[] = self::mapActionsSummary($action); } break; case ReportType::PAYBYLINK_DETAIL: @@ -441,19 +450,19 @@ public static function mapReportResponse($response, $reportType) case ReportType::FIND_PAYBYLINK_PAGED: $report = self::setPagingInfo($response); foreach ($response->links as $link) { - array_push($report->result, self::mapPayByLinkSummary($link)); + $report->result[] = self::mapPayByLinkSummary($link); } break; case ReportType::FIND_MERCHANTS_PAGED: $report = self::setPagingInfo($response); foreach ($response->merchants as $merchant) { - array_push($report->result, self::mapMerchantSummary($merchant)); + $report->result[] = self::mapMerchantSummary($merchant); } return $report; case ReportType::FIND_ACCOUNTS_PAGED: $report = self::setPagingInfo($response); foreach ($response->accounts as $account) { - array_push($report->result, self::mapMerchantAccountSummary($account)); + $report->result[] = self::mapMerchantAccountSummary($account); } return $report; case ReportType::FIND_ACCOUNT_DETAIL: @@ -470,22 +479,23 @@ public static function mapReportResponse($response, $reportType) * * @param $response * @return TransactionSummary + * @throws Exception */ - public static function mapTransactionSummary($response) + public static function mapTransactionSummary($response): TransactionSummary { $summary = self::createTransactionSummary($response); $summary->transactionLocalDate = !empty($response->time_created_reference) ? - new \DateTime($response->time_created_reference) : ''; + new DateTime($response->time_created_reference) : ''; $summary->batchSequenceNumber = $response->batch_id; $summary->country = !empty($response->country) ? $response->country : null; $summary->originalTransactionId = !empty($response->parent_resource_id) ? $response->parent_resource_id : null; $summary->depositReference = !empty($response->deposit_id) ? $response->deposit_id : ''; $summary->depositStatus = !empty($response->deposit_status) ? $response->deposit_status : ''; $summary->depositTimeCreated = !empty($response->deposit_time_created) ? - new \DateTime($response->deposit_time_created) : ''; + new DateTime($response->deposit_time_created) : ''; $summary->settlementAmount = !empty($response->deposit_amount) ? StringUtils::toAmount($response->deposit_amount) : null; - $summary->batchCloseDate = !empty($response->batch_time_created) ? new \DateTime($response->batch_time_created) : ''; + $summary->batchCloseDate = !empty($response->batch_time_created) ? new DateTime($response->batch_time_created) : ''; $summary->orderId = $response->order->reference ?? null; if (isset($response->system)) { @@ -501,7 +511,7 @@ public static function mapTransactionSummary($response) /** map card details */ if (isset($paymentMethod->card)) { $card = $paymentMethod->card; - $summary->aquirerReferenceNumber = isset($card->arn) ? $card->arn : null; + $summary->aquirerReferenceNumber = $card->arn ?? null; $summary->maskedCardNumber = $card->masked_number_first6last4 ?? null; $summary->paymentType = PaymentMethodName::CARD; $summary->cardDetails = self::mapCardDetails($card); @@ -547,7 +557,8 @@ public static function mapTransactionSummary($response) $bankPaymentResponse->redirectUrl = $response->payment_method->redirect_url ?? null; $summary->bankPaymentResponse = $bankPaymentResponse; $summary->accountNumberLast4 = $response->payment_method->bank_transfer->masked_account_number_last4 ?? null; - } else { /** map APMs (Paypal) response info */ + } else { + /** map APMs (Paypal) response info */ $apm = $response->payment_method->apm; $alternativePaymentResponse = new AlternativePaymentResponse(); $alternativePaymentResponse->redirectUrl = !empty($response->payment_method->redirect_url) ? @@ -588,12 +599,13 @@ public static function mapTransactionSummary($response) * @param Object $response * * @return DepositSummary + * @throws Exception */ - public static function mapDepositSummary($response) + public static function mapDepositSummary(object $response): DepositSummary { $summary = new DepositSummary(); $summary->depositId = $response->id; - $summary->depositDate = new \DateTime($response->time_created); + $summary->depositDate = new DateTime($response->time_created); $summary->status = $response->status; $summary->type = $response->funding_type; $summary->amount = StringUtils::toAmount($response->amount); @@ -603,13 +615,13 @@ public static function mapDepositSummary($response) } if (isset($response->sales)) { $sales = $response->sales; - $summary->salesTotalCount = isset($sales->count) ? $sales->count : 0; + $summary->salesTotalCount = $sales->count ?? 0; $summary->salesTotalAmount = isset($sales->amount) ? StringUtils::toAmount($sales->amount) : 0; } if (isset($response->refunds)) { $refunds = $response->refunds; - $summary->refundsTotalCount = isset($refunds->count) ? $refunds->count : 0; + $summary->refundsTotalCount = $refunds->count ?? 0; $summary->refundsTotalAmount = isset($refunds->amount) ? StringUtils::toAmount($refunds->amount) : 0; } @@ -620,11 +632,11 @@ public static function mapDepositSummary($response) if (isset($response->disputes)) { $disputes = $response->disputes; - $summary->chargebackTotalCount = isset($disputes->chargebacks->count) ? $disputes->chargebacks->count : 0; + $summary->chargebackTotalCount = $disputes->chargebacks->count ?? 0; $summary->chargebackTotalAmount = isset($disputes->chargebacks->amount) ? StringUtils::toAmount($disputes->chargebacks->amount) : 0; - $summary->adjustmentTotalCount = isset($disputes->reversals->count) ? $disputes->reversals->count : 0; + $summary->adjustmentTotalCount = $disputes->reversals->count ?? 0; $summary->adjustmentTotalAmount = isset($disputes->reversals->amount) ? StringUtils::toAmount($disputes->reversals->amount) : 0; } @@ -640,16 +652,17 @@ public static function mapDepositSummary($response) * @param Object $response * * @return DisputeSummary + * @throws Exception */ - public static function mapDisputeSummary($response) + public static function mapDisputeSummary(object $response): DisputeSummary { $summary = new DisputeSummary(); $summary->caseId = $response->id; - $summary->caseIdTime = !empty($response->time_created) ? new \DateTime($response->time_created) : null; + $summary->caseIdTime = !empty($response->time_created) ? new DateTime($response->time_created) : null; $summary->caseStatus = $response->status; $summary->caseStage = $response->stage; $summary->disputeStageTime = - (!empty($response->stage_time_created) ? new \DateTime($response->stage_time_created) : null); + (!empty($response->stage_time_created) ? new DateTime($response->stage_time_created) : null); $summary->caseAmount = StringUtils::toAmount($response->amount); $summary->caseCurrency = $response->currency; @@ -695,7 +708,7 @@ public static function mapDisputeSummary($response) $summary->reasonCode = $response->reason_code; $summary->reason = $response->reason_description; $summary->respondByDate = !empty($response->time_to_respond_by) ? - new \DateTime($response->time_to_respond_by) : null; + new DateTime($response->time_to_respond_by) : null; $summary->result = $response->result; $summary->fundingType = $response->funding_type ?? null; $summary->lastAdjustmentAmount = !empty($response->last_adjustment_amount) ? @@ -730,12 +743,13 @@ public static function mapDisputeSummary($response) * @param $response * * @return StoredPaymentMethodSummary + * @throws Exception */ - public static function mapStoredPaymentMethodSummary($response) + public static function mapStoredPaymentMethodSummary($response): StoredPaymentMethodSummary { $summary = new StoredPaymentMethodSummary(); $summary->paymentMethodId = $response->id; - $summary->timeCreated = !empty($response->time_created) ? new \DateTime($response->time_created) : ''; + $summary->timeCreated = !empty($response->time_created) ? new DateTime($response->time_created) : ''; $summary->status = !empty($response->status) ? $response->status : ''; $summary->reference = !empty($response->reference) ? $response->reference : ''; $summary->cardHolderName = !empty($response->name) ? $response->name : ''; @@ -750,12 +764,15 @@ public static function mapStoredPaymentMethodSummary($response) return $summary; } - public static function mapActionsSummary($response) + /** + * @throws Exception + */ + public static function mapActionsSummary($response): ActionSummary { $summary = new ActionSummary(); $summary->id = $response->id; - $summary->timeCreated = !empty($response->time_created) ? new \DateTime($response->time_created) : null; + $summary->timeCreated = !empty($response->time_created) ? new DateTime($response->time_created) : null; $summary->type = !empty($response->type) ? $response->type : null; $summary->resource = !empty($response->resource) ? $response->resource : null; $summary->resourceId = !empty($response->resource_id) ? $response->resource_id : null; @@ -770,10 +787,11 @@ public static function mapActionsSummary($response) $summary->accountId = !empty($response->account_id) ? $response->account_id : null; $summary->rawRequest = $response->message_received ?? null; $summary->rawResponse = $response->message_sent ?? null; + return $summary; } - public static function mapRiskAssessmentResponse($response) + public static function mapRiskAssessmentResponse($response): RiskAssessment { $riskAssessment = new RiskAssessment(); $riskAssessment->id = $response->id; @@ -803,7 +821,7 @@ public static function mapRiskAssessmentResponse($response) return $riskAssessment; } - public static function mapCardDetails($paymentMethod) : Card + public static function mapCardDetails($paymentMethod): Card { $card = new Card(); $card->maskedNumberLast4 = $paymentMethod->masked_number_last4 ?? null; @@ -827,8 +845,9 @@ public static function mapCardDetails($paymentMethod) : Card /** * @param Object $response + * @return Transaction */ - public static function mapResponseSecure3D($response) + public static function mapResponseSecure3D(object $response): Transaction { $transaction = new Transaction(); $threeDSecure = new ThreeDSecure(); @@ -836,16 +855,11 @@ public static function mapResponseSecure3D($response) if (!empty($response->three_ds->message_version)) { $messageVersion = $response->three_ds->message_version; - switch (substr($messageVersion, 0, 2)) { - case '1.': - $version = Secure3dVersion::ONE; - break; - case '2.': - $version = Secure3dVersion::TWO; - break; - default: - $version = Secure3dVersion::ANY; - } + $version = match (substr($messageVersion, 0, 2)) { + '1.' => Secure3dVersion::ONE, + '2.' => Secure3dVersion::TWO, + default => Secure3dVersion::ANY, + }; $threeDSecure->messageVersion = $messageVersion; $threeDSecure->setVersion($version); } @@ -865,8 +879,8 @@ public static function mapResponseSecure3D($response) $response->three_ds->acs_info_indicator : null; $threeDSecure->acsReferenceNumber = $response->three_ds->acs_reference_number ?? null; $threeDSecure->providerServerTransRef = $response->three_ds->server_trans_ref ?? null; - $threeDSecure->challengeMandated = !empty($response->three_ds->challenge_status) ? - ($response->three_ds->challenge_status == 'MANDATED') : false; + $threeDSecure->challengeMandated = !empty($response->three_ds->challenge_status) && + $response->three_ds->challenge_status == 'MANDATED'; $threeDSecure->payerAuthenticationRequest = !empty($response->three_ds->method_data->encoded_method_data) ? $response->three_ds->method_data->encoded_method_data : null; $threeDSecure->issuerAcsUrl = !empty($response->three_ds->method_url) ? $response->three_ds->method_url : null; @@ -923,7 +937,7 @@ public static function mapResponseSecure3D($response) foreach ($response->three_ds->message_extension as $messageExtension) { $msgItem = new MessageExtension(); $msgItem->criticalityIndicator = !empty($messageExtension->criticality_indicator) ? - $messageExtension->criticality_indicator : null; + $messageExtension->criticality_indicator : null; $msgItem->messageExtensionData = !empty($messageExtension->data) ? json_encode($messageExtension->data) : null; $msgItem->messageExtensionId = !empty($messageExtension->id) ? $messageExtension->id : null; @@ -937,15 +951,15 @@ public static function mapResponseSecure3D($response) return $transaction; } - private static function setPagingInfo($response) + private static function setPagingInfo($response): PagedResult { $pageInfo = new PagedResult(); $pageInfo->totalRecordCount = !empty($response->total_count) ? $response->total_count : (!empty($response->total_record_count) ? $response->total_record_count : null); - $pageInfo->pageSize = !empty($response->paging->page_size) ? $response->paging->page_size : null; - $pageInfo->page = !empty($response->paging->page) ? $response->paging->page : null; - $pageInfo->order = !empty($response->paging->order) ? $response->paging->order : null; - $pageInfo->orderBy = !empty($response->paging->order_by) ? $response->paging->order_by : null; + $pageInfo->pageSize = !empty($response->paging->page_size) ? $response->paging->page_size : null; + $pageInfo->page = !empty($response->paging->page) ? $response->paging->page : null; + $pageInfo->order = !empty($response->paging->order) ? $response->paging->order : null; + $pageInfo->orderBy = !empty($response->paging->order_by) ? $response->paging->order_by : null; return $pageInfo; } @@ -956,8 +970,9 @@ private static function setPagingInfo($response) * @param Object $response * * @return Transaction + * @throws Exception */ - public static function mapResponseAPM($response) + public static function mapResponseAPM(object $response): Transaction { $apm = new AlternativePaymentResponse(); $transaction = self::mapResponse($response); @@ -965,6 +980,8 @@ public static function mapResponseAPM($response) $apm->redirectUrl = !empty($response->payment_method->redirect_url) ? $response->payment_method->redirect_url : ($paymentMethodApm->redirect_url ?? null); $apm->qrCodeImage = $response->payment_method->qr_code ?? null; + $apm->timeCreatedReference = !empty($paymentMethodApm->time_created_reference) ? + new DateTime($paymentMethodApm->time_created_reference) : null; if (is_string($paymentMethodApm->provider)) { $apm->providerName = $paymentMethodApm->provider; } elseif (is_object($paymentMethodApm->provider)) { @@ -979,10 +996,9 @@ public static function mapResponseAPM($response) $apm->correlationReference = $paymentMethodApm->correlation_reference ?? null; $apm->versionReference = $paymentMethodApm->version_reference ?? null; $apm->buildReference = $paymentMethodApm->build_reference ?? null; - $apm->timeCreatedReference = !empty($paymentMethodApm->time_created_reference) ? - new \DateTime($paymentMethodApm->time_created_reference) : null; + $apm->transactionReference = !empty($paymentMethodApm->transaction_reference) ? - $paymentMethodApm->transaction_reference: null; + $paymentMethodApm->transaction_reference : null; $apm->secureAccountReference = !empty($paymentMethodApm->secure_account_reference) ? $paymentMethodApm->secure_account_reference : null; $apm->reasonCode = !empty($paymentMethodApm->reason_code) ? $paymentMethodApm->reason_code : null; @@ -990,7 +1006,7 @@ public static function mapResponseAPM($response) $apm->grossAmount = !empty($paymentMethodApm->gross_amount) ? StringUtils::toAmount($paymentMethodApm->gross_amount) : null; $apm->paymentTimeReference = !empty($paymentMethodApm->payment_time_reference) ? - new \DateTime($paymentMethodApm->payment_time_reference) : null; + new DateTime($paymentMethodApm->payment_time_reference) : null; $apm->paymentType = !empty($paymentMethodApm->payment_type) ? $paymentMethodApm->payment_type : null; $apm->paymentStatus = !empty($paymentMethodApm->payment_status) ? $paymentMethodApm->payment_status : null; $apm->type = !empty($paymentMethodApm->type) ? $paymentMethodApm->type : null; @@ -1026,8 +1042,9 @@ public static function mapResponseAPM($response) /** * @param $response * @return PayByLinkSummary + * @throws Exception */ - public static function mapPayByLinkSummary($response) + public static function mapPayByLinkSummary($response): PayByLinkSummary { $summary = new PayByLinkSummary(); $summary->merchantId = $response->merchant_id ?? null; @@ -1045,7 +1062,7 @@ public static function mapPayByLinkSummary($response) $summary->description = $response->description ?? null; $summary->viewedCount = $response->viewed_count ?? null; $summary->expirationDate = !empty($response->expiration_date) ? - new \DateTime($response->expiration_date) : null; + new DateTime($response->expiration_date) : null; $summary->shippable = $response->shippable ?? null; $summary->usageCount = $response->usage_count ?? null; @@ -1058,7 +1075,7 @@ public static function mapPayByLinkSummary($response) $summary->allowedPaymentMethods = $response->transactions->allowed_payment_methods ?? null; //@TODO check if (!empty($response->transactions->transaction_list)) { foreach ($response->transactions->transaction_list as $transaction) { - $summary->transactions[] = self::createTransactionSummary($transaction); + $summary->transactions[] = self::createTransactionSummary($transaction); } } } @@ -1066,7 +1083,10 @@ public static function mapPayByLinkSummary($response) return $summary; } - public static function mapPayByLinkResponse($response) + /** + * @throws Exception + */ + public static function mapPayByLinkResponse($response): PayByLinkResponse { $payByLinkResponse = new PayByLinkResponse(); $payByLinkResponse->id = $response->id; @@ -1080,7 +1100,7 @@ public static function mapPayByLinkResponse($response) $payByLinkResponse->name = $response->name ?? null; $payByLinkResponse->description = $response->description ?? null; $payByLinkResponse->viewedCount = $response->viewed_count ?? null; - $payByLinkResponse->expirationDate = !empty($response->expiration_date) ? new \DateTime($response->expiration_date) : null; + $payByLinkResponse->expirationDate = !empty($response->expiration_date) ? new DateTime($response->expiration_date) : null; $payByLinkResponse->isShippable = $response->shippable ?? null; return $payByLinkResponse; @@ -1092,13 +1112,14 @@ public static function mapPayByLinkResponse($response) * @param $response * * @return TransactionSummary + * @throws Exception */ private static function createTransactionSummary($response): TransactionSummary { $transaction = new TransactionSummary(); $transaction->transactionId = $response->id ?? null; $timeCreated = self::validateStringDate($response->time_created); - $transaction->transactionDate = !empty($timeCreated) ? new \DateTime($timeCreated) : ''; + $transaction->transactionDate = !empty($timeCreated) ? new DateTime($timeCreated) : ''; $transaction->transactionStatus = $response->status; $transaction->transactionType = $response->type; $transaction->channel = !empty($response->channel) ? $response->channel : null; @@ -1118,7 +1139,7 @@ private static function createTransactionSummary($response): TransactionSummary * * @return Transaction */ - private static function mapBNPLResponse($response,Transaction $transaction) + private static function mapBNPLResponse($response, Transaction $transaction): Transaction { $transaction->paymentMethodType = PaymentMethodType::BNPL; $bnplResponse = new BNPLResponse(); @@ -1131,9 +1152,9 @@ private static function mapBNPLResponse($response,Transaction $transaction) return $transaction; } - private static function mapMerchantAccountSummary($account) + private static function mapMerchantAccountSummary($account): MerchantAccountSummary { - $merchantAccountSummary = new MerchantAccountSummary(); + $merchantAccountSummary = new MerchantAccountSummary(); $merchantAccountSummary->id = $account->id ?? null; $merchantAccountSummary->type = $account->type ?? null; $merchantAccountSummary->name = $account->name ?? null; @@ -1175,14 +1196,14 @@ private static function mapMerchantSummary($merchant): MerchantSummary /** * @param $response - * @param UserReference $userReference * @return User * @throws UnsupportedTransactionException + * @throws Exception */ - public static function mapMerchantsEndpointResponse($response,?UserReference $userReference): User + public static function mapMerchantsEndpointResponse($response): User { if (empty($response->action->type)) { - throw new UnsupportedTransactionException(sprintf("Empty action type response!")); + throw new UnsupportedTransactionException("Empty action type response!"); } switch ($response->action->type) { @@ -1210,9 +1231,9 @@ public static function mapMerchantsEndpointResponse($response,?UserReference $us $user->name = $response->name ?? null; $user->userStatus = $response->status; $user->userType = $response->type; - $user->timeCreate = !empty($response->time_created) ? new \DateTime($response->time_created) : null; + $user->timeCreate = !empty($response->time_created) ? new DateTime($response->time_created) : null; $user->timeLastUpdated = !empty($response->time_last_updated) ? - new \DateTime($response->time_last_updated) : null; + new DateTime($response->time_last_updated) : null; $user->responseCode = $response->action->result_code ?? null; $user->statusDescription = $response->status_description ?? null; $user->email = $response->email ?? null; @@ -1235,7 +1256,7 @@ public static function mapMerchantsEndpointResponse($response,?UserReference $us self::mapMerchantPersonList($response->persons, $user); } if (!empty($response->payment_methods)) { - self::mapMerchantPaymentMethods($response->payment_methods,$user); + self::mapMerchantPaymentMethods($response->payment_methods, $user); } return $user; case self::FUNDS: @@ -1261,7 +1282,10 @@ public static function mapMerchantsEndpointResponse($response,?UserReference $us } } - public static function mapFileProcessingResponse($response) + /** + * @throws UnsupportedTransactionException + */ + public static function mapFileProcessingResponse($response): FileProcessor { $fp = new FileProcessor(); switch ($response->action->type) { @@ -1304,8 +1328,9 @@ private static function mapGeneralFileProcessingResponse($response, &$fp) /** * @param $paymentMethods * @param User $user + * @throws ArgumentException */ - private static function mapMerchantPaymentMethods($paymentMethods, &$user) + private static function mapMerchantPaymentMethods($paymentMethods, User &$user): void { $pmList = new PaymentMethodList(); foreach ($paymentMethods as $paymentMethod) { @@ -1334,13 +1359,12 @@ private static function mapMerchantPaymentMethods($paymentMethods, &$user) $user->paymentMethodList = $pmList; } - private static function mapAddressObject($address, $type = null) + private static function mapAddressObject($address, $type = null): ?Address { if (empty($address)) { return null; } $userAddress = new Address(); - $userAddress->type = $type; $userAddress->streetAddress1 = $address->line_1 ?? null; $userAddress->streetAddress2 = $address->line_2 ?? null; $userAddress->streetAddress3 = $address->line_3 ?? null; @@ -1353,7 +1377,7 @@ private static function mapAddressObject($address, $type = null) return $userAddress; } - private static function mapMerchantPersonList($persons, &$user) + private static function mapMerchantPersonList($persons, &$user): void { $personList = new PersonList(); foreach ($persons as $person) { @@ -1386,9 +1410,9 @@ private static function mapMerchantPersonList($persons, &$user) private static function validateStringDate($date): string { try { - new \DateTime($date); - } catch (\Exception $e) { - $errors = \DateTime::getLastErrors(); + new DateTime($date); + } catch (Exception) { + $errors = DateTime::getLastErrors(); if (isset($errors['error_count']) && $errors['error_count'] > 0) { return ''; } @@ -1403,7 +1427,7 @@ private static function validateStringDate($date): string * @param CardIssuerResponse $cardIssuer * @param $cardIssuerResponse */ - private static function mapCardIssuerResponse(CardIssuerResponse &$cardIssuer, $cardIssuerResponse) + private static function mapCardIssuerResponse(CardIssuerResponse &$cardIssuer, $cardIssuerResponse): void { $cardIssuer->result = $cardIssuerResponse->result ?? null; $cardIssuer->avsResult = $cardIssuerResponse->avs_result ?? null; @@ -1416,7 +1440,7 @@ private static function mapCardIssuerResponse(CardIssuerResponse &$cardIssuer, $ * @param TransactionSummary|DepositSummary $summary * @param $system */ - private static function mapSystemResponse(TransactionSummary|DepositSummary &$summary, $system) + private static function mapSystemResponse(TransactionSummary|DepositSummary &$summary, $system): void { if (!isset($system)) { return; @@ -1428,7 +1452,7 @@ private static function mapSystemResponse(TransactionSummary|DepositSummary &$su $summary->merchantDbaName = $system->dba ?? null; } - public static function mapRecurringEntity($response,RecurringEntity $recurringEntity) : RecurringEntity + public static function mapRecurringEntity($response, RecurringEntity $recurringEntity): ?RecurringEntity { switch (get_class($recurringEntity)) { case Customer::class: @@ -1449,8 +1473,7 @@ public static function mapRecurringEntity($response,RecurringEntity $recurringEn } return $payer; default: - break; + return null; } } - } \ No newline at end of file diff --git a/src/Terminals/Abstractions/IDeviceInterface.php b/src/Terminals/Abstractions/IDeviceInterface.php index 420c52d4..91bc6d5a 100644 --- a/src/Terminals/Abstractions/IDeviceInterface.php +++ b/src/Terminals/Abstractions/IDeviceInterface.php @@ -15,6 +15,7 @@ use GlobalPayments\Api\Terminals\Entities\PromptMessages; use GlobalPayments\Api\Terminals\Entities\ScanData; use GlobalPayments\Api\Terminals\Entities\UDData; +use GlobalPayments\Api\Terminals\Enums\BatchReportType; use GlobalPayments\Api\Terminals\Enums\DeviceConfigType; use GlobalPayments\Api\Terminals\Enums\DisplayOption; use GlobalPayments\Api\Terminals\Enums\PromptType; @@ -255,7 +256,7 @@ public function getLastResponse(); public function getSAFReport() : TerminalReportBuilder; public function getBatchReport() : TerminalReportBuilder; - public function getBatchDetails(?string $batchId = null, bool $printReport = false) : ITerminalReport; + public function getBatchDetails(?string $batchId = null, bool $printReport = false, string|BatchReportType $reportType = null) : ITerminalReport; public function findBatches() : TerminalReportBuilder; public function getOpenTabDetails() : TerminalReportBuilder; } diff --git a/src/Terminals/Builders/TerminalSearchBuilder.php b/src/Terminals/Builders/TerminalSearchBuilder.php index 31916ec9..c8c7f274 100644 --- a/src/Terminals/Builders/TerminalSearchBuilder.php +++ b/src/Terminals/Builders/TerminalSearchBuilder.php @@ -25,6 +25,7 @@ class TerminalSearchBuilder public string $ecrId; public string $reportOutput; + public ?string $reportType; public string $batch; public function __construct($reportBuilder) diff --git a/src/Terminals/DeviceInterface.php b/src/Terminals/DeviceInterface.php index ed06c78a..2426a845 100644 --- a/src/Terminals/DeviceInterface.php +++ b/src/Terminals/DeviceInterface.php @@ -22,6 +22,7 @@ use GlobalPayments\Api\Terminals\Entities\PromptMessages; use GlobalPayments\Api\Terminals\Entities\ScanData; use GlobalPayments\Api\Terminals\Entities\UDData; +use GlobalPayments\Api\Terminals\Enums\BatchReportType; use GlobalPayments\Api\Terminals\Enums\CurrencyType; use GlobalPayments\Api\Terminals\Abstractions\IDeviceInterface; use GlobalPayments\Api\Terminals\Enums\DeviceConfigType; @@ -436,7 +437,7 @@ public function getBatchReport(): TerminalReportBuilder ); } - public function getBatchDetails(?string $batchId = null,bool $printReport = false): ITerminalReport + public function getBatchDetails(?string $batchId = null,bool $printReport = false, string|BatchReportType $reportType = null): ITerminalReport { throw new UnsupportedTransactionException( "This method is not supported by the currently configured device." diff --git a/src/Terminals/DeviceResponse.php b/src/Terminals/DeviceResponse.php index 2fdffd00..1b4a1921 100644 --- a/src/Terminals/DeviceResponse.php +++ b/src/Terminals/DeviceResponse.php @@ -18,6 +18,5 @@ abstract class DeviceResponse implements IDeviceResponse, IBatchCloseResponse, I public $deviceResponseCode; /** @var string */ public $deviceResponseText; - /** @var string */ - public $referenceNumber; + public ?string $referenceNumber; } \ No newline at end of file diff --git a/src/Terminals/Diamond/Entities/Enums/TransactionType.php b/src/Terminals/Diamond/Entities/Enums/TransactionTypeResponse.php similarity index 88% rename from src/Terminals/Diamond/Entities/Enums/TransactionType.php rename to src/Terminals/Diamond/Entities/Enums/TransactionTypeResponse.php index 36083a00..e158a773 100644 --- a/src/Terminals/Diamond/Entities/Enums/TransactionType.php +++ b/src/Terminals/Diamond/Entities/Enums/TransactionTypeResponse.php @@ -4,7 +4,7 @@ use GlobalPayments\Api\Entities\Enum; -class TransactionType extends Enum +class TransactionTypeResponse extends Enum { const UNKNOWN = '0'; const SALE = '1'; diff --git a/src/Terminals/Diamond/Responses/DiamondCloudResponse.php b/src/Terminals/Diamond/Responses/DiamondCloudResponse.php index 94587a05..b382f9e6 100644 --- a/src/Terminals/Diamond/Responses/DiamondCloudResponse.php +++ b/src/Terminals/Diamond/Responses/DiamondCloudResponse.php @@ -7,7 +7,7 @@ use GlobalPayments\Api\Terminals\Diamond\Entities\Enums\AuthorizationType; use GlobalPayments\Api\Terminals\Diamond\Entities\Enums\CardSource; use GlobalPayments\Api\Terminals\Diamond\Entities\Enums\TransactionResult; -use GlobalPayments\Api\Terminals\Diamond\Entities\Enums\TransactionType; +use GlobalPayments\Api\Terminals\Diamond\Entities\Enums\TransactionTypeResponse; use GlobalPayments\Api\Terminals\TerminalResponse; use GlobalPayments\Api\Utils\StringUtils; @@ -196,7 +196,7 @@ public function __construct($rawResponse) $this->transactionCurrency = $paymentDetails->transactionCurrency ?? null; $this->transactionTitle = $paymentDetails->transactionTitle ?? null; $this->transactionType = isset($paymentDetails->type) ? - TransactionType::getKey($paymentDetails->type) : null; + TransactionTypeResponse::getKey($paymentDetails->type) : null; $this->emvCardTransactionCounter = $paymentDetails->ATC ?? null; $this->emvCryptogram = $paymentDetails->AC ?? null; $this->emvApplicationId = $paymentDetails->AID ?? null; diff --git a/src/Terminals/Entities/UDData.php b/src/Terminals/Entities/UDData.php index 0cb0a465..da14c01e 100644 --- a/src/Terminals/Entities/UDData.php +++ b/src/Terminals/Entities/UDData.php @@ -15,8 +15,8 @@ class UDData /** @var UDFileTypes Contains the parameters for the file to be loaded */ public string $fileType; - /** @var string Slot number of the data file */ - public string $slotNum; + /** @var int Slot number of the data file */ + public int $slotNum; /** @var string Filename of the file to be stored in the device. Must include the file extension. * Must not contain a file path. diff --git a/src/Terminals/Enums/BatchReportType.php b/src/Terminals/Enums/BatchReportType.php new file mode 100644 index 00000000..c5855168 --- /dev/null +++ b/src/Terminals/Enums/BatchReportType.php @@ -0,0 +1,11 @@ +parseResponse($jsonResponse); } - protected function parseResponse($jsonResponse) + /** + * @throws GatewayException + * @throws MessageException + */ + protected function parseResponse($jsonResponse): void { - if (empty($jsonResponse->data) || empty($jsonResponse->data->cmdResult)) { + parent::parseResponse(ArrayUtils::jsonToArray($jsonResponse)); + $firstDataNode = $this->isGpApiResponse($jsonResponse) ? $jsonResponse->response : ($jsonResponse->data ?? null); + if (empty($firstDataNode) || empty($firstDataNode->cmdResult)) { throw new GatewayException(self::INVALID_RESPONSE_FORMAT); } - $firstDataNode = $jsonResponse->data; + $secondNode = $firstDataNode->data; $cmdResult = $firstDataNode->cmdResult; $this->status = $cmdResult->result ?? null; $this->command = $firstDataNode->response; - $this->ecrId = $firstDataNode->ecrId ?? null; - if (empty($this->status) || $this->status !== 'Success') { - $this->deviceResponseText = sprintf("Error: %s - %s", $cmdResult->errorCode, $cmdResult->errorMessage); - return; - } - $batches = $firstDataNode->data->batchesAvail ?? null; + $this->ecrId = $firstDataNode->EcrId ?? null; + $this->referenceNumber = $jsonResponse->id ?? null; + $this->requestId = $firstDataNode->requestId ?? null; + $this->deviceResponseCode = in_array($this->status, ['Success', 'COMPLETE']) ? '00' : null; + $batches = $secondNode->batchesAvail ?? null; foreach ($batches as $batch) { $this->batchIds[] = $batch->batchId; } diff --git a/src/Terminals/UPA/Responses/BatchReportResponse.php b/src/Terminals/UPA/Responses/BatchReportResponse.php index cd47a613..7efc1e5d 100644 --- a/src/Terminals/UPA/Responses/BatchReportResponse.php +++ b/src/Terminals/UPA/Responses/BatchReportResponse.php @@ -2,6 +2,7 @@ namespace GlobalPayments\Api\Terminals\UPA\Responses; +use GlobalPayments\Api\Entities\Card; use GlobalPayments\Api\Entities\Exceptions\GatewayException; use GlobalPayments\Api\Entities\Reporting\TransactionList; use GlobalPayments\Api\Entities\Reporting\TransactionSummary; @@ -28,7 +29,7 @@ protected function parseJsonResponse($jsonResponse): void { parent::parseJsonResponse($jsonResponse); $firstDataNode = $this->isGpApiResponse($jsonResponse) ? $jsonResponse->response : $jsonResponse->data; - $this->ecrId = $firstDataNode->ecrId ?? null; + $this->ecrId = $firstDataNode->EcrId ?? null; $secondDataNode = $firstDataNode->data ?? null; $this->merchantName = $secondDataNode->merchantName ?? null; $this->multipleMessage = $secondDataNode->multipleMessage ?? null; @@ -69,7 +70,10 @@ private function mapTransactionDetails(BatchRecordResponse &$batchRecordResponse $transactionSummary->authorizedAmount = $transaction->authorizedAmount; $transactionSummary->cardEntryMethod = $transaction->cardAcquisition ?? null; $transactionSummary->cardType = $transaction->cardType; - $transactionSummary->maskedCardNumber = $transaction->maskedPAN ?? null; + $transactionSummary->maskedCardNumber = $transaction->maskedPan ?? null; + $transactionSummary->cardDetails = new Card(); + $transactionSummary->cardDetails->brand = $transaction->cardType ?? null; + $transactionSummary->cardDetails->maskedCardNumber = $transaction->maskedPan ?? null; $transactionSummary->referenceNumber = $transaction->referenceNumber; $transactionSummary->issuerTransactionId = $transaction->gatewayTxnId; $transactionSummary->clerkId = $transaction->clerkId ?? null; @@ -81,6 +85,7 @@ private function mapTransactionDetails(BatchRecordResponse &$batchRecordResponse $transactionSummary->gratuityAmount = $transaction->tipAmount ?? null; $transactionSummary->settlementAmount = $transaction->settleAmount ?? null; $transactionSummary->taxAmount = $transaction->taxAmount ?? null; + $transactionSummary->cardSwiped = $transaction->cardSwiped ?? null; $batchRecordResponse->transactionDetails->add($transactionSummary); } } diff --git a/src/Terminals/UPA/Responses/SignatureResponse.php b/src/Terminals/UPA/Responses/SignatureResponse.php index 3d9479d7..5fe42ec2 100644 --- a/src/Terminals/UPA/Responses/SignatureResponse.php +++ b/src/Terminals/UPA/Responses/SignatureResponse.php @@ -16,13 +16,13 @@ public function __construct($jsonResponse) public function parseResponse($jsonResponse): void { parent::parseResponse($jsonResponse); - - if (empty($jsonResponse['data']['data'])) { + $firstDataNode = $this->isGpApiResponse($jsonResponse) ? $jsonResponse['response'] : $jsonResponse['data']; + if (empty($firstDataNode['data'])) { throw new MessageException(self::INVALID_RESPONSE_FORMAT); } switch ($this->command) { case UpaMessageId::GET_SIGNATURE: - $this->signatureData = $jsonResponse['data']['data']['signatureData'] ?? null; + $this->signatureData = $firstDataNode['data']['signatureData'] ?? null; break; } } diff --git a/src/Terminals/UPA/Responses/TerminalSetupResponse.php b/src/Terminals/UPA/Responses/TerminalSetupResponse.php index dcb6c8d9..59f9843e 100644 --- a/src/Terminals/UPA/Responses/TerminalSetupResponse.php +++ b/src/Terminals/UPA/Responses/TerminalSetupResponse.php @@ -22,10 +22,11 @@ public function __construct($jsonResponse) public function parseResponse($jsonResponse): void { parent::parseResponse($jsonResponse); - if (empty($jsonResponse['data']['data'])) { + $firstDataNode = $this->isGpApiResponse($jsonResponse) ? $jsonResponse['response'] : $jsonResponse['data']; + if (empty($firstDataNode['data'])) { return; } - $secondNode = $jsonResponse['data']['data']; + $secondNode = $firstDataNode['data']; switch ($this->command) { case UpaMessageId::GET_CONFIG_CONTENTS: $this->configType = $secondNode['configType'] ?? ''; diff --git a/src/Terminals/UPA/Responses/TransactionResponse.php b/src/Terminals/UPA/Responses/TransactionResponse.php index 6abe28fc..7518d1ed 100644 --- a/src/Terminals/UPA/Responses/TransactionResponse.php +++ b/src/Terminals/UPA/Responses/TransactionResponse.php @@ -2,7 +2,6 @@ namespace GlobalPayments\Api\Terminals\UPA\Responses; -use GlobalPayments\Api\Entities\Enums\GatewayProvider; use GlobalPayments\Api\Entities\Exceptions\MessageException; use GlobalPayments\Api\Terminals\Abstractions\IBatchCloseResponse; use GlobalPayments\Api\Terminals\Entities\{PANDetails, ThreeDesDukpt,TrackData}; diff --git a/src/Terminals/UPA/Responses/UpaBatchReport.php b/src/Terminals/UPA/Responses/UpaBatchReport.php index 91b44924..cfc2a1e0 100644 --- a/src/Terminals/UPA/Responses/UpaBatchReport.php +++ b/src/Terminals/UPA/Responses/UpaBatchReport.php @@ -4,8 +4,6 @@ class UpaBatchReport extends UpaResponseHandler { - public $deviceResponseCode; - public $merchantName; public $batchSummary; diff --git a/src/Terminals/UPA/Responses/UpaResponseHandler.php b/src/Terminals/UPA/Responses/UpaResponseHandler.php index 00d02949..e96f78a2 100644 --- a/src/Terminals/UPA/Responses/UpaResponseHandler.php +++ b/src/Terminals/UPA/Responses/UpaResponseHandler.php @@ -6,6 +6,7 @@ use GlobalPayments\Api\Entities\Exceptions\GatewayException; use GlobalPayments\Api\Entities\Exceptions\MessageException; use GlobalPayments\Api\Terminals\TerminalResponse; +use GlobalPayments\Api\Utils\ArrayUtils; class UpaResponseHandler extends TerminalResponse { @@ -47,25 +48,20 @@ protected function parseResponse(array $response): void $this->ecrId = $firstNodeData['EcrId'] ?? ''; } - protected function isGpApiResponse($jsonResponse) : bool - { - if (is_object($jsonResponse)) { - $jsonResponse = $this->jsonToArray($jsonResponse); - } - return !empty($jsonResponse['provider']) && $jsonResponse['provider'] === GatewayProvider::GP_API; - } - /** * @throws MessageException */ protected function parseJsonResponse($response): void { - $response = $this->jsonToArray($response); + $response = ArrayUtils::jsonToArray($response); $this->parseResponse($response); } - private function jsonToArray(object $response) : array + protected function isGpApiResponse($jsonResponse) : bool { - return json_decode(json_encode($response), true); + if (is_object($jsonResponse)) { + $jsonResponse = ArrayUtils::jsonToArray($jsonResponse); + } + return !empty($jsonResponse['provider']) && $jsonResponse['provider'] === GatewayProvider::GP_API; } } diff --git a/src/Terminals/UPA/SubGroups/RequestParamFields.php b/src/Terminals/UPA/SubGroups/RequestParamFields.php index d1fe9a62..5477e7a1 100644 --- a/src/Terminals/UPA/SubGroups/RequestParamFields.php +++ b/src/Terminals/UPA/SubGroups/RequestParamFields.php @@ -2,11 +2,12 @@ namespace GlobalPayments\Api\Terminals\UPA\SubGroups; +use GlobalPayments\Api\Entities\Enums\TransactionModifier; +use GlobalPayments\Api\Entities\Enums\TransactionType; use GlobalPayments\Api\PaymentMethods\CreditCardData; use GlobalPayments\Api\Terminals\Abstractions\IRequestSubGroup; use GlobalPayments\Api\Entities\Enums\StoredCredentialInitiator; use GlobalPayments\Api\Terminals\Builders\TerminalBuilder; -use GlobalPayments\Api\Terminals\Diamond\Entities\Enums\TransactionType; class RequestParamFields implements IRequestSubGroup { @@ -65,10 +66,21 @@ public function setParams(TerminalBuilder $builder) case TransactionType::VOID: $this->clerkId = $builder->clerkId ?? null; return; + case TransactionType::SALE: + case TransactionType::REFUND: + case TransactionType::VERIFY: + case TransactionType::AUTH: + case TransactionType::CAPTURE: + $this->clerkId = $builder->clerkId; + break; + case TransactionType::EDIT: + if ($builder->transactionModifier !== TransactionModifier::UPDATE_LODGING_DETAILS) { + $this->clerkId = $builder->clerkId; + } + break; default: break; } - $this->clerkId = $builder->clerkId ?? null; if (!empty($builder->cardOnFileIndicator)) { $this->cardOnFileIndicator = ($builder->cardOnFileIndicator === StoredCredentialInitiator::CARDHOLDER) @@ -80,15 +92,16 @@ public function setParams(TerminalBuilder $builder) } if (!empty($builder->requestMultiUseToken)) { - $this->tokenRequest = $builder->requestMultiUseToken; + $this->tokenRequest = $builder->requestMultiUseToken === true ? 1 : 0; } - if ($builder->paymentMethod != null && - $builder->paymentMethod instanceof CreditCardData && - !empty($builder->paymentMethod->token) - ) { + if ( + $builder->paymentMethod instanceof CreditCardData && + !empty($builder->paymentMethod->token) + ) { $this->tokenValue = $builder->paymentMethod->token; } + if (isset($builder->shippingDate) && !empty($builder->invoiceNumber)) { $this->directMktInvoiceNbr = $builder->invoiceNumber; $this->directMktShipMonth = $builder->shippingDate->format('m'); diff --git a/src/Terminals/UPA/SubGroups/RequestTransactionFields.php b/src/Terminals/UPA/SubGroups/RequestTransactionFields.php index df03503d..926e3478 100644 --- a/src/Terminals/UPA/SubGroups/RequestTransactionFields.php +++ b/src/Terminals/UPA/SubGroups/RequestTransactionFields.php @@ -135,6 +135,7 @@ public function setParams(TerminalBuilder $builder) case TransactionType::EDIT: if ($builder->transactionModifier == TransactionModifier::UPDATE_LODGING_DETAILS) { $this->amount = sprintf('%07.2f', $builder->amount); + $this->clerkId = $builder->clerkId ?? null; } break; case TransactionType::SALE: @@ -200,7 +201,6 @@ public function setParams(TerminalBuilder $builder) $this->taxIndicator = $builder->taxExempt ?? null; $this->processCPC = $builder->processCPC ?? null; $this->purchaseOrder = $builder->orderId ?? null; - $this->clerkId = $builder->clerkId ?? null; if (isset($builder->confirmAmount)) { $this->confirmAmount = $builder->confirmAmount === true ? "Y" : "N"; } diff --git a/src/Terminals/UPA/UpaController.php b/src/Terminals/UPA/UpaController.php index 5ddaf7f3..6b550677 100644 --- a/src/Terminals/UPA/UpaController.php +++ b/src/Terminals/UPA/UpaController.php @@ -351,6 +351,7 @@ private function buildReportTransaction(TerminalReportBuilder $builder) : IDevic case TerminalReportType::GET_BATCH_DETAILS: $requestMessage['data']["params"] = [ "reportOutput" => $builder->searchBuilder->reportOutput ?? null, + "reportType" => $builder->searchBuilder->reportType ?? null, "batch" => $builder->searchBuilder->batch ?? null ]; break; diff --git a/src/Terminals/UPA/UpaInterface.php b/src/Terminals/UPA/UpaInterface.php index e1d65cec..885a25cd 100644 --- a/src/Terminals/UPA/UpaInterface.php +++ b/src/Terminals/UPA/UpaInterface.php @@ -23,7 +23,8 @@ TerminalAuthBuilder, TerminalManageBuilder, TerminalReportBuilder }; -use GlobalPayments\Api\Terminals\Enums\{DebugLevel, +use GlobalPayments\Api\Terminals\Enums\{BatchReportType, + DebugLevel, DeviceConfigType, DisplayOption, PromptType, @@ -337,11 +338,15 @@ public function batchReport($batchId) return new UpaBatchReport($rawResponse, UpaMessageId::GET_BATCH_REPORT); } - public function getBatchDetails(?string $batchId = null, bool $printReport = false) : ITerminalReport + public function getBatchDetails(?string $batchId = null, bool $printReport = false, string|BatchReportType $reportType = null) : ITerminalReport { $builder = (new TerminalReportBuilder(TerminalReportType::GET_BATCH_DETAILS)) - ->where(UpaSearchCriteria::BATCH, $batchId) - ->andCondition(UpaSearchCriteria::ECR_ID, "1"); + ->where(UpaSearchCriteria::ECR_ID, "1"); + + if (!empty($batchId)) { + $builder->andCondition(UpaSearchCriteria::BATCH, $batchId); + } + if (true === $printReport) { $builder->andCondition( UpaSearchCriteria::REPORT_OUTPUT, @@ -349,6 +354,10 @@ public function getBatchDetails(?string $batchId = null, bool $printReport = fal ); } + if (!empty($reportType)) { + $builder->andCondition(UpaSearchCriteria::REPORT_TYPE, $reportType); + } + return $builder->execute(); } @@ -712,7 +721,7 @@ public function executeUDData(UDData $screen) : IDeviceScreen $data['params'] = [ 'fileType' => $screen->fileType, 'slotNum' => $screen->slotNum, - 'displayOption' => $screen->displayOption + 'displayOption' => $screen->displayOption ?? null ]; $message = TerminalUtils::buildUPAMessage( diff --git a/src/Utils/ArrayUtils.php b/src/Utils/ArrayUtils.php index 4e2fbc21..a491eb92 100644 --- a/src/Utils/ArrayUtils.php +++ b/src/Utils/ArrayUtils.php @@ -11,14 +11,14 @@ class ArrayUtils * * @return array */ - public static function array_remove_empty($haystack) + public static function array_remove_empty(?array $haystack): array { if (is_null($haystack)) { return []; } foreach ($haystack as $key => $value) { if (is_array($value) || is_object($value)) { - $v = (array) $haystack[$key]; + $v = (array)$value; $haystack[$key] = self::array_remove_empty($v); } if (empty($haystack[$key])) { @@ -30,4 +30,9 @@ public static function array_remove_empty($haystack) return $haystack; } + + public static function jsonToArray(object $response) : array + { + return json_decode(json_encode($response), true); + } } \ No newline at end of file diff --git a/test/Integration/Gateways/GpApiConnector/CreditCardNotPresentTest.php b/test/Integration/Gateways/GpApiConnector/CreditCardNotPresentTest.php index d667e2f6..735a0cd6 100644 --- a/test/Integration/Gateways/GpApiConnector/CreditCardNotPresentTest.php +++ b/test/Integration/Gateways/GpApiConnector/CreditCardNotPresentTest.php @@ -18,6 +18,8 @@ use GlobalPayments\Api\Entities\Enums\StoredPaymentMethodSortProperty; use GlobalPayments\Api\Entities\Enums\TransactionStatus; use GlobalPayments\Api\Entities\Enums\TransactionType; +use GlobalPayments\Api\Entities\Exceptions\ArgumentException; +use GlobalPayments\Api\Entities\Exceptions\BuilderException; use GlobalPayments\Api\Entities\Exceptions\GatewayException; use GlobalPayments\Api\Entities\Reporting\SearchCriteria; use GlobalPayments\Api\Entities\StoredCredential; @@ -741,6 +743,9 @@ public function testCardDelete_WrongId() } } + /** + * @throws BuilderException + */ public function testCardTokenizationThenUpdate() { // process an auto-capture authorization @@ -1257,12 +1262,16 @@ public function testCreditSaleWith_InvalidFingerPrint() } } + /** + * @throws ArgumentException + * @throws BuilderException + */ public function testUpdatePaymentToken() { $startDate = (new DateTime())->modify('-30 days')->setTime(0, 0, 0); $response = ReportingService::findStoredPaymentMethodsPaged(1, 1) - ->orderBy(StoredPaymentMethodSortProperty::TIME_CREATED, SortDirection::DESC) + ->orderBy(StoredPaymentMethodSortProperty::TIME_CREATED) ->where(SearchCriteria::START_DATE, $startDate) ->execute(); diff --git a/test/Integration/Gateways/GpApiConnector/GpApiDigitalWalletTest.php b/test/Integration/Gateways/GpApiConnector/GpApiDigitalWalletTest.php index f930a185..ee2e5f39 100644 --- a/test/Integration/Gateways/GpApiConnector/GpApiDigitalWalletTest.php +++ b/test/Integration/Gateways/GpApiConnector/GpApiDigitalWalletTest.php @@ -4,6 +4,7 @@ use GlobalPayments\Api\Entities\Address; use GlobalPayments\Api\Entities\Enums\AddressType; +use GlobalPayments\Api\Entities\Enums\CardType; use GlobalPayments\Api\Entities\Enums\Channel; use GlobalPayments\Api\Entities\Enums\EncyptedMobileType; use GlobalPayments\Api\Entities\Enums\TransactionModifier; @@ -32,9 +33,9 @@ public function setup(): void $this->card->cardHolderName = "James Mason"; $this->clickToPayToken = '8144735251653223601'; $this->googlePayToken = '{ - "signature": "MEUCIQCYj/IzxdfmoL03Buao0VyKERNQ5b9Sk0Wqr2KNDdyy5wIgfax2jPYMpcKgfLtsIwMRLojt3/nqVfz4Y5njfnaQ0Jc=", + "signature": "MEUCIHES+D2qscALKRtWzGb9ti5USOkP1M5myGG+n2gnLw7oAiEAwFj7JeulajB71ZdW9LvjRNwB6A4v7yjgNwTkzAR+fNo=", "protocolVersion": "ECv1", - "signedMessage": "{\"encryptedMessage\":\"2UHlq0quCxAIvVaFX3QioTH38wAPR9a2RXYZmIKkfMhB1ilOECL3c0u01aDsBr/gd+wMGT41CzxT+rrlbt+nouko823FgFyP+ofdJmvmcm23LsqvCL3TRp3mvpSVZ221Jb8P9W2DriaFbVej6hb/zYcNwxfcydR44TCS2YpZGW/ZwydG898JOwCdZiMw5gh19UdmiioDGF09dYijGWsiQjgX9e5kJXJ0LTuuxxP2s6oc6CrMOtTmTP9qBhaYpCyYsZCpXDYwycBUQLcB5StFLmJD6msD9gtMWgK0cyKcZdRyQozdngNUa09EIvvrOr1SmGGHkTnddJze0gWD/G8rpZj4wosZoMmc6FAwEOFMkqKEUGqrYLpZRXKFzmU1wM76I5JBg9cy7XH78YJLeLopn9xgOd9VtNrccqWJN1UV\",\"ephemeralPublicKey\":\"BKpOmGO/XVU8FjhVY5xgNLvfz7HAxaUVdn/d5G43V/vE4Y2Yjdr8Z1Ot4WUWBWGh5kFx2MTjP+tPJiAUggc4DUU\\u003d\",\"tag\":\"KiOuZ7MyMamxKLGhEfiMzj4WmjFLda/9tAgK1yKGSJc\\u003d\"}" + "signedMessage": "{\"encryptedMessage\":\"a0X0HwBemGudk84o6K+MUZG1YwInK4rgmNT4bLwtOrbVhQ/2jaT2EX0HYaxi3C5o063++A7EJ5KIl7uwqTSp1GWHtAFqZaWdIMKgK+0ZuGliVkPqFmYmXSD1ksQJQw/veDbANfbtQUiR1c4ZBWm9l2SUDTAbk/BICbYzdWlMIxv/d+wQEWaxYLekCRojSMPAA/tsJigswY8tGAbimvi6Q0eKP7LBd0lLYCP2OnICorODdYcv9kM8RPNXniPphxJ+DKIw9brWb4zSUq0/sJjQYoXIbz/eVXJ5wxZOHf0FUXz2gRwAteziL2HpuxlnNKWgi06TC/CuxrfnqWgJ8QE6bb9NDOrjHxiZS4hZnFsqhmcHZL8Idnmc0fSNY+2zbDkLS+sNrsbzahpEIrHJBGNkNbOEcq3JmFIR8U7Nc30z\",\"ephemeralPublicKey\":\"BCs/ogxOtzEsAmrHwSw1M2Ly2AhX7dUQ/M+HFjFwT4J9MD+nIl8Raruw488czk43t7nC0+wJhWCDzpR3W3Af3TM\\u003d\",\"tag\":\"bjWbrD74J3QPaLtCk7/4RKOPlb0xe33eYcRqUSqMovI\\u003d\"}" }'; } @@ -202,6 +203,8 @@ public function testPayWithGooglePayEncrypted() ->execute(); $this->assertTransactionResponse($response, TransactionStatus::CAPTURED); + $this->assertNotEmpty($response->cardBrandTransactionId); + $this->assertEquals(CardType::VISA, $response->cardDetails->brand); } public function testGooglePayEncrypted_LinkedRefund() diff --git a/test/Integration/Gateways/Terminals/UPA/UpaMicTests.php b/test/Integration/Gateways/Terminals/UPA/UpaMicTests.php index ddf9cdd5..a54dbcd3 100644 --- a/test/Integration/Gateways/Terminals/UPA/UpaMicTests.php +++ b/test/Integration/Gateways/Terminals/UPA/UpaMicTests.php @@ -2,19 +2,46 @@ namespace GlobalPayments\Api\Tests\Integration\Gateways\Terminals\UPA; +use GlobalPayments\Api\Entities\AutoSubstantiation; use GlobalPayments\Api\Entities\Enums\Channel; +use GlobalPayments\Api\Entities\Enums\StoredCredentialInitiator; +use GlobalPayments\Api\Entities\Enums\TaxType; +use GlobalPayments\Api\Entities\Enums\TimeZoneIdentifier; 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\Abstractions\IDeviceResponse; use GlobalPayments\Api\Terminals\ConnectionConfig; +use GlobalPayments\Api\Terminals\Entities\MessageLines; +use GlobalPayments\Api\Terminals\Entities\PrintData; +use GlobalPayments\Api\Terminals\Entities\PromptMessages; +use GlobalPayments\Api\Terminals\Entities\ScanData; +use GlobalPayments\Api\Terminals\Entities\UDData; +use GlobalPayments\Api\Terminals\Enums\BatchReportType; use GlobalPayments\Api\Terminals\Enums\ConnectionModes; +use GlobalPayments\Api\Terminals\Enums\DebugLevel; +use GlobalPayments\Api\Terminals\Enums\DebugLogsOutput; +use GlobalPayments\Api\Terminals\Enums\DeviceConfigType; use GlobalPayments\Api\Terminals\Enums\DeviceType; +use GlobalPayments\Api\Terminals\Enums\DisplayOption; +use GlobalPayments\Api\Terminals\Enums\LogFileIndicator; +use GlobalPayments\Api\Terminals\Enums\UDFileTypes; use GlobalPayments\Api\Terminals\UPA\Entities\CancelParameters; +use GlobalPayments\Api\Terminals\UPA\Entities\Enums\UpaMessageId; +use GlobalPayments\Api\Terminals\UPA\Entities\Enums\UpaSearchCriteria; +use GlobalPayments\Api\Terminals\UPA\Entities\SignatureData; +use GlobalPayments\Api\Terminals\UPA\Responses\BatchList; +use GlobalPayments\Api\Terminals\UPA\Responses\BatchReportResponse; +use GlobalPayments\Api\Terminals\UPA\Responses\BatchTransaction; +use GlobalPayments\Api\Terminals\UPA\Responses\BatchTransactionList; use GlobalPayments\Api\Terminals\UPA\Responses\OpenTabDetailsResponse; +use GlobalPayments\Api\Terminals\UPA\Responses\SignatureResponse; +use GlobalPayments\Api\Terminals\UPA\Responses\TerminalSetupResponse; use GlobalPayments\Api\Terminals\UPA\Responses\TransactionResponse; +use GlobalPayments\Api\Terminals\UPA\Responses\UDScreenResponse; use GlobalPayments\Api\Tests\Data\BaseGpApiTestConfig; use GlobalPayments\Api\Tests\Integration\Gateways\Terminals\RequestIdProvider; use GlobalPayments\Api\Utils\GenerationUtils; @@ -94,22 +121,17 @@ public function testCreditSaleWithTerminalRefNumber() public function testLineItem() { $this->device->ecrId = '12'; - - $response = $this->device->lineItem("Line Item #1", "10.00"); - $this->assertNotNull($response); - $this->assertEquals('00', $response->deviceResponseCode); - - $response = $this->device->lineItem("Line Item #2", "11.00"); - $this->assertNotNull($response); - $this->assertEquals('00', $response->deviceResponseCode); - - $response = $this->device->lineItem("Line Item #3", "12.00"); - $this->assertNotNull($response); - $this->assertEquals('00', $response->deviceResponseCode); - - $this->assertNotNull($response); - $this->assertEquals('00', $response->deviceResponseCode); - $this->assertEquals('COMPLETE', $response->deviceResponseText); + $items = [ + ["Line Item #1", 10], + ["Line Item #2", 11], + ["Line Item #3", 13], + ]; + foreach ($items as $item) { + $response = $this->device->lineItem($item[0], $item[1]); + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->deviceResponseText); + } sleep(5); @@ -146,8 +168,9 @@ public function testCreditRefund() public function testCreditVerify() { + /** @var TransactionResponse $response */ $response = $this->device->verify() - ->withEcrId('13') + ->withEcrId('1') ->execute(); $this->assertNotNull($response); @@ -155,10 +178,15 @@ public function testCreditVerify() $this->assertEquals('COMPLETE', $response->deviceResponseText); } + /** + * @throws ApiException + */ public function testCreditVoid() { + /** @var TransactionResponse $response */ $response = $this->device->sale(10) ->withEcrId('13') + ->withClerkId('1234') ->execute(); $this->assertNotNull($response); @@ -171,6 +199,8 @@ public function testCreditVoid() $response = $this->device->void() ->withEcrId('13') ->withTransactionId($response->transactionId) + ->withClerkId($response->clerkId) + ->withClientTransactionId($response->referenceNumber) ->execute(); $this->assertNotNull($response); @@ -218,6 +248,7 @@ public function testEndOfDay() $this->assertEquals('00', $response->deviceResponseCode); $this->assertEquals('COMPLETE', $response->deviceResponseText); $this->assertNotEmpty($response->batchId); + $this->assertNotEmpty($response->batchSeqNbr); } public function testCancel() @@ -266,10 +297,367 @@ public function testGetOpenTabDetails() public function testReboot() { + /** @var TransactionResponse $response */ $response = $this->device->reboot(); $this->assertNotNull($response); $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + $this->assertNotNull($response->transactionId); sleep(60); } + + public function testGetAppInfo() + { + /** @var TransactionResponse $response */ + $response = $this->device->getAppInfo(); + + $this->assertNotNull($response); + $this->assertInstanceOf(IDeviceResponse::class, $response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + $this->assertNotNull($response->deviceSerialNum); + } + + public function testGetParam() + { + $params = ["TerminalLanguage", "PinBypassIsSupported"]; + /** @var TransactionResponse $response */ + $response = $this->device->getParam($params); + + $this->assertNotNull($response); + $this->assertInstanceOf(IDeviceResponse::class, $response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + } + + public function testSetTimeZone() + { + $response = $this->device->setTimeZone(TimeZoneIdentifier::AMERICA_LOS_ANGELES); + + $this->assertNotNull($response); + $this->assertInstanceOf(IDeviceResponse::class, $response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + } + + public function testClearDataLake() + { + $response = $this->device->clearDataLake(); + + $this->assertNotNull($response); + $this->assertInstanceOf(IDeviceResponse::class, $response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + } + + public function testRestart() + { + $response = $this->device->reset(); + + $this->assertNotNull($response); + $this->assertInstanceOf(IDeviceResponse::class, $response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + } + + public function testReturnToIdle() + { + $this->device->ecrId = '12'; + /** @var TransactionResponse $response */ + $response = $this->device->returnToIdle(); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + $this->assertNotNull($response->transactionId); + } + + public function testGetConfigContents() + { + /** @var TerminalSetupResponse $response */ + $response = $this->device->getDeviceConfig(DeviceConfigType::CONTACT_TERMINAL_CONFIG); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + $this->assertEquals(DeviceConfigType::CONTACT_TERMINAL_CONFIG, $response->configType); + $this->assertNotNull($response->fileContent); + file_put_contents("configuration.txt", $response->fileContent); + } + + public function testPrint() + { + $printData = new PrintData(); + $printData->line1 = 'Printing...'; + $printData->line2 = 'Please Wait...'; + $printData->displayOption = DisplayOption::NO_SCREEN_CHANGE; + $printData->filePath = __DIR__ . "/samples/download.png"; + + $this->device->ecrId = '12'; + /** @var TransactionResponse $response */ + $response = $this->device->print($printData); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + } + public function testScanQR() + { + $scanData = new ScanData(); + $scanData->header = 'SCAN'; + $scanData->prompts = new PromptMessages(); + $scanData->prompts->prompt1 = 'SCAN QR CODE'; + $scanData->prompts->prompt2 = 'ALIGN THE QR CODE WITHIN THE FRAME TO SCAN'; + + $this->device->ecrId = '12'; + /** @var TransactionResponse $response */ + $response = $this->device->scan($scanData); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + $this->assertNotNull($response->scanData); + } + + public function testGetDebugInfo() + { + $this->device->ecrId = '1'; + /** @var TransactionResponse $response */ + $response = $this->device->getDebugInfo("logs/DebugLogs", LogFileIndicator::DEBUG_FILE_1); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + $this->assertNotNull($response->debugFileContents); + $this->assertNotNull($response->debugFileLength); + } + + public function testSetDebugLevel() + { + $this->device->ecrId = '1'; + $response = $this->device->setDebugLevel([DebugLevel::PACKETS, DebugLevel::DATA], DebugLogsOutput::FILE); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + } + + public function testGetDebugLevel() + { + $this->device->ecrId = '1'; + /** @var TransactionResponse $response */ + $response = $this->device->getDebugLevel(); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + $this->assertEquals('PACKETS|DATA', $response->debugLevel); + } + + public function testGetSignature() + { + $data = new SignatureData(); + $data->prompts = new PromptMessages(); + $data->prompts->prompt1 = 'Please sign'; + $data->displayOption = DisplayOption::RETURN_TO_IDLE_SCREEN; + /** @var SignatureResponse $response */ + $response = $this->device->getSignatureFile($data); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + $this->assertNotNull($response->signatureData); + + /*Save image to file*/ + $imageType = explode('/', getimagesizefromstring(base64_decode($response->signatureData))['mime'])[1]; + file_put_contents("signature.$imageType", base64_decode($response->signatureData)); + } + + public function testCommunicationCheck() + { + /** @var TransactionResponse $response */ + $response = $this->device->communicationCheck(); + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + $this->assertEquals('Success', $response->gatewayResponseMessage); + } + + public function testLogon() + { + /** @var TransactionResponse $response */ + $response = $this->device->logon(); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + $this->assertEquals(UpaMessageId::LOGON, $response->command); + } + + public function testReturnDefaultScreen() + { + $this->markTestSkipped("APP005 - COMMAND NOT ALLOWED IN THE CURRENT APP MODE"); + $response = $this->device->returnDefaultScreen(DisplayOption::RETURN_TO_IDLE_SCREEN); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + } + + public function testGetEncryptionType() + { + $this->markTestSkipped("APP005 - COMMAND NOT ALLOWED IN THE CURRENT APP MODE"); + /** @var TransactionResponse $response */ + $response = $this->device->getEncryptionType(); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + $this->assertNotEmpty($response->dataEncryptionType); + } + + public function testLoadUDDataFile() + { + $this->markTestSkipped("UD006 - CANNOT LOAD USER-DEFINED FILE"); + $screen = new UDData(); + $screen->fileType = UDFileTypes::HTML5; + $screen->slotNum = '22'; + $screen->file = 'samples/UDDataFile.html'; + /** @var UDScreenResponse $response */ + $response = $this->device->loadUDData($screen); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + } + public function testRemoveUDDataFile() + { + $this->markTestSkipped("UD005 - CANNOT DELETE USER-DEFINED FILE"); + $screen = new UDData(); + $screen->fileType = UDFileTypes::HTML5; + $screen->slotNum = '1'; + + $this->device->ecrId = '1'; + /** @var UDScreenResponse $response */ + $response = $this->device->removeUDData($screen); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + } + + public function testExecuteUDDataFile() + { + $this->markTestSkipped("UD001 - INVALID SLOT NUMBER"); + $screen = new UDData(); + $screen->fileType = UDFileTypes::HTML5; + $screen->slotNum = 1; + $screen->displayOption = DisplayOption::RETURN_TO_IDLE_SCREEN; + + $this->device->ecrId = '1'; + /** @var UDScreenResponse $response */ + $response = $this->device->executeUDData($screen); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + } + + public function testInjectUDDataFile() + { + $this->markTestSkipped("INVALID_REQUEST_DATA - Request contains unexpected data"); + $screen = new UDData(); + $screen->fileType = UDFileTypes::HTML5; + $screen->fileName = "index.html"; + // this is the file with the content you want to inject + $screen->localFile = __DIR__ . '/samples/UDDataFile.html'; + + $this->device->ecrId = '1'; + /** @var UDScreenResponse $response */ + $response = $this->device->injectUDData($screen); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + } + + public function testFindAvailableBatches() + { + $this->device->ecrId = '12'; + /** @var BatchList $response */ + $response = $this->device->findBatches()->execute(); + + $this->assertNotNull($response); + $this->assertEquals('Success', $response->status); + $this->assertIsArray($response->batchIds); + $this->assertGreaterThan(0, $this->count($response->batchIds)); + } + + public function testGetBatchDetails() + { + /** @var BatchReportResponse $response */ + $response = $this->device->getBatchDetails(printReport: true); + + $this->assertNotNull($response); + $this->assertEquals('COMPLETE', $response->status); + $this->assertNotNull($response->batchRecord); + $this->assertNotNull($response->batchRecord->transactionDetails); + $this->assertNotEmpty($response->batchRecord->batchId); + } + + public function testGetBatchReport() + { + $this->device->ecrId = '13'; + /** @var BatchReportResponse $response */ + $response = $this->device->getBatchReport() + ->where(UpaSearchCriteria::BATCH, "1035184") + ->execute(); + + $this->assertNotNull($response); + $this->assertEquals('COMPLETE', $response->status); + } + + public function testCreditSaleFullParams() + { + $autoSubAmounts = new AutoSubstantiation(); + $autoSubAmounts->setDentalSubTotal(5.00); + $autoSubAmounts->setClinicSubTotal(5); + $autoSubAmounts->setVisionSubTotal(5); + $autoSubAmounts->setPrescriptionSubTotal(12.50); + + $response = $this->device->sale(5) + ->withEcrId('13') + ->withClerkId('1234') + ->withCardOnFileIndicator(StoredCredentialInitiator::CARDHOLDER) + ->withCardBrandTransId("transId") + ->withInvoiceNumber('123A10') + ->withShippingDate(new \DateTime()) + ->withTaxAmount(2.18) + ->withGratuity(12.56) + ->withTaxType(TaxType::TAX_EXEMPT) + ->withCashBack(0.1) + ->withConfirmationAmount(true) + ->withProcessCPC(true) +// ->withAutoSubstantiation($autoSubAmounts) + ->withRequestMultiUseToken(true) + ->execute(); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->deviceResponseText); + } + + public function testDisplayMessage() + { + $messageLines = new MessageLines(); + $messageLines->line1 = 'Please wait...'; + $messageLines->timeout = 0; + + $response = $this->device->displayMessage($messageLines); + + $this->assertNotNull($response); + $this->assertEquals('00', $response->deviceResponseCode); + $this->assertEquals('COMPLETE', $response->status); + } } \ No newline at end of file diff --git a/test/Integration/Gateways/Terminals/UPA/samples/download.png b/test/Integration/Gateways/Terminals/UPA/samples/download.png index c4c2ce42c04d9590ef7ebcd6275569d25d2ec069..4406f69e8a6bb4b3fefaae9a90149d513c5fbede 100644 GIT binary patch literal 10837 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 PU7~bX{Z5ITY2g0=NzErX