From 901b6258fe2361159a809e0de92ca5f71bff8f88 Mon Sep 17 00:00:00 2001 From: Siew-Mai Chan Date: Sun, 28 Jan 2018 21:38:39 +0800 Subject: [PATCH 01/10] Implement refund --- .gitignore | 1 + src/Gateway.php | 11 ++++ src/Message/RefundRequest.php | 91 ++++++++++++++++++++++++++++ src/Message/RefundResponse.php | 59 ++++++++++++++++++ tests/GatewayTest.php | 14 +++++ tests/Message/RefundRequestTest.php | 36 +++++++++++ tests/Message/RefundResponseTest.php | 41 +++++++++++++ tests/Mock/RefundFailure.txt | 12 ++++ tests/Mock/RefundSuccess.txt | 12 ++++ 9 files changed, 277 insertions(+) create mode 100644 src/Message/RefundRequest.php create mode 100644 src/Message/RefundResponse.php create mode 100644 tests/Message/RefundRequestTest.php create mode 100644 tests/Message/RefundResponseTest.php create mode 100644 tests/Mock/RefundFailure.txt create mode 100644 tests/Mock/RefundSuccess.txt diff --git a/.gitignore b/.gitignore index 8a282a5..0d77fdb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ composer.lock composer.phar phpunit.xml +.idea diff --git a/src/Gateway.php b/src/Gateway.php index 9d829ca..c52edf3 100644 --- a/src/Gateway.php +++ b/src/Gateway.php @@ -202,4 +202,15 @@ public function completePurchase(array $parameters = array()) ) ); } + + /** + * Create a refund request + * + * @param array $parameters + * @return \Omnipay\Common\Message\AbstractRequest + */ + public function refund(array $parameters = array()) + { + return $this->createRequest('\Omnipay\MOLPay\Message\RefundRequest', $parameters); + } } diff --git a/src/Message/RefundRequest.php b/src/Message/RefundRequest.php new file mode 100644 index 0000000..0639c05 --- /dev/null +++ b/src/Message/RefundRequest.php @@ -0,0 +1,91 @@ +validate('transactionReference', 'merchantId'); + + $data = array(); + $data['txnID'] = $this->getTransactionReference(); + $data['domain'] = $this->getMerchantId(); + $data['skey'] = $this->generateSKey(); + + return $data; + } + + /** + * {@inheritdoc} + */ + public function getEndpoint() + { + return $this->getTestMode() ? $this->sandboxEndpoint : $this->endpoint; + } + + /** + * {@inheritdoc} + */ + public function getHttpMethod() + { + return 'POST'; + } + + /** + * {@inheritdoc} + */ + public function sendData($data) + { + $httpRequest = $this->httpClient->createRequest( + $this->getHttpMethod(), + $this->getEndpoint(), + null, + $data + ); + + $httpResponse = $httpRequest->send(); + + return $this->response = new RefundResponse($this, $httpResponse->getBody()); + } + + /** + * Generate SKey + * @return string + */ + protected function generateSKey() + { + $this->validate('transactionReference', 'merchantId', 'secretKey'); + + return md5($this->getTransactionReference() . $this->getMerchantId() . $this->getSecretKey()); + } +} diff --git a/src/Message/RefundResponse.php b/src/Message/RefundResponse.php new file mode 100644 index 0000000..3ff858e --- /dev/null +++ b/src/Message/RefundResponse.php @@ -0,0 +1,59 @@ + 'Success', + '11' => 'Failure', + '12' => 'Invalid or unmatched security hash string', + '13' => 'Not a refundable transaction', + '14' => 'Transaction date more than 45 days', + '15' => 'Requested day is on settlement day', + '16' => 'Forbidden transaction', + '17' => 'Transaction not found' + ); + + /** + * RefundResponse constructor. + * @param RequestInterface $request + * @param mixed $data + */ + public function __construct(RequestInterface $request, $data) + { + $this->request = $request; + + $search = array("\r\n", "\n", "\r"); + $data = str_replace($search, '&', $data); + + // Parse string to key value mapping + parse_str($data, $this->data); + } + + /** + * {@inheritdoc} + */ + public function getMessage() + { + return $this->statCodeMessages[$this->data['StatCode']]; + } + + /** + * {@inheritdoc} + */ + public function isSuccessful() + { + return '00' === $this->data['StatCode']; + } +} diff --git a/tests/GatewayTest.php b/tests/GatewayTest.php index bd94b9f..1ef185b 100644 --- a/tests/GatewayTest.php +++ b/tests/GatewayTest.php @@ -107,4 +107,18 @@ public function testCompletePurchaseError() $this->assertNull($response->getTransactionReference()); $this->assertEquals('Invalid date', $response->getMessage()); } + + public function testRefund() + { + $request = $this->gateway->refund(array( + 'transactionReference' => '25248208' + )); + + $this->assertInstanceOf('\Omnipay\MOLPay\Message\RefundRequest', $request); + $this->assertSame('25248208', $request->getTransactionReference()); + $endPoint = $request->getEndpoint(); + $this->assertSame('https://api.molpay.com/MOLPay/API/refundAPI/refundAPI/refund.php', $endPoint); + $data = $request->getData(); + $this->assertNotEmpty($data); + } } diff --git a/tests/Message/RefundRequestTest.php b/tests/Message/RefundRequestTest.php new file mode 100644 index 0000000..fb04704 --- /dev/null +++ b/tests/Message/RefundRequestTest.php @@ -0,0 +1,36 @@ +getHttpClient(); + + $request = $this->getHttpRequest(); + + $this->request = new RefundRequest($client, $request); + } + + public function testGetData() + { + $this->request->setTransactionReference('25248208'); + $this->request->setMerchantId('your_merchant_id'); + $this->request->setSecretKey('your_secret_key'); + + $expected = array(); + $expected['txnID'] = '25248208'; + $expected['domain'] = 'your_merchant_id'; + $expected['skey'] = 'd07b97e2b8c7234792d3fb1fe56db619'; + + $this->assertEquals($expected, $this->request->getData()); + } +} diff --git a/tests/Message/RefundResponseTest.php b/tests/Message/RefundResponseTest.php new file mode 100644 index 0000000..cf542e0 --- /dev/null +++ b/tests/Message/RefundResponseTest.php @@ -0,0 +1,41 @@ +getMockRequest(), + "TxnID=25248203\nDomain=your_merchant_id\nStatDate=2018-01-28 15:53:19\nStatCode=00\nVrfKey=f56d5ea9932861454b7fd69851f57f7c"); + + $this->assertEquals(array( + 'TxnID' => '25248203', + 'Domain' => 'your_merchant_id', + 'StatDate' => '2018-01-28 15:53:19', + 'StatCode' => '00', + 'VrfKey' => 'f56d5ea9932861454b7fd69851f57f7c'), + $response->getData()); + } + + public function testRefundSuccess() + { + $httpResponse = $this->getMockHttpResponse('RefundSuccess.txt'); + $response = new RefundResponse($this->getMockRequest(), $httpResponse->getBody()); + + $this->assertTrue($response->isSuccessful()); + $this->assertSame('Success', $response->getMessage()); + } + + public function testRefundFailure() + { + $httpResponse = $this->getMockHttpResponse('RefundFailure.txt'); + $response = new RefundResponse($this->getMockRequest(), $httpResponse->getBody()); + + $this->assertFalse($response->isSuccessful()); + $this->assertSame('Forbidden transaction', $response->getMessage()); + } +} diff --git a/tests/Mock/RefundFailure.txt b/tests/Mock/RefundFailure.txt new file mode 100644 index 0000000..167b56b --- /dev/null +++ b/tests/Mock/RefundFailure.txt @@ -0,0 +1,12 @@ +HTTP/1.1 200 OK +Date: Sun, 28 Jan 2018 11:21:38 GMT +Server: Apache +Content-Length: 119 +Connection: close +Content-Type: text/plain; charset=utf-8 + +TxnID=25248203 +Domain=your_merchant_id +StatDate=2018-01-28 15:53:19 +StatCode=16 +VrfKey=3917697f5dd0cda28ba7408eb5d07e62 \ No newline at end of file diff --git a/tests/Mock/RefundSuccess.txt b/tests/Mock/RefundSuccess.txt new file mode 100644 index 0000000..a799e7d --- /dev/null +++ b/tests/Mock/RefundSuccess.txt @@ -0,0 +1,12 @@ +HTTP/1.1 200 OK +Date: Sun, 28 Jan 2018 11:21:38 GMT +Server: Apache +Content-Length: 119 +Connection: close +Content-Type: text/plain; charset=utf-8 + +TxnID=25248203 +Domain=your_merchant_id +StatDate=2018-01-28 15:53:19 +StatCode=00 +VrfKey=f56d5ea9932861454b7fd69851f57f7c \ No newline at end of file From 555cb7830ee3885ffb733667f0747c491a7ada2b Mon Sep 17 00:00:00 2001 From: Siew-Mai Chan Date: Mon, 29 Jan 2018 03:12:51 +0800 Subject: [PATCH 02/10] Implement partial refund --- src/Gateway.php | 8 +- src/Message/CompletePurchaseRequest.php | 10 ++ src/Message/PartialRefundRequest.php | 136 ++++++++++++++++++++++++ src/Message/PartialRefundResponse.php | 46 ++++++++ 4 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 src/Message/PartialRefundRequest.php create mode 100644 src/Message/PartialRefundResponse.php diff --git a/src/Gateway.php b/src/Gateway.php index c52edf3..3be3e89 100644 --- a/src/Gateway.php +++ b/src/Gateway.php @@ -198,6 +198,7 @@ public function completePurchase(array $parameters = array()) 'sKey' => $this->httpRequest->request->get('skey'), 'status' => $this->httpRequest->request->get('status'), 'transactionReference' => $this->httpRequest->request->get('tranID'), + 'channel' => $this->httpRequest->request->get('channel') ) ) ); @@ -211,6 +212,11 @@ public function completePurchase(array $parameters = array()) */ public function refund(array $parameters = array()) { - return $this->createRequest('\Omnipay\MOLPay\Message\RefundRequest', $parameters); + if (array_key_exists('refundType', $parameters) && $parameters['refundType'] === 'P') { + return $this->createRequest('\Omnipay\MOLPay\Message\PartialRefundRequest', $parameters); + } else { + return $this->createRequest('\Omnipay\MOLPay\Message\RefundRequest', $parameters); + } + } } diff --git a/src/Message/CompletePurchaseRequest.php b/src/Message/CompletePurchaseRequest.php index b8a14e3..9100508 100644 --- a/src/Message/CompletePurchaseRequest.php +++ b/src/Message/CompletePurchaseRequest.php @@ -179,6 +179,16 @@ public function setTransactionReference($value) return $this->setParameter('transactionReference', $value); } + public function getChannel() + { + return $this->getParameter('channel'); + } + + public function setChannel($value) + { + return $this->setParameter('channel', $value); + } + /** * {@inheritdoc} */ diff --git a/src/Message/PartialRefundRequest.php b/src/Message/PartialRefundRequest.php new file mode 100644 index 0000000..fcdf39c --- /dev/null +++ b/src/Message/PartialRefundRequest.php @@ -0,0 +1,136 @@ +setParameter('refundType', $value); + } + + public function getRefundType() + { + return $this->getParameter('refundType'); + } + + public function setRefId($value) + { + return $this->setParameter('refId', $value); + } + + public function getRefId() + { + return $this->getParameter('refId'); + } + + public function setBankCode($value) + { + return $this->setParameter('bankCode', $value); + } + + public function getBankCode() + { + return $this->getParameter('bankCode'); + } + + public function setBeneficiaryName($value) + { + return $this->setParameter('beneficiaryName', $value); + } + + public function getBeneficiaryName() + { + return $this->getParameter('beneficiaryName'); + } + + public function setBeneficiaryAccountNo($value) + { + return $this->setParameter('beneficiaryAccountNo', $value); + } + + public function getBeneficiaryAccountNo() + { + return $this->getParameter('beneficiaryAccountNo'); + } + + public function setChannel($value) + { + return $this->setParameter('channel', $value); + } + + public function getChannel() + { + return $this->getParameter('channel'); + } + + /** + * {@inheritdoc} + */ + public function getData() + { + $this->validate('merchantId', 'refId', 'transactionReference', 'amount'); + + $data = array(); + $data['RefundType'] = $this->getRefundType(); + $data['MerchantID'] = $this->getMerchantId(); + $data['RefID'] = $this->getRefId(); + $data['TxnID'] = $this->getTransactionReference(); + $data['Channel'] = $this->getChannel(); + $data['Amount'] = $this->getAmount(); + $data['BankCode'] = $this->getBankCode(); + $data['BeneficiaryName'] = $this->getBeneficiaryName(); + $data['BeneficiaryAccNo'] = $this->getBeneficiaryAccountNo(); + $data['Signature'] = $this->generateSignature(); + + return $data; + } + + /** + * {@inheritdoc} + */ + public function getEndpoint() + { + return $this->getTestMode() ? $this->sandboxEndpoint : $this->endpoint; + } + + /** + * {@inheritdoc} + */ + public function getHttpMethod() + { + return 'POST'; + } + + /** + * {@inheritdoc} + */ + public function sendData($data) + { + $httpRequest = $this->httpClient->createRequest( + $this->getHttpMethod(), + $this->getEndpoint(), + null, + $data + ); + + $httpResponse = $httpRequest->send(); + + return $this->response = new PartialRefundResponse($this, $httpResponse->json()); + } + + /** + * Generate Signature + * @return string + */ + protected function generateSignature() + { + $this->validate('merchantId', 'refId', 'transactionReference', 'amount', 'secretKey'); + + return md5($this->getRefundType() . $this->getMerchantId() .$this->getRefId() . $this->getTransactionReference() . $this->getAmount() . $this->getSecretKey()); + } +} diff --git a/src/Message/PartialRefundResponse.php b/src/Message/PartialRefundResponse.php new file mode 100644 index 0000000..f325946 --- /dev/null +++ b/src/Message/PartialRefundResponse.php @@ -0,0 +1,46 @@ +data)) { + return $this->data['error_desc']; + } else if (array_key_exists('reason', $this->data)) { + return $this->data['reason']; + } else { + return 'Unknown error'; + } + } + + /** + * {@inheritdoc} + */ + public function isSuccessful() + { + if (array_key_exists('error_code', $this->data)) { + return false; + } + + return ($this->data['Status'] === '00' || strtolower($this->data['Status']) === 'success'); + } + + /** + * {@inheritdoc} + */ + public function isPending() + { + if (array_key_exists('error_code', $this->data)) { + return false; + } + + return ($this->data['Status'] === '22' || strtolower($this->data['Status']) === 'pending'); + } +} From f1823a3a7654340fd2fadeb70c10ad06de2149b3 Mon Sep 17 00:00:00 2001 From: Siew-Mai Chan Date: Mon, 29 Jan 2018 12:54:28 +0800 Subject: [PATCH 03/10] Remove refundType flag checking in refund() --- src/Gateway.php | 7 +------ src/Message/PartialRefundRequest.php | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/Gateway.php b/src/Gateway.php index 3be3e89..01c6662 100644 --- a/src/Gateway.php +++ b/src/Gateway.php @@ -212,11 +212,6 @@ public function completePurchase(array $parameters = array()) */ public function refund(array $parameters = array()) { - if (array_key_exists('refundType', $parameters) && $parameters['refundType'] === 'P') { - return $this->createRequest('\Omnipay\MOLPay\Message\PartialRefundRequest', $parameters); - } else { - return $this->createRequest('\Omnipay\MOLPay\Message\RefundRequest', $parameters); - } - + return $this->createRequest('\Omnipay\MOLPay\Message\PartialRefundRequest', $parameters); } } diff --git a/src/Message/PartialRefundRequest.php b/src/Message/PartialRefundRequest.php index fcdf39c..6f5c322 100644 --- a/src/Message/PartialRefundRequest.php +++ b/src/Message/PartialRefundRequest.php @@ -8,14 +8,9 @@ class PartialRefundRequest extends AbstractRequest protected $sandboxEndpoint = 'https://sandbox.molpay.com/MOLPay/API/refundAPI/index.php'; - public function setRefundType($value) - { - return $this->setParameter('refundType', $value); - } - public function getRefundType() { - return $this->getParameter('refundType'); + return 'P'; } public function setRefId($value) From b3df54a3ac201a95bff709310098635f3c59e16d Mon Sep 17 00:00:00 2001 From: Siew-Mai Chan Date: Mon, 29 Jan 2018 17:04:38 +0800 Subject: [PATCH 04/10] Rename Refund to Reversal Request and implement gateway void method --- src/Gateway.php | 11 +++++++++++ .../{RefundRequest.php => ReversalRequest.php} | 6 +++--- .../{RefundResponse.php => ReversalResponse.php} | 11 ++++++++--- tests/GatewayTest.php | 6 +++--- ...fundRequestTest.php => ReversalRequestTest.php} | 7 +++---- ...ndResponseTest.php => ReversalResponseTest.php} | 14 +++++++------- .../{RefundFailure.txt => ReversalFailure.txt} | 0 .../{RefundSuccess.txt => ReversalSuccess.txt} | 0 8 files changed, 35 insertions(+), 20 deletions(-) rename src/Message/{RefundRequest.php => ReversalRequest.php} (92%) rename src/Message/{RefundResponse.php => ReversalResponse.php} (89%) rename tests/Message/{RefundRequestTest.php => ReversalRequestTest.php} (81%) rename tests/Message/{RefundResponseTest.php => ReversalResponseTest.php} (66%) rename tests/Mock/{RefundFailure.txt => ReversalFailure.txt} (100%) rename tests/Mock/{RefundSuccess.txt => ReversalSuccess.txt} (100%) diff --git a/src/Gateway.php b/src/Gateway.php index 01c6662..ad70cb3 100644 --- a/src/Gateway.php +++ b/src/Gateway.php @@ -214,4 +214,15 @@ public function refund(array $parameters = array()) { return $this->createRequest('\Omnipay\MOLPay\Message\PartialRefundRequest', $parameters); } + + /** + * Create a void request + * + * @param array $parameters + * @return \Omnipay\Common\Message\AbstractRequest + */ + public function void(array $parameters = array()) + { + return $this->createRequest('\Omnipay\MOLPay\Message\ReversalRequest', $parameters); + } } diff --git a/src/Message/RefundRequest.php b/src/Message/ReversalRequest.php similarity index 92% rename from src/Message/RefundRequest.php rename to src/Message/ReversalRequest.php index 0639c05..f1b4e72 100644 --- a/src/Message/RefundRequest.php +++ b/src/Message/ReversalRequest.php @@ -4,7 +4,7 @@ /** - * Class RefundRequest + * Class ReversalRequest * @package Omnipay\MOLPay\Message * * MOLPay Reversal Request @@ -14,7 +14,7 @@ * * domain [required] - Merchant ID in MOLPay system * * skey [required] - This is the data integrity protection hash string */ -class RefundRequest extends AbstractRequest +class ReversalRequest extends AbstractRequest { /** * Reversal Request URL @@ -75,7 +75,7 @@ public function sendData($data) $httpResponse = $httpRequest->send(); - return $this->response = new RefundResponse($this, $httpResponse->getBody()); + return $this->response = new ReversalResponse($this, $httpResponse->getBody()); } /** diff --git a/src/Message/RefundResponse.php b/src/Message/ReversalResponse.php similarity index 89% rename from src/Message/RefundResponse.php rename to src/Message/ReversalResponse.php index 3ff858e..2f80e65 100644 --- a/src/Message/RefundResponse.php +++ b/src/Message/ReversalResponse.php @@ -5,11 +5,15 @@ use Omnipay\Common\Message\AbstractResponse; use Omnipay\Common\Message\RequestInterface; + /** - * Class RefundResponse + * Class ReversalResponse * @package Omnipay\MOLPay\Message + * + * MOLPay Reversal Response + * */ -class RefundResponse extends AbstractResponse +class ReversalResponse extends AbstractResponse { /** * Reversal Request Response statCodes and messages @@ -25,8 +29,9 @@ class RefundResponse extends AbstractResponse '17' => 'Transaction not found' ); + /** - * RefundResponse constructor. + * ReversalResponse constructor. * @param RequestInterface $request * @param mixed $data */ diff --git a/tests/GatewayTest.php b/tests/GatewayTest.php index 1ef185b..7424fff 100644 --- a/tests/GatewayTest.php +++ b/tests/GatewayTest.php @@ -108,13 +108,13 @@ public function testCompletePurchaseError() $this->assertEquals('Invalid date', $response->getMessage()); } - public function testRefund() + public function testVoid() { - $request = $this->gateway->refund(array( + $request = $this->gateway->void(array( 'transactionReference' => '25248208' )); - $this->assertInstanceOf('\Omnipay\MOLPay\Message\RefundRequest', $request); + $this->assertInstanceOf('\Omnipay\MOLPay\Message\ReversalRequest', $request); $this->assertSame('25248208', $request->getTransactionReference()); $endPoint = $request->getEndpoint(); $this->assertSame('https://api.molpay.com/MOLPay/API/refundAPI/refundAPI/refund.php', $endPoint); diff --git a/tests/Message/RefundRequestTest.php b/tests/Message/ReversalRequestTest.php similarity index 81% rename from tests/Message/RefundRequestTest.php rename to tests/Message/ReversalRequestTest.php index fb04704..3a78a3c 100644 --- a/tests/Message/RefundRequestTest.php +++ b/tests/Message/ReversalRequestTest.php @@ -4,20 +4,19 @@ use Omnipay\Tests\TestCase; -class RefundRequestTest extends TestCase +class ReversalRequestTest extends TestCase { /** - * @var \Omnipay\MOLPay\Message\RefundRequest + * @var \Omnipay\MOLPay\Message\ReversalRequest */ private $request; public function setUp() { $client = $this->getHttpClient(); - $request = $this->getHttpRequest(); - $this->request = new RefundRequest($client, $request); + $this->request = new ReversalRequest($client, $request); } public function testGetData() diff --git a/tests/Message/RefundResponseTest.php b/tests/Message/ReversalResponseTest.php similarity index 66% rename from tests/Message/RefundResponseTest.php rename to tests/Message/ReversalResponseTest.php index cf542e0..159485e 100644 --- a/tests/Message/RefundResponseTest.php +++ b/tests/Message/ReversalResponseTest.php @@ -4,11 +4,11 @@ use Omnipay\Tests\TestCase; -class ResponseTest extends TestCase +class ReversalResponseTest extends TestCase { public function testConstruct() { - $response = new RefundResponse( + $response = new ReversalResponse( $this->getMockRequest(), "TxnID=25248203\nDomain=your_merchant_id\nStatDate=2018-01-28 15:53:19\nStatCode=00\nVrfKey=f56d5ea9932861454b7fd69851f57f7c"); @@ -21,10 +21,10 @@ public function testConstruct() $response->getData()); } - public function testRefundSuccess() + public function testReversalSuccess() { - $httpResponse = $this->getMockHttpResponse('RefundSuccess.txt'); - $response = new RefundResponse($this->getMockRequest(), $httpResponse->getBody()); + $httpResponse = $this->getMockHttpResponse('ReversalSuccess.txt'); + $response = new ReversalResponse($this->getMockRequest(), $httpResponse->getBody()); $this->assertTrue($response->isSuccessful()); $this->assertSame('Success', $response->getMessage()); @@ -32,8 +32,8 @@ public function testRefundSuccess() public function testRefundFailure() { - $httpResponse = $this->getMockHttpResponse('RefundFailure.txt'); - $response = new RefundResponse($this->getMockRequest(), $httpResponse->getBody()); + $httpResponse = $this->getMockHttpResponse('ReversalFailure.txt'); + $response = new ReversalResponse($this->getMockRequest(), $httpResponse->getBody()); $this->assertFalse($response->isSuccessful()); $this->assertSame('Forbidden transaction', $response->getMessage()); diff --git a/tests/Mock/RefundFailure.txt b/tests/Mock/ReversalFailure.txt similarity index 100% rename from tests/Mock/RefundFailure.txt rename to tests/Mock/ReversalFailure.txt diff --git a/tests/Mock/RefundSuccess.txt b/tests/Mock/ReversalSuccess.txt similarity index 100% rename from tests/Mock/RefundSuccess.txt rename to tests/Mock/ReversalSuccess.txt From 5d3fe517f03c5e216580e852817508449fc33126 Mon Sep 17 00:00:00 2001 From: Siew-Mai Chan Date: Mon, 29 Jan 2018 18:54:56 +0800 Subject: [PATCH 05/10] Add partial refund unit tests --- src/Message/PartialRefundRequest.php | 100 +++++++++++++++++++- src/Message/PartialRefundResponse.php | 24 ++++- src/Message/ReversalRequest.php | 12 ++- src/Message/ReversalResponse.php | 15 ++- tests/GatewayTest.php | 25 +++++ tests/Message/PartialRefundRequestTest.php | 50 ++++++++++ tests/Message/PartialRefundResponseTest.php | 26 +++++ tests/Mock/PartialRefundError.txt | 11 +++ tests/Mock/PartialRefundPending.txt | 17 ++++ 9 files changed, 269 insertions(+), 11 deletions(-) create mode 100644 tests/Message/PartialRefundRequestTest.php create mode 100644 tests/Message/PartialRefundResponseTest.php create mode 100644 tests/Mock/PartialRefundError.txt create mode 100644 tests/Mock/PartialRefundPending.txt diff --git a/src/Message/PartialRefundRequest.php b/src/Message/PartialRefundRequest.php index 6f5c322..d8f09ff 100644 --- a/src/Message/PartialRefundRequest.php +++ b/src/Message/PartialRefundRequest.php @@ -2,65 +2,155 @@ namespace Omnipay\MOLPay\Message; +/** + * Class PartialRefundRequest + * @package Omnipay\MOLPay\Message + * + * MOLPay Partial Refund + * + * ### Parameters + * + * * RefundType [mandatory] - P for Partial Refund + * * MerchantID [mandatory] - Merchant ID provided by MOLPay + * * RefID [mandatory] - Unique tracking/references ID from merchant + * * TxnID [mandatory] - MOLPay Transaction ID + * * Channel [mandatory] - Refer to Channel List + * * Amount [mandatory] - eg. '5.00' Amount to be refund + * * BankCode [conditional] - Applicable for Online Banking and Physical Payment transaction only + * * BeneficiaryName [conditional] - Applicable for Online Banking and Physical Payment transaction only + * * BeneficiaryAccNo [conditional] - Applicable for Online Banking and Physical Payment transaction only + * * Signature [mandatory] - This is data integrity protection hash string + * * mdr_flag [optional] - This is to include or exclude MDR refund to buyer if the amount is same as bill amount + * Available value is as below: + * 0 - Include MDR/Full Refund (Default) + * 1 - Exclude/Reserved MDR + * * notify_url [optional] - This is the URL for merchant to receive refund status + * + */ class PartialRefundRequest extends AbstractRequest { + /** + * Partial Refund URL + * + * @var string + */ protected $endpoint = 'https://api.molpay.com/MOLPay/API/refundAPI/index.php'; + /** + * Sandbox Partial Refund URL + * + * @var string + */ protected $sandboxEndpoint = 'https://sandbox.molpay.com/MOLPay/API/refundAPI/index.php'; + /** + * @return string + */ public function getRefundType() { return 'P'; } + /** + * @param $value + * @return \Omnipay\Common\Message\AbstractRequest + */ public function setRefId($value) { return $this->setParameter('refId', $value); } + /** + * @return mixed + */ public function getRefId() { return $this->getParameter('refId'); } + /** + * @param $value + * @return \Omnipay\Common\Message\AbstractRequest + */ + public function setChannel($value) + { + return $this->setParameter('channel', $value); + } + + /** + * @return mixed + */ + public function getChannel() + { + return $this->getParameter('channel'); + } + + /** + * @param $value + * @return \Omnipay\Common\Message\AbstractRequest + */ public function setBankCode($value) { return $this->setParameter('bankCode', $value); } + /** + * @return mixed + */ public function getBankCode() { return $this->getParameter('bankCode'); } + /** + * @param $value + * @return \Omnipay\Common\Message\AbstractRequest + */ public function setBeneficiaryName($value) { return $this->setParameter('beneficiaryName', $value); } + /** + * @return mixed + */ public function getBeneficiaryName() { return $this->getParameter('beneficiaryName'); } + /** + * @param $value + * @return \Omnipay\Common\Message\AbstractRequest + */ public function setBeneficiaryAccountNo($value) { return $this->setParameter('beneficiaryAccountNo', $value); } + /** + * @return mixed + */ public function getBeneficiaryAccountNo() { return $this->getParameter('beneficiaryAccountNo'); } - public function setChannel($value) + /** + * @param $value + * @return \Omnipay\Common\Message\AbstractRequest + */ + public function setMdrFlag($value) { - return $this->setParameter('channel', $value); + return $this->setParameter('mdrFlag', $value); } - public function getChannel() + /** + * @return mixed + */ + public function getMdrFlag() { - return $this->getParameter('channel'); + return $this->getParameter('mdrFlag'); } /** @@ -81,6 +171,8 @@ public function getData() $data['BeneficiaryName'] = $this->getBeneficiaryName(); $data['BeneficiaryAccNo'] = $this->getBeneficiaryAccountNo(); $data['Signature'] = $this->generateSignature(); + $data['mdr_flag'] = $this->getMdrFlag(); + $data['notify_url'] = $this->getNotifyUrl(); return $data; } diff --git a/src/Message/PartialRefundResponse.php b/src/Message/PartialRefundResponse.php index f325946..128e1f5 100644 --- a/src/Message/PartialRefundResponse.php +++ b/src/Message/PartialRefundResponse.php @@ -4,6 +4,27 @@ use Omnipay\Common\Message\AbstractResponse; +/** + * Class PartialRefundResponse + * @package Omnipay\MOLPay\Message + * + * MOLPay Partial Refund Response + * + * ### Positive Result + * * RefundType [mandatory] - Content follow merchant request + * * MerchantID [mandatory] - Content follow merchant request + * * RefID [mandatory] - Content follow merchant request + * * RefundID [mandatory] - Refund ID provided by MOLPay + * * TxnID [mandatory] - Content follow merchant request + * * Amount [mandatory] - Content follow merchant request + * * Status [mandatory] - 22 for 'Pending' , 11 for 'Rejected' and 00 for 'Success' + * * Signature [mandatory] - This is data integrity protection hash string + * * reason [optional] - Reason for rejected status + * + * ### Negative Result + * * error_code - Refer to API Spec Appendix C + * * error_desc - Refer to API Spec Appendix C + */ class PartialRefundResponse extends AbstractResponse { /** @@ -11,7 +32,7 @@ class PartialRefundResponse extends AbstractResponse */ public function getMessage() { - if(array_key_exists('error_desc', $this->data)) { + if (array_key_exists('error_desc', $this->data)) { return $this->data['error_desc']; } else if (array_key_exists('reason', $this->data)) { return $this->data['reason']; @@ -41,6 +62,7 @@ public function isPending() return false; } + // API returned 'pending', not actual '22' at this development time return ($this->data['Status'] === '22' || strtolower($this->data['Status']) === 'pending'); } } diff --git a/src/Message/ReversalRequest.php b/src/Message/ReversalRequest.php index f1b4e72..eb607e5 100644 --- a/src/Message/ReversalRequest.php +++ b/src/Message/ReversalRequest.php @@ -2,17 +2,19 @@ namespace Omnipay\MOLPay\Message; - /** * Class ReversalRequest * @package Omnipay\MOLPay\Message * * MOLPay Reversal Request - * * ### Parameters + * ### Parameters * - * * txnID [required] - Unique transaction ID for tracking purpose - * * domain [required] - Merchant ID in MOLPay system - * * skey [required] - This is the data integrity protection hash string + * * txnID [mandatory] - Unique transaction ID for tracking purpose + * * domain [mandatory] - Merchant ID in MOLPay system + * * skey [mandatory] - This is the data integrity protection hash string + * * url [optional] - The URL to receive POST response from MOLPay + * * type [optional] - 0 = plain text result (default) + * * 1 = result via POST method */ class ReversalRequest extends AbstractRequest { diff --git a/src/Message/ReversalResponse.php b/src/Message/ReversalResponse.php index 2f80e65..ca7abdd 100644 --- a/src/Message/ReversalResponse.php +++ b/src/Message/ReversalResponse.php @@ -5,13 +5,26 @@ use Omnipay\Common\Message\AbstractResponse; use Omnipay\Common\Message\RequestInterface; - /** * Class ReversalResponse * @package Omnipay\MOLPay\Message * * MOLPay Reversal Response * + * * TranID - Unique transaction ID for tracking purpose + * * Domain - Merchant ID in MOLPay system + * * VrfKey - This is the data integrity protection hash string + * * StatCode - 00 = Success + * 11 = Failure + * 12 = Invalid or unmatched security hash string + * 13 = Not a refundable transaction + * 14 = Transaction date more than 45 days + * 15 = Requested day is on settlement day + * 16 = Forbidden transaction + * 17 = Transaction not found + * + * * StatDate - Response date & time + * */ class ReversalResponse extends AbstractResponse { diff --git a/tests/GatewayTest.php b/tests/GatewayTest.php index 7424fff..6f35d58 100644 --- a/tests/GatewayTest.php +++ b/tests/GatewayTest.php @@ -121,4 +121,29 @@ public function testVoid() $data = $request->getData(); $this->assertNotEmpty($data); } + + public function testRefund() + { + $request = $this->gateway->refund(array( + 'transactionReference' => '25248208', + 'refId' => 'merchant_refund_ref_id', + 'amount' => '10.00', + 'bankCode' => 'MBBEMYKL', + 'beneficiaryName' => 'beneficiary_name', + 'beneficiaryAccountNo' => 'beneficiary_account_no', + )); + + $this->assertInstanceOf('\Omnipay\MOLPay\Message\PartialRefundRequest', $request); + $this->assertSame('25248208', $request->getTransactionReference()); + $this->assertSame('merchant_refund_ref_id', $request->getRefId()); + $this->assertSame('10.00', $request->getAmount()); + $this->assertSame('MBBEMYKL', $request->getBankCode()); + $this->assertSame('beneficiary_name', $request->getBeneficiaryName()); + $this->assertSame('beneficiary_account_no', $request->getBeneficiaryAccountNo()); + + $endPoint = $request->getEndpoint(); + $this->assertSame('https://api.molpay.com/MOLPay/API/refundAPI/index.php', $endPoint); + $data = $request->getData(); + $this->assertNotEmpty($data); + } } diff --git a/tests/Message/PartialRefundRequestTest.php b/tests/Message/PartialRefundRequestTest.php new file mode 100644 index 0000000..134d3e0 --- /dev/null +++ b/tests/Message/PartialRefundRequestTest.php @@ -0,0 +1,50 @@ +getHttpClient(); + $request = $this->getHttpRequest(); + + $this->request = new PartialRefundRequest($client, $request); + } + + public function testGetData() + { + $this->request->setMerchantId('your_merchant_id'); + $this->request->setRefId('merchant_refund_ref_id'); + $this->request->setTransactionReference('25248208'); + $this->request->setChannel('FPX_MB2U'); + $this->request->setAmount('10.00'); + $this->request->setBankCode('MBBEMYKL'); + $this->request->setBeneficiaryName('beneficiary_name'); + $this->request->setBeneficiaryAccountNo('beneficiary_account_no'); + $this->request->setSecretKey('your_secret_key'); + + $expected = array(); + $expected['RefundType'] = 'P'; + $expected['MerchantID'] = 'your_merchant_id'; + $expected['RefID'] = 'merchant_refund_ref_id'; + $expected['TxnID'] = '25248208'; + $expected['Channel'] = 'FPX_MB2U'; + $expected['Amount'] = '10.00'; + $expected['BankCode'] = 'MBBEMYKL'; + $expected['BeneficiaryName'] = 'beneficiary_name'; + $expected['BeneficiaryAccNo'] = 'beneficiary_account_no'; + $expected['Signature'] = 'aafbef8720b13a33b37370a1a3b1c238'; + $expected['mdr_flag'] = null; + $expected['notify_url'] = null; + + $this->assertEquals($expected, $this->request->getData()); + } +} diff --git a/tests/Message/PartialRefundResponseTest.php b/tests/Message/PartialRefundResponseTest.php new file mode 100644 index 0000000..e15b416 --- /dev/null +++ b/tests/Message/PartialRefundResponseTest.php @@ -0,0 +1,26 @@ +getMockHttpResponse('PartialRefundPending.txt'); + $response = new PartialRefundResponse($this->getMockRequest(), $httpResponse->json()); + + $this->assertTrue($response->isPending()); + } + + public function testPartialRefundError() + { + $httpResponse = $this->getMockHttpResponse('PartialRefundError.txt'); + $response = new PartialRefundResponse($this->getMockRequest(), $httpResponse->json()); + + $this->assertFalse($response->isSuccessful()); + $this->assertFalse($response->isPending()); + $this->assertSame('Exceed refund amount for this transaction.', $response->getMessage()); + } +} \ No newline at end of file diff --git a/tests/Mock/PartialRefundError.txt b/tests/Mock/PartialRefundError.txt new file mode 100644 index 0000000..9a28c43 --- /dev/null +++ b/tests/Mock/PartialRefundError.txt @@ -0,0 +1,11 @@ +HTTP/1.1 200 OK +Date: Sun, 28 Jan 2018 11:21:38 GMT +Server: Apache +Content-Length: 93 +Connection: close +Content-Type: application/json + +{ + "error_code": "PR011", + "error_desc": "Exceed refund amount for this transaction." +} \ No newline at end of file diff --git a/tests/Mock/PartialRefundPending.txt b/tests/Mock/PartialRefundPending.txt new file mode 100644 index 0000000..6e17388 --- /dev/null +++ b/tests/Mock/PartialRefundPending.txt @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +Date: Sun, 28 Jan 2018 11:21:38 GMT +Server: Apache +Content-Length: 119 +Connection: close +Content-Type: application/json + +{ + "RefundType": "P", + "MerchantID": "your_merchant_id", + "RefID": "merchant_refund_ref_id", + "RefundID": 19, + "TxnID": 25248208, + "Amount": "10.00", + "Status": "pending", + "Signature": "816071a1a72a260b87d7570beb0a670a" +} \ No newline at end of file From 198e38ff010affabc55322a2e03fa9c2edf7f0d4 Mon Sep 17 00:00:00 2001 From: Siew-Mai Chan Date: Mon, 29 Jan 2018 19:55:17 +0800 Subject: [PATCH 06/10] Add void and refund example --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/README.md b/README.md index 095201d..c660862 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,60 @@ if ($response->isSuccessful()) { } ``` +### Void or Reverse a 'captured' transaction +###### Only available for limited merchants and channels + +The following is the example to void a captured transaction, your can refer to MOLPay Reversal Request api spec. + +```php +$gateway = Omnipay::create('MOLPay'); + +$gateway->setMerchantId('your_merchant_id); +$gateway->setSecretKey('your_secret_key'); + +$request = $gateway->void([ + 'transactionReference' => '25248208' +]); + +$response = $request->send(); + +if ($response->isSuccessful()) { + // Update your data model +} else { + echo($response->getMessage()); +} +``` + +### Request Partial Refund for a 'captured' or 'settled' transaction +###### Only available for limited merchants and channels + +To perform a partial refund, you need to specify more parameters as below + +```php +$gateway = Omnipay::create('MOLPay'); + +$gateway->setMerchantId('your_merchant_id); +$gateway->setSecretKey('your_secret_key'); + +$request = $gateway->refund([ + 'transactionReference' => '25248208', + 'refId' => 'merchant_refund_red_id', + 'amount' => '10.00', + 'bankCode' => $bank_code, // from user who request to refund + 'beneficiaryName' => $beneficiary_name, // from user who request to refund + 'beneficiaryAccountNo' => $beneficiary_account_no, // from user who request to refund +]); + +$response = $request->send(); + +// The refund process will take about 7-14 days after the request sent +if ($response->isSuccessful() || $response->isPending() ) { + // Update your data model +} else { + echo($response->getMessage()); +} +``` + ## Out Of Scope Omnipay does not cover recurring payments or billing agreements, and so those features are not included in this package. Extensions to this gateway are always welcome. From 73f2b103305d5cc997f7ea61e9dff9a70ba5f967 Mon Sep 17 00:00:00 2001 From: Siew-Mai Chan Date: Mon, 29 Jan 2018 19:59:53 +0800 Subject: [PATCH 07/10] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c660862..7b2a80d 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ The following is the example to void a captured transaction, your can refer to M ```php $gateway = Omnipay::create('MOLPay'); -$gateway->setMerchantId('your_merchant_id); +$gateway->setMerchantId('your_merchant_id'); $gateway->setSecretKey('your_secret_key'); $request = $gateway->void([ @@ -123,7 +123,7 @@ To perform a partial refund, you need to specify more parameters as below ```php $gateway = Omnipay::create('MOLPay'); -$gateway->setMerchantId('your_merchant_id); +$gateway->setMerchantId('your_merchant_id'); $gateway->setSecretKey('your_secret_key'); $request = $gateway->refund([ From 932386a6cf77033b14c80f475270775e41a4f224 Mon Sep 17 00:00:00 2001 From: Siew-Mai Chan Date: Mon, 29 Jan 2018 22:31:08 +0800 Subject: [PATCH 08/10] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 7b2a80d..e21722e 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ The following is the example to void a captured transaction, your can refer to M $gateway = Omnipay::create('MOLPay'); $gateway->setMerchantId('your_merchant_id'); +$gateway->setVerifyKey('your_verify_key'); $gateway->setSecretKey('your_secret_key'); $request = $gateway->void([ @@ -124,12 +125,14 @@ To perform a partial refund, you need to specify more parameters as below $gateway = Omnipay::create('MOLPay'); $gateway->setMerchantId('your_merchant_id'); +$gateway->setVerifyKey('your_verify_key'); $gateway->setSecretKey('your_secret_key'); $request = $gateway->refund([ 'transactionReference' => '25248208', 'refId' => 'merchant_refund_red_id', 'amount' => '10.00', + 'channel' => $transaction_channel, // data saved from $gateway->purchase() response, e.g FPX_MB2U 'bankCode' => $bank_code, // from user who request to refund 'beneficiaryName' => $beneficiary_name, // from user who request to refund 'beneficiaryAccountNo' => $beneficiary_account_no, // from user who request to refund From 64cc4e201b611ed3023c39ae575d180682aaca1d Mon Sep 17 00:00:00 2001 From: Siew-Mai Chan Date: Tue, 30 Jan 2018 12:59:53 +0800 Subject: [PATCH 09/10] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e21722e..443b453 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ $response = $request->send(); if ($response->isSuccessful()) { // Update your data model } else { - echo($response->getMessage()); + echo $response->getMessage(); } ``` @@ -144,7 +144,7 @@ $response = $request->send(); if ($response->isSuccessful() || $response->isPending() ) { // Update your data model } else { - echo($response->getMessage()); + echo $response->getMessage(); } ``` From 73d9683a9af09f9eeecdfe93c19661bfba8d3fa7 Mon Sep 17 00:00:00 2001 From: Siew-Mai Chan Date: Sun, 4 Feb 2018 15:53:48 +0800 Subject: [PATCH 10/10] Add more explanative comments in partial refund response --- src/Message/PartialRefundResponse.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Message/PartialRefundResponse.php b/src/Message/PartialRefundResponse.php index 128e1f5..90a1e85 100644 --- a/src/Message/PartialRefundResponse.php +++ b/src/Message/PartialRefundResponse.php @@ -32,11 +32,16 @@ class PartialRefundResponse extends AbstractResponse */ public function getMessage() { + // Handle MOLPay returned error if (array_key_exists('error_desc', $this->data)) { return $this->data['error_desc']; - } else if (array_key_exists('reason', $this->data)) { + } + // Handle MOLPay return success with status 'Rejected' + else if (array_key_exists('reason', $this->data)) { return $this->data['reason']; - } else { + } + // Handle MOLPay returned unknown exceptions that not specified in spec + else { return 'Unknown error'; } } @@ -50,6 +55,7 @@ public function isSuccessful() return false; } + // API returned 'success', not actual '00' at this development time return ($this->data['Status'] === '00' || strtolower($this->data['Status']) === 'success'); }