diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 48efa81..1ac070e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -58,6 +58,7 @@ src/ - Backward-compat typo alias (`vefiyFingerPrint`) remains supported. - DirectPost supports purchase, authorize, complete callbacks, store-only flow, and additive server-to-server operations (`capture`, `refund`, `void`). - EMV 3DS order management is exposed via `createEMV3DSOrder()`. +- Advanced optional DirectPost fields are supported for surcharge reporting, MCR card scheme routing hint, and result/callback parameter passthrough. ### 3) Hosted Payment diff --git a/README.md b/README.md index 04a5b28..2678202 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,32 @@ The following gateways are provided by this package: } ``` +#### DirectPost Advanced Optional Parameters + +```php + $response = $gateway->purchase([ + 'amount' => '112.00', + 'currency' => 'AUD', + 'transactionId' => 'ORDER-ADV-100', + 'card' => $card, + 'returnUrl' => 'https://example.com/payment/response', + 'notifyUrl' => 'https://example.com/payment/callback', + + // Optional reporting/callback field control + 'resultParams' => 'merchant,refid,rescode,restext', + 'callbackParams' => 'merchant,refid,rescode,restext', + + // Optional surcharge reporting fields + 'surchargeEnabled' => true, + 'surchargeAmount' => '12.00', + 'surchargeRate' => '5.00', + 'surchargeFee' => '7.00', + + // Optional MCR route hint + 'cardScheme' => 'scheme', // or 'eftpos' + ])->send(); +``` + ### NAB Transact DirectPost v2 UnionPay Online Payment ```php diff --git a/docs/feature-matrix.md b/docs/feature-matrix.md index 573b8db..951048a 100644 --- a/docs/feature-matrix.md +++ b/docs/feature-matrix.md @@ -37,6 +37,9 @@ Out of scope: | Reversal/void (server-to-server) | `void()` | `DirectPostReversalRequest` | Implemented | | DirectPost API response mapper | n/a | `DirectPostApiResponse` | Implemented | | Fingerprint verification helper | `webhook()` | `DirectPostWebhookRequest` | Implemented | +| Result/callback parameter passthrough | `setResultParams()/setCallbackParams()` | `DirectPostAbstractRequest` | Implemented | +| Surcharge reporting parameters | `setSurcharge*()` | `DirectPostAbstractRequest` | Implemented | +| MCR card scheme passthrough | `setCardScheme()` | `DirectPostAbstractRequest` | Implemented | | EMV 3DS txnType mapping | `setHasEMV3DSEnabled(true)` | `DirectPostAbstractRequest` | Implemented | | Risk-managed txnType mapping | `setHasRiskManagementEnabled(true)` | `DirectPostAbstractRequest` | Implemented | | Risk + EMV txnType mapping | both flags enabled | `DirectPostAbstractRequest` | Implemented | @@ -57,6 +60,7 @@ Out of scope: | UnionPay purchase redirect | `purchase()` | `UnionPayPurchaseRequest` | Implemented | | UnionPay callback completion | `completePurchase()` | `UnionPayCompletePurchaseRequest` | Implemented | | UnionPay completion response mapping | n/a | `UnionPayCompletePurchaseResponse` | Implemented | +| UPOP currency/flow guards | `purchase()` validation | `UnionPayPurchaseRequest` | Implemented | ## Transport Layer diff --git a/docs/features-implemented-tree.md b/docs/features-implemented-tree.md index 9600504..72328d0 100644 --- a/docs/features-implemented-tree.md +++ b/docs/features-implemented-tree.md @@ -33,6 +33,9 @@ omnipay-nabtransact │ │ ├── Security/txn behavior │ │ │ ├── fingerprint generation + verification │ │ │ ├── risk + EMV txnType resolution +│ │ │ ├── surcharge parameter passthrough +│ │ │ ├── card scheme (MCR) passthrough +│ │ │ └── result/callback parameter passthrough │ │ │ └── legacy typo alias support: vefiyFingerPrint() │ │ └── transport + timeout control for server-to-server calls │ │ @@ -44,7 +47,8 @@ omnipay-nabtransact │ │ │ └── NABTransact_UnionPay (UnionPayGateway) │ ├── purchase() -> UnionPayPurchaseRequest -│ └── completePurchase() -> UnionPayCompletePurchaseRequest +│ ├── completePurchase() -> UnionPayCompletePurchaseRequest +│ └── UPOP guard rails (currency/risk/EMV validation) │ ├── Response Models │ ├── SecureXMLResponse diff --git a/src/Message/DirectPostAbstractRequest.php b/src/Message/DirectPostAbstractRequest.php index 9412f16..bcd5462 100644 --- a/src/Message/DirectPostAbstractRequest.php +++ b/src/Message/DirectPostAbstractRequest.php @@ -25,6 +25,121 @@ abstract class DirectPostAbstractRequest extends AbstractRequest */ protected $txnType = '0'; + public function getResultParams() + { + return $this->getParameter('resultParams'); + } + + public function setResultParams($value) + { + return $this->setParameter('resultParams', $value); + } + + public function getCallbackParams() + { + return $this->getParameter('callbackParams'); + } + + public function setCallbackParams($value) + { + return $this->setParameter('callbackParams', $value); + } + + public function getCardScheme() + { + return $this->getParameter('cardScheme'); + } + + public function setCardScheme($value) + { + return $this->setParameter('cardScheme', $value); + } + + public function getSurchargeEnabled() + { + return $this->getParameter('surchargeEnabled'); + } + + public function setSurchargeEnabled($value) + { + return $this->setParameter('surchargeEnabled', $value); + } + + public function getSurchargeAmount() + { + return $this->getParameter('surchargeAmount'); + } + + public function setSurchargeAmount($value) + { + return $this->setParameter('surchargeAmount', $value); + } + + public function getSurchargeRate() + { + return $this->getParameter('surchargeRate'); + } + + public function setSurchargeRate($value) + { + return $this->setParameter('surchargeRate', $value); + } + + public function getSurchargeFee() + { + return $this->getParameter('surchargeFee'); + } + + public function setSurchargeFee($value) + { + return $this->setParameter('surchargeFee', $value); + } + + /** + * @param array $data + * + * @return string + */ + protected function buildFingerprintFromFields(array $data) + { + $fields = [ + 'EPS_MERCHANT', + '__TRANSACTION_PASSWORD__', + 'EPS_TXNTYPE', + 'EPS_REFERENCEID', + 'EPS_AMOUNT', + 'EPS_TIMESTAMP', + ]; + + if (isset($data['EPS_ORDERID'])) { + $fields[] = 'EPS_ORDERID'; + } + + if (isset($data['EPS_CARDSCHEME'])) { + $fields[] = 'EPS_CARDSCHEME'; + } + + if (isset($data['EPS_SURCHARGEENABLED'])) { + $fields[] = 'EPS_SURCHARGEENABLED'; + $fields[] = 'EPS_SURCHARGEAMOUNT'; + $fields[] = 'EPS_SURCHARGERATE'; + $fields[] = 'EPS_SURCHARGEFEE'; + } + + $hashable = []; + foreach ($fields as $field) { + if ($field === '__TRANSACTION_PASSWORD__') { + $hashable[] = (string) $this->getTransactionPassword(); + + continue; + } + + $hashable[] = isset($data[$field]) ? (string) $data[$field] : ''; + } + + return hash_hmac('sha256', implode('|', $hashable), $this->getTransactionPassword()); + } + /** * @return string */ @@ -54,25 +169,7 @@ protected function resolveTxnType() */ public function generateFingerprint(array $data) { - $hashable = [ - $data['EPS_MERCHANT'], - $this->getTransactionPassword(), - $data['EPS_TXNTYPE'], - $data['EPS_REFERENCEID'], - $data['EPS_AMOUNT'], - $data['EPS_TIMESTAMP'], - ]; - - if ($this->getHasEMV3DSEnabled()) { - $hashable = array_merge( - $hashable, - [$data['EPS_ORDERID']] - ); - } - - $hash = implode('|', $hashable); - - return hash_hmac('sha256', $hash, $this->getTransactionPassword()); + return $this->buildFingerprintFromFields($data); } /** @@ -95,10 +192,22 @@ public function getBaseData() $data['EPS_CALLBACKURL'] = $this->getNotifyUrl(); } + if ($resultParams = $this->getResultParams()) { + $data['EPS_RESULTPARAMS'] = $resultParams; + } + + if ($callbackParams = $this->getCallbackParams()) { + $data['EPS_CALLBACKPARAMS'] = $callbackParams; + } + if ($currency = $this->getCurrency()) { $data['EPS_CURRENCY'] = $currency; } + if ($cardScheme = $this->getCardScheme()) { + $data['EPS_CARDSCHEME'] = $cardScheme; + } + $card = $this->getParameter('card'); if ($card instanceof CreditCard) { @@ -135,6 +244,13 @@ public function getBaseData() $data['EPS_ORDERID'] = $this->getTransactionReference(); } + if ((bool) $this->getSurchargeEnabled()) { + $data['EPS_SURCHARGEENABLED'] = 'true'; + $data['EPS_SURCHARGEAMOUNT'] = (string) ($this->getSurchargeAmount() !== null ? $this->getSurchargeAmount() : '0.00'); + $data['EPS_SURCHARGERATE'] = (string) ($this->getSurchargeRate() !== null ? $this->getSurchargeRate() : '0.00'); + $data['EPS_SURCHARGEFEE'] = (string) ($this->getSurchargeFee() !== null ? $this->getSurchargeFee() : '0.00'); + } + $data['EPS_FINGERPRINT'] = $this->generateFingerprint($data); return $data; diff --git a/src/Message/DirectPostStoreRequest.php b/src/Message/DirectPostStoreRequest.php index e6c266b..229f0f5 100644 --- a/src/Message/DirectPostStoreRequest.php +++ b/src/Message/DirectPostStoreRequest.php @@ -12,6 +12,16 @@ class DirectPostStoreRequest extends DirectPostAuthorizeRequest */ public $txnType = '8'; + public function getStoreType() + { + return $this->getParameter('storeType'); + } + + public function setStoreType($value) + { + return $this->setParameter('storeType', $value); + } + /** * @return array */ @@ -23,6 +33,32 @@ public function getData() $this->setAmount('0.00'); } - return parent::getData(); + $data = parent::getData(); + $data['EPS_STORE'] = 'true'; + $data['EPS_STORETYPE'] = (string) ($this->getStoreType() ?: 'TOKEN'); + $data['EPS_FINGERPRINT'] = $this->generateFingerprint($data); + + return $data; + } + + /** + * @param array $data + */ + public function generateFingerprint(array $data) + { + if ((string) $this->txnType !== '8' || !isset($data['EPS_STORETYPE'])) { + return parent::generateFingerprint($data); + } + + $hashable = [ + $data['EPS_MERCHANT'], + $this->getTransactionPassword(), + $data['EPS_TXNTYPE'], + $data['EPS_STORETYPE'], + $data['EPS_REFERENCEID'], + $data['EPS_TIMESTAMP'], + ]; + + return hash_hmac('sha256', implode('|', $hashable), $this->getTransactionPassword()); } } diff --git a/src/Message/UnionPayPurchaseRequest.php b/src/Message/UnionPayPurchaseRequest.php index 74918ec..ba271f0 100644 --- a/src/Message/UnionPayPurchaseRequest.php +++ b/src/Message/UnionPayPurchaseRequest.php @@ -2,6 +2,8 @@ namespace Omnipay\NABTransact\Message; +use Omnipay\Common\Exception\InvalidRequestException; + /** * UnionPayPurchaseRequest. */ @@ -19,6 +21,21 @@ public function getData() { $this->validate('amount', 'returnUrl', 'transactionId'); + if ((bool) $this->getHasRiskManagementEnabled()) { + throw new InvalidRequestException('UPOP does not support risk-management transaction types.'); + } + + if ((bool) $this->getHasEMV3DSEnabled()) { + throw new InvalidRequestException('UPOP does not support EMV 3DS transaction types.'); + } + + if ($this->getCurrency() !== null) { + $currency = strtoupper((string) $this->getCurrency()); + if (!in_array($currency, ['AUD', 'CNY'], true)) { + throw new InvalidRequestException('UPOP only supports AUD or CNY currencies.'); + } + } + $data = $this->getBaseData(); $data['EPS_PAYMENTCHOICE'] = 'UPOP'; diff --git a/tests/Message/DirectPostAdvancedFieldsRequestTest.php b/tests/Message/DirectPostAdvancedFieldsRequestTest.php new file mode 100644 index 0000000..304c98f --- /dev/null +++ b/tests/Message/DirectPostAdvancedFieldsRequestTest.php @@ -0,0 +1,46 @@ +getHttpClient(), $this->getHttpRequest()); + $request->initialize([ + 'merchantId' => 'XYZ0010', + 'transactionPassword' => 'abcd1234', + 'amount' => '112.00', + 'currency' => 'AUD', + 'returnUrl' => 'https://www.example.com/return', + 'notifyUrl' => 'https://www.example.com/callback', + 'transactionId' => 'ORDER-ADV-100', + 'card' => [ + 'number' => '4444333322221111', + 'expiryMonth' => '12', + 'expiryYear' => '2030', + 'cvv' => '123', + ], + 'surchargeEnabled' => true, + 'surchargeAmount' => '12.00', + 'surchargeRate' => '5.00', + 'surchargeFee' => '7.00', + 'cardScheme' => 'scheme', + 'resultParams' => 'merchant,refid,rescode,restext', + 'callbackParams' => 'merchant,refid,rescode,restext', + ]); + + $data = $request->getData(); + + $this->assertSame('scheme', $data['EPS_CARDSCHEME']); + $this->assertSame('true', $data['EPS_SURCHARGEENABLED']); + $this->assertSame('12.00', $data['EPS_SURCHARGEAMOUNT']); + $this->assertSame('5.00', $data['EPS_SURCHARGERATE']); + $this->assertSame('7.00', $data['EPS_SURCHARGEFEE']); + $this->assertSame('merchant,refid,rescode,restext', $data['EPS_RESULTPARAMS']); + $this->assertSame('merchant,refid,rescode,restext', $data['EPS_CALLBACKPARAMS']); + } +} + diff --git a/tests/Message/DirectPostStoreRequestTest.php b/tests/Message/DirectPostStoreRequestTest.php index 6d57851..416ad00 100644 --- a/tests/Message/DirectPostStoreRequestTest.php +++ b/tests/Message/DirectPostStoreRequestTest.php @@ -31,6 +31,21 @@ public function testStoreDefaultsAmountAndUsesStoreTxnType() $this->assertSame('8', $data['EPS_TXNTYPE']); $this->assertSame('0.00', $data['EPS_AMOUNT']); + $this->assertSame('true', $data['EPS_STORE']); + $this->assertSame('TOKEN', $data['EPS_STORETYPE']); $this->assertArrayHasKey('EPS_FINGERPRINT', $data); } + + public function testStoreFingerprintIncludesStoreType() + { + $this->request->setStoreType('TOKEN'); + + $data = $this->request->getData(); + $data['EPS_TIMESTAMP'] = '20190215173250'; + + $this->assertSame( + '1eaf7fc13922cd2222de4866cb41ded4c4c7358cb2a2728759aaf4efb3dd015e', + $this->request->generateFingerprint($data) + ); + } } diff --git a/tests/Message/UnionPayPurchaseRequestTest.php b/tests/Message/UnionPayPurchaseRequestTest.php index a264366..74a968f 100644 --- a/tests/Message/UnionPayPurchaseRequestTest.php +++ b/tests/Message/UnionPayPurchaseRequestTest.php @@ -2,6 +2,7 @@ namespace Omnipay\NABTransact\Message; +use Omnipay\Common\Exception\InvalidRequestException; use Omnipay\NABTransact\Tests\Support\TestCase; class UnionPayPurchaseRequestTest extends TestCase @@ -47,4 +48,34 @@ public function testPurchase() $this->assertSame('GET', $response->getRedirectMethod()); $this->assertArrayHasKey('EPS_FINGERPRINT', $response->getData()); } + + public function testRejectsUnsupportedCurrencyForUpop() + { + $this->request->setCurrency('USD'); + + $this->expectException(InvalidRequestException::class); + $this->expectExceptionMessage('UPOP only supports AUD or CNY currencies.'); + + $this->request->getData(); + } + + public function testRejectsRiskManagementForUpop() + { + $this->request->setHasRiskManagementEnabled(true); + + $this->expectException(InvalidRequestException::class); + $this->expectExceptionMessage('UPOP does not support risk-management transaction types.'); + + $this->request->getData(); + } + + public function testRejectsEmvForUpop() + { + $this->request->setHasEMV3DSEnabled(true); + + $this->expectException(InvalidRequestException::class); + $this->expectExceptionMessage('UPOP does not support EMV 3DS transaction types.'); + + $this->request->getData(); + } }