diff --git a/README.md b/README.md index 65b4d28..da584e1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ -# TendoPay SDK for PHP +# TendoPay SDK for PHP (v2) + +If you find a document for v1, please go to [TendoPay SDK for PHP (v1)](./README_v1.md) ## Requirements PHP 7.0 and later. +## Upgrade + +[UPGRADE from v1](./UPGRADE.md) + ## Installation ### Using Composer @@ -34,22 +40,15 @@ http://localhost:8000/ > MERCHANT_ID,MERCHANT_SECRET for test can get them at [TendoPay Sandbox](https://sandbox.tendopay.ph) ```bash -## Merchant Credentials -MERCHANT_ID= -MERCHANT_SECRET= - ## Client Credentials CLIENT_ID= CLIENT_SECRET= -## Enable Sandbox, it must be false in production -TENDOPAY_SANDBOX_ENABLED=false - ## Redirect URI when the transaction succeed REDIRECT_URL=https://localhost:8000/purhase.php -## Redirect URI when the transaction fails -ERROR_REDIRECT_URL=https://localhost:8000/purchase.php +## Enable Sandbox, it must be false in production +TENDOPAY_SANDBOX_ENABLED=false ``` ```php @@ -64,12 +63,9 @@ $client = new TendoPayClient(); use TendoPay\SDK\TendoPayClient; $config = [ - 'MERCHANT_ID' => '', - 'MERCHANT_SECRET' => '', 'CLIENT_ID' => '', 'CLIENT_SECRET' => '', 'REDIRECT_URL' => '', - 'ERROR_REDIRECT_URL' => '', 'TENDOPAY_SANDBOX_ENABLED' => false, ]; $client = new TendoPayClient($config); @@ -81,13 +77,13 @@ $client = new TendoPayClient($config); ```php use TendoPay\SDK\Exception\TendoPayConnectionException; use TendoPay\SDK\Models\Payment; -use TendoPay\SDK\TendoPayClient; +use TendoPay\SDK\V2\TendoPayClient; ### S:Merchant set proper values $merchant_order_id = $_POST['tp_merchant_order_id']; $request_order_amount = $_POST['tp_amount']; $request_order_title = $_POST['tp_description']; -$_SESSION['merchant_order_id'] = $merchant_order_id; +$redirectUrl = $_POST['tp_redirect_url'] ?? ''; ### E:Merchant set proper values $client = new TendoPayClient(); @@ -96,7 +92,10 @@ try { $payment = new Payment(); $payment->setMerchantOrderId($merchant_order_id) ->setDescription($request_order_title) - ->setRequestAmount($request_order_amount); + ->setRequestAmount($request_order_amount) + ->setCurrency('PHP') + ->setRedirectUrl($redirectUrl); + $client->setPayment($payment); @@ -114,21 +113,27 @@ try { ```php use TendoPay\SDK\Exception\TendoPayConnectionException; use TendoPay\SDK\Models\VerifyTransactionRequest; -use TendoPay\SDK\TendoPayClient; +use TendoPay\SDK\V2\TendoPayClient; $client = new TendoPayClient(); try { if (TendoPayClient::isCallBackRequest($_REQUEST)) { - $merchant_order_id = $_SESSION['merchant_order_id'] ?? null; - $transaction = $client->verifyTransaction($merchant_order_id, new VerifyTransactionRequest($_REQUEST)); + $transaction = $client->verifyTransaction(new VerifyTransactionRequest($_REQUEST)); if (!$transaction->isVerified()) { throw new UnexpectedValueException('Invalid signature for the verification'); } - // Save $transactionNumber here - // Proceed merchant post order process + if ($transaction->getStatus() == \TendoPay\SDK\V2\ConstantsV2::STATUS_SUCCESS) { + // PAID + // Save $transactionNumber here + // Proceed merchant post order process + } else if ($transaction->getStatus() == \TendoPay\SDK\V2\ConstantsV2::STATUS_FAILURE) { + // FAILED + // do something in failure case + // error message $transaction->getMessage() + } } } catch (TendoPayConnectionException $e) { echo 'Connection Error:'.$e->getMessage(); @@ -141,7 +146,7 @@ try { ```php use TendoPay\SDK\Exception\TendoPayConnectionException; -use TendoPay\SDK\TendoPayClient; +use TendoPay\SDK\V2\TendoPayClient; $client = new TendoPayClient(); @@ -161,7 +166,7 @@ try { ```php use TendoPay\SDK\Exception\TendoPayConnectionException; -use TendoPay\SDK\TendoPayClient; +use TendoPay\SDK\V2\TendoPayClient; $client = new TendoPayClient(); diff --git a/README_v1.md b/README_v1.md new file mode 100644 index 0000000..65b4d28 --- /dev/null +++ b/README_v1.md @@ -0,0 +1,185 @@ +# TendoPay SDK for PHP + +## Requirements + +PHP 7.0 and later. + +## Installation + +### Using Composer + +You can install the sdk via [Composer](http://getcomposer.org/). Run the following command: + +```bash +composer require tendopay/tendopay-sdk-php +``` + +## Run SDK Tester + +- Run a sample server +```bash +php -s localhost:8000 -t vendor/tendopay/tendopay-sdk-php/samples +``` + +- Open browser and goto +```bash +http://localhost:8000/ +``` + +## Code Examples + +### Create TendoPayClient + +- Using .env + > MERCHANT_ID,MERCHANT_SECRET for test can get them at [TendoPay Sandbox](https://sandbox.tendopay.ph) + +```bash +## Merchant Credentials +MERCHANT_ID= +MERCHANT_SECRET= + +## Client Credentials +CLIENT_ID= +CLIENT_SECRET= + +## Enable Sandbox, it must be false in production +TENDOPAY_SANDBOX_ENABLED=false + +## Redirect URI when the transaction succeed +REDIRECT_URL=https://localhost:8000/purhase.php + +## Redirect URI when the transaction fails +ERROR_REDIRECT_URL=https://localhost:8000/purchase.php +``` + +```php +use TendoPay\SDK\TendoPayClient; + +$client = new TendoPayClient(); +``` + +- Using $config variable + +```php +use TendoPay\SDK\TendoPayClient; + +$config = [ + 'MERCHANT_ID' => '', + 'MERCHANT_SECRET' => '', + 'CLIENT_ID' => '', + 'CLIENT_SECRET' => '', + 'REDIRECT_URL' => '', + 'ERROR_REDIRECT_URL' => '', + 'TENDOPAY_SANDBOX_ENABLED' => false, +]; +$client = new TendoPayClient($config); +``` + + +### Make Payment + +```php +use TendoPay\SDK\Exception\TendoPayConnectionException; +use TendoPay\SDK\Models\Payment; +use TendoPay\SDK\TendoPayClient; + +### S:Merchant set proper values +$merchant_order_id = $_POST['tp_merchant_order_id']; +$request_order_amount = $_POST['tp_amount']; +$request_order_title = $_POST['tp_description']; +$_SESSION['merchant_order_id'] = $merchant_order_id; +### E:Merchant set proper values + +$client = new TendoPayClient(); + +try { + $payment = new Payment(); + $payment->setMerchantOrderId($merchant_order_id) + ->setDescription($request_order_title) + ->setRequestAmount($request_order_amount); + + $client->setPayment($payment); + + $redirectURL = $client->getAuthorizeLink(); + header('Location: '.$redirectURL); +} catch (TendoPayConnectionException $e) { + echo 'Connection Error:'.$e->getMessage(); +} catch (Exception $e) { + echo 'Runtime Error:'.$e->getMessage(); +} +``` + +### Callback (redirected page) + +```php +use TendoPay\SDK\Exception\TendoPayConnectionException; +use TendoPay\SDK\Models\VerifyTransactionRequest; +use TendoPay\SDK\TendoPayClient; + +$client = new TendoPayClient(); + +try { + if (TendoPayClient::isCallBackRequest($_REQUEST)) { + $merchant_order_id = $_SESSION['merchant_order_id'] ?? null; + $transaction = $client->verifyTransaction($merchant_order_id, new VerifyTransactionRequest($_REQUEST)); + + if (!$transaction->isVerified()) { + throw new UnexpectedValueException('Invalid signature for the verification'); + } + + // Save $transactionNumber here + // Proceed merchant post order process + } +} catch (TendoPayConnectionException $e) { + echo 'Connection Error:'.$e->getMessage(); +} catch (Exception $e) { + echo 'Runtime Error:'.$e->getMessage(); +} +``` + +### Cancel Payment + +```php +use TendoPay\SDK\Exception\TendoPayConnectionException; +use TendoPay\SDK\TendoPayClient; + +$client = new TendoPayClient(); + +try { + $client->cancelPayment($transactionNumber); + // merchant process here + +} catch (TendoPayConnectionException $e) { + echo 'Connection Error:'.$e->getMessage(); +} catch (Exception $e) { + echo 'Runtime Error:'.$e->getMessage(); +} +``` + + +### Show Transaction Detail + +```php +use TendoPay\SDK\Exception\TendoPayConnectionException; +use TendoPay\SDK\TendoPayClient; + +$client = new TendoPayClient(); + +try { + + $transaction = $client->getTransactionDetail($transactionNumber); + + // merchant process here + // $transaction->getMerchantId(); + // $transaction->getMerchantOrderId(); + // $transaction->getAmount(); + // $transaction->getTransactionNumber(); + // $transaction->getCreatedAt(); + // $transaction->getStatus(); + +} catch (TendoPayConnectionException $e) { + echo 'Connection Error:'.$e->getMessage(); +} catch (Exception $e) { + echo 'Runtime Error:'.$e->getMessage(); +} +``` diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..a707ea5 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,31 @@ +# Upgrade Guide + +## # Upgrading to v2 from 0.8.x + +### Changes + +##### New Client ID/ Client Secret + Merchant should create a new Client App(REST V2) in the Merchant Dashboard to use SDK 2.x + Existing credentils is not compatible with SDK 2.x + +##### MERCHANT_ID/MERCHANT_SECRET + MERCHANT_ID/MERCHANT_SECRET has been removed + +##### ERROR_REDIRECT_URL + ERROR_REDIRECT_URL has been removed. + All successful or unsuccessful response will be redirected to REDIRECT_URL + +##### Backend Notification + When a merchant creates an app (REST V2), + if 'NOTIFICATION_URL' is set, TendoPay notifies some changes of transactions to the notification callback URL. + This callback is asynchronous back-end API request. + 'PAID', 'FAILED', 'CANCELLED' events of transactions are triggered. + +##### Change the namespace of TendoPayClient +```php + # TendoPayClient for v1 + use TendoPay\SDK\TendoPayClient; + + # TendoPayClient for v2 + use TendoPay\SDK\V2\TendoPayClient; +```` diff --git a/samples/api.php b/samples/api.php index 933e957..7a67692 100644 --- a/samples/api.php +++ b/samples/api.php @@ -2,20 +2,8 @@ require_once __DIR__.'/common.php'; -use TendoPay\SDK\TendoPayClient; global $config; -/** - * - * @param mixed $response - * @param int $status - */ -function json($response = [], $status = 200) -{ - echo header('Content-Type: application/json', true, $status); - echo json_encode($response); -} - ## Main $request = json_decode(file_get_contents('php://input'), false); $job = $request->job ?? null; @@ -31,13 +19,17 @@ function json($response = [], $status = 200) case 'GET_TRANSACTIONS': return json($_SESSION['transactions'] ?? []); case 'GET_TRANSACTION': - $client = new TendoPayClient($config); + $client = ($config['TP_SDK_VERSION'] ?? null) === 'v2' ? + new \TendoPay\SDK\V2\TendoPayClient($config) : + new \TendoPay\SDK\TendoPayClient($config); $response = $client->getTransactionDetail($request->transactionNumber); return json($response->toArray()); case 'CANCEL_TRANSACTION': $transactionNumber = $request->transactionNumber; - $client = new TendoPayClient($config); + $client = ($config['TP_SDK_VERSION'] ?? null) === 'v2' ? + new \TendoPay\SDK\V2\TendoPayClient($config) : + new \TendoPay\SDK\TendoPayClient($config); $client->cancelPayment($transactionNumber); $_SESSION['transactions'] = array_filter($_SESSION['transactions'] ?? [], diff --git a/samples/callback.php b/samples/callback.php index 98ce8fa..7cb4f51 100644 --- a/samples/callback.php +++ b/samples/callback.php @@ -4,13 +4,32 @@ use TendoPay\SDK\Exception\TendoPayConnectionException; use TendoPay\SDK\Models\VerifyTransactionRequest; -use TendoPay\SDK\TendoPayClient; global $config; -$client = new TendoPayClient($config); +//echo "
";print_r($_REQUEST);echo "";exit; +$client = $config['TP_SDK_VERSION'] === 'v2' ? + new \TendoPay\SDK\V2\TendoPayClient($config) : + new \TendoPay\SDK\TendoPayClient($config); try { - if (TendoPayClient::isCallBackRequest($_REQUEST)) { + if ($config['TP_SDK_VERSION'] === 'v2' && + \TendoPay\SDK\V2\TendoPayClient::isCallBackRequest($_REQUEST)) { + + $transaction = $client->verifyTransaction(new VerifyTransactionRequest($_REQUEST)); + + if (!$transaction->isVerified()) { + throw new UnexpectedValueException('Invalid signature for the verification'); + } + +// dump('verificationResult:', $transaction->toArray());exit; + // @Note check request amount with approved amount. it can be different but must be enough to purchase + // Save $transactionNumber here + $transactions = $_SESSION['transactions'] ?? []; + $transactions[] = $transaction->toArray(); + $_SESSION['transactions'] = $transactions; + header('Location: /'); + + } elseif (\TendoPay\SDK\TendoPayClient::isCallBackRequest($_REQUEST)) { $merchant_order_id = $_SESSION['merchant_order_id'] ?? null; $transaction = $client->verifyTransaction($merchant_order_id, new VerifyTransactionRequest($_REQUEST)); @@ -18,7 +37,7 @@ throw new UnexpectedValueException('Invalid signature for the verification'); } -// dump('verificationResult:', $transaction->toArray()); +// dump('verificationResult:', $transaction->toArray());exit; // @Note check request amount with approved amount. it can be different but must be enough to purchase // Save $transactionNumber here $transactions = $_SESSION['transactions'] ?? []; diff --git a/samples/common.php b/samples/common.php index 6dabd8f..0624ac6 100644 --- a/samples/common.php +++ b/samples/common.php @@ -25,6 +25,17 @@ function dump() echo ''; } +/** + * + * @param mixed $response + * @param int $status + */ +function json($response = [], $status = 200) +{ + echo header('Content-Type: application/json', true, $status); + echo json_encode($response); +} + $credentials = $_SESSION['credentials'] ?? null; //putenv('MERCHANT_ID='.$credentials->merchant_id); //putenv('MERCHANT_SECRET='.$credentials->merchant_secret); @@ -35,6 +46,7 @@ function dump() //putenv('TENDOPAY_SANDBOX_ENABLED=true'); $config = [ + 'TP_SDK_VERSION' => $credentials->tp_sdk_version ?? '', 'MERCHANT_ID' => $credentials->merchant_id ?? '', 'MERCHANT_SECRET' => $credentials->merchant_secret ?? '', 'CLIENT_ID' => $credentials->client_id ?? '', diff --git a/samples/index.html.php b/samples/index.html.php index 5019ba4..1d49986 100644 --- a/samples/index.html.php +++ b/samples/index.html.php @@ -39,10 +39,10 @@
-
-
{{transaction}}+
{{ transaction }}
";print_r($config);echo "";exit; +//echo "
";print_r($_REQUEST);echo "";exit; + +$client = $config['TP_SDK_VERSION'] === 'v2' ? + new \TendoPay\SDK\V2\TendoPayClient($config) : + new \TendoPay\SDK\TendoPayClient($config); try { $payment = new Payment(); @@ -22,10 +27,15 @@ ->setDescription($request_order_title) ->setRequestAmount($request_order_amount); + if ($redirectUrl) { + $payment->setCurrency('PHP') + ->setRedirectUrl($redirectUrl); + } + $client->setPayment($payment); - $redirectURL = $client->getAuthorizeLink(); - header('Location: '.$redirectURL); + $authUrl = $client->getAuthorizeLink(); + header('Location: '.$authUrl); } catch (TendoPayConnectionException $e) { dump('Connection Error:'.$e->getMessage()); } catch (Exception $e) { diff --git a/src/Models/Payment.php b/src/Models/Payment.php index 6067ad8..480ff1e 100644 --- a/src/Models/Payment.php +++ b/src/Models/Payment.php @@ -30,15 +30,36 @@ class Payment */ private $items; + + /** + * Redirect Url + * @var + */ + private $redirectUrl; + + /** + * Currency + * @var + */ + private $currency; + /** * TendoPayOrder constructor. * @param array $params */ public function __construct(array $params = []) { - $this->merchantOrderId = $params['merchant_order_id'] ?? null; - $this->description = $params['description'] ?? ''; - $this->requestAmount = $params['request_amount'] ?? 0; + if ($params['tp_amount'] ?? null) { + $this->requestAmount = $params['tp_amount'] ?? 0; + $this->currency = $params['tp_currency'] ?? 'PHP'; + $this->merchantOrderId = $params['tp_merchant_order_id'] ?? null; + $this->description = $params['tp_description'] ?? ''; + $this->redirectUrl = $params['tp_redirect_url'] ?? ''; + } else { + $this->merchantOrderId = $params['merchant_order_id'] ?? null; + $this->description = $params['description'] ?? ''; + $this->requestAmount = $params['request_amount'] ?? 0; + } } /** @@ -113,4 +134,40 @@ public function setItems(array $items): self $this->items = $items; return $this; } + + /** + * @param string $currency + * @return Payment + */ + public function setCurrency(string $currency): self + { + $this->currency = $currency; + return $this; + } + + /** + * @return mixed|string + */ + public function getCurrency() + { + return $this->currency; + } + + /** + * @param string $redirectUrl + * @return Payment + */ + public function setRedirectUrl(string $redirectUrl): self + { + $this->redirectUrl = $redirectUrl; + return $this; + } + + /** + * @return mixed|string + */ + public function getRedirectUrl() + { + return $this->redirectUrl; + } } diff --git a/src/Models/Transaction.php b/src/Models/Transaction.php index bca0054..a1c7081 100644 --- a/src/Models/Transaction.php +++ b/src/Models/Transaction.php @@ -5,6 +5,7 @@ use TendoPay\SDK\Constants; use TendoPay\SDK\Exception\TendoPayParameterException; +use TendoPay\SDK\V2\ConstantsV2; /** * Class Transaction @@ -36,18 +37,34 @@ class Transaction */ public function __construct(array $response = []) { - $this->merchantId = $response[Constants::MERCHANT_ID] ?? null; - $this->merchantOrderId = $response[Constants::MERCHANT_ORDER_ID] ?? null; - $this->amount = $response[Constants::AMOUNT] ?? null; - $this->transactionNumber = $response[Constants::TRANSACTION_NO_PARAM] ?? null; - $this->status = $response[Constants::TRANSACTION_STATUS] ?? null; - $this->createdAt = $response[Constants::CREATED_AT] ?? null; - - if (!$this->transactionNumber || !$this->merchantOrderId) { - throw new TendoPayParameterException(sprintf('%s, %s cannot be null', - Constants::TRANSACTION_NO_PARAM, - Constants::MERCHANT_ORDER_ID - )); + if ($response[ConstantsV2::TRANSACTION_NO_PARAM] ?? null) { + $this->merchantId = $response[ConstantsV2::MERCHANT_ID] ?? null; + $this->merchantOrderId = $response[ConstantsV2::MERCHANT_ORDER_ID] ?? null; + $this->amount = $response[ConstantsV2::AMOUNT] ?? null; + $this->transactionNumber = $response[ConstantsV2::TRANSACTION_NO_PARAM] ?? null; + $this->status = $response[ConstantsV2::TRANSACTION_STATUS] ?? null; + $this->createdAt = $response[ConstantsV2::CREATED_AT] ?? null; + + if (!$this->transactionNumber || !$this->merchantOrderId) { + throw new TendoPayParameterException(sprintf('%s, %s cannot be null', + ConstantsV2::TRANSACTION_NO_PARAM, + ConstantsV2::MERCHANT_ORDER_ID + )); + } + } else { + $this->merchantId = $response[Constants::MERCHANT_ID] ?? null; + $this->merchantOrderId = $response[Constants::MERCHANT_ORDER_ID] ?? null; + $this->amount = $response[Constants::AMOUNT] ?? null; + $this->transactionNumber = $response[Constants::TRANSACTION_NO_PARAM] ?? null; + $this->status = $response[Constants::TRANSACTION_STATUS] ?? null; + $this->createdAt = $response[Constants::CREATED_AT] ?? null; + + if (!$this->transactionNumber || !$this->merchantOrderId) { + throw new TendoPayParameterException(sprintf('%s, %s cannot be null', + Constants::TRANSACTION_NO_PARAM, + Constants::MERCHANT_ORDER_ID + )); + } } } diff --git a/src/Models/VerifyTransactionRequest.php b/src/Models/VerifyTransactionRequest.php index 316fb49..b8f1765 100644 --- a/src/Models/VerifyTransactionRequest.php +++ b/src/Models/VerifyTransactionRequest.php @@ -3,6 +3,7 @@ namespace TendoPay\SDK\Models; use TendoPay\SDK\Constants; +use TendoPay\SDK\V2\ConstantsV2; class VerifyTransactionRequest { @@ -18,6 +19,8 @@ class VerifyTransactionRequest private $hash; + private $request; + /** * TendoPayCallbackRequest constructor. * @param array $request @@ -33,12 +36,27 @@ class VerifyTransactionRequest */ public function __construct(array $request) { - $this->disposition = $request[Constants::DISPOSITION_PARAM] ?? Constants::STATUS_FAILURE; - $this->transactionNumber = $request[Constants::TRANSACTION_NO_PARAM] ?? ''; - $this->verificationToken = $request[Constants::VERIFICATION_TOKEN_PARAM] ?? ''; - $this->merchantOrderId = $request[Constants::ORDER_ID_PARAM] ?? ''; - $this->userId = $request[Constants::USER_ID_PARAM] ?? ''; - $this->hash = $request[Constants::HASH_PARAM] ?? ''; + if ($request['tp_transaction_id'] ?? null) { + //[tp_transaction_status] => FAILED + //[tp_transaction_id] => 20412 + //[tp_merchant_order_id] => TEST-ORD-1603907027113 + //[tp_message] => Your available TendoPay credit is ₱1. You do not have the sufficient credits for this purchase. Please make a purchase below this amount. + //[x_signature] => 77daab16115b25454f4d769ea96de2dcfd22ffb362651cc5274e5635c5a4e924 + $this->request = $request; + $this->disposition = $request[ConstantsV2::STATUS_PARAM] ?? Constants::STATUS_FAILURE; + $this->transactionNumber = $request[ConstantsV2::TRANSACTION_NO_PARAM] ?? ''; + $this->verificationToken = $request[ConstantsV2::VERIFICATION_TOKEN_PARAM] ?? ''; + $this->merchantOrderId = $request[ConstantsV2::ORDER_ID_PARAM] ?? ''; + $this->userId = $request[ConstantsV2::USER_ID_PARAM] ?? ''; + $this->hash = $request[ConstantsV2::HASH_PARAM] ?? ''; + } else { + $this->disposition = $request[Constants::DISPOSITION_PARAM] ?? Constants::STATUS_FAILURE; + $this->transactionNumber = $request[Constants::TRANSACTION_NO_PARAM] ?? ''; + $this->verificationToken = $request[Constants::VERIFICATION_TOKEN_PARAM] ?? ''; + $this->merchantOrderId = $request[Constants::ORDER_ID_PARAM] ?? ''; + $this->userId = $request[Constants::USER_ID_PARAM] ?? ''; + $this->hash = $request[Constants::HASH_PARAM] ?? ''; + } } /** @@ -89,4 +107,13 @@ public function getUserId() return $this->userId; } + /** + * return raw request + * @return mixed + */ + public function getRequest() + { + return $this->request; + } + } diff --git a/src/Models/VerifyTransactionResponse.php b/src/Models/VerifyTransactionResponse.php index dfd7684..c524317 100644 --- a/src/Models/VerifyTransactionResponse.php +++ b/src/Models/VerifyTransactionResponse.php @@ -9,22 +9,25 @@ class VerifyTransactionResponse /** * @var string */ - private $status; + protected $status; /** * @var string */ - private $message; + protected $message; /** * @var string */ - private $transactionNumber; + protected $transactionNumber; /** * @var string */ - private $hash; + protected $hash; + + + protected $isHashValid = null; /** * VerifyTransactionResult constructor. @@ -40,6 +43,9 @@ class VerifyTransactionResponse */ public function __construct(array $response) { + if (array_key_exists('is_hash_valid', $response)) { + $this->isHashValid = $response['is_hash_valid'] ?? null; + } $this->status = $response[Constants::STATUS_PARAM] ?? 'failure'; $this->message = $response[Constants::MESSAGE_PARAM] ?? ''; $this->transactionNumber = $response[Constants::TRANSACTION_NO_PARAM] ?? ''; @@ -83,6 +89,9 @@ public function getHash(): string */ public function isVerified(): bool { + if ($this->isHashValid !== null) { + return $this->isHashValid; + } return $this->status === Constants::STATUS_SUCCESS; } diff --git a/src/Traits/TendoPayHelper.php b/src/Traits/TendoPayHelper.php index 5b33ac9..da1872e 100644 --- a/src/Traits/TendoPayHelper.php +++ b/src/Traits/TendoPayHelper.php @@ -33,6 +33,19 @@ public static function hash(array $data): string return hash_hmac(Constants::get_hash_algorithm(), $message, $secret); } + /** + * @param array $payload + * @return string + */ + public static function hashForV2(array $payload): string + { + ksort($payload); + $message = array_reduce(array_keys($payload), static function ($p, $k) use ($payload) { + return strpos($k, 'tp_') === 0 ? $p.$k.trim($payload[$k]) : $p; + }, ''); + return hash_hmac('sha256', $message, self::getClientSecret()); + } + /** * @param array $params * @return array @@ -45,6 +58,18 @@ public static function appendHash(array $params): array ]); } + /** + * @param array $params + * @return array + */ + public static function appendV2Hash(array $params): array + { + $hash = static::hashForV2($params); + return array_merge($params, [ + 'x_signature' => $hash + ]); + } + /** * Generate compatible legacy tendopay_description * @return string diff --git a/src/Traits/TpHttpHelper.php b/src/Traits/TpHttpHelper.php new file mode 100644 index 0000000..e29b4a2 --- /dev/null +++ b/src/Traits/TpHttpHelper.php @@ -0,0 +1,146 @@ + $v) { + $curlHeaders[] = "$k: $v"; + } + + curl_setopt($ch, CURLOPT_URL, $endPointURL); + curl_setopt($ch, CURLOPT_VERBOSE, $debug); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 4); + curl_setopt($ch, CURLOPT_HTTPHEADER, $curlHeaders); + + $body = curl_exec($ch); + $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + + if ((int) $status >= 400) { + $response = json_decode($body, false); + $error = $response->error ?? $response->message ?? curl_error($ch); + curl_close($ch); + throw new \UnexpectedValueException($error, $status); + } + + curl_close($ch); + return $body; + } + + /** + * @param $method + * @param $requestURI + * @param $params + * @param array $headers + * @param bool $rawOutput + * @return mixed + * @throws TendoPayConnectionException + */ + protected function request($method, $requestURI, $params, $headers = [], $rawOutput = false) + { + try { + $data = $method === 'GET' ? $params : json_encode($params); + $response = $this->requestCurl( + $method, + Constants::get_base_api_url().DIRECTORY_SEPARATOR.$requestURI, + $data, + array_merge([ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'X-Using' => 'TendoPay_PHP_SDK_Client/1.0', + ], $headers), $this->debug); + + return $rawOutput ? $response : json_decode($response, false); + } catch (\Exception $e) { + throw new TendoPayConnectionException($e->getMessage(), $e->getCode()); + } + } + + /** + * @param bool $usePersonalAccessToken + * @return array + * @throws TendoPayConnectionException + */ + protected function getAuthorizationHeader($usePersonalAccessToken = false): array + { + $accessToken = $usePersonalAccessToken ? + $this->getPersonalAccessToken() : + $this->getAccessToken(); + + return [ + 'Authorization' => 'Bearer '.$accessToken, + ]; + } + + /** + * @return string + * @throws TendoPayConnectionException + */ + protected function getAccessToken(): string + { + if ($this->accessToken instanceof AccessToken + && !$this->accessToken->isExpired()) { + return $this->accessToken->getToken(); + } + + $params = [ + 'grant_type' => 'client_credentials', + 'client_id' => $this->getClientId(), + 'client_secret' => $this->getClientSecret(), + ]; + $body = $this->request('POST', + Constants::get_bearer_token_endpoint_uri(), + $params, [], true); + + $token = json_decode($body, true); + $this->accessToken = new AccessToken($token); + + return $this->accessToken->getToken(); + } + + /** + * Return Personal Access Token + * @return string + */ + protected function getPersonalAccessToken(): string + { + $token = $this->config['MERCHANT_PERSONAL_ACCESS_TOKEN'] ?? (string) getenv('MERCHANT_PERSONAL_ACCESS_TOKEN', + true); + if (!$token) { + throw new \InvalidArgumentException('MERCHANT_PERSONAL_ACCESS_TOKEN does not exists'); + } + + return $token; + } +} diff --git a/src/V2/ConstantsV2.php b/src/V2/ConstantsV2.php new file mode 100644 index 0000000..2e6a058 --- /dev/null +++ b/src/V2/ConstantsV2.php @@ -0,0 +1,221 @@ +config = $config; + $this->debug = $config['TENDOPAY_DEBUG'] ?? getenv('TENDOPAY_DEBUG') ?? false; + $this->initCredentials(); + $this->setSandBoxMode($config['TENDOPAY_SANDBOX_ENABLED'] ?? getenv('TENDOPAY_SANDBOX_ENABLED', true) ?? false); + $this->initRedirectURL(); + } + + /** + * Set Sandbox configuration + * @param $bool + */ + protected function setSandBoxMode($bool): void + { + putenv('TENDOPAY_SANDBOX_ENABLED='.$bool); + } + + /** + * Set credentials + */ + protected function initCredentials(): void + { +// if (isset($this->config['MERCHANT_ID'])) { +// putenv("MERCHANT_ID=".$this->config['MERCHANT_ID']); +// } +// if (isset($this->config['MERCHANT_SECRET'])) { +// putenv("MERCHANT_SECRET=".$this->config['MERCHANT_SECRET']); +// } + if (isset($this->config['CLIENT_ID'])) { + putenv("CLIENT_ID=".$this->config['CLIENT_ID']); + } + if (isset($this->config['CLIENT_SECRET'])) { + putenv("CLIENT_SECRET=".$this->config['CLIENT_SECRET']); + } + } + + /** + * Set Redirect urls + */ + protected function initRedirectURL(): void + { + $this->redirectURL = $this->config['REDIRECT_URL'] ?? (string) getenv('REDIRECT_URL', true); +// $this->errorRedirectURL = $this->config['ERROR_REDIRECT_URL'] ?? (string) getenv('ERROR_REDIRECT_URL', true); + } + + /** + * @param $method + * @param $endPointURL + * @param $data + * @param $headers + * @param false $debug + * @return mixed + */ + protected function requestCurl($method, $endPointURL, $data, $headers, $debug = false) + { + $ch = curl_init(); + + if (strtoupper($method) == 'GET') { + if ($data) { + $endPointURL .= '?'.http_build_query($data); + } + } else { + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + } + + $curlHeaders = []; + foreach ($headers as $k => $v) { + $curlHeaders[] = "$k: $v"; + } + + curl_setopt($ch, CURLOPT_URL, $endPointURL); + curl_setopt($ch, CURLOPT_VERBOSE, $debug); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 4); + curl_setopt($ch, CURLOPT_HTTPHEADER, $curlHeaders); + + $body = curl_exec($ch); + $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + + if ((int) $status >= 400) { + $response = json_decode($body, false); + $error = $response->error ?? $response->message ?? curl_error($ch); + curl_close($ch); + throw new \UnexpectedValueException($error, $status); + } + + curl_close($ch); + + return $body; + } + + /** + * @param $method + * @param $requestURI + * @param $params + * @param array $headers + * @param bool $rawOutput + * @return mixed + * @throws TendoPayConnectionException + */ + protected function request($method, $requestURI, $params, $headers = [], $rawOutput = false) + { + try { + $data = $method === 'GET' ? $params : json_encode($params); + $response = $this->requestCurl( + $method, + ConstantsV2::get_base_api_url().DIRECTORY_SEPARATOR.$requestURI, + $data, + array_merge([ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'X-Using' => 'TendoPay_PHP_SDK_Client/1.0', + ], $headers), $this->debug); + + return $rawOutput ? $response : json_decode($response, false); + } catch (\Exception $e) { + throw new TendoPayConnectionException($e->getMessage(), $e->getCode()); + } + } + + /** + * @param bool $usePersonalAccessToken + * @return array + * @throws TendoPayConnectionException + */ + protected function getAuthorizationHeader($usePersonalAccessToken = false): array + { + $accessToken = $usePersonalAccessToken ? + $this->getPersonalAccessToken() : + $this->getAccessToken(); + + return [ + 'Authorization' => 'Bearer '.$accessToken, + ]; + } + + /** + * @return string + * @throws TendoPayConnectionException + */ + protected function getAccessToken(): string + { + if ($this->accessToken instanceof AccessToken + && !$this->accessToken->isExpired()) { + return $this->accessToken->getToken(); + } + + $params = [ + 'grant_type' => 'client_credentials', + 'client_id' => $this->getClientId(), + 'client_secret' => $this->getClientSecret(), + ]; + $body = $this->request('POST', + ConstantsV2::get_bearer_token_endpoint_uri(), + $params, [], true); + + $token = json_decode($body, true); + $this->accessToken = new AccessToken($token); + + return $this->accessToken->getToken(); + } + + /** + * Return Personal Access Token + * @return string + */ + protected function getPersonalAccessToken(): string + { + $token = $this->config['MERCHANT_PERSONAL_ACCESS_TOKEN'] ?? (string) getenv('MERCHANT_PERSONAL_ACCESS_TOKEN', + true); + if (!$token) { + throw new InvalidArgumentException('MERCHANT_PERSONAL_ACCESS_TOKEN does not exists'); + } + + return $token; + } + + /** + * @param $params + * @return mixed|string + * @throws TendoPayConnectionException + * @deprecated in v2 + */ + protected function getRequestToken($params) + { + return $this->request('POST', + ConstantsV2::get_authorization_endpoint_uri(), + static::appendV2Hash($params), + $this->getAuthorizationHeader()); + } + + /** + * @param $params + * @return array|string + * @throws TendoPayConnectionException + */ + protected function requestPaymentDescription($params) + { + return $this->request('POST', + ConstantsV2::get_description_endpoint_uri(), + static::appendV2Hash($params), + $this->getAuthorizationHeader(), + true); + } + + /** Public Methods **/ + + /** + * Simple ping to check the server is alive or not + * + * @return bool + */ + public function ping(): bool + { + //@TODO check server is alive or throw an Exception + return true; +// throw new TendoPayConnectionException('There is some problem to communicate with the server'); + } + + /** + * @param string $url + * @return TendoPayClient + */ + public function setRedirectURL(string $url): self + { + $this->redirectURL = $url; + + return $this; + } + +// /** +// * @param string $url +// * @return TendoPayClient +// */ +// public function setErrorRedirectURL(string $url): self +// { +// $this->errorRedirectURL = $url; +// +// return $this; +// } + + /** + * Enable Sandbox mode + * + * @param bool $bool + * @return TendoPayClient + */ + public function enableSandBox(bool $bool = true): self + { + $this->setSandBoxMode($bool); + + return $this; + } + + /** + * @param Payment $payment + * @return TendoPayClient + */ + public function setPayment(Payment $payment): self + { + $this->payment = $payment; + + return $this; + } + + /** + * @return string + * @throws TendoPayConnectionException + */ + public function getAuthorizeLink(): string + { + $params = [ + 'tp_currency' => $this->payment->getCurrency(), + ConstantsV2::AMOUNT_PARAM => $this->payment->getRequestAmount(), + ConstantsV2::ORDER_ID_PARAM => $this->payment->getMerchantOrderId(), + ConstantsV2::DESC_PARAM => $this->payment->getDescription(), + ConstantsV2::REDIRECT_URL_PARAM => $this->payment->getRedirectUrl(), + ]; + + $order = $this->createPaymentOrder($params); + + return $order->authorize_url ?? ''; + } + + /** + * Just validity check with x_signature + * @return VerifyTransactionResponse + */ + public function verifyTransaction(): VerifyTransactionResponse + { + if (func_num_args() === 2) { + $request = func_get_arg(1); + } else { + $request = func_get_arg(0); + } + + if (!($request instanceof VerifyTransactionRequest)) { + throw new InvalidArgumentException('$request should be VerifyTransactionRequest'); + } + + $expected = static::hashForV2($request->getRequest()); + + $isHashValid = hash_equals($expected, $request->getHash()); + + $status = $request->getDisposition(); + $message = $request->getRequest()['tp_message'] ?? ''; + + if (!$isHashValid) { + $status = ConstantsV2::STATUS_FAILURE; + $message = 'Invalid signature'; + } + + $data = [ + 'is_hash_valid' => $isHashValid, + Constants::STATUS_PARAM => $status, + Constants::MESSAGE_PARAM => $message, + Constants::TRANSACTION_NO_PARAM => $request->getTransactionNumber(), + Constants::HASH_PARAM => $request->getHash(), + ]; + + return new VerifyTransactionResponse($data); + } + + /** + * Check required fields of the callback request + * if the given request is a callback request or not + * + * @param $request + * @return bool + */ + public static function isCallbackRequest($request): bool + { + return isset( + $request[ConstantsV2::STATUS_PARAM], + $request[ConstantsV2::TRANSACTION_NO_PARAM] + ); + } + + /** + * Retrieve Transaction Details with the transactionNumber + * @param $transactionNumber + * @return Transaction + * @throws Exception + */ + public function getTransactionDetail($transactionNumber): Transaction + { + $params = [ + ConstantsV2::TRANSACTION_NO_PARAM => $transactionNumber, + ]; + $response = $this->request('POST', + ConstantsV2::getTransactionDetailEndpointURI($transactionNumber), + static::appendV2Hash($params), + $this->getAuthorizationHeader(), true); + + $transaction = json_decode($response, true); + + return new Transaction($transaction); + } + + /** + * Cancel transaction + * @param $transactionNumber + * @return mixed|string + * @throws TendoPayConnectionException + */ + public function cancelPayment($transactionNumber) + { + $params = [ + ConstantsV2::TRANSACTION_NO_PARAM => $transactionNumber, + ]; + + return $this->request('POST', + ConstantsV2::get_cancel_payment_endpoint_uri(), + static::appendV2Hash($params), + $this->getAuthorizationHeader()); + } + + /** + * @param $params + * @return mixed + * @throws TendoPayConnectionException + */ + public function createPaymentOrder($params) + { + return $this->request('POST', + ConstantsV2::get_create_payment_order_endpoint_uri(), + static::appendV2Hash($params), + $this->getAuthorizationHeader()); + } +} + +