diff --git a/README.md b/README.md index 888a667..05cb04d 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ This package implements the following methods: * ``refund($options)`` – refund an already processed (settled) transaction. * ``void($options)`` – reverse a previously authorized (unsettled) transaction. * ``status($options)`` – check the status of a previous transaction. +* ``createCard($options)`` – create a stored card and return the reference token for future transactions. +* ``updateCard($options)`` – update a stored card's expiry or customer reference. For general usage instructions, please see the [Omnipay documentation](http://omnipay.thephpleague.com/). diff --git a/src/CreditCard.php b/src/CreditCard.php new file mode 100644 index 0000000..3c6414a --- /dev/null +++ b/src/CreditCard.php @@ -0,0 +1,69 @@ +parameters->get($key); + + if ( empty($value) ) + { + throw new InvalidCreditCardException("The $key parameter is required"); + } + } + + if ( isset($parameters['expiryMonth']) && isset($parameters['expiryYear']) ) + { + if ( $this->getExpiryDate('Ym') < gmdate('Ym') ) + { + throw new InvalidCreditCardException('Card has expired'); + } + } + + if ( isset($parameters['number']) ) + { + if ( !Helper::validateLuhn( $this->getNumber() ) ) + { + throw new InvalidCreditCardException('Card number is invalid'); + } + + if ( !is_null( $this->getNumber() ) && !preg_match( '/^\d{12,19}$/i', $this->getNumber() ) ) + { + throw new InvalidCreditCardException('Card number should have 12 to 19 digits'); + } + } + + if ( isset($parameters['cvv']) ) + { + if ( !is_null( $this->getCvv() ) && !preg_match( '/^\d{3,4}$/i', $this->getCvv() ) ) + { + throw new InvalidCreditCardException('Card CVV should have 3 to 4 digits'); + } + } + } +} diff --git a/src/Gateway.php b/src/Gateway.php index fe51c44..1ffa163 100644 --- a/src/Gateway.php +++ b/src/Gateway.php @@ -105,4 +105,28 @@ public function status(array $parameters = []) { return $this->createRequest('\Omnipay\FirstAtlanticCommerce\Message\StatusRequest', $parameters); } + + /** + * Create a stored card and return the reference token for future transactions. + * + * @param array $parameters + * + * @return \Omnipay\FirstAtlanticCommerce\Message\CreateCardRequest + */ + public function createCard(array $parameters = []) + { + return $this->createRequest('\Omnipay\FirstAtlanticCommerce\Message\CreateCardRequest', $parameters); + } + + /** + * Update a stored card. + * + * @param array $parameters + * + * @return \Omnipay\FirstAtlanticCommerce\Message\UpdateCardRequest + */ + public function updateCard(array $parameters = []) + { + return $this->createRequest('\Omnipay\FirstAtlanticCommerce\Message\UpdateCardRequest', $parameters); + } } diff --git a/src/Message/AbstractRequest.php b/src/Message/AbstractRequest.php index f92e70c..f186fbd 100644 --- a/src/Message/AbstractRequest.php +++ b/src/Message/AbstractRequest.php @@ -3,6 +3,7 @@ namespace Omnipay\FirstAtlanticCommerce\Message; use Omnipay\Common\Message\AbstractRequest as BaseAbstractRequest; +use Omnipay\FirstAtlanticCommerce\CreditCard; use Omnipay\FirstAtlanticCommerce\ParameterTrait; use SimpleXMLElement; @@ -125,4 +126,21 @@ protected function xmlSerialize($data, $xml = null) return $xml->asXML(); } + + /** + * Sets the card. + * + * @param CreditCard $value + * + * @return AbstractRequest Provides a fluent interface + */ + public function setCard($value) + { + if ($value && !$value instanceof CreditCard) + { + $value = new CreditCard($value); + } + + return $this->setParameter('card', $value); + } } diff --git a/src/Message/AuthorizeRequest.php b/src/Message/AuthorizeRequest.php index b0cf708..bbe5832 100644 --- a/src/Message/AuthorizeRequest.php +++ b/src/Message/AuthorizeRequest.php @@ -3,6 +3,7 @@ namespace Omnipay\FirstAtlanticCommerce\Message; use Alcohol\ISO3166; +use Omnipay\Common\Exception\InvalidRequestException; use Omnipay\FirstAtlanticCommerce\Message\AbstractRequest; /** @@ -48,7 +49,42 @@ protected function generateSignature() public function getData() { $this->validate('merchantId', 'merchantPassword', 'acquirerId', 'transactionId', 'amount', 'currency', 'card'); - $this->getCard()->validate(); + + // Check for AVS and require billingAddress1 and billingPostcode + if ( $this->getRequireAvsCheck() ) + { + $this->getCard()->validate('billingAddress1', 'billingPostcode'); + } + + // Tokenized cards require the CVV and nothing else, token replaces the card number + if ( $this->getCardReference() ) + { + $this->validate('cardReference'); + $this->getCard()->validate('cvv', 'expiryMonth', 'expiryYear'); + + $cardDetails = [ + 'CardCVV2' => $this->getCard()->getCvv(), + 'CardExpiryDate' => $this->getCard()->getExpiryDate('my'), + 'CardNumber' => $this->getCardReference() + ]; + } + else + { + $this->getCard()->validate(); + + $cardDetails = [ + 'CardCVV2' => $this->getCard()->getCvv(), + 'CardExpiryDate' => $this->getCard()->getExpiryDate('my'), + 'CardNumber' => $this->getCard()->getNumber(), + 'IssueNumber' => $this->getCard()->getIssueNumber() + ]; + } + + // Only pass the StartDate if year/month are set otherwise it returns 1299 + if ( $this->getCard()->getStartYear() && $this->getCard()->getStartMonth() ) + { + $cardDetails['StartDate'] = $this->getCard()->getStartDate('my'); + } $transactionDetails = [ 'AcquirerId' => $this->getAcquirerId(), @@ -63,19 +99,6 @@ public function getData() 'TransactionCode' => $this->getTransactionCode() ]; - $cardDetails = [ - 'CardCVV2' => $this->getCard()->getCvv(), - 'CardExpiryDate' => $this->getCard()->getExpiryDate('my'), - 'CardNumber' => $this->getCard()->getNumber(), - 'IssueNumber' => $this->getCard()->getIssueNumber() - ]; - - // Only pass the StartDate if year/month are set otherwise it returns 1299 - if ( $this->getCard()->getStartYear() && $this->getCard()->getStartMonth() ) - { - $cardDetails['StartDate'] = $this->getCard()->getStartDate('my'); - } - $billingDetails = [ 'BillToAddress' => $this->getCard()->getAddress1(), 'BillToZipPostCode' => $this->getCard()->getPostcode(), @@ -114,7 +137,7 @@ protected function formatCountry() { $country = $this->getCard()->getCountry(); - if ( !is_numeric($country) ) + if ( !is_null($country) && !is_numeric($country) ) { $iso3166 = new ISO3166; diff --git a/src/Message/CreateCardRequest.php b/src/Message/CreateCardRequest.php new file mode 100644 index 0000000..e13c400 --- /dev/null +++ b/src/Message/CreateCardRequest.php @@ -0,0 +1,95 @@ +getMerchantPassword(); + $signature .= $this->getMerchantId(); + $signature .= $this->getAcquirerId(); + + return base64_encode( sha1($signature, true) ); + } + + /** + * Validate and construct the data for the request + * + * @return array + */ + public function getData() + { + $this->validate('merchantId', 'merchantPassword', 'acquirerId', 'customerReference', 'card'); + $this->getCard()->validate(); + + $data = [ + 'CardNumber' => $this->getCard()->getNumber(), + 'CustomerReference' => $this->getCustomerReference(), + 'ExpiryDate' => $this->getCard()->getExpiryDate('my'), + 'MerchantNumber' => $this->getMerchantId(), + 'Signature' => $this->generateSignature() + ]; + + return $data; + } + + /** + * Get the customer reference. + * + * @return string + */ + public function getCustomerReference() + { + return $this->getParameter('customerReference'); + } + + /** + * Set the customer reference. + * + * @param string $value + */ + public function setCustomerReference($value) + { + return $this->setParameter('customerReference', $value); + } + + /** + * Returns endpoint for tokenize requests + * + * @return string Endpoint URL + */ + protected function getEndpoint() + { + return parent::getEndpoint() . 'Tokenize'; + } + + /** + * Return the tokenize response object + * + * @param \SimpleXMLElement $xml Response xml object + * + * @return CreateCardResponse + */ + protected function newResponse($xml) + { + return new CreateCardResponse($this, $xml); + } + +} diff --git a/src/Message/CreateCardResponse.php b/src/Message/CreateCardResponse.php new file mode 100644 index 0000000..c140088 --- /dev/null +++ b/src/Message/CreateCardResponse.php @@ -0,0 +1,61 @@ +request = $request; + $this->data = $this->xmlDeserialize($data); + } + + /** + * Return whether or not the response was successful + * + * @return boolean + */ + public function isSuccessful() + { + return isset($this->data['Success']) && 'true' === $this->data['Success']; + } + + /** + * Return the response's reason message + * + * @return string + */ + public function getMessage() + { + return isset($this->data['ErrorMsg']) && !empty($this->data['ErrorMsg']) ? $this->data['ErrorMsg'] : null; + } + + /** + * Return card reference + * + * @return string + */ + public function getCardReference() + { + return isset($this->data['Token']) ? $this->data['Token'] : null; + } + +} diff --git a/src/Message/UpdateCardRequest.php b/src/Message/UpdateCardRequest.php new file mode 100644 index 0000000..b96c0e8 --- /dev/null +++ b/src/Message/UpdateCardRequest.php @@ -0,0 +1,95 @@ +getMerchantPassword(); + $signature .= $this->getMerchantId(); + $signature .= $this->getAcquirerId(); + + return base64_encode( sha1($signature, true) ); + } + + /** + * Validate and construct the data for the request + * + * @return array + */ + public function getData() + { + $this->validate('merchantId', 'merchantPassword', 'acquirerId', 'customerReference', 'cardReference', 'card'); + $this->getCard()->validate(); + + $data = [ + 'CustomerReference' => $this->getCustomerReference(), + 'ExpiryDate' => $this->getCard()->getExpiryDate('my'), + 'MerchantNumber' => $this->getMerchantId(), + 'Signature' => $this->generateSignature(), + 'TokenPAN' => $this->getCardReference() + ]; + + return $data; + } + + /** + * Get the customer reference. + * + * @return string + */ + public function getCustomerReference() + { + return $this->getParameter('customerReference'); + } + + /** + * Set the customer reference. + * + * @param string $value + */ + public function setCustomerReference($value) + { + return $this->setParameter('customerReference', $value); + } + + /** + * Returns endpoint for update token requests + * + * @return string Endpoint URL + */ + protected function getEndpoint() + { + return parent::getEndpoint() . 'UpdateToken'; + } + + /** + * Return the update token response object + * + * @param \SimpleXMLElement $xml Response xml object + * + * @return UpdateCardResponse + */ + protected function newResponse($xml) + { + return new UpdateCardResponse($this, $xml); + } + +} diff --git a/src/Message/UpdateCardResponse.php b/src/Message/UpdateCardResponse.php new file mode 100644 index 0000000..f79418c --- /dev/null +++ b/src/Message/UpdateCardResponse.php @@ -0,0 +1,61 @@ +request = $request; + $this->data = $this->xmlDeserialize($data); + } + + /** + * Return whether or not the response was successful + * + * @return boolean + */ + public function isSuccessful() + { + return isset($this->data['Success']) && 'true' === $this->data['Success']; + } + + /** + * Return the response's reason message + * + * @return string + */ + public function getMessage() + { + return isset($this->data['ErrorMsg']) && !empty($this->data['ErrorMsg']) ? $this->data['ErrorMsg'] : null; + } + + /** + * Return card reference + * + * @return string + */ + public function getCardReference() + { + return isset($this->data['TokenPAN']) ? $this->data['TokenPAN'] : null; + } + +}