elgibor-solution / laravel-payment-bni
Laravel package for BNI eCollection (VA) & QRIS with auditing, granular errors, tests & Postman.
Installs: 9
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/elgibor-solution/laravel-payment-bni
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.9
- illuminate/contracts: ^11 || ^12
- illuminate/http: ^11 || ^12
- illuminate/support: ^11 || ^12
Requires (Dev)
- orchestra/testbench: ^9 || ^10 || ^11
- phpunit/phpunit: ^11 || ^12
README
Namespace: ESolution\BNIPayment
Laravel package to integrate BNI Virtual Account / eCollection and BNI QRIS SNAP BI (MPM) with:
- ✅ VA: create, update, inquiry
- ✅ QRIS SNAP BI (Dynamic QR MPM + Inquiry)
- ✅ Access Token B2B (SNAP)
- ✅ X-SIGNATURE (HMAC / RSA)
- ✅ Webhook payment notification
- ✅ Audit trail ke DB (request/response)
- ✅ Error handling granular (kode BNI)
- ✅ DB Config (override config file)
- ✅ Mirror billing + reconcile (scheduler)
- ✅ Events:
BniPaymentReceived,BniBillingPaid,BniBillingExpired - ✅ Unit tests (Orchestra), Postman collection
📦 Installation
composer require elgibor-solution/laravel-payment-bni
Publish config & migration:
php artisan vendor:publish --provider="ESolution\BNIPayment\BNIPaymentServiceProvider" php artisan vendor:publish --provider="ESolution\BNIPayment\BNIPaymentServiceProvider" --tag=bni-migrations php artisan migrate
⚙️ Configuration
File utama konfigurasi: config/bni.php
ENV minimal
# General BNI BNI_HOSTNAME=api.bni-ecollection.com BNI_HOSTNAME_STAGING=apibeta.bni-ecollection.com BNI_PORT=443 BNI_ORIGIN=https://your-domain.com BNI_CLIENT_ID=your_bni_client_id BNI_TIMEOUT=15 BNI_VERIFY_SSL=true BNI_DEBUG=true # true = sandbox / staging # ==== QRIS SNAP / SNAP BI ==== BNI_SNAP_BASE_URL=https://merchant-api.qris-bni.com/apisnap BNI_SNAP_BASE_URL_STAGING=https://qris-merchant-api.spesandbox.com/apisnap BNI_SNAP_CLIENT_ID=your_snap_client_id BNI_SNAP_CLIENT_KEY=your_snap_client_key BNI_SNAP_CLIENT_SECRET=your_snap_client_secret BNI_SNAP_PARTNER_ID=your_partner_id BNI_SNAP_VERSION=v1.0 BNI_SNAP_PRIVATE_KEY_PATH=/full/path/to/private_key.pem BNI_SNAP_SIGNATURE_TYPE=1 # 1=HMAC+Token, 2=RSA no token # ==== QRIS Merchant Info ==== BNI_QRIS_MERCHANT_ID=00007100010926 BNI_QRIS_TERMINAL_ID=213141251124 BNI_QRIS_PATH_GENERATE_QR=/v1.0/debit/payment-qr/qr-mpm BNI_QRIS_PATH_QUERY_PAYMENT=/v1.0/debit/payment-qr/qr-mpm/status BNI_QRIS_PATH_ACCESS_TOKEN=/access-token/b2b
Potongan config/bni.php (versi baru)
return [ 'hostname' => env('BNI_HOSTNAME', 'api.bni-ecollection.com'), 'hostname_staging' => env('BNI_HOSTNAME_STAGING', 'apibeta.bni-ecollection.com'), 'port' => (int) env('BNI_PORT', 443), 'origin' => env('BNI_ORIGIN', 'your-origin'), 'client_id' => env('BNI_CLIENT_ID', '320'), 'timeout' => (int) env('BNI_TIMEOUT', 15), 'verify_ssl' => (bool) env('BNI_VERIFY_SSL', true), 'debug' => (bool) env('BNI_DEBUG', false), 'snap' => [ 'base_url' => env('BNI_SNAP_BASE_URL', 'https://merchant-api.qris-bni.com/apisnap'), 'base_url_staging' => env('BNI_SNAP_BASE_URL_STAGING', 'https://qris-merchant-api.spesandbox.com/apisnap'), 'version' => env('BNI_SNAP_VERSION', 'v1.0'), 'client_id' => env('BNI_SNAP_CLIENT_ID', env('BNI_CLIENT_ID')), 'client_key' => env('BNI_SNAP_CLIENT_KEY', env('BNI_CLIENT_ID')), 'client_secret' => env('BNI_SNAP_CLIENT_SECRET', ''), 'partner_id' => env('BNI_SNAP_PARTNER_ID', ''), 'private_key_path' => env('BNI_SNAP_PRIVATE_KEY_PATH', storage_path('app/bni/snap_private_key.pem')), 'public_key_path' => env('BNI_SNAP_PUBLIC_KEY_PATH', storage_path('app/bni/snap_public_key.pem')), 'signature_type' => (int) env('BNI_SNAP_SIGNATURE_TYPE', 1), 'timeout' => (int) env('BNI_SNAP_TIMEOUT', env('BNI_TIMEOUT', 15)), 'verify_ssl' => (bool) env('BNI_SNAP_VERIFY_SSL', env('BNI_VERIFY_SSL', true)), ], 'routes' => [ 'prefix' => env('BNI_ROUTE_PREFIX', ''), 'middleware' => ['api'], ], 'qris' => [ 'merchant_id' => env('BNI_QRIS_MERCHANT_ID', ''), 'terminal_id' => env('BNI_QRIS_TERMINAL_ID', ''), 'path_access_token' => env('BNI_QRIS_PATH_ACCESS_TOKEN', '/access-token/b2b'), 'path_generate_qr' => env('BNI_QRIS_PATH_GENERATE_QR', '/v1.0/debit/payment-qr/qr-mpm'), 'path_query_payment' => env('BNI_QRIS_PATH_QUERY_PAYMENT', '/v1.0/debit/payment-qr/qr-mpm/status'), // fallback lama 'path_create_dynamic' => '/qris/create', 'path_inquiry_status' => '/qris/inquiry', ], 'schedule' => [ 'enabled' => env('BNI_SCHEDULE_ENABLED', false), 'cron' => env('BNI_SCHEDULE_CRON', '*/5 * * * *'), ], ];
⚠️ Catatan: Untuk VA / eCollection masih memakai mekanisme lama (BniEnc). SNAP BI khusus untuk channel
qris.
🧱 Arsitektur Singkat
| Class | Fungsi |
|---|---|
BaseClient |
HTTP client, logger, routing ke host BNI (VA / SNAP) |
BniVaClient |
Integrasi BNI Virtual Account / eCollection |
BniQrisClient |
Integrasi BNI QRIS SNAP BI (generate QR & inquiry) |
BniSnapAuth |
Access Token B2B, X-SIGNATURE (HMAC / RSA) |
BniEnc |
Enkripsi / dekripsi payload BNI VA |
BniPaymentLog |
Audit trail request/response |
BniBilling |
Mirror data billing VA di DB |
Event BniPaymentReceived |
Dikirim saat ada webhook payment |
Event BniBillingPaid |
Dikirim saat VA sukses dibayar |
Event BniBillingExpired |
Dikirim saat VA kedaluwarsa |
🧾 Penggunaan BNI VA / eCollection
1. Persiapan data kredensial
Untuk VA, kamu akan menerima dari BNI:
client_idsecret_key(kadang disebutclient_secretatauapi_keydi dokumen lama)- Prefix billing (optional, tergantung setup)
Saran: simpan di .env kamu sendiri:
BNI_VA_CLIENT_ID=your_va_client_id BNI_VA_SECRET=your_va_secret_key BNI_VA_PREFIX=SAAS # contoh prefix untuk nomor VA
Dan di config/services.php (opsional):
'bni_va' => [ 'client_id' => env('BNI_VA_CLIENT_ID', env('BNI_CLIENT_ID')), 'secret' => env('BNI_VA_SECRET', ''), 'prefix' => env('BNI_VA_PREFIX', ''), ],
2. Membuat VA (Create Billing)
use ESolution\BNIPayment\Clients\BniVaClient; $clientId = config('services.bni_va.client_id'); // atau config('bni.client_id') $secret = config('services.bni_va.secret'); $prefix = config('services.bni_va.prefix', ''); $client = app(BniVaClient::class); $response = $client->createVa([ 'type' => 'createbilling', 'client_id' => $clientId, 'trx_id' => 'INV-2025-0001', // unique per invoice 'trx_amount' => '150000', // tanpa desimal 'billing_type' => 'c', // 'c' = closed / fixed amount 'customer_name' => 'PT Pelanggan Makmur', 'customer_email' => 'finance@pelanggan.com', 'customer_phone' => '08123456789', 'datetime_expired' => '2025-12-31 23:59:00', 'description' => 'Tagihan langganan SaaS bulan Desember 2025', ], $clientId, $prefix, $secret);
Paket ini akan otomatis:
- Meng-enkripsi payload
- Memanggil endpoint BNI eCollection
- Menyimpan / mengupdate data ke tabel
bni_billingsvia modelBniBilling:trx_idvirtual_accounttrx_amountcustomer_name/customer_email/customer_phonebilling_typedescriptionexpired_at
3. Update VA (Update Billing)
$response = $client->updateVa([ 'type' => 'updatebilling', 'client_id' => $clientId, 'trx_id' => 'INV-2025-0001', 'trx_amount' => '200000', 'customer_name' => 'PT Pelanggan Makmur', 'customer_email' => 'finance@pelanggan.com', 'customer_phone' => '08123456789', 'billing_type' => 'c', 'description' => 'Update nominal tagihan', 'datetime_expired' => '2026-01-15 23:59:00', ], $clientId, $prefix, $secret);
4. Inquiry VA (Cek Status Billing)
$response = $client->inquiryVa('INV-2025-0001', $clientId, $prefix, $secret); // Contoh akses data: $status = $response['status'] ?? null; $paidAmt = $response['payment_amount'] ?? null;
🔔 Webhook Payment Notification (VA)
Paket ini menyediakan route webhook default untuk notifikasi pembayaran BNI VA.
1. Route
Setelah publish config, route default:
POST /bni/va/payment-notification
Route ini:
- Memverifikasi dan mendekripsi payload
- Menyimpan log ke
bni_payment_logs - Mengupdate status
BniBilling - Mem-broadcast event
BniPaymentReceived
2. Contoh Listener
Daftarkan listener di EventServiceProvider:
protected $listen = [ \ESolution\BNIPayment\Events\BniPaymentReceived::class => [ \App\Listeners\HandleBniPaymentReceived::class, ], ];
Contoh listener sederhana:
namespace App\Listeners; use ESolution\BNIPayment\Events\BniPaymentReceived; class HandleBniPaymentReceived { public function handle(BniPaymentReceived $event) { $payload = $event->payload; // contoh: tandai invoice sebagai paid $trxId = $payload['trx_id'] ?? null; if ($trxId) { // update invoice internal kamu di sini } } }
🕒 Scheduler & Reconcile
Aktifkan scheduler di .env:
BNI_SCHEDULE_ENABLED=true BNI_SCHEDULE_CRON=*/5 * * * * # setiap 5 menit
Tambahkan ke app/Console/Kernel.php:
protected function schedule(Schedule $schedule) { if (config('bni.schedule.enabled')) { $schedule->command('bni:reconcile')->cron(config('bni.schedule.cron')); } }
Jalankan manual:
php artisan bni:reconcile
💳 Penggunaan BNI QRIS SNAP BI (MPM)
1. Inisialisasi Client
use ESolution\BNIPayment\Clients\BniQrisClient; $qris = new BniQrisClient();
2. Generate Dynamic QR (MPM)
$response = $qris->generateQr([ 'partnerReferenceNo' => 'INV-2025-0001', 'amount' => [ 'value' => '15000.00', 'currency' => 'IDR', ], 'merchantId' => config('bni.qris.merchant_id'), 'terminalId' => config('bni.qris.terminal_id'), 'validityPeriod'=> '2025-12-31T23:59:00+07:00', 'additionalInfo'=> [ 'additionalData' => 'Tagihan SaaS Desember 2025', ], ]); $qrContent = $response['qrContent'] ?? null;
3. Inquiry Payment (MPM Query Payment)
// cukup dengan partnerReferenceNo $response = $qris->queryPayment('INV-2025-0001'); // atau dengan payload lengkap $response = $qris->queryPayment([ 'partnerReferenceNo' => 'INV-2025-0001', ]); $status = $response['responseCode'] ?? null;
BniQrisClient::createDynamic()danBniQrisClient::inquiryStatus()masih ada sebagai alias untuk kompatibilitas mundur, namun disarankan pindah kegenerateQr()danqueryPayment().
🔑 Access Token B2B SNAP
Access Token (../{version}/access-token/b2b) diambil otomatis oleh BaseClient ketika:
- Channel =
qris signature_type = 1(Symmetric Signature with Get Token)
Token disimpan di Laravel Cache.
Jika ingin ambil manual:
use ESolution\BNIPayment\Services\BniSnapAuth; $token = BniSnapAuth::getAccessToken();
🔐 X-SIGNATURE (HMAC / RSA)
Implementasi mengikuti dokumen MPM – BNI QR ACQUIRING MERCHANT API SNAP BI v1.5.5.
Signature Type 1 – Symmetric (HMAC SHA512)
stringToSign =
HTTPMethod + ":" +
EndpointUrl + ":" +
AccessToken + ":" +
Lowercase(HexEncode(SHA-256(minify(RequestBody)))) + ":" +
TimeStamp
Header:
X-SIGNATURE = HMAC_SHA512(clientSecret, stringToSign)Authorization: Bearer {accessToken}
Signature Type 2 – Asymmetric (RSA SHA256)
stringToSign =
HTTPMethod + ":" +
EndpointUrl + ":" +
Lowercase(HexEncode(SHA-256(minify(RequestBody)))) + ":" +
TimeStamp
Header:
X-SIGNATURE = base64(RSA-SHA256(privateKey, stringToSign))
Paket ini menangani detail tersebut secara otomatis lewat BniSnapAuth::buildRequestSignature() dan BaseClient::snapRequest().
🧪 Testing
php artisan test
Atau dari Tinker:
php artisan tinker >>> app(ESolution\BNIPayment\Clients\BniVaClient::class)->createVa([...]); >>> app(ESolution\BNIPayment\Clients\BniQrisClient::class)->generateQr([...]);
🤝 Contributing
Pull request dan issue sangat diterima.
- Fork repository
- Buat branch feature:
git checkout -b feature/nama-feature - Commit & push
- Buka Pull Request
📄 License
Apache 2.0
🧑💻 Maintainer
PT Elgibor Solusi Digital
https://elgibor-solution.com