Skip to content

Instantly share code, notes, and snippets.

@kura1420
Created June 9, 2025 03:08
Show Gist options
  • Select an option

  • Save kura1420/b79286675937094ad9686027b5470a6a to your computer and use it in GitHub Desktop.

Select an option

Save kura1420/b79286675937094ad9686027b5470a6a to your computer and use it in GitHub Desktop.
Service Xendit hit API
<?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