diff --git a/README.md b/README.md index 4e95d7f..3e54be8 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

## Requirements -- PHP v7.2+ +- PHP v7.2.5+ - PHP JSON Extension - PHP cURL Extension @@ -31,6 +31,7 @@ require 'path/to/your/vendor/autoload.php'; use ZerosDev\TriPay\Client as TriPayClient; use ZerosDev\TriPay\Support\Constant; +use ZerosDev\TriPay\Support\Helper; use ZerosDev\TriPay\Transaction; $merchantCode = 'T12345'; @@ -42,13 +43,21 @@ $guzzleOptions = []; // Your additional Guzzle options (https://docs.guzzlephp.o $client = new TriPayClient($merchantCode, $apiKey, $privateKey, $mode, $guzzleOptions); $transaction = new Transaction($client); +/** + * `amount` will be calculated automatically from order items + * so you don't have to enter it + * In this example, amount will be 40.000 + */ $result = $transaction ->addOrderItem('Gula', 10000, 1) - ->addOrderItem('Kopi', 6000, 1) + ->addOrderItem('Kopi', 6000, 5) ->create([ 'method' => 'BRIVA', + 'merchant_ref' => 'INV123', 'customer_name' => 'Nama Pelanggan', 'customer_email' => 'email@konsumen.id', + 'customer_phone' => '081234567890', + 'expired_time' => Helper::makeTimestamp('6 HOUR'), // see Supported Time Units ]); echo $result->getBody()->getContents(); @@ -61,3 +70,10 @@ echo json_encode($debugs, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); ``` Please check the `/examples` for the other examples + +## Supported Time Units +> :exclamation: All time units are in a singular noun +- SECOND +- MINUTE +- HOUR +- DAY diff --git a/composer.json b/composer.json index 929928c..3e2d260 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Unofficial TriPay.co.id Integration Kit for PHP", "type": "library", "require": { - "php": ">=7.2.0", + "php": ">=7.2.5", "ext-curl": "*", "ext-json": "*", "guzzlehttp/guzzle": "^6.5|^7.0" diff --git a/examples/callback-handling.php b/examples/callback-handling.php new file mode 100644 index 0000000..8d63938 --- /dev/null +++ b/examples/callback-handling.php @@ -0,0 +1,39 @@ +enableDebug(); + +/** + * Run validation + * It will throws Exception when validation fail + */ +try { + $callback->validate(); +} catch (Exception $e) { + echo $e->getMessage(); + die; +} + +/** + * Get callback data as object + */ +$data = $callback->data(); + +print_r($data); diff --git a/examples/merchant-fee-calculator.php b/examples/merchant-fee-calculator.php new file mode 100644 index 0000000..85b0b49 --- /dev/null +++ b/examples/merchant-fee-calculator.php @@ -0,0 +1,14 @@ +feeCalculator(100000, 'BRIVA'); +echo $result->getBody()->getContents(); diff --git a/examples/merchant-payment-channel.php b/examples/merchant-payment-channel.php new file mode 100644 index 0000000..fececa2 --- /dev/null +++ b/examples/merchant-payment-channel.php @@ -0,0 +1,14 @@ +paymentChannels(); +echo $result->getBody()->getContents(); diff --git a/examples/merchant-transactions.php b/examples/merchant-transactions.php new file mode 100644 index 0000000..7bf633c --- /dev/null +++ b/examples/merchant-transactions.php @@ -0,0 +1,14 @@ +transactions([]); +echo $result->getBody()->getContents(); diff --git a/examples/open-payment-create.php b/examples/open-payment-create.php new file mode 100644 index 0000000..470e62c --- /dev/null +++ b/examples/open-payment-create.php @@ -0,0 +1,18 @@ +create([ + 'method' => 'BSIVAOP', + 'merchant_ref' => 'USER-123', + 'customer_name' => 'Nama Pelanggan', +]); +echo $result->getBody()->getContents(); diff --git a/examples/open-payment-detail.php b/examples/open-payment-detail.php new file mode 100644 index 0000000..2390f94 --- /dev/null +++ b/examples/open-payment-detail.php @@ -0,0 +1,14 @@ +detail('T1234OP234GDGT4'); +echo $result->getBody()->getContents(); diff --git a/examples/open-payment-transactions.php b/examples/open-payment-transactions.php new file mode 100644 index 0000000..0de84f7 --- /dev/null +++ b/examples/open-payment-transactions.php @@ -0,0 +1,14 @@ +transactions('T1234OP234GDGT4'); +echo $result->getBody()->getContents(); diff --git a/examples/payment-instruction.php b/examples/payment-instruction.php new file mode 100644 index 0000000..0ec40f9 --- /dev/null +++ b/examples/payment-instruction.php @@ -0,0 +1,14 @@ +instruction('BRIVA'); +echo $result->getBody()->getContents(); diff --git a/examples/transaction-create.php b/examples/transaction-create.php new file mode 100644 index 0000000..cd1bf4b --- /dev/null +++ b/examples/transaction-create.php @@ -0,0 +1,32 @@ +addOrderItem('Gula', 10000, 1) + ->addOrderItem('Kopi', 5000, 3) + ->addOrderItem('Teh', 3000, 1) + ->addOrderItem('Nasi', 15000, 1) + ->create([ + 'method' => 'BRIVA', + 'merchant_ref' => 'INV123', + 'customer_name' => 'Nama Pelanggan', + 'customer_email' => 'email@konsumen.id', + 'customer_phone' => '081234567890', + 'return_url' => 'https://example.com/return', + 'expired_time' => Helper::makeTimestamp('1 DAY') + ]); +echo $result->getBody()->getContents(); diff --git a/examples/transaction-detail.php b/examples/transaction-detail.php new file mode 100644 index 0000000..3c8533e --- /dev/null +++ b/examples/transaction-detail.php @@ -0,0 +1,14 @@ +detail('T11111111111'); +echo $result->getBody()->getContents(); diff --git a/src/Callback.php b/src/Callback.php index 365a44d..93c5400 100644 --- a/src/Callback.php +++ b/src/Callback.php @@ -5,6 +5,7 @@ use Exception; use UnexpectedValueException; use ZerosDev\TriPay\Exception\SignatureException; +use ZerosDev\TriPay\Support\Constant; class Callback { @@ -29,13 +30,20 @@ class Callback */ protected ?object $parsedJson; + /** + * Enable/disable debug mode + * + * @var boolean + */ + protected bool $debug = false; + /** * Callback instance * * @param Client $client * @param bool $verifyOnLoad */ - public function __construct(Client $client, bool $verifyOnLoad = true) + public function __construct(Client $client) { if (!function_exists('file_get_contents')) { throw new Exception('`file_get_contents` function is disabled on your system. Please contact your hosting provider'); @@ -44,10 +52,21 @@ public function __construct(Client $client, bool $verifyOnLoad = true) $this->client = $client; $this->json = (string) file_get_contents("php://input"); $this->parsedJson = json_decode($this->json); + } - if ($verifyOnLoad) { - $this->validate(); - } + /** + * Enable debugging + * + * !! WARNING !! + * Only enable it while debugging. + * Leaving it enabled can lead to security vulnerabilities + * + * @return self + */ + public function enableDebug(): self + { + $this->debug = true; + return $this; } /** @@ -63,9 +82,9 @@ public function localSignature(): string /** * Get incoming signature * - * @return string|null + * @return string */ - public function incomingSignature(): ?string + public function incomingSignature(): string { return (string) (isset($_SERVER['HTTP_X_CALLBACK_SIGNATURE']) ? $_SERVER['HTTP_X_CALLBACK_SIGNATURE'] : ""); } @@ -79,19 +98,26 @@ public function incomingSignature(): ?string */ public function validate(): bool { + $localSignature = $this->localSignature(); + $incomingSignature = $this->incomingSignature(); + $validSignature = hash_equals( - $this->localSignature(), - $this->incomingSignature() + $localSignature, + $incomingSignature ); if (!$validSignature) { - throw new SignatureException('Incoming signature does not match local signature'); + $message = 'Incoming signature does not match local signature'; + if ($this->debug) { + $message .= ': local(' . $localSignature . ') vs incoming(' . $incomingSignature . ')'; + } + throw new SignatureException($message); } $validData = !is_null($this->data()); if (!$validData) { - throw new UnexpectedValueException('Callback data is invalid'); + throw new UnexpectedValueException('Callback data is invalid. Invalid or empty JSON'); } return true; diff --git a/src/Client.php b/src/Client.php index 45e08f7..0a332af 100644 --- a/src/Client.php +++ b/src/Client.php @@ -120,8 +120,8 @@ public function __construct(...$args) $this->merchantCode = (string) is_array($args[0]) ? $args[0]['merchant_code'] : $args[0]; $this->apiKey = (string) is_array($args[0]) ? $args[0]['api_key'] : $args[1]; - $this->privateKey = (string) is_array($args[0]) ? $args[0]['private_key'] : $args[1]; - $this->mode = (string) is_array($args[0]) ? $args[0]['mode'] : $args[2]; + $this->privateKey = (string) is_array($args[0]) ? $args[0]['private_key'] : $args[2]; + $this->mode = (string) is_array($args[0]) ? $args[0]['mode'] : $args[3]; $baseUri = ($this->mode == Constant::MODE_DEVELOPMENT) ? Constant::URL_DEVELOPMENT @@ -150,7 +150,7 @@ public function __construct(...$args) }, 'headers' => [ 'Authorization' => 'Bearer ' . $this->apiKey, - 'User-Agent' => 'zerosdev/tripay-sdk-php', + 'User-Agent' => 'github:zerosdev/tripay-sdk-php', ] ]; @@ -165,19 +165,40 @@ public function __construct(...$args) $this->client = $this->createHttpClient($options); } + /** + * Create HTTP client + * + * @param array $options + * @return HttpClient + */ private function createHttpClient(array $options): HttpClient { return new HttpClient($options); } - public function get($endpoint, array $headers = []): Response + /** + * Performe GET request + * + * @param string $endpoint + * @param array $headers + * @return Response + */ + public function get(string $endpoint, array $headers = []): Response { return $this->client->get($endpoint, [ 'headers' => $headers, ]); } - public function post($endpoint, array $payloads, array $headers = []): Response + /** + * Performe POST request + * + * @param string $endpoint + * @param array $payloads + * @param array $headers + * @return Response + */ + public function post(string $endpoint, array $payloads = [], array $headers = []): Response { return $this->client->post($endpoint, [ 'json' => $payloads, @@ -185,6 +206,11 @@ public function post($endpoint, array $payloads, array $headers = []): Response ]); } + /** + * Get debug data + * + * @return object + */ public function debugs(): object { return (object) $this->debugs; diff --git a/src/Support/Constant.php b/src/Support/Constant.php index 7924f5c..ee35c1c 100644 --- a/src/Support/Constant.php +++ b/src/Support/Constant.php @@ -9,4 +9,8 @@ class Constant public const MODE_DEVELOPMENT = 'development'; public const MODE_PRODUCTION = 'production'; + + public const LEVEL_LOW = 0; + public const LEVEL_MEDIUM = 1; + public const LEVEL_HIGH = 2; } diff --git a/src/Support/Helper.php b/src/Support/Helper.php index b96214c..a0aed69 100644 --- a/src/Support/Helper.php +++ b/src/Support/Helper.php @@ -19,8 +19,7 @@ public static function makeSignature(Client $client, array $payloads): string { $merchantRef = isset($payloads['merchant_ref']) ? $payloads['merchant_ref'] : null; $amount = self::formatAmount($payloads['amount']); - - $payloads['amount'] = self::formatAmount($payloads['amount']); + return hash_hmac('sha256', $client->merchantCode . $merchantRef . $amount, $client->privateKey); } @@ -70,4 +69,55 @@ public static function checkRequiredPayloads(array $requireds, array $payloads): } } } + + /** + * Make unix timestamp + * Supported time unit: SECOND, MINUTE, HOUR, DAY + * i.e: "1 DAY", "13 HOUR", etc + * + * @param string $value + * @return integer + */ + public static function makeTimestamp(string $value): int + { + if (!preg_match('/^[0-9]+[\s][A-Z]+$/is', $value)) { + throw new InvalidArgumentException("Value must be in '[value] [unit]' format: i.e: 1 DAY"); + } + + [$value, $unit] = explode(' ', $value); + + $value = (int) $value; + + if ($value === 0) { + throw new InvalidArgumentException('Value must be greater than 0'); + } + + $unit = strtoupper($unit); + + $supportedUnits = ['SECOND', 'MINUTE', 'HOUR', 'DAY']; + + if (!in_array($unit, $supportedUnits)) { + throw new InvalidArgumentException('Unsupported time unit. Supported: ' . implode(', ', $supportedUnits)); + } + + switch ($unit) { + case 'SECOND': + $timestamp = $value; + break; + + case 'MINUTE': + $timestamp = $value * 60; + break; + + case 'HOUR': + $timestamp = $value * 60 * 60; + break; + + case 'DAY': + $timestamp = $value * 24 * 60 * 60; + break; + } + + return (int) (time() + $timestamp); + } }