├── .gitignore ├── docs └── authorizenet-default-payment-form.png ├── src ├── Message │ ├── PurchaseRequest.php │ ├── HostedPage │ │ ├── PurchaseRequest.php │ │ ├── Response.php │ │ └── AuthorizeRequest.php │ ├── VoidRequest.php │ ├── Response.php │ ├── FetchTransactionRequest.php │ ├── FetchTransactionResponse.php │ ├── RefundRequest.php │ ├── CaptureRequest.php │ ├── AuthorizeResponse.php │ ├── AbstractResponse.php │ ├── AbstractRequest.php │ ├── AcceptNotification.php │ └── AuthorizeRequest.php ├── Traits │ ├── HasHostedPageGatewayParams.php │ └── HasGatewayParams.php ├── AbstractGateway.php ├── ApiGateway.php └── HostedPageGateway.php ├── tests ├── ApiGatewayTest.php ├── HostedPageGatewayTest.php ├── Mock │ └── AcceptNotificationSuccess.txt └── Message │ ├── AcceptNotificationTest.php │ └── AuthorizeRequestTest.php ├── phpunit.xml.dist ├── .travis.yml ├── LICENSE ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | composer.phar 4 | phpunit.xml 5 | .idea 6 | -------------------------------------------------------------------------------- /docs/authorizenet-default-payment-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/academe/omnipay-authorizenetapi/HEAD/docs/authorizenet-default-payment-form.png -------------------------------------------------------------------------------- /src/Message/PurchaseRequest.php: -------------------------------------------------------------------------------- 1 | gateway = new ApiGateway( 14 | $this->getHttpClient(), 15 | $this->getHttpRequest() 16 | ); 17 | 18 | $this->gateway->setAuthName('authName'); 19 | $this->gateway->setTransactionKey('transactionKey'); 20 | $this->gateway->setRefId('refId'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Message/HostedPage/PurchaseRequest.php: -------------------------------------------------------------------------------- 1 | gateway = new ApiGateway( 14 | $this->getHttpClient(), 15 | $this->getHttpRequest() 16 | ); 17 | 18 | $this->gateway->setAuthName('authName'); 19 | $this->gateway->setTransactionKey('transactionKey'); 20 | $this->gateway->setRefId('refId'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Traits/HasHostedPageGatewayParams.php: -------------------------------------------------------------------------------- 1 | setParameter('cancelUrl', $value); 17 | } 18 | 19 | /** 20 | * Used only by the hosted payment page at this time. 21 | */ 22 | public function setReturnUrl($value) 23 | { 24 | $this->setParameter('returnUrl', $value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | 21 | 22 | src 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.0 5 | - 7.1 6 | - 7.2 7 | - 7.3 8 | 9 | # This triggers builds to run on the new TravisCI infrastructure. 10 | # See: http://docs.travis-ci.com/user/workers/container-based-infrastructure/ 11 | sudo: false 12 | 13 | ## Cache composer 14 | cache: 15 | directories: 16 | - $HOME/.composer/cache 17 | 18 | env: 19 | global: 20 | - setup=basic 21 | 22 | matrix: 23 | include: 24 | 25 | install: 26 | - if [[ $setup = 'basic' ]]; then travis_retry composer install --prefer-dist --no-interaction; fi 27 | - if [[ $setup = 'lowest' ]]; then travis_retry composer update --prefer-dist --no-interaction --prefer-lowest --prefer-stable; fi 28 | 29 | script: vendor/bin/phpcs --standard=PSR2 src && vendor/bin/phpunit --coverage-text 30 | -------------------------------------------------------------------------------- /tests/Mock/AcceptNotificationSuccess.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Date: Sat, 16 Feb 2013 06:39:41 GMT 3 | X-Opnet-Transaction-Trace: 2b1726b9-2b7a-49f7-a5c6-24cd2622000b-8580-2210 4 | Content-Type: application/json 5 | X-Anet-Signature: sha512=13151697A33C77CB102AAA060CD58FB2394696BAEA981047A31830A8A1853A4C8D454E696871A38A68B6940B8746FC096111334D584579F6792F568A622A0373 6 | Content-Length: 340 7 | Connection: close 8 | X-Accel-Internal: /internal-nginx-static-location 9 | X-Forwarded-For: 198.241.207.38 10 | X-Real-Ip: 198.241.207.38 11 | Host: omnipay.example.co.uk 12 | 13 | {"notificationId":"701bf27d-d46f-4c3b-82f2-066448e2901e","eventType":"net.authorize.payment.authorization.created","eventDate":"2019-01-31T14:38:42.6937313Z","webhookId":"e6b3764d-5677-4fb1-a929-2e25a02f3073","payload":{"responseCode":1,"authCode":"P1XHLC","avsResponse":"Y","authAmount":7.67,"entityName":"transaction","id":"60116007277"}} 14 | -------------------------------------------------------------------------------- /src/Message/VoidRequest.php: -------------------------------------------------------------------------------- 1 | getTransactionReference(); 20 | 21 | $transaction = new VoidTransaction($refTransId); 22 | 23 | return $transaction; 24 | } 25 | 26 | /** 27 | * Accept a transaction and sends it as a request. 28 | * 29 | * @param $data TransactionRequestInterface 30 | * @returns CaptureResponse 31 | */ 32 | public function sendData($data) 33 | { 34 | $response_data = $this->sendTransaction($data); 35 | 36 | return new Response($this, $response_data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jason Judge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "academe/omnipay-authorizenetapi", 3 | "type": "library", 4 | "description": "Authorize.Net payment gateway driver for the Omnipay 3.x payment processing library", 5 | "keywords": [ 6 | "authorize.net", 7 | "gateway", 8 | "merchant", 9 | "omnipay", 10 | "pay", 11 | "payment" 12 | ], 13 | "homepage": "https://github.com/academe/omnipay-authorizenetapi", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Jason Judge", 18 | "email": "jason@academe.co.uk" 19 | } 20 | ], 21 | "autoload": { 22 | "psr-4": { 23 | "Omnipay\\AuthorizeNetApi\\": "src/" 24 | } 25 | }, 26 | "require": { 27 | "omnipay/common": "^3", 28 | "academe/authorizenet-objects": "~0.7", 29 | "symfony/property-access": "^3.2 || ^4.0" 30 | }, 31 | "require-dev": { 32 | "omnipay/tests": "^3", 33 | "squizlabs/php_codesniffer": "^3" 34 | }, 35 | "extra": { 36 | "branch-alias": { 37 | "dev-master": "3.0.x-dev" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Message/Response.php: -------------------------------------------------------------------------------- 1 | getTransactionCode() ?: parent::getCode(); 29 | } 30 | 31 | /** 32 | * Get the transaction message text if available, falling back 33 | * to the response envelope. 34 | */ 35 | public function getMessage() 36 | { 37 | return $this->getTransactionMessage() ?: parent::getMessage(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Message/FetchTransactionRequest.php: -------------------------------------------------------------------------------- 1 | getAuth(), 20 | $this->getTransactionReference() 21 | ); 22 | 23 | if ($this->getTransactionId()) { 24 | $request = $request->withRefId($this->getTransactionId()); 25 | } 26 | 27 | return $request; 28 | } 29 | 30 | /** 31 | * Accept a transaction and sends it as a request. 32 | * 33 | * @param $data TransactionRequestInterface 34 | * @returns TransactionResponse 35 | */ 36 | public function sendData($data) 37 | { 38 | // Send the request to the gateway. 39 | $response_data = $this->sendMessage($data); 40 | 41 | // We should be getting a transactino back. 42 | return new FetchTransactionResponse($this, $response_data); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Message/FetchTransactionResponse.php: -------------------------------------------------------------------------------- 1 | ). 30 | // So I don't trust the data type we get back, and we will play loose and 31 | // fast with implicit conversions here. 32 | 33 | return $this->getResponseCode() == TransactionResponseModel::RESPONSE_CODE_APPROVED; 34 | } 35 | 36 | /** 37 | * Get the transaction response code. 38 | * Expected values are one of TransactionResponseModel::RESPONSE_CODE_* 39 | */ 40 | public function getResponseCode() 41 | { 42 | return $this->getValue($this->transactionIndex . '.responseCode'); 43 | } 44 | 45 | /** 46 | * Tells us whether the transaction is pending or not. 47 | */ 48 | public function isPending() 49 | { 50 | return $this->getResponseCode() == TransactionResponseModel::RESPONSE_CODE_PENDING; 51 | } 52 | 53 | public function getTransactionType() 54 | { 55 | return $this->getValue($this->transactionIndex . '.transactionType'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Message/RefundRequest.php: -------------------------------------------------------------------------------- 1 | getCard(); 19 | 20 | if ($card) { 21 | // A credit card has been supplied. 22 | 23 | if ($card->getNumber()) { 24 | $creditCard = new CreditCard( 25 | $card->getNumber(), 26 | // Either MMYY or MMYYYY will work. 27 | // (This will be overwritten with 'XXXX' for now) 28 | $card->getExpiryMonth() . $card->getExpiryYear() 29 | ); 30 | 31 | $transaction = $transaction->withPayment($creditCard); 32 | } 33 | } 34 | 35 | // Instead of supplying the full credit card dtails, just 36 | // provide the lasy four digits of the card number. 37 | 38 | if ($this->getNumberLastFour()) { 39 | $creditCard = new CreditCard( 40 | $this->getNumberLastFour(), 41 | 'XXXX' 42 | ); 43 | 44 | $transaction = $transaction->withPayment($creditCard); 45 | } 46 | 47 | return $transaction; 48 | } 49 | 50 | protected function createTransaction(AmountInterface $amount, $refTransId) 51 | { 52 | return new Refund($amount, $refTransId); 53 | } 54 | 55 | /** 56 | * The last four digits of the origonal credit card. 57 | */ 58 | public function getNumberLastFour() 59 | { 60 | return $this->getParameter('numberLastFour'); 61 | } 62 | 63 | public function setNumberLastFour($value) 64 | { 65 | return $this->setParameter('numberLastFour', $value); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/AbstractGateway.php: -------------------------------------------------------------------------------- 1 | null, 26 | // Required. 27 | // The access token assigned to this application. 28 | 'transactionKey' => null, 29 | // Optional. 30 | // Either mobileDeviceId or refId can be provided. 31 | 'mobileDeviceId' => null, 32 | 'refId' => null, 33 | // True to run against the sandbox. 34 | 'testMode' => false, 35 | // The shared key used to sign notifications. 36 | 'signatureKey' => null, 37 | // Set to disable the webhook signature assertions. 38 | 'disableWebhookSignature' => false, 39 | ); 40 | } 41 | 42 | /** 43 | * The capture transaction. 44 | */ 45 | public function capture(array $parameters = []) 46 | { 47 | return $this->createRequest( 48 | \Omnipay\AuthorizeNetApi\Message\CaptureRequest::class, 49 | $parameters 50 | ); 51 | } 52 | 53 | /** 54 | * Fetch a transaction. 55 | */ 56 | public function fetchTransaction(array $parameters = []) 57 | { 58 | return $this->createRequest( 59 | \Omnipay\AuthorizeNetApi\Message\FetchTransactionRequest::class, 60 | $parameters 61 | ); 62 | } 63 | 64 | /** 65 | * Handle notifcation server requests (webhooks). 66 | */ 67 | public function acceptNotification(array $parameters = []) 68 | { 69 | return $this->createRequest( 70 | \Omnipay\AuthorizeNetApi\Message\AcceptNotification::class, 71 | $parameters 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ApiGateway.php: -------------------------------------------------------------------------------- 1 | createRequest( 34 | AuthorizeRequest::class, 35 | $parameters 36 | ); 37 | } 38 | 39 | /** 40 | * The purchase transaction. 41 | */ 42 | public function purchase(array $parameters = []) 43 | { 44 | return $this->createRequest( 45 | PurchaseRequest::class, 46 | $parameters 47 | ); 48 | } 49 | 50 | /** 51 | * Void an authorized transaction. 52 | */ 53 | public function void(array $parameters = []) 54 | { 55 | return $this->createRequest( 56 | VoidRequest::class, 57 | $parameters 58 | ); 59 | } 60 | 61 | /** 62 | * Refund a captured transaction (before it is cleared). 63 | */ 64 | public function refund(array $parameters = []) 65 | { 66 | return $this->createRequest( 67 | RefundRequest::class, 68 | $parameters 69 | ); 70 | } 71 | 72 | /** 73 | * Fetch an existing transaction details. 74 | */ 75 | public function fetchTransaction(array $parameters = []) 76 | { 77 | return $this->createRequest( 78 | FetchTransactionRequest::class, 79 | $parameters 80 | ); 81 | } 82 | 83 | /** 84 | * Accept a notification. 85 | */ 86 | public function acceptNotification(array $parameters = []) 87 | { 88 | return $this->createRequest( 89 | AcceptNotification::class, 90 | $parameters 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Message/CaptureRequest.php: -------------------------------------------------------------------------------- 1 | getCurrency(), 21 | $this->getAmountInteger() 22 | ); 23 | 24 | // Identify the original transaction being authorised. 25 | $refTransId = $this->getTransactionReference(); 26 | 27 | $transaction = $this->createTransaction($amount, $refTransId); 28 | 29 | // The description and invoice number go into an Order object. 30 | if ($this->getInvoiceNumber() || $this->getDescription()) { 31 | $order = new Order( 32 | $this->getInvoiceNumber(), 33 | $this->getDescription() 34 | ); 35 | 36 | $transaction = $transaction->withOrder($order); 37 | } 38 | 39 | $transaction = $transaction->with([ 40 | 'terminalNumber' => $this->getTerminalNumber(), 41 | ]); 42 | 43 | return $transaction; 44 | } 45 | 46 | /** 47 | * Create a new instance of the transaction object. 48 | * 49 | * - PriorAuthCapture is used for transactions authorised through 50 | * the API, e.g. a credit card authorisation. 51 | * - CaptureOnly is used to capture amounts authorized through 52 | * other channels, such as a telephone order (MOTO). 53 | * 54 | * Only the first is supported at this time. Which gets used will 55 | * depend on what data is passed in. 56 | */ 57 | protected function createTransaction(AmountInterface $amount, $refTransId) 58 | { 59 | return new PriorAuthCapture($amount, $refTransId); 60 | } 61 | 62 | /** 63 | * Accept a transaction and sends it as a request. 64 | * 65 | * @param $data TransactionRequestInterface 66 | * @returns CaptureResponse 67 | */ 68 | public function sendData($data) 69 | { 70 | $response_data = $this->sendTransaction($data); 71 | 72 | return new Response($this, $response_data); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/HostedPageGateway.php: -------------------------------------------------------------------------------- 1 | createRequest( 37 | AuthorizeRequest::class, 38 | $parameters 39 | ); 40 | } 41 | 42 | /** 43 | * The purchase transaction, through a hosted page. 44 | */ 45 | public function purchase(array $parameters = array()) 46 | { 47 | return $this->createRequest( 48 | PurchaseRequest::class, 49 | $parameters 50 | ); 51 | } 52 | 53 | /** 54 | * Void an authorized transaction. 55 | */ 56 | public function void(array $parameters = []) 57 | { 58 | return $this->createRequest( 59 | VoidRequest::class, 60 | $parameters 61 | ); 62 | } 63 | 64 | /** 65 | * Refund a captured transaction (before it is cleared). 66 | */ 67 | public function refund(array $parameters = []) 68 | { 69 | return $this->createRequest( 70 | RefundRequest::class, 71 | $parameters 72 | ); 73 | } 74 | 75 | /** 76 | * Fetch an existing transaction details. 77 | */ 78 | public function fetchTransaction(array $parameters = []) 79 | { 80 | return $this->createRequest( 81 | FetchTransactionRequest::class, 82 | $parameters 83 | ); 84 | } 85 | 86 | /** 87 | * Accept a notification. 88 | */ 89 | public function acceptNotification(array $parameters = []) 90 | { 91 | return $this->createRequest( 92 | AcceptNotification::class, 93 | $parameters 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Message/HostedPage/Response.php: -------------------------------------------------------------------------------- 1 | getTestMode()) { 34 | return $this->endpointHostedPageSandbox; 35 | } else { 36 | return $this->endpointHostedPageLive; 37 | } 38 | } 39 | 40 | /** 41 | * Not yet complete, as the hosted payment form still needs to be processed. 42 | */ 43 | public function isSuccessful() 44 | { 45 | return false; 46 | } 47 | 48 | /** 49 | * @returns string The token used to invoke the remote form. 50 | */ 51 | public function getToken() 52 | { 53 | return $this->getValue('token'); 54 | } 55 | 56 | /** 57 | * @returns bool Is a redirect if the request is successful. 58 | */ 59 | public function isRedirect() 60 | { 61 | return $this->responseIsSuccessful(); 62 | } 63 | 64 | /** 65 | * @returns string The redirect method is a POST. 66 | */ 67 | public function getRedirectMethod() 68 | { 69 | return 'POST'; 70 | } 71 | 72 | /** 73 | * @returns array 74 | */ 75 | public function getRedirectData() 76 | { 77 | return [ 78 | 'token' => $this->getToken() 79 | ]; 80 | } 81 | 82 | /** 83 | * @returns array 84 | */ 85 | public function getRedirectUrl() 86 | { 87 | return $this->getEndpoint(); 88 | } 89 | 90 | /** 91 | * Gets the test mode of the original request. 92 | * 93 | * @return boolean 94 | */ 95 | public function getTestMode() 96 | { 97 | return $this->testMode; 98 | } 99 | 100 | /** 101 | * Sets the test mode of the response. 102 | * 103 | * @param boolean $value True for test mode on. 104 | * @return AbstractRequest 105 | */ 106 | public function setTestMode($value) 107 | { 108 | return $this->testMode = $value; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Traits/HasGatewayParams.php: -------------------------------------------------------------------------------- 1 | setParameter('authName', $value); 23 | } 24 | 25 | public function getAuthName() 26 | { 27 | return $this->getParameter('authName'); 28 | } 29 | 30 | /** 31 | * The mobile device ID. 32 | */ 33 | public function setMobileDeviceId($value) 34 | { 35 | if ($value !== null && ! is_string($value)) { 36 | throw new InvalidRequestException('Mobile device ID must be a string.'); 37 | } 38 | 39 | return $this->setParameter('mobileDeviceId', $value); 40 | } 41 | 42 | public function getMobileDeviceId() 43 | { 44 | return $this->getParameter('mobileDeviceId'); 45 | } 46 | 47 | /** 48 | * The ref ID. 49 | */ 50 | public function setRefId($value) 51 | { 52 | if ($value !== null && ! is_string($value)) { 53 | throw new InvalidRequestException('Ref ID must be a string.'); 54 | } 55 | 56 | return $this->setParameter('refId', $value); 57 | } 58 | 59 | public function getRefId() 60 | { 61 | return $this->getParameter('refId'); 62 | } 63 | 64 | /** 65 | * The application auth transaction key. 66 | */ 67 | public function setTransactionKey($value) 68 | { 69 | if (! is_string($value)) { 70 | throw new InvalidRequestException('Transaction Key must be a string.'); 71 | } 72 | 73 | return $this->setParameter('transactionKey', $value); 74 | } 75 | 76 | public function getTransactionKey() 77 | { 78 | return $this->getParameter('transactionKey'); 79 | } 80 | 81 | /** 82 | * The shared signature key.used to sign notifications sent by the 83 | * webhooks in the X-Anet-Signature HTTP header. 84 | * Only needed when receiving a notification. 85 | * Optional; the signature hash will only be checked if the signature 86 | * is supplied. 87 | */ 88 | public function setSignatureKey($value) 89 | { 90 | if ($value !== null && ! is_string($value)) { 91 | throw new InvalidRequestException('Signature Key must be a string.'); 92 | } 93 | 94 | return $this->setParameter('signatureKey', $value); 95 | } 96 | 97 | public function getSignatureKey() 98 | { 99 | return $this->getParameter('signatureKey'); 100 | } 101 | 102 | /** 103 | * @param mixed $value cast to boolean when referenced 104 | */ 105 | public function setDisableWebhookSignature($value) 106 | { 107 | return $this->setParameter('disableWebhookSignature', $value); 108 | } 109 | 110 | public function getDisableWebhookSignature() 111 | { 112 | return $this->getParameter('disableWebhookSignature'); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/Message/AcceptNotificationTest.php: -------------------------------------------------------------------------------- 1 | getHttpRequest(); 22 | 23 | // Omnipay needs a way to quickly set these up. 24 | // 'AcceptNotificationSuccess.txt' 25 | 26 | $payload = '{"notificationId":"701bf27d-d46f-4c3b-82f2-066448e2901e","eventType":"net.authorize.payment.authorization.created","eventDate":"2019-01-31T14:38:42.6937313Z","webhookId":"e6b3764d-5677-4fb1-a929-2e25a02f3073","payload":{"responseCode":1,"authCode":"P1XHLC","avsResponse":"Y","authAmount":7.67,"entityName":"transaction","id":"60116007277"}}'; 27 | 28 | // Should actually be a POST, but can't work out how to do that. 29 | // Send headers through server parameter. 30 | 31 | $httpRequest->initialize( 32 | [], 33 | [], 34 | [], 35 | [], 36 | [], 37 | [ 38 | 'HTTP_X-Anet-Signature' => 'sha512=13151697A33C77CB102AAA060CD58FB2394696BAEA981047A31830A8A1853A4C8D454E696871A38A68B6940B8746FC096111334D584579F6792F568A622A0373', 39 | 'HTTP_Content-Type' => 'application/json', 40 | ], 41 | $payload 42 | ); 43 | //var_dump((string)$httpRequest); 44 | 45 | $this->request = new AcceptNotification( 46 | $this->getHttpClient(), 47 | $httpRequest 48 | ); 49 | //var_dump($this->request); 50 | 51 | $this->request->initialize([ 52 | 'signatureKey' => '339E42F5D962293A925C244313E8C3546FD765AD202BFF3805F4C8459193E2AA1C106F476D6907C7A85714CCD69BB7BF184D17ECCDD546CED7EF69DB4C4AD723', 53 | ]); 54 | } 55 | 56 | public function testSuccess() 57 | { 58 | $this->assertSame('60116007277', $this->request->getTransactionReference()); 59 | 60 | $this->assertSame('completed', $this->request->getTransactionStatus()); 61 | $this->assertSame('', $this->request->getMessage()); 62 | 63 | $this->assertSame('payment', $this->request->getEventTarget()); 64 | $this->assertSame('authorization', $this->request->getEventSubTarget()); 65 | $this->assertSame('created', $this->request->getEventAction()); 66 | 67 | $this->assertTrue($this->request->isSignatureValid()); 68 | $this->assertNull($this->request->assertSignature()); 69 | } 70 | 71 | /** 72 | * @expectedException \Omnipay\Common\Exception\InvalidRequestException 73 | * @expectedExceptionMessage Invalid or missing signature 74 | */ 75 | public function testInvalidSignature() 76 | { 77 | // Mix up the key. 78 | $this->request->setSignatureKey(str_shuffle($this->request->getSignatureKey())); 79 | 80 | $this->assertFalse($this->request->isSignatureValid()); 81 | $this->request->assertSignature(); 82 | } 83 | 84 | /** 85 | * Signature assertion can be suppressed - it still shows as 86 | * invalid if the application checks, but does not thrown an exception. 87 | */ 88 | public function testInvalidSignatureSuppressed() 89 | { 90 | // Mix up the key. 91 | $this->request->setSignatureKey(str_shuffle($this->request->getSignatureKey())); 92 | $this->request->setDisableWebhookSignature(true); 93 | 94 | $this->assertFalse($this->request->isSignatureValid()); 95 | $this->assertNull($this->request->assertSignature()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/Message/AuthorizeRequestTest.php: -------------------------------------------------------------------------------- 1 | gateway = new ApiGateway( 17 | $this->getHttpClient(), 18 | $this->getHttpRequest() 19 | ); 20 | 21 | $this->gateway->setAuthName('authName'); 22 | $this->gateway->setTransactionKey('transactionKey'); 23 | $this->gateway->setRefId('refId'); 24 | } 25 | 26 | public function testOpaqueData() 27 | { 28 | $opaqueDescriptor = 'COMMON.ACCEPT.INAPP.PAYMENT'; 29 | $opaqueValue = str_shuffle(str_repeat('1234567890ABCDEFGHIJ', 10)); 30 | 31 | $cardToken = $opaqueDescriptor . ':' . $opaqueValue; 32 | 33 | $request = $this->gateway->authorize([ 34 | 'opaqueDataDescriptor' => $opaqueDescriptor, 35 | 'opaqueDataValue' => $opaqueValue, 36 | ]); 37 | 38 | $this->assertSame($cardToken, $request->getToken()); 39 | 40 | $this->assertSame($opaqueDescriptor, $request->getOpaqueDataDescriptor()); 41 | $this->assertSame($opaqueValue, $request->getOpaqueDataValue()); 42 | } 43 | 44 | public function testCardToken() 45 | { 46 | $opaqueDescriptor = 'COMMON.ACCEPT.INAPP.PAYMENT'; 47 | $opaqueValue = str_shuffle(str_repeat('1234567890ABCDEFGHIJ', 10)); 48 | 49 | $cardToken = $opaqueDescriptor . ':' . $opaqueValue; 50 | 51 | $request = $this->gateway->authorize([ 52 | 'token' => $cardToken, 53 | ]); 54 | 55 | $this->assertSame($cardToken, $request->getToken()); 56 | 57 | $this->assertSame($opaqueDescriptor, $request->getOpaqueDataDescriptor()); 58 | $this->assertSame($opaqueValue, $request->getOpaqueDataValue()); 59 | } 60 | 61 | public function testCustomerData() 62 | { 63 | $request = $this->gateway->authorize([ 64 | 'amount' => 1.23, 65 | 'customerId' => 'customerId', 66 | 'customerType' => 'individual', 67 | 'customerTaxId' => 'customerTaxId', 68 | 'customerDriversLicense' => 'customerDriversLicense', 69 | 'card' => new CreditCard([ 70 | 'email' => 'email@example.com', 71 | ]), 72 | ]); 73 | 74 | // The request data will have a customer object with this data in. 75 | 76 | $this->assertArraySubset( 77 | [ 78 | 'id' => 'customerId', 79 | 'type' => 'individual', 80 | 'email' => 'email@example.com', 81 | 'driversLicense' => 'customerDriversLicense', 82 | 'taxId' => 'customerTaxId', 83 | ], 84 | $request->getData()->getCustomer()->jsonSerialize() 85 | ); 86 | } 87 | 88 | /** 89 | * If there is no address information, then don't send empty 90 | * address objects to the gateway; just suppress them. 91 | */ 92 | public function testAddressSetNotSet() 93 | { 94 | $request = $this->gateway->authorize([ 95 | 'amount' => 1.23, 96 | 'card' => new CreditCard([ 97 | ]), 98 | ]); 99 | 100 | $this->assertNull($request->getData()->getBillTo()); 101 | $this->assertNull($request->getData()->getShipTo()); 102 | 103 | $request = $this->gateway->authorize([ 104 | 'amount' => 1.23, 105 | 'card' => new CreditCard([ 106 | 'billingAddress1' => 'Street Number', 107 | 'shippingCity' => 'City', 108 | ]), 109 | ]); 110 | 111 | $this->assertInstanceOf( 112 | NameAddress::class, 113 | $request->getData()->getBillTo() 114 | ); 115 | 116 | $this->assertInstanceOf( 117 | NameAddress::class, 118 | $request->getData()->getShipTo() 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Message/AuthorizeResponse.php: -------------------------------------------------------------------------------- 1 | ). 34 | // So I don't trust the data type we get back, and we will play loose and 35 | // fast with implicit conversions here. 36 | 37 | return parent::isSuccessful() 38 | && $this->getResponseCode() == TransactionResponseModel::RESPONSE_CODE_APPROVED; 39 | } 40 | 41 | /** 42 | * Tells us whether the transaction is pending or not. 43 | */ 44 | public function isPending() 45 | { 46 | return $this->responseIsSuccessful() 47 | && $this->getResponseCode() == TransactionResponseModel::RESPONSE_CODE_PENDING; 48 | } 49 | 50 | /** 51 | * Get the transaction response code. 52 | * Expected values are one of TransactionResponseModel::RESPONSE_CODE_* 53 | */ 54 | public function getResponseCode() 55 | { 56 | return $this->getValue($this->transactionIndex . '.responseCode'); 57 | } 58 | 59 | /** 60 | * Collection of transaction message objects, or null if there are none. 61 | * 62 | * @returns TransactionMessages 63 | */ 64 | public function getTransactionMessages() 65 | { 66 | return $this->getValue($this->transactionIndex . '.transactionMessages') 67 | ?: new TransactionMessages(); 68 | } 69 | 70 | /** 71 | * Collection of transaction errors, or null if none. 72 | * 73 | * @returns Errors 74 | */ 75 | public function getTransactionErrors() 76 | { 77 | return $this->getValue($this->transactionIndex . '.errors') 78 | ?: new Errors(); 79 | } 80 | 81 | /** 82 | * Return the message code from the transaction if available, 83 | * or the response envelope. 84 | */ 85 | public function getCode() 86 | { 87 | return $this->getTransactionCode() ?: parent::getCode(); 88 | } 89 | 90 | /** 91 | * Get the transaction message text if available, falling back 92 | * to the response envelope. 93 | */ 94 | public function getMessage() 95 | { 96 | return $this->getTransactionMessage() ?: parent::getMessage(); 97 | } 98 | 99 | /** 100 | * @return string Six characters. 101 | */ 102 | public function getAuthCode() 103 | { 104 | return $this->getValue($this->transactionIndex . '.authCode'); 105 | } 106 | 107 | /** 108 | * @returns string Single letter; one of TransactionResponse::AVS_RESULT_CODE_* 109 | */ 110 | public function getAvsResultCode() 111 | { 112 | return $this->getValue($this->transactionIndex . '.avsResultCode'); 113 | } 114 | 115 | /** 116 | * @returns string Single letter; one of TransactionResponse::CVV_RESULT_CODE_* 117 | */ 118 | public function getCvvResultCode() 119 | { 120 | return $this->getValue($this->transactionIndex . '.cvvResultCode'); 121 | } 122 | 123 | /** 124 | * @returns int One of TransactionResponse::CAVV_RESULT_CODE_* 125 | */ 126 | public function getCavvResultCode() 127 | { 128 | return $this->getValue($this->transactionIndex . '.cavvResultCode'); 129 | } 130 | 131 | /** 132 | * Related transactionReference 133 | * @returns string Reference to previous transaction 134 | */ 135 | public function getRefTransID() 136 | { 137 | return $this->getValue($this->transactionIndex . '.refTransID'); 138 | } 139 | 140 | /** 141 | * @returns string Transaction hash, upper case MD5. 142 | */ 143 | public function getTransHash() 144 | { 145 | return $this->getValue($this->transactionIndex . '.transHash'); 146 | } 147 | 148 | /** 149 | * @returns string The last four digits of either the card number 150 | * or bank account number used for the transaction in the format XXXX1234. 151 | */ 152 | public function getAccountNumber() 153 | { 154 | return $this->getValue($this->transactionIndex . '.accountNumber'); 155 | } 156 | 157 | /** 158 | * @returns string Either the credit card type or in the case of 159 | * eCheck, the value is eCheck. 160 | */ 161 | public function getAccountType() 162 | { 163 | return $this->getValue($this->transactionIndex . '.accountType'); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Message/AbstractResponse.php: -------------------------------------------------------------------------------- 1 | setParsedData(new Response($data)); 42 | } 43 | 44 | /** 45 | * Get a value from the parsed data, based on a path. 46 | * e.g. 'object.arrayProperty[0].stringProperty'. 47 | * Returns null if the dependency path is broken at any point. 48 | * See http://symfony.com/doc/current/components/property_access.html 49 | */ 50 | public function getValue($path) 51 | { 52 | $accessor = $this->getAccessor(); 53 | 54 | // If the accessor has not already been set, then create the default 55 | // accessor now. 56 | if (empty($accessor)) { 57 | $accessor = PropertyAccess::createPropertyAccessorBuilder() 58 | ->enableMagicCall() 59 | ->disableExceptionOnInvalidIndex() 60 | ->getPropertyAccessor(); 61 | 62 | $this->setAccessor($accessor); 63 | } 64 | 65 | try { 66 | // Get the property using its path. 67 | // If the path breaks at any point, an exception will be 68 | // thrown, but we just want to return a null. 69 | 70 | return $accessor->getValue($this->getParsedData(), $path); 71 | } catch (ExceptionInterface $e) { 72 | return null; 73 | } 74 | } 75 | 76 | /** 77 | * Set the property accessor helper. 78 | */ 79 | public function setAccessor(PropertyAccessor $value) 80 | { 81 | $this->accessor = $value; 82 | } 83 | 84 | /** 85 | * Get the property accessor helper. 86 | */ 87 | public function getAccessor() 88 | { 89 | return $this->accessor; 90 | } 91 | 92 | /** 93 | * Set the data parsed into a nested value object. 94 | */ 95 | public function setParsedData(Response $value) 96 | { 97 | $this->parsedData = $value; 98 | } 99 | 100 | /** 101 | * Get the data parsed into a nested value object. 102 | */ 103 | public function getParsedData() 104 | { 105 | return $this->parsedData; 106 | } 107 | 108 | /** 109 | * The merchant supplied ID. 110 | * Up to 20 characters. 111 | * aka transactionId 112 | */ 113 | public function getRefId() 114 | { 115 | return $this->getValue('refId'); 116 | } 117 | 118 | /** 119 | * The transactionId is returned only if sent in the request. 120 | */ 121 | public function getTransactionId() 122 | { 123 | return $this->getRefId(); 124 | } 125 | 126 | /** 127 | * Get the first top-level result code. 128 | * Note this will be unsuitable for most transactions, as the response can 129 | * be successful ("Ok") even while the transaction response is not. 130 | * This is the result code of the envelope that the transaction response 131 | * is returned in. 132 | * e.g. "Ok" 133 | */ 134 | public function getResultCode() 135 | { 136 | return $this->getValue('resultCode'); 137 | } 138 | 139 | /** 140 | * Get the first top-level message text. 141 | * e.g. "Successful." 142 | * e.g. "The transaction was unsuccessful." 143 | */ 144 | public function getResponseMessage() 145 | { 146 | return $this->getValue('messages.first.text'); 147 | } 148 | 149 | /** 150 | * Get the transaction message text from the response envelope. 151 | * Inheriting responses will normally refine this to look deeper into 152 | * the response body. 153 | */ 154 | public function getMessage() 155 | { 156 | return $this->getResponseMessage(); 157 | } 158 | 159 | /** 160 | * Get the first top-level (i.e. message wrapper) message code. 161 | * e.g. "I00001" 162 | * e.g. "E00027" 163 | */ 164 | public function getResponseCode() 165 | { 166 | return $this->getValue('messages.first.code'); 167 | } 168 | 169 | /** 170 | * Return the message code from the response envelope. 171 | * Inheriting responses will normally refine this to look deeper into 172 | * the response body. 173 | */ 174 | public function getCode() 175 | { 176 | return $this->getResponseCode(); 177 | } 178 | 179 | /** 180 | * Get all top-level (envelope) response message collection. 181 | */ 182 | public function getResponseMessages() 183 | { 184 | return $this->getValue('messages'); 185 | } 186 | 187 | /** 188 | * Tell us whether the response was successful overall. 189 | * This is just about the response as a whole; the response may 190 | * still represent a failed transaction. 191 | */ 192 | public function responseIsSuccessful() 193 | { 194 | return $this->getResultCode() === Response::RESULT_CODE_OK; 195 | } 196 | 197 | public function isSuccessful() 198 | { 199 | return $this->getResultCode() === Response::RESULT_CODE_OK; 200 | } 201 | 202 | /** 203 | * Return the last four digits of the crddit card used, if availale. 204 | * @return string 205 | */ 206 | public function getNumberLastFour() 207 | { 208 | return substr($this->getValue($this->transactionIndex . '.accountNumber'), -4, 4) ?: null; 209 | } 210 | 211 | /** 212 | * Return the text of the first error or message in the transaction response. 213 | */ 214 | public function getTransactionMessage() 215 | { 216 | return $this->getValue($this->transactionIndex . '.errors.first.text') 217 | ?: $this->getValue($this->transactionIndex . '.transactionMessages.first.text'); 218 | } 219 | 220 | /** 221 | * Return the code of the first error or message in the transaction response. 222 | */ 223 | public function getTransactionCode() 224 | { 225 | return $this->getValue($this->transactionIndex . '.errors.first.code') 226 | ?: $this->getValue($this->transactionIndex . '.transactionMessages.first.code'); 227 | } 228 | 229 | /** 230 | * ID created for the transaction by the remote gateway. 231 | */ 232 | public function getTransactionReference() 233 | { 234 | return $this->getValue($this->transactionIndex . '.transId'); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Message/AbstractRequest.php: -------------------------------------------------------------------------------- 1 | getAuthName(), $this->getTransactionKey()); 34 | } 35 | 36 | /** 37 | * Return the relevant endpoint. 38 | */ 39 | public function getEndpoint() 40 | { 41 | if ($this->getTestMode()) { 42 | return $this->endpointSandbox; 43 | } else { 44 | return $this->endpointLive; 45 | } 46 | } 47 | 48 | /** 49 | * Send a HTTP request to the gateway. 50 | * 51 | * @param array|\JsonSerializable $data The body data to send to the gateway 52 | * @return GuzzleHttp\Psr7\Response 53 | */ 54 | protected function sendRequest($data, $method = 'POST') 55 | { 56 | $response = $this->httpClient->request( 57 | $method, 58 | $this->getEndpoint(), 59 | array( 60 | 'Content-Type' => 'application/json', 61 | ), 62 | json_encode($data) 63 | ); 64 | 65 | return $response; 66 | } 67 | 68 | /** 69 | * Strip a Byte Order Mark (BOM) from the start of a string. 70 | * 71 | * @param string $string A string with a potential BOM prefix. 72 | * @return string The string with the BOM removed. 73 | */ 74 | public function removeBOM($string) 75 | { 76 | return preg_replace('/^[\x00-\x1F\x80-\xFF]{1,3}/', '', $string); 77 | } 78 | 79 | /** 80 | * Send a transaction and return the decoded data. 81 | * Any movement of funds is normnally done by creating a transaction 82 | * to perform the action. Requests that involve profiles, fetching 83 | * information, won't involve transactions. 84 | * 85 | * @param TransactionRequestInterface $transaction The transaction object 86 | * @return array The decoded data returned by the gateway. 87 | */ 88 | public function sendTransaction(TransactionRequestInterface $transaction) 89 | { 90 | // Wrap the transaction detail into a request. 91 | $request = $this->wrapTransaction($this->getAuth(), $transaction); 92 | 93 | // The merchant site ID. 94 | $request = $request->withRefId($this->getTransactionId()); 95 | 96 | return $this->sendMessage($request); 97 | } 98 | 99 | /** 100 | * Send a messgae and return the resulting decoded response data. 101 | * 102 | * TODO: handle unexpected results and HTTP return codes. 103 | * 104 | * @param ApiAbstractRequest $message The hydrated request message 105 | * (from the academe/authorizenet-objects package) 106 | */ 107 | protected function sendMessage(ApiAbstractRequest $message) 108 | { 109 | // Send the request to the gateway. 110 | $response = $this->sendRequest($message); 111 | 112 | // The caller will know what object to put this data into. 113 | $body = (string)($response->getBody()); 114 | 115 | // The body will be JSON, but *may* have a Byte Order Mark (BOM) prefix. 116 | // Remove the BOM. 117 | $body = $this->removeBOM($body); 118 | 119 | // Now decode the JSON body. 120 | $data = json_decode($body, true); 121 | 122 | // Return a data response. 123 | return $data; 124 | } 125 | 126 | /** 127 | * Wrap the transaction detail into a full request for an action on 128 | * the transaction. 129 | */ 130 | protected function wrapTransaction($auth, $transaction) 131 | { 132 | return new CreateTransaction($auth, $transaction); 133 | } 134 | 135 | /** 136 | * @param string Merchant-defined invoice number associated with the order. 137 | * @return $this 138 | */ 139 | public function setInvoiceNumber($value) 140 | { 141 | return $this->setParameter('invoiceNumber', $value); 142 | } 143 | 144 | /** 145 | * @return string 146 | */ 147 | public function getInvoiceNumber() 148 | { 149 | return $this->getParameter('invoiceNumber'); 150 | } 151 | 152 | /** 153 | * @param string Merchant-defined invoice number associated with the order. 154 | * @return $this 155 | */ 156 | public function setTerminalNumber($value) 157 | { 158 | return $this->setParameter('terminalNumber', $value); 159 | } 160 | 161 | /** 162 | * @return string 163 | */ 164 | public function getTerminalNumber() 165 | { 166 | return $this->getParameter('terminalNumber'); 167 | } 168 | 169 | /** 170 | * authenticationIndicator and authenticationValue are used as a pair. 171 | * @param string 3D Secure indicator. 172 | * @return $this 173 | */ 174 | public function setAuthenticationIndicator($value) 175 | { 176 | return $this->setParameter('authenticationIndicator', $value); 177 | } 178 | 179 | /** 180 | * @return string 181 | */ 182 | public function getAuthenticationIndicator() 183 | { 184 | return $this->getParameter('authenticationIndicator'); 185 | } 186 | 187 | /** 188 | * authenticationIndicator and authenticationValue are used as a pair. 189 | * @param string 3D Secure value. 190 | * @return $this 191 | */ 192 | public function setAuthenticationValue($value) 193 | { 194 | return $this->setParameter('authenticationValue', $value); 195 | } 196 | 197 | /** 198 | * @return string 199 | */ 200 | public function getAuthenticationValue() 201 | { 202 | return $this->getParameter('authenticationValue'); 203 | } 204 | 205 | /** 206 | * @param string customer ID. 207 | * @return $this 208 | */ 209 | public function setCustomerId($value) 210 | { 211 | return $this->setParameter('customerId', $value); 212 | } 213 | 214 | /** 215 | * @return string 216 | */ 217 | public function getCustomerId() 218 | { 219 | return $this->getParameter('customerId'); 220 | } 221 | 222 | /** 223 | * Valid values are one of 224 | * \Academe\AuthorizeNet\Request\Model\Customer::CUSTOMER_TYPE_* 225 | * @param string customer type 226 | * @return $this 227 | */ 228 | public function setCustomerType($value) 229 | { 230 | return $this->setParameter('customerType', $value); 231 | } 232 | 233 | /** 234 | * @return string 235 | */ 236 | public function getCustomerType() 237 | { 238 | return $this->getParameter('customerType'); 239 | } 240 | 241 | /** 242 | * @param string Customer Drivers License. 243 | * @return $this 244 | */ 245 | public function setCustomerDriversLicense($value) 246 | { 247 | return $this->setParameter('customerDriversLicense', $value); 248 | } 249 | 250 | /** 251 | * @return string 252 | */ 253 | public function getCustomerDriversLicense() 254 | { 255 | return $this->getParameter('customerDriversLicense'); 256 | } 257 | 258 | /** 259 | * @param string Customer Tax ID. 260 | * @return $this 261 | */ 262 | public function setCustomerTaxId($value) 263 | { 264 | return $this->setParameter('customerTaxId', $value); 265 | } 266 | 267 | /** 268 | * @return string 269 | */ 270 | public function getCustomerTaxId() 271 | { 272 | return $this->getParameter('customerTaxId'); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/Message/AcceptNotification.php: -------------------------------------------------------------------------------- 1 | getContentType() === 'json') { 53 | $this->payload = (string)$httpRequest->getContent(); 54 | } 55 | 56 | $this->data = json_decode($this->payload, true); 57 | 58 | $this->setParsedData(new Notification($this->data)); 59 | 60 | // Save the signature for validating later. 61 | // It cannot be validated until this object is initialised with parameters. 62 | 63 | $this->signature = $httpRequest->headers->get( 64 | static::SIGNATURE_HEADER_NAME 65 | ); 66 | } 67 | 68 | /** 69 | * Set the data parsed into a nested value object. 70 | */ 71 | public function setParsedData(Notification $value) 72 | { 73 | $this->parsedData = $value; 74 | } 75 | 76 | /** 77 | * Get the data parsed into a nested value object. 78 | */ 79 | public function getParsedData() 80 | { 81 | return $this->parsedData; 82 | } 83 | 84 | /** 85 | * Get the raw data array for this message. 86 | * The raw data is from the JSON payload. 87 | * 88 | * @return mixed 89 | */ 90 | public function getData() 91 | { 92 | return $this->data; 93 | } 94 | 95 | /** 96 | * Gateway Reference 97 | * 98 | * @throws InvalidRequestException 99 | * @return string The gateway key for this transaction 100 | */ 101 | public function getTransactionReference() 102 | { 103 | $this->assertSignature(); 104 | 105 | if ($this->getEventTarget() === $this->getParsedData()::EVENT_TARGET_PAYMENT) { 106 | return $this->getPayload()->getTransId(); 107 | } 108 | } 109 | 110 | /** 111 | * Was the transaction successful? 112 | * 113 | * @throws InvalidRequestException 114 | * @return string Transaction status, one of {@see STATUS_COMPLETED}, {@see #STATUS_PENDING}, 115 | * or {@see #STATUS_FAILED}. 116 | */ 117 | public function getTransactionStatus() 118 | { 119 | $this->assertSignature(); 120 | 121 | $responseCode = $this->getResponseCode(); 122 | 123 | if ($responseCode === TransactionResponse::RESPONSE_CODE_APPROVED) { 124 | return static::STATUS_COMPLETED; 125 | } elseif ($responseCode === TransactionResponse::RESPONSE_CODE_PENDING) { 126 | return static::STATUS_PENDIND; 127 | } elseif ($responseCode !== null) { 128 | return static::STATUS_FAILED; 129 | } 130 | } 131 | 132 | /** 133 | * Response Message 134 | * 135 | * @throws InvalidRequestException 136 | * @return string A response message from the payment gateway 137 | */ 138 | public function getMessage() 139 | { 140 | $this->assertSignature(); 141 | 142 | // There are actually no messages in the notifications. 143 | 144 | return ''; 145 | } 146 | 147 | /** 148 | * There is nothing to send in order to response to this webhook. 149 | * The merchant site just needs to return a HTTP 200. 150 | * 151 | * @param mixed $data The data to send 152 | * @return ResponseInterface 153 | */ 154 | public function sendData($data) 155 | { 156 | return $this; 157 | } 158 | 159 | /** 160 | * The main target of the notificaiton: payment or customer. 161 | */ 162 | public function getEventTarget() 163 | { 164 | return $this->getParsedData()->getEventTarget(); 165 | } 166 | 167 | /** 168 | * The sub-target of the notificaiton. 169 | */ 170 | public function getEventSubtarget() 171 | { 172 | return $this->getParsedData()->getEventSubtarget(); 173 | } 174 | 175 | /** 176 | * The action against the target of the notificaito. 177 | */ 178 | public function getEventAction() 179 | { 180 | return $this->getParsedData()->getEventAction(); 181 | } 182 | 183 | /** 184 | * The UUID identifying this specific notification. 185 | */ 186 | public function getNotificationId() 187 | { 188 | return $this->getParsedData()->getNotificationId(); 189 | } 190 | 191 | /** 192 | * The UUID identifying the webhook being fired. 193 | */ 194 | public function getWebhookId() 195 | { 196 | return $this->getParsedData()->getWebhookId(); 197 | } 198 | 199 | /** 200 | * Optional notification payload. 201 | */ 202 | public function getPayload() 203 | { 204 | return $this->getParsedData()->getPayload(); 205 | } 206 | 207 | /** 208 | * @return int Raw response code 209 | */ 210 | public function getResponseCode() 211 | { 212 | if ($this->getEventTarget() === $this->getParsedData()::EVENT_TARGET_PAYMENT) { 213 | return $this->getPayload()->getResponseCode(); 214 | } 215 | } 216 | 217 | /** 218 | * @return string Raw response code 219 | */ 220 | public function getAuthCode() 221 | { 222 | if ($this->getEventTarget() === $this->getParsedData()::EVENT_TARGET_PAYMENT) { 223 | return $this->getPayload()->getAuthCode(); 224 | } 225 | } 226 | 227 | /** 228 | * @return string Raw AVS response code 229 | */ 230 | public function getAvsResponse() 231 | { 232 | if ($this->getEventTarget() === $this->getParsedData()::EVENT_TARGET_PAYMENT) { 233 | return $this->getPayload()->getAvsResponse(); 234 | } 235 | } 236 | 237 | /** 238 | * @return float authAmount, no currency, no stated units 239 | */ 240 | public function getAuthAmount() 241 | { 242 | if ($this->getEventTarget() === $this->getParsedData()::EVENT_TARGET_PAYMENT) { 243 | return $this->getPayload()->getAuthAmount(); 244 | } 245 | } 246 | 247 | /** 248 | * Assert that the signature of the webhook is valid. 249 | * Will honour the flag to skip this check. 250 | * 251 | * @throws InvalidRequestException 252 | */ 253 | public function assertSignature() 254 | { 255 | // Signature checking can be explicitly disabled. 256 | 257 | if ((bool)$this->getDisableWebhookSignature()) { 258 | return; 259 | } 260 | 261 | if (! $this->isSignatureValid()) { 262 | throw new InvalidRequestException('Invalid or missing signature'); 263 | } 264 | } 265 | 266 | /** 267 | * Check whether the signature is valid. 268 | * 269 | * @return bool true = valid; false = invalid. 270 | */ 271 | public function isSignatureValid() 272 | { 273 | // A missing or malformed signature is invalid. 274 | 275 | if ($this->signature === null || strpos($this->signature, 'sha512=') !== 0) { 276 | return false; 277 | } 278 | 279 | // A missing signature key is also invalid. 280 | 281 | if (($signatureKey = $this->getSignatureKey()) === null) { 282 | return false; 283 | } 284 | 285 | // Check the signature. 286 | 287 | list ($algorithm, $signatureString) = explode('=', $this->signature, 2); 288 | 289 | $hashedPayload = strtoupper( 290 | hash_hmac($algorithm, $this->payload, $signatureKey) 291 | ); 292 | 293 | return $hashedPayload === $signatureString; 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/Message/HostedPage/AuthorizeRequest.php: -------------------------------------------------------------------------------- 1 | getCancelUrl()) { 38 | $this->setReturnOptionsCancelUrl($cancelUrl); 39 | } 40 | 41 | if ($returnUrl = $this->getReturnUrl()) { 42 | $this->setReturnOptionsUrl($returnUrl); 43 | } 44 | 45 | // Then add the settings to the outer requuest object. 46 | 47 | if ($settings = $this->getHostedPaymentSettings()) { 48 | $request = $request->withHostedPaymentSettings($settings); 49 | } 50 | 51 | return $request; 52 | } 53 | 54 | public function getData() 55 | { 56 | $transaction = parent::getData(); 57 | 58 | return $transaction; 59 | } 60 | 61 | /** 62 | * Accept a transaction and sends it as a request. 63 | * 64 | * @param $data TransactionRequestInterface 65 | * @returns TransactionResponse 66 | */ 67 | public function sendData($data) 68 | { 69 | $response_data = $this->sendTransaction($data); 70 | 71 | $response = new Response($this, $response_data); 72 | 73 | // The response needs to know whether we are in test mode or not, 74 | // so that it chooses the correct hosted page URL to redirect to. 75 | 76 | $response->setTestMode($this->getTestMode()); 77 | 78 | return $response; 79 | } 80 | 81 | /** 82 | * Add all the hosted payment settings all at once. 83 | * @param array $value Name/value pairs for each setting. 84 | */ 85 | public function setHostedPaymentSettings(array $value) 86 | { 87 | foreach ($value as $name => $value) { 88 | $this->setHostedPaymentSetting($name, $value); 89 | } 90 | } 91 | 92 | /** 93 | * @returns HostedPaymentSettings|null 94 | */ 95 | public function getHostedPaymentSettings() 96 | { 97 | return $this->hostedPaymentSettings; 98 | } 99 | 100 | /** 101 | * @param mixed $name The Name of the setting, 102 | * one of \Academe\AuthorizeNet\Request\Model\HostedPaymentSetting::SETTING_NAME_* 103 | * @param string|array $value The value of the setting. 104 | */ 105 | public function setHostedPaymentSetting($name, $value) 106 | { 107 | // Initialise the collection if not intialised. 108 | if (empty($this->hostedPaymentSettings)) { 109 | $this->hostedPaymentSettings = new HostedPaymentSettings(); 110 | } 111 | 112 | $this->hostedPaymentSettings->push( 113 | new HostedPaymentSetting($name, $value) 114 | ); 115 | } 116 | 117 | /** 118 | * Set a named parameter on a named payment page setting. 119 | */ 120 | public function setHostedPaymentSettingParameter($settingName, $parameterName, $parameterValue) 121 | { 122 | if ($settings = $this->getHostedPaymentSettings()) { 123 | // The settings collection already exists, so add to it. 124 | $settings->setSettingParameter($settingName, $parameterName, $parameterValue); 125 | } else { 126 | // No settings at all so far, so add this one to start. 127 | $this->setHostedPaymentSetting($settingName, [$parameterName => $parameterValue]); 128 | } 129 | } 130 | 131 | // The following parameters follow a consistent pattern that could be implemented 132 | // as a magic __call method, but aren't at this time (in case they need to go into 133 | // the gateway settings). 134 | // They exist to allow these settings to provided as scalar values, even though 135 | // they end up quite deep in the request strucure. 136 | // There are no equivalent getters for these setters. Do we need getters? 137 | 138 | /** 139 | * @param bool $value 140 | */ 141 | public function setReturnOptionsShowReceipt($value) 142 | { 143 | return $this->setHostedPaymentSettingParameter( 144 | HostedPaymentSetting::SETTING_NAME_RETURN_OPTIONS, 145 | 'showReceipt', 146 | (bool)$value 147 | ); 148 | } 149 | 150 | /** 151 | * @param string $value 152 | */ 153 | public function setReturnOptionsUrl($value) 154 | { 155 | return $this->setHostedPaymentSettingParameter( 156 | HostedPaymentSetting::SETTING_NAME_RETURN_OPTIONS, 157 | 'url', 158 | $value 159 | ); 160 | } 161 | 162 | /** 163 | * @param string $value 164 | */ 165 | public function setReturnOptionsUrlText($value) 166 | { 167 | return $this->setHostedPaymentSettingParameter( 168 | HostedPaymentSetting::SETTING_NAME_RETURN_OPTIONS, 169 | 'urlText', 170 | $value 171 | ); 172 | } 173 | 174 | /** 175 | * @param string $value 176 | */ 177 | public function setReturnOptionsCancelUrl($value) 178 | { 179 | return $this->setHostedPaymentSettingParameter( 180 | HostedPaymentSetting::SETTING_NAME_RETURN_OPTIONS, 181 | 'cancelUrl', 182 | $value 183 | ); 184 | } 185 | 186 | /** 187 | * @param string $value 188 | */ 189 | public function setReturnOptionsCancelUrlText($value) 190 | { 191 | return $this->setHostedPaymentSettingParameter( 192 | HostedPaymentSetting::SETTING_NAME_RETURN_OPTIONS, 193 | 'cancelUrlText', 194 | $value 195 | ); 196 | } 197 | 198 | /** 199 | * @param string $value 200 | */ 201 | public function setButtonOptionsText($value) 202 | { 203 | return $this->setHostedPaymentSettingParameter( 204 | HostedPaymentSetting::SETTING_NAME_BUTTON_OPTIONS, 205 | 'text', 206 | $value 207 | ); 208 | } 209 | 210 | /** 211 | * @param string $value 212 | */ 213 | public function setStyleOptionsBgColor($value) 214 | { 215 | return $this->setHostedPaymentSettingParameter( 216 | HostedPaymentSetting::SETTING_NAME_STYLE_OPTIONS, 217 | 'bgColor', 218 | $value 219 | ); 220 | } 221 | 222 | /** 223 | * @param bool $value 224 | */ 225 | public function setPaymentOptionsCardCodeRequired($value) 226 | { 227 | return $this->setHostedPaymentSettingParameter( 228 | HostedPaymentSetting::SETTING_NAME_PAYMENT_OPTIONS, 229 | 'cardCodeRequired', 230 | (bool)$value 231 | ); 232 | } 233 | 234 | /** 235 | * @param bool $value 236 | */ 237 | public function setPaymentOptionsShowCreditCard($value) 238 | { 239 | return $this->setHostedPaymentSettingParameter( 240 | HostedPaymentSetting::SETTING_NAME_PAYMENT_OPTIONS, 241 | 'showCreditCard', 242 | (bool)$value 243 | ); 244 | } 245 | 246 | /** 247 | * @param bool $value 248 | */ 249 | public function setPaymentOptionsShowBankAccount($value) 250 | { 251 | return $this->setHostedPaymentSettingParameter( 252 | HostedPaymentSetting::SETTING_NAME_PAYMENT_OPTIONS, 253 | 'showBankAccount', 254 | (bool)$value 255 | ); 256 | } 257 | 258 | /** 259 | * @param bool $value 260 | */ 261 | public function setSecurityOptionsCaptcha($value) 262 | { 263 | return $this->setHostedPaymentSettingParameter( 264 | HostedPaymentSetting::SETTING_NAME_SECURITY_OPTIONS, 265 | 'captcha', 266 | (bool)$value 267 | ); 268 | } 269 | 270 | /** 271 | * @param bool $value 272 | */ 273 | public function setShippingAddressOptionsShow($value) 274 | { 275 | return $this->setHostedPaymentSettingParameter( 276 | HostedPaymentSetting::SETTING_NAME_SHIPPING_ADDRESS_OPTIONS, 277 | 'show', 278 | (bool)$value 279 | ); 280 | } 281 | 282 | /** 283 | * @param bool $value 284 | */ 285 | public function setShippingAddressOptionsRequired($value) 286 | { 287 | return $this->setHostedPaymentSettingParameter( 288 | HostedPaymentSetting::SETTING_NAME_SHIPPING_ADDRESS_OPTIONS, 289 | 'required', 290 | (bool)$value 291 | ); 292 | } 293 | 294 | /** 295 | * @param bool $value 296 | */ 297 | public function setBillingAddressOptionsShow($value) 298 | { 299 | return $this->setHostedPaymentSettingParameter( 300 | HostedPaymentSetting::SETTING_NAME_BILLING_ADDRESS_OPTIONS, 301 | 'show', 302 | (bool)$value 303 | ); 304 | } 305 | 306 | /** 307 | * @param bool $value 308 | */ 309 | public function setBillingAddressOptionsRequired($value) 310 | { 311 | return $this->setHostedPaymentSettingParameter( 312 | HostedPaymentSetting::SETTING_NAME_BILLING_ADDRESS_OPTIONS, 313 | 'required', 314 | (bool)$value 315 | ); 316 | } 317 | 318 | /** 319 | * @param bool $value 320 | */ 321 | public function setCustomerOptionsShowEmail($value) 322 | { 323 | return $this->setHostedPaymentSettingParameter( 324 | HostedPaymentSetting::SETTING_NAME_CUSTOMER_OPTIONS, 325 | 'showEmail', 326 | (bool)$value 327 | ); 328 | } 329 | 330 | /** 331 | * @param bool $value 332 | */ 333 | public function setCustomerOptionsRequiredEmail($value) 334 | { 335 | return $this->setHostedPaymentSettingParameter( 336 | HostedPaymentSetting::SETTING_NAME_CUSTOMER_OPTIONS, 337 | 'requiredEmail', 338 | (bool)$value 339 | ); 340 | } 341 | 342 | /** 343 | * @param bool $value 344 | */ 345 | public function setOrderOptionsShow($value) 346 | { 347 | return $this->setHostedPaymentSettingParameter( 348 | HostedPaymentSetting::SETTING_NAME_ORDER_OPTIONS, 349 | 'show', 350 | (bool)$value 351 | ); 352 | } 353 | 354 | /** 355 | * @param string $value 356 | */ 357 | public function setOrderOptionsMerchantName($value) 358 | { 359 | return $this->setHostedPaymentSettingParameter( 360 | HostedPaymentSetting::SETTING_NAME_ORDER_OPTIONS, 361 | 'merchantName', 362 | $value 363 | ); 364 | } 365 | 366 | /** 367 | * Name is no typo - it just follows the same patterns as above. 368 | * @param string $value 369 | */ 370 | public function setIFrameCommunicatorUrlUrl($value) 371 | { 372 | return $this->setHostedPaymentSettingParameter( 373 | HostedPaymentSetting::SETTING_NAME_FRAME_COMMUNICATOR_URL, 374 | 'url', 375 | $value 376 | ); 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /src/Message/AuthorizeRequest.php: -------------------------------------------------------------------------------- 1 | getCurrency(), $this->getAmountInteger()); 49 | 50 | $transaction = $this->createTransaction($amount); 51 | 52 | // Build the customer, and add the customer to the transaction 53 | // if it has any attributes set. 54 | 55 | $customer = new Customer(); 56 | 57 | $customer = $customer 58 | ->withId($this->getCustomerId()) 59 | ->withCustomerType($this->getCustomerType()) 60 | ->withDriversLicense($this->getCustomerDriversLicense()) 61 | ->withTaxId($this->getCustomerTaxId()); 62 | 63 | if ($card = $this->getCard()) { 64 | $billingAddress = trim( 65 | $card->getBillingAddress1() . ' ' . $card->getBillingAddress2() 66 | ); 67 | 68 | if ($billingAddress === '') { 69 | $billingAddress = null; 70 | } 71 | 72 | $billTo = new NameAddress( 73 | $card->getBillingFirstName(), 74 | $card->getBillingLastName(), 75 | $card->getBillingCompany(), 76 | $billingAddress, 77 | $card->getBillingCity(), 78 | $card->getBillingState(), 79 | $card->getBillingPostcode(), 80 | $card->getBillingCountry() 81 | ); 82 | 83 | // The billTo may have phone and fax number, but the shipTo does not. 84 | $billTo = $billTo->withPhoneNumber($card->getBillingPhone()); 85 | $billTo = $billTo->withFaxNumber($card->getBillingFax()); 86 | 87 | if ($billTo->hasAny()) { 88 | $transaction = $transaction->withBillTo($billTo); 89 | } 90 | 91 | $shippingAddress = trim( 92 | $card->getShippingAddress1() . ' ' . $card->getShippingAddress2() 93 | ); 94 | 95 | if ($shippingAddress === '') { 96 | $shippingAddress = null; 97 | } 98 | 99 | $shipTo = new NameAddress( 100 | $card->getShippingFirstName(), 101 | $card->getShippingLastName(), 102 | $card->getShippingCompany(), 103 | $shippingAddress, 104 | $card->getShippingCity(), 105 | $card->getShippingState(), 106 | $card->getShippingPostcode(), 107 | $card->getShippingCountry() 108 | ); 109 | 110 | if ($shipTo->hasAny()) { 111 | $transaction = $transaction->withShipTo($shipTo); 112 | } 113 | 114 | if ($card->getEmail()) { 115 | $customer = $customer->withEmail($card->getEmail()); 116 | } 117 | 118 | // Credit card, track 1 and track 2 are mutually exclusive. 119 | 120 | if ($card->getNumber()) { 121 | // A credit card has been supplied. 122 | 123 | $card->validate(); 124 | 125 | $creditCard = new CreditCard( 126 | $card->getNumber(), 127 | // Either MMYY or MMYYYY will work. 128 | $card->getExpiryMonth() . $card->getExpiryYear() 129 | ); 130 | 131 | if ($card->getCvv()) { 132 | $creditCard = $creditCard->withCardCode($card->getCvv()); 133 | } 134 | 135 | $transaction = $transaction->withPayment($creditCard); 136 | } elseif ($card->getTrack1()) { 137 | // A card magnetic track has been supplied (aka card present). 138 | 139 | $transaction = $transaction->withPayment( 140 | new Track1($card->getTrack1()) 141 | ); 142 | } elseif ($card->getTrack2()) { 143 | $transaction = $transaction->withPayment( 144 | new Track2($card->getTrack2()) 145 | ); 146 | } 147 | } // credit card 148 | 149 | if ($customer->hasAny()) { 150 | $transaction = $transaction->withCustomer($customer); 151 | } 152 | 153 | // Allow "Accept JS" nonce (in two parts) instead of card (aka OpaqueData). 154 | 155 | $descriptor = $this->getOpaqueDataDescriptor(); 156 | $value = $this->getOpaqueDataValue(); 157 | 158 | if ($descriptor && $value) { 159 | $transaction = $transaction->withPayment( 160 | new OpaqueData($descriptor, $value) 161 | ); 162 | } 163 | 164 | if ($this->getClientIp()) { 165 | $transaction = $transaction->withCustomerIp($this->getClientIp()); 166 | } 167 | 168 | // The MarketType and DeviceType is mandatory if tracks are supplied. 169 | 170 | if ($this->getDeviceType() 171 | || $this->getMarketType() 172 | || (isset($card) && $card->getTracks()) 173 | ) { 174 | // TODO: accept optional customerSignature 175 | 176 | $retail = new Retail( 177 | $this->getMarketType() ?: Retail::MARKET_TYPE_RETAIL, 178 | $this->getDeviceType() ?: Retail::DEVICE_TYPE_UNKNOWN 179 | ); 180 | 181 | $transaction = $transaction->withRetail($retail); 182 | } 183 | 184 | // The description and invoice number go into an Order object. 185 | 186 | if ($this->getInvoiceNumber() || $this->getDescription()) { 187 | $order = new Order( 188 | $this->getInvoiceNumber(), 189 | $this->getDescription() 190 | ); 191 | 192 | $transaction = $transaction->withOrder($order); 193 | } 194 | 195 | // 3D Secure is handled by a thirds party provider. 196 | // These two fields submit the authentication values provided. 197 | // It is not really clear if both these fields must be always provided together, 198 | // or whether just one is permitted. 199 | 200 | if ($this->getAuthenticationIndicator() || $this->getAuthenticationValue()) { 201 | $cardholderAuthentication = new CardholderAuthentication( 202 | $this->getAuthenticationIndicator(), 203 | $this->getAuthenticationValue() 204 | ); 205 | 206 | $transaction = $transaction->withCardholderAuthentication($cardholderAuthentication); 207 | } 208 | 209 | // Is a basket of items to go into the request? 210 | 211 | if ($this->getItems()) { 212 | $lineItems = new LineItems(); 213 | 214 | $currencies = new ISOCurrencies(); 215 | $moneyParser = new DecimalMoneyParser($currencies); 216 | 217 | foreach ($this->getItems() as $itemId => $item) { 218 | // Parse to a Money object. 219 | 220 | $itemMoney = $moneyParser->parse((string)$item->getPrice(), $this->getCurrency()); 221 | 222 | // Omnipay provides the line price, but the LineItem wants the unit price. 223 | 224 | $itemQuantity = $item->getQuantity(); 225 | 226 | if (! empty($itemQuantity)) { 227 | // Divide the line price by the quantity to get the item price. 228 | 229 | $itemMoney = $itemMoney->divide($itemQuantity); 230 | } 231 | 232 | // Wrap in a MoneyPhp object for the AmountInterface. 233 | 234 | $amount = new MoneyPhp($itemMoney); 235 | 236 | $lineItem = new LineItem( 237 | $itemId, 238 | $item->getName(), 239 | $item->getDescription(), 240 | $itemQuantity, 241 | $amount, // AmountInterface (unit price) 242 | null // $taxable 243 | ); 244 | 245 | $lineItems->push($lineItem); 246 | } 247 | 248 | if ($lineItems->count()) { 249 | $transaction = $transaction->withLineItems($lineItems); 250 | } 251 | } 252 | 253 | $transaction = $transaction->with([ 254 | 'terminalNumber' => $this->getTerminalNumber(), 255 | ]); 256 | 257 | if ($sourceUserFields = $this->getUserFields()) { 258 | // Can be provided as key/value array, array of name/value pairs 259 | // or a readymade collection of models. 260 | 261 | if ($sourceUserFields instanceof UserFields) { 262 | // Already a collection; just use it. 263 | 264 | $userFields = $sourceUserFields; 265 | } else { 266 | $userFields = new UserFields(); 267 | 268 | if (is_array($sourceUserFields)) { 269 | foreach ($sourceUserFields as $key => $value) { 270 | if (is_string($key) && is_string($value)) { 271 | // key/value pairs: 'key' => 'value' 272 | 273 | $userFields->push(new UserField($key, $value)); 274 | } 275 | 276 | if (is_array($value) && count($value) === 2) { 277 | // name/value pairs: ['name' => 'the name', 'value' => 'the value'] 278 | 279 | $userFields->push(new UserField($value['name'], $value['value'])); 280 | } 281 | 282 | if ($value instanceof UserField) { 283 | // An array of UserField objects was supplied. 284 | 285 | $userFields->push($value); 286 | } 287 | } 288 | } 289 | } 290 | 291 | if ($userFields->count()) { 292 | $transaction = $transaction->withUserFields($userFields); 293 | } 294 | } 295 | 296 | return $transaction; 297 | } 298 | 299 | /** 300 | * Create a new instance of the transaction object. 301 | */ 302 | protected function createTransaction(AmountInterface $amount) 303 | { 304 | return new AuthOnly($amount); 305 | } 306 | 307 | /** 308 | * Accept a transaction and sends it as a request. 309 | * 310 | * @param $data TransactionRequestInterface 311 | * @returns TransactionResponse 312 | */ 313 | public function sendData($data) 314 | { 315 | $responseData = $this->sendTransaction($data); 316 | 317 | return new AuthorizeResponse($this, $responseData); 318 | } 319 | 320 | /** 321 | * Value must be one of Retail::DEVICE_TYPE_* 322 | * @param int $value The retail device type. 323 | * @return $this 324 | */ 325 | public function setDeviceType($value) 326 | { 327 | return $this->setParameter('deviceType', $value); 328 | } 329 | 330 | /** 331 | * @return int 332 | */ 333 | public function getDeviceType() 334 | { 335 | return $this->getParameter('deviceType'); 336 | } 337 | 338 | /** 339 | * Value must be one of Retail::MARKET_TYPE_* 340 | * @param int $value The retail market type. 341 | * @return $this 342 | */ 343 | public function setMarketType($value) 344 | { 345 | return $this->setParameter('marketType', $value); 346 | } 347 | 348 | /** 349 | * @return int 350 | */ 351 | public function getMarketType() 352 | { 353 | return $this->getParameter('marketType'); 354 | } 355 | 356 | /** 357 | * @param string $value Example: 'COMMON.ACCEPT.INAPP.PAYMENT'. 358 | * @return $this 359 | */ 360 | public function setOpaqueDataDescriptor($value) 361 | { 362 | return $this->setParameter('opaqueDataDescriptor', $value); 363 | } 364 | 365 | /** 366 | * @return string 367 | */ 368 | public function getOpaqueDataDescriptor() 369 | { 370 | return $this->getParameter('opaqueDataDescriptor'); 371 | } 372 | 373 | /** 374 | * @param string $value Long text token usually 216 bytes long. 375 | * @return $this 376 | */ 377 | public function setOpaqueDataValue($value) 378 | { 379 | return $this->setParameter('opaqueDataValue', $value); 380 | } 381 | 382 | /** 383 | * @return string 384 | */ 385 | public function getOpaqueDataValue() 386 | { 387 | return $this->getParameter('opaqueDataValue'); 388 | } 389 | 390 | /** 391 | * @param string $descriptor 392 | * @param string $value 393 | * @return $this 394 | */ 395 | public function setOpaqueData($descriptor, $value) 396 | { 397 | $this->setOpaqueDataDataDescriptor($descriptor); 398 | $this->setOpaqueDataValue($value); 399 | 400 | return $this; 401 | } 402 | 403 | /** 404 | * The opaque data comes in two parts, but Omnipay uses just 405 | * one parameter for a card token. 406 | * Join the descriptor and the value with a colon. 407 | */ 408 | public function setToken($value) 409 | { 410 | list($opaqueDataDescriptor, $opaqueDataValue) = explode(static::CARD_TOKEN_SEPARATOR, $value, 2); 411 | 412 | $this->setOpaqueDataDescriptor($opaqueDataDescriptor); 413 | $this->setOpaqueDataValue($opaqueDataValue); 414 | 415 | return $this; 416 | } 417 | 418 | public function getToken() 419 | { 420 | $opaqueDataDescriptor = $this->getOpaqueDataDescriptor(); 421 | $opaqueDataValue = $this->getOpaqueDataValue(); 422 | 423 | if ($opaqueDataDescriptor && $opaqueDataValue) { 424 | return $opaqueDataDescriptor 425 | . static::CARD_TOKEN_SEPARATOR 426 | . $opaqueDataValue; 427 | } 428 | } 429 | 430 | public function setUserFields($value) 431 | { 432 | return $this->setParameter('userFields', $value); 433 | } 434 | 435 | public function getUserFields() 436 | { 437 | return $this->getParameter('userFields'); 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Build Status](https://travis-ci.org/academe/omnipay-authorizenetapi.svg?branch=master)](https://travis-ci.org/academe/omnipay-authorizenetapi) 3 | [![Latest Stable Version](https://poser.pugx.org/academe/omnipay-authorizenetapi/v/stable)](https://packagist.org/packages/academe/omnipay-authorizenetapi) 4 | [![Total Downloads](https://poser.pugx.org/academe/omnipay-authorizenetapi/downloads)](https://packagist.org/packages/academe/omnipay-authorizenetapi) 5 | [![Latest Unstable Version](https://poser.pugx.org/academe/omnipay-authorizenetapi/v/unstable)](https://packagist.org/packages/academe/omnipay-authorizenetapi) 6 | [![License](https://poser.pugx.org/academe/omnipay-authorizenetapi/license)](https://packagist.org/packages/academe/omnipay-authorizenetapi) 7 | 8 | Table of Contents 9 | ================= 10 | 11 | * [Table of Contents](#table-of-contents) 12 | * [Omnipay-AuthorizeNetApi](#omnipay-authorizenetapi) 13 | * [Installation](#installation) 14 | * [Authorize.Net API](#authorizenet-api) 15 | * [API Authorize/Purchase (Credit Card)](#api-authorizepurchase-credit-card) 16 | * [API Capture](#api-capture) 17 | * [API Authorize/Purchase (Opaque Data)](#api-authorizepurchase-opaque-data) 18 | * [API Void](#api-void) 19 | * [API Refund](#api-refund) 20 | * [API Fetch Transaction](#api-fetch-transaction) 21 | * [Hosted Payment Page](#hosted-payment-page) 22 | * [Hosted Payment Page Authorize/Purchase](#hosted-payment-page-authorizepurchase) 23 | * [Webhook Notifications](#webhook-notifications) 24 | 25 | # Omnipay-AuthorizeNetApi 26 | 27 | Omnipay 3.x implementation of Authorize.Net API 28 | 29 | # Installation 30 | 31 | composer require "academe/omnipay-authorizenetapi: ~3.0" 32 | 33 | # Authorize.Net API 34 | 35 | The *Authorize.Net API* driver handles server-to-server requests. 36 | It is used both for direct card payment (though check PCI requirements) 37 | and for creating transactions using a card token generated client-side. 38 | 39 | ## API Authorize/Purchase (Credit Card) 40 | 41 | The following example is a simple authorize with supplied card details. 42 | *You would normally avoid allowing card details near your merchant site 43 | back end for PCI compliance reasons, 44 | supplying a tokenised card reference instead (see later section for this).* 45 | 46 | ```php 47 | setAuthName('XXXXXxxxxxx'); 54 | $gateway->setTransactionKey('XXXXX99999xxxxx'); 55 | $gateway->setTestMode(true); 56 | 57 | $creditCard = new Omnipay\Common\CreditCard([ 58 | // Swiped tracks can be provided instead, if the card is present. 59 | 'number' => '4000123412341234', 60 | 'expiryMonth' => '12', 61 | 'expiryYear' => '2020', 62 | 'cvv' => '123', 63 | // Billing and shipping details can be added here. 64 | ]); 65 | 66 | // Generate a unique merchant site transaction ID. 67 | $transactionId = rand(100000000, 999999999); 68 | 69 | $response = $gateway->authorize([ 70 | 'amount' => '7.99', 71 | 'currency' => 'USD', 72 | 'transactionId' => $transactionId, 73 | 'card' => $creditCard, 74 | // Additional optional attributes: 75 | 'customerId' => '123456', 76 | 'customerType' => \Academe\AuthorizeNet\Request\Model\Customer::CUSTOMER_TYPE_INDIVIDUAL, 77 | 'customerDriversLicense' => [ 78 | 'number' => '123456', 79 | 'state' => 'NY', 80 | 'dateOfBirth' => '1967-01-01', 81 | ], 82 | 'customerTaxId' => 'TAX456', 83 | ])->send(); 84 | 85 | // Or use $gateway->purchase() to immediately capture. 86 | 87 | var_dump($response->isSuccessful()); 88 | // bool(true) 89 | 90 | var_dump($response->getCode()); 91 | // string(1) "1" 92 | 93 | var_dump($response->getMessage()); 94 | // string(35) "This transaction has been approved." 95 | 96 | var_dump($response->getTransactionReference()); 97 | // string(11) "60103474871" 98 | ``` 99 | 100 | ## API Capture 101 | 102 | Once authorized, the amount can be captured: 103 | 104 | ```php 105 | // Captured from the authorization response. 106 | $transactionReference = $response->getTransactionReference(); 107 | 108 | $response = $gateway->capture([ 109 | 'amount' => '7.99', 110 | 'currency' => 'USD', 111 | 'transactionReference' => $transactionReference, 112 | ])->send(); 113 | ``` 114 | 115 | ## API Authorize/Purchase (Opaque Data) 116 | 117 | The "Opaque Data" here is a tokenised credit or debit card. 118 | Authorize.Net can tokenise cards in a number of ways, one of which 119 | is through the `accept.js` package on the front end. 120 | It works like this: 121 | 122 | You build a payment form in your page. 123 | As well as hard-coding it as shown below, the gateway provides a method 124 | you can use to generate it dynamically. 125 | 126 | ```html 127 |
130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 |
138 | ``` 139 | 140 | Note the card detail elements do not have names, so will not be submitted 141 | to your site. This is important for PCI reasons. 142 | Two hidden fields are defined to carry the opaque data to your site. 143 | You can include as many other fields as you like in the same form, 144 | which may include a name and an address. 145 | 146 | After the payment form, you will need the `accept.js` JavaScript: 147 | 148 | ```javascript 149 | 153 | ``` 154 | 155 | Use the `https://js.authorize.net/v1/Accept.js` URL for production. 156 | 157 | You need to catch the "Pay Now" submission and send it to a function to 158 | process the card details. Either an `onclick` attribute or a jQuery event 159 | will work. For example: 160 | 161 | 162 | 163 | The `sendPaymentDataToAnet` function handles the tokenisation. 164 | 165 | ```javascript 166 | 192 | ``` 193 | 194 | The response handler is able to provide errors that may have been 195 | generated while trying to tokenise the card. 196 | But if all is well, it updates the payment form with the opaque data 197 | (another function `paymentFormUpdate`): 198 | 199 | ```javascript 200 | function responseHandler(response) { 201 | if (response.messages.resultCode === "Error") { 202 | var i = 0; 203 | while (i < response.messages.message.length) { 204 | console.log( 205 | response.messages.message[i].code + ": " + 206 | response.messages.message[i].text 207 | ); 208 | i = i + 1; 209 | } 210 | } else { 211 | paymentFormUpdate(response.opaqueData); 212 | } 213 | } 214 | ``` 215 | 216 | Populate the opaque data hidden form items, then finally submit the form: 217 | 218 | ```javascript 219 | function paymentFormUpdate(opaqueData) { 220 | document.getElementById("opaqueDataDescriptor").value = opaqueData.dataDescriptor; 221 | document.getElementById("opaqueDataValue").value = opaqueData.dataValue; 222 | document.getElementById("paymentForm").submit(); 223 | } 224 | ``` 225 | 226 | Back at the server, you will have two opaque data fields to capture: 227 | 228 | * opaqueDataDescriptor 229 | * opaqueDataValue 230 | 231 | Initiate an `authorize()` or `purchase()` at the backend, as described in 232 | the previous section. In the `creditCard` object, leave the card details 233 | blank, not set. Instead, send the opaque data: 234 | 235 | ```php 236 | $request = $gateway->authorize([ 237 | ... 238 | 'opaqueDataDescriptor' => $opaqueDataDescriptor, 239 | 'opaqueDataValue' => $opaqueDataValue, 240 | ]); 241 | ``` 242 | 243 | or 244 | 245 | ```php 246 | $request->setOpaqueData($opaqueDataDescriptor, $opaqueDataValue); 247 | ``` 248 | 249 | or join with a colon (:) to handle as a card token: 250 | 251 | ```php 252 | $request->setToken($opaqueDataDescriptor . ':' . $opaqueDataValue); 253 | ``` 254 | 255 | The authorize or purchase should then go ahead as though the card 256 | details were provided directly. In the result, the last four digits 257 | of the card will be made available in case a refund needs to be performed. 258 | 259 | Further details can be 260 | [found in the official documentation](https://developer.authorize.net/api/reference/features/acceptjs.html). 261 | 262 | Note also that the opaque data is used for other payment sources, such as 263 | bank accounts and PayPal. 264 | 265 | ## API Void 266 | 267 | An authorized transaction can be voided: 268 | 269 | ```php 270 | // Captured from the authorization response. 271 | $transactionReference = $response->getTransactionReference(); 272 | 273 | $response = $gateway->void([ 274 | 'transactionReference' => $transactionReference, 275 | ])->send(); 276 | ``` 277 | 278 | ## API Refund 279 | 280 | A cleared credit card payment can be refunded, given the original 281 | transaction reference, the original amount, and the last four digits 282 | of the credit card: 283 | 284 | ```php 285 | $response = $gateway->refund([ 286 | 'amount' => '7.99', 287 | 'currency' => 'USD', 288 | 'transactionReference' => $transactionReference, 289 | 'numberLastFour' => '1234', 290 | ])->send(); 291 | ``` 292 | 293 | ## API Fetch Transaction 294 | 295 | An existing transaction can be fetched from the gateway given 296 | its `transactionReference`: 297 | 298 | ```php 299 | $response = $gateway->fetchTransaction([ 300 | 'transactionReference' => $transactionReference, 301 | ])->send(); 302 | ``` 303 | 304 | The Hosted Payment Page will host the payment form on the gateway. 305 | The form can be presented to the user as a full page redirect or in an iframe. 306 | 307 | # Hosted Payment Page 308 | 309 | The Hosted Payment Page is a different gateway: 310 | 311 | ```php 312 | $gateway = Omnipay\Omnipay::create('AuthorizeNetApi_HostedPage'); 313 | ``` 314 | 315 | The gateway is configured the same way as the direct API gateway, 316 | and the authorize/purchase 317 | requests are created in the same way, except for the addition of 318 | `return` and `cancel` URLs: 319 | 320 | ## Hosted Payment Page Authorize/Purchase 321 | 322 | ```php 323 | $request = $gateway->authorize([ 324 | 'amount' => $amount, 325 | // etc. 326 | 'returnUrl' => 'return URL after the transaction is approved or rejected', 327 | 'cancelUrl' => 'URL to use if the user cancels the transaction', 328 | ]); 329 | ``` 330 | 331 | The response will be a redirect, with the following details used to 332 | construct the redirect in the merchant site: 333 | 334 | ```php 335 | $response = $request->send(); 336 | 337 | $response->getRedirectMethod(); 338 | // Usually "POST" 339 | 340 | $response->getRedirectUrl(); 341 | // The redirect URL or POST form action. 342 | 343 | $response->getRedirectData() 344 | // Array of name/value elements used to construct hidden fields 345 | // in the POST form. 346 | ``` 347 | 348 | A naive POST "pay now" button may look like the following form. 349 | 350 | ```php 351 | $method = $response->getRedirectMethod(); 352 | $action = $response->getRedirectUrl(); 353 | 354 | echo "
"; 355 | foreach ($response->getRedirectData() as $name => $value) { 356 | $dataName = htmlspecialchars($name); 357 | $dataValue = htmlspecialchars($value); 358 | 359 | echo ""; 360 | } 361 | echo ""; 362 | echo "
"; 363 | ``` 364 | 365 | This will take the user to the gateway payment page, looking something 366 | like this by default: 367 | 368 | ------ 369 | ![Default Gateway Payment Page](docs/authorizenet-default-payment-form.png) 370 | ------ 371 | 372 | The billing details will be prefilled with the card details supplied 373 | in the `$gateway->authorize()`. 374 | What the user can change and/or see, can be changed using options or 375 | confiration in the account. 376 | 377 | Taking the `hostedPaymentPaymentOptions` as an example, 378 | this is how the options are set: 379 | 380 | The [documentation](https://developer.authorize.net/api/reference/features/accept_hosted.html) 381 | lists `hostedPaymentPaymentOptions` as supporting these options: 382 | `{"cardCodeRequired": false, "showCreditCard": true, "showBankAccount": true}` 383 | 384 | To set any of the options, drop the `hostedPayment` prefix from the options 385 | name, then append with the specific option you want to set, and use the 386 | result as the parameter, keeping the name in *camelCase*. 387 | So the above set of options are supported by the following parameters: 388 | 389 | * paymentOptionsCardCodeRequired 390 | * paymentOptionsShowCreditCard 391 | * paymentOptionsShowBankAccount 392 | 393 | You can set these in the `authorize()` stage: 394 | 395 | ```php 396 | $request = $gateway->authorize([ 397 | ... 398 | // Hide the bank account form but show the credit card form. 399 | 'paymentOptionsShowCreditCard' => true, 400 | 'paymentOptionsShowBankAccount' => false, 401 | // Change the "Pay" buton text. 402 | 'buttonOptionsText' => 'Pay now', 403 | ]); 404 | ``` 405 | 406 | or use the `set*()` form to do the same thing: 407 | 408 | $request->setPaymentOptionsShowBankAccount(false); 409 | 410 | # Webhook Notifications 411 | 412 | The Authorize.Net gateway provides a rich set of webhooks to notify the 413 | merchant site (and/or other backend systems) about events related to 414 | customers or payments. 415 | The [current documentation can be found here](https://developer.authorize.net/api/reference/features/webhooks.html). 416 | 417 | For some API methods, such as the Hosted Payment Page, the webhooks 418 | are necessary for operation. For other API methods they provide additional 419 | information. 420 | 421 | The webhooks can be configured in the Authorize.Net account settings page. 422 | They can also be fully managed through a REST API, so that a merchant 423 | site can register for all the webhooks that it needs. 424 | *Note that the webhook management RESTful API has not yet been implemented here.* 425 | 426 | Your notification handler is set up like this at your webhook endpoint: 427 | 428 | ```php 429 | $gateway = Omnipay::create('AuthorizeNetApi_Api'); 430 | 431 | $gateway->setAuthName($authName); 432 | $gateway->setTransactionKey($authKey); 433 | $gateway->setSignatureKey($signatureKey); // HMAC-256 434 | $gateway->setTestMode(true); // for false 435 | 436 | $notification = $gateway->acceptNotification(); 437 | ``` 438 | 439 | This will read and parse the webhook `POST` data. 440 | The raw nested array data can be found at: 441 | 442 | $notification->getData(); 443 | 444 | The parsed `Notification` value object can be found at: 445 | 446 | $notification->getParsedData(); 447 | 448 | Some details that describe the nature of the notification are: 449 | 450 | ```php 451 | // The main target: payment or customer 452 | $notification->getEventTarget(); 453 | 454 | // The event subtarget. e.g. capture, fraud, void, subscription 455 | $notification->getEventSubtarget(); 456 | 457 | // The event action. e.g. created, updated, deleted, held, approved, declined 458 | $notification->getEventMethod(); 459 | ``` 460 | 461 | See here for a full list of the target, subtarget and actions: 462 | https://github.com/academe/authorizenet-objects/blob/master/src/ServerRequest/Notification.php#L24 463 | 464 | For those notifications that contain the `transactionReference`, this can be 465 | obtained: 466 | 467 | $notification->getTransactionReference(); 468 | 469 | For any notifications that do not involve a transaction, this will be `null`. 470 | Note that the webhook does not include the merchant `transactionId`, 471 | so there is nothing to tie the payment webhook back to a Hosted Page request. 472 | In this case, you can supply the `transactionId` as a query parameter 473 | on the `notifyUrl` when creating the Hosted Payment Page data. 474 | However, do be aware this ID will be visible to end users monitoring 475 | browser traffic, and the ID (being in the URL) will not be included 476 | in the notification signing, so could be faked. It is unlikely, but just 477 | be aware that it is a potential attack vector, so maybe self-sign the URL 478 | too. 479 | 480 | Notifications can be signed by the gateway using a `signatureKey`. 481 | By default, this notification handler will verify the `signature` 482 | and throw an exception if it failes to validate against the key 483 | you provide when fetching the result of the transaction. 484 | 485 | A manual check of the `signature` can be made using: 486 | 487 | $notification->isSignatureValid() 488 | 489 | This will return `true` if the signature is valid, or `false` if any 490 | part of the verification process fails. 491 | 492 | Validation of the `signature` can be disabled if needed: 493 | 494 | $gateway->setDisableWebhookSignature(true); 495 | 496 | For consistency with other Omipay Drivers, this driver *may* make an 497 | opinionated decision on how the `transactionId` is passed into the 498 | notification handler, but only after researchign how other people are 499 | handling it. 500 | There is a front-end way to do it through an iframe, but it seems 501 | vulnerable to user manipulation to me. 502 | 503 | --------------------------------------------------------------------------------