├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── ext.store_rabobank.php ├── phpunit.xml.dist ├── src ├── Exception │ ├── DescriptionToLongException.php │ ├── InvalidArgumentException.php │ ├── InvalidLanguageCodeException.php │ └── InvalidSignatureException.php ├── Gateway.php ├── Message │ ├── Request │ │ ├── AbstractRabobankRequest.php │ │ ├── CompletePurchaseRequest.php │ │ ├── PurchaseRequest.php │ │ └── StatusRequest.php │ └── Response │ │ ├── AbstractRabobankResponse.php │ │ ├── CompletePurchaseResponse.php │ │ ├── PurchaseResponse.php │ │ └── StatusResponse.php └── Order.php └── tests ├── GatewayTest.php ├── Message ├── PurchaseRequestTest.php └── StatusRequestTest.php └── Mock ├── PurchaseInvalidSignature.txt ├── PurchaseSuccess.txt └── StatusSuccess.txt /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - "*" 10 | schedule: 11 | - cron: '0 0 * * *' 12 | 13 | jobs: 14 | php-tests: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 15 17 | env: 18 | COMPOSER_NO_INTERACTION: 1 19 | 20 | strategy: 21 | matrix: 22 | php: [8.0, 7.4, 7.3, 7.2] 23 | dependency-version: [prefer-lowest, prefer-stable] 24 | 25 | name: P${{ matrix.php }} - ${{ matrix.dependency-version }} 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v2 30 | 31 | - name: Setup PHP 32 | uses: shivammathur/setup-php@v2 33 | with: 34 | php-version: ${{ matrix.php }} 35 | coverage: none 36 | tools: composer:v2 37 | 38 | - name: Install dependencies 39 | run: | 40 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-progress 41 | 42 | - name: Execute Unit Tests 43 | run: composer test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | composer.phar 4 | phpunit.xml 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | * Fork the project. 4 | * Make your feature addition or bug fix. 5 | * Add tests for it. This is important so I don't break it in a future version unintentionally. 6 | * Commit just the modifications, do not mess with the composer.json or CHANGELOG.md files. 7 | * 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) 8 | style and that all tests pass. 9 | * Send the pull request. 10 | * Check that the Travis CI build passed. If not, rinse and repeat. 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2013 Adrian Macneil 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Omnipay: Rabobank OmniKassa V2 2 | 3 | **Rabobank OmniKassa driver for the Omnipay PHP payment processing library** 4 | 5 | [![Unit Tests](https://github.com/thephpleague/omnipay-rabobank/actions/workflows/run-tests.yml/badge.svg)](https://github.com/thephpleague/omnipay-rabobank/actions/workflows/run-tests.yml) 6 | [![Latest Stable Version](https://poser.pugx.org/omnipay/rabobank/version.png)](https://packagist.org/packages/omnipay/rabobank) 7 | [![Total Downloads](https://poser.pugx.org/omnipay/rabobank/d/total.png)](https://packagist.org/packages/omnipay/rabobank) 8 | 9 | [Omnipay](https://github.com/thephpleague/omnipay) is a framework agnostic, multi-gateway payment 10 | processing library for PHP PHP. This package implements Rabobank OmniKassa V2 support for Omnipay. 11 | 12 | ## Installation 13 | 14 | Omnipay is installed via [Composer](http://getcomposer.org/). To install, simply require `league/omnipay` and `omnipay/rabobank` with Composer: 15 | 16 | ``` 17 | composer require league/omnipay omnipay/rabobank 18 | ``` 19 | 20 | 21 | ## Basic Usage 22 | 23 | The following gateways are provided by this package: 24 | 25 | * Rabobank 26 | 27 | For general usage instructions, please see the main [Omnipay](https://github.com/thephpleague/omnipay) 28 | repository. 29 | 30 | 31 | ## Updating to V2 32 | 33 | In order to upgrade to Omnikassa V2 you need the following codes: 34 | 35 | * `RefreshToken` 36 | * `SigningKey` 37 | 38 | You can look those up in your Omnikassa V2 Dashboard. See the [documentation on rabobank.nl](https://www.rabobank.nl/images/handleiding-signing-key-en-refresh-token-ophalen_29951774.pdf). 39 | 40 | 41 | 1. The `Merchant ID` and `Key Version` parameters are not needed anymore. The refresh token is the new merchant identification. 42 | 2. It's not possible anymore to provide more then one Payment Method per purchase by separating them with a comma. (eg. `IDEAL,CARDS`) 43 | 3. There is no option anymore for providing a notice URL per purchase. The notice URL needs to be configured in the Omnikassa V2 Dashboard. 44 | 4. The notify `POST` doesn't have any status information about the order anymore. You can fetch information about orders by making a `StatusRequest`. You'll need the ``notificationToken`` that's received in the notification `POST`. 45 | Make sure you fetch all open order statuses by checking the ``moreOrderResultsAvailable`` parameter in the response. If set to true, make another `StatusRequest` with the same `notificationToken`, until ``moreOrderResultsAvailable`` is `false`. 46 | 47 | ## Support 48 | 49 | If you are having general issues with Omnipay, we suggest posting on 50 | [Stack Overflow](http://stackoverflow.com/). Be sure to add the 51 | [omnipay tag](http://stackoverflow.com/questions/tagged/omnipay) so it can be easily found. 52 | 53 | If you want to keep up to date with release anouncements, discuss ideas for the project, 54 | or ask more detailed questions, there is also a [mailing list](https://groups.google.com/forum/#!forum/omnipay) which 55 | you can subscribe to. 56 | 57 | If you believe you have found a bug, please report it using the [GitHub issue tracker](https://github.com/thephpleague/omnipay-rabobank/issues), 58 | or better yet, fork the library and submit a pull request. 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omnipay/rabobank", 3 | "type": "library", 4 | "description": "Rabobank Omnikassa driver for the Omnipay payment processing library", 5 | "keywords": [ 6 | "gateway", 7 | "merchant", 8 | "omnipay", 9 | "pay", 10 | "payment", 11 | "ideal", 12 | "omnikassa", 13 | "rabobank" 14 | ], 15 | "homepage": "https://github.com/thephpleague/omnipay-rabobank", 16 | "license": "MIT", 17 | "authors": [ 18 | { 19 | "name": "Adrian Macneil", 20 | "email": "adrian@adrianmacneil.com" 21 | }, 22 | { 23 | "name": "Omnipay Contributors", 24 | "homepage": "https://github.com/thephpleague/omnipay-rabobank/contributors" 25 | } 26 | ], 27 | "autoload": { 28 | "psr-4": { "Omnipay\\Rabobank\\" : "src/" } 29 | }, 30 | "require": { 31 | "php": "^7.2|^8", 32 | "omnipay/common": "^3.1" 33 | }, 34 | "require-dev": { 35 | "omnipay/tests": "^4.1.1", 36 | "squizlabs/php_codesniffer": "^3.6.0" 37 | }, 38 | "scripts": { 39 | "test": "phpunit", 40 | "check-style": "phpcs -p --standard=PSR2 src/", 41 | "fix-style": "phpcbf -p --standard=PSR2 src/" 42 | }, 43 | "extra": { 44 | "branch-alias": { 45 | "dev-master": "3.1.x-dev" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ext.store_rabobank.php: -------------------------------------------------------------------------------- 1 | __CLASS__, 15 | 'method' => 'store_payment_gateways', 16 | 'hook' => 'store_payment_gateways', 17 | 'priority' => 10, 18 | 'settings' => '', 19 | 'version' => $this->version, 20 | 'enabled' => 'y' 21 | ); 22 | 23 | ee()->db->insert('extensions', $data); 24 | } 25 | 26 | /** 27 | * This hook is called when Store is searching for available payment gateways 28 | * We will use it to tell Store about our custom gateway 29 | */ 30 | public function store_payment_gateways($gateways) 31 | { 32 | // tell Store about our new payment gateway 33 | // (this must match the name of your gateway in the Omnipay directory) 34 | $gateways[] = 'Rabobank'; 35 | 36 | // tell PHP where to find the gateway classes 37 | // Store will automatically include your files when they are needed 38 | $composer = require(PATH_THIRD.'store/autoload.php'); 39 | $composer->add('Omnipay\Rabobank', __DIR__.'/src'); 40 | 41 | return $gateways; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | ./tests/ 11 | 12 | 13 | 14 | 15 | ./src 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Exception/DescriptionToLongException.php: -------------------------------------------------------------------------------- 1 | '', 31 | 'refreshToken' => '', 32 | ); 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getSigningKey() 39 | { 40 | return $this->getParameter('signingKey'); 41 | } 42 | 43 | /** 44 | * @param string $value 45 | * @return Gateway 46 | */ 47 | public function setSigningKey($value) 48 | { 49 | return $this->setParameter('signingKey', $value); 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getRefreshToken() 56 | { 57 | return $this->getParameter('refreshToken'); 58 | } 59 | 60 | /** 61 | * @param string $value 62 | * @return $this 63 | */ 64 | public function setRefreshToken($value) 65 | { 66 | return $this->setParameter('refreshToken', $value); 67 | } 68 | 69 | /** 70 | * @param array $data 71 | * @return string 72 | */ 73 | public function generateSignature(array $data) 74 | { 75 | $data = $this->flattenSignatureData($data); 76 | $data = $this->massageSignatureData($data); 77 | 78 | return hash_hmac( 79 | static::SIGNING_HASH_ALGORITHM, 80 | implode(',', $data), 81 | base64_decode($this->getParameter('signingKey')) 82 | ); 83 | } 84 | 85 | protected function massageSignatureData(array $data) 86 | { 87 | return array_map(function ($value) { 88 | if (is_bool($value)) { 89 | return $value ? 'true' : 'false'; 90 | } 91 | 92 | return (string)$value; 93 | }, $data); 94 | } 95 | 96 | /** 97 | * Flattens signature data 98 | * 99 | * @param array $data 100 | * @return array 101 | */ 102 | protected function flattenSignatureData($data) 103 | { 104 | return iterator_to_array(new \RecursiveIteratorIterator(new \RecursiveArrayIterator($data)), false); 105 | } 106 | 107 | /** 108 | * @param array $parameters 109 | * @return PurchaseRequest 110 | */ 111 | public function purchase(array $parameters = []) 112 | { 113 | /** @var PurchaseRequest $request */ 114 | $request = $this->createRequest(PurchaseRequest::class, $parameters); 115 | 116 | return $request; 117 | } 118 | 119 | public function completePurchase(array $parameters = []) 120 | { 121 | /** @var CompletePurchaseRequest $request */ 122 | $request = $this->createRequest(CompletePurchaseRequest::class, $parameters); 123 | 124 | return $request; 125 | } 126 | 127 | /** 128 | * @param array $parameters 129 | * @return StatusRequest 130 | */ 131 | public function status(array $parameters = []) 132 | { 133 | /** @var StatusRequest $request */ 134 | $request = $this->createRequest(StatusRequest::class, $parameters); 135 | 136 | return $request; 137 | } 138 | 139 | protected function createRequest($class, array $parameters) 140 | { 141 | $obj = new $class($this->httpClient, $this->httpRequest, $this); 142 | 143 | return $obj->initialize(array_replace($this->getParameters(), $parameters)); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Message/Request/AbstractRabobankRequest.php: -------------------------------------------------------------------------------- 1 | gateway = $gateway; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getSigningKey() 56 | { 57 | return $this->getParameter('signingKey'); 58 | } 59 | 60 | /** 61 | * @param string $value 62 | * @return $this 63 | */ 64 | public function setSigningKey($value) 65 | { 66 | return $this->setParameter('signingKey', $value); 67 | } 68 | 69 | /** 70 | * @return string 71 | */ 72 | public function getRefreshToken() 73 | { 74 | return $this->getParameter('refreshToken'); 75 | } 76 | 77 | /** 78 | * @param string $value 79 | * @return $this 80 | */ 81 | public function setRefreshToken($value) 82 | { 83 | return $this->setParameter('refreshToken', $value); 84 | } 85 | 86 | /** 87 | * @return string 88 | */ 89 | public function getAccessToken() 90 | { 91 | if (!isset(self::$accessToken)) { 92 | $this->setAccessToken($this->fetchAccessToken()); 93 | } 94 | 95 | return self::$accessToken; 96 | } 97 | 98 | /** 99 | * @param string $value 100 | * @return $this 101 | */ 102 | public function setAccessToken($value) 103 | { 104 | self::$accessToken = $value; 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * @return string 111 | */ 112 | public function getBaseUrl() 113 | { 114 | if ($this->gateway->getTestMode()) { 115 | return $this->baseUrlTesting; 116 | } 117 | 118 | return $this->baseUrl; 119 | } 120 | 121 | /** 122 | * @return string 123 | */ 124 | public function getRequestContentType() 125 | { 126 | return $this->requestContentType; 127 | } 128 | 129 | /** 130 | * @param string $method 131 | * @param string $endpoint 132 | * @param array $data 133 | * @param array $headers 134 | * @return array 135 | */ 136 | protected function sendRequest($method, $endpoint, array $data = null, array $headers = []) 137 | { 138 | if (!isset($headers['Authorization'])) { 139 | $headers['Authorization'] = 'Bearer '.$this->getAccessToken(); 140 | } 141 | 142 | $headers['Content-Type'] = $this->getRequestContentType(); 143 | 144 | $response = $this->httpClient->request( 145 | $method, 146 | $this->getBaseUrl().$endpoint, 147 | $headers, 148 | ($data === null || $data === []) ? null : json_encode($data) 149 | ); 150 | 151 | return json_decode((string)$response->getBody(), true); 152 | } 153 | 154 | protected function fetchAccessToken() 155 | { 156 | $data = $this->sendRequest(self::GET, $this->refreshEndpoint, null, [ 157 | 'Authorization' => 'Bearer '.$this->gateway->getRefreshToken(), 158 | ]); 159 | 160 | if (!isset($data['token'])) { 161 | throw new InvalidResponseException($data['consumerMessage'], $data['errorCode']); 162 | } 163 | 164 | return $data['token']; 165 | } 166 | 167 | protected function createAmountObject() 168 | { 169 | return [ 170 | 'currency' => $this->getCurrency(), 171 | 'amount' => $this->getAmountInteger(), 172 | ]; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Message/Request/CompletePurchaseRequest.php: -------------------------------------------------------------------------------- 1 | httpRequest->query->all(); 15 | } 16 | 17 | public function sendData($data) 18 | { 19 | return $this->response = new CompletePurchaseResponse($this, $data); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Message/Request/PurchaseRequest.php: -------------------------------------------------------------------------------- 1 | getParameter('orderId'); 37 | } 38 | 39 | /** 40 | * @param string $value 41 | * @return $this 42 | */ 43 | public function setOrderId($value) 44 | { 45 | return $this->setParameter('orderId', $value); 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function getDescription() 52 | { 53 | return $this->getParameter('description'); 54 | } 55 | 56 | /** 57 | * @param string $value 58 | * @return $this 59 | * @throws DescriptionToLongException 60 | */ 61 | public function setDescription($value) 62 | { 63 | if (strlen($value) > 35) { 64 | throw new DescriptionToLongException('Description can only be 35 characters long'); 65 | } 66 | 67 | return $this->setParameter('description', $value); 68 | } 69 | 70 | /** 71 | * @return string 72 | */ 73 | public function getLanguageCode() 74 | { 75 | return $this->getParameter('languageCode'); 76 | } 77 | 78 | /** 79 | * Must be a valid ISO 639-1 language code 80 | * 81 | * @param string $value 82 | * @return $this 83 | * @throws InvalidLanguageCodeException 84 | */ 85 | public function setLanguageCode($value) 86 | { 87 | if (strlen($value) > 2) { 88 | throw new InvalidLanguageCodeException('Language code must be a valid ISO 639-1 language code'); 89 | } 90 | 91 | return $this->setParameter('languageCode', $value); 92 | } 93 | 94 | /** 95 | * @return string 96 | */ 97 | public function getEnforcePaymentMethod() 98 | { 99 | return $this->getParameter('enforcePaymentMethod'); 100 | } 101 | 102 | /** 103 | * Set if the payment method should be enforced. 104 | * 105 | * This field can be used to send or, 106 | * after a failed payment, the consumer can or can not 107 | * select another payment method to still pay the payment. 108 | * Valid values are: 109 | * static::PAYMENT_METHOD_ENFORCE_ONCE 110 | * static::PAYMENT_METHOD_ENFORCE_ALWAYS 111 | * In the case of FORCE_ONCE, the indicated paymentMethod is 112 | * only enforced on the first transaction. If this fails, 113 | * the consumer can still choose another payment method. 114 | * When FORCE_ALWAYS is chosen, the consumer can 115 | * not choose another payment method 116 | * 117 | * @param string $value 118 | * @return $this 119 | */ 120 | public function setEnforcePaymentMethod($value) 121 | { 122 | return $this->setParameter('enforcePaymentMethod', $value); 123 | } 124 | 125 | /** 126 | * @return null|int 127 | */ 128 | public function getPaymentBrandMetaDataIssuerId() 129 | { 130 | return $this->getParameter('paymentBrandMetaDataIssuerId'); 131 | } 132 | 133 | /** 134 | * @param null|int $value 135 | * @return $this 136 | */ 137 | public function setPaymentBrandMetaDataIssuerId($value) 138 | { 139 | return $this->setParameter('paymentBrandMetaDataIssuerId', $value); 140 | } 141 | 142 | /** 143 | * @return array 144 | * @throws InvalidRequestException 145 | */ 146 | public function getData() 147 | { 148 | $this->validate('orderId', 'amount', 'currency', 'returnUrl'); 149 | 150 | $data = []; 151 | 152 | $data['timestamp'] = date('c'); 153 | $data['merchantOrderId'] = $this->getOrderId(); 154 | $data['description'] = $this->getDescription(); 155 | $data['amount'] = $this->createAmountObject(); 156 | $data['language'] = $this->getLanguageCode(); 157 | $data['merchantReturnURL'] = $this->getReturnUrl(); 158 | $data['paymentBrand'] = $this->getPaymentMethod(); 159 | 160 | if ($data['paymentBrand']) { 161 | $data['paymentBrandForce'] = $this->getEnforcePaymentMethod(); 162 | $data['paymentBrandMetaData'] = [ 163 | 'issuerId' => $this->getPaymentBrandMetaDataIssuerId(), 164 | ]; 165 | } 166 | 167 | $card = $this->getCard(); 168 | if (isset($card)) { 169 | $data['shippingDetail'] = [ 170 | 'firstName' => (string)$card->getFirstName(), 171 | 'middleName' => '', 172 | 'lastName' => (string)$card->getLastName(), 173 | 'street' => (string)$card->getAddress1(), 174 | 'postalCode' => (string)$card->getPostcode(), 175 | 'city' => (string)$card->getCity(), 176 | 'countryCode' => (string)$card->getCountry(), 177 | ]; 178 | } 179 | 180 | $data['signature'] = $this->generateSignature($data); 181 | 182 | return $data; 183 | } 184 | 185 | /** 186 | * @param array $data 187 | * @return ResponseInterface|PurchaseResponse 188 | * @throws \Omnipay\Rabobank\Exception\InvalidSignatureException 189 | */ 190 | public function sendData($data) 191 | { 192 | $response = $this->sendRequest(self::POST, '/order/server/api/order', $data); 193 | 194 | return $this->response = new PurchaseResponse($this, $response); 195 | } 196 | 197 | protected function generateSignature(array $requestData) 198 | { 199 | $signatureData = [ 200 | $requestData['timestamp'], 201 | $requestData['merchantOrderId'], 202 | $requestData['amount']['currency'], 203 | $requestData['amount']['amount'], 204 | isset($requestData['language']) ? $requestData['language'] : '', 205 | isset($requestData['description']) ? $requestData['description'] : '', 206 | $requestData['merchantReturnURL'], 207 | ]; 208 | 209 | if (isset($requestData['shippingDetail'])) { 210 | $signatureData[] = $requestData['shippingDetail']; 211 | } 212 | 213 | if (isset($requestData['paymentBrand'])) { 214 | $signatureData[] = $requestData['paymentBrand']; 215 | } 216 | 217 | if (isset($requestData['paymentBrandForce'])) { 218 | $signatureData[] = $requestData['paymentBrandForce']; 219 | } 220 | 221 | return $this->gateway->generateSignature($signatureData); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/Message/Request/StatusRequest.php: -------------------------------------------------------------------------------- 1 | getParameter('notificationToken'); 21 | } 22 | 23 | /** 24 | * @param string $value 25 | * @return $this 26 | */ 27 | public function setNotificationToken($value) 28 | { 29 | return $this->setParameter('notificationToken', $value); 30 | } 31 | 32 | /** 33 | * @return array 34 | * @throws InvalidRequestException 35 | */ 36 | public function getData() 37 | { 38 | $this->validate('notificationToken'); 39 | 40 | return []; 41 | } 42 | 43 | /** 44 | * @param array $data 45 | * @return ResponseInterface|StatusResponse 46 | * @throws \Omnipay\Rabobank\Exception\InvalidSignatureException 47 | */ 48 | public function sendData($data) 49 | { 50 | $headers = []; 51 | $headers['Authorization'] = 'Bearer '.$this->getNotificationToken(); 52 | $response = $this->sendRequest( 53 | self::GET, 54 | 'order/server/api/events/results/merchant.order.status.changed', 55 | $data, 56 | $headers 57 | ); 58 | 59 | return $this->response = new StatusResponse($this, $response); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Message/Response/AbstractRabobankResponse.php: -------------------------------------------------------------------------------- 1 | validateResponse(); 28 | $this->validateSignature(); 29 | } 30 | 31 | /** 32 | * Is the response successful? 33 | * 34 | * @return boolean 35 | */ 36 | public function isSuccessful() 37 | { 38 | return isset($this->data['signature']); 39 | } 40 | 41 | /** 42 | * Check if we've received an error 43 | * 44 | * @throws InvalidResponseException 45 | */ 46 | protected function validateResponse() 47 | { 48 | if (isset($this->data['errorCode']) && isset($this->data['consumerMessage'])) { 49 | throw new InvalidResponseException($this->data['consumerMessage'], $this->data['errorCode']); 50 | } 51 | } 52 | 53 | /** 54 | * @throws InvalidSignatureException 55 | */ 56 | protected function validateSignature() 57 | { 58 | if (!isset($this->data['signature'])) { 59 | return; 60 | } 61 | 62 | $signatureData = $this->data; 63 | unset($signatureData['signature']); 64 | unset($signatureData['timestamp']); 65 | 66 | $signature = $this->request->gateway->generateSignature($this->flattenData($signatureData)); 67 | 68 | if (!hash_equals($signature, $this->data['signature'])) { 69 | throw new InvalidSignatureException('Signature returned from server is invalid'); 70 | } 71 | } 72 | 73 | protected function flattenData(array $data) 74 | { 75 | $flattened = []; 76 | foreach ($data as $value) { 77 | if (is_array($value)) { 78 | $flattened = array_merge($flattened, $this->flattenData($value)); 79 | continue; 80 | } 81 | $flattened[] = $value; 82 | } 83 | 84 | return $flattened; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Message/Response/CompletePurchaseResponse.php: -------------------------------------------------------------------------------- 1 | data['status']) && $this->data['status'] === 'COMPLETED'; 10 | } 11 | 12 | public function isCancelled() 13 | { 14 | return isset($this->data['status']) && $this->data['status'] === 'CANCELLED'; 15 | } 16 | 17 | public function isExpired() 18 | { 19 | return isset($this->data['status']) && $this->data['status'] === 'EXPIRED'; 20 | } 21 | 22 | public function getOrderId() 23 | { 24 | return isset($this->data['order_id']) ? $this->data['order_id'] : null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Message/Response/PurchaseResponse.php: -------------------------------------------------------------------------------- 1 | data['redirectUrl']; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Message/Response/StatusResponse.php: -------------------------------------------------------------------------------- 1 | data['moreOrderResultsAvailable']; 21 | } 22 | 23 | /** 24 | * @return Order[] 25 | */ 26 | public function getOrders() 27 | { 28 | $orders = []; 29 | foreach ((array)$this->data['orderResults'] as $orderResult) { 30 | $order = new Order(); 31 | 32 | foreach ($orderResult as $field => $value) { 33 | $order->{$field} = $value; 34 | } 35 | 36 | $orders[] = $order; 37 | } 38 | 39 | return $orders; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Order.php: -------------------------------------------------------------------------------- 1 | gateway = new Gateway(); 21 | $this->gateway->setSigningKey(base64_encode('secret')); 22 | } 23 | 24 | public function testPurchase() 25 | { 26 | /** @var PurchaseRequest $request */ 27 | $request = $this->gateway->purchase(array('amount' => '10.00', 'currency' => 'EUR')); 28 | 29 | $this->assertInstanceOf(PurchaseRequest::class, $request); 30 | $this->assertSame(1000, $request->getAmountInteger()); 31 | $this->assertSame('EUR', $request->getCurrency()); 32 | } 33 | 34 | public function testStatus() 35 | { 36 | /** @var StatusRequest $request */ 37 | $request = $this->gateway->status(array('notificationToken' => 'secret')); 38 | 39 | $this->assertInstanceOf(StatusRequest::class, $request); 40 | $this->assertSame('secret', $request->getNotificationToken()); 41 | } 42 | 43 | public function testGenerateSignature() 44 | { 45 | $data = [ 46 | date('c'), 47 | '6', 48 | 'EUR', 49 | 1000, 50 | 'EN', 51 | '', 52 | 'https://www.example.com/return', 53 | 'IDEAL', 54 | 'FORCE_ONCE' 55 | ]; 56 | 57 | $expected = hash_hmac('sha512', implode(',', $data), 'secret'); 58 | $this->assertSame($expected, $this->gateway->generateSignature($data)); 59 | 60 | $data = [ 61 | true, 62 | false, 63 | 0, 64 | 1, 65 | .1 66 | ]; 67 | 68 | $signatureData = [ 69 | 'true', 70 | 'false', 71 | '0', 72 | '1', 73 | '0.1' 74 | ]; 75 | 76 | $expected = hash_hmac('sha512', implode(',', $signatureData), 'secret'); 77 | $this->assertSame($expected, $this->gateway->generateSignature($data)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Message/PurchaseRequestTest.php: -------------------------------------------------------------------------------- 1 | gateway = new Gateway(); 31 | $this->gateway->setSigningKey(base64_encode('secret')); 32 | 33 | $this->request = new PurchaseRequest($this->getHttpClient(), $this->getHttpRequest(), $this->gateway); 34 | $this->request->initialize( 35 | array( 36 | 'refreshToken' => 'secret', 37 | 'amount' => '10.00', 38 | 'currency' => 'EUR', 39 | 'returnUrl' => 'https://www.example.com/return', 40 | 'orderId' => '1' 41 | ) 42 | ); 43 | $this->request->setAccessToken('secret'); 44 | } 45 | 46 | public function testGetData(): void 47 | { 48 | $this->request->setPaymentMethod('IDEAL'); 49 | $this->request->setOrderId('6'); 50 | $this->request->setLanguageCode('EN'); 51 | 52 | $card = new CreditCard([ 53 | 'firstName' => 'John', 54 | 'lastName' => 'Doe', 55 | 'address1' => 'Main street 123', 56 | 'postcode' => '1234AA', 57 | 'city' => 'Anytown', 58 | 'country' => 'NL' 59 | ]); 60 | $this->request->setCard($card); 61 | 62 | $data = $this->request->getData(); 63 | 64 | $this->assertRegExp('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|(\+|-)\d{2}(:?\d{2})?)$/', $data['timestamp']); 65 | $this->assertEquals('6', $data['merchantOrderId']); 66 | $this->assertEquals(array( 67 | 'amount' => 1000, 68 | 'currency' => 'EUR' 69 | ), $data['amount']); 70 | $this->assertEquals('EN', $data['language']); 71 | $this->assertEquals('', $data['description']); 72 | $this->assertEquals('https://www.example.com/return', $data['merchantReturnURL']); 73 | $this->assertEquals(array( 74 | 'firstName' => 'John', 75 | 'middleName' => '', 76 | 'lastName' => 'Doe', 77 | 'street' => 'Main street 123', 78 | 'postalCode' => '1234AA', 79 | 'city' => 'Anytown', 80 | 'countryCode' => 'NL', 81 | ), $data['shippingDetail']); 82 | $this->assertEquals('IDEAL', $data['paymentBrand']); 83 | $this->assertEquals('FORCE_ONCE', $data['paymentBrandForce']); 84 | 85 | $signatureData = array( 86 | $data['timestamp'], 87 | '6', 88 | 'EUR', 89 | 1000, 90 | 'EN', 91 | '', 92 | 'https://www.example.com/return', 93 | 'John', 94 | '', 95 | 'Doe', 96 | 'Main street 123', 97 | '1234AA', 98 | 'Anytown', 99 | 'NL', 100 | 'IDEAL', 101 | 'FORCE_ONCE' 102 | ); 103 | 104 | $signature = hash_hmac('sha512', implode(',', $signatureData), 'secret'); 105 | $this->assertEquals($signature, $data['signature']); 106 | } 107 | 108 | public function testBaseUrl(): void 109 | { 110 | $this->gateway->setTestMode(false); 111 | $this->assertEquals('https://betalen.rabobank.nl/omnikassa-api/', $this->request->getBaseUrl()); 112 | 113 | $this->gateway->setTestMode(true); 114 | $this->assertEquals('https://betalen.rabobank.nl/omnikassa-api-sandbox/', $this->request->getBaseUrl()); 115 | 116 | } 117 | 118 | public function testDescription(): void 119 | { 120 | $this->request->setDescription('a description'); 121 | $data = $this->request->getData(); 122 | 123 | $this->assertEquals('a description', $data['description']); 124 | } 125 | 126 | public function testDescriptionToLong(): void 127 | { 128 | $this->expectException(DescriptionToLongException::class); 129 | $this->expectExceptionMessage('Description can only be 35 characters long'); 130 | 131 | $this->request->setDescription('a very long description that is longer then 35 characters that is not allowed'); 132 | } 133 | 134 | public function testLanguageCode(): void 135 | { 136 | $this->request->setLanguageCode('EN'); 137 | $data = $this->request->getData(); 138 | 139 | $this->assertEquals('EN', $data['language']); 140 | } 141 | 142 | public function testLanguageCodeInvalid(): void 143 | { 144 | $this->expectException(InvalidLanguageCodeException::class); 145 | $this->expectExceptionMessage('Language code must be a valid ISO 639-1 language code'); 146 | 147 | $this->request->setLanguageCode('ENG'); 148 | } 149 | 150 | public function testSendSuccess(): void 151 | { 152 | $this->setMockHttpResponse('PurchaseSuccess.txt'); 153 | 154 | $this->request->setPaymentMethod('IDEAL'); 155 | $this->request->setOrderId('6'); 156 | 157 | /** @var PurchaseResponse $response */ 158 | $response = $this->request->send(); 159 | 160 | $this->assertInstanceOf(PurchaseResponse::class, $response); 161 | $this->assertFalse($response->isSuccessful()); 162 | $this->assertTrue($response->isRedirect()); 163 | $this->assertEquals('https://www.example.com/redirect', $response->getRedirectUrl()); 164 | } 165 | 166 | public function testSendInvalidSignature(): void 167 | { 168 | $this->setMockHttpResponse('PurchaseInvalidSignature.txt'); 169 | 170 | $this->request->setPaymentMethod('IDEAL'); 171 | $this->request->setOrderId('6'); 172 | 173 | $this->expectException(InvalidSignatureException::class); 174 | $this->expectExceptionMessage('Signature returned from server is invalid'); 175 | 176 | $this->request->send(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /tests/Message/StatusRequestTest.php: -------------------------------------------------------------------------------- 1 | gateway = new Gateway(); 28 | $this->gateway->setSigningKey(base64_encode('secret')); 29 | 30 | $this->request = new StatusRequest($this->getHttpClient(), $this->getHttpRequest(), $this->gateway); 31 | $this->request->initialize(array('notificationToken' => 'token')); 32 | } 33 | 34 | public function testGetData(): void 35 | { 36 | $data = $this->request->getData(); 37 | $this->assertEquals([], $data); 38 | } 39 | 40 | public function testSendSuccess(): void 41 | { 42 | $this->setMockHttpResponse('StatusSuccess.txt'); 43 | 44 | /** @var StatusResponse $response */ 45 | $response = $this->request->send(); 46 | 47 | $this->assertInstanceOf(StatusResponse::class, $response); 48 | $this->assertTrue($response->isSuccessful()); 49 | $this->assertFalse($response->getMoreStatusesAvailable()); 50 | 51 | $orders = $response->getOrders(); 52 | $this->assertCount(1, $response->getOrders()); 53 | 54 | $order = reset($orders); 55 | $this->assertInstanceOf(Order::class, $order); 56 | 57 | $orderExpected = new Order(); 58 | $orderExpected->merchantOrderId = 'order123'; 59 | $orderExpected->omnikassaOrderId = '1d0a95f4-2589-439b-9562-c50aa19f9caf'; 60 | $orderExpected->poiId = '2004'; 61 | $orderExpected->orderStatus = 'IN_PROGRESS'; 62 | $orderExpected->orderStatusDateTime = '2018-11-22T13:20:03.157+01:00'; 63 | $orderExpected->errorCode = ''; 64 | $orderExpected->paidAmount = [ 65 | 'currency' => 'USD', 66 | 'amount' => '2000' 67 | ]; 68 | $orderExpected->totalAmount = [ 69 | 'currency' => 'EUR', 70 | 'amount' => '4999' 71 | ]; 72 | 73 | $this->assertEquals($orderExpected, $order); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Mock/PurchaseInvalidSignature.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json; charset=utf-8 3 | 4 | { 5 | "signature": "invalid", 6 | "redirectUrl": "https://www.example.com/redirect" 7 | } 8 | -------------------------------------------------------------------------------- /tests/Mock/PurchaseSuccess.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json; charset=utf-8 3 | 4 | { 5 | "signature": "3cd9acd01e78b094e1ca203706d6615fc9618b97c39acf64a83473270724f327154333b000e73f36c6f1d478ced03142821adffb33b2fa2bf35063d1bbc55ac8", 6 | "redirectUrl": "https://www.example.com/redirect" 7 | } 8 | -------------------------------------------------------------------------------- /tests/Mock/StatusSuccess.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json; charset=utf-8 3 | 4 | { 5 | "signature": "5ddb1851bd4b2afdc022dfa1b3be854bb34ba8eccf6da50e9da4ab077ba3112e3b41b568fb60d3bf67f774c8d431fe9421e0025deb13c0b4d9bdf72182d7660b", 6 | "moreOrderResultsAvailable": false, 7 | "orderResults": [ 8 | { 9 | "merchantOrderId": "order123", 10 | "omnikassaOrderId": "1d0a95f4-2589-439b-9562-c50aa19f9caf", 11 | "poiId": "2004", 12 | "orderStatus": "IN_PROGRESS", 13 | "orderStatusDateTime": "2018-11-22T13:20:03.157+01:00", 14 | "errorCode": "", 15 | "paidAmount": { 16 | "currency": "USD", 17 | "amount": "2000" 18 | }, 19 | "totalAmount": { 20 | "currency": "EUR", 21 | "amount": "4999" 22 | } 23 | } 24 | ] 25 | } 26 | --------------------------------------------------------------------------------