From 20ca1372173c7973341f2e197d1c47f333d1ce8b Mon Sep 17 00:00:00 2001 From: Alireza Sedghi Date: Thu, 23 Jan 2025 13:23:39 +0330 Subject: [PATCH] Added support for the latest version of Pasargad Bank API Removed unnecessary configurations. Removed RSA and RSAProcessor classes as they are no longer needed. Updated implementation based on the documentation for version 9 of Pasargad Bank's payment gateway (https://pep.co.ir/wp-content/uploads/2024/01/Parsa-IPG-2.pdf). --- config/payment.php | 11 +- src/Drivers/Pasargad/Pasargad.php | 214 +++++++++++++------- src/Drivers/Pasargad/Utils/RSA.php | 141 ------------- src/Drivers/Pasargad/Utils/RSAProcessor.php | 103 ---------- 4 files changed, 146 insertions(+), 323 deletions(-) delete mode 100644 src/Drivers/Pasargad/Utils/RSA.php delete mode 100644 src/Drivers/Pasargad/Utils/RSAProcessor.php diff --git a/config/payment.php b/config/payment.php index 800c1d3b..77f6466d 100755 --- a/config/payment.php +++ b/config/payment.php @@ -201,16 +201,13 @@ 'currency' => 'T', // Can be R, T (Rial, Toman) ], 'pasargad' => [ - 'apiPaymentUrl' => 'https://pep.shaparak.ir/payment.aspx', - 'apiGetToken' => 'https://pep.shaparak.ir/Api/v1/Payment/GetToken', - 'apiCheckTransactionUrl' => 'https://pep.shaparak.ir/Api/v1/Payment/CheckTransactionResult', - 'apiVerificationUrl' => 'https://pep.shaparak.ir/Api/v1/Payment/VerifyPayment', + 'baseUrl' => 'https://pep.shaparak.ir/dorsa1', + 'userName' => '', + 'password' => '', 'merchantId' => '', 'terminalCode' => '', - 'certificate' => '', // can be string (and set certificateType to xml_string) or an xml file path (and set cetificateType to xml_file) - 'certificateType' => 'xml_file', // can be: xml_file, xml_string 'callbackUrl' => 'http://yoursite.com/path/to', - 'currency' => 'R', //Can be R, T (Rial, Toman) + 'currency' => 'T', //Can be R, T (Rial, Toman) ], 'payir' => [ 'apiPurchaseUrl' => 'https://pay.ir/pg/send', diff --git a/src/Drivers/Pasargad/Pasargad.php b/src/Drivers/Pasargad/Pasargad.php index 0cc18d1d..bdde5c0f 100644 --- a/src/Drivers/Pasargad/Pasargad.php +++ b/src/Drivers/Pasargad/Pasargad.php @@ -3,12 +3,12 @@ namespace Shetabit\Multipay\Drivers\Pasargad; use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; use Shetabit\Multipay\Invoice; use Shetabit\Multipay\Receipt; use Shetabit\Multipay\Abstracts\Driver; use Shetabit\Multipay\Contracts\ReceiptInterface; use Shetabit\Multipay\Exceptions\InvalidPaymentException; -use Shetabit\Multipay\Drivers\Pasargad\Utils\RSAProcessor; use Shetabit\Multipay\RedirectionForm; use Shetabit\Multipay\Request; use DateTimeZone; @@ -19,9 +19,9 @@ class Pasargad extends Driver /** * Guzzle client * - * @var object + * @var Client */ - protected \GuzzleHttp\Client $client; + protected Client $client; /** * Invoice @@ -42,12 +42,13 @@ class Pasargad extends Driver * * @var array */ - protected $preparedData = []; + protected array $preparedData = array(); /** * Pasargad(PEP) constructor. * Construct the class with the relevant settings. * + * @param Invoice $invoice * @param $settings */ public function __construct(Invoice $invoice, $settings) @@ -61,12 +62,13 @@ public function __construct(Invoice $invoice, $settings) * Purchase Invoice. * * @return string + * @throws \DateMalformedStringException */ - public function purchase() + public function purchase(): string { $invoiceData = $this->getPreparedInvoiceData(); - $this->invoice->transactionId($invoiceData['InvoiceNumber']); + $this->invoice->transactionId($invoiceData['invoice']); // return the transaction's id return $this->invoice->getTransactionId(); @@ -74,47 +76,72 @@ public function purchase() /** * Pay the Invoice + * + * @return RedirectionForm + * @throws \DateMalformedStringException + * @throws InvalidPaymentException|GuzzleException */ public function pay() : RedirectionForm { - $paymentUrl = $this->settings->apiPaymentUrl; - $getTokenUrl = $this->settings->apiGetToken; - $tokenData = $this->request($getTokenUrl, $this->getPreparedInvoiceData()); + $baseUrl = $this->settings->baseUrl; + + $paymentResult = $this->request($baseUrl . '/api/payment/purchase', $this->getPreparedInvoiceData()); + + if ($paymentResult['resultCode'] !== 0 || empty($paymentResult['data']) || empty($paymentResult['data']['url'])) { + throw new InvalidPaymentException($result['resultMsg'] ?? $this->getDefaultExceptionMessage()); + } + + $paymentUrl = $paymentResult['data']['url']; - // redirect using HTML form - return $this->redirectWithForm($paymentUrl, $tokenData, 'POST'); + // redirect using the HTML form + return $this->redirectWithForm($paymentUrl); } /** * Verify payment * + * @return ReceiptInterface * * @throws InvalidPaymentException - * @throws \GuzzleHttp\Exception\GuzzleException + * @throws GuzzleException */ public function verify() : ReceiptInterface { - $invoiceDetails = $this->request( - $this->settings->apiCheckTransactionUrl, + $baseUrl = $this->settings->baseUrl; + + $invoiceInquiry = $this->request( + $baseUrl . '/api/payment/payment-inquiry', [ - 'TransactionReferenceID' => Request::input('tref') + 'invoiceId' => Request::input('invoiceId') ] ); + + if ($invoiceInquiry['resultCode'] !== 0 || empty($invoiceInquiry['data'])) { + throw new InvalidPaymentException($result['resultMsg'] ?? $this->getDefaultExceptionMessage()); + } + + $invoiceDetails = $invoiceInquiry['data']; + $invoiceInquiryStatus = $invoiceDetails['status']; + + $responseErrorStatusMessage = $this->getResponseErrorStatusMessage($invoiceInquiryStatus); + if (!empty($responseErrorStatusMessage)) { + throw new InvalidPaymentException($responseErrorStatusMessage); + } + $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial - if ($amount != $invoiceDetails['Amount']) { + if ($amount != $invoiceDetails['amount']) { throw new InvalidPaymentException('Invalid amount'); } - $iranTime = new DateTime('now', new DateTimeZone('Asia/Tehran')); - $fields = [ - 'MerchantCode' => $invoiceDetails['MerchantCode'], - 'TerminalCode' => $invoiceDetails['TerminalCode'], - 'InvoiceNumber' => $invoiceDetails['InvoiceNumber'], - 'InvoiceDate' => $invoiceDetails['InvoiceDate'], - 'Amount' => $invoiceDetails['Amount'], - 'Timestamp' => $iranTime->format("Y/m/d H:i:s"), - ]; - $verifyResult = $this->request($this->settings->apiVerificationUrl, $fields); + $paymentUrlId = $invoiceDetails['url']; + + $verifyResult = $this->request( + $baseUrl . '/api/payment/confirm-transactions', + [ + 'invoice' => Request::input('invoiceId'), + 'urlId' => $paymentUrlId + ] + ); return $this->createReceipt($verifyResult, $invoiceDetails); } @@ -122,57 +149,65 @@ public function verify() : ReceiptInterface /** * Generate the payment's receipt * - * @param $referenceId + * @param $verifyResult + * @param $invoiceDetails + * @return Receipt */ - protected function createReceipt(array $verifyResult, array $invoiceDetails): \Shetabit\Multipay\Receipt + protected function createReceipt($verifyResult, $invoiceDetails): Receipt { - $referenceId = $invoiceDetails['TransactionReferenceID']; - $traceNumber = $invoiceDetails['TraceNumber']; - $referenceNumber = $invoiceDetails['ReferenceNumber']; + $verifyResultData = $verifyResult['data']; + $referenceId = $invoiceDetails['transactionId']; + $trackId = $invoiceDetails['trackId']; + $referenceNumber = $invoiceDetails['referenceNumber']; - $reciept = new Receipt('Pasargad', $referenceId); + $receipt = new Receipt('Pasargad', $referenceId); - $reciept->detail('TraceNumber', $traceNumber); - $reciept->detail('ReferenceNumber', $referenceNumber); - $reciept->detail('MaskedCardNumber', $verifyResult['MaskedCardNumber']); - $reciept->detail('ShaparakRefNumber', $verifyResult['ShaparakRefNumber']); + $receipt->detail('TraceNumber', $trackId); + $receipt->detail('ReferenceNumber', $referenceNumber); + $receipt->detail('urlId', $invoiceDetails['url']); + $receipt->detail('MaskedCardNumber', $verifyResultData['maskedCardNumber']); - return $reciept; + return $receipt; } /** * A default message for exceptions + * + * @return string */ protected function getDefaultExceptionMessage(): string { - return 'مشکلی در دریافت اطلاعات از بانک به وجود آمده است'; + return 'مشکلی در دریافت اطلاعات از بانک به وجود آمده‌است.'; } /** - * Sign given data. - * - * @param string $data + * Return response status message * + * @param $status * @return string */ - public function sign($data) + protected function getResponseErrorStatusMessage($status): string { - $certificate = $this->settings->certificate; - $certificateType = $this->settings->certificateType; - - $processor = new RSAProcessor($certificate, $certificateType); - - return $processor->sign($data); + return match ($status) { + 13005 => 'عدم امکان دسترسی موقت به سرور پرداخت بانک', + 13016, 13018 => 'تراکنشی یافت نشد!', + 13021 => 'پرداخت از پیش لغو شده است!', + 13022 => 'تراکنش پرداخت نشده‌است!', + 13025 => 'تراکنش ناموفق است!', + 13030 => 'تراکنش تایید شده و ناموفق است!', + default => '' + }; } /** * Retrieve prepared invoice's data * * @return array + * @throws \DateMalformedStringException */ - protected function getPreparedInvoiceData() + protected function getPreparedInvoiceData(): array { - if ($this->preparedData === []) { + if (empty($this->preparedData)) { $this->preparedData = $this->prepareInvoiceData(); } @@ -181,18 +216,19 @@ protected function getPreparedInvoiceData() /** * Prepare invoice data + * + * @return array + * @throws \DateMalformedStringException */ protected function prepareInvoiceData(): array { - $action = 1003; // 1003 : for buy request (bank standard) - $merchantCode = $this->settings->merchantId; + $serviceCode = 8; // 8 : for PURCHASE request $terminalCode = $this->settings->terminalCode; $amount = $this->invoice->getAmount() * ($this->settings->currency == 'T' ? 10 : 1); // convert to rial $redirectAddress = $this->settings->callbackUrl; - $invoiceNumber = crc32($this->invoice->getUuid()) . random_int(0, time()); + $invoiceNumber = crc32($this->invoice->getUuid()) . rand(0, time()); $iranTime = new DateTime('now', new DateTimeZone('Asia/Tehran')); - $timeStamp = $iranTime->format("Y/m/d H:i:s"); $invoiceDate = $iranTime->format("Y/m/d H:i:s"); if (!empty($this->invoice->getDetails()['date'])) { @@ -200,34 +236,63 @@ protected function prepareInvoiceData(): array } return [ - 'InvoiceNumber' => $invoiceNumber, - 'InvoiceDate' => $invoiceDate, - 'Amount' => $amount, - 'TerminalCode' => $terminalCode, - 'MerchantCode' => $merchantCode, - 'RedirectAddress' => $redirectAddress, - 'Timestamp' => $timeStamp, - 'Action' => $action, + 'invoice' => $invoiceNumber, + 'invoiceDate' => $invoiceDate, + 'amount' => $amount, + 'serviceCode' => $serviceCode, + 'serviceType' => 'PURCHASE', + 'terminalNumber' => $terminalCode, + 'callbackApi' => $redirectAddress, ]; } /** - * Prepare signature based on Pasargad document + * Get action token + * + * @throws InvalidPaymentException + * @throws GuzzleException */ - public function prepareSignature(string $data): string + protected function getToken() { - return base64_encode($this->sign(sha1($data, true))); + $baseUrl = $this->settings->baseUrl; + $userName = $this->settings->userName; + $password = $this->settings->password; + + $response = $this->client->request( + 'POST', + $baseUrl . '/token/getToken', + [ + 'body' => json_encode(['username' => $userName, 'password' => $password]), + 'headers' => [ + 'content-type' => 'application/json' + ], + 'http_errors' => false, + ] + ); + + $result = json_decode($response->getBody(), true); + + if ($result['resultCode'] !== 0 || empty($result['token'])) { + throw new InvalidPaymentException($result['resultMsg'] ?? 'Invalid Authentication'); + } + + return $result['token']; } /** - * Make request to pasargad's Api + * Make request to Pasargad's Api * + * @param string $url + * @param array $body * @param string $method + * @return array + * @throws InvalidPaymentException + * @throws GuzzleException */ - protected function request(string $url, array $body, $method = 'POST'): array + protected function request(string $url, array $body, string $method = 'POST'): array { $body = json_encode($body); - $sign = $this->prepareSignature($body); + $token = $this->getToken(); $response = $this->client->request( 'POST', @@ -236,16 +301,21 @@ protected function request(string $url, array $body, $method = 'POST'): array 'body' => $body, 'headers' => [ 'content-type' => 'application/json', - 'Sign' => $sign + 'Authorization' => "Bearer {$token}" ], - "http_errors" => false, + 'http_errors' => false, ] ); $result = json_decode($response->getBody(), true); - if ($result['IsSuccess'] === false) { - throw new InvalidPaymentException($result['Message']); + if ($result['resultCode'] !== 0) { + $responseStatusMessage = $this->getResponseErrorStatusMessage($result['resultCode']); + throw new InvalidPaymentException( + !empty($responseStatusMessage) + ? $responseStatusMessage + : ($result['resultMsg'] ?? $result['resultCode']) + ); } return $result; diff --git a/src/Drivers/Pasargad/Utils/RSA.php b/src/Drivers/Pasargad/Utils/RSA.php deleted file mode 100644 index e76fca06..00000000 --- a/src/Drivers/Pasargad/Utils/RSA.php +++ /dev/null @@ -1,141 +0,0 @@ -= 0; $i--) { - $digit = ord($data[$i]); - $part_res = bcmul($digit, $radix); - $result = bcadd($result, $part_res); - $radix = bcmul($radix, $base); - } - return $result; - } - - public static function numberToBinary($number, $blocksize): string - { - $base = "256"; - $result = ""; - $div = $number; - while ($div > 0) { - $mod = bcmod($div, $base); - $div = bcdiv($div, $base); - $result = chr($mod) . $result; - } - return str_pad($result, $blocksize, "\x00", STR_PAD_LEFT); - } -} diff --git a/src/Drivers/Pasargad/Utils/RSAProcessor.php b/src/Drivers/Pasargad/Utils/RSAProcessor.php deleted file mode 100644 index 7fd973d3..00000000 --- a/src/Drivers/Pasargad/Utils/RSAProcessor.php +++ /dev/null @@ -1,103 +0,0 @@ -modulus = RSA::binaryToNumber(base64_decode($xmlObject->Modulus)); - $this->publicKey = RSA::binaryToNumber(base64_decode($xmlObject->Exponent)); - $this->privateKey = RSA::binaryToNumber(base64_decode($xmlObject->D)); - $this->keyLength = strlen(base64_decode($xmlObject->Modulus)) * 8; - } - - /** - * Retrieve public key - */ - public function getPublicKey(): string - { - return $this->publicKey; - } - - /** - * Retrieve private key - */ - public function getPrivateKey(): string - { - return $this->privateKey; - } - - /** - * Retrieve key length - */ - public function getKeyLength(): int - { - return $this->keyLength; - } - - /** - * Retrieve modulus - */ - public function getModulus(): string - { - return $this->modulus; - } - - /** - * Encrypt given data - */ - public function encrypt(string $data): string - { - return base64_encode(RSA::rsaEncrypt($data, $this->publicKey, $this->modulus, $this->keyLength)); - } - - /** - * Decrypt given data - * - * @param $data - */ - public function decrypt($data): string - { - return RSA::rsaDecrypt($data, $this->privateKey, $this->modulus, $this->keyLength); - } - - /** - * Sign given data - * - * - */ - public function sign(string $data): string - { - return RSA::rsaSign($data, $this->privateKey, $this->modulus, $this->keyLength); - } - - /** - * Verify RSA data - * - * @param string $data - */ - public function verify($data): string - { - return RSA::rsaVerify($data, $this->publicKey, $this->modulus, $this->keyLength); - } -}