diff --git a/composer.json b/composer.json index 402d88fb..2cd0e1c0 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "psr-4": { "Omnipay\\AuthorizeNet\\" : "src/" } }, "require": { - "omnipay/common": "~2.2" + "omnipay/common": "~2.5" }, "require-dev": { "omnipay/tests": "~2.0" diff --git a/src/AIMGateway.php b/src/AIMGateway.php index 6955017e..37f3c883 100644 --- a/src/AIMGateway.php +++ b/src/AIMGateway.php @@ -14,6 +14,20 @@ */ class AIMGateway extends AbstractGateway { + /** + * The device type collecting credit card data. + */ + const DEVICE_TYPE_UNKNOWN = 1; + const DEVICE_TYPE_UNATTENDED_TERMINAL = 2; + const DEVICE_TYPE_SELF_SERVICE_TERMINAL = 3; + const DEVICE_TYPE_ELECTRONIC_CASH_REGISTER = 4; + const DEVICE_TYPE_PC_TERMINAL = 5; + const DEVICE_TYPE_AIRPAY = 6; + const DEVICE_TYPE_WIRELESS_POS = 7; + const DEVICE_TYPE_WEBSITE = 8; + const DEVICE_TYPE_DIAL_TERMINAL = 9; + const DEVICE_TYPE_VIRTUAL_TERMINAL = 10; + public function getName() { return 'Authorize.Net AIM'; @@ -29,6 +43,7 @@ public function getDefaultParameters() 'hashSecret' => '', 'liveEndpoint' => 'https://api2.authorize.net/xml/v1/request.api', 'developerEndpoint' => 'https://apitest.authorize.net/xml/v1/request.api', + 'deviceType' => static::DEVICE_TYPE_UNKNOWN ); } @@ -108,6 +123,35 @@ public function setDuplicateWindow($value) return $this->setParameter('duplicateWindow', $value); } + public function getDeviceType() + { + return $this->getParameter('deviceType'); + } + + /** + * Sets the type of device used to collect the credit card data. + * A device type is required for card present transactions. + * + * 1 = Unknown + * 2 = Unattended Terminal + * 3 = Self Service Terminal + * 4 = Electronic Cash Register + * 5 = Personal Computer-Based Terminal + * 6 = AirPay + * 7 = Wireless POS + * 8 = Website + * 9 = Dial Terminal + * 10 = Virtual Terminal + * + * @see http://developer.authorize.net/api/reference/#payment-transactions-charge-a-credit-card + * @param $value + * @return $this + */ + public function setDeviceType($value) + { + return $this->setParameter('deviceType', $value); + } + /** * @param array $parameters * @return AIMAuthorizeRequest diff --git a/src/Message/AIMAbstractRequest.php b/src/Message/AIMAbstractRequest.php index 660714f7..a284db1c 100644 --- a/src/Message/AIMAbstractRequest.php +++ b/src/Message/AIMAbstractRequest.php @@ -110,6 +110,16 @@ public function setSolutionId($value) return $this->setParameter('solutionId', $value); } + public function getDeviceType() + { + return $this->getParameter('deviceType'); + } + + public function setDeviceType($value) + { + return $this->setParameter('deviceType', $value); + } + /** * @return TransactionReference */ @@ -325,18 +335,23 @@ protected function addTransactionSettings(\SimpleXMLElement $data) $i = 0; // The test mode setting indicates whether or not this is a live request or a test request - $data->transactionRequest->transactionSettings->setting[$i]->settingName = 'testRequest'; - $data->transactionRequest->transactionSettings->setting[$i]->settingValue = $this->getTestMode() - ? 'true' - : 'false'; + $transactionRequest = $data->transactionRequest; + $transactionRequest->transactionSettings->setting[$i]->settingName = 'testRequest'; + $transactionRequest->transactionSettings->setting[$i]->settingValue = $this->getTestMode() ? 'true' : 'false'; // The duplicate window setting specifies the threshold for AuthorizeNet's duplicate transaction detection logic if (!is_null($this->getDuplicateWindow())) { $i++; - $data->transactionRequest->transactionSettings->setting[$i]->settingName = 'duplicateWindow'; - $data->transactionRequest->transactionSettings->setting[$i]->settingValue = $this->getDuplicateWindow(); + $transactionRequest->transactionSettings->setting[$i]->settingName = 'duplicateWindow'; + $transactionRequest->transactionSettings->setting[$i]->settingValue = $this->getDuplicateWindow(); } return $data; } + + protected function isCardPresent() + { + // If the credit card has track data, then consider this a "card present" scenario + return ($card = $this->getCard()) && $card->getTracks(); + } } diff --git a/src/Message/AIMAuthorizeRequest.php b/src/Message/AIMAuthorizeRequest.php index a4f0a58e..84f5732d 100644 --- a/src/Message/AIMAuthorizeRequest.php +++ b/src/Message/AIMAuthorizeRequest.php @@ -20,6 +20,7 @@ public function getData() $this->addSolutionId($data); $this->addBillingData($data); $this->addCustomerIP($data); + $this->addRetail($data); $this->addTransactionSettings($data); return $data; @@ -36,13 +37,29 @@ protected function addPayment(\SimpleXMLElement $data) return; } + // The CreditCard object must be present. $this->validate('card'); + /** @var CreditCard $card */ $card = $this->getCard(); - $card->validate(); - $data->transactionRequest->payment->creditCard->cardNumber = $card->getNumber(); - $data->transactionRequest->payment->creditCard->expirationDate = $card->getExpiryDate('my'); - $data->transactionRequest->payment->creditCard->cardCode = $card->getCvv(); + + if ($card->getTracks()) { + // Card present + if ($track1 = $card->getTrack1()) { + $data->transactionRequest->payment->trackData->track1 = $track1; + } elseif ($track2 = $card->getTrack2()) { + $data->transactionRequest->payment->trackData->track2 = $track2; + } + } else { + // Card not present. + + // Validate sufficient card details have been supplied. + $card->validate(); + + $data->transactionRequest->payment->creditCard->cardNumber = $card->getNumber(); + $data->transactionRequest->payment->creditCard->expirationDate = $card->getExpiryDate('my'); + $data->transactionRequest->payment->creditCard->cardCode = $card->getCvv(); + } } protected function addCustomerIP(\SimpleXMLElement $data) @@ -52,4 +69,13 @@ protected function addCustomerIP(\SimpleXMLElement $data) $data->transactionRequest->customerIP = $ip; } } + + protected function addRetail(\SimpleXMLElement $data) + { + if ($this->isCardPresent()) { + // Retail element is required for card present transactions + $data->transactionRequest->retail->marketType = 2; + $data->transactionRequest->retail->deviceType = $this->getDeviceType(); + } + } } diff --git a/src/Message/CIMAuthorizeRequest.php b/src/Message/CIMAuthorizeRequest.php index d39fb972..612280e7 100644 --- a/src/Message/CIMAuthorizeRequest.php +++ b/src/Message/CIMAuthorizeRequest.php @@ -11,38 +11,48 @@ class CIMAuthorizeRequest extends AIMAuthorizeRequest { protected function addPayment(\SimpleXMLElement $data) { - $this->validate('cardReference'); + if ($this->isCardPresent()) { + // Prefer the track data if present over the payment profile (better rate) + return parent::addPayment($data); + } else { + $this->validate('cardReference'); - /** @var mixed $req */ - $req = $data->transactionRequest; + /** @var mixed $req */ + $req = $data->transactionRequest; - /** @var CardReference $cardRef */ - $cardRef = $this->getCardReference(false); + /** @var CardReference $cardRef */ + $cardRef = $this->getCardReference(false); - $req->profile->customerProfileId = $cardRef->getCustomerProfileId(); + $req->profile->customerProfileId = $cardRef->getCustomerProfileId(); - $req->profile->paymentProfile->paymentProfileId = $cardRef->getPaymentProfileId(); + $req->profile->paymentProfile->paymentProfileId = $cardRef->getPaymentProfileId(); - if ($shippingProfileId = $cardRef->getShippingProfileId()) { - $req->profile->shippingProfileId = $shippingProfileId; - } + if ($shippingProfileId = $cardRef->getShippingProfileId()) { + $req->profile->shippingProfileId = $shippingProfileId; + } - $invoiceNumber = $this->getInvoiceNumber(); - if (!empty($invoiceNumber)) { - $req->order->invoiceNumber = $invoiceNumber; - } + $invoiceNumber = $this->getInvoiceNumber(); + if (!empty($invoiceNumber)) { + $req->order->invoiceNumber = $invoiceNumber; + } - $description = $this->getDescription(); - if (!empty($description)) { - $req->order->description = $description; + $description = $this->getDescription(); + if (!empty($description)) { + $req->order->description = $description; + } + + return $data; } - return $data; } protected function addBillingData(\SimpleXMLElement $data) { - // Do nothing since billing information is already part of the customer profile - return $data; + if ($this->isCardPresent()) { + return parent::addBillingData($data); + } else { + // Do nothing since billing information is already part of the customer profile + return $data; + } } } diff --git a/src/Message/SIMAbstractRequest.php b/src/Message/SIMAbstractRequest.php index 4167e1fe..8fed3f12 100644 --- a/src/Message/SIMAbstractRequest.php +++ b/src/Message/SIMAbstractRequest.php @@ -94,6 +94,16 @@ public function getInvoiceNumber() return $this->getParameter('invoiceNumber'); } + public function getDeviceType() + { + return $this->getParameter('deviceType'); + } + + public function setDeviceType($value) + { + return $this->setParameter('deviceType', $value); + } + /** * Base data used only for the AIM API. */ diff --git a/tests/AIMGatewayIntegrationTest.php b/tests/AIMGatewayIntegrationTest.php index 299161d8..ed2f71a4 100644 --- a/tests/AIMGatewayIntegrationTest.php +++ b/tests/AIMGatewayIntegrationTest.php @@ -93,4 +93,21 @@ public function testPurchaseRefundAutoVoid() $response = $request->send(); $this->assertTrue($response->isSuccessful(), 'Automatic void should succeed'); } + + public function testPurchaseCardPresent() + { + $amount = rand(10, 100) . '.' . rand(0, 99); + $card = array( + 'number' => '4242424242424242', + 'expiryMonth' => rand(1, 12), + 'expiryYear' => gmdate('Y') + rand(1, 5), + 'tracks' => '%B4242424242424242^SMITH/JOHN ^2511126100000000000000444000000?;4242424242424242=25111269999944401?' + ); + $request = $this->gateway->purchase(array( + 'amount' => $amount, + 'card' => $card + )); + $response = $request->send(); + $this->assertTrue($response->isSuccessful()); + } } diff --git a/tests/Message/AIMAuthorizeRequestTest.php b/tests/Message/AIMAuthorizeRequestTest.php index e410a8f5..91d3be79 100644 --- a/tests/Message/AIMAuthorizeRequestTest.php +++ b/tests/Message/AIMAuthorizeRequestTest.php @@ -58,6 +58,8 @@ public function testGetData() $setting = $data->transactionRequest->transactionSettings->setting[0]; $this->assertEquals('testRequest', $setting->settingName); $this->assertEquals('false', $setting->settingValue); + $this->assertObjectNotHasAttribute('trackData', $data->transactionRequest->payment); + $this->assertObjectNotHasAttribute('retail', $data->transactionRequest); } public function testGetDataTestMode() @@ -89,4 +91,53 @@ public function testShouldIncludeDuplicateWindowSetting() $this->assertEquals('duplicateWindow', $setting->settingName); $this->assertEquals('0', $setting->settingValue); } + + public function testGetDataCardPresentTrack1() + { + $card = $this->getValidCard(); + $card['tracks'] = '%B4242424242424242^SMITH/JOHN ^2511126100000000000000444000000?;4242424242424242=25111269999944401?'; + + // If sending tracks, then the card number and expiry date are not required. + unset($card['number']); + unset($card['expiryMonth']); + unset($card['expiryYear']); + unset($card['cvv']); + + $this->request->initialize(array( + 'amount' => '12.12', + 'card' => $card, + 'deviceType' => 1 + )); + + $data = $this->request->getData(); + + $this->assertEquals('12.12', $data->transactionRequest->amount); + $this->assertEquals( + '%B4242424242424242^SMITH/JOHN ^2511126100000000000000444000000?', + $data->transactionRequest->payment->trackData->track1); + $this->assertObjectNotHasAttribute('creditCard', $data->transactionRequest->payment); + $this->assertEquals('2', $data->transactionRequest->retail->marketType); + $this->assertEquals('1', $data->transactionRequest->retail->deviceType); + } + + public function testGetDataCardPresentTrack2() + { + $card = $this->getValidCard(); + $card['tracks'] = ';4242424242424242=25111269999944401?'; + $this->request->initialize(array( + 'amount' => '12.12', + 'card' => $card, + 'deviceType' => 1 + )); + + $data = $this->request->getData(); + + $this->assertEquals('12.12', $data->transactionRequest->amount); + $this->assertEquals( + ';4242424242424242=25111269999944401?', + $data->transactionRequest->payment->trackData->track2); + $this->assertObjectNotHasAttribute('creditCard', $data->transactionRequest->payment); + $this->assertEquals('2', $data->transactionRequest->retail->marketType); + $this->assertEquals('1', $data->transactionRequest->retail->deviceType); + } } diff --git a/tests/Message/CIMAuthorizeRequestTest.php b/tests/Message/CIMAuthorizeRequestTest.php index 819d855e..e2b0beb7 100644 --- a/tests/Message/CIMAuthorizeRequestTest.php +++ b/tests/Message/CIMAuthorizeRequestTest.php @@ -33,4 +33,19 @@ public function testGetData() $this->assertEquals('00001234', $data->transactionRequest->order->invoiceNumber); $this->assertEquals('Test authorize transaction', $data->transactionRequest->order->description); } + + public function testShouldUseTrackDataIfCardPresent() + { + $card = $this->getValidCard(); + $card['tracks'] = '%B4242424242424242^SMITH/JOHN ^2511126100000000000000444000000?;4242424242424242=25111269999944401?'; + $this->request->initialize(array( + 'card' => $card, + 'amount' => 21.00 + )); + + $data = $this->request->getData(); + + $this->assertObjectNotHasAttribute('profile', $data->transactionRequest); + $this->assertObjectHasAttribute('trackData', $data->transactionRequest->payment); + } }