diff --git a/composer.json b/composer.json index c2407b56..9d7caf63 100644 --- a/composer.json +++ b/composer.json @@ -29,11 +29,15 @@ ] }, "require": { + "ext-json": "*", + "ext-openssl": "*", "omnipay/common": "^3.0", - "php-http/guzzle6-adapter": "*" + "php-http/guzzle6-adapter": "^2.0" }, "require-dev": { "omnipay/tests": "^3.0", - "squizlabs/php_codesniffer": "3.*" - } + "squizlabs/php_codesniffer": "^3.4" + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/src/AbstractAopGateway.php b/src/AbstractAopGateway.php index fe3db56f..6550163a 100644 --- a/src/AbstractAopGateway.php +++ b/src/AbstractAopGateway.php @@ -3,6 +3,7 @@ namespace Omnipay\Alipay; use Omnipay\Alipay\Requests\AopCompletePurchaseRequest; +use Omnipay\Alipay\Requests\AopCompleteRefundRequest; use Omnipay\Alipay\Requests\AopTradeCancelRequest; use Omnipay\Alipay\Requests\AopTradeCloseRequest; use Omnipay\Alipay\Requests\AopTradeOrderSettleRequest; @@ -14,6 +15,7 @@ use Omnipay\Alipay\Requests\DataServiceBillDownloadUrlQueryRequest; use Omnipay\Common\AbstractGateway; use Omnipay\Common\Exception\InvalidRequestException; +use Omnipay\Common\Message\AbstractRequest; abstract class AbstractAopGateway extends AbstractGateway { @@ -305,6 +307,10 @@ public function setAlipaySdk($value) } + /** + * @return AbstractAopGateway + * @throws InvalidRequestException + */ public function production() { return $this->setEnvironment('production'); @@ -342,6 +348,10 @@ public function setEndpoint($value) } + /** + * @return AbstractAopGateway + * @throws InvalidRequestException + */ public function sandbox() { return $this->setEnvironment('sandbox'); @@ -349,9 +359,11 @@ public function sandbox() /** + * @noinspection PhpDocRedundantThrowsInspection + * * @param array $parameters * - * @return \Omnipay\Alipay\Requests\AopCompletePurchaseRequest + * @return AopCompletePurchaseRequest|AbstractRequest * @throws InvalidRequestException */ public function completePurchase(array $parameters = []) @@ -360,12 +372,26 @@ public function completePurchase(array $parameters = []) } + /** + * @noinspection PhpDocRedundantThrowsInspection + * + * @param array $parameters + * + * @return AopCompleteRefundRequest|AbstractRequest + * @throws InvalidRequestException + */ + public function completeRefund(array $parameters = []) + { + return $this->createRequest(AopCompleteRefundRequest::class, $parameters); + } + + /** * Query Order Status * * @param array $parameters * - * @return \Omnipay\Alipay\Requests\AopTradeQueryRequest + * @return AopTradeQueryRequest|AbstractRequest */ public function query(array $parameters = []) { @@ -378,7 +404,7 @@ public function query(array $parameters = []) * * @param array $parameters * - * @return \Omnipay\Alipay\Requests\AopTradeRefundRequest + * @return AopTradeRefundRequest|AbstractRequest */ public function refund(array $parameters = []) { @@ -391,7 +417,7 @@ public function refund(array $parameters = []) * * @param array $parameters * - * @return \Omnipay\Alipay\Requests\AopTradeRefundQueryRequest + * @return AopTradeRefundQueryRequest|AbstractRequest */ public function refundQuery(array $parameters = []) { @@ -404,7 +430,7 @@ public function refundQuery(array $parameters = []) * * @param array $parameters * - * @return \Omnipay\Alipay\Requests\AopTradeCloseRequest + * @return AopTradeCloseRequest|AbstractRequest */ public function close(array $parameters = []) { @@ -417,7 +443,7 @@ public function close(array $parameters = []) * * @param array $parameters * - * @return \Omnipay\Alipay\Requests\AopTradeCancelRequest + * @return AopTradeCancelRequest|AbstractRequest */ public function cancel(array $parameters = []) { @@ -430,7 +456,7 @@ public function cancel(array $parameters = []) * * @param array $parameters * - * @return \Omnipay\Alipay\Requests\AopTransferToAccountRequest + * @return AopTransferToAccountRequest|AbstractRequest */ public function transfer(array $parameters = []) { @@ -443,7 +469,7 @@ public function transfer(array $parameters = []) * * @param array $parameters * - * @return \Omnipay\Alipay\Requests\AopTransferToAccountQueryRequest + * @return AopTransferToAccountQueryRequest|AbstractRequest */ public function transferQuery(array $parameters = []) { @@ -456,7 +482,7 @@ public function transferQuery(array $parameters = []) * * @param array $parameters * - * @return \Omnipay\Alipay\Requests\AopTradeCancelRequest + * @return AopTradeCancelRequest|AbstractRequest */ public function settle(array $parameters = []) { @@ -467,7 +493,7 @@ public function settle(array $parameters = []) /** * @param array $parameters * - * @return \Omnipay\Common\Message\AbstractRequest + * @return AbstractRequest */ public function queryBillDownloadUrl(array $parameters = []) { diff --git a/src/Requests/AbstractAopRequest.php b/src/Requests/AbstractAopRequest.php index 2311a4dd..349d75da 100644 --- a/src/Requests/AbstractAopRequest.php +++ b/src/Requests/AbstractAopRequest.php @@ -4,7 +4,10 @@ use Omnipay\Alipay\Common\Signer; use Omnipay\Common\Exception\InvalidRequestException; +use Omnipay\Common\Http\Exception\NetworkException; use Omnipay\Common\Message\AbstractRequest; +use Omnipay\Common\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; abstract class AbstractAopRequest extends AbstractRequest { @@ -28,6 +31,7 @@ abstract class AbstractAopRequest extends AbstractRequest * gateway, but will usually be either an associative array, or a SimpleXMLElement. * * @return mixed + * @throws InvalidRequestException */ public function getData() { @@ -49,6 +53,9 @@ public function getData() } + /** + * @throws InvalidRequestException + */ public function validateParams() { $this->validate( @@ -101,6 +108,13 @@ protected function convertToString() } + /** + * @param array $params + * @param string $signType + * + * @return string|null + * @throws InvalidRequestException + */ protected function sign($params, $signType) { $signer = new Signer($params); @@ -176,9 +190,8 @@ public function setAlipayPublicKey($value) /** * @param mixed $data * - * @return mixed|\Omnipay\Common\Message\ResponseInterface|\Psr\Http\Message\StreamInterface - * @throws \Psr\Http\Client\Exception\NetworkException - * @throws \Psr\Http\Client\Exception\RequestException + * @return mixed|ResponseInterface|StreamInterface + * @throws NetworkException */ public function sendData($data) { @@ -503,6 +516,9 @@ public function setAppAuthToken($value) } + /** + * @throws InvalidRequestException + */ public function validateBizContent() { $data = $this->getBizContent(); @@ -519,6 +535,9 @@ public function validateBizContent() } + /** + * @throws InvalidRequestException + */ public function validateBizContentOne() { $data = $this->getBizContent(); @@ -558,6 +577,9 @@ protected function filter($data) } + /** + * @throws InvalidRequestException + */ protected function validateOne() { $keys = func_get_args(); diff --git a/src/Requests/AopCompleteRefundRequest.php b/src/Requests/AopCompleteRefundRequest.php new file mode 100644 index 00000000..73bd910e --- /dev/null +++ b/src/Requests/AopCompleteRefundRequest.php @@ -0,0 +1,86 @@ +validateParams(); + + return $this->getParams(); + } + + + public function validateParams() + { + $this->validate('params'); + } + + + /** + * @return mixed + */ + public function getParams() + { + return $this->getParameter('params'); + } + + + /** + * Send the request with specified data + * + * @param mixed $data The data to send + * + * @return AopCompleteRefundResponse + */ + public function sendData($data) + { + $request = new AopNotifyRequest($this->httpClient, $this->httpRequest); + $request->initialize(['params' => $data]); + $request->setEndpoint($this->getEndpoint()); + $request->setAlipayPublicKey($this->getAlipayPublicKey()); + $data = $request->send()->getData(); + + if (!array_get($data, 'trade_status')) { + $tn = array_get($data, 'trade_no'); + + $request = new AopTradeQueryRequest($this->httpClient, $this->httpRequest); + $request->initialize($this->getParameters()); + $request->setEndpoint($this->getEndpoint()); + $request->setBizContent(['trade_no' => $tn]); + $request->setPrivateKey($this->getPrivateKey()); + + /** + * @var AopTradeQueryResponse $response + */ + $response = $request->send(); + + $tradeStatus = $response->getAlipayResponse('trade_status'); + + $data['trade_status'] = $tradeStatus; + } + return $this->response = new AopCompleteRefundResponse($this, $data); + } + + + /** + * @param $value + * + * @return $this + */ + public function setParams($value) + { + return $this->setParameter('params', $value); + } +} diff --git a/src/Responses/AopCompleteRefundResponse.php b/src/Responses/AopCompleteRefundResponse.php new file mode 100644 index 00000000..e340d748 --- /dev/null +++ b/src/Responses/AopCompleteRefundResponse.php @@ -0,0 +1,49 @@ +isSuccessful()) { + return 'success'; + } else { + return 'fail'; + } + } + + public function isRefunded() + { + $trade_status = array_get($this->data, 'trade_status'); + if ($trade_status) { + // 全额退款为 TRADE_CLOSED;非全额退款为 TRADE_SUCCESS + if ($trade_status == 'TRADE_CLOSED' || $trade_status == 'TRADE_SUCCESS') { + return true; + } else { + return false; + } + } elseif (array_get($this->data, 'code') == '10000') { + return true; + } + return false; + } +} diff --git a/tests/AopAppGatewayTest.php b/tests/AopAppGatewayTest.php index 1eac58e7..f592081c 100644 --- a/tests/AopAppGatewayTest.php +++ b/tests/AopAppGatewayTest.php @@ -5,7 +5,9 @@ use Omnipay\Alipay\AopAppGateway; use Omnipay\Alipay\Common\Signer; use Omnipay\Alipay\Responses\AopCompletePurchaseResponse; +use Omnipay\Alipay\Responses\AopCompleteRefundResponse; use Omnipay\Alipay\Responses\AopTradeAppPayResponse; +use Omnipay\Common\Exception\InvalidRequestException; class AopAppGatewayTest extends AbstractGatewayTestCase { @@ -109,10 +111,12 @@ public function testCompletePurchaseReturn() $this->gateway->setAlipayPublicKey($testPublicKey); - /** - * @var AopCompletePurchaseResponse $response - */ - $response = $this->gateway->completePurchase(['params' => $data])->send(); + try { + /** @var AopCompletePurchaseResponse $response */ + $response = $this->gateway->completePurchase()->setParams($data)->send(); + } catch (InvalidRequestException $e) { + $this->assertTrue(false); + } $this->assertEquals( '{"code":"10000","msg":"Success","app_id":"20151128008123456","auth_app_id":"20151128008123456","charset":"UTF-8","timestamp":"2016-09-23 18:32:16","total_amount":"0.01","trade_no":"2016092321001003060123456789","seller_id":"2088011466123456789","out_trade_no":"201609231231447556","sign":"jdl2MwvZLETOGMCrBvFuIHBlg+DUdd3fsuOqZWr78i1MRLoWOYWGoZNionb9hlW\/UwsRJU8D5Su1LgVADpQH9K\/yTjSH6eMQ4uZ+92QLsmeJxWWW2q85Ah36SULKMrJQDoap\/zWAl\/RV56BH8QpzBIPzby9idkt9VCIbIcSTaA0=","sign_type":"RSA"}', @@ -150,10 +154,12 @@ public function testCompletePurchaseNotify() $this->gateway->setAlipayPublicKey($testPublicKey); - /** - * @var AopCompletePurchaseResponse $response - */ - $response = $this->gateway->completePurchase(['params' => $data])->send(); + try { + /** @var AopCompletePurchaseResponse $response */ + $response = $this->gateway->completePurchase()->setParams($data)->send(); + } catch (InvalidRequestException $e) { + $this->assertTrue(false); + } $this->assertEquals( '{"total_amount":"0.01","buyer_id":"20882025611234567","trade_no":"201609232100100306021234567","refund_fee":"0.00","notify_time":"2016-09-23 19:12:33","subject":"test","sign_type":"RSA","notify_type":"trade_status_sync","out_trade_no":"2016092313071234567","gmt_close":"2016-09-23 19:08:10","trade_status":"TRADE_FINISHED","gmt_payment":"2016-09-23 19:08:10","sign":"Xa2NyOsxOBjW\/q\/RUFZhii2epa4B3ka+2aGsG8knqkiCD8llXrTDm11QtGkSRVw\/hbfcgFPiTkuaKnaaDu\/UfypsVSHToy28PiH5xkBSSd6zHNZCP\/jvjzOa6GPf4tIpfYNVvjaRMRcbn+TRlOFtHOnMMubjsg7K52P+LCugZIA=","gmt_create":"2016-09-23 19:08:09","app_id":"20151128001234567","seller_id":"20880114661234567","notify_id":"da3e56af64bcb163f167240dc0f781agge"}', @@ -191,10 +197,12 @@ public function testCompletePurchaseNotifyWithInlineKey() $this->gateway->setAlipayPublicKey($testPublicKey); - /** - * @var AopCompletePurchaseResponse $response - */ - $response = $this->gateway->completePurchase(['params' => $data])->send(); + try { + /** @var AopCompletePurchaseResponse $response */ + $response = $this->gateway->completePurchase()->setParams($data)->send(); + } catch (InvalidRequestException $e) { + $this->assertTrue(false); + } $this->assertEquals( '{"total_amount":"0.01","buyer_id":"20882025611234567","trade_no":"201609232100100306021234567","refund_fee":"0.00","notify_time":"2016-09-23 19:12:33","subject":"test","sign_type":"RSA","notify_type":"trade_status_sync","out_trade_no":"2016092313071234567","gmt_close":"2016-09-23 19:08:10","trade_status":"TRADE_FINISHED","gmt_payment":"2016-09-23 19:08:10","sign":"Xa2NyOsxOBjW\/q\/RUFZhii2epa4B3ka+2aGsG8knqkiCD8llXrTDm11QtGkSRVw\/hbfcgFPiTkuaKnaaDu\/UfypsVSHToy28PiH5xkBSSd6zHNZCP\/jvjzOa6GPf4tIpfYNVvjaRMRcbn+TRlOFtHOnMMubjsg7K52P+LCugZIA=","gmt_create":"2016-09-23 19:08:09","app_id":"20151128001234567","seller_id":"20880114661234567","notify_id":"da3e56af64bcb163f167240dc0f781agge"}', @@ -206,4 +214,45 @@ public function testCompletePurchaseNotifyWithInlineKey() $this->assertTrue($response->isPaid()); $this->assertEquals('201609232100100306021234567', $response->getData()['trade_no']); } + + public function testCompleteRefundNotify() + { + $testPrivateKey = ALIPAY_ASSET_DIR . '/dist/common/rsa_private_key.pem'; + $testPublicKey = ALIPAY_ASSET_DIR . '/dist/common/rsa_public_key_inline.pem'; + + $this->gateway = new AopAppGateway($this->getHttpClient(), $this->getHttpRequest()); + $this->gateway->setAppId($this->appId); + $this->gateway->setPrivateKey($this->appPrivateKey); + $this->gateway->setNotifyUrl('https://www.example.com/notify'); + + $str = '{"total_amount":"0.01","buyer_id":"20882025611234567","trade_no":"201609232100100306021234567","refund_fee":"0.00","notify_time":"2016-09-23 19:12:33","subject":"test","sign_type":"RSA","notify_type":"trade_status_sync","out_trade_no":"2016092313071234567","gmt_close":"2016-09-23 19:08:10","trade_status":"TRADE_FINISHED","gmt_payment":"2016-09-23 19:08:10","sign":"vCAj0n6vUVggDzZUqV4P2IucMeguUMaLBl5Uld7PeLHCo74/d3AcWCNCsGDxtW9Jm7+suyo6Y0jRY7OUi0PKZJre84m2q9Oo30AdgbMFRT91uZFYp9miJGWlQWwHhJDo3cU5iAYf5bnPPYgH8073kTFtmDPmrP9pvEUm3lsroUw=","gmt_create":"2016-09-23 19:08:09","app_id":"20151128001234567","seller_id":"20880114661234567","notify_id":"da3e56af64bcb163f167240dc0f781agge"}'; + + $str = stripslashes($str); + + $data = json_decode($str, true); + + $signer = new Signer($data); + $signer->setSort(true); + $signer->setEncodePolicy(Signer::ENCODE_POLICY_QUERY); + $data['sign'] = $signer->signWithRSA($testPrivateKey); + $data['sign_type'] = 'RSA'; + + $this->gateway->setAlipayPublicKey($testPublicKey); + + try { + /** @var AopCompleteRefundResponse $response */ + $response = $this->gateway->completeRefund()->setParams($data)->send(); + } catch (InvalidRequestException $e) { + // Params error or sign not match. + $response = -1; + } + + if ($response->isSuccessful() && $response->isRefunded()) { + // Refund successful + } else { + // Refund not successful + } + + $this->assertTrue($response !== -1); + } } diff --git a/tests/AopF2FGatewayTest.php b/tests/AopF2FGatewayTest.php index 64216b4e..ca3c8426 100644 --- a/tests/AopF2FGatewayTest.php +++ b/tests/AopF2FGatewayTest.php @@ -5,6 +5,7 @@ use Omnipay\Alipay\AopF2FGateway; use Omnipay\Alipay\Common\Signer; use Omnipay\Alipay\Responses\AopCompletePurchaseResponse; +use Omnipay\Alipay\Responses\AopCompleteRefundResponse; use Omnipay\Alipay\Responses\DataServiceBillDownloadUrlQueryResponse; use Omnipay\Alipay\Responses\AopTradePayResponse; use Omnipay\Alipay\Responses\AopTradePreCreateResponse; @@ -235,4 +236,42 @@ public function testCompletePurchase() $this->assertTrue($response->isPaid()); $this->assertEquals('2015061121001004400068549373', $response->getData()['trade_no']); } + + public function testCompleteRefund() + { + $testPrivateKey = ALIPAY_ASSET_DIR . '/dist/common/rsa_private_key.pem'; + $testPublicKey = ALIPAY_ASSET_DIR . '/dist/common/rsa_public_key.pem'; + + $this->gateway = new AopF2FGateway($this->getHttpClient(), $this->getHttpRequest()); + $this->gateway->setAppId($this->appId); + $this->gateway->setPrivateKey($this->appPrivateKey); + $this->gateway->setNotifyUrl('https://www.example.com/notify'); + + $str = 'gmt_payment=2015-06-11 22:33:59¬ify_id=42af7baacd1d3746cf7b56752b91edcj34&seller_email=testyufabu07@alipay.com¬ify_type=trade_status_sync&sign=kPbQIjX+xQc8F0/A6/AocELIjhhZnGbcBN6G4MM/HmfWL4ZiHM6fWl5NQhzXJusaklZ1LFuMo+lHQUELAYeugH8LYFvxnNajOvZhuxNFbN2LhF0l/KL8ANtj8oyPM4NN7Qft2kWJTDJUpQOzCzNnV9hDxh5AaT9FPqRS6ZKxnzM=&trade_no=2015061121001004400068549373&out_trade_no=21repl2ac2eOutTradeNo322&gmt_create=2015-06-11 22:33:46&seller_id=2088211521646673¬ify_time=2015-06-11 22:34:03&subject=FACE_TO_FACE_PAYMENT_PRECREATE中文&trade_status=TRADE_SUCCESS&sign_type=RSA'; + + parse_str($str, $data); + + $signer = new Signer($data); + $signer->setSort(true); + $signer->setEncodePolicy(Signer::ENCODE_POLICY_QUERY); + $data['sign'] = $signer->signWithRSA($testPrivateKey); + $data['sign_type'] = 'RSA'; + + $this->gateway->setAlipayPublicKey($testPublicKey); + + /** + * @var AopCompleteRefundResponse $response + */ + $response = $this->gateway->completeRefund(['params' => $data])->send(); + + $this->assertEquals( + '{"gmt_payment":"2015-06-11 22:33:59","notify_id":"42af7baacd1d3746cf7b56752b91edcj34","seller_email":"testyufabu07@alipay.com","notify_type":"trade_status_sync","sign":"T4JCUXoO5sK\/7UjupKEfsSQnjDnw\/1aSJnC6s53SYJyqdjFl+1Lt8dWdNuuXl5yX39leQsYzmk2CDwZx6F\/YIQWCo1LHZME3DYMqH\/F5wT5uiSUk2KYsYbLluW9pi7YHtBXRWKB6jtnn73DWWbC2sN3tDky9KySPizL5jQ1Cd0I=","trade_no":"2015061121001004400068549373","out_trade_no":"21repl2ac2eOutTradeNo322","gmt_create":"2015-06-11 22:33:46","seller_id":"2088211521646673","notify_time":"2015-06-11 22:34:03","subject":"FACE_TO_FACE_PAYMENT_PRECREATE\u4e2d\u6587","trade_status":"TRADE_SUCCESS","sign_type":"RSA"}', + json_encode($response->data()) + ); + + $this->assertEquals('21repl2ac2eOutTradeNo322', $response->data('out_trade_no')); + $this->assertTrue($response->isSuccessful()); + $this->assertTrue($response->isRefunded()); + $this->assertEquals('2015061121001004400068549373', $response->getData()['trade_no']); + } } diff --git a/tests/AopWapGatewayTest.php b/tests/AopWapGatewayTest.php index af2b701a..91922a7f 100644 --- a/tests/AopWapGatewayTest.php +++ b/tests/AopWapGatewayTest.php @@ -5,6 +5,7 @@ use Omnipay\Alipay\AopWapGateway; use Omnipay\Alipay\Common\Signer; use Omnipay\Alipay\Responses\AopCompletePurchaseResponse; +use Omnipay\Alipay\Responses\AopCompleteRefundResponse; use Omnipay\Alipay\Responses\AopTradeWapPayResponse; class AopWapGatewayTest extends AbstractGatewayTestCase @@ -85,4 +86,39 @@ public function testCompletePurchase() $this->assertTrue($response->isSuccessful()); $this->assertFalse($response->isPaid()); } + + public function testCompleteRefund() + { + $testPrivateKey = ALIPAY_ASSET_DIR . '/dist/common/rsa_private_key.pem'; + $testPublicKey = ALIPAY_ASSET_DIR . '/dist/common/rsa_public_key.pem'; + + $this->gateway = new AopWapGateway($this->getHttpClient(), $this->getHttpRequest()); + $this->gateway->setAppId($this->appId); + $this->gateway->setPrivateKey($this->appPrivateKey); + $this->gateway->setNotifyUrl('https://www.example.com/notify'); + $this->gateway->setReturnUrl('https://www.example.com/return'); + + $str = 'total_amount=0.01×tamp=2016-09-23+18%3A21%3A58&trade_no=201609232100100306012345678&auth_app_id=201511280012345678&charset=UTF-8&seller_id=20880114612345678&method=alipay.trade.wap.pay.return&app_id=20151128001234567&out_trade_no=201609231211234567&version=1.0'; + + parse_str($str, $data); + + $data['sign'] = (new Signer($data))->signWithRSA($testPrivateKey); + $data['sign_type'] = 'RSA'; + + $this->gateway->setAlipayPublicKey($testPublicKey); + + /** + * @var AopCompleteRefundResponse $response + */ + $response = $this->gateway->completeRefund(['params' => $data])->send(); + + $this->assertEquals( + '{"total_amount":"0.01","timestamp":"2016-09-23 18:21:58","trade_no":"201609232100100306012345678","auth_app_id":"201511280012345678","charset":"UTF-8","seller_id":"20880114612345678","method":"alipay.trade.wap.pay.return","app_id":"20151128001234567","out_trade_no":"201609231211234567","version":"1.0","sign":"ZuYCQRwbU50H2x1qevu0ZEFKwTE1piXpBG7GATUh3AZXF3S7CZ07Jj+mVDoa6WrOCGFfA8lHSbE28RX\/pl5QGxRuT+8B4KVo\/NWm3R10NCgqhkvBB+qPfUMSaBgaM+RR5m647QiKROmzX8sd4IgcedIZNKGicem+DJwNPayTLug=","sign_type":"RSA","trade_status":null}', + json_encode($response->data()) + ); + + $this->assertEquals('201609231211234567', $response->data('out_trade_no')); + $this->assertTrue($response->isSuccessful()); + $this->assertFalse($response->isRefunded()); + } }