From a983048bf83737637c6293fc300d8baf9d4e040c Mon Sep 17 00:00:00 2001 From: Ivan Toth Date: Tue, 5 Sep 2017 16:41:27 +0200 Subject: [PATCH 1/3] Card token purchase --- README.md | 21 +++++ src/Gateway.php | 5 ++ src/Message/AbstractRequest.php | 20 +++++ src/Message/CardTokenPurchaseRequest.php | 50 ++++++++++++ src/Message/CardTokenPurchaseResponse.php | 50 ++++++++++++ tests/GatewayTest.php | 8 ++ .../Message/CardTokenPurchaseRequestTest.php | 73 ++++++++++++++++++ .../Message/CardTokenPurchaseResponseTest.php | 38 +++++++++ tests/Mock/CardTokenPurchaseFailure.txt | 77 +++++++++++++++++++ .../CardTokenPurchaseFailureValidation.txt | 17 ++++ tests/Mock/CardTokenPurchaseSuccess.txt | 77 +++++++++++++++++++ 11 files changed, 436 insertions(+) create mode 100644 src/Message/CardTokenPurchaseRequest.php create mode 100644 src/Message/CardTokenPurchaseResponse.php create mode 100644 tests/Message/CardTokenPurchaseRequestTest.php create mode 100644 tests/Message/CardTokenPurchaseResponseTest.php create mode 100644 tests/Mock/CardTokenPurchaseFailure.txt create mode 100644 tests/Mock/CardTokenPurchaseFailureValidation.txt create mode 100644 tests/Mock/CardTokenPurchaseSuccess.txt diff --git a/README.md b/README.md index d2a0a87..116f2ab 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ repository. ### Checkout.js +Currently this package provides implementation of 2 workflows: + +#### 1. Authorize payment and then capture + The Checkout.com integration is fairly straight forward. Essentially you just pass the order data and receive a payment token, which you can use in the checkout.js payment form. After your customer has entered his data, you'll receive @@ -66,6 +70,23 @@ if ($response->isSuccessful()) { } ``` +#### 2. Payment with card token (card token purchase) + +- In this method we first validate card data via form and js provided from Checkout.com, see [https://docs.checkout.com/getting-started/checkoutkit-js](https://docs.checkout.com/getting-started/checkoutkit-js) +- After card is validated, we receive card token ([https://docs.checkout.com/getting-started/checkoutkit-js#example](https://docs.checkout.com/getting-started/checkoutkit-js#example)) +- in the final step we complete payment providing order data and a card token: +```php +$response = $gateway->cardTokenPurchase([ + 'amount' => $amount, + 'currency' => $currency, + 'email' => 'customer@email.com', + 'cardToken' => 'some_token', + 'description' => 'some nice description' +]); +``` + +Note that `amount`, `currency`, `email` and `cardToken` are required fields here. + ## Support If you are having general issues with Omnipay, we suggest posting on diff --git a/src/Gateway.php b/src/Gateway.php index 64b44c8..7d7b924 100644 --- a/src/Gateway.php +++ b/src/Gateway.php @@ -57,4 +57,9 @@ public function completePurchase(array $parameters = array()) { return $this->createRequest('\Omnipay\CheckoutCom\Message\CompletePurchaseRequest', $parameters); } + + public function cardTokenPurchase(array $parameters = array()) + { + return $this->createRequest('\Omnipay\CheckoutCom\Message\CardTokenPurchaseRequest', $parameters); + } } diff --git a/src/Message/AbstractRequest.php b/src/Message/AbstractRequest.php index d9856de..f98d15f 100644 --- a/src/Message/AbstractRequest.php +++ b/src/Message/AbstractRequest.php @@ -77,6 +77,26 @@ public function setUdf($value) return $this->setParameter('udf', $value); } + public function setCardToken($value) + { + return $this->setParameter('cardToken', $value); + } + + public function getCardToken() + { + return $this->getParameter('cardToken'); + } + + public function setEmail($value) + { + return $this->setParameter('email', $value); + } + + public function getEmail() + { + return $this->getParameter('email'); + } + public function sendRequest($data) { // don't throw exceptions for 4xx errors diff --git a/src/Message/CardTokenPurchaseRequest.php b/src/Message/CardTokenPurchaseRequest.php new file mode 100644 index 0000000..c37ef45 --- /dev/null +++ b/src/Message/CardTokenPurchaseRequest.php @@ -0,0 +1,50 @@ +validate('amount', 'currency'); + + $data = array(); + $data['value'] = $this->getAmountInteger(); + $data['currency'] = strtoupper($this->getCurrency()); + $data['description'] = $this->getDescription(); + $data['metadata'] = $this->getMetadata(); + $data['cardToken'] = $this->getCardToken(); + $data['email'] = $this->getEmail(); + + + if ($udf = $this->getUdfValues()) { + $data['udf1'] = $udf[0]; + $data['udf2'] = isset($udf[1]) ? $udf[1] : null; + $data['udf3'] = isset($udf[2]) ? $udf[2] : null; + $data['udf4'] = isset($udf[3]) ? $udf[3] : null; + $data['udf5'] = isset($udf[4]) ? $udf[4] : null; + } + + return $data; + } + + public function sendData($data) + { + $httpResponse = $this->sendRequest($data); + + return $this->response = new CardTokenPurchaseResponse($this, $httpResponse->json()); + } + + public function getEndpoint() + { + return parent::getEndpoint() . '/charges/token'; + } +} diff --git a/src/Message/CardTokenPurchaseResponse.php b/src/Message/CardTokenPurchaseResponse.php new file mode 100644 index 0000000..ea6b78c --- /dev/null +++ b/src/Message/CardTokenPurchaseResponse.php @@ -0,0 +1,50 @@ +data['errorCode'])) { + return false; + } + + if (!empty($this->data['status'])) { + return ($this->data['status'] == 'Authorised'); + } + + return false; + } + + /** + * Get the error message from the response. + * + * Returns null if the request was successful. + * + * @return string|null + */ + public function getMessage() + { + if (!$this->isSuccessful() && isset($this->data['errorCode'])) { + return $this->data['errorCode'] . ': ' . $this->data['message']; + } + + if (!$this->isSuccessful() && isset($this->data['responseCode'])) { + return $this->data['responseCode'] . ': ' . $this->data['responseMessage']; + } + + return null; + } +} diff --git a/tests/GatewayTest.php b/tests/GatewayTest.php index 68add04..da4252b 100644 --- a/tests/GatewayTest.php +++ b/tests/GatewayTest.php @@ -28,4 +28,12 @@ public function testCompletePurchase() $this->assertInstanceOf('Omnipay\CheckoutCom\Message\CompletePurchaseRequest', $request); $this->assertSame('10.00', $request->getAmount()); } + + public function testCardTokenPurchase() + { + $request = $this->gateway->cardTokenPurchase(array('amount' => '10.00')); + + $this->assertInstanceOf('Omnipay\CheckoutCom\Message\CardTokenPurchaseRequest', $request); + $this->assertSame('10.00', $request->getAmount()); + } } diff --git a/tests/Message/CardTokenPurchaseRequestTest.php b/tests/Message/CardTokenPurchaseRequestTest.php new file mode 100644 index 0000000..e316a5a --- /dev/null +++ b/tests/Message/CardTokenPurchaseRequestTest.php @@ -0,0 +1,73 @@ +request = new CardTokenPurchaseRequest($this->getHttpClient(), $this->getHttpRequest()); + $this->request->initialize( + array( + 'amount' => '12.00', + 'currency' => 'uSd', + 'description' => 'Order #42', + 'email' => 'customer@test.com', + 'metadata' => array( + 'foo' => 'bar', + ), + 'udf' => array( + 'first' => 'lorem', + 'second' => 'ipsum' + ) + ) + ); + } + + public function testGetData() + { + $data = $this->request->getData(); + + $this->assertSame(1200, $data['value']); + $this->assertSame('USD', $data['currency']); + $this->assertSame('Order #42', $data['description']); + $this->assertSame(array('foo' => 'bar'), $data['metadata']); + $this->assertSame('lorem', $data['udf1']); + $this->assertSame('ipsum', $data['udf2']); + $this->assertSame('customer@test.com', $data['email']); + + } + + public function testSendSuccess() + { + $this->setMockHttpResponse('CardTokenPurchaseSuccess.txt'); + $response = $this->request->send(); + + $this->assertTrue($response->isSuccessful()); + $this->assertSame('charge_test_DD0BF9EC548R752B79E2', $response->getTransactionReference()); + $this->assertNull($response->getMessage()); + } + + public function testSendErrorValidation() + { + $this->setMockHttpResponse('CardTokenPurchaseFailureValidation.txt'); + $response = $this->request->send(); + + $this->assertFalse($response->isSuccessful()); + $this->assertFalse($response->isRedirect()); + $this->assertNull($response->getTransactionReference()); + $this->assertSame('70000: Validation error', $response->getMessage()); + } + + public function testSendError() + { + $this->setMockHttpResponse('CardTokenPurchaseFailure.txt'); + $response = $this->request->send(); + + $this->assertFalse($response->isSuccessful()); + $this->assertSame('charge_test_EE0E09FC548L752B6C12', $response->getTransactionReference()); + $this->assertSame('20087: Bad Track Data', $response->getMessage()); + } +} diff --git a/tests/Message/CardTokenPurchaseResponseTest.php b/tests/Message/CardTokenPurchaseResponseTest.php new file mode 100644 index 0000000..4323d05 --- /dev/null +++ b/tests/Message/CardTokenPurchaseResponseTest.php @@ -0,0 +1,38 @@ +getMockHttpResponse('CardTokenPurchaseSuccess.txt'); + $response = new CardTokenPurchaseResponse($this->getMockRequest(), $httpResponse->json()); + + $this->assertTrue($response->isSuccessful()); + $this->assertSame('charge_test_DD0BF9EC548R752B79E2', $response->getTransactionReference()); + $this->assertNull($response->getMessage()); + } + + public function testPurchaseFailure() + { + $httpResponse = $this->getMockHttpResponse('CardTokenPurchaseFailure.txt'); + $response = new CardTokenPurchaseResponse($this->getMockRequest(), $httpResponse->json()); + + $this->assertFalse($response->isSuccessful()); + $this->assertSame('charge_test_EE0E09FC548L752B6C12', $response->getTransactionReference()); + $this->assertSame('20087: Bad Track Data', $response->getMessage()); + } + + public function testPurchaseFailureValidation() + { + $httpResponse = $this->getMockHttpResponse('CardTokenPurchaseFailureValidation.txt'); + $response = new CardTokenPurchaseResponse($this->getMockRequest(), $httpResponse->json()); + + $this->assertFalse($response->isSuccessful()); + $this->assertNull($response->getTransactionReference()); + $this->assertSame('70000: Validation error', $response->getMessage()); + } +} diff --git a/tests/Mock/CardTokenPurchaseFailure.txt b/tests/Mock/CardTokenPurchaseFailure.txt new file mode 100644 index 0000000..ffaf40d --- /dev/null +++ b/tests/Mock/CardTokenPurchaseFailure.txt @@ -0,0 +1,77 @@ +HTTP/1.1 200 OK +Server: nginx +Date: Sun, 05 May 2013 08:51:15 GMT +Content-Type: application/json;charset=utf-8 +Content-Length: 997 +Connection: keep-alive +Cache-Control: no-cache, no-store +Access-Control-Max-Age: 300 +Access-Control-Allow-Credentials: true + +{ + "id": "charge_test_EE0E09FC548L752B6C12", + "liveMode": false, + "created": "2017-09-05T11:47:36Z", + "value": 319, + "currency": "EUR", + "trackId": null, + "description": "Order Key: 20479114264", + "email": "test@solutica.de", + "chargeMode": 1, + "transactionIndicator": 1, + "customerIp": null, + "responseMessage": "Bad Track Data", + "responseAdvancedInfo": "Bad Track Data", + "responseCode": "20087", + "status": "Declined", + "authCode": "000000", + "isCascaded": false, + "autoCapture": "Y", + "autoCapTime": 0, + "card": { + "customerId": "cust_EC34DF5E-7A0E-43C3-868F-170DA09B0DD6", + "expiryMonth": "06", + "expiryYear": "2025", + "billingDetails": { + "addressLine1": null, + "addressLine2": null, + "postcode": null, + "country": null, + "city": null, + "state": null, + "phone": [] + }, + "id": "", + "last4": "4242", + "bin": "424242", + "paymentMethod": "Visa", + "fingerprint": "9F3BAD2E48C6C8579F2F5DC0710B7C11A8ACD5072C3363A72579A6FB227D64BE", + "name": null, + "cvvCheck": "D", + "avsCheck": "S" + }, + "riskCheck": true, + "customerPaymentPlans": null, + "metadata": { + "ordernumber": "426", + "invoicenumber": "410", + "order_id": "59ae8f05-e750-4922-99c0-5e527f000102", + "customer_id": "57977215-2990-4354-a733-3db4c0a8171f", + "customer_name": "Testvorname Testnachname" + }, + "shippingDetails": { + "addressLine1": null, + "addressLine2": null, + "postcode": null, + "country": null, + "city": null, + "state": null, + "phone": [] + }, + "products": [], + "udf1": "Order-No:426", + "udf2": "Invoice-No:410", + "udf3": "Order-Id:59ae8f05-e750-4922-99c0-5e527f000102", + "udf4": "Customer-Id:57977215-2990-4354-a733-3db4c0a8171f", + "udf5": null +} diff --git a/tests/Mock/CardTokenPurchaseFailureValidation.txt b/tests/Mock/CardTokenPurchaseFailureValidation.txt new file mode 100644 index 0000000..71fa844 --- /dev/null +++ b/tests/Mock/CardTokenPurchaseFailureValidation.txt @@ -0,0 +1,17 @@ +HTTP/1.1 400 Bad Request +Server: nginx +Date: Sun, 05 May 2013 08:52:09 GMT +Content-Type: application/json;charset=utf-8 +Content-Length: 127 +Connection: keep-alive +Cache-Control: no-cache, no-store +Access-Control-Max-Age: 300 +Access-Control-Allow-Credentials: true + +{ + "errorCode":"70000", + "message":"Validation error", + "errors": [ + "Invalid value for 'value'" + ] +} diff --git a/tests/Mock/CardTokenPurchaseSuccess.txt b/tests/Mock/CardTokenPurchaseSuccess.txt new file mode 100644 index 0000000..ccbe7d8 --- /dev/null +++ b/tests/Mock/CardTokenPurchaseSuccess.txt @@ -0,0 +1,77 @@ +HTTP/1.1 200 OK +Server: nginx +Date: Sun, 05 May 2013 08:51:15 GMT +Content-Type: application/json;charset=utf-8 +Content-Length: 997 +Connection: keep-alive +Cache-Control: no-cache, no-store +Access-Control-Max-Age: 300 +Access-Control-Allow-Credentials: true + +{ + "id": "charge_test_DD0BF9EC548R752B79E2", + "liveMode": false, + "created": "2017-09-05T10:18:06Z", + "value": 319, + "currency": "EUR", + "trackId": null, + "description": "Order Key: 20479114249", + "email": "test@solutica.de", + "chargeMode": 1, + "transactionIndicator": 1, + "customerIp": null, + "responseMessage": "Approved", + "responseAdvancedInfo": "Approved", + "responseCode": "10000", + "status": "Authorised", + "authCode": "373865", + "isCascaded": false, + "autoCapture": "Y", + "autoCapTime": 0, + "card": { + "customerId": "cust_EC34DF5E-7A0E-43C3-868F-170DA09B0DD6", + "expiryMonth": "06", + "expiryYear": "2018", + "billingDetails": { + "addressLine1": null, + "addressLine2": null, + "postcode": null, + "country": null, + "city": null, + "state": null, + "phone": [] + }, + "id": "card_52069248-88D6-43CF-8EA1-2A6104EB1593", + "last4": "4242", + "bin": "424242", + "paymentMethod": "Visa", + "fingerprint": "F639CAB2745BEE4140BF86DF6B6D6E255C5945AAC3788D923FA047EA4C208622", + "name": null, + "cvvCheck": "Y", + "avsCheck": "S" + }, + "riskCheck": true, + "customerPaymentPlans": null, + "metadata": { + "ordernumber": "424", + "invoicenumber": "408", + "order_id": "59ae7a0a-353c-435d-a9b4-4b897f000102", + "customer_id": "57977215-2990-4354-a733-3db4c0a8171f", + "customer_name": "Testvorname Testnachname" + }, + "shippingDetails": { + "addressLine1": null, + "addressLine2": null, + "postcode": null, + "country": null, + "city": null, + "state": null, + "phone": [] + }, + "products": [], + "udf1": "Order-No:424", + "udf2": "Invoice-No:408", + "udf3": "Order-Id:59ae7a0a-353c-435d-a9b4-4b897f000102", + "udf4": "Customer-Id:57977215-2990-4354-a733-3db4c0a8171f", + "udf5": null +} From 2f7a582d7bb6ee598548d8aafcb6380bac37b816 Mon Sep 17 00:00:00 2001 From: Ivan Toth Date: Tue, 5 Sep 2017 16:56:53 +0200 Subject: [PATCH 2/3] spaces instead of tabs --- src/Message/CardTokenPurchaseRequest.php | 70 +++++++++++------------ src/Message/CardTokenPurchaseResponse.php | 24 ++++---- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/Message/CardTokenPurchaseRequest.php b/src/Message/CardTokenPurchaseRequest.php index c37ef45..9afe2af 100644 --- a/src/Message/CardTokenPurchaseRequest.php +++ b/src/Message/CardTokenPurchaseRequest.php @@ -12,39 +12,39 @@ */ class CardTokenPurchaseRequest extends AbstractRequest { - public function getData() - { - $this->validate('amount', 'currency'); - - $data = array(); - $data['value'] = $this->getAmountInteger(); - $data['currency'] = strtoupper($this->getCurrency()); - $data['description'] = $this->getDescription(); - $data['metadata'] = $this->getMetadata(); - $data['cardToken'] = $this->getCardToken(); - $data['email'] = $this->getEmail(); - - - if ($udf = $this->getUdfValues()) { - $data['udf1'] = $udf[0]; - $data['udf2'] = isset($udf[1]) ? $udf[1] : null; - $data['udf3'] = isset($udf[2]) ? $udf[2] : null; - $data['udf4'] = isset($udf[3]) ? $udf[3] : null; - $data['udf5'] = isset($udf[4]) ? $udf[4] : null; - } - - return $data; - } - - public function sendData($data) - { - $httpResponse = $this->sendRequest($data); - - return $this->response = new CardTokenPurchaseResponse($this, $httpResponse->json()); - } - - public function getEndpoint() - { - return parent::getEndpoint() . '/charges/token'; - } + public function getData() + { + $this->validate('amount', 'currency'); + + $data = array(); + $data['value'] = $this->getAmountInteger(); + $data['currency'] = strtoupper($this->getCurrency()); + $data['description'] = $this->getDescription(); + $data['metadata'] = $this->getMetadata(); + $data['cardToken'] = $this->getCardToken(); + $data['email'] = $this->getEmail(); + + + if ($udf = $this->getUdfValues()) { + $data['udf1'] = $udf[0]; + $data['udf2'] = isset($udf[1]) ? $udf[1] : null; + $data['udf3'] = isset($udf[2]) ? $udf[2] : null; + $data['udf4'] = isset($udf[3]) ? $udf[3] : null; + $data['udf5'] = isset($udf[4]) ? $udf[4] : null; + } + + return $data; + } + + public function sendData($data) + { + $httpResponse = $this->sendRequest($data); + + return $this->response = new CardTokenPurchaseResponse($this, $httpResponse->json()); + } + + public function getEndpoint() + { + return parent::getEndpoint() . '/charges/token'; + } } diff --git a/src/Message/CardTokenPurchaseResponse.php b/src/Message/CardTokenPurchaseResponse.php index ea6b78c..a4ba8db 100644 --- a/src/Message/CardTokenPurchaseResponse.php +++ b/src/Message/CardTokenPurchaseResponse.php @@ -15,20 +15,20 @@ class CardTokenPurchaseResponse extends AbstractResponse { - public function isSuccessful() - { - if (!empty($this->data['errorCode'])) { - return false; - } + public function isSuccessful() + { + if (!empty($this->data['errorCode'])) { + return false; + } - if (!empty($this->data['status'])) { - return ($this->data['status'] == 'Authorised'); - } + if (!empty($this->data['status'])) { + return ($this->data['status'] == 'Authorised'); + } - return false; - } + return false; + } - /** + /** * Get the error message from the response. * * Returns null if the request was successful. @@ -42,7 +42,7 @@ public function getMessage() } if (!$this->isSuccessful() && isset($this->data['responseCode'])) { - return $this->data['responseCode'] . ': ' . $this->data['responseMessage']; + return $this->data['responseCode'] . ': ' . $this->data['responseMessage']; } return null; From a28e4a4ddeb6a39023d966ce6b3876ba521114ba Mon Sep 17 00:00:00 2001 From: Ivan Toth Date: Tue, 5 Sep 2017 17:06:00 +0200 Subject: [PATCH 3/3] remove php 5.3 and add 7.0 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 67753ce..0b8a1cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,11 @@ language: php php: - - 5.3 - 5.4 - 5.5 - 5.6 - hhvm + - 7.0 before_script: - composer install -n --dev --prefer-source