From 413e61e7d24a693130881010be96a78b76207f54 Mon Sep 17 00:00:00 2001 From: Leith Caldwell Date: Wed, 3 Dec 2025 21:38:42 +1300 Subject: [PATCH] Add saved card support Also: - minor null coalesce refactors - fix scrutinizer coverage upload - remove doesNotPerformAssertions where exceptions are expected --- .github/workflows/phpunit.yml | 11 +++++ composer.json | 2 +- src/Message/CompletePurchaseResponse.php | 12 +++++- src/Message/PurchaseRequest.php | 15 ++++++- .../Message/CompletePurchaseResponseTest.php | 39 ++++++++++++++---- tests/Message/PurchaseRequestTest.php | 41 +++++++++++++++++++ tests/Message/SecurityTest.php | 2 - .../WebservicePurchaseResponseTest.php | 18 -------- tests/RedirectGatewayTest.php | 1 + tests/WebserviceGatewayTest.php | 3 -- 10 files changed, 109 insertions(+), 35 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 2e4aaa7..e7b956e 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -33,6 +33,9 @@ jobs: steps: - name: "Checkout" uses: "actions/checkout@v4" + with: + # Fetch arbitrary more-than-one commit or Scrutinizer will error + fetch-depth: 10 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -63,3 +66,11 @@ jobs: - name: "Tests (PHPUnit 10+)" if: ${{ matrix.php-version >= '8.1' }} run: "vendor/bin/phpunit" + + - name: Upload Scrutinizer coverage + continue-on-error: true + uses: sudo-bot/action-scrutinizer@latest + # Do not run this step on forked versions of the main repository (example: contributor forks) + if: github.repository == 'patronbase/omnipay-redsys' + with: + cli-args: "--format=php-clover build/logs/clover.xml --revision=${{ github.event.pull_request.head.sha || github.sha }}" \ No newline at end of file diff --git a/composer.json b/composer.json index cfb24e5..4819d60 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ "require-dev": { "omnipay/tests": "dev-address3-support", "squizlabs/php_codesniffer": "^3.5", - "http-interop/http-factory-guzzle": "^1.1" + "guzzlehttp/psr7": "^2.0" }, "suggest": { "ext-openssl": "Required for hashing functions to check message signatures" diff --git a/src/Message/CompletePurchaseResponse.php b/src/Message/CompletePurchaseResponse.php index f33d5fc..28e6889 100644 --- a/src/Message/CompletePurchaseResponse.php +++ b/src/Message/CompletePurchaseResponse.php @@ -102,7 +102,7 @@ protected function getKey($key) if ($this->usingUpcaseParameters) { $key = strtoupper($key); } - return isset($this->merchantParameters[$key]) ? $this->merchantParameters[$key] : null; + return $this->merchantParameters[$key] ?? null; } /** @@ -134,4 +134,14 @@ public function getCardType() { return $this->getKey('Ds_Card_Type'); } + + /** + * Get the card reference (payment token) if available + * + * @return null|string + */ + public function getCardReference() + { + return $this->getKey('Ds_Merchant_Identifier'); + } } diff --git a/src/Message/PurchaseRequest.php b/src/Message/PurchaseRequest.php index 21460e1..8d9fc9d 100644 --- a/src/Message/PurchaseRequest.php +++ b/src/Message/PurchaseRequest.php @@ -171,7 +171,7 @@ public function setConsumerLanguage($value) } $value = str_pad($value, 3, '0', STR_PAD_LEFT); } elseif (!is_numeric($value)) { - $value = isset(self::$consumerLanguages[$value]) ? self::$consumerLanguages[$value] : '001'; + $value = self::$consumerLanguages[$value] ?? '001'; } return $this->setParameter('consumerLanguage', $value); @@ -262,6 +262,16 @@ public function setTerminalId($value) return $this->setParameter('terminalId', $value); } + public function getCreateToken() + { + return $this->getParameter('createToken'); + } + + public function setCreateToken($value) + { + return $this->setParameter('createToken', $value); + } + /** * Get the protocolVersion field * Corresponds to the Ds_Merchant_Emv3Ds.protocolVersion. field in Redsys documentation. @@ -1716,6 +1726,9 @@ public function getBaseData() 'Ds_Merchant_MerchantName' => $this->getMerchantName(), 'Ds_Merchant_ConsumerLanguage' => $this->getConsumerLanguage(), 'Ds_Merchant_MerchantData' => $this->getMerchantData(), + 'Ds_Merchant_Identifier' => $this->getCreateToken() + ? 'REQUIRED' + : ($this->getToken() ?? $this->getCardReference()), ]; } diff --git a/tests/Message/CompletePurchaseResponseTest.php b/tests/Message/CompletePurchaseResponseTest.php index 9e76bdd..f65fce7 100644 --- a/tests/Message/CompletePurchaseResponseTest.php +++ b/tests/Message/CompletePurchaseResponseTest.php @@ -31,6 +31,8 @@ public function testCompletePurchaseSuccess() $this->assertFalse($this->response->isRedirect()); $this->assertSame('999999', $this->response->getTransactionReference()); $this->assertSame(0, (int) $this->response->getMessage()); + $this->assertSame('C', $this->response->getCardType()); + $this->assertNull($this->response->getCardReference()); $checks = array( 'Ds_SignatureVersion' => 'HMAC_SHA256_V1', @@ -75,6 +77,8 @@ public function testCompletePurchaseSuccessUpperParameters() $this->assertFalse($this->response->isRedirect()); $this->assertSame('999999', $this->response->getTransactionReference()); $this->assertSame(0, (int) $this->response->getMessage()); + $this->assertSame('C', $this->response->getCardType()); + $this->assertNull($this->response->getCardReference()); $checks = array( 'DS_SIGNATUREVERSION' => 'HMAC_SHA256_V1', @@ -120,6 +124,7 @@ public function testCompletePurchaseFailure() $this->assertFalse($this->response->isRedirect()); $this->assertSame(180, (int) $this->response->getMessage()); $this->assertNull($this->response->getCardType()); + $this->assertNull($this->response->getCardReference()); $checks = array( 'Ds_SignatureVersion' => 'HMAC_SHA256_V1', @@ -163,6 +168,7 @@ public function testCompletePurchaseError() $this->assertFalse($this->response->isRedirect()); $this->assertSame(909, (int) $this->response->getMessage()); $this->assertNull($this->response->getCardType()); + $this->assertNull($this->response->getCardReference()); $checks = array( 'Ds_SignatureVersion' => 'HMAC_SHA256_V1', @@ -184,9 +190,30 @@ public function testCompletePurchaseError() $this->runChecks($checks); } - /** - * @doesNotPerformAssertions - */ + public function testCompletePurchaseReturnsCardReference() + { + $this->getMockRequest()->shouldReceive('getHmacKey')->once()->andReturn('Mk9m98IfEblmPfrpsawt7BmxObt98Jev'); + + $this->response = new CompletePurchaseResponse( + $this->getMockRequest(), + [ + 'Ds_SignatureVersion' => 'HMAC_SHA256_V1', + 'Ds_MerchantParameters' => 'eyJEc19EYXRlIjoiMTBcLzExXC8yMDE1IiwiRHNfSG91ciI6IjEyOjAwIiwiRHNfU2VjdXJlUG' + .'F5bWVudCI6IjEiLCJEc19BbW91bnQiOiIxNDUiLCJEc19DdXJyZW5jeSI6Ijk3OCIsIkRzX09yZGVyIjoiMDEyM2FiYyIsIk' + .'RzX01lcmNoYW50Q29kZSI6Ijk5OTAwODg4MSIsIkRzX1Rlcm1pbmFsIjoiODcxIiwiRHNfUmVzcG9uc2UiOiIwMDAwIiwiRH' + .'NfVHJhbnNhY3Rpb25UeXBlIjoiMCIsIkRzX01lcmNoYW50RGF0YSI6IlJlZjogOTl6eiIsIkRzX0F1dGhvcmlzYXRpb25Db2' + .'RlIjoiOTk5OTk5IiwiRHNfQ29uc3VtZXJMYW5ndWFnZSI6IjIiLCJEc19DYXJkX0NvdW50cnkiOiI3MjQiLCJEc19DYXJkX1' + .'R5cGUiOiJDIiwiRHNfTWVyY2hhbnRfSWRlbnRpZmllciI6IjEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Nj' + .'c4OTAifQ==', + 'Ds_Signature' => 'D_t6g3K47mE_DtF8ZHjmZBFw54E_lFxNVsZJ0NbEX2o=', + ] + ); + + $this->assertTrue($this->response->isSuccessful()); + $this->assertFalse($this->response->isRedirect()); + $this->assertSame('1234567890123456789012345678901234567890', $this->response->getCardReference()); + } + public function testCompletePurchaseInvalidNoParameters() { $this->expectException('Omnipay\Common\Exception\InvalidResponseException'); @@ -201,9 +228,6 @@ public function testCompletePurchaseInvalidNoParameters() ); } - /** - * @doesNotPerformAssertions - */ public function testCompletePurchaseInvalidNoOrder() { $this->expectException('Omnipay\Common\Exception\InvalidResponseException'); @@ -222,9 +246,6 @@ public function testCompletePurchaseInvalidNoOrder() ); } - /** - * @doesNotPerformAssertions - */ public function testCompletePurchaseInvalidSignature() { $this->expectException('Omnipay\Common\Exception\InvalidResponseException'); diff --git a/tests/Message/PurchaseRequestTest.php b/tests/Message/PurchaseRequestTest.php index 3470634..c4d6673 100644 --- a/tests/Message/PurchaseRequestTest.php +++ b/tests/Message/PurchaseRequestTest.php @@ -41,6 +41,7 @@ public function setUp(): void 'consumerLanguage' => 'en', 'merchantData' => 'Ref: 99zz', 'directPayment' => false, + 'createToken' => false, ]; $this->full3DSParams = $this->fullBaseParams + [ @@ -136,6 +137,7 @@ public function testGetBaseData() $this->assertSame('My Store', $data['Ds_Merchant_MerchantName']); $this->assertSame('002', $data['Ds_Merchant_ConsumerLanguage']); $this->assertSame('Ref: 99zz', $data['Ds_Merchant_MerchantData']); + $this->assertNull($data['Ds_Merchant_Identifier']); } public function testGetDataTestMode() @@ -186,6 +188,45 @@ public function testSetDirectPayment() $this->assertNull($this->request->getDirectPayment()); } + public function testGetDataOnlyCreateToken() + { + $options = array_merge($this->fullBaseParams, ['createToken' => true]); + $this->request->initialize($options); + $this->assertTrue($this->request->getCreateToken()); + + $data = $this->request->getData(); + + $this->assertSame('REQUIRED', $data['Ds_Merchant_Identifier']); + } + + public function testCardReference() + { + $options = array_merge($this->fullBaseParams, [ + 'createToken' => null, + 'cardReference' => '12345678901234567890', + 'token' => null, + ]); + $this->request->initialize($options); + + $data = $this->request->getData(); + + $this->assertSame('12345678901234567890', $data['Ds_Merchant_Identifier']); + } + + public function testToken() + { + $options = array_merge($this->fullBaseParams, [ + 'createToken' => null, + 'cardReference' => null, + 'token' => '23456789012345678901', + ]); + $this->request->initialize($options); + + $data = $this->request->getData(); + + $this->assertSame('23456789012345678901', $data['Ds_Merchant_Identifier']); + } + public function testGet3DSAccountInfoData() { $data = $this->request->get3DSAccountInfoData(); diff --git a/tests/Message/SecurityTest.php b/tests/Message/SecurityTest.php index 0012126..998ca72 100644 --- a/tests/Message/SecurityTest.php +++ b/tests/Message/SecurityTest.php @@ -61,8 +61,6 @@ public function testEncryptMessageSuccess() /** * Make sure correct exception fires when no valid extension is installed - * - * @doesNotPerformAssertions */ public function testEncryptMessageException() { diff --git a/tests/Message/WebservicePurchaseResponseTest.php b/tests/Message/WebservicePurchaseResponseTest.php index 9b4f212..fe5378b 100644 --- a/tests/Message/WebservicePurchaseResponseTest.php +++ b/tests/Message/WebservicePurchaseResponseTest.php @@ -108,9 +108,6 @@ public function testPurchaseFailure() $this->assertSame(180, (int) $this->response->getMessage()); } - /** - * @doesNotPerformAssertions - */ public function testPurchaseInvalidNoReturnCode() { $this->expectException('Omnipay\Common\Exception\InvalidResponseException'); @@ -123,9 +120,6 @@ public function testPurchaseInvalidNoReturnCode() ); } - /** - * @doesNotPerformAssertions - */ public function testPurchaseInvalidNoTransactionData() { $this->expectException('Omnipay\Common\Exception\InvalidResponseException'); @@ -138,9 +132,6 @@ public function testPurchaseInvalidNoTransactionData() ); } - /** - * @doesNotPerformAssertions - */ public function testPurchaseIntegrationError() { $this->expectException('Omnipay\Common\Exception\InvalidResponseException'); @@ -153,9 +144,6 @@ public function testPurchaseIntegrationError() ); } - /** - * @doesNotPerformAssertions - */ public function testCompletePurchaseInvalidNoOrder() { $this->expectException('Omnipay\Common\Exception\InvalidResponseException'); @@ -171,9 +159,6 @@ public function testCompletePurchaseInvalidNoOrder() ); } - /** - * @doesNotPerformAssertions - */ public function testCompletePurchaseInvalidMissingData() { $this->expectException('Omnipay\Common\Exception\InvalidResponseException'); @@ -191,9 +176,6 @@ public function testCompletePurchaseInvalidMissingData() } - /** - * @doesNotPerformAssertions - */ public function testPurchaseBadSignature() { $this->expectException('Omnipay\Common\Exception\InvalidResponseException'); diff --git a/tests/RedirectGatewayTest.php b/tests/RedirectGatewayTest.php index c932669..ace9b07 100644 --- a/tests/RedirectGatewayTest.php +++ b/tests/RedirectGatewayTest.php @@ -27,6 +27,7 @@ public function setUp(): void 'protocolVersion' => '2.1.0', 'transactionId' => '123abc', 'testMode' => true, + 'createToken' => true, ); } diff --git a/tests/WebserviceGatewayTest.php b/tests/WebserviceGatewayTest.php index 0fc7c9c..84657f3 100644 --- a/tests/WebserviceGatewayTest.php +++ b/tests/WebserviceGatewayTest.php @@ -83,9 +83,6 @@ public function testPurchaseError() $this->assertSame(909, (int) $response->getMessage()); } - /** - * @doesNotPerformAssertions - */ public function testPurchaseInvalid() { $this->expectException('Omnipay\Common\Exception\InvalidResponseException');