diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d6c274 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +vendor/ +.idea/ + +# Ignore version lock file as lib's not standalone +composer.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4df3287 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: php +php: + - '5.6' + - '7.0' + - '7.1' + +install: + - composer install + +script: + - composer test + +git: + depth: 3 + +cache: + directories: + - $HOME/.composer/cache diff --git a/README.md b/README.md index 7ecf179..d52eb5f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **WorldPay XML Direct driver for the Omnipay PHP payment processing library** [Omnipay](https://github.com/omnipay/omnipay) is a framework agnostic, -multi-gateway payment processing library for PHP 5.3+. This package implements +multi-gateway payment processing library for PHP 5.6+. This package implements WorldPay XML Direct support for Omnipay. ## Installation diff --git a/composer.json b/composer.json index 8ad28a8..7b6621c 100755 --- a/composer.json +++ b/composer.json @@ -16,21 +16,39 @@ { "name": "Dave Nash", "email": "dave.nash@teaandcode.com" + }, + { + "name": "Noel Light-Hilary", + "email": "n.light-hilary@comicrelief.com" } ], "autoload": { "psr-0": { "Omnipay\\WorldPayXML\\" : "src/" } }, "require": { + "php": "^5.6|^7", "guzzle/plugin-cookie": "~3.7", "omnipay/common": "~2.3" }, "require-dev": { "omnipay/tests": "~2.0" }, + "scripts": { + "test": [ + "phpunit", + "@lint" + ], + "lint": [ + "phpcs --standard=PSR2 --encoding=utf-8 src tests" + ] + }, "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } + }, + "config": { + "optimize-autoloader": true, + "sort-packages": true } } diff --git a/src/Omnipay/WorldPayXML/ApplePayCreditCard.php b/src/Omnipay/WorldPayXML/ApplePayCreditCard.php new file mode 100644 index 0000000..6c83eaa --- /dev/null +++ b/src/Omnipay/WorldPayXML/ApplePayCreditCard.php @@ -0,0 +1,20 @@ + '', 'merchant' => '', 'password' => '', 'testMode' => false, - ); + ]; } /** @@ -134,6 +134,28 @@ public function setPaResponse($value) return $this->setParameter('pa_response', $value); } + + /** + * Get the separate username if configured (more secure approach for basic auth) or fallback to merchant if not + * + * @return string + */ + public function getUsername() + { + return $this->parameters->get('username', $this->getParameter('merchant')); + } + + /** + * Set basic auth username + * + * @param string $value + * @return Gateway + */ + public function setUsername($value) + { + return $this->setParameter('username', $value); + } + /** * Get password * @@ -286,10 +308,10 @@ public function setUserIP($value) * @access public * @return \Omnipay\WorldPayXML\Message\PurchaseRequest */ - public function purchase(array $parameters = array()) + public function purchase(array $parameters = []) { return $this->createRequest( - '\Omnipay\WorldPayXML\Message\PurchaseRequest', + \Omnipay\WorldPayXML\Message\PurchaseRequest::class, $parameters ); } diff --git a/src/Omnipay/WorldPayXML/Message/PurchaseRequest.php b/src/Omnipay/WorldPayXML/Message/PurchaseRequest.php index 64f85cc..50c31fb 100755 --- a/src/Omnipay/WorldPayXML/Message/PurchaseRequest.php +++ b/src/Omnipay/WorldPayXML/Message/PurchaseRequest.php @@ -22,15 +22,12 @@ class PurchaseRequest extends AbstractRequest /** * @var \Guzzle\Plugin\Cookie\CookiePlugin - * - * @access protected */ protected $cookiePlugin; /** * Get accept header * - * @access public * @return string */ public function getAcceptHeader() @@ -42,9 +39,7 @@ public function getAcceptHeader() * Set accept header * * @param string $value Accept header - * - * @access public - * @return void + * @return PurchaseRequest */ public function setAcceptHeader($value) { @@ -54,7 +49,6 @@ public function setAcceptHeader($value) /** * Get cookie plugin * - * @access public * @return \Guzzle\Plugin\Cookie\CookiePlugin */ public function getCookiePlugin() @@ -65,7 +59,6 @@ public function getCookiePlugin() /** * Get installation * - * @access public * @return string */ public function getInstallation() @@ -77,9 +70,7 @@ public function getInstallation() * Set installation * * @param string $value Installation - * - * @access public - * @return void + * @return PurchaseRequest */ public function setInstallation($value) { @@ -89,7 +80,6 @@ public function setInstallation($value) /** * Get merchant * - * @access public * @return string */ public function getMerchant() @@ -101,19 +91,37 @@ public function getMerchant() * Set merchant * * @param string $value Merchant - * - * @access public - * @return void + * @return PurchaseRequest */ public function setMerchant($value) { return $this->setParameter('merchant', $value); } + /** + * Get the separate username if configured (more secure approach for basic auth) or fallback to merchant if not + * + * @return string + */ + public function getUsername() + { + return $this->parameters->get('username', $this->getParameter('merchant')); + } + + /** + * Set basic auth username + * + * @param string $value + * @return AbstractRequest + */ + public function setUsername($value) + { + return $this->setParameter('username', $value); + } + /** * Get pa response * - * @access public * @return string */ public function getPaResponse() @@ -125,9 +133,7 @@ public function getPaResponse() * Set pa response * * @param string $value Pa response - * - * @access public - * @return void + * @return PurchaseRequest */ public function setPaResponse($value) { @@ -137,7 +143,6 @@ public function setPaResponse($value) /** * Get password * - * @access public * @return string */ public function getPassword() @@ -149,9 +154,7 @@ public function getPassword() * Set password * * @param string $value Password - * - * @access public - * @return void + * @return PurchaseRequest */ public function setPassword($value) { @@ -161,7 +164,6 @@ public function setPassword($value) /** * Get redirect cookie * - * @access public * @return string */ public function getRedirectCookie() @@ -173,9 +175,7 @@ public function getRedirectCookie() * Set redirect cookie * * @param string $value Password - * - * @access public - * @return void + * @return PurchaseRequest */ public function setRedirectCookie($value) { @@ -185,7 +185,6 @@ public function setRedirectCookie($value) /** * Get redirect echo * - * @access public * @return string */ public function getRedirectEcho() @@ -197,9 +196,7 @@ public function getRedirectEcho() * Set redirect echo * * @param string $value Password - * - * @access public - * @return void + * @return PurchaseRequest */ public function setRedirectEcho($value) { @@ -209,7 +206,6 @@ public function setRedirectEcho($value) /** * Get session * - * @access public * @return string */ public function getSession() @@ -221,9 +217,7 @@ public function getSession() * Set session * * @param string $value Session - * - * @access public - * @return void + * @return PurchaseRequest */ public function setSession($value) { @@ -233,7 +227,6 @@ public function setSession($value) /** * Get term url * - * @access public * @return string */ public function getTermUrl() @@ -245,9 +238,7 @@ public function getTermUrl() * Set term url * * @param string $value Term url - * - * @access public - * @return void + * @return PurchaseRequest */ public function setTermUrl($value) { @@ -257,7 +248,6 @@ public function setTermUrl($value) /** * Get user agent header * - * @access public * @return string */ public function getUserAgentHeader() @@ -269,25 +259,42 @@ public function getUserAgentHeader() * Set user agent header * * @param string $value User agent header - * - * @access public - * @return void + * @return PurchaseRequest */ public function setUserAgentHeader($value) { return $this->setParameter('userAgentHeader', $value); } + /** + * @param $appleToken + * @return PurchaseRequest + */ + public function setAppleToken($appleToken) + { + return $this->setParameter('appleToken', $appleToken); + } + + /** + * @return array + */ + public function getAppleToken() + { + return $this->getParameter('appleToken'); + } + /** * Get data * - * @access public * @return \SimpleXMLElement */ public function getData() { $this->validate('amount', 'card'); - $this->getCard()->validate(); + + if (!$this->getAppleToken()) { + $this->getCard()->validate(); + } // Else for Apple Pay, we use a dummy 'card' with partial metadata, which won't validate. $data = new \SimpleXMLElement(''); $data->addAttribute('version', self::VERSION); @@ -297,7 +304,8 @@ public function getData() $order->addAttribute('orderCode', $this->getTransactionId()); $order->addAttribute('installationId', $this->getInstallation()); - $order->addChild('description', $this->getDescription()); + $description = $this->getDescription() ? $this->getDescription() : 'Merchandise'; + $order->addChild('description', $description); $amount = $order->addChild('amount'); $amount->addAttribute('value', $this->getAmountInteger()); @@ -306,7 +314,7 @@ public function getData() $payment = $order->addChild('paymentDetails'); - $codes = array( + $codes = [ CreditCard::BRAND_AMEX => 'AMEX-SSL', CreditCard::BRAND_DANKORT => 'DANKORT-SSL', CreditCard::BRAND_DINERS_CLUB => 'DINERS-SSL', @@ -317,38 +325,60 @@ public function getData() CreditCard::BRAND_MASTERCARD => 'ECMC-SSL', CreditCard::BRAND_SWITCH => 'MAESTRO-SSL', CreditCard::BRAND_VISA => 'VISA-SSL' - ); + ]; - $card = $payment->addChild($codes[$this->getCard()->getBrand()]); - $card->addChild('cardNumber', $this->getCard()->getNumber()); + if ($this->getCard()->getBrand() === 'apple') { + // With Apple Pay we have no card number so can't pattern match via getBrand() + $card = $payment->addChild('APPLEPAY-SSL'); - $expiry = $card->addChild('expiryDate')->addChild('date'); - $expiry->addAttribute('month', $this->getCard()->getExpiryDate('m')); - $expiry->addAttribute('year', $this->getCard()->getExpiryDate('Y')); + $appleData = $this->getAppleToken()['paymentData']; - $card->addChild('cardHolderName', $this->getCard()->getName()); + $header = $card->addChild('header'); - if ( - $this->getCard()->getBrand() == CreditCard::BRAND_MAESTRO - || $this->getCard()->getBrand() == CreditCard::BRAND_SWITCH - ) { - $start = $card->addChild('startDate')->addChild('date'); - $start->addAttribute('month', $this->getCard()->getStartDate('m')); - $start->addAttribute('year', $this->getCard()->getStartDate('Y')); + $header->addChild('ephemeralPublicKey', $appleData['header']['ephemeralPublicKey']); + $header->addChild('publicKeyHash', $appleData['header']['publicKeyHash']); + $header->addChild('transactionId', $appleData['header']['transactionId']); + if (isset($appleData['header']['applicationData'])) { + $header->addChild('applicationData', $appleData['header']['applicationData']); + } - $card->addChild('issueNumber', $this->getCard()->getIssueNumber()); - } + $card->addChild('signature', $appleData['signature']); + $card->addChild('version', $appleData['version']); + $card->addChild('data', $appleData['data']); + } else { + $card = $payment->addChild($codes[$this->getCard()->getBrand()]); + $card->addChild('cardNumber', $this->getCard()->getNumber()); - $card->addChild('cvc', $this->getCard()->getCvv()); + $expiry = $card->addChild('expiryDate')->addChild('date'); + $expiry->addAttribute('month', $this->getCard()->getExpiryDate('m')); + $expiry->addAttribute('year', $this->getCard()->getExpiryDate('Y')); - $address = $card->addChild('cardAddress')->addChild('address'); - $address->addChild('street', $this->getCard()->getAddress1()); - $address->addChild('postalCode', $this->getCard()->getPostcode()); - $address->addChild('countryCode', $this->getCard()->getCountry()); + $card->addChild('cardHolderName', $this->getCard()->getName()); - $session = $payment->addChild('session'); - $session->addAttribute('shopperIPAddress', $this->getClientIP()); - $session->addAttribute('id', $this->getSession()); + if ( + $this->getCard()->getBrand() == CreditCard::BRAND_MAESTRO + || $this->getCard()->getBrand() == CreditCard::BRAND_SWITCH + ) { + $start = $card->addChild('startDate')->addChild('date'); + $start->addAttribute('month', $this->getCard()->getStartDate('m')); + $start->addAttribute('year', $this->getCard()->getStartDate('Y')); + + $card->addChild('issueNumber', $this->getCard()->getIssueNumber()); + } + + $card->addChild('cvc', $this->getCard()->getCvv()); + + $address = $card->addChild('cardAddress')->addChild('address'); + $address->addChild('street', $this->getCard()->getAddress1()); + $address->addChild('postalCode', $this->getCard()->getPostcode()); + $address->addChild('countryCode', $this->getCard()->getCountry()); + + $session = $payment->addChild('session'); // Empty tag is valid but setting an empty ID attr isn't + if ($this->getClientIp() && $this->getSession()) { + $session->addAttribute('shopperIPAddress', $this->getClientIP()); + $session->addAttribute('id', $this->getSession()); + } + } $paResponse = $this->getPaResponse(); @@ -385,8 +415,6 @@ public function getData() * Send data * * @param \SimpleXMLElement $data Data - * - * @access public * @return RedirectResponse */ public function sendData($data) @@ -406,13 +434,13 @@ public function sendData($data) $document->appendChild($node); $authorisation = base64_encode( - $this->getMerchant() . ':' . $this->getPassword() + $this->getUsername() . ':' . $this->getPassword() ); - $headers = array( + $headers = [ 'Authorization' => 'Basic ' . $authorisation, 'Content-Type' => 'text/xml; charset=utf-8' - ); + ]; $cookieJar = new ArrayCookieJar(); @@ -422,14 +450,12 @@ public function sendData($data) $url = parse_url($this->getEndpoint()); $cookieJar->add( - new Cookie( - array( - 'domain' => $url['host'], - 'name' => 'machine', - 'path' => '/', - 'value' => $redirectCookie - ) - ) + new Cookie([ + 'domain' => $url['host'], + 'name' => 'machine', + 'path' => '/', + 'value' => $redirectCookie + ]) ); } @@ -443,10 +469,13 @@ public function sendData($data) ->post($this->getEndpoint(), $headers, $xml) ->send(); - return $this->response = new RedirectResponse( - $this, - $httpResponse->getBody() - ); + if ($this->getCard()->getBrand() === 'apple') { + $this->response = new Response($this, $httpResponse->getBody()); + } else { + $this->response = new RedirectResponse($this, $httpResponse->getBody()); + } + + return $this->response; } /** @@ -454,7 +483,6 @@ public function sendData($data) * * Returns endpoint depending on test mode * - * @access protected * @return string */ protected function getEndpoint() diff --git a/src/Omnipay/WorldPayXML/Message/RedirectResponse.php b/src/Omnipay/WorldPayXML/Message/RedirectResponse.php index 2d98c38..36690fe 100755 --- a/src/Omnipay/WorldPayXML/Message/RedirectResponse.php +++ b/src/Omnipay/WorldPayXML/Message/RedirectResponse.php @@ -47,10 +47,10 @@ public function getRedirectEcho() */ public function getRedirectData() { - return array( + return [ 'PaReq' => $this->data->requestInfo->request3DSecure->paRequest, 'TermUrl' => $this->request->getTermUrl() - ); + ]; } /** diff --git a/src/Omnipay/WorldPayXML/Message/Response.php b/src/Omnipay/WorldPayXML/Message/Response.php index 0cfb834..8012327 100755 --- a/src/Omnipay/WorldPayXML/Message/Response.php +++ b/src/Omnipay/WorldPayXML/Message/Response.php @@ -12,13 +12,21 @@ */ class Response extends AbstractResponse { + /** @var string */ + const PAYMENT_STATUS_AUTHORISED = 'AUTHORISED'; + /** @var string */ + const PAYMENT_STATUS_CAPTURED = 'CAPTURED'; + /** @var string */ + const PAYMENT_STATUS_SETTLED_BY_MERCHANT = 'SETTLED_BY_MERCHANT'; + /** @var string */ + const PAYMENT_STATUS_SENT_FOR_AUTHORISATION = 'SENT_FOR_AUTHORISATION'; + /** @var string */ + const PAYMENT_STATUS_CANCELLED = 'CANCELLED'; + /** - * Constructor - * * @param RequestInterface $request Request * @param string $data Data - * - * @access public + * @throws InvalidResponseException if non-XML response received */ public function __construct(RequestInterface $request, $data) { @@ -29,11 +37,17 @@ public function __construct(RequestInterface $request, $data) } $responseDom = new DOMDocument; - $responseDom->loadXML($data); + if (!@$responseDom->loadXML($data)) { + throw new InvalidResponseException('Non-XML notification response received'); + } - $this->data = simplexml_import_dom( - $responseDom->documentElement->firstChild->firstChild + $this->data = @simplexml_import_dom( + $responseDom->documentElement->firstChild // or ); + + if (empty($this->data)) { + throw new InvalidResponseException('Could not import response XML: ' . $data); + } } /** @@ -44,7 +58,7 @@ public function __construct(RequestInterface $request, $data) */ public function getMessage() { - $codes = array( + $codes = [ 0 => 'AUTHORISED', 2 => 'REFERRED', 3 => 'INVALID ACCEPTOR', @@ -90,75 +104,120 @@ public function getMessage() 92 => 'CREDITCARD TYPE NOT PROCESSED BY ACQUIRER', 94 => 'DUPLICATE REQUEST ERROR', 97 => 'SECURITY BREACH' - ); - - $message = 'PENDING'; + ]; if (isset($this->data->error)) { - $message = 'ERROR: ' . $this->data->error; + return 'ERROR: ' . $this->data->error; // Cast to string to get CDATA content } - if (isset($this->data->payment->ISO8583ReturnCode)) { - $returnCode = $this->data->payment->ISO8583ReturnCode->attributes(); + $payment = $this->getOrder()->payment; + if (isset($payment->ISO8583ReturnCode)) { + $returnCode = $payment->ISO8583ReturnCode->attributes(); foreach ($returnCode as $name => $value) { if ($name == 'code') { - $message = $codes[intval($value)]; + return $codes[intval($value)]; } } } if ($this->isSuccessful()) { - $message = $codes[0]; + return $codes[0]; } - return $message; + return 'PENDING'; } /** - * Get transaction reference + * Get transaction reference provided with order (the ID in Omnipay parlance), and sent back with notifications. * - * @access public - * @return string + * @return string|null */ - public function getTransactionReference() + public function getTransactionId() { - $attributes = $this->data->attributes(); + if (empty($this->getOrder())) { + return null; + } + + $attributes = $this->getOrder()->attributes(); if (isset($attributes['orderCode'])) { return $attributes['orderCode']; } + + return null; } /** - * Get is redirect + * Get *your* reference a.k.a Omnipay transaction ID (!) * - * @access public - * @return boolean + * @return string + * @deprecated This was named inconsistently with other Omnipay adapters. Use getTransactionId(), whose name + * reflects the actual contents returned. */ - public function isRedirect() + public function getTransactionReference() { - if (isset($this->data->requestInfo->request3DSecure->issuerURL)) { - return true; + return $this->getTransactionId(); + } + + /** + * @return string|null + */ + public function getErrorCode() + { + if (!isset($this->data->error) || empty($this->data->error->attributes()['code'])) { + return null; + } + + return (string) $this->data->error->attributes()['code']; + } + + /** + * @return null|\SimpleXMLElement + */ + public function getOrder() + { + if (isset($this->data->orderStatusEvent)) { + return $this->data->orderStatusEvent; // Notifications + } + + if (isset($this->data->orderStatus)) { + return $this->data->orderStatus; // Order responses } - return false; + return null; } /** - * Get is successful + * Get is redirect * * @access public * @return boolean */ + public function isRedirect() + { + return isset($this->data->requestInfo->request3DSecure->issuerURL); + } + + /** + * Whether transaction's last state indicates success + * + * @return bool + */ public function isSuccessful() { - if (isset($this->data->payment->lastEvent)) { - if (strtoupper($this->data->payment->lastEvent) == 'AUTHORISED') { - return true; - } + if (!isset($this->getOrder()->payment->lastEvent)) { + return false; } - return false; + return in_array( + strtoupper($this->getOrder()->payment->lastEvent), + [ + self::PAYMENT_STATUS_AUTHORISED, + self::PAYMENT_STATUS_CAPTURED, + self::PAYMENT_STATUS_SETTLED_BY_MERCHANT, + ], + true + ); } } diff --git a/tests/Omnipay/WorldPayXML/GatewayTest.php b/tests/Omnipay/WorldPayXML/GatewayTest.php index 95980f1..403b5fc 100755 --- a/tests/Omnipay/WorldPayXML/GatewayTest.php +++ b/tests/Omnipay/WorldPayXML/GatewayTest.php @@ -7,6 +7,13 @@ class GatewayTest extends GatewayTestCase { + /** @var Gateway */ + protected $gateway; + /** @var array */ + private $parameters; + /** @var CreditCard */ + private $card; + public function setUp() { parent::setUp(); @@ -15,28 +22,34 @@ public function setUp() $this->getHttpClient(), $this->getHttpRequest() ); + $this->gateway->setMerchant('ACMECO'); + $this->gateway->setTestMode(true); + $this->gateway->setInstallation('ABC123'); - $this->options = array( + $this->parameters = [ 'amount' => '10.00', - 'card' => new CreditCard( - array( - 'firstName' => 'Example', - 'lastName' => 'User', - 'number' => '4111111111111111', - 'expiryMonth' => '12', - 'expiryYear' => '2016', - 'cvv' => '123', - ) - ), + 'card' => new CreditCard([ + 'firstName' => 'Example', + 'lastName' => 'User', + 'number' => '4111111111111111', + 'expiryMonth' => '12', + 'expiryYear' => '2026', + 'cvv' => '123', + ]), 'transactionId' => 'T0211010', - ); + ]; } public function testPurchaseSuccess() { $this->setMockHttpResponse('PurchaseSuccess.txt'); - $response = $this->gateway->purchase($this->options)->send(); + $purchase = $this->gateway->purchase($this->parameters); + + // Confirm basic auth uses merchant code to authenticate when there's no username. + $this->assertEquals('ACMECO', $purchase->getUsername()); + + $response = $purchase->send(); $this->assertTrue($response->isSuccessful()); $this->assertEquals('T0211010', $response->getTransactionReference()); @@ -46,9 +59,62 @@ public function testPurchaseError() { $this->setMockHttpResponse('PurchaseFailure.txt'); - $response = $this->gateway->purchase($this->options)->send(); + $response = $this->gateway->purchase($this->parameters)->send(); $this->assertFalse($response->isSuccessful()); $this->assertSame('CARD EXPIRED', $response->getMessage()); } + + public function testApplePaySuccess() + { + $card = new ApplePayCreditCard(); + + $this->assertEquals('apple', $card->getBrand()); + + $applePayOptions = [ + 'amount' => '10.00', + 'card' => new ApplePayCreditCard(), + 'appleToken' => [ + 'transactionIdentifier' => '5394..00', + 'paymentData' => [ + 'data' => 'kdHd..GQ==', + 'signature' => 'MIAGCSqGSIb3DQEH...AAA', + 'version' => 'EC_v1', + 'header' => [ + 'applicationData' => '94ee0..C2', + 'ephemeralPublicKey' => 'MFkwE..Q==', + 'publicKeyHash' => 'dxCK..6o=', + 'transactionId' => 'd3b28af..f8', + ], + ], + ], + 'transactionId' => 'T0211010', + ]; + + $this->setMockHttpResponse('PurchaseSuccessApplePay.txt'); + + $purchase = $this->gateway->purchase($applePayOptions); + + // Confirm basic auth uses merchant code to authenticate when there's no username. + $this->assertEquals('ACMECO', $purchase->getUsername()); + + $response = $purchase->send(); + + $this->assertTrue($response->isSuccessful()); + $this->assertEquals('T0211011', $response->getTransactionReference()); + } + + /** + * Confirm basic auth uses a username when set rather than merchant code. + */ + public function testUsernameAuthSetup() + { + $gatewayWithUsername = clone $this->gateway; + $gatewayWithUsername->setUsername('MYSECRETUSERNAME987'); + + $purchase = $gatewayWithUsername->purchase($this->parameters); + $purchase->setCard($this->card); + + $this->assertEquals('MYSECRETUSERNAME987', $purchase->getUsername()); + } } diff --git a/tests/Omnipay/WorldPayXML/Message/PurchaseRequestTest.php b/tests/Omnipay/WorldPayXML/Message/PurchaseRequestTest.php new file mode 100644 index 0000000..6997539 --- /dev/null +++ b/tests/Omnipay/WorldPayXML/Message/PurchaseRequestTest.php @@ -0,0 +1,95 @@ +purchase = new PurchaseRequest($this->getHttpClient(), $this->getHttpRequest()); + $this->purchase->setAmount(7.45); + $this->purchase->setCurrency('GBP'); + } + + public function testDirectCardPayload() + { + $this->purchase->setCard(new CreditCard([ + 'billingFirstName' => 'Vince', + 'billingLastName' => 'Staples', + 'shippingFirstName' => 'Vince', + 'shippingLastName' => 'Staples', + 'email' => 'cr+vs@noellh.com', + 'address1' => '745 THORNBURY CLOSE', + 'address2' => '', + 'city' => 'LONDON', + 'country' => 'GB', + 'postcode' => 'N16 8UX', + 'number' => '4111111111111111', + 'expiryMonth' => '12', + 'expiryYear' => '2026', + 'cvv' => '123', + ])); + + $data = $this->purchase->getData(); + + $this->assertEquals('745', $data->submit->order->amount->attributes()['value']); + $this->assertEquals('GBP', $data->submit->order->amount->attributes()['currencyCode']); + $this->assertEquals('2', $data->submit->order->amount->attributes()['exponent']); + + $visaDetails = $data->submit->order->paymentDetails->{'VISA-SSL'}; + $this->assertEquals('745 THORNBURY CLOSE', $visaDetails->cardAddress->address->street); + $this->assertEquals('N16 8UX', $visaDetails->cardAddress->address->postalCode); + $this->assertEquals('GB', $visaDetails->cardAddress->address->countryCode); + + $this->assertEquals('4111111111111111', $visaDetails->cardNumber); + $this->assertEquals('12', $visaDetails->expiryDate->date->attributes()['month']); + $this->assertEquals('2026', $visaDetails->expiryDate->date->attributes()['year']); + $this->assertEquals('123', $visaDetails->cvc); + } + + public function testApplePayPayload() + { + $this->purchase->setCard(new ApplePayCreditCard()); + $this->purchase->setAppleToken([ + 'transactionIdentifier' => '5394..00', + 'paymentData' => [ + 'data' => 'kdHd..GQ==', + 'signature' => 'MIAGCSqGSIb3DQEH...AAA', + 'version' => 'EC_v1', + 'header' => [ + 'applicationData' => '94ee0..C2', + 'ephemeralPublicKey' => 'MFkwE..Q==', + 'publicKeyHash' => 'dxCK..6o=', + 'transactionId' => 'd3b28af..f8', + ], + ], + ]); + + $data = $this->purchase->getData(); + + $this->assertEquals('745', $data->submit->order->amount->attributes()['value']); + $this->assertEquals('GBP', $data->submit->order->amount->attributes()['currencyCode']); + $this->assertEquals('2', $data->submit->order->amount->attributes()['exponent']); + + $appleDetails = $data->submit->order->paymentDetails->{'APPLEPAY-SSL'}; + $this->assertEquals('kdHd..GQ==', $appleDetails->data); + $this->assertEquals('94ee0..C2', $appleDetails->header->applicationData); + $this->assertEquals('MFkwE..Q==', $appleDetails->header->ephemeralPublicKey); + $this->assertEquals('dxCK..6o=', $appleDetails->header->publicKeyHash); + $this->assertEquals('d3b28af..f8', $appleDetails->header->transactionId); + $this->assertEquals('MIAGCSqGSIb3DQEH...AAA', $appleDetails->signature); + $this->assertEquals('EC_v1', $appleDetails->version); + + $this->assertEmpty($data->submit->order->billingAddress); + } +} diff --git a/tests/Omnipay/WorldPayXML/Message/ResponseTest.php b/tests/Omnipay/WorldPayXML/Message/ResponseTest.php index 3e98db8..71b7770 100755 --- a/tests/Omnipay/WorldPayXML/Message/ResponseTest.php +++ b/tests/Omnipay/WorldPayXML/Message/ResponseTest.php @@ -7,7 +7,7 @@ class ResponseTest extends TestCase { /** - * @expectedException Omnipay\Common\Exception\InvalidResponseException + * @expectedException \Omnipay\Common\Exception\InvalidResponseException */ public function testConstructEmpty() { @@ -28,6 +28,20 @@ public function testPurchaseSuccess() $this->assertEquals('AUTHORISED', $response->getMessage()); } + public function testPurchaseSuccessApplePay() + { + $httpResponse = $this->getMockHttpResponse('PurchaseSuccessApplePay.txt'); + $response = new Response( + $this->getMockRequest(), + $httpResponse->getBody() + ); + + $this->assertTrue($response->isSuccessful()); + $this->assertFalse($response->isRedirect()); + $this->assertEquals('T0211011', $response->getTransactionReference()); + $this->assertEquals('AUTHORISED', $response->getMessage()); + } + public function testPurchaseFailure() { $httpResponse = $this->getMockHttpResponse('PurchaseFailure.txt'); @@ -41,4 +55,66 @@ public function testPurchaseFailure() $this->assertEquals('T0211234', $response->getTransactionReference()); $this->assertSame('CARD EXPIRED', $response->getMessage()); } + + public function testPurchaseErrorGeneric() + { + $httpResponse = $this->getMockHttpResponse('PurchaseErrorGeneric.txt'); + + $response = new Response( + $this->getMockRequest(), + $httpResponse->getBody() + ); + + $this->assertFalse($response->isSuccessful()); + $this->assertFalse($response->isRedirect()); + $this->assertSame('ERROR: Nasty internal error!', $response->getMessage()); + $this->assertNull($response->getErrorCode()); + } + + public function testPurchaseErrorDuplicateOrder() + { + $httpResponse = $this->getMockHttpResponse('PurchaseErrorDuplicateOrder.txt'); + + $response = new Response( + $this->getMockRequest(), + $httpResponse->getBody() + ); + + $this->assertFalse($response->isSuccessful()); + $this->assertFalse($response->isRedirect()); + $this->assertSame('ERROR: Duplicate Order', $response->getMessage()); + $this->assertSame('5', $response->getErrorCode()); + } + + /** + * You can get this e.g. if you are authenticated but your merchant code in the body is wrong. + */ + public function testPurchaseErrorSecurityFailure() + { + $httpResponse = $this->getMockHttpResponse('PurchaseErrorSecurityViolation.txt'); + + $response = new Response( + $this->getMockRequest(), + $httpResponse->getBody() + ); + + $this->assertFalse($response->isSuccessful()); + $this->assertFalse($response->isRedirect()); + $this->assertSame('ERROR: Security violation', $response->getMessage()); + $this->assertSame('4', $response->getErrorCode()); + } + + /** + * @expectedException \Omnipay\Common\Exception\InvalidResponseException + * @expectedExceptionMessage Could not import response XML: + */ + public function testPurchaseUnauthenticated() + { + $httpResponse = $this->getMockHttpResponse('PurchaseUnauthenticated.txt'); + + new Response( + $this->getMockRequest(), + $httpResponse->getBody() + ); + } } diff --git a/tests/Omnipay/WorldPayXML/Mock/PurchaseErrorDuplicateOrder.txt b/tests/Omnipay/WorldPayXML/Mock/PurchaseErrorDuplicateOrder.txt new file mode 100755 index 0000000..1738ea6 --- /dev/null +++ b/tests/Omnipay/WorldPayXML/Mock/PurchaseErrorDuplicateOrder.txt @@ -0,0 +1,11 @@ +HTTP/1.1 200 OK +Connection: close +Server: VPS-3.033.00 +Date: Sat, 23 Feb 2013 05:17:32 GMT +Content-type: text/xml +Content-length: 341 + + + + diff --git a/tests/Omnipay/WorldPayXML/Mock/PurchaseErrorGeneric.txt b/tests/Omnipay/WorldPayXML/Mock/PurchaseErrorGeneric.txt new file mode 100755 index 0000000..fa1641f --- /dev/null +++ b/tests/Omnipay/WorldPayXML/Mock/PurchaseErrorGeneric.txt @@ -0,0 +1,11 @@ +HTTP/1.1 200 OK +Connection: close +Server: VPS-3.033.00 +Date: Sat, 23 Feb 2013 05:17:32 GMT +Content-type: text/xml +Content-length: 376 + + + +Nasty internal error! diff --git a/tests/Omnipay/WorldPayXML/Mock/PurchaseErrorSecurityViolation.txt b/tests/Omnipay/WorldPayXML/Mock/PurchaseErrorSecurityViolation.txt new file mode 100755 index 0000000..93fa6c2 --- /dev/null +++ b/tests/Omnipay/WorldPayXML/Mock/PurchaseErrorSecurityViolation.txt @@ -0,0 +1,11 @@ +HTTP/1.1 200 OK +Connection: close +Server: VPS-3.033.00 +Date: Sat, 23 Feb 2013 05:17:32 GMT +Content-type: text/xml +Content-length: 344 + + + + diff --git a/tests/Omnipay/WorldPayXML/Mock/PurchaseSuccessApplePay.txt b/tests/Omnipay/WorldPayXML/Mock/PurchaseSuccessApplePay.txt new file mode 100755 index 0000000..a6c1b74 --- /dev/null +++ b/tests/Omnipay/WorldPayXML/Mock/PurchaseSuccessApplePay.txt @@ -0,0 +1,11 @@ +HTTP/1.1 200 OK +Connection: close +Server: VPS-3.033.00 +Date: Sat, 23 Feb 2013 05:17:32 GMT +Content-type: text/xml +Content-length: 721 + + + +APPLEPAY-SSL AUTHORISED \ No newline at end of file diff --git a/tests/Omnipay/WorldPayXML/Mock/PurchaseUnauthenticated.txt b/tests/Omnipay/WorldPayXML/Mock/PurchaseUnauthenticated.txt new file mode 100755 index 0000000..3f70a89 --- /dev/null +++ b/tests/Omnipay/WorldPayXML/Mock/PurchaseUnauthenticated.txt @@ -0,0 +1,28 @@ +HTTP/1.1 401 Unauthorized +Connection: close +Server: VPS-3.033.00 +Date: Sat, 23 Feb 2013 05:17:32 GMT +Content-type: text/html +Content-length: 767 + + + + + + 401: Requires Authentication + + + + +
+ Requires Authentication +
Status: 401
+

+ This request requires HTTP authentication. + +
+

+
+ +