diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b5154bd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Zeros Developer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf38c7c --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Tripay Library + +For usage examples. See `/examples` directory. \ No newline at end of file diff --git a/autoload.php b/autoload.php new file mode 100644 index 0000000..87699bc --- /dev/null +++ b/autoload.php @@ -0,0 +1,10 @@ +=5.4.0", + "ext-json": "*", + "ext-curl": "*" + }, + "autoload": { + "psr-4" : { + "Tripay\\": "src" + } + }, + "require-dev": { + "phpunit/phpunit": "^7" + } +} \ No newline at end of file diff --git a/examples/closed-payment/detail-transaksi.php b/examples/closed-payment/detail-transaksi.php new file mode 100644 index 0000000..d3e9663 --- /dev/null +++ b/examples/closed-payment/detail-transaksi.php @@ -0,0 +1,22 @@ +apiKey($apiKey) + + ->forClosedPayment(); + + +$detail = $transaction->detail($reference); + +print_r($detail); diff --git a/examples/closed-payment/request-transaksi.php b/examples/closed-payment/request-transaksi.php new file mode 100644 index 0000000..9211247 --- /dev/null +++ b/examples/closed-payment/request-transaksi.php @@ -0,0 +1,49 @@ +apiKey($apiKey) + ->privateKey($privateKey) + ->merchantCode($merchantCode) + ->merchantRef($merchantRef) + ->channelCode($channelCode) + + ->expiresAfter($minutes) + + ->customerName($customerName) + ->customerEmail($customerEmail) + ->customerPhone($customerPhone) + + // ->addItem('Nama Produk', 'Harga Satuan', 'Jumlah', 'Kode SKU') + ->addItem('Nama Produk 1', 100000, 2, 'SKU-PRODUK-1') + ->addItem('Nama Produk 2', 100000, 6, 'SKU-PRODUK-2') + ->addItem('Nama Produk 3', 100000, 3, 'SKU-PRODUK-3') + ->addItem('Nama Produk 4', 100000, 1, 'SKU-PRODUK-4') + + ->forClosedPayment(); + + +$response = $transaction->process(); + +print_r($response); diff --git a/examples/merchant/channel-pembayaran.php b/examples/merchant/channel-pembayaran.php new file mode 100644 index 0000000..c6d704f --- /dev/null +++ b/examples/merchant/channel-pembayaran.php @@ -0,0 +1,19 @@ +apiKey($apiKey); + + // Jika panggil, hanya akan ditampilkan data sesuai channel code saja + // ->channelCode('BRIVA'); + +$channels = $merchant->channels(); + +print_r($channels); diff --git a/examples/merchant/daftar-transaksi.php b/examples/merchant/daftar-transaksi.php new file mode 100644 index 0000000..3ae939d --- /dev/null +++ b/examples/merchant/daftar-transaksi.php @@ -0,0 +1,20 @@ +apiKey($apiKey); + +$transactions = $merchant->transactions($page, $per_page); + +print_r($transactions); diff --git a/examples/merchant/kalkulator-biaya.php b/examples/merchant/kalkulator-biaya.php new file mode 100644 index 0000000..ec2c332 --- /dev/null +++ b/examples/merchant/kalkulator-biaya.php @@ -0,0 +1,22 @@ +apiKey($apiKey) + ->channelCode($channelCode); + + +$calculate = $merchant->calculate($amount); + +print_r($calculate); diff --git a/examples/open-payment/daftar-pembayaran.php b/examples/open-payment/daftar-pembayaran.php new file mode 100644 index 0000000..c178f5b --- /dev/null +++ b/examples/open-payment/daftar-pembayaran.php @@ -0,0 +1,22 @@ +apiKey($apiKey) + + ->forOpenPayment(); + + +$payments = $transaction->payments($uuid); + +print_r($payments); diff --git a/examples/open-payment/detail-transaksi.php b/examples/open-payment/detail-transaksi.php new file mode 100644 index 0000000..477b20f --- /dev/null +++ b/examples/open-payment/detail-transaksi.php @@ -0,0 +1,22 @@ +apiKey($apiKey) + + ->forOpenPayment(); + + +$detail = $transaction->detail($uuid); + +print_r($detail); diff --git a/examples/open-payment/request-transaksi.php b/examples/open-payment/request-transaksi.php new file mode 100644 index 0000000..6768dd8 --- /dev/null +++ b/examples/open-payment/request-transaksi.php @@ -0,0 +1,31 @@ +apiKey($apiKey) + ->privateKey($privateKey) + ->merchantCode($merchantCode) + ->channelCode($channelCode) + ->merchantRef($merchantRef) + ->customerName($customerName) + + ->forOpenPayment(); + +$response = $transaction->process(); + +print_r($response); diff --git a/src/Base.php b/src/Base.php new file mode 100644 index 0000000..60d4357 --- /dev/null +++ b/src/Base.php @@ -0,0 +1,175 @@ +base = Constants\Endpoint::BASE_LIVE; + break; + + case Constants\Environment::DEVELOPMENT: + $this->base = Constants\Endpoint::BASE_SANDBOX; + break; + + default: + throw new Exceptions\InvalidEndpointException( + 'Only Environment::PRODUCTION and Environment::DEVELOPMENT are supported' + ); + } + + $this->environment = $environment; + } + + /** + * Mengambil instruksi pembayaran dari masing-masing channel. + * + * @return \stdClass + */ + public function instructions() + { + $payloads = [ + 'code' => $this->channelCode, + ]; + + return $this->sendRequest('payment/instruction', $payloads); + } + + /** + * Set api key. + * + * @param string $apiKey + * + * @return Merchant + */ + public function apiKey($apiKey) + { + $this->apiKey = (string) $apiKey; + + return $this; + } + + /** + * Set private key. + * + * @param string $privateKey + * + * @return Merchant + */ + public function privateKey($privateKey) + { + $this->privateKey = (string) $privateKey; + + return $this; + } + + /** + * Set payment channel. + * + * @param string $channelCode + * + * @return Merchant + */ + public function channelCode($channelCode) + { + $this->channelCode = (string) $channelCode; + + return $this; + } + + /** + * Set merchant code. + * + * @param string $merchantCode + * + * @return Merchant + */ + public function merchantCode($merchantCode) + { + $this->merchantCode = (string) $merchantCode; + + return $this; + } + + /** + * Set reference code. + * + * @param string $merchantRef + * + * @return Merchant + */ + public function merchantRef($merchantRef) + { + $this->merchantRef = (string) $merchantRef; + + return $this; + } + + /** + * Kirim request. + * + * @param string $uri + * @param array $payloads + * + * @return \stdClass + */ + + protected function request($method, $uri, array $payloads) + { + $method = (string) $method; + $uri = $this->base.$uri; + + // TODO: gunakan socket jika cURL tidak tersedia di server. + $driver = new Drivers\Curl(); + $options = [ + CURLOPT_HEADER => false, + CURLOPT_HTTPHEADER => ['Authorization: Bearer '.$this->apiKey], + CURLOPT_FAILONERROR => false, + ]; + + // if (extension_loaded('curl') && is_callable('curl_init')) { + // $driver = new Drivers\Curl(); + // $options = [ + // CURLOPT_HEADER => false, + // CURLOPT_HTTPHEADER => ['Authorization: Bearer '.$this->apiKey], + // CURLOPT_FAILONERROR => false, + // ]; + // } elseif (function_exists('stream_context_create') && is_callable('stream_context_create')) { + // $driver = new Drivers\Stream(); + // $options = [ + // // .. + // ]; + // } else { + // throw new \RuntimeException( + // 'Please enable curl or stream context before using this library.' + // ); + // } + + if (! in_array($method, ['get', 'post', 'put', 'delete'])) { + throw new Exceptions\InvalidRequestTypeException(sprintf( + 'Only GET, POST, PUT and DELETE request are currently suported. Got: %s', + $this->requestType + )); + } + + return $driver->request($method, $uri, $payloads, $options); + } +} diff --git a/src/Constants/Endpoint.php b/src/Constants/Endpoint.php new file mode 100644 index 0000000..ad1e039 --- /dev/null +++ b/src/Constants/Endpoint.php @@ -0,0 +1,12 @@ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:[v].0) Gecko/20100101 Firefox/[v].0', + 'Linux' => 'Mozilla/5.0 (Linux x86_64; rv:[v].0) Gecko/20100101 Firefox/[v].0', + 'Darwin' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:[v].0) Gecko/20100101 Firefox/[v].0', + 'BSD' => 'Mozilla/5.0 (X11; FreeBSD amd64; rv:[v].0) Gecko/20100101 Firefox/[v].0', + 'Solaris' => 'Mozilla/5.0 (Solaris; Solaris x86_64; rv:[v].0) Gecko/20100101 Firefox/[v].0', + ]; + + $platform = $this->platform(); + $platform = $platform === 'Unknown' ? 'Linux' : $platform; + + return str_replace('[v]', $version, $agents[$platform]); + } + + /** + * Ambil sistem operasi server. + * + * @return string + */ + public function platform() + { + if ('\\' === DIRECTORY_SEPARATOR) { + return 'Windows'; + } + + $platforms = [ + 'Darwin' => 'Darwin', + 'DragonFly' => 'BSD', + 'FreeBSD' => 'BSD', + 'NetBSD' => 'BSD', + 'OpenBSD' => 'BSD', + 'Linux' => 'Linux', + 'SunOS' => 'Solaris', + ]; + + return isset($platforms[PHP_OS]) ? $platforms[PHP_OS] : 'Unknown'; + } +} diff --git a/src/Drivers/Curl.php b/src/Drivers/Curl.php new file mode 100644 index 0000000..e8cf364 --- /dev/null +++ b/src/Drivers/Curl.php @@ -0,0 +1,122 @@ +agent()); + + $query = empty($params) ? null : http_build_query($params, '', '&', PHP_QUERY_RFC1738); + + switch (strtolower($method)) { + case 'get': + $url .= $query ? '?'.$query : ''; + curl_setopt($curl, CURLOPT_HTTPGET, 1); + break; + + case 'post': + if ($query) { + curl_setopt($curl, CURLOPT_POSTFIELDS, $query); + } + + if (isset($options[CURLOPT_HTTPHEADER]) && is_array($options[CURLOPT_HTTPHEADER])) { + $options[CURLOPT_HTTPHEADER] = array_merge( + $options[CURLOPT_HTTPHEADER], + ['Content-Type: application/x-www-form-urlencoded'] + ); + } else { + $options[CURLOPT_HTTPHEADER] = ['Content-Type: application/x-www-form-urlencoded']; + } + + curl_setopt($curl, CURLOPT_POST, 1); + break; + + case 'put': + if ($query) { + curl_setopt($curl, CURLOPT_POSTFIELDS, $query); + } + + if (isset($options[CURLOPT_HTTPHEADER]) && is_array($options[CURLOPT_HTTPHEADER])) { + $options[CURLOPT_HTTPHEADER] = array_merge( + $options[CURLOPT_HTTPHEADER], + ['Content-Type: application/x-www-form-urlencoded'] + ); + } else { + $options[CURLOPT_HTTPHEADER] = ['Content-Type: application/x-www-form-urlencoded']; + } + + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT'); + break; + + case 'delete': + if ($query) { + curl_setopt($curl, CURLOPT_POSTFIELDS, $query); + } + + if (isset($options[CURLOPT_HTTPHEADER]) && is_array($options[CURLOPT_HTTPHEADER])) { + $options[CURLOPT_HTTPHEADER] = array_merge( + $options[CURLOPT_HTTPHEADER], + ['Content-Type: application/x-www-form-urlencoded'] + ); + } else { + $options[CURLOPT_HTTPHEADER] = ['Content-Type: application/x-www-form-urlencoded']; + } + + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); + break; + + default: throw new InvalidArgumentException(sprintf( + 'Usupported request method: %s', + strtoupper($method) + )); + } + + if (is_array($options) && ! empty($options)) { + curl_setopt_array($curl, $options); + } + + curl_setopt($curl, CURLOPT_URL, $url); + + $response = curl_exec($curl); + + if (false === $response) { + $code = curl_errno($curl); + $message = curl_error($curl); + curl_close($curl); + + throw new \Exception($message, $code); + } + + curl_close($curl); + + return json_decode($response); + } +} diff --git a/src/Drivers/Stream.php b/src/Drivers/Stream.php new file mode 100644 index 0000000..23c5083 --- /dev/null +++ b/src/Drivers/Stream.php @@ -0,0 +1,32 @@ + false, + 'message' => $this->getMessage(), + ]); + } +} diff --git a/src/Exceptions/InvalidEndpointException.php b/src/Exceptions/InvalidEndpointException.php new file mode 100644 index 0000000..8ded2aa --- /dev/null +++ b/src/Exceptions/InvalidEndpointException.php @@ -0,0 +1,8 @@ + $this->channelCode, + ]; + + return $this->request('get', 'payment/instruction', $payloads); + } + + /** + * Mendapatkan daftar channel pembayaran yang aktif pada akun merchant anda + * beserta informasi lengkap termasuk biaya transaksi dari masing-masing channel. + * + * @return \stdClass + */ + public function channels() + { + $payloads = [ + 'code' => $this->channelCode, + ]; + + return $this->request('get', 'merchant/payment-channel', $payloads); + } + + /** + * Mendapatkan rincian perhitungan biaya transaksi untuk masing-masing channel + * berdasarkan nominal yang ditentukan. + * + * @param int $price + * + * @return \stdClass + */ + public function calculate($price) + { + $payloads = [ + 'code' => $this->channelCode, + 'amount' => (int) $price, + ]; + + return $this->request('get', 'merchant/fee-calculator', $payloads); + } + + /** + * Mendapatkan daftar transaksi merchant. + * + * @param int $page + * @param int $per_page + * + * @return \stdClass + */ + public function transactions($page = 1, $per_page = 25) + { + $payloads = [ + 'code' => $this->channelCode, + 'page' => (int) $page, + 'per_page' => (int) $per_page, + ]; + + return $this->request('get', 'merchant/transactions', $payloads); + } +} diff --git a/src/Transaction.php b/src/Transaction.php new file mode 100644 index 0000000..872d523 --- /dev/null +++ b/src/Transaction.php @@ -0,0 +1,247 @@ +expiresAfter = $this->expiresAfter(1440); + + parent::__construct($environment); + } + + /** + * Set operasi untuk open payment. + * + * @return Transaction + */ + public function forOpenPayment() + { + $this->transactionType = 'open'; + + return $this; + } + + /** + * Set operasi untuk closed payment. + * + * @return Transaction + */ + public function forClosedPayment() + { + $this->transactionType = 'closed'; + + return $this; + } + + /** + * Tambahakan item ke list request. + * + * @param string $name + * @param int $price + * @param int $quantity + * @param string $sku + * + * @return Transaction + */ + public function addItem($name, $price, $quantity, $sku = null) + { + $this->items[] = [ + 'sku' => (string) $sku, + 'name' => (string) $name, + 'price' => (int) $price, + 'quantity' => (int) $quantity, + ]; + + return $this; + } + + /** + * Set nama customer. + * + * @param string $name + * + * @return Transaction + */ + public function customerName($name) + { + $this->customerName = (string) $name; + + return $this; + } + + + + /** + * Set email customer. + * + * @param string $email + * + * @return Transaction + */ + public function customerEmail($email) + { + $this->customerEmail = (string) $email; + + return $this; + } + + /** + * Set nomor telepon customer. + * + * @param string $phone + * + * @return Transaction + */ + public function customerPhone($phone) + { + $this->customerPhone = (string) $phone; + + return $this; + } + + /** + * Set return URL. + * + * @param string $url + * + * @return Transaction + */ + public function returnUrl($url) + { + if (! filter_var($url, FILTER_VALIDATE_URL)) { + throw new Exceptions\InvalidRedirectException(sprintf('Invalid URL fornat: %s', $url)); + } + + $this->returnUrl = $url; + + return $this; + } + + /** + * Set waktu kedaluwarsa invoice (dalam menit). + * + * @param int $minutes + * + * @return Transaction + */ + public function expiresAfter($minutes) + { + $this->expiresAfter = time() + ((int) $minutes * 60); + + return $this; + } + + /** + * Proses data transaksi. + * + * @return \stdClass + */ + public function process() + { + switch ($this->transactionType) { + case 'open': + $signature = hash_hmac( + 'sha256', + $this->merchantCode.$this->channelCode.$this->merchantRef, + $this->privateKey + ); + + $payloads = [ + 'method' => $this->channelCode, + 'merchant_ref' => $this->merchantRef, + 'customer_name' => $this->customerName, + 'signature' => $signature, + ]; + + return $this->request('post', 'open-payment/create', $payloads); + + case 'closed': + $amount = 0; + + for ($i = 0; $i < count($this->items); $i++) { + $amount += (int) $this->items[$i]['price'] * (int) $this->items[$i]['quantity']; + } + + $signature = hash_hmac( + 'sha256', + $this->merchantCode.$this->merchantRef.$amount, + $this->privateKey + ); + + $payloads = [ + 'method' => $this->channelCode, + 'merchant_ref' => $this->merchantRef, + 'amount' => $amount, + 'customer_name' => $this->customerName, + 'customer_email' => $this->customerEmail, + 'customer_phone' => $this->customerPhone, + 'order_items' => $this->items, + 'return_url' => $this->returnUrl, + 'expired_time' => $this->expiresAfter, + 'signature' => $signature, + ]; + + return $this->request('post', 'transaction/create', $payloads); + + default: + throw new Exceptions\InvalidTransactionTypeException(sprintf( + 'Only OPEN and CLOSED transaction are supported, got: %s', + $this->transactionType + )); + } + } + + + public function detail($uuid) + { + if (! is_string($uuid) || strlen(trim($uuid)) <= 0) { + throw new Exceptions\InvalidTransactionUuidException('Transaction UUID should be a non empty string.'); + } + + switch ($this->transactionType) { + case 'open': + $payloads = []; + return $this->request('get', 'open-payment/'.$uuid.'/detail', $payloads); + + case 'closed': + $payloads = ['reference' => $uuid]; // TODO: apakah reference sama dengan uuid? + return $this->request('get', 'transaction/detail', $payloads); + + default: throw new Exceptions\InvalidTransactionTypeException(sprintf( + 'Only OPEN and CLOSED transaction types are supported, got: %s', + $this->transactionType + )); + + } + } + + + public function payments($uuid) + { + if (! is_string($uuid) || strlen(trim($uuid)) <= 0) { + throw new Exceptions\InvalidTransactionUuidException('Transaction UUID should be a non-empty string.'); + } + + $payloads = []; + + return $this->request('get', 'open-payment/'.$uuid.'/transactions', $payloads); + } +}