Created
June 9, 2025 03:08
-
-
Save kura1420/b79286675937094ad9686027b5470a6a to your computer and use it in GitHub Desktop.
Service Xendit hit API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| namespace App\Services; | |
| use App\Helpers\Formatter; | |
| use App\Models\XenditPaymentlink; | |
| use App\Models\XenditQrcode; | |
| use App\Models\XenditRetail; | |
| use App\Models\XenditVirtualaccount; | |
| use Illuminate\Support\Facades\Http; | |
| use Illuminate\Support\Facades\Log; | |
| /** | |
| * Simulating Payment: | |
| * | |
| * 1) Retail | |
| * - POST = https://api.xendit.co/fixed_payment_code/simulate_payment | |
| * - Payload | |
| * { | |
| * "retail_outlet_name": "ALFAMART", | |
| * "payment_code": "TEST163412", | |
| * "transfer_amount": 25000 | |
| * } | |
| * | |
| * 2) Virtual Account | |
| * - POST = https://api.xendit.co/callback_virtual_accounts/external_id=01jw5z6yqmt5s1qsqj4wgpwa57/simulate_payment | |
| * - Payload | |
| * { | |
| * "amount": 59081 | |
| * } | |
| * | |
| * 3) QR Code | |
| * - POST = https://api.xendit.co/qr_codes/:id/payments/simulate | |
| * - Payload | |
| * { | |
| * "amount": 10000 | |
| * } | |
| * | |
| */ | |
| class XenditService | |
| { | |
| public static $banks = [ | |
| 'BCA' => 'Bank Central Asia', | |
| 'BNI' => 'Bank Negara Indonesia', | |
| 'BRI' => 'Bank Rakyat Indonesia', | |
| 'BSI' => 'Bank Syariah Indonesia', | |
| 'MANDIRI' => 'Bank Mandiri' | |
| ]; | |
| public static $retailOutlets = [ | |
| 'ALFAMART', | |
| 'INDOMARET' | |
| ]; | |
| public static $QRPartners = [ | |
| 'DANA', | |
| 'LINKAJA' | |
| ]; | |
| public static function setup(): void | |
| { | |
| Log::build([ | |
| 'driver' => 'daily', | |
| 'path' => storage_path('logs/xendit.log'), | |
| ]); | |
| Http::macro('xendit', function () { | |
| $username = env('XENDIT_SECRET_KEY'); | |
| $password = ''; | |
| if (empty($username)) { | |
| return abort(403, "XENDIT SECRET KEY IS EMPTY"); | |
| } | |
| $url = 'https://api.xendit.co'; | |
| return Http::withBasicAuth($username, $password)->baseUrl($url); | |
| }); | |
| } | |
| # https://developers.xendit.co/api-reference/#create-invoice | |
| public static function createPaymentLink(object $row): object | |
| { | |
| try { | |
| $dueTimestamp = strtotime($row->duedate); | |
| $now = time(); | |
| $invoiceDuration = $dueTimestamp - $now; | |
| if ($invoiceDuration <= 0) { | |
| throw new \Exception("Date time is wrong", 1); | |
| } | |
| $params = [ | |
| 'external_id' => $row->id, | |
| 'amount' => (int) $row->total_payment, | |
| 'description' => $row->note, | |
| 'invoice_duration' => $invoiceDuration, // 86400 default 1 day on second | |
| "currency" => "IDR", | |
| 'customer' => [ | |
| 'given_names' => $row->customer->profile->name, | |
| // 'surname' => '', | |
| 'email' => $row->customer->profile->email, | |
| 'mobile_number' => Formatter::numberFormatE164($row->customer->profile->handphone_1), | |
| 'addresses' => [ | |
| [ | |
| 'city' => $row->customer->kota->name, | |
| 'country' => 'Indonesia', | |
| 'postal_code' => null, | |
| 'state' => $row->customer->provinsi->name, | |
| 'street_line1' => $row->customer->address, | |
| 'street_line2' => null, | |
| ], | |
| ], | |
| ], | |
| // 'customer_notification_preference' => [ | |
| // 'invoice_created' => [ | |
| // "whatsapp", | |
| // // "email", | |
| // ], | |
| // "invoice_reminder" => [ | |
| // "whatsapp", | |
| // // "email", | |
| // ], | |
| // "invoice_paid" => [ | |
| // "whatsapp", | |
| // // "email", | |
| // ] | |
| // ], | |
| // "success_redirect_url" => "https://www.google.com", | |
| // "failure_redirect_url" => "https://www.google.com", | |
| ]; | |
| $res = Http::xendit()->post('v2/invoices', $params); | |
| if (! $res->successful()) { | |
| $obj = $res->object(); | |
| throw new \Exception($obj->message, 1); | |
| } | |
| $obj = $res->object(); | |
| XenditPaymentlink::updateOrCreate( | |
| [ | |
| 'xendit_id' => $obj->id, | |
| 'invoice_id' => $obj->external_id, | |
| ], | |
| [ | |
| 'status' => $obj->status, | |
| 'amount' => $obj->amount, | |
| 'description' => $obj->description, | |
| 'expiry_date' => $obj->expiry_date, | |
| 'invoice_url' => $obj->invoice_url, | |
| 'created' => $obj->created, | |
| 'updated' => $obj->updated, | |
| 'payload' => json_encode($obj), | |
| ] | |
| ); | |
| return $obj; | |
| } catch (\Throwable $th) { | |
| throw $th; | |
| } | |
| } | |
| /** | |
| * Refrence: https://developers.xendit.co/api-reference/#create-virtual-account | |
| * | |
| * BNI Virtual Accounts | |
| * Range nomor Closed Virtual Account: 8808 - 8808 | |
| * Range nomor Open Virtual Account: 8808 - 8808 | |
| * | |
| * BRI Virtual Accounts | |
| * Range nomor Closed Virtual Account: 13282 9999000001 - 13282 9999999999 | |
| * Range nomor Open Virtual Account: 13281 9999000001 - 13281 9999999999 | |
| * | |
| * BSI Virtual Accounts | |
| * Range nomor Closed Virtual Account: 9347 99990000001 - 9347 99999999999 | |
| * Range nomor Open Virtual Account: 9655 99990000001 - 9655 99999999999 | |
| * | |
| * Mandiri Virtual Accounts | |
| * Range nomor Closed Virtual Account: 88908 10000000 - 88908 99999999 | |
| * Range nomor Open Virtual Account: 88608 9999000001 - 88608 9999999999 | |
| * | |
| */ | |
| public static function createVirtualAccount(object $row, string $bankCode, bool $VAFix): object | |
| { | |
| try { | |
| $dueTimestamp = strtotime($row->duedate); | |
| $now = time(); | |
| $invoiceDuration = $dueTimestamp - $now; | |
| if ($invoiceDuration <= 0) { | |
| throw new \Exception("Date time is wrong", 1); | |
| } | |
| $handphone = $row->customer->profile->handphone_1; | |
| $virtual_account_number = match ($bankCode) { | |
| // 'BCA' => , | |
| // 'BNI' => , | |
| 'BRI' => 9999 . substr($handphone, -6), | |
| 'BSI' => 9999 . substr($handphone, -7), | |
| 'MANDIRI' => substr($handphone, -8), | |
| default => throw new \Exception("Bank Code '{$bankCode}' is not found") | |
| }; | |
| $params = [ | |
| 'external_id' => $row->id, | |
| 'bank_code' => $bankCode, | |
| 'name' => $row->customer->profile->name, | |
| 'virtual_account_number' => $VAFix ? $virtual_account_number : 'random', | |
| 'is_single_use' => $VAFix, # true: va sekali pakai, false: va bisa di gunakan berulang kali | |
| 'is_closed' => true, # true: customer harus membayar sejumlah nominal yang sudah di tetapkan, false: customer bisa membayar sesuaka hati | |
| 'expected_amount' => (int) $row->total_payment, | |
| 'expiration_date' => Formatter::toISO8601UTC($row->duedate . ' 23:59:59'), | |
| ]; | |
| $res = Http::xendit()->post('callback_virtual_accounts', $params); | |
| if (! $res->successful()) { | |
| $obj = $res->object(); | |
| throw new \Exception($obj->message, 1); | |
| } | |
| $obj = $res->object(); | |
| XenditVirtualaccount::updateOrCreate( | |
| [ | |
| 'xendit_id' => $obj->id, | |
| 'invoice_id' => $obj->external_id, | |
| ], | |
| [ | |
| 'account_number' => $obj->account_number, | |
| 'bank_code' => $obj->bank_code, | |
| 'name' => $obj->name, | |
| 'is_closed' => $obj->is_closed, | |
| 'is_single_use' => $obj->is_single_use, | |
| 'status' => $obj->status, | |
| 'expected_amount' => $obj->expected_amount, | |
| 'expiration_date' => $obj->expiration_date, | |
| 'payload' => json_encode($obj), | |
| ] | |
| ); | |
| return $obj; | |
| } catch (\Throwable $th) { | |
| throw $th; | |
| } | |
| } | |
| /** | |
| * Reference: https://developers.xendit.co/api-reference/#update-virtual-account | |
| * | |
| * Status of Virtual Account that defines if it’s PENDING, INACTIVE, or ACTIVE | |
| * Status is PENDING if Virtual Account creation request has been sent and request is being processed by the bank | |
| * Status is INACTIVE either the single use Virtual Account has been paid or already expired | |
| * If status is ACTIVE the Virtual Account is ready to be used by the end user | |
| */ | |
| public static function updateVirtualAccount(string $xendit_id, array $params) | |
| { | |
| try { | |
| $res = Http::xendit()->patch("callback_virtual_accounts/{$xendit_id}", $params); | |
| if (! $res->successful()) { | |
| $obj = $res->object(); | |
| throw new \Exception($obj->message, 1); | |
| } | |
| $obj = $res->object(); | |
| $fields = [ | |
| 'bank_code', | |
| 'name', | |
| 'is_closed', | |
| 'is_single_use', | |
| 'status', | |
| 'expected_amount', | |
| 'expiration_date', | |
| ]; | |
| $update = []; | |
| foreach ($fields as $field) { | |
| if (property_exists($obj, $field) && array_key_exists($field, $params)) { | |
| $update[$field] = $obj->$field; | |
| } | |
| } | |
| $update['payload'] = json_encode($obj); | |
| XenditVirtualaccount::where('xendit_id', $xendit_id) | |
| ->update($update); | |
| return $obj; | |
| } catch (\Throwable $th) { | |
| throw $th; | |
| } | |
| } | |
| # https://developers.xendit.co/api-reference/#create-fixed-payment-code | |
| public static function createRetail(object $row, string $retailName, bool $isSingleUse): object | |
| { | |
| try { | |
| $params = [ | |
| 'external_id' => $row->id, | |
| 'retail_outlet_name' => $retailName, | |
| 'name' => $row->customer->profile->name, | |
| 'expected_amount' => (int) $row->total_payment, | |
| // 'payment_code' => '', | |
| 'expiration_date' => Formatter::toISO8601UTC($row->duedate . ' 23:59:59'), | |
| 'is_single_use' => $isSingleUse | |
| ]; | |
| $res = Http::xendit()->post('fixed_payment_code', $params); | |
| if (! $res->successful()) { | |
| $obj = $res->object(); | |
| throw new \Exception($obj->message, 1); | |
| } | |
| $obj = $res->object(); | |
| XenditRetail::updateOrCreate( | |
| [ | |
| 'xendit_id' => $obj->id, | |
| 'invoice_id' => $obj->external_id, | |
| ], | |
| [ | |
| 'payment_code' => $obj->payment_code, | |
| 'retail_outlet_name' => $obj->retail_outlet_name, | |
| 'prefix' => $obj->prefix, | |
| 'type' => $obj->type, | |
| 'expected_amount' => $obj->expected_amount, | |
| 'is_single_use' => $obj->is_single_use, | |
| 'status' => $obj->status, | |
| 'expiration_date' => $obj->expiration_date, | |
| 'payload' => json_encode($obj), | |
| ] | |
| ); | |
| return $obj; | |
| } catch (\Throwable $th) { | |
| throw $th; | |
| } | |
| } | |
| /** | |
| * Reference: https://developers.xendit.co/api-reference/#update-fixed-payment-code | |
| * | |
| * Status of fixed payment code that defines if it’s ACTIVE, INACTIVE, or EXPIRED. | |
| * Status is ACTIVE if fixed payment code has not paid or has not expired yet. | |
| * Status is INACTIVE if fixed payment code is already paid. | |
| * Status is EXPIRED if fixed payment code that has expiration date and been updated to the date less than now. | |
| */ | |
| public static function updateRetail(string $xendit_id, array $params): object | |
| { | |
| try { | |
| $res = Http::xendit()->patch("fixed_payment_code/{$xendit_id}", $params); | |
| if (! $res->successful()) { | |
| $obj = $res->object(); | |
| throw new \Exception($obj->message, 1); | |
| } | |
| $obj = $res->object(); | |
| $fields = [ | |
| 'payment_code', | |
| 'retail_outlet_name', | |
| 'prefix', | |
| 'type', | |
| 'expected_amount', | |
| 'is_single_use', | |
| 'status', | |
| 'expiration_date', | |
| ]; | |
| $update = []; | |
| foreach ($fields as $field) { | |
| if (property_exists($obj, $field) && array_key_exists($field, $params)) { | |
| $update[$field] = $obj->$field; | |
| } | |
| } | |
| $update['payload'] = json_encode($obj); | |
| XenditRetail::where('xendit_id', $xendit_id) | |
| ->update($update); | |
| return $obj; | |
| } catch (\Throwable $th) { | |
| throw $th; | |
| } | |
| } | |
| # https://developers.xendit.co/api-reference/id/#buat-kode-qr | |
| public static function createQrcode(object $row): object | |
| { | |
| try { | |
| $params = [ | |
| 'external_id' => $row->id . rand(1111, 9999), | |
| 'amount' => (int) $row->total_payment, | |
| 'type' => 'DYNAMIC', | |
| // 'callback_url' => route('webhook.xendit'), | |
| 'callback_url' => 'https://webhook-test.com/8145311ae79c56bf8ffe3742b43911ee', | |
| 'currency' => 'IDR', | |
| 'expires_at' => Formatter::toISO8601UTC($row->duedate . ' 23:59:59'), | |
| 'metadata' => [ | |
| 'customer_name' => $row->customer->profile->name | |
| ], | |
| ]; | |
| $res = Http::xendit()->post('qr_codes', $params); | |
| if (! $res->successful()) { | |
| $obj = $res->object(); | |
| throw new \Exception($obj->message, 1); | |
| } | |
| $obj = $res->object(); | |
| XenditQrcode::updateOrCreate( | |
| [ | |
| 'xendit_id' => $obj->id, | |
| // 'invoice_id' => $obj->external_id, | |
| 'invoice_id' => $row->id, | |
| ], | |
| [ | |
| 'qr_string' => $obj->qr_string, | |
| 'amount' => $obj->amount, | |
| 'callback_url' => $obj->callback_url, | |
| 'description' => $obj->description, | |
| 'type' => $obj->type, | |
| 'status' => $obj->status, | |
| // 'expires_at' => $obj->expires_at, | |
| 'created' => $obj->created, | |
| 'updated' => $obj->updated, | |
| 'payload' => json_encode($obj), | |
| ] | |
| ); | |
| return $obj; | |
| } catch (\Throwable $th) { | |
| throw $th; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment