From 1a7a3e5634fbb2ee02608eee1068cd13ad244b91 Mon Sep 17 00:00:00 2001 From: Linus Larsson Date: Mon, 18 Jan 2016 23:46:20 +0100 Subject: [PATCH] Initial commit --- .gitignore | 11 ++ .travis.yml | 13 ++ LICENSE | 21 +++ README.md | 52 ++++++++ composer.json | 31 +++++ phpunit.xml.dist | 25 ++++ src/Gateway.php | 64 +++++++++ src/Message/AbstractRequest.php | 165 ++++++++++++++++++++++++ src/Message/FetchTransactionRequest.php | 23 ++++ src/Message/PurchaseRequest.php | 11 ++ src/Message/PurchaseResponse.php | 48 +++++++ tests/GatewayTest.php | 23 ++++ tests/Message/PurchaseRequestTest.php | 39 ++++++ tests/Message/PurchaseResponseTest.php | 26 ++++ tests/Mock/PurchaseFailure.txt | 7 + tests/Mock/PurchaseSuccess.txt | 5 + 16 files changed, 564 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 src/Gateway.php create mode 100644 src/Message/AbstractRequest.php create mode 100644 src/Message/FetchTransactionRequest.php create mode 100644 src/Message/PurchaseRequest.php create mode 100644 src/Message/PurchaseResponse.php create mode 100644 tests/GatewayTest.php create mode 100644 tests/Message/PurchaseRequestTest.php create mode 100644 tests/Message/PurchaseResponseTest.php create mode 100644 tests/Mock/PurchaseFailure.txt create mode 100644 tests/Mock/PurchaseSuccess.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..331cce1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/vendor +composer.lock +composer.phar +phpunit.xml +.idea/ + +.directory +dirlist.app +dirlist.vendor +dirlist.cache +documents/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..67753ce --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: php + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - hhvm + +before_script: + - composer install -n --dev --prefer-source + +script: vendor/bin/phpcs --standard=PSR2 src && vendor/bin/phpunit --coverage-text diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..421aaf5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Nessla AB + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b4271db --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# Omnipay: Swish + +**Swish driver for the Omnipay PHP payment processing library** + +[![Build Status](https://travis-ci.org/nessla/omnipay-swish.svg?branch=master)](https://travis-ci.org/nessla/omnipay-swish) +[![Latest Stable Version](https://poser.pugx.org/nessla/omnipay-swish/version.svg)](https://packagist.org/packages/nessla/omnipay-swish) +[![Total Downloads](https://poser.pugx.org/nessla/omnipay-swish/d/total.png)](https://packagist.org/packages/nessla/omnipay-swish) + +[Omnipay](https://github.com/thephpleague/omnipay) is a framework agnostic, multi-gateway payment +processing library for PHP 5.3+. This package implements Swish support for Omnipay. + +## Installation + +Omnipay is installed via [Composer](http://getcomposer.org/). To install, simply add it +to your `composer.json` file: + +```json +{ + "require": { + "nessla/omnipay-swish": "~1.0" + } +} +``` + +And run composer to update your dependencies: + + $ curl -s http://getcomposer.org/installer | php + $ php composer.phar update + +## Basic Usage + +The following gateways are provided by this package: + +* [Swish](https://www.getswish.se) + +For general usage instructions, please see the main [Omnipay](https://github.com/thephpleague/omnipay) +repository. + + + +## Support + +If you are having general issues with Omnipay, we suggest posting on +[Stack Overflow](http://stackoverflow.com/). Be sure to add the +[omnipay tag](http://stackoverflow.com/questions/tagged/omnipay) so it can be easily found. + +If you want to keep up to date with release announcements, discuss ideas for the project, +or ask more detailed questions, there is also a [mailing list](https://groups.google.com/forum/#!forum/omnipay) which +you can subscribe to. + +If you believe you have found a bug, please report it using the [GitHub issue tracker](https://github.com/nessla/omnipay-swish/issues), +or better yet, fork the library and submit a pull request. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b942c1c --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "nessla/omnipay-swish", + "type": "library", + "description": "Swish gateway for Omnipay payment processing library", + "keywords": [ + "gateway", + "merchant", + "omnipay", + "pay", + "payment", + "swish", + "purchase" + ], + "homepage": "https://github.com/nessla/omnipay-swish", + "license": "MIT", + "authors": [ + { + "name": "Linus Larsson", + "email": "linus@nessla.se" + } + ], + "autoload": { + "psr-4": { "Omnipay\\Swish\\" : "src/" } + }, + "require": { + "omnipay/common": "~2.0" + }, + "require-dev": { + "omnipay/tests": "~2.0" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..a35b736 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + ./tests/ + + + + + + + + ./src + + + diff --git a/src/Gateway.php b/src/Gateway.php new file mode 100644 index 0000000..cec90ec --- /dev/null +++ b/src/Gateway.php @@ -0,0 +1,64 @@ + null, + 'privateKey' => null, + 'caCert' => null, + 'testMode' => false + ); + } + + public function getCert() + { + return $this->getParameter('cert'); + } + + public function setCert($value) + { + return $this->setParameter('cert', $value); + } + + public function getPrivateKey() + { + return $this->getParameter('privateKey'); + } + + public function setPrivateKey($value) + { + return $this->setParameter('privateKey', $value); + } + + public function getCaCert() + { + return $this->getParameter('caCert'); + } + + public function setCaCert($value) + { + return $this->setParameter('caCert', $value); + } + + public function purchase(array $parameters = array()) + { + return $this->createRequest('\Omnipay\Swish\Message\PurchaseRequest', $parameters); + } + + public function fetchTransaction(array $parameters = array()) + { + return $this->createRequest('\Omnipay\Swish\Message\FetchTransactionRequest', $parameters); + } +} diff --git a/src/Message/AbstractRequest.php b/src/Message/AbstractRequest.php new file mode 100644 index 0000000..e0249ba --- /dev/null +++ b/src/Message/AbstractRequest.php @@ -0,0 +1,165 @@ +getParameter('cert'); + } + + public function setCert($value) + { + return $this->setParameter('cert', $value); + } + + public function getPrivateKey() + { + return $this->getParameter('privateKey'); + } + + public function setPrivateKey($value) + { + return $this->setParameter('privateKey', $value); + } + + public function getCaCert() + { + return $this->getParameter('caCert'); + } + + public function setCaCert($value) + { + return $this->setParameter('caCert', $value); + } + + public function getPayeePaymentReference() + { + return $this->getParameter('payeePaymentReference'); + } + + public function setPayeePaymentReference($value) + { + return $this->setParameter('payeePaymentReference', $value); + } + + public function getPayerAlias() + { + return $this->getParameter('payerAlias'); + } + + public function setPayerAlias($value) + { + return $this->setParameter('payerAlias', $value); + } + + public function getPayeeAlias() + { + return $this->getParameter('payeeAlias'); + } + + public function setPayeeAlias($value) + { + return $this->setParameter('payeeAlias', $value); + } + + protected function getHttpMethod() + { + return 'POST'; + } + + protected function getEndpoint() + { + $url = $this->getTestMode() ? $this->testEndpoint : $this->liveEndpoint; + return $url . '/' . self::API_VERSION; + } + + public function getData() + { + $this->validate('notifyUrl', 'amount', 'currency', 'payeeAlias'); + + $data = array( + 'callbackUrl' => $this->getNotifyUrl(), + 'amount' => $this->getAmount(), + 'currency' => $this->getCurrency(), + 'payerAlias' => $this->getPayerAlias(), + 'payeeAlias' => $this->getPayeeAlias() + ); + + return $data; + } + + public function sendData($data) + { + // don't throw exceptions for 4xx errors + $this->httpClient->getEventDispatcher()->addListener( + 'request.error', + function ($event) { + if ($event['response']->isClientError()) { + $event->stopPropagation(); + } + } + ); + + // Guzzle HTTP Client createRequest does funny things when a GET request + // has attached data, so don't send the data if the method is GET. + if ($this->getHttpMethod() == 'GET') { + $httpRequest = $this->httpClient->createRequest( + $this->getHttpMethod(), + $this->getEndpoint() . '?' . http_build_query($data), + array( + 'Content-type' => 'application/json' + ), + null, + array( + 'cert' => $this->getCert(), + 'ssl_key' => $this->getPrivateKey(), + 'verify' => $this->getCaCert() + ) + ); + } else { + $httpRequest = $this->httpClient->createRequest( + $this->getHttpMethod(), + $this->getEndpoint(), + array( + 'Content-type' => 'application/json', + ), + json_encode($data), + array( + 'cert' => $this->getCert(), + 'ssl_key' => $this->getPrivateKey(), + 'verify' => $this->getCaCert() + ) + ); + } + + try { + $httpResponse = $httpRequest->send(); + return $this->response = $this->createResponse($httpResponse); + } catch (\Exception $e) { + throw new InvalidResponseException( + 'Error communicating with payment gateway: ' . $e->getMessage(), + $e->getCode() + ); + } + } + + + protected function createResponse($response) + { + $data = $response->json(); + $statusCode = $response->getStatusCode(); + + return $this->response = new PurchaseResponse($this, $response, $data, $statusCode); + } +} diff --git a/src/Message/FetchTransactionRequest.php b/src/Message/FetchTransactionRequest.php new file mode 100644 index 0000000..b04d382 --- /dev/null +++ b/src/Message/FetchTransactionRequest.php @@ -0,0 +1,23 @@ +validate('transactionReference'); + + return array(); + } + + protected function getHttpMethod() + { + return 'GET'; + } + + public function getEndpoint() + { + return parent::getEndpoint() . '/paymentrequests/' . $this->getTransactionReference(); + } +} diff --git a/src/Message/PurchaseRequest.php b/src/Message/PurchaseRequest.php new file mode 100644 index 0000000..2aaa215 --- /dev/null +++ b/src/Message/PurchaseRequest.php @@ -0,0 +1,11 @@ +statusCode = $statusCode; + $this->response = $response; + } + + public function isSuccessful() + { + return ($this->getCode() == 200 || $this->getCode() == 201); + } + + public function getTransactionReference() + { + if (!empty($location = $this->response->getHeader('location'))) { + $urlParts = explode('/', $location); + return end($urlParts); + } + + return null; + } + + public function getMessage() + { + if (isset($this->data[0]['errorMessage'])) { + return $this->data[0]['errorMessage']; + } + + return null; + } + + public function getCode() + { + return $this->statusCode; + } +} diff --git a/tests/GatewayTest.php b/tests/GatewayTest.php new file mode 100644 index 0000000..c11f514 --- /dev/null +++ b/tests/GatewayTest.php @@ -0,0 +1,23 @@ +gateway = new Gateway($this->getHttpClient(), $this->getHttpRequest()); + } + + public function testPurchase() + { + $request = $this->gateway->purchase(array('amount' => '100.00')); + + $this->assertInstanceOf('Omnipay\Swish\Message\PurchaseRequest', $request); + $this->assertSame('100.00', $request->getAmount()); + } +} diff --git a/tests/Message/PurchaseRequestTest.php b/tests/Message/PurchaseRequestTest.php new file mode 100644 index 0000000..ddb1a00 --- /dev/null +++ b/tests/Message/PurchaseRequestTest.php @@ -0,0 +1,39 @@ +request = new PurchaseRequest($this->getHttpClient(), $this->getHttpRequest()); + $this->request->initialize( + array( + 'payeePaymentReference' => '0123456789', + 'notifyUrl' => 'https://example.com/api/swishcb/paymentrequests', + 'payerAlias' => '46701234567', + 'payeeAlias' => '1234760039', + 'amount' => '100.00', + 'currency' => 'SEK', + 'message' => 'Kingston USB Flash Drive 8 GB', + ) + ); + } + public function testGetData() + { + $data = $this->request->getData(); + $this->assertSame('https://example.com/api/swishcb/paymentrequests', $data['callbackUrl']); + $this->assertSame('46701234567', $data['payerAlias']); + $this->assertSame('1234760039', $data['payeeAlias']); + $this->assertSame('100.00', $data['amount']); + $this->assertSame('SEK', $data['currency']); + } + public function testSend() + { + $this->setMockHttpResponse('PurchaseSuccess.txt'); + $response = $this->request->send(); + $this->assertTrue($response->isSuccessful()); + } +} diff --git a/tests/Message/PurchaseResponseTest.php b/tests/Message/PurchaseResponseTest.php new file mode 100644 index 0000000..3dfd874 --- /dev/null +++ b/tests/Message/PurchaseResponseTest.php @@ -0,0 +1,26 @@ +getMockHttpResponse('PurchaseSuccess.txt'); + $response = new PurchaseResponse($this->getMockRequest(), $httpResponse, $httpResponse->json(), $httpResponse->getStatusCode()); + + $this->assertTrue($response->isSuccessful()); + $this->assertSame('AB23D7406ECE4542A80152D909EF9F6B', $response->getTransactionReference()); + } + + public function testPurchaseFailure() + { + $httpResponse = $this->getMockHttpResponse('PurchaseFailure.txt'); + $response = new PurchaseResponse($this->getMockRequest(), $httpResponse, $httpResponse->json(), $httpResponse->getStatusCode()); + + $this->assertFalse($response->isSuccessful()); + $this->assertSame('Amount value is missing or not a valid number', $response->getMessage()); + } +} diff --git a/tests/Mock/PurchaseFailure.txt b/tests/Mock/PurchaseFailure.txt new file mode 100644 index 0000000..57e10d0 --- /dev/null +++ b/tests/Mock/PurchaseFailure.txt @@ -0,0 +1,7 @@ +HTTP/1.1 422 Unprocessable Entity +Server: Apache-Coyote/1.1 +Content-Type: application/json +Transfer-Encoding: chunked +Date: Thu, 21 Jan 2016 18:36:05 GMT + +[{"errorCode":"PA02","errorMessage":"Amount value is missing or not a valid number","additionalInformation":null}] diff --git a/tests/Mock/PurchaseSuccess.txt b/tests/Mock/PurchaseSuccess.txt new file mode 100644 index 0000000..d23d799 --- /dev/null +++ b/tests/Mock/PurchaseSuccess.txt @@ -0,0 +1,5 @@ +HTTP/1.1 201 Created +Server: Apache-Coyote/1.1 +Location: http://172.31.21.186:8580/swish-cpcapi/api/v1/paymentrequests/AB23D7406ECE4542A80152D909EF9F6B +Content-Length: 0 +Date: Thu, 21 Jan 2016 18:34:45 GMT