[FIX-PAYMENT-WEBHOOK-RACE-CONDITION] Resolusi Race Condition Webhook Pembayaran Tagihan Kombinasi PNBP + BLU (Mandiri & Simponi)
Modul: Single Billing | Tipe: Bugfix | Stage Saat Ini:
PRODUCTION| Tanggal Dokumen: 2026-09-07
PRODUCTION RELEASED
Dokumen ini telah berada dalam tahap PRODUCTION. Perubahan logika backend, migrasi skema/konfigurasi, dan rilis v1.9.2 telah diterapkan secara penuh di lingkungan Production via MR !232.
LIFECYCLE GUIDE — UPDATE BERTAHAP:
- Buat file ini saat selesai development. Isi semua section kecuali Staging & Production.
- Tanggal Dokumen (Header) & Riwayat Update Log: Gunakan tanggal hari ini (pembuatan dokumen).
- Baris Development (Lifecycle): Gunakan tanggal & jam commit terakhir terkait di branch development.
- Update baris Staging saat naik ke UAT. Ubah "Stage Saat Ini" di header menjadi STAGING.
- Update baris Production saat naik ke Prod. Ubah "Stage Saat Ini" di header menjadi PRODUCTION.
- Update entry di README.md setiap kali status berubah. -->
1. Informasi Tugas & Referensi Bisnis
Task ID / Nomor Referensi:
FIX-PAYMENT-WEBHOOK-RACE-CONDITIONSumber Instruksi: Investigasi Masalah Pelunasan Tagihan Kombinasi PNBP + BLU & Sinkronisasi Mandiri-Simponi
Dokumen Referensi / Lampiran:
-Task / Dependensi Terkait (Cross-Service):
FEAT-SIMPONI-V3-ISOLATIONModul Terkait:
Single BillingService / Repository:
single-billing-BEPIC Pengembang: Hermansyah Efendi
Latar Belakang & Ringkasan Kebutuhan: Pada tagihan bundling yang memuat komponen PNBP dan Badan Layanan Umum (BLU), sistem Single Billing menerima dua notifikasi pembayaran secara asinkron dari dua pihak berbeda: gateway SIMPONI (untuk porsi PNBP) dan Bank Mandiri (untuk penagihan terpadu & porsi BLU). Kondisi ini memicu race condition: jika webhook SIMPONI tiba beberapa milidetik lebih awal, status tagihan langsung diubah menjadi
fl_paid = true. Akibatnya, saat webhook Bank Mandiri tiba, validasi awal dipym.reqPaymentVAMandirimenolak request sebagai "Paid bill" (responseCode: "4042514"), sehingga pencatatan pelunasan BLU terblokir dan data settlement BLU gagal dikirimkan ke downstream services (Inaportnet, Sinkarkes, Pengangkut). Di samping itu, webhook SIMPONI mencoba mendisposisikan flag lunas ke downstream service secara prematur sebelum data pembayaran BLU dari Bank Mandiri lengkap. Perbaikan ini menetapkan Bank Mandiri sebagai single source of truth untuk tagihan berunsur BLU, melewati guardfl_paidumum pada Mandiri jika tagihan memiliki komponen BLU dengan pengaman idempotensi berbasis modelbilling.payment_sb, serta melakukan early return pada webhook SIMPONI agar hanya mencatat penerimaan PNBP tanpa memicu disposisi lunas prematur.Ruang Lingkup & Rincian Sub-Pekerjaan (Technical Scope & Milestones):
- [x] Bypass validasi
userData.flPaidpada controller pembayaran Mandiri (pym.reqPaymentVAMandiri) khusus untuk transaksi yang memiliki komponen BLU (userData.flBlu === true) - [x] Implementasi pengecekan idempotensi pada webhook Bank Mandiri menggunakan model
billing.paymentSbuntuk mencegah duplikasi eksekusi request retry dari bank - [x] Penambahan mekanisme early return pada webhook SIMPONI (
activeinquiry_v2) untuk tagihan PNBP+BLU agar hanya mencatat status pembayaran PNBP tanpa mengubahfl_paiddan tanpa mengirim flag lunas prematur ke Inaportnet, Sinkarkes, dan Pengangkut - [x] Penghapusan blok kode legacy redundan pada
pembayaran_controller.jsyang sebelumnya memproses settlement BLU secara prematur di webhook SIMPONI (-54 baris) - [x] Pendefinisian skema model Sequelize
billing.paymentSbuntuk memetakan tabelbilling.payment_sbpadaapp/models/banking/inquiry_models.js
- [x] Bypass validasi
2. Deployment Lifecycle
| # | Environment | Tanggal & Waktu | PIC | Status | Catatan |
|---|---|---|---|---|---|
| 1 | Development | 2026-06-26 16:20 | Hermansyah Efendi | DEPLOYED | Branch: development |
| 2 | Staging (UAT) | 2026-10-14 13:00 | Hermansyah Efendi / QA | DEPLOYED | Release Candidate via MR Staging !229. Co-deployed: [FEAT-SIMPONI-V3-ISOLATION, TASK-SET-EXPIRED-BILLING-V3, FEAT-BILLING-REKAP-EXPIRED-DATE-PAYLOAD, REFACTOR-TAGIHAN-CONTROLLER-DEDUPLICATION, FIX-PAYMENT-WEBHOOK-RACE-CONDITION, FIX-TAGIHAN-PREVENT-PAID-INVOICE-REORDER, FEAT-REQUEST-LOGGER-POSTMAN, TEST-DRAGONFLY-VITEST-SUITE, FEAT-RUNTIME-ENV-VALIDATION, TASK-NON-BLOCKING-BILLING-DISPATCH-AND-RETRY-CONFIG] |
| 3 | Production | 2026-10-14 20:06 | achmad fauzi | DEPLOYED | Branch: prod via MR !232 (Release v1.9.2). Co-deployed: [FEAT-SIMPONI-V3-ISOLATION, TASK-SET-EXPIRED-BILLING-V3, FEAT-BILLING-REKAP-EXPIRED-DATE-PAYLOAD, REFACTOR-TAGIHAN-CONTROLLER-DEDUPLICATION, FIX-PAYMENT-WEBHOOK-RACE-CONDITION, FIX-TAGIHAN-PREVENT-PAID-INVOICE-REORDER, FEAT-REQUEST-LOGGER-POSTMAN, TEST-DRAGONFLY-VITEST-SUITE, FEAT-RUNTIME-ENV-VALIDATION, TASK-NON-BLOCKING-BILLING-DISPATCH-AND-RETRY-CONFIG] |
Riwayat Update Log
| Tanggal | Environment | Perubahan | PIC |
|---|---|---|---|
| 2026-10-14 | Production | Status diupdate ke PRODUCTION (DEPLOYED, CLOSED), rilis Release v1.9.2 via MR !232 | Hermansyah Efendi |
| 2026-09-07 | Development | Inisialisasi draft audit log dari context block backend (commit dc66325) | Hermansyah Efendi |
3. Analisis Teknis & Rincian Perubahan
Alur Logika & Bisnis:
- Source of Truth Pembayaran BLU: Untuk tagihan PNBP murni, SIMPONI tetap menjadi penentu pelunasan. Namun untuk tagihan kombinasi PNBP + BLU, Bank Mandiri ditetapkan sebagai single source of truth karena seluruh tagihan induk dibayarkan secara terpadu satu pintu melalui Virtual Account Mandiri.
- Pencegahan Penolakan Prematur Mandiri: Validasi
if (userData.flPaid)diapp/controllers/banking/payment_controller.jsdiubah menjadiif (userData.flPaid && !isBluBilling). Jika tagihan mengandung komponen BLU (userData.flBlu === true), controller Mandiri tetap memproses pembayaran meskipun SIMPONI telah mengeset statusfl_paidterlebih dahulu. - Pengecekan Idempotensi (Idempotency Guard): Untuk mencegah eksekusi ganda jika Bank Mandiri melakukan retry callback, sistem memeriksa keberadaan
kode_billing_indukdi tabelbilling.payment_sbmelalui modelbilling.paymentSb. Jika record pembayaran Mandiri sudah pernah tercatat, request ditolak dengan kode4042514("Paid bill"). - Isolasi Alur Callback SIMPONI (Early Return): Pada webhook SIMPONI
activeinquiry_v2(app/controllers/pembayaran_controller.js), jika terdeteksidataSubbilerHdr.fl_pnbp && dataSubbilerHdr.fl_blu && !dataSubbilerHdr.fl_bup, sistem mencatat pembayaran PNBP, menyimpan log bahwa transaksi PNBP+BLU akan ditangani oleh Bank Payment, dan langsung mengembalikan kode respons sukses3ke SIMPONI tanpa mengubah flag lunas global serta tanpa mendisposisikan data lunas prematur ke Inaportnet, Sinkarkes, dan Pengangkut. - Daftar File yang Berubah:
app/controllers/banking/payment_controller.js(Controller — bypassfl_paiduntuk BLU dan penambahan cek idempotensi viabilling.paymentSb)app/controllers/pembayaran_controller.js(Controller — early return PNBP+BLU padaactiveinquiry_v2dan penghapusan kode legacy settlement)app/models/banking/inquiry_models.js(Model — pendefinisian model Sequelizebilling.paymentSb)
Perubahan Database (DDL / DML):
sql-- Tidak ada migration file DDL/DML terdeteksi. -- Tabel billing.payment_sb telah tersedia di database PostgreSQL dan dimapping ke model Sequelize runtime billing.paymentSb.Mapping model Sequelize runtime (
app/models/banking/inquiry_models.js):javascriptbilling.paymentSb = db.define('payment_sb', { idBillingSb: { type: Sequelize.BIGINT, field: 'id_billing_sb', autoIncrement: true, primaryKey: true }, kodeBillingInduk: { field: 'kode_billing_induk', type: Sequelize.STRING(50) }, kodeBillingDetail: { field: 'kode_billing_detail', type: Sequelize.TEXT }, tglJamPembayaran: { field: 'tgl_jam_pembayaran', type: Sequelize.STRING }, trxIdBilling: { field: 'trx_id_billing', type: Sequelize.STRING }, bankPersepsi: { field: 'bank_persepsi', type: Sequelize.STRING }, channelPembayaran: { field: 'channel_pembayaran', type: Sequelize.STRING }, wkRekam: { field: 'wk_rekam', type: Sequelize.DATE }, }, { schema: 'billing', freezeTableName: true, timestamps: false });Kontrak API & Response Payload:(Tidak ada perubahan kontrak endpoint publik/eksternal. Penyesuaian internal berlaku pada penanganan payload webhook callback)
POST/mandiri/payment- Deskripsi: Internal Bank Callback dari Bank Mandiri untuk eksekusi pelunasan Virtual Account tagihan Single Billing.
- Autentikasi / Guard: Signature & IP Whitelist Bank Mandiri
- Request Body (Contoh):json
{ "customerNo": "89202606260001", "paymentAmount": "1500000.00", "trxDateTime": "2026-06-26 16:20:00", "bankCode": "MANDIRI" } - Response Payload (
200 OK- Success):json{ "responseCode": "2002500", "responseMessage": "Success" } - Response Error (
404 Not Found/ Duplicate Request):json{ "responseCode": "4042514", "responseMessage": "Paid bill" }
POST/dja/activeinquiry_v2- Deskripsi: Internal Simponi Callback dari DJA Kemenkeu untuk pelunasan billing PNBP.
- Autentikasi / Guard: DJA Signature / Basic Auth
- Response Payload (
200 OK- Early Return PNBP+BLU):json{ "simponi_result": { "response_code": "3" } }
4. Bukti Teknis (Technical Proof & Audit Trail)
- Repository:
single-billing-BE - Branch Utama:
development - Merge Request / PR: !232 (Release v1.9.2 — Merged to prod) | Staging: !229
Daftar Commit Terkait (Multi-Commit Trail)
| Commit ID | Branch | Author | Tanggal & Jam | Ringkasan Perubahan |
|---|---|---|---|---|
dc66325 | development | Hermansyah Efendi | 2026-06-26 16:20 | fix(payment): resolve webhook race condition on PNBP+BLU billings |
Cuplikan Kode / Implementasi Inti
// app/controllers/banking/payment_controller.js
// Untuk PNBP+BLU, SIMPONI bisa set fl_paid lebih dulu.
// MANDIRI tetap harus proses karena MANDIRI adalah source of truth untuk BLU.
const isBluBilling = userData.flBlu === true;
if (userData.flPaid && !isBluBilling) {
logging[9] = 'Paid bill'
logging[3] = Date.now() / 1000;
await insertLogRawQuery(logging);
return res.status(status.notfound).json({
responseCode: "4042514",
responseMessage: "Paid bill"
})
}
// Idempotency: cek apakah MANDIRI sudah pernah proses billing ini
const existingMandiriPayment = await billing.paymentSb.findOne({
where: { kode_billing_induk: payload.customerNo }
});
if (existingMandiriPayment) {
logging[9] = 'Duplicate MANDIRI payment request';
logging[3] = Date.now() / 1000;
await insertLogRawQuery(logging);
return res.status(status.notfound).json({
responseCode: "4042514",
responseMessage: "Paid bill"
});
}// app/controllers/pembayaran_controller.js
if (dataSubbilerHdr.fl_pnbp && dataSubbilerHdr.fl_blu && !dataSubbilerHdr.fl_bup) {
logging[3] = Date.now() / 1000;
logging[7] = statusPembayaran;
logging[9] = "PNBP + BLU: Pembayaran diterima dari SIMPONI, tidak dilanjutkan ke Inaport, Sinkarkes, Pengangkut (Akan ditangani oleh BANK Payment)";
logging[11] = false;
logging[13] = true;
await insertLogRawQuery(logging);
sendResponse.simponi_result.response_code = "3";
return res.status(status.success).send({
simponi_result: sendResponse.simponi_result,
});
}5. Bukti Verifikasi Pengujian (QA / Unit Test / API Verification)
A. Verifikasi Unit Test (Development Stage)
- Test Suite File:
tests/models/banking/inquiry_models.test.js,tests/models/banking/payment_models.test.js - Status Eksekusi CLI:
PASSED
RUN v4.1.11 D:/WORK/LNSW/single-billing-BE
✓ tests/models/banking/inquiry_models.test.js (36 tests) 24ms
✓ tests/models/banking/payment_models.test.js (31 tests) 30ms
Test Files 2 passed (2)
Tests 67 passed (67)
Start at 12:12:26
Duration 488ms (transform 163ms, setup 166ms, import 224ms, tests 54ms, environment 0ms)B. Panduan Pengujian QA & Verifikasi Staging (QA Playbook)
- Cakupan / Surface Pengujian:
API / Postman Only (Webhook Payment Callback Simulation) - Kebutuhan Akses / Role Akun: Akses endpoint webhook payment Bank Mandiri & SIMPONI DJA, Token/Header otentikasi signature banking.
- Hasil Pengujian UAT:
PASSED - Catatan QA: Verifikasi konkurensi webhook Mandiri dan Simponi serta idempotensi pembayaran BLU berhasil lolos pengujian UAT.
Skenario Pengujian QA (Test Scenarios)
Skenario 1 — Concurrency Test: Webhook SIMPONI Masuk Lebih Dulu daripada Mandiri (Kasus Utama):
- Pre-kondisi: Terdapat tagihan kombinasi PNBP + BLU terbit (
fl_pnbp = true,fl_blu = true,fl_paid = false). - Trigger / Request Langkah 1: Kirim callback SIMPONI
POST/dja/activeinquiry_v2dengan status sukses pembayaran PNBP. - Ekspektasi Langkah 1: Webhook SIMPONI merespons
response_code: "3", log mencatat bahwa pelunasan akan ditangani Bank Payment, dan flag lunas belum dikirim ke Inaport/Sinkarkes. - Trigger / Request Langkah 2: Kirim callback Bank Mandiri
POST/mandiri/paymentuntuk kode billing induk terkait. - Ekspektasi Langkah 2: Webhook Mandiri TIDAK menolak dengan error
4042514, melainkan sukses memproses pembayaran BLU, mencatat record kebilling.payment_sb, dan mendisposisikan flag lunas lengkap (PNBP + BLU) ke Inaportnet dan Sinkarkes.
- Pre-kondisi: Terdapat tagihan kombinasi PNBP + BLU terbit (
Skenario 2 — Idempotency Test: Callback Berulang Bank Mandiri:
- Pre-kondisi: Webhook Mandiri pada Skenario 1 telah sukses diproses dan tersimpan di
billing.payment_sb. - Trigger / Request: Kirim ulang payload callback Bank Mandiri yang sama untuk kedua kalinya.
- Ekspektasi Response: HTTP
404 Not FounddenganresponseCode: "4042514"("Paid bill") dan pesan log "Duplicate MANDIRI payment request", membuktikan sistem kebal terhadap duplikasi transaksi.
- Pre-kondisi: Webhook Mandiri pada Skenario 1 telah sukses diproses dan tersimpan di
Skenario 3 — Regression Test: Pembayaran Tagihan PNBP Murni (Non-BLU):
- Pre-kondisi: Tagihan hanya memiliki komponen PNBP (
fl_pnbp = true,fl_blu = false). - Trigger / Request: Kirim callback SIMPONI
POST/dja/activeinquiry_v2. - Ekspektasi Response: SIMPONI langsung mengupdate
fl_paid = true, men-trigger disposisi flag lunas ke downstream service seperti alur reguler.
- Pre-kondisi: Tagihan hanya memiliki komponen PNBP (
Bukti Response Verifikasi Staging (Diisi saat UAT)
{
"status": "PASSED",
"note": "Pengujian verifikasi UAT dan validasi integrasi berhasil lolos, rilis production v1.9.2 telah live."
}