diff --git a/.gitignore b/.gitignore index 8e830ab..54dd5d7 100644 --- a/.gitignore +++ b/.gitignore @@ -52,5 +52,8 @@ Temporary Items # VSCode project's cache .vscode +# phpunit cache +.phpunit.result.cache + # Vendor directory vendor/ diff --git a/.travis.yml b/.travis.yml index bad31a9..9305f90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,13 @@ language: php + php: - - '7.2' - - '7.3' -before_script: composer install + - 7.2 + - 7.3 + - 7.4 + +before_script: + - travis_retry composer self-update + - travis_retry composer update + - travis_retry composer install --prefer-source --no-interaction --dev + +script: phpunit diff --git a/README-FA.md b/README-FA.md index 805d423..9a4092f 100644 --- a/README-FA.md +++ b/README-FA.md @@ -52,6 +52,7 @@ - [اسان پرداخت](https://asanpardakht.ir/) :heavy_check_mark: - [به‌پرداخت (mellat)](http://www.behpardakht.com/) :heavy_check_mark: +- [دیجی پی](https://www.mydigipay.com/) :heavy_check_mark: - [ایدی پی](https://idpay.ir/) :heavy_check_mark: - [ایرانکیش](http://irankish.com/) :heavy_check_mark: - [نکست پی](https://nextpay.ir/) :heavy_check_mark: @@ -65,6 +66,7 @@ - [سداد (بانک ملی)](https://sadadpsp.ir/) :heavy_check_mark: - [سامان](https://www.sep.ir) :heavy_check_mark: - [سپهر (بانک صادرات)](https://www.sepehrpay.com/) :heavy_check_mark: +- [والتا (پرداخت اقساطی)](https://walleta.ir/) :heavy_check_mark: - [یک پی](https://yekpay.com/) :heavy_check_mark: - [زرین پال](https://www.zarinpal.com/) :heavy_check_mark: - [زیبال](https://www.zibal.ir/) :heavy_check_mark: diff --git a/README-ZH.md b/README-ZH.md index f8b31bc..6d9ba55 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -49,6 +49,7 @@ For **Laravel** integration you can use [shetabit/payment](https://github.com/sh # 可用驱动列表 - [asanpardakht](https://asanpardakht.ir/) :heavy_check_mark: - [behpardakht (mellat)](http://www.behpardakht.com/) :heavy_check_mark: +- [digipay](https://www.mydigipay.com/) :heavy_check_mark: - [idpay](https://idpay.ir/) :heavy_check_mark: - [irankish](http://irankish.com/) :heavy_check_mark: - [nextpay](https://nextpay.ir/) :heavy_check_mark: diff --git a/README.md b/README.md index 86ee542..fd48a8d 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ For **Laravel** integration you can use [shetabit/payment](https://github.com/sh # List of available drivers - [asanpardakht](https://asanpardakht.ir/) :heavy_check_mark: - [behpardakht (mellat)](http://www.behpardakht.com/) :heavy_check_mark: +- [digipay](https://www.mydigipay.com/) :heavy_check_mark: - [idpay](https://idpay.ir/) :heavy_check_mark: - [irankish](http://irankish.com/) :heavy_check_mark: - [nextpay](https://nextpay.ir/) :heavy_check_mark: @@ -63,6 +64,7 @@ For **Laravel** integration you can use [shetabit/payment](https://github.com/sh - [sadad (melli)](https://sadadpsp.ir/) :heavy_check_mark: - [saman](https://www.sep.ir) :heavy_check_mark: - [sepehr (saderat)](https://www.sepehrpay.com/) :heavy_check_mark: +- [walleta (Installment payment)](https://walleta.ir/) :heavy_check_mark: - [yekpay](https://yekpay.com/) :heavy_check_mark: - [zarinpal](https://www.zarinpal.com/) :heavy_check_mark: - [zibal](https://www.zibal.ir/) :heavy_check_mark: diff --git a/config/payment.php b/config/payment.php index f57ff99..a771e62 100755 --- a/config/payment.php +++ b/config/payment.php @@ -33,15 +33,11 @@ 'cancelButton' => 'پرداخت ناموفق', ], 'asanpardakht' => [ - 'apiPurchaseUrl' => 'https://ipgsoap.asanpardakht.ir/paygate/merchantservices.asmx?wsdl', 'apiPaymentUrl' => 'https://asan.shaparak.ir', - 'apiVerificationUrl' => 'https://ipgsoap.asanpardakht.ir/paygate/merchantservices.asmx?wsdl', - 'apiUtilsUrl' => 'https://ipgsoap.asanpardakht.ir/paygate/internalutils.asmx?wsdl', - 'key' => '', - 'iv' => '', + 'apiRestPaymentUrl' => 'https://ipgrest.asanpardakht.ir/v1/', 'username' => '', 'password' => '', - 'merchantId' => '', + 'merchantConfigID' => '', 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using asanpardakht', ], @@ -55,6 +51,17 @@ 'callbackUrl' => 'http://yoursite.com/path/to', 'description' => 'payment using behpardakht', ], + 'digipay' => [ + 'apiOauthUrl' => 'https://api.mydigipay.com/digipay/api/oauth/token', + 'apiPurchaseUrl' => 'https://api.mydigipay.com/digipay/api/businesses/ticket?type=0', + 'apiPaymentUrl' => 'https://api.mydigipay.com/digipay/api/purchases/ipg/pay/', + 'apiVerificationUrl' => 'https://api.mydigipay.com/digipay/api/purchases/verify/', + 'username' => 'username', + 'password' => 'password', + 'client_id' => '', + 'client_secret' => '', + 'callbackUrl' => 'http://yoursite.com/path/to', + ], 'idpay' => [ 'apiPurchaseUrl' => 'https://api.idpay.ir/v1.1/payment', 'apiPaymentUrl' => 'https://idpay.ir/p/ws/', @@ -176,6 +183,14 @@ 'callbackUrl' => '', 'description' => 'payment using sepehr(saderat)', ], + 'walleta' => [ + 'apiPurchaseUrl' => 'https://cpg.walleta.ir/payment/request.json', + 'apiPaymentUrl' => 'https://cpg.walleta.ir/ticket/', + 'apiVerificationUrl' => 'https://cpg.walleta.ir/payment/verify.json', + 'merchantId' => '', + 'callbackUrl' => 'http://yoursite.com/path/to', + 'description' => 'payment using walleta', + ], 'yekpay' => [ 'apiPurchaseUrl' => 'https://gate.yekpay.com/api/payment/server?wsdl', 'apiPaymentUrl' => 'https://gate.yekpay.com/api/payment/start/', @@ -188,9 +203,9 @@ ], 'zarinpal' => [ /* normal api */ - 'apiPurchaseUrl' => 'https://ir.zarinpal.com/pg/services/WebGate/wsdl', + 'apiPurchaseUrl' => 'https://api.zarinpal.com/pg/v4/payment/request.json', 'apiPaymentUrl' => 'https://www.zarinpal.com/pg/StartPay/', - 'apiVerificationUrl' => 'https://ir.zarinpal.com/pg/services/WebGate/wsdl', + 'apiVerificationUrl' => 'https://api.zarinpal.com/pg/v4/payment/verify.json', /* sandbox api */ 'sandboxApiPurchaseUrl' => 'https://sandbox.zarinpal.com/pg/services/WebGate/wsdl', @@ -237,6 +252,7 @@ 'local' => \Shetabit\Multipay\Drivers\Local\Local::class, 'asanpardakht' => \Shetabit\Multipay\Drivers\Asanpardakht\Asanpardakht::class, 'behpardakht' => \Shetabit\Multipay\Drivers\Behpardakht\Behpardakht::class, + 'digipay' => \Shetabit\Multipay\Drivers\Digipay\Digipay::class, 'idpay' => \Shetabit\Multipay\Drivers\Idpay\Idpay::class, 'irankish' => \Shetabit\Multipay\Drivers\Irankish\Irankish::class, 'nextpay' => \Shetabit\Multipay\Drivers\Nextpay\Nextpay::class, @@ -250,6 +266,7 @@ 'sadad' => \Shetabit\Multipay\Drivers\Sadad\Sadad::class, 'saman' => \Shetabit\Multipay\Drivers\Saman\Saman::class, 'sepehr' => \Shetabit\Multipay\Drivers\Sepehr\Sepehr::class, + 'walleta' => \Shetabit\Multipay\Drivers\Walleta\Walleta::class, 'yekpay' => \Shetabit\Multipay\Drivers\Yekpay\Yekpay::class, 'zarinpal' => \Shetabit\Multipay\Drivers\Zarinpal\Zarinpal::class, 'zibal' => \Shetabit\Multipay\Drivers\Zibal\Zibal::class, diff --git a/phpunit.xml b/phpunit.xml index db3e54a..d0d4f3b 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,18 +1,16 @@ - - - - ./src/ - - + stopOnFailure="false" + > ./tests/ diff --git a/src/Drivers/Asanpardakht/Asanpardakht.php b/src/Drivers/Asanpardakht/Asanpardakht.php index 116a7e4..f73680d 100644 --- a/src/Drivers/Asanpardakht/Asanpardakht.php +++ b/src/Drivers/Asanpardakht/Asanpardakht.php @@ -2,17 +2,25 @@ namespace Shetabit\Multipay\Drivers\Asanpardakht; +use GuzzleHttp\Client; use Shetabit\Multipay\Abstracts\Driver; -use Shetabit\Multipay\Exceptions\InvalidPaymentException; use Shetabit\Multipay\Exceptions\PurchaseFailedException; use Shetabit\Multipay\Contracts\ReceiptInterface; use Shetabit\Multipay\Invoice; use Shetabit\Multipay\Receipt; use Shetabit\Multipay\RedirectionForm; -use Shetabit\Multipay\Request; class Asanpardakht extends Driver { + const TokenURL = 'Token'; + const TimeURL = 'Time'; + const TranResultURL = 'TranResult'; + const CardHashURL = 'CardHash'; + const SettlementURL = 'Settlement'; + const VerifyURL = 'Verify'; + const CancelURL = 'Cancel'; + const ReverseURL = 'Reverse'; + /** * Invoice * @@ -20,6 +28,19 @@ class Asanpardakht extends Driver */ protected $invoice; + /** + * Response + * + * @var object + */ + protected $response; + + /** + * PayGateTransactionId + * + */ + protected $payGateTransactionId; + /** * Driver settings * @@ -38,6 +59,9 @@ public function __construct(Invoice $invoice, $settings) { $this->invoice($invoice); $this->settings = (object)$settings; + + //convert to rial + $this->invoice->amount($this->invoice->getAmount() * 10); } /** @@ -46,25 +70,18 @@ public function __construct(Invoice $invoice, $settings) * @return string * * @throws PurchaseFailedException - * @throws \SoapFault */ public function purchase() { - $client = $this->createSoapClient($this->settings->apiPurchaseUrl); + $this->invoice->uuid(crc32($this->invoice->getUuid())); - $params = $this->preparePurchaseData(); - $result = $client->RequestOperation($params); - if (!$result) { - throw new PurchaseFailedException('خطای فراخوانی متد درخواست تراکنش.'); - } + $result = $this->token(); - $result = $result->RequestOperationResult; - if ($result[0] != '0') { - $message = "خطای شماره " . $result . " رخ داده است."; - throw new PurchaseFailedException($message); + if (!isset($result['status_code']) or $result['status_code'] != 200) { + $this->purchaseFailed($result['status_code']); } - $this->invoice->transactionId(substr($result, 2)); + $this->invoice->transactionId($result['content']); // return the transaction's id return $this->invoice->getTransactionId(); @@ -77,10 +94,8 @@ public function purchase() */ public function pay(): RedirectionForm { - $payUrl = $this->settings->apiPaymentUrl; - $data = [ - 'RefId' => $this->invoice->getTransactionId() + 'RefID' => $this->invoice->getTransactionId() ]; //set mobileap for get user cards @@ -88,7 +103,7 @@ public function pay(): RedirectionForm $data['mobileap'] = $this->invoice->getDetails()['mobile']; } - return $this->redirectWithForm($payUrl, $data, 'POST'); + return $this->redirectWithForm($this->settings->apiPaymentUrl, $data, 'POST'); } /** @@ -96,95 +111,65 @@ public function pay(): RedirectionForm * * @return mixed|Receipt * - * @throws InvalidPaymentException - * @throws \SoapFault + * @throws PurchaseFailedException */ public function verify(): ReceiptInterface { - $encryptedReturningParamsString = Request::input('ReturningParams'); - $returningParamsString = $this->decrypt($encryptedReturningParamsString); - $returningParams = explode(",", $returningParamsString); - - /** - * other data: - * $amount = $returningParams[0]; - * $saleOrderId = $returningParams[1]; - * $refId = $this->invoice->getTransactionId() ?? $returningParams[2]; - * $resMessage = $returningParams[4]; - * $rrn = $returningParams[6]; - * $lastFourDigitOfPAN = $returningParams[7]; - **/ - - $resCode = $returningParams[3]; - $payGateTranID = $returningParams[5]; - - if ($resCode != '0' && $resCode != '00') { - $message = "خطای شماره " . $resCode . " رخ داده و تراکنش ناموفق بوده است."; - throw new InvalidPaymentException($message); + $result = $this->transactionResult(); + + if (!isset($result['status_code']) or $result['status_code'] != 200) { + $this->purchaseFailed($result['status_code']); } - $client = $this->createSoapClient($this->settings->apiVerificationUrl); - $params = $this->prepareVerificationData($payGateTranID); + $this->payGateTransactionId = $result['content']['payGateTranID']; + + //step1: verify + $verify_result = $this->verifyTransaction(); - // step1: verify - $this->verifyStep($client, $params); + if (!isset($verify_result['status_code']) or $verify_result['status_code'] != 200) { + $this->purchaseFailed($verify_result['status_code']); + } - // step2: settle - $this->settleStep($client, $params); + //step2: settlement + $this->settlement(); - $receipt = $this->createReceipt($payGateTranID); + $receipt = $this->createReceipt($this->payGateTransactionId); $receipt->detail([ - 'traceNo' => $payGateTranID, - 'referenceNo' => $returningParams[6], - 'transactionId' => $returningParams[2], - 'cardNo' => $returningParams[7], + 'traceNo' => $this->payGateTransactionId, + 'referenceNo' => $result['content']['rrn'], + 'transactionId' => $result['content']['refID'], + 'cardNo' => $result['content']['cardNumber'], ]); return $receipt; } /** - * payment verification step - * - * @param $client - * @param $params - * - * @throws InvalidPaymentException - */ - protected function verifyStep($client, $params) - { - $result = $client->RequestVerification($params); - if (!$result) { - throw new InvalidPaymentException("خطای فراخوانی متد وريفای رخ داده است."); - } - - $result = $result->RequestVerificationResult; - if ($result != '500') { - $message = "خطای شماره: " . $result . " در هنگام Verify"; - throw new InvalidPaymentException($message); - } - } - - /** - * payment settlement step. + * send request to Asanpardakht * - * @param $client - * @param $params - * - * @throws InvalidPaymentException + * @param $method + * @param $url + * @param array $data + * @return array */ - protected function settleStep($client, $params) + protected function callApi($method, $url, $data = []): array { - $result = $client->RequestReconciliation($params); - if (!$result) { - throw new InvalidPaymentException('خطای فراخوانی متد تسويه رخ داده است.'); - } + $client = new Client(['base_uri' => $this->settings->apiRestPaymentUrl]); + + $response = $client->request($method, $url, [ + "json" => $data, + "headers" => [ + 'Content-Type' => 'application/json', + 'usr' => $this->settings->username, + 'pwd' => $this->settings->password + ], + "http_errors" => false, + ]); - $result = $result->RequestReconciliationResult; - if ($result != '600') { - $message = "خطای شماره: " . $result . " در هنگام Settlement"; - throw new InvalidPaymentException($message); - } + return [ + 'status_code' => $response->getStatusCode(), + 'content' => json_decode($response->getBody()->getContents(), true) + ]; } /** @@ -194,7 +179,7 @@ protected function settleStep($client, $params) * * @return Receipt */ - protected function createReceipt($referenceId) + protected function createReceipt($referenceId): Receipt { $receipt = new Receipt('asanpardakht', $referenceId); @@ -202,134 +187,161 @@ protected function createReceipt($referenceId) } /** - * Prepare data for payment verification - * - * @param $payGateTranID + * call create token request * * @return array - * - * @throws \SoapFault */ - protected function prepareVerificationData($payGateTranID) + public function token(): array { - $credentials = array( - $this->settings->username, - $this->settings->password - ); - - $encryptedCredentials = $this->encrypt(implode(',', $credentials)); - - return array( - 'merchantConfigurationID' => $this->settings->merchantId, - 'encryptedCredentials' => $encryptedCredentials, - 'payGateTranID' => $payGateTranID - ); + return $this->callApi('POST', self::TokenURL, [ + 'serviceTypeId' => 1, + 'merchantConfigurationId' => $this->settings->merchantConfigID, + 'localInvoiceId' => $this->invoice->getUuid(), + 'amountInRials' => $this->invoice->getAmount(), + 'localDate' => $this->getTime()['content'], + 'callbackURL' => $this->settings->callbackUrl . "?" . http_build_query(['invoice' => $this->invoice->getUuid()]), + 'paymentId' => "0", + 'additionalData' => '', + ]); } /** - * Prepare data for purchasing invoice + * call reserve request * * @return array - * - * @throws \SoapFault */ - protected function preparePurchaseData() + public function reverse(): array { - if (!empty($this->invoice->getDetails()['description'])) { - $description = $this->invoice->getDetails()['description']; - } else { - $description = $this->settings->description; - } - - // configs - $username = $this->settings->username; - $password = $this->settings->password; - $callBackUrl = $this->settings->callbackUrl; - - // invoice details - $price = $this->invoice->getAmount() * 10; // convert to rial - $additionalData = $description ?? ''; - $orderId = crc32($this->invoice->getUuid()); - $localDate = date("Ymd His"); - - // box and encrypt everything - $requestString = "1,{$username},{$password},{$orderId},{$price},{$localDate},{$additionalData},{$callBackUrl},0"; - $encryptedRequestString = $this->encrypt($requestString); - - return array( - 'merchantConfigurationID' => $this->settings->merchantId, - 'encryptedRequest' => $encryptedRequestString - ); + return $this->callApi('POST', self::ReverseURL, [ + 'merchantConfigurationId' => (int)$this->settings->merchantConfigID, + 'payGateTranId' => (int)$this->invoice->getUuid() + ]); } /** - * Encrypt given string. + * send cancel request * - * @param $string - * - * @return mixed - * - * @throws \SoapFault + * @return array */ - protected function encrypt($string) + public function cancel(): array { - $client = $this->createSoapClient($this->settings->apiUtilsUrl); - - $params = array( - 'aesKey' => $this->settings->key, - 'aesVector' => $this->settings->iv, - 'toBeEncrypted' => $string - ); - - $result = $client->EncryptInAES($params); - - return $result->EncryptInAESResult; + return $this->callApi('POST', self::CancelURL, [ + 'merchantConfigurationId' => (int)$this->settings->merchantConfigID, + 'payGateTranId' => (int)$this->payGateTransactionId + ]); } /** - * Decrypt given string. - * - * @param $string - * - * @return mixed + * send verify request * - * @throws \SoapFault + * @return array */ - protected function decrypt($string) + public function verifyTransaction(): array { - $client = $this->createSoapClient($this->settings->apiUtilsUrl); + return $this->callApi('POST', self::VerifyURL, [ + 'merchantConfigurationId' => (int)$this->settings->merchantConfigID, + 'payGateTranId' => (int)$this->payGateTransactionId + ]); + } - $params = array( - 'aesKey' => $this->settings->key, - 'aesVector' => $this->settings->iv, - 'toBeDecrypted' => $string - ); + /** + * send settlement request + * + * @return array + */ + public function settlement(): array + { + return $this->callApi('POST', self::SettlementURL, [ + 'merchantConfigurationId' => (int)$this->settings->merchantConfigID, + 'payGateTranId' => (int)$this->payGateTransactionId + ]); + } - $result = $client->DecryptInAES($params); + /** + * get card hash request + * + * @return array + */ + public function cardHash(): array + { + return $this->callApi('GET', self::CardHashURL . '?merchantConfigurationId=' . $this->settings->merchantConfigID . '&localInvoiceId=' . $this->invoice->getTransactionId(), []); + } - return $result->DecryptInAESResult; + /** + * get transaction result + * + * @return array + */ + public function transactionResult(): array + { + return $this->callApi('GET', self::TranResultURL . '?merchantConfigurationId=' . $this->settings->merchantConfigID . '&localInvoiceId=' . $this->invoice->getTransactionId(), []); } /** - * create a new SoapClient + * get Asanpardakht server time * - * @param $url + * @return array + */ + public function getTime(): array + { + return $this->callApi('GET', self::TimeURL); + } + + /** + * Trigger an exception * - * @return \SoapClient + * @param $status * - * @throws \SoapFault + * @throws PurchaseFailedException */ - protected function createSoapClient($url) + protected function purchaseFailed($status) { - $opts = array( - 'ssl' => array( - 'verify_peer' => false, - 'verify_peer_name' => false - ) - ); - - $configs = array('stream_context' => stream_context_create($opts)); + $translations = [ + 400 => "bad request", + 401 => "unauthorized. probably wrong or unsent header(s)", + 471 => "identity not trusted to proceed", + 472 => "no records found", + 473 => "invalid merchant username or password", + 474 => "invalid incoming request machine ip. check response body to see your actual public IP address", + 475 => "invoice identifier is not a number", + 476 => "request amount is not a number", + 477 => "request local date length is invalid", + 478 => "request local date is not in valid format", + 479 => "invalid service type id", + 480 => "invalid payer id", + 481 => "incorrect settlement description format", + 482 => "settlement slices does not match total amount", + 483 => "unregistered iban", + 484 => "internal error for other reasons", + 485 => "invalid local date", + 486 => "amount not in range", + 487 => "service not found or not available for merchant", + 488 => "invalid default callback", + 489 => "duplicate local invoice id", + 490 => "merchant disabled or misconfigured", + 491 => "too many settlement destinations", + 492 => "unprocessable request", + 493 => "error processing special request for other reasons like business restrictions", + 494 => "invalid payment_id for governmental payment", + 495 => "invalid referenceId in additionalData", + 496 => "invalid json in additionalData", + 497 => "invalid payment_id location", + 571 => "misconfiguration OR not yet processed", + 572 => "misconfiguration OR transaction status undetermined", + 573 => "misconfiguraed valid ips for configuration OR unable to request for verification due to an internal error", + 574 => "internal error in uthorization", + 575 => "no valid ibans found for merchant", + 576 => "internal error", + 577 => "internal error", + 578 => "no default sharing is defined for merchant", + 579 => "cant submit ibans with default sharing endpoint", + 580 => "error processing special request" + ]; - return new \SoapClient($url, $configs); + if (array_key_exists($status, $translations)) { + throw new PurchaseFailedException($translations[$status]); + } else { + throw new PurchaseFailedException('خطای ناشناخته ای رخ داده است.'); + } } } diff --git a/src/Drivers/Behpardakht/Behpardakht.php b/src/Drivers/Behpardakht/Behpardakht.php index ae21da0..d7652b5 100644 --- a/src/Drivers/Behpardakht/Behpardakht.php +++ b/src/Drivers/Behpardakht/Behpardakht.php @@ -52,7 +52,22 @@ public function __construct(Invoice $invoice, $settings) public function purchase() { - $soap = new \SoapClient($this->settings->apiPurchaseUrl); + if (isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] == "HTTP/2.0") { + $context = stream_context_create( + [ + 'ssl' => array( + 'verify_peer' => false, + 'verify_peer_name' => false + )] + ); + + $soap = new \SoapClient($this->settings->apiPurchaseUrl, [ + 'stream_context' => $context + ]); + } else { + $soap = new \SoapClient($this->settings->apiPurchaseUrl); + } + $response = $soap->bpPayRequest($this->preparePurchaseData()); // fault has happened in bank gateway @@ -110,7 +125,22 @@ public function verify(): ReceiptInterface } $data = $this->prepareVerificationData(); - $soap = new \SoapClient($this->settings->apiVerificationUrl); + + if (isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] == "HTTP/2.0") { + $context = stream_context_create( + [ + 'ssl' => array( + 'verify_peer' => false, + 'verify_peer_name' => false + )] + ); + + $soap = new \SoapClient($this->settings->apiPurchaseUrl, [ + 'stream_context' => $context + ]); + } else { + $soap = new \SoapClient($this->settings->apiPurchaseUrl); + } // step1: verify request $verifyResponse = (int)$soap->bpVerifyRequest($data)->return; @@ -133,8 +163,16 @@ public function verify(): ReceiptInterface } throw new InvalidPaymentException($this->translateStatus($settleResponse), $settleResponse); } + + $receipt = $this->createReceipt($data['saleReferenceId']); + $receipt->detail([ + 'traceNo' => Request::input('SaleOrderId'), + 'referenceNo' => Request::input('SaleReferenceId'), + 'transactionId' => Request::input('RefId'), + 'cardNo' => Request::input('CardHolderPan'), + ]); - return $this->createReceipt($data['saleReferenceId']); + return $receipt; } /** diff --git a/src/Drivers/Digipay/Digipay.php b/src/Drivers/Digipay/Digipay.php new file mode 100644 index 0000000..f252622 --- /dev/null +++ b/src/Drivers/Digipay/Digipay.php @@ -0,0 +1,168 @@ +invoice($invoice); + $this->settings=$settings; + $this->client = new Client(); + $this->oauthToken = $this->oauth(); + } + + public function purchase() + { + $details = $this->invoice->getDetails(); + + $phone = null; + if (!empty($details['phone'])) { + $phone = $details['phone']; + } elseif (!empty($details['mobile'])) { + $phone = $details['mobile']; + } + $data = array( + 'amount' => $this->invoice->getAmount(), + 'phone' => $phone, + 'providerId' => $this->invoice->getUuid(), + 'redirectUrl' => $this->settings->callbackUrl, + 'type' => 0, + 'userType' => is_null($phone) ? 2 : 0 + ); + + $response = $this + ->client + ->request( + 'POST', + $this->settings->apiPurchaseUrl, + [ + "json" => $data, + "headers" => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '.$this->oauthToken + ], + "http_errors" => false, + ] + ); + + $body = json_decode($response->getBody()->getContents(), true); + if ($response->getStatusCode() != 200) { + // error has happened + $message = $body['result']['message'] ?? 'خطا در هنگام درخواست برای پرداخت رخ داده است.'; + throw new PurchaseFailedException($message); + } + + $this->invoice->transactionId($body['ticket']); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + public function pay(): RedirectionForm + { + $payUrl = $this->settings->apiPaymentUrl.$this->invoice->getTransactionId(); + + return $this->redirectWithForm($payUrl, [], 'GET'); + } + + public function verify(): ReceiptInterface + { + $tracingId=Request::input("trackingCode"); + + $response = $this->client->request( + 'POST', + $this->settings->apiVerificationUrl.$tracingId, + [ + 'json' => [], + "headers" => [ + "Accept" => "application/json", + "Authorization" => "Bearer ".$this->oauthToken, + ], + "http_errors" => false, + ] + ); + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() != 200) { + $message = 'تراکنش تایید نشد'; + + throw new InvalidPaymentException($message); + } + + return new Receipt('digipay', $body["trackingCode"]); + } + + protected function oauth() + { + $response = $this + ->client + ->request( + 'POST', + $this->settings->apiOauthUrl, + [ + "headers" => [ + 'Content-Type' => 'multipart/form-data', + 'Authorization' => 'Basic '.base64_encode("{$this->settings->client_id}:{$this->settings->client_secret}") + ], + "username" => $this->settings->username, + "password" => $this->settings->password, + "grant_type" => 'password', + ] + ); + if ($response->getStatusCode()!=200) { + if ($response->getStatusCode()==401) { + throw new PurchaseFailedException("خطا نام کاربری یا رمز عبور شما اشتباه می باشد."); + } else { + throw new PurchaseFailedException("خطا در هنگام احراز هویت."); + } + } + $body = json_decode($response->getBody()->getContents(), true); + return $body['access_token']; + } +} diff --git a/src/Drivers/Sadad/Sadad.php b/src/Drivers/Sadad/Sadad.php index 40e907e..0c07a20 100644 --- a/src/Drivers/Sadad/Sadad.php +++ b/src/Drivers/Sadad/Sadad.php @@ -173,7 +173,7 @@ public function verify() : ReceiptInterface $receipt->detail([ 'orderId' => $body->OrderId, 'traceNo' => $body->SystemTraceNo, - 'referenceNo' => $body->RetrievalRefNo, + 'referenceNo' => $body->RetrivalRefNo, 'description' => $body->Description, ]); diff --git a/src/Drivers/Walleta/Walleta.php b/src/Drivers/Walleta/Walleta.php new file mode 100644 index 0000000..fd3dedd --- /dev/null +++ b/src/Drivers/Walleta/Walleta.php @@ -0,0 +1,226 @@ +invoice($invoice); + $this->settings = (object)$settings; + } + + /** + * Purchase Invoice.09214125578 + * + * @return string + * + * @throws PurchaseFailedException + */ + public function purchase() + { + $result = $this->token(); + + if (!isset($result['status_code']) or $result['status_code'] != 200) { + $this->purchaseFailed($result['content']['type']); + } + + $this->invoice->transactionId($result['content']['token']); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay(): RedirectionForm + { + return $this->redirectWithForm($this->settings->apiPaymentUrl . $this->invoice->getTransactionId(), [], 'GET'); + } + + /** + * Verify payment + * + * @return mixed|Receipt + * + * @throws PurchaseFailedException + */ + public function verify(): ReceiptInterface + { + $result = $this->verifyTransaction(); + + if (!isset($result['status_code']) or $result['status_code'] != 200) { + $this->purchaseFailed($result['content']['type']); + } + + $receipt = $this->createReceipt($this->invoice->getTransactionId()); + + return $receipt; + } + + /** + * send request to Walleta + * + * @param $method + * @param $url + * @param array $data + * @return array + */ + protected function callApi($method, $url, $data = []): array + { + $client = new Client(); + + $response = $client->request($method, $url, [ + "json" => $data, + "headers" => [ + 'Content-Type' => 'application/json', + ], + "http_errors" => false, + ]); + + return [ + 'status_code' => $response->getStatusCode(), + 'content' => json_decode($response->getBody()->getContents(), true) + ]; + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + protected function createReceipt($referenceId): Receipt + { + $receipt = new Receipt('walleta', $referenceId); + + return $receipt; + } + + /** + * call create token request + * + * @return array + */ + public function token(): array + { + return $this->callApi('POST', $this->settings->apiPurchaseUrl, [ + 'merchant_code' => $this->settings->merchantId, + 'invoice_reference' => $this->invoice->getUuid(), + 'invoice_date' => date('Y-m-d H:i:s'), + 'invoice_amount' => $this->invoice->getAmount(), + 'payer_first_name' => $this->invoice->getDetails()['first_name'], + 'payer_last_name' => $this->invoice->getDetails()['last_name'], + 'payer_national_code' => $this->invoice->getDetails()['national_code'], + 'payer_mobile' => $this->invoice->getDetails()['mobile'], + 'callback_url' => $this->settings->callbackUrl, + 'items' => $this->getItems(), + ]); + } + + /** + * call verift transaction request + * + * @return array + */ + public function verifyTransaction(): array + { + return $this->callApi('POST', $this->settings->apiVerificationUrl, [ + 'merchant_code' => $this->settings->merchantId, + 'token' => $this->invoice->getTransactionId(), + 'invoice_reference' => $this->invoice->getDetail('uuid'), + 'invoice_amount' => $this->invoice->getAmount(), + ]); + } + + /** + * get Items for + * + * + */ + private function getItems() + { + /** + * example data + * + * $items = [ + * [ + * "reference" => "string", + * "name" => "string", + * "quantity" => 0, + * "unit_price" => 0, + * "unit_discount" => 0, + * "unit_tax_amount" => 0, + * "total_amount" => 0 + * ] + * ]; + */ + return $this->invoice->getDetails()['items']; + } + + + /** + * Trigger an exception + * + * @param $status + * + * @throws PurchaseFailedException + */ + protected function purchaseFailed($status) + { + $translations = [ + "server_error" => "یک خطای داخلی رخ داده است.", + "ip_address_error" => "آدرس IP پذیرنده صحیح نیست.", + "validation_error" => "اطلاعات ارسال شده صحیح نیست.", + "merchant_error" => "کد پذیرنده معتبر نیست.", + "payment_token_error" => "شناسه پرداخت معتبر نیست.", + "invoice_amount_error" => "مبلغ تراکنش با مبلغ پرداخت شده مطابقت ندارد.", + ]; + + if (array_key_exists($status, $translations)) { + throw new PurchaseFailedException($translations[$status]); + } else { + throw new PurchaseFailedException('خطای ناشناخته ای رخ داده است.'); + } + } +} diff --git a/src/Drivers/Zarinpal/Strategies/Normal.php b/src/Drivers/Zarinpal/Strategies/Normal.php new file mode 100644 index 0000000..1c3ee7d --- /dev/null +++ b/src/Drivers/Zarinpal/Strategies/Normal.php @@ -0,0 +1,218 @@ +invoice($invoice); + $this->settings = (object) $settings; + $this->client = new Client(); + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + * @throws \SoapFault + */ + public function purchase() + { + $metadata = []; + + if (!empty($this->invoice->getDetails()['description'])) { + $description = $this->invoice->getDetails()['description']; + } else { + $description = $this->settings->description; + } + + if (!empty($this->invoice->getDetails()['mobile'])) { + $metadata['mobile'] = $this->invoice->getDetails()['mobile']; + } + + if (!empty($this->invoice->getDetails()['email'])) { + $metadata['email'] = $this->invoice->getDetails()['email']; + } + + $data = [ + "merchant_id" => $this->settings->merchantId, + "amount" => $this->invoice->getAmount() * 10, // convert toman to rial + "callback_url" => $this->settings->callbackUrl, + "description" => $description, + "metadata" => array_merge($this->invoice->getDetails() ?? [], $metadata), + ]; + + + $response = $this + ->client + ->request( + 'POST', + $this->settings->apiPurchaseUrl, + [ + "json" => $data, + "headers" => [ + 'Content-Type' => 'application/json', + ], + "http_errors" => false, + ] + ); + + $result = json_decode($response->getBody()->getContents(), true); + + // some error has happened + if (! empty($result['errors']) || empty($result['data']) || $result['data']['code'] != 100) { + throw new PurchaseFailedException($result['errors']['message'], $result['errors']['code']); + } + + $this->invoice->transactionId($result['data']["authority"]); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay() : RedirectionForm + { + $transactionId = $this->invoice->getTransactionId(); + $paymentUrl = $this->getPaymentUrl(); + + $payUrl = $paymentUrl.$transactionId; + + return $this->redirectWithForm($payUrl, [], 'GET'); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + */ + public function verify() : ReceiptInterface + { + $status = Request::input('Status'); + if ($status != 'OK') { + throw new InvalidPaymentException('عملیات پرداخت توسط کاربر لغو شد.', -54); + } + + $authority = $this->invoice->getTransactionId() ?? Request::input('Authority'); + $data = [ + "merchant_id" => $this->settings->merchantId, + "authority" => $authority, + "amount" => $this->invoice->getAmount() * 10, // convert toman to rial + ]; + + $response = $this->client->request( + 'POST', + $this->getVerificationUrl(), + [ + 'json' => $data, + "headers" => [ + 'Content-Type' => 'application/json', + ], + "http_errors" => false, + ] + ); + + $result = json_decode($response->getBody()->getContents(), true); + + if (empty($result['data']) || ! isset($result['data']['ref_id']) || ($result['data']['code'] != 100 && $result['data']['code'] != 101)) { + $message = $result['errors']['message']; + $code = $result['errors']['code']; + throw new InvalidPaymentException($message, $code); + } + if ($result['data']['code'] == 101) { + $message = $result['data']['message']; + $code = $result['data']['code']; + throw new InvalidPaymentException($message, $code); + } + return $this->createReceipt($result['data']['ref_id']); + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + public function createReceipt($referenceId) + { + return new Receipt('zarinpal', $referenceId); + } + + /** + * Retrieve purchase url + * + * @return string + */ + protected function getPurchaseUrl() : string + { + return $this->settings->apiPurchaseUrl; + } + + /** + * Retrieve Payment url + * + * @return string + */ + protected function getPaymentUrl() : string + { + return $this->settings->apiPaymentUrl; + } + + /** + * Retrieve verification url + * + * @return string + */ + protected function getVerificationUrl() : string + { + return $this->settings->apiVerificationUrl; + } +} diff --git a/src/Drivers/Zarinpal/Strategies/Sandbox.php b/src/Drivers/Zarinpal/Strategies/Sandbox.php new file mode 100644 index 0000000..669bd48 --- /dev/null +++ b/src/Drivers/Zarinpal/Strategies/Sandbox.php @@ -0,0 +1,213 @@ +invoice($invoice); + $this->settings = (object) $settings; + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + * @throws \SoapFault + */ + public function purchase() + { + if (!empty($this->invoice->getDetails()['description'])) { + $description = $this->invoice->getDetails()['description']; + } else { + $description = $this->settings->description; + } + + if (!empty($this->invoice->getDetails()['mobile'])) { + $mobile = $this->invoice->getDetails()['mobile']; + } + + if (!empty($this->invoice->getDetails()['email'])) { + $email = $this->invoice->getDetails()['email']; + } + + $data = array( + 'MerchantID' => $this->settings->merchantId, + 'Amount' => $this->invoice->getAmount(), + 'CallbackURL' => $this->settings->callbackUrl, + 'Description' => $description, + 'Mobile' => $mobile ?? '', + 'Email' => $email ?? '', + 'AdditionalData' => $this->invoice->getDetails() + ); + + $client = new \SoapClient($this->getPurchaseUrl(), ['encoding' => 'UTF-8']); + $result = $client->PaymentRequest($data); + + if ($result->Status != 100 || empty($result->Authority)) { + // some error has happened + $message = $this->translateStatus($result->Status); + throw new PurchaseFailedException($message, $result->Status); + } + + $this->invoice->transactionId($result->Authority); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay() : RedirectionForm + { + $transactionId = $this->invoice->getTransactionId(); + $paymentUrl = $this->getPaymentUrl(); + + $payUrl = $paymentUrl.$transactionId; + + return $this->redirectWithForm($payUrl, [], 'GET'); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + * @throws \SoapFault + */ + public function verify() : ReceiptInterface + { + $status = Request::input('Status'); + if ($status != 'OK') { + throw new InvalidPaymentException('عملیات پرداخت توسط کاربر لغو شد.', -22); + } + + $authority = $this->invoice->getTransactionId() ?? Request::input('Authority'); + $data = [ + 'MerchantID' => $this->settings->merchantId, + 'Authority' => $authority, + 'Amount' => $this->invoice->getAmount(), + ]; + + $client = new \SoapClient($this->getVerificationUrl(), ['encoding' => 'UTF-8']); + $result = $client->PaymentVerification($data); + + if ($result->Status != 100) { + $message = $this->translateStatus($result->Status); + throw new InvalidPaymentException($message, $result->Status); + } + + return $this->createReceipt($result->RefID); + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + public function createReceipt($referenceId) + { + return new Receipt('zarinpal', $referenceId); + } + + /** + * Convert status to a readable message. + * + * @param $status + * + * @return mixed|string + */ + private function translateStatus($status) + { + $translations = array( + "-1" => "اطلاعات ارسال شده ناقص است.", + "-2" => "IP و يا مرچنت كد پذيرنده صحيح نيست", + "-3" => "با توجه به محدوديت هاي شاپرك امكان پرداخت با رقم درخواست شده ميسر نمي باشد", + "-4" => "سطح تاييد پذيرنده پايين تر از سطح نقره اي است.", + "-11" => "درخواست مورد نظر يافت نشد.", + "-12" => "امكان ويرايش درخواست ميسر نمي باشد.", + "-21" => "هيچ نوع عمليات مالي براي اين تراكنش يافت نشد", + "-22" => "تراكنش نا موفق ميباشد", + "-33" => "رقم تراكنش با رقم پرداخت شده مطابقت ندارد", + "-34" => "سقف تقسيم تراكنش از لحاظ تعداد يا رقم عبور نموده است", + "-40" => "اجازه دسترسي به متد مربوطه وجود ندارد.", + "-41" => "اطلاعات ارسال شده مربوط به AdditionalData غيرمعتبر ميباشد.", + "-42" => "مدت زمان معتبر طول عمر شناسه پرداخت بايد بين 30 دقيه تا 45 روز مي باشد.", + "-54" => "درخواست مورد نظر آرشيو شده است", + "101" => "عمليات پرداخت موفق بوده و قبلا PaymentVerification تراكنش انجام شده است.", + ); + + $unknownError = 'خطای ناشناخته رخ داده است.'; + + return array_key_exists($status, $translations) ? $translations[$status] : $unknownError; + } + + /** + * Retrieve purchase url + * + * @return string + */ + protected function getPurchaseUrl() : string + { + return $this->settings->sandboxApiPurchaseUrl; + } + + /** + * Retrieve Payment url + * + * @return string + */ + protected function getPaymentUrl() : string + { + return $this->settings->sandboxApiPaymentUrl; + } + + /** + * Retrieve verification url + * + * @return string + */ + protected function getVerificationUrl() : string + { + return $this->settings->sandboxApiVerificationUrl; + } +} diff --git a/src/Drivers/Zarinpal/Strategies/Zaringate.php b/src/Drivers/Zarinpal/Strategies/Zaringate.php new file mode 100644 index 0000000..8ad2a46 --- /dev/null +++ b/src/Drivers/Zarinpal/Strategies/Zaringate.php @@ -0,0 +1,213 @@ +invoice($invoice); + $this->settings = (object) $settings; + } + + /** + * Purchase Invoice. + * + * @return string + * + * @throws PurchaseFailedException + * @throws \SoapFault + */ + public function purchase() + { + if (!empty($this->invoice->getDetails()['description'])) { + $description = $this->invoice->getDetails()['description']; + } else { + $description = $this->settings->description; + } + + if (!empty($this->invoice->getDetails()['mobile'])) { + $mobile = $this->invoice->getDetails()['mobile']; + } + + if (!empty($this->invoice->getDetails()['email'])) { + $email = $this->invoice->getDetails()['email']; + } + + $data = array( + 'MerchantID' => $this->settings->merchantId, + 'Amount' => $this->invoice->getAmount(), + 'CallbackURL' => $this->settings->callbackUrl, + 'Description' => $description, + 'Mobile' => $mobile ?? '', + 'Email' => $email ?? '', + 'AdditionalData' => $this->invoice->getDetails() + ); + + $client = new \SoapClient($this->getPurchaseUrl(), ['encoding' => 'UTF-8']); + $result = $client->PaymentRequest($data); + + if ($result->Status != 100 || empty($result->Authority)) { + // some error has happened + $message = $this->translateStatus($result->Status); + throw new PurchaseFailedException($message, $result->Status); + } + + $this->invoice->transactionId($result->Authority); + + // return the transaction's id + return $this->invoice->getTransactionId(); + } + + /** + * Pay the Invoice + * + * @return RedirectionForm + */ + public function pay() : RedirectionForm + { + $transactionId = $this->invoice->getTransactionId(); + $paymentUrl = $this->getPaymentUrl(); + + $payUrl = str_replace(':authority', $transactionId, $paymentUrl); + + return $this->redirectWithForm($payUrl, [], 'GET'); + } + + /** + * Verify payment + * + * @return ReceiptInterface + * + * @throws InvalidPaymentException + * @throws \SoapFault + */ + public function verify() : ReceiptInterface + { + $status = Request::input('Status'); + if ($status != 'OK') { + throw new InvalidPaymentException('عملیات پرداخت توسط کاربر لغو شد.', -22); + } + + $authority = $this->invoice->getTransactionId() ?? Request::input('Authority'); + $data = [ + 'MerchantID' => $this->settings->merchantId, + 'Authority' => $authority, + 'Amount' => $this->invoice->getAmount(), + ]; + + $client = new \SoapClient($this->getVerificationUrl(), ['encoding' => 'UTF-8']); + $result = $client->PaymentVerification($data); + + if ($result->Status != 100) { + $message = $this->translateStatus($result->Status); + throw new InvalidPaymentException($message, $result->Status); + } + + return $this->createReceipt($result->RefID); + } + + /** + * Generate the payment's receipt + * + * @param $referenceId + * + * @return Receipt + */ + public function createReceipt($referenceId) + { + return new Receipt('zarinpal', $referenceId); + } + + /** + * Convert status to a readable message. + * + * @param $status + * + * @return mixed|string + */ + private function translateStatus($status) + { + $translations = array( + "-1" => "اطلاعات ارسال شده ناقص است.", + "-2" => "IP و يا مرچنت كد پذيرنده صحيح نيست", + "-3" => "با توجه به محدوديت هاي شاپرك امكان پرداخت با رقم درخواست شده ميسر نمي باشد", + "-4" => "سطح تاييد پذيرنده پايين تر از سطح نقره اي است.", + "-11" => "درخواست مورد نظر يافت نشد.", + "-12" => "امكان ويرايش درخواست ميسر نمي باشد.", + "-21" => "هيچ نوع عمليات مالي براي اين تراكنش يافت نشد", + "-22" => "تراكنش نا موفق ميباشد", + "-33" => "رقم تراكنش با رقم پرداخت شده مطابقت ندارد", + "-34" => "سقف تقسيم تراكنش از لحاظ تعداد يا رقم عبور نموده است", + "-40" => "اجازه دسترسي به متد مربوطه وجود ندارد.", + "-41" => "اطلاعات ارسال شده مربوط به AdditionalData غيرمعتبر ميباشد.", + "-42" => "مدت زمان معتبر طول عمر شناسه پرداخت بايد بين 30 دقيه تا 45 روز مي باشد.", + "-54" => "درخواست مورد نظر آرشيو شده است", + "101" => "عمليات پرداخت موفق بوده و قبلا PaymentVerification تراكنش انجام شده است.", + ); + + $unknownError = 'خطای ناشناخته رخ داده است.'; + + return array_key_exists($status, $translations) ? $translations[$status] : $unknownError; + } + + /** + * Retrieve purchase url + * + * @return string + */ + protected function getPurchaseUrl() : string + { + return $this->settings->zaringateApiPurchaseUrl; + } + + /** + * Retrieve Payment url + * + * @return string + */ + protected function getPaymentUrl() : string + { + return $this->settings->zaringateApiPaymentUrl; + } + + /** + * Retrieve verification url + * + * @return string + */ + protected function getVerificationUrl() : string + { + return $this->settings->zaringateApiVerificationUrl; + } +} diff --git a/src/Drivers/Zarinpal/Zarinpal.php b/src/Drivers/Zarinpal/Zarinpal.php index 9746395..9c9c1ba 100644 --- a/src/Drivers/Zarinpal/Zarinpal.php +++ b/src/Drivers/Zarinpal/Zarinpal.php @@ -3,16 +3,37 @@ namespace Shetabit\Multipay\Drivers\Zarinpal; use Shetabit\Multipay\Abstracts\Driver; +use Shetabit\Multipay\Contracts\DriverInterface; use Shetabit\Multipay\Exceptions\InvalidPaymentException; use Shetabit\Multipay\Exceptions\PurchaseFailedException; use Shetabit\Multipay\Contracts\ReceiptInterface; +use Shetabit\Multipay\Drivers\Zarinpal\Strategies\Normal; +use Shetabit\Multipay\Drivers\Zarinpal\Strategies\Sandbox; +use Shetabit\Multipay\Drivers\Zarinpal\Strategies\Zaringate; +use Shetabit\Multipay\Exceptions\DriverNotFoundException; use Shetabit\Multipay\Invoice; -use Shetabit\Multipay\Receipt; use Shetabit\Multipay\RedirectionForm; -use Shetabit\Multipay\Request; class Zarinpal extends Driver { + /** + * Strategies map. + * + * @var array + */ + public static $strategies = [ + 'normal' => Normal::class, + 'sandbox' => Sandbox::class, + 'zaringate' => Zaringate::class, + ]; + + /** + * Current strategy instance. + * + * @var DriverInterface $strategy + */ + protected $strategy; + /** * Invoice * @@ -36,8 +57,9 @@ class Zarinpal extends Driver */ public function __construct(Invoice $invoice, $settings) { - $this->invoice($invoice); + $this->invoice = $invoice; $this->settings = (object) $settings; + $this->strategy = $this->getFreshStrategyInstance($this->invoice, $this->settings); } /** @@ -50,43 +72,7 @@ public function __construct(Invoice $invoice, $settings) */ public function purchase() { - if (!empty($this->invoice->getDetails()['description'])) { - $description = $this->invoice->getDetails()['description']; - } else { - $description = $this->settings->description; - } - - if (!empty($this->invoice->getDetails()['mobile'])) { - $mobile = $this->invoice->getDetails()['mobile']; - } - - if (!empty($this->invoice->getDetails()['email'])) { - $email = $this->invoice->getDetails()['email']; - } - - $data = array( - 'MerchantID' => $this->settings->merchantId, - 'Amount' => $this->invoice->getAmount(), - 'CallbackURL' => $this->settings->callbackUrl, - 'Description' => $description, - 'Mobile' => $mobile ?? '', - 'Email' => $email ?? '', - 'AdditionalData' => $this->invoice->getDetails() - ); - - $client = new \SoapClient($this->getPurchaseUrl(), ['encoding' => 'UTF-8']); - $result = $client->PaymentRequest($data); - - if ($result->Status != 100 || empty($result->Authority)) { - // some error has happened - $message = $this->translateStatus($result->Status); - throw new PurchaseFailedException($message, $result->Status); - } - - $this->invoice->transactionId($result->Authority); - - // return the transaction's id - return $this->invoice->getTransactionId(); + return $this->strategy->purchase(); } /** @@ -96,16 +82,7 @@ public function purchase() */ public function pay() : RedirectionForm { - $transactionId = $this->invoice->getTransactionId(); - $paymentUrl = $this->getPaymentUrl(); - - if (strtolower($this->getMode()) == 'zaringate') { - $payUrl = str_replace(':authority', $transactionId, $paymentUrl); - } else { - $payUrl = $paymentUrl.$transactionId; - } - - return $this->redirectWithForm($payUrl, [], 'GET'); + return $this->strategy->pay(); } /** @@ -118,144 +95,35 @@ public function pay() : RedirectionForm */ public function verify() : ReceiptInterface { - $authority = $this->invoice->getTransactionId() ?? Request::input('Authority'); - $status = Request::input('Status'); - - $data = [ - 'MerchantID' => $this->settings->merchantId, - 'Authority' => $authority, - 'Amount' => $this->invoice->getAmount(), - ]; - - if ($status != 'OK') { - throw new InvalidPaymentException('عملیات پرداخت توسط کاربر لغو شد.', -22); - } - - $client = new \SoapClient($this->getVerificationUrl(), ['encoding' => 'UTF-8']); - $result = $client->PaymentVerification($data); - - if ($result->Status != 100) { - $message = $this->translateStatus($result->Status); - throw new InvalidPaymentException($message, $result->Status); - } - - return $this->createReceipt($result->RefID); + return $this->strategy->verify(); } /** - * Generate the payment's receipt + * Get zarinpal payment's strategy according to config's mode. * - * @param $referenceId - * - * @return Receipt - */ - public function createReceipt($referenceId) - { - return new Receipt('zarinpal', $referenceId); - } - - /** - * Convert status to a readable message. - * - * @param $status - * - * @return mixed|string - */ - private function translateStatus($status) - { - $translations = array( - "-1" => "اطلاعات ارسال شده ناقص است.", - "-2" => "IP و يا مرچنت كد پذيرنده صحيح نيست", - "-3" => "با توجه به محدوديت هاي شاپرك امكان پرداخت با رقم درخواست شده ميسر نمي باشد", - "-4" => "سطح تاييد پذيرنده پايين تر از سطح نقره اي است.", - "-11" => "درخواست مورد نظر يافت نشد.", - "-12" => "امكان ويرايش درخواست ميسر نمي باشد.", - "-21" => "هيچ نوع عمليات مالي براي اين تراكنش يافت نشد", - "-22" => "تراكنش نا موفق ميباشد", - "-33" => "رقم تراكنش با رقم پرداخت شده مطابقت ندارد", - "-34" => "سقف تقسيم تراكنش از لحاظ تعداد يا رقم عبور نموده است", - "-40" => "اجازه دسترسي به متد مربوطه وجود ندارد.", - "-41" => "اطلاعات ارسال شده مربوط به AdditionalData غيرمعتبر ميباشد.", - "-42" => "مدت زمان معتبر طول عمر شناسه پرداخت بايد بين 30 دقيه تا 45 روز مي باشد.", - "-54" => "درخواست مورد نظر آرشيو شده است", - "101" => "عمليات پرداخت موفق بوده و قبلا PaymentVerification تراكنش انجام شده است.", - ); - - $unknownError = 'خطای ناشناخته رخ داده است.'; - - return array_key_exists($status, $translations) ? $translations[$status] : $unknownError; - } - - /** - * Retrieve purchase url - * - * @return string - */ - protected function getPurchaseUrl() : string - { - $mode = $this->getMode(); - - switch ($mode) { - case 'sandbox': - $url = $this->settings->sandboxApiPurchaseUrl; - break; - case 'zaringate': - $url = $this->settings->zaringateApiPurchaseUrl; - break; - default: // default: normal - $url = $this->settings->apiPurchaseUrl; - break; - } - - return $url; - } - - /** - * Retrieve Payment url - * - * @return string + * @param Invoice $invoice + * @param $settings + * @return DriverInterface */ - protected function getPaymentUrl() : string + protected function getFreshStrategyInstance($invoice, $settings) : DriverInterface { - $mode = $this->getMode(); + $strategy = static::$strategies[$this->getMode()] ?? null; - switch ($mode) { - case 'sandbox': - $url = $this->settings->sandboxApiPaymentUrl; - break; - case 'zaringate': - $url = $this->settings->zaringateApiPaymentUrl; - break; - default: // default: normal - $url = $this->settings->apiPaymentUrl; - break; + if (! $strategy) { + $this->strategyNotFound(); } - return $url; + return new $strategy($invoice, $settings); } - /** - * Retrieve verification url - * - * @return string - */ - protected function getVerificationUrl() : string + protected function strategyNotFound() { - $mode = $this->getMode(); - - switch ($mode) { - case 'sandbox': - $url = $this->settings->sandboxApiVerificationUrl; - break; - case 'zaringate': - $url = $this->settings->zaringateApiVerificationUrl; - break; - default: // default: normal - $url = $this->settings->apiVerificationUrl; - break; - } + $message = sprintf( + 'Zarinpal payment mode not found (check your settings), valid modes are: %s', + implode(',', array_keys(static::$strategies)) + ); - return $url; + throw new DriverNotFoundException($message); } /** diff --git a/src/Receipt.php b/src/Receipt.php index ba78ed5..1ca1a5b 100644 --- a/src/Receipt.php +++ b/src/Receipt.php @@ -27,6 +27,6 @@ public function __set($name, $value) */ public function __get($name) { - $this->getDetail($name); + return $this->getDetail($name); } } diff --git a/tests/Drivers/BarDriver.php b/tests/Drivers/BarDriver.php new file mode 100644 index 0000000..6270e2a --- /dev/null +++ b/tests/Drivers/BarDriver.php @@ -0,0 +1,44 @@ +invoice($invoice); + $this->settings=$settings; + } + + public function purchase() + { + return static::TRANSACTION_ID; + } + + public function pay(): RedirectionForm + { + return $this->redirectWithForm('/', [ + 'amount' => $this->invoice->getAmount() + ], 'GET'); + } + + public function verify(): ReceiptInterface + { + return new Receipt(static::DRIVER_NAME, static::REFERENCE_ID); + } +} diff --git a/tests/Mocks/MockPaymentManager.php b/tests/Mocks/MockPaymentManager.php new file mode 100644 index 0000000..8cae352 --- /dev/null +++ b/tests/Mocks/MockPaymentManager.php @@ -0,0 +1,33 @@ +driver; + } + + public function getConfig() : array + { + return $this->config; + } + + public function getCallbackUrl() : string + { + return $this->settings['callbackUrl']; + } + + public function getInvoice() + { + return $this->invoice; + } + + public function getCurrentDriverSetting() + { + return $this->settings; + } +} diff --git a/tests/PaymentTest.php b/tests/PaymentTest.php new file mode 100644 index 0000000..c411c3b --- /dev/null +++ b/tests/PaymentTest.php @@ -0,0 +1,139 @@ +config(); + $manager = $this->getManagerFreshInstance(); + + $this->assertEquals($config['default'], $manager->getDriver()); + } + + public function testItWontAcceptInvalidDriver() + { + $this->expectException(\Exception::class); + + $manager = $this->getManagerFreshInstance(); + $manager->via('none_existance_driver_name'); + } + + public function testConfigCanBeModified() + { + $manager = $this->getManagerFreshInstance(); + + $manager->config('foo', 'bar'); + + $config = $manager->getCurrentDriverSetting(); + + $this->assertArrayHasKey('foo', $config); + $this->assertSame('bar', $config['foo']); + } + + public function testCallbackUrlCanBeModified() + { + $manager = $this->getManagerFreshInstance(); + $manager->callbackUrl('/random_url'); + + $this->assertEquals('/random_url', $manager->getCallbackUrl()); + } + + public function testAmountCanBeSetted() + { + $amount = 10000; + $manager = $this->getManagerFreshInstance(); + $manager->amount($amount); + + $this->assertSame($amount, $manager->getInvoice()->getAmount()); + } + + public function testDeteilCanBeSetted() + { + $manager = $this->getManagerFreshInstance(); + + // array style + $manager->detail(['foo' => 'bar']); + + // normal style + $manager->detail('john', 'doe'); + + $invoice = $manager->getInvoice(); + + $this->assertEquals('bar', $invoice->getDetail('foo')); + $this->assertEquals('doe', $invoice->getDetail('john')); + } + + public function testDriverCanBeChanged() + { + $driverName = 'bar'; + $manager = $this->getManagerFreshInstance(); + $manager->via($driverName); + + $this->assertEquals($driverName, $manager->getDriver()); + } + + public function testPurchase() + { + $amount = 10000; + $manager = $this->getManagerFreshInstance(); + + $manager + ->via('bar') + ->amount($amount) + ->purchase(null, function ($driver, $transactionId) use ($amount) { + $this->assertEquals(BarDriver::TRANSACTION_ID, $transactionId); + $this->assertSame($amount, $driver->getInvoice()->getAmount()); + }); + } + + public function testCustomInvoiceCanBeUsedInPurchase() + { + $manager = $this->getManagerFreshInstance(); + + $invoice = new Invoice; + $invoice->amount(10000); + + $manager + ->via('bar') + ->purchase($invoice, function ($driver, $transactionId) use ($invoice) { + $this->assertEquals(BarDriver::TRANSACTION_ID, $transactionId); + $this->assertSame($invoice->getAmount(), $driver->getInvoice()->getAmount()); + }); + } + + public function testPay() + { + $amount = 10000; + $manager = $this->getManagerFreshInstance(); + + $redirectionForm = $manager->amount($amount)->via('bar')->purchase()->pay(); + $inputs = $redirectionForm->getInputs(); + + $this->assertInstanceOf(RedirectionForm::class, $redirectionForm); + $this->assertEquals($inputs['amount'], $amount); + } + + public function testVerify() + { + $amount = 10000; + $manager = $this->getManagerFreshInstance(); + + $receipt = $manager->amount($amount)->via('bar')->transactionId(BarDriver::TRANSACTION_ID)->verify(); + + $this->assertSame(BarDriver::REFERENCE_ID, $receipt->getReferenceId()); + $this->assertInstanceOf(Carbon::class, $receipt->getDate()); + } + + protected function getManagerFreshInstance() + { + return new MockPaymentManager($this->config()); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 4cdcd45..b0d23e3 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,29 +2,35 @@ namespace Shetabit\Multipay\Tests; -use Orchestra\Testbench\TestCase as BaseTestCase; -use Shetabit\Multipay\Tests\Mocks\Drivers\BarDriver; +use PHPUnit\Framework\TestCase as BaseTestCase; +use Shetabit\Multipay\Tests\Drivers\BarDriver; class TestCase extends BaseTestCase { - protected function getPackageProviders($app) + private $config = []; + + protected function setUp() : void { - return ['Shetabit\Multipay\Provider\PaymentServiceProvider']; + $this->environmentSetUp(); } - protected function getPackageAliases($app) + protected function config() : array { - return [ - 'Payment' => 'Shetabit\Multipay\Payment', - ]; + return $this->config; } - protected function getEnvironmentSetUp($app) + private function environmentSetUp() { - $settings = require __DIR__.'/../src/Config/payment.php'; - $settings['drivers']['bar'] = ['key' => 'foo']; - $settings['map']['bar'] = BarDriver::class; + $this->config = $this->loadConfig(); + + $this->config['map']['bar'] = BarDriver::class; + $this->config['drivers']['bar'] = [ + 'callback' => '/callback' + ]; + } - $app['config']->set('payment', $settings); + private function loadConfig() : array + { + return require(__DIR__.'/../config/payment.php'); } } diff --git a/tests/Traits/DriverCommon.php b/tests/Traits/DriverCommon.php new file mode 100644 index 0000000..88b63ac --- /dev/null +++ b/tests/Traits/DriverCommon.php @@ -0,0 +1,77 @@ +driverInstance = $this->getDriverInstance(); + $this->transactionId = 'biSBUv86G'; + } + + /** + * Test Purchase method. + * + * @throws \Exception + */ + public function testPurchase() + { + $amount = 10000; + $detailKey = 'foo'; + $detailValue = 'bar'; + + $this + ->driverInstance + ->detail($detailKey, $detailValue) + ->purchase($amount, function ($driver, $transactionId) { + $this->assertEquals($this->amount, $driver->getInvoice()->getAmount()); + $this->assertEquals([$this->detailKey => $this->detailValue], $driver->getInvoice()->getDetails()); + $this->assertEquals($this->transactionId, $transactionId); + }); + } + + /** + * Test pay method. + * + * @throws \Exception + */ + public function testPay() + { + $amount = 1000; + + $this + ->driverInstance + ->amount($amount) + ->purchase(); + + $this->assertInstanceOf(RedirectionForm::class, $this->driverInstance); + } + + /** + * Test Verify method + * + * @throws \Shetabit\Multipay\Exceptions\InvoiceNotFoundException + */ + public function testVerify() + { + $amount = 1000; + + $receipt = $this + ->driverInstance + ->amount($amount) + ->transactionId($this->transactionId) + ->verify(); + + $this->assertInstanceOf(Carbon::class, $receipt->getDate()); + $this->assertEquals("test", $receipt->getDriver()); + $this->assertEquals("122156415036", $receipt->getReferenceId()); + } +}