diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..ba58495 --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,75 @@ +name: "PHPUnit tests" + +on: + pull_request: + push: + branches: + - "master" + +jobs: + phpunit: + name: "PHPUnit tests" + + runs-on: "ubuntu-latest" + + strategy: + matrix: + dependencies: + - "highest" + php-version: + - "7.2" + - "7.3" + - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" + - "8.4" + + include: + - php-version: '7.2' + dependencies: "lowest" + + 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" + with: + coverage: "pcov" + php-version: "${{ matrix.php-version }}" + ini-values: memory_limit=-1 + tools: composer:v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Fixes any pubkeys failure (add a `composer diagnose` step to debug if necessary) + - name: "Composer force self-update" + run: "composer self-update" + + - name: "Install lowest dependencies" + if: ${{ matrix.dependencies == 'lowest' }} + run: "composer update --prefer-lowest --no-interaction --no-progress" + + - name: "Install highest dependencies" + if: ${{ matrix.dependencies == 'highest' }} + run: "composer update --no-interaction --no-progress" + + - name: "Tests (PHPUnit 9)" + if: ${{ matrix.php-version <= '8.0' }} + run: "vendor/bin/phpunit --configuration phpunit9.xml.dist" + + - 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-bpoint' + with: + cli-args: "--format=php-clover build/logs/clover.xml --revision=${{ github.event.pull_request.head.sha || github.sha }}" diff --git a/.gitignore b/.gitignore index 8a282a5..472a3b6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ composer.lock composer.phar phpunit.xml +/.phpunit.result.cache +/build diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 22814ad..0000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -language: php - -dist: trusty -php: - - "7.2" - - "7.1" - - "7.0" - - "5.6" - - "5.5" - - "5.4" - -matrix: - include: - - php: "5.3" - dist: precise - -before_script: - - composer install -n --dev --prefer-source - -script: - - vendor/bin/phpcs --standard=PSR2 src - - vendor/bin/phpunit --verbose --coverage-clover coverage.clover - -after_success: - - wget https://scrutinizer-ci.com/ocular.phar - - php ocular.phar code-coverage:upload --format=php-clover coverage.clover diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d67ed30..ea026a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,4 +7,4 @@ * Ensure your code is nicely formatted in the [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) style and that all tests pass. * Send the pull request. -* Check that the Travis CI build passed. If not, rinse and repeat. +* Check that the CI build passed. If not, rinse and repeat. diff --git a/LICENSE b/LICENSE index 758bb84..af5cb67 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Leith Caldwell +Copyright (c) 2020 Leith Caldwell Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index 64a5562..c25ef42 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **BPOINT driver for the Omnipay PHP payment processing library** -[![Build Status](https://travis-ci.org/PatronBase/omnipay-bpoint.png?branch=master)](https://travis-ci.org/PatronBase/omnipay-bpoint) +[![Build Status](https://github.com/PatronBase/omnipay-bpoint/actions/workflows/phpunit.yml/badge.svg?branch=master)](https://github.com/PatronBase/omnipay-bpoint/actions) [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/PatronBase/omnipay-bpoint.svg?style=flat)](https://scrutinizer-ci.com/g/PatronBase/omnipay-bpoint/code-structure) [![Code Quality](https://img.shields.io/scrutinizer/g/PatronBase/omnipay-bpoint.svg?style=flat)](https://scrutinizer-ci.com/g/PatronBase/omnipay-bpoint/?branch=master) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](LICENSE.md) @@ -11,8 +11,8 @@ [Omnipay](https://github.com/thephpleague/omnipay) is a framework agnostic, multi-gateway payment -processing library for PHP 5.3+. This package implements BPOINT support for Omnipay. It includes -support for both redirect (3-party) and webservice (2-party) versions of the gateway. +processing library for PHP 7.2+. This package implements BPOINT support for Omnipay. It includes +support for only the redirect (3-party) version of the gateway. ## Installation @@ -22,7 +22,7 @@ to your `composer.json` file: ```json { "require": { - "PatronBase/omnipay-bpoint": "~2.0" + "PatronBase/omnipay-bpoint": "~3.0" } } ``` diff --git a/composer.json b/composer.json index 366a236..35e313b 100644 --- a/composer.json +++ b/composer.json @@ -30,14 +30,29 @@ "psr-4": { "Omnipay\\BPOINT\\" : "tests/" } }, "require": { - "omnipay/common": "~2.0" + "php": "^7.2|^8.0", + "omnipay/common": "^3.1" }, "require-dev": { - "omnipay/tests": "~2.0" + "omnipay/tests": "^4.2", + "squizlabs/php_codesniffer": "^3.5", + "symfony/psr-http-message-bridge": "~1.1.1|^2.0", + "guzzlehttp/psr7": "^2.0" + }, + "scripts": { + "test": "phpunit", + "check-style": "phpcs -p --standard=PSR2 src/", + "fix-style": "phpcbf -p --standard=PSR2 src/" }, "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.2.x-dev" + } + }, + "prefer-stable": true, + "config": { + "allow-plugins": { + "php-http/discovery": true } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 913e932..21263b8 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,28 +1,30 @@ - - - - - - - ./tests/ - - - - - - - - ./src - - + stopOnFailure="false"> + + + + + + + + + + src/ + + + + + tests + + + + + diff --git a/phpunit9.xml.dist b/phpunit9.xml.dist new file mode 100644 index 0000000..5ab568e --- /dev/null +++ b/phpunit9.xml.dist @@ -0,0 +1,32 @@ + + + + + src/ + + + + + + + + + + tests + + + + + + diff --git a/src/Message/CompletePurchaseRequest.php b/src/Message/CompletePurchaseRequest.php index ffb5622..325aa9a 100644 --- a/src/Message/CompletePurchaseRequest.php +++ b/src/Message/CompletePurchaseRequest.php @@ -15,7 +15,7 @@ class CompletePurchaseRequest extends PurchaseRequest public function getData() { - return $this->httpRequest->request->all(); + return $this->httpRequest->request->all() + $this->httpRequest->query->all(); } /** @@ -26,14 +26,13 @@ public function sendData($data) // if we have a valid API response and a result key, let's get more detail about the transaction if (isset($data['ResponseCode']) && $data['ResponseCode'] == 0 && isset($data['ResultKey'])) { // submit request with the returned result key - $httpRequest = $this->httpClient->createRequest( + $httpResponse = $this->httpClient->request( 'GET', $this->getEndpoint()."/".$data['ResultKey'], - null + ['Authorization' => $this->getAuthHeader()] ); - $httpResponse = $httpRequest->setHeader('Authorization', $this->getAuthHeader())->send(); // get response data - $responseData = $httpResponse->json(); + $responseData = json_decode($httpResponse->getBody()->getContents(), true); $data = array_merge($data, $responseData['TxnResp']); } return $this->response = new CompletePurchaseResponse($this, $data); diff --git a/src/Message/CompletePurchaseResponse.php b/src/Message/CompletePurchaseResponse.php index 74d6e18..47e339b 100644 --- a/src/Message/CompletePurchaseResponse.php +++ b/src/Message/CompletePurchaseResponse.php @@ -27,7 +27,7 @@ public function isSuccessful() */ public function getTransactionReference() { - return isset($this->data['AuthoriseId']) ? $this->data['AuthoriseId'] : null; + return $this->data['AuthoriseId'] ?? null; } /** @@ -37,7 +37,7 @@ public function getTransactionReference() */ public function getMessage() { - return isset($this->data['ResponseText']) ? $this->data['ResponseText'] : null; + return $this->data['ResponseText'] ?? null; } /** @@ -47,6 +47,16 @@ public function getMessage() */ public function getCardType() { - return isset($this->data['CardType']) ? $this->data['CardType'] : null; + return $this->data['CardType'] ?? null; + } + + /** + * Get the card reference (payment token) if available + * + * @return null|string + */ + public function getCardReference() + { + return $this->data['DVToken'] ?? null; } } diff --git a/src/Message/PurchaseRequest.php b/src/Message/PurchaseRequest.php index 30a4828..0ca8948 100644 --- a/src/Message/PurchaseRequest.php +++ b/src/Message/PurchaseRequest.php @@ -45,6 +45,16 @@ public function setMerchantNumber($value) return $this->setParameter('merchantNumber', $value); } + public function getCreateToken() + { + return $this->getParameter('createToken'); + } + + public function setCreateToken($value) + { + return $this->setParameter('createToken', $value); + } + public function getMerchantShortName() { return $this->getParameter('merchantShortName'); @@ -55,6 +65,21 @@ public function setMerchantShortName($value) return $this->setParameter('merchantShortName', $value); } + public function getBillerCode() + { + return $this->getParameter('billerCode'); + } + + /** + * Set biller code for the transaction (differentiate income streams); required for using stored card tokens + * + * @param string $value String, max 50 characters + */ + public function setBillerCode($value) + { + return $this->setParameter('billerCode', $value); + } + public function getCustomerReferenceNumber1() { return $this->getParameter('customerReferenceNumber1'); @@ -63,7 +88,7 @@ public function getCustomerReferenceNumber1() /** * Set customer configurable reference #1 * - * @param bool $value String, max 50 characters + * @param string $value String, max 50 characters */ public function setCustomerReferenceNumber1($value) { @@ -78,7 +103,7 @@ public function getCustomerReferenceNumber2() /** * Set customer configurable reference #2 * - * @param bool $value String, max 50 characters + * @param string $value String, max 50 characters */ public function setCustomerReferenceNumber2($value) { @@ -93,26 +118,91 @@ public function getCustomerReferenceNumber3() /** * Set customer configurable reference #3 * - * @param bool $value String, max 50 characters + * @param string $value String, max 50 characters */ public function setCustomerReferenceNumber3($value) { return $this->setParameter('customerReferenceNumber3', $value); } + public function getHideBillerCode() + { + return $this->getParameter('hideBillerCode'); + } + + /** + * Whether to hide the biller code from the end-user on the hosted checkout page + * + * @param bool $value + */ + public function setHideBillerCode($value) + { + return $this->setParameter('hideBillerCode', $value); + } + + public function getHideCustomerReferenceNumber1() + { + return $this->getParameter('hideCustomerReferenceNumber1'); + } + + /** + * Whether to hide the customer configurable reference #1 from the end-user on the hosted checkout page + * + * @param bool $value + */ + public function setHideCustomerReferenceNumber1($value) + { + return $this->setParameter('hideCustomerReferenceNumber1', $value); + } + + public function getHideCustomerReferenceNumber2() + { + return $this->getParameter('hideCustomerReferenceNumber2'); + } + + /** + * Whether to hide the customer configurable reference #2 from the end-user on the hosted checkout page + * + * @param bool $value + */ + public function setHideCustomerReferenceNumber2($value) + { + return $this->setParameter('hideCustomerReferenceNumber2', $value); + } + + public function getHideCustomerReferenceNumber3() + { + return $this->getParameter('hideCustomerReferenceNumber3'); + } + + /** + * Whether to hide the customer configurable reference #3 from the end-user on the hosted checkout page + * + * @param bool $value + */ + public function setHideCustomerReferenceNumber3($value) + { + return $this->setParameter('hideCustomerReferenceNumber3', $value); + } + + /** + * @deprecated Alias. Use standard `getCreateToken()` instead. + */ public function getGenerateToken() { - return $this->getParameter('generateToken'); + return $this->getCreateToken(); } /** * Indicate whether or not to generate a token for the card used in the transaction * + * @deprecated Alias. Use standard `setCreateToken()` instead. + * * @param bool $value Generate a token or not */ public function setGenerateToken($value) { - return $this->setParameter('generateToken', $value); + return $this->setCreateToken($value); } public function getCustomerNumber() @@ -123,7 +213,7 @@ public function getCustomerNumber() /** * Set the unique customer ID in the merchant system * - * @param bool $value Customer number to set + * @param string $value Customer number to set */ public function setCustomerNumber($value) { @@ -136,16 +226,23 @@ public function getData() $amount = $this->getAmountInteger(); $data = array( + 'HppParameters' => array( + 'HideBillerCode' => (bool) $this->getHideBillerCode(), + 'HideCrn1' => (bool) $this->getHideCustomerReferenceNumber1(), + 'HideCrn2' => (bool) $this->getHideCustomerReferenceNumber2(), + 'HideCrn3' => (bool) $this->getHideCustomerReferenceNumber3(), + ), 'ProcessTxnData' => array( 'Action' => $amount > 0 ? 'payment' : 'verify_only', 'TestMode' => $this->getTestMode(), 'Amount' => $this->getAmountInteger(), + 'BillerCode' => $this->getBillerCode(), 'Crn1' => $this->getCustomerReferenceNumber1(), 'Crn2' => $this->getCustomerReferenceNumber2(), 'Crn3' => $this->getCustomerReferenceNumber3(), 'Currency' => $this->getCurrency(), // 1 - no; 3 - always (don't leave it up to system or customer) - 'TokenisationMode' => $this->getGenerateToken() ? 3 : 1, + 'TokenisationMode' => $this->getCreateToken() ? 3 : 1, 'MerchantReference' => $this->getTransactionId(), 'SubType' => 'single', 'Type' => 'internet', @@ -153,6 +250,10 @@ public function getData() 'RedirectionUrl' => $this->getReturnUrl(), 'WebHookUrl' => $this->getNotifyUrl(), ); + if ($this->getCancelUrl()) { + $data['HppParameters']['ReturnBarLabel'] = 'Cancel'; + $data['HppParameters']['ReturnBarUrl'] = $this->getCancelUrl(); + } // add item details if available $items = $this->getItems(); if ($items) { @@ -202,17 +303,32 @@ public function getData() } } + // add stored card token if available + if ($this->getToken() || $this->getCardReference()) { + $data['ProcessTxnData']['DVTokenData'] = array( + 'DVToken' => $this->getToken() ?? $this->getCardReference(), + 'UpdateDVTokenExpiryDate' => false, + ); + } + return $data; } public function sendData($data) { // submit data as request to Authkey endpoint - $httpRequest = $this->httpClient->createRequest('POST', $this->getEndpoint(), null); - $httpRequest->setBody(json_encode($data), 'application/json'); - $httpResponse = $httpRequest->setHeader('Authorization', $this->getAuthHeader())->send(); + $httpResponse = $this->httpClient->request( + 'POST', + $this->getEndpoint(), + [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Authorization' => $this->getAuthHeader(), + ], + json_encode($data) + ); // get response data - $responseData = $httpResponse->json(); + $responseData = json_decode($httpResponse->getBody()->getContents(), true); return $this->response = new PurchaseResponse($this, $responseData); } diff --git a/src/Message/PurchaseResponse.php b/src/Message/PurchaseResponse.php index b9bd8b2..484f3fa 100644 --- a/src/Message/PurchaseResponse.php +++ b/src/Message/PurchaseResponse.php @@ -3,11 +3,12 @@ namespace Omnipay\BPOINT\Message; use Omnipay\Common\Message\AbstractResponse; +use Omnipay\Common\Message\RedirectResponseInterface; /** * BPOINT Purchase Response */ -class PurchaseResponse extends AbstractResponse +class PurchaseResponse extends AbstractResponse implements RedirectResponseInterface { public function isSuccessful() { diff --git a/src/Message/WebhookNotification.php b/src/Message/WebhookNotification.php new file mode 100644 index 0000000..4b47377 --- /dev/null +++ b/src/Message/WebhookNotification.php @@ -0,0 +1,150 @@ +data = json_decode($httpRequest->getContent(), true); + } + + /** + * ResponseInterface implemented so that we can return self here for any legacy support that uses send() + */ + public function sendData($data) + { + return $this; + } + + /** + * Get the authorisation code if available. + * + * @return null|string + */ + public function getTransactionReference() + { + return isset($this->data['AuthoriseId']) ? $this->data['AuthoriseId'] : null; + } + + /** + * Was the transaction successful? + * + * @return string Transaction status, one of {@link NotificationInterface::STATUS_COMPLETED}, + * {@link NotificationInterface::STATUS_PENDING}, or {@link NotificationInterface::STATUS_FAILED}. + */ + public function getTransactionStatus() + { + if (!isset($this->data['ResponseCode'])) { + return NotificationInterface::STATUS_FAILED; + } + if ($this->data['ResponseCode'] == '0') { + return NotificationInterface::STATUS_COMPLETED; + } + if ($this->data['ResponseCode'] == 'P') { + return NotificationInterface::STATUS_PENDING; + } + + // last resort, assume failure + return NotificationInterface::STATUS_FAILED; + } + + /** + * Get the merchant response message if available. + * + * @return null|string + */ + public function getMessage() + { + return isset($this->data['ResponseText']) ? $this->data['ResponseText'] : null; + } + + public function getData() + { + return $this->data; + } + + /** + * Get the original request which generated this response + * + * @return RequestInterface + */ + public function getRequest() + { + return $this; + } + + /** + * Is the response successful? + * + * @return boolean + */ + public function isSuccessful() + { + return $this->getTransactionStatus() == NotificationInterface::STATUS_COMPLETED; + } + + /** + * Does the response require a redirect? + * + * @return boolean + */ + public function isRedirect() + { + return false; + } + + /** + * Is the transaction cancelled by the user? + * + * @return boolean + */ + public function isCancelled() + { + return isset($this->data['ResponseCode']) && $this->data['ResponseCode'] == 'C'; + } + + /** + * Response code + * + * @return null|string A response code from the payment gateway + */ + public function getCode() + { + return isset($this->data['ResponseCode']) ? $this->data['ResponseCode'] : null; + } + + /** + * Get the card type if available. + * + * @return null|string + */ + public function getCardType() + { + return isset($this->data['CardType']) ? $this->data['CardType'] : null; + } +} diff --git a/src/RedirectGateway.php b/src/RedirectGateway.php index 2fb2f70..db3aacb 100644 --- a/src/RedirectGateway.php +++ b/src/RedirectGateway.php @@ -5,6 +5,7 @@ use Omnipay\Common\AbstractGateway; use Omnipay\BPOINT\Message\CompletePurchaseRequest; use Omnipay\BPOINT\Message\PurchaseRequest; +use Omnipay\BPOINT\Message\WebhookNotification; /** * BPOINT Redirect Gateway @@ -78,4 +79,9 @@ public function completePurchase(array $parameters = array()) { return $this->createRequest('\Omnipay\BPOINT\Message\CompletePurchaseRequest', $parameters); } + + public function acceptNotification() + { + return $this->createRequest('\Omnipay\BPOINT\Message\WebhookNotification', array()); + } } diff --git a/tests/Message/CompletePurchaseResponseTest.php b/tests/Message/CompletePurchaseResponseTest.php index 4f07709..30a0acd 100644 --- a/tests/Message/CompletePurchaseResponseTest.php +++ b/tests/Message/CompletePurchaseResponseTest.php @@ -12,7 +12,7 @@ class CompletePurchaseResponseTest extends TestCase /** @var mixed[] Parsed TxnResp data from a transaction result */ private $responseData; - public function setUp() + public function setUp(): void { // demo data from the BPOINT API documentation $this->responseData = array( @@ -62,7 +62,7 @@ public function setUp() 'DVToken' => null, 'Type' => 'internet', 'FraudScreeningResponse' => array( - 'ReDResponse' => array( + 'ReDResponse' => array( 'FRAUD_REC_ID' => '123412341234SAX20150101100000000', 'FRAUD_RSP_CD' => '0100', 'FRAUD_STAT_CD' => 'ACCEPT', @@ -104,6 +104,7 @@ public function testCompletePurchaseSuccess() $this->assertSame('Approved', $this->response->getMessage()); $this->assertSame('372626', $this->response->getTransactionReference()); $this->assertSame('MC', $this->response->getCardType()); + $this->assertNull($this->response->getCardReference()); // confirm the request format was valid $requestData = $this->response->getRequest()->getData(); @@ -138,6 +139,7 @@ public function testCompletePurchaseFailure() $this->assertSame('Invalid card number', $this->response->getMessage()); $this->assertNull($this->response->getTransactionReference()); $this->assertNull($this->response->getCardType()); + $this->assertNull($this->response->getCardReference()); // confirm the request format was valid $requestData = $this->response->getRequest()->getData(); @@ -165,9 +167,22 @@ public function testCompletePurchaseError() $this->assertSame('Invalid credentials', $this->response->getMessage()); $this->assertNull($this->response->getTransactionReference()); $this->assertNull($this->response->getCardType()); + $this->assertNull($this->response->getCardReference()); $data = $this->response->getData(); $this->assertSame(1, $data['ResponseCode']); } + + public function testCompletePurchaseReturnsCardReference() + { + // adjust to have a token + $this->responseData['DVToken'] = '1234567890123456'; + + $this->response = new CompletePurchaseResponse($this->getMockRequest(), $this->responseData); + + $this->assertTrue($this->response->isSuccessful()); + $this->assertFalse($this->response->isRedirect()); + $this->assertSame('1234567890123456', $this->response->getCardReference()); + } } diff --git a/tests/Message/PurchaseRequestTest.php b/tests/Message/PurchaseRequestTest.php index d2a1afd..96d7436 100644 --- a/tests/Message/PurchaseRequestTest.php +++ b/tests/Message/PurchaseRequestTest.php @@ -12,7 +12,7 @@ class PurchaseRequestTest extends TestCase /** @var mixed[] Data to initialize the request with */ private $options; - public function setUp() + public function setUp(): void { $this->request = new PurchaseRequest($this->getHttpClient(), $this->getHttpRequest()); $this->options = array( @@ -22,10 +22,11 @@ public function setUp() 'password' => 'DemoPassword!', 'merchantNumber' => '5353109000000000', 'merchantShortName' => 'DEMO123', + 'billerCode' => '1234567', 'customerReferenceNumber1' => 'cr1', 'customerReferenceNumber2' => 'cr2', 'customerReferenceNumber3' => 'cr3', - 'generateToken' => false, + 'createToken' => false, 'customerNumber' => 'cust456', 'notifyUrl' => 'https://www.example.com/notify', 'returnUrl' => 'https://www.example.com/return', @@ -39,9 +40,16 @@ public function testGetData() { $data = $this->request->getData(); + $this->assertArrayHasKey('HppParameters', $data); + $this->assertFalse($data['HppParameters']['HideBillerCode']); + $this->assertFalse($data['HppParameters']['HideCrn1']); + $this->assertFalse($data['HppParameters']['HideCrn2']); + $this->assertFalse($data['HppParameters']['HideCrn3']); + $this->assertSame('payment', $data['ProcessTxnData']['Action']); $this->assertTrue($data['ProcessTxnData']['TestMode']); $this->assertSame(145, $data['ProcessTxnData']['Amount']); + $this->assertSame('1234567', $data['ProcessTxnData']['BillerCode']); $this->assertSame('cr1', $data['ProcessTxnData']['Crn1']); $this->assertSame('cr2', $data['ProcessTxnData']['Crn2']); $this->assertSame('cr3', $data['ProcessTxnData']['Crn3']); @@ -54,13 +62,90 @@ public function testGetData() $this->assertSame('https://www.example.com/notify', $data['WebHookUrl']); } + public function testHideFlags() + { + $this->options = array_merge($this->options, array( + 'hideBillerCode' => true, + 'hideCustomerReferenceNumber1' => true, + 'hideCustomerReferenceNumber2' => true, + 'hideCustomerReferenceNumber3' => true, + )); + $this->request->initialize($this->options); + + $data = $this->request->getData(); + + $this->assertArrayHasKey('HppParameters', $data); + $this->assertTrue($data['HppParameters']['HideBillerCode']); + $this->assertTrue($data['HppParameters']['HideCrn1']); + $this->assertTrue($data['HppParameters']['HideCrn2']); + $this->assertTrue($data['HppParameters']['HideCrn3']); + } + + public function testCancelUrl() + { + $this->options = array_merge($this->options, array('cancelUrl' => 'https://www.example.com/cancel')); + $this->request->initialize($this->options); + + $data = $this->request->getData(); + + $this->assertArrayHasKey('HppParameters', $data); + $this->assertArrayHasKey('ReturnBarLabel', $data['HppParameters']); + $this->assertSame('Cancel', $data['HppParameters']['ReturnBarLabel']); + $this->assertArrayHasKey('ReturnBarUrl', $data['HppParameters']); + $this->assertSame('https://www.example.com/cancel', $data['HppParameters']['ReturnBarUrl']); + } + + public function testCardReference() + { + $this->options = array_merge($this->options, array('cardReference' => '1234567890123456')); + $this->request->initialize($this->options); + + $data = $this->request->getData(); + + $this->assertArrayHasKey('ProcessTxnData', $data); + $this->assertArrayHasKey('DVTokenData', $data['ProcessTxnData']); + $this->assertArrayHasKey('DVToken', $data['ProcessTxnData']['DVTokenData']); + $this->assertSame('1234567890123456', $data['ProcessTxnData']['DVTokenData']['DVToken']); + $this->assertArrayHasKey('UpdateDVTokenExpiryDate', $data['ProcessTxnData']['DVTokenData']); + $this->assertFalse($data['ProcessTxnData']['DVTokenData']['UpdateDVTokenExpiryDate']); + } + + public function testToken() + { + $this->options = array_merge($this->options, array('token' => '1234567890123456')); + $this->request->initialize($this->options); + + $data = $this->request->getData(); + + $this->assertArrayHasKey('ProcessTxnData', $data); + $this->assertArrayHasKey('DVTokenData', $data['ProcessTxnData']); + $this->assertArrayHasKey('DVToken', $data['ProcessTxnData']['DVTokenData']); + $this->assertSame('1234567890123456', $data['ProcessTxnData']['DVTokenData']['DVToken']); + $this->assertArrayHasKey('UpdateDVTokenExpiryDate', $data['ProcessTxnData']['DVTokenData']); + $this->assertFalse($data['ProcessTxnData']['DVTokenData']['UpdateDVTokenExpiryDate']); + } + public function testGetDataOnlyGetToken() { // override some data - $this->options = array_merge($this->options, array('generateToken' => true, 'amount' => '0.00')); + $this->options = array_merge($this->options, array('createToken' => true, 'amount' => '0.00')); $this->request->initialize($this->options); + $this->assertTrue($this->request->getCreateToken()); + $data = $this->request->getData(); + $this->assertSame('verify_only', $data['ProcessTxnData']['Action']); + $this->assertSame(0, $data['ProcessTxnData']['Amount']); + $this->assertSame(3, $data['ProcessTxnData']['TokenisationMode']); + } + public function testGetDataDeprecatedGetToken() + { + // override some data + $this->options = array_merge($this->options, array('generateToken' => true, 'amount' => '0.00')); + $this->request->initialize($this->options); + $this->assertTrue($this->request->getGenerateToken()); + + $data = $this->request->getData(); $this->assertSame('verify_only', $data['ProcessTxnData']['Action']); $this->assertSame(0, $data['ProcessTxnData']['Amount']); $this->assertSame(3, $data['ProcessTxnData']['TokenisationMode']); diff --git a/tests/Message/PurchaseResponseTest.php b/tests/Message/PurchaseResponseTest.php index 1c2bdf0..4092c63 100644 --- a/tests/Message/PurchaseResponseTest.php +++ b/tests/Message/PurchaseResponseTest.php @@ -12,7 +12,7 @@ class PurchaseResponseTest extends TestCase /** * Set up for the tests in this class */ - public function setUp() + public function setUp(): void { $this->response = new PurchaseResponse($this->getMockRequest(), array( 'APIResponse' => array( diff --git a/tests/Mock/AcceptNotificationError.txt b/tests/Mock/AcceptNotificationError.txt new file mode 100644 index 0000000..f953b1d --- /dev/null +++ b/tests/Mock/AcceptNotificationError.txt @@ -0,0 +1,93 @@ +POST http://yourmerchantwebsite.com/txnWebHook HTTP/1.1 +Content-Type: application/json; charset=utf-8 +Host: merchant.com +Content-Length: 827 +Expect: 100-continue +Proxy-Connection: Keep-Alive + +{ + "Action" : "payment", + "Amount" : 19900, + "AmountOriginal" : 19800, + "AmountSurcharge" : 100, + "AuthoriseId" : "", + "BankAccountDetails" : { + "AccountName" : "", + "AccountNumber" : "", + "BSBNumber" : "", + "TruncatedAccountNumber" : "" + }, + "BankResponseCode" : "", + "BillerCode" : "", + "CardDetails" : { + "CardHolderName" : "John Smith", + "Category" : "", + "ExpiryDate" : "0521", + "Issuer" : "", + "IssuerCountryCode" : "", + "Localisation" : "", + "MaskedCardNumber" : "512345...346", + "SubType" : "extra comma here causes a malformed JSON error", + }, + "CardType" : "", + "Crn1" : "Test reference 1", + "Crn2" : "", + "Crn3" : "", + "Currency" : "AUD", + "CVNResult" : { + "CVNResultCode" : "" + }, + "DVToken" : "", + "EmailAddress" : "john.smith@email.com", + "FraudScreeningResponse" : { + "ReDResponse" : { + "FRAUD_REC_ID" : "", + "FRAUD_RSP_CD" : "", + "FRAUD_STAT_CD" : "", + "ORD_ID" : "", + "REQ_ID" : "", + "STAT_CD" : "" + }, + "ResponseCode" : "", + "ResponseMessage" : "", + "TxnRejected" : false + }, + "IsCVNPresent" : false, + "IsTestTxn" : false, + "IsThreeDS" : false, + "MerchantNumber" : "", + "MerchantReference" : "Test merchant reference", + "OriginalTxnNumber" : "", + "ProcessedDateTime" : "", + "ReceiptNumber" : "", + "ResponseCode" : "", + "ResponseText" : "", + "RRN" : "", + "SettlementDate" : "", + "Source" : "", + "StoreCard" : false, + "SubType" : "single", + "ThreeDSResponse" : { + "Eci" : "", + "Enrolled" : "", + "Status" : "", + "VerifySecurityLevel" : "", + "VerifyStatus" : "", + "VerifyToken" : "", + "VerifyType" : "", + "XID" : "" + }, + "TxnNumber" : "", + "Type" : "internet", + "StatementDescriptor" : { + "AddressLine1" : "", + "AddressLine2" : "", + "City" : "", + "CompanyName" : "", + "CountryCode" : "", + "Postcode" : "", + "State" : "", + "MerchantName" : "", + "PhoneNumber" : "" + } +} diff --git a/tests/Mock/AcceptNotificationFailure.txt b/tests/Mock/AcceptNotificationFailure.txt new file mode 100644 index 0000000..39c6d92 --- /dev/null +++ b/tests/Mock/AcceptNotificationFailure.txt @@ -0,0 +1,71 @@ +POST http://yourmerchantwebsite.com/txnWebHook HTTP/1.1 +Content-Type: application/json; charset=utf-8 +Content-Length: 827 +Expect: 100-continue +Proxy-Connection: Keep-Alive + +{ + "Action" : "payment", + "Amount" : 19900, + "AmountOriginal" : 19800, + "AmountSurcharge" : 100, + "AuthoriseId" : "372627", + "BankAccountDetails" : null, + "BankResponseCode" : "00", + "BillerCode" : null, + "CardDetails" : { + "CardHolderName" : "John Smith", + "Category" : "STANDARD", + "ExpiryDate" : "0521", + "Issuer" : "BANCO DEL PICHINCHA, C.A.", + "IssuerCountryCode" : "ECU", + "Localisation" : "international", + "MaskedCardNumber" : "512345...346", + "SubType" : "credit" + }, + "CardType" : "MC", + "Crn1" : "test crn1", + "Crn2" : "test crn2", + "Crn3" : "test crn3", + "Currency" : "AUD", + "CVNResult" : { + "CVNResultCode" : "M" + }, + "DVToken" : null, + "EmailAddress" : null, + "FraudScreeningResponse" : { + "ReDResponse" : null, + "ResponseCode" : "", + "ResponseMessage" : "", + "TxnRejected" : false + }, + "IsCVNPresent" : true, + "IsTestTxn" : true, + "IsThreeDS" : false, + "MerchantNumber" : "5353109000000000", + "MerchantReference" : "test merchant ref", + "OriginalTxnNumber" : null, + "ProcessedDateTime" : "2014-12-12T12:15:19.6370000", + "ReceiptNumber" : "49316411177", + "ResponseCode" : "6", + "ResponseText" : "Transaction Declined", + "RRN" : "434612372626", + "SettlementDate" : "20141212", + "Source" : "internet", + "StoreCard" : false, + "SubType" : "single", + "ThreeDSResponse" : null, + "TxnNumber" : "1177", + "Type" : "internet", + "StatementDescriptor" : { + "AddressLine1" : "123 Drive Street", + "AddressLine2" : "", + "City" : "Melbourne", + "CompanyName" : "A Company Name", + "CountryCode" : "AUS", + "Postcode" : "3000", + "State" : "Victoria", + "MerchantName" : "A Merchant Name", + "PhoneNumber" : "0123456789" + } +} diff --git a/tests/Mock/AcceptNotificationPending.txt b/tests/Mock/AcceptNotificationPending.txt new file mode 100644 index 0000000..350ca37 --- /dev/null +++ b/tests/Mock/AcceptNotificationPending.txt @@ -0,0 +1,71 @@ +POST http://yourmerchantwebsite.com/txnWebHook HTTP/1.1 +Content-Type: application/json; charset=utf-8 +Content-Length: 827 +Expect: 100-continue +Proxy-Connection: Keep-Alive + +{ + "Action" : "payment", + "Amount" : 19900, + "AmountOriginal" : 19800, + "AmountSurcharge" : 100, + "AuthoriseId" : "372628", + "BankAccountDetails" : null, + "BankResponseCode" : "00", + "BillerCode" : null, + "CardDetails" : { + "CardHolderName" : "John Smith", + "Category" : "STANDARD", + "ExpiryDate" : "0521", + "Issuer" : "BANCO DEL PICHINCHA, C.A.", + "IssuerCountryCode" : "ECU", + "Localisation" : "international", + "MaskedCardNumber" : "512345...346", + "SubType" : "credit" + }, + "CardType" : "MC", + "Crn1" : "test crn1", + "Crn2" : "test crn2", + "Crn3" : "test crn3", + "Currency" : "AUD", + "CVNResult" : { + "CVNResultCode" : "M" + }, + "DVToken" : null, + "EmailAddress" : null, + "FraudScreeningResponse" : { + "ReDResponse" : null, + "ResponseCode" : "", + "ResponseMessage" : "", + "TxnRejected" : false + }, + "IsCVNPresent" : true, + "IsTestTxn" : true, + "IsThreeDS" : false, + "MerchantNumber" : "5353109000000000", + "MerchantReference" : "test merchant ref", + "OriginalTxnNumber" : null, + "ProcessedDateTime" : "2014-12-12T12:15:19.6370000", + "ReceiptNumber" : "49316411177", + "ResponseCode" : "P", + "ResponseText" : "Transaction is Pending", + "RRN" : "434612372626", + "SettlementDate" : "20141212", + "Source" : "internet", + "StoreCard" : false, + "SubType" : "single", + "ThreeDSResponse" : null, + "TxnNumber" : "1177", + "Type" : "internet", + "StatementDescriptor" : { + "AddressLine1" : "123 Drive Street", + "AddressLine2" : "", + "City" : "Melbourne", + "CompanyName" : "A Company Name", + "CountryCode" : "AUS", + "Postcode" : "3000", + "State" : "Victoria", + "MerchantName" : "A Merchant Name", + "PhoneNumber" : "0123456789" + } +} diff --git a/tests/Mock/AcceptNotificationSuccess.txt b/tests/Mock/AcceptNotificationSuccess.txt new file mode 100644 index 0000000..5152c40 --- /dev/null +++ b/tests/Mock/AcceptNotificationSuccess.txt @@ -0,0 +1,71 @@ +POST http://yourmerchantwebsite.com/txnWebHook HTTP/1.1 +Content-Type: application/json; charset=utf-8 +Content-Length: 827 +Expect: 100-continue +Proxy-Connection: Keep-Alive + +{ + "Action" : "payment", + "Amount" : 19900, + "AmountOriginal" : 19800, + "AmountSurcharge" : 100, + "AuthoriseId" : "372626", + "BankAccountDetails" : null, + "BankResponseCode" : "00", + "BillerCode" : null, + "CardDetails" : { + "CardHolderName" : "John Smith", + "Category" : "STANDARD", + "ExpiryDate" : "0521", + "Issuer" : "BANCO DEL PICHINCHA, C.A.", + "IssuerCountryCode" : "ECU", + "Localisation" : "international", + "MaskedCardNumber" : "512345...346", + "SubType" : "credit" + }, + "CardType" : "MC", + "Crn1" : "test crn1", + "Crn2" : "test crn2", + "Crn3" : "test crn3", + "Currency" : "AUD", + "CVNResult" : { + "CVNResultCode" : "M" + }, + "DVToken" : null, + "EmailAddress" : null, + "FraudScreeningResponse" : { + "ReDResponse" : null, + "ResponseCode" : "", + "ResponseMessage" : "", + "TxnRejected" : false + }, + "IsCVNPresent" : true, + "IsTestTxn" : true, + "IsThreeDS" : false, + "MerchantNumber" : "5353109000000000", + "MerchantReference" : "test merchant ref", + "OriginalTxnNumber" : null, + "ProcessedDateTime" : "2014-12-12T12:15:19.6370000", + "ReceiptNumber" : "49316411177", + "ResponseCode" : "0", + "ResponseText" : "Approved", + "RRN" : "434612372626", + "SettlementDate" : "20141212", + "Source" : "internet", + "StoreCard" : false, + "SubType" : "single", + "ThreeDSResponse" : null, + "TxnNumber" : "1177", + "Type" : "internet", + "StatementDescriptor" : { + "AddressLine1" : "123 Drive Street", + "AddressLine2" : "", + "City" : "Melbourne", + "CompanyName" : "A Company Name", + "CountryCode" : "AUS", + "Postcode" : "3000", + "State" : "Victoria", + "MerchantName" : "A Merchant Name", + "PhoneNumber" : "0123456789" + } +} diff --git a/tests/RedirectGatewayTest.php b/tests/RedirectGatewayTest.php index a72260c..b342b23 100644 --- a/tests/RedirectGatewayTest.php +++ b/tests/RedirectGatewayTest.php @@ -2,14 +2,21 @@ namespace Omnipay\BPOINT; +use Exception; +use GuzzleHttp\Psr7\Message; +use GuzzleHttp\Psr7\ServerRequest; +use Omnipay\Common\Message\NotificationInterface; +use ReflectionObject; use Omnipay\Tests\GatewayTestCase; +use Symfony\Component\HttpFoundation\Request as HttpRequest; +use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory; class RedirectGatewayTest extends GatewayTestCase { /** @var array */ protected $options; - public function setUp() + public function setUp(): void { parent::setUp(); @@ -22,10 +29,11 @@ public function setUp() 'password' => 'DemoPassword!', 'merchantNumber' => '5353109000000000', 'merchantShortName' => 'DEMO123', + 'billerCode' => '1234567', 'customerReferenceNumber1' => 'cr1', 'customerReferenceNumber2' => 'cr2', 'customerReferenceNumber3' => 'cr3', - 'generateToken' => true, + 'createToken' => true, 'customerNumber' => 'cust456', 'notifyUrl' => 'https://www.example.com/notify', 'returnUrl' => 'https://www.example.com/return', @@ -115,4 +123,107 @@ public function testCompletePurchaseError() $this->assertNull($response->getTransactionReference()); $this->assertNull($response->getCardType()); } + + public function testAcceptNotificationSuccess() + { + $httpRequest = $this->setMockHttpRequest('AcceptNotificationSuccess.txt'); + $gateway = new RedirectGateway($this->getHttpClient(), $httpRequest); + $notification = $gateway->acceptNotification(); + + // NotificationInterface methods + $this->assertSame('372626', $notification->getTransactionReference()); + $this->assertSame(NotificationInterface::STATUS_COMPLETED, $notification->getTransactionStatus()); + $this->assertSame('Approved', $notification->getMessage()); + + // ResponseInterface methods + $response = $notification->send(); + $this->assertSame($notification, $response); + $this->assertSame($notification, $response->getRequest()); + $this->assertTrue($response->isSuccessful()); + $this->assertFalse($response->isRedirect()); + $this->assertFalse($response->isCancelled()); + $this->assertSame('0', $response->getCode()); + + $this->assertSame('MC', $notification->getCardType()); + } + + public function testAcceptNotificationFailure() + { + $httpRequest = $this->setMockHttpRequest('AcceptNotificationFailure.txt'); + $gateway = new RedirectGateway($this->getHttpClient(), $httpRequest); + $notification = $gateway->acceptNotification(); + + // NotificationInterface methods + $this->assertSame('372627', $notification->getTransactionReference()); + $this->assertSame(NotificationInterface::STATUS_FAILED, $notification->getTransactionStatus()); + $this->assertSame('Transaction Declined', $notification->getMessage()); + + // ResponseInterface methods + $response = $notification->send(); + $this->assertFalse($response->isSuccessful()); + } + + public function testAcceptNotificationPending() + { + $httpRequest = $this->setMockHttpRequest('AcceptNotificationPending.txt'); + $gateway = new RedirectGateway($this->getHttpClient(), $httpRequest); + $notification = $gateway->acceptNotification(); + + // NotificationInterface methods + $this->assertSame('372628', $notification->getTransactionReference()); + $this->assertSame(NotificationInterface::STATUS_PENDING, $notification->getTransactionStatus()); + $this->assertSame('Transaction is Pending', $notification->getMessage()); + } + + public function testAcceptNotificationError() + { + $httpRequest = $this->setMockHttpRequest('AcceptNotificationError.txt'); + $gateway = new RedirectGateway($this->getHttpClient(), $httpRequest); + $notification = $gateway->acceptNotification(); + + // NotificationInterface methods + $this->assertNull($notification->getTransactionReference()); + $this->assertSame(NotificationInterface::STATUS_FAILED, $notification->getTransactionStatus()); + $this->assertNull($notification->getMessage()); + + // bonus malformed JSON test + $this->assertTrue(json_last_error() == JSON_ERROR_SYNTAX); + } + + /** + * Parses a saved raw request file into a new HTTP request object + * + * Initial file parsing adapted from TestCase::getMockHttpResponse() + * + * @param string $path The request file + * + * @return HttpRequest The new request + */ + protected function setMockHttpRequest($path) + { + $ref = new ReflectionObject($this); + $dir = dirname($ref->getFileName()); + // if mock file doesn't exist, check parent directory + if (file_exists($dir.'/Mock/'.$path)) { + $raw = file_get_contents($dir.'/Mock/'.$path); + } elseif (file_exists($dir.'/../Mock/'.$path)) { + $raw = file_get_contents($dir.'/../Mock/'.$path); + } else { + throw new Exception("Cannot open '{$path}'"); + } + + $guzzleRequest = Message::parseRequest($raw); + // PSR-bridge requires a ServerRequestInterface + $guzzleServerRequest = new ServerRequest( + $guzzleRequest->getMethod(), + $guzzleRequest->getUri(), + $guzzleRequest->getHeaders(), + $guzzleRequest->getBody(), + $guzzleRequest->getProtocolVersion(), + $_SERVER + ); + + $httpFoundationFactory = new HttpFoundationFactory(); + return $httpFoundationFactory->createRequest($guzzleServerRequest); + } }