Skip to content

[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:

  1. 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.
  2. Update baris Staging saat naik ke UAT. Ubah "Stage Saat Ini" di header menjadi STAGING.
  3. Update baris Production saat naik ke Prod. Ubah "Stage Saat Ini" di header menjadi PRODUCTION.
  4. Update entry di README.md setiap kali status berubah. -->

1. Informasi Tugas & Referensi Bisnis ​

  • Task ID / Nomor Referensi: FIX-PAYMENT-WEBHOOK-RACE-CONDITION

  • Sumber Instruksi: Investigasi Masalah Pelunasan Tagihan Kombinasi PNBP + BLU & Sinkronisasi Mandiri-Simponi

  • Dokumen Referensi / Lampiran: -

  • Task / Dependensi Terkait (Cross-Service): FEAT-SIMPONI-V3-ISOLATION

  • Modul Terkait: Single Billing

  • Service / Repository: single-billing-BE

  • PIC 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 di pym.reqPaymentVAMandiri menolak 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 guard fl_paid umum pada Mandiri jika tagihan memiliki komponen BLU dengan pengaman idempotensi berbasis model billing.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.flPaid pada 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.paymentSb untuk 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 mengubah fl_paid dan tanpa mengirim flag lunas prematur ke Inaportnet, Sinkarkes, dan Pengangkut
    • [x] Penghapusan blok kode legacy redundan pada pembayaran_controller.js yang sebelumnya memproses settlement BLU secara prematur di webhook SIMPONI (-54 baris)
    • [x] Pendefinisian skema model Sequelize billing.paymentSb untuk memetakan tabel billing.payment_sb pada app/models/banking/inquiry_models.js

2. Deployment Lifecycle ​

#EnvironmentTanggal & WaktuPICStatusCatatan
1Development2026-06-26 16:20Hermansyah EfendiDEPLOYEDBranch: development
2Staging (UAT)2026-10-14 13:00Hermansyah Efendi / QADEPLOYEDRelease 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]
3Production2026-10-14 20:06achmad fauziDEPLOYEDBranch: 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 ​

TanggalEnvironmentPerubahanPIC
2026-10-14ProductionStatus diupdate ke PRODUCTION (DEPLOYED, CLOSED), rilis Release v1.9.2 via MR !232Hermansyah Efendi
2026-09-07DevelopmentInisialisasi 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) di app/controllers/banking/payment_controller.js diubah menjadi if (userData.flPaid && !isBluBilling). Jika tagihan mengandung komponen BLU (userData.flBlu === true), controller Mandiri tetap memproses pembayaran meskipun SIMPONI telah mengeset status fl_paid terlebih dahulu.
    • Pengecekan Idempotensi (Idempotency Guard): Untuk mencegah eksekusi ganda jika Bank Mandiri melakukan retry callback, sistem memeriksa keberadaan kode_billing_induk di tabel billing.payment_sb melalui model billing.paymentSb. Jika record pembayaran Mandiri sudah pernah tercatat, request ditolak dengan kode 4042514 ("Paid bill").
    • Isolasi Alur Callback SIMPONI (Early Return): Pada webhook SIMPONI activeinquiry_v2 (app/controllers/pembayaran_controller.js), jika terdeteksi dataSubbilerHdr.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 sukses 3 ke 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 — bypass fl_paid untuk BLU dan penambahan cek idempotensi via billing.paymentSb)
      • app/controllers/pembayaran_controller.js (Controller — early return PNBP+BLU pada activeinquiry_v2 dan penghapusan kode legacy settlement)
      • app/models/banking/inquiry_models.js (Model — pendefinisian model Sequelize billing.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):

    javascript
    billing.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 IDBranchAuthorTanggal & JamRingkasan Perubahan
dc66325developmentHermansyah Efendi2026-06-26 16:20fix(payment): resolve webhook race condition on PNBP+BLU billings

Cuplikan Kode / Implementasi Inti ​

javascript
// 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"
    });
}
javascript
// 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
text
 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) ​
  1. 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_v2 dengan 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/payment untuk kode billing induk terkait.
    • Ekspektasi Langkah 2: Webhook Mandiri TIDAK menolak dengan error 4042514, melainkan sukses memproses pembayaran BLU, mencatat record ke billing.payment_sb, dan mendisposisikan flag lunas lengkap (PNBP + BLU) ke Inaportnet dan Sinkarkes.
  2. 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 Found dengan responseCode: "4042514" ("Paid bill") dan pesan log "Duplicate MANDIRI payment request", membuktikan sistem kebal terhadap duplikasi transaksi.
  3. 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.
Bukti Response Verifikasi Staging (Diisi saat UAT) ​
json
{
  "status": "PASSED",
  "note": "Pengujian verifikasi UAT dan validasi integrasi berhasil lolos, rilis production v1.9.2 telah live."
}

Single Source of Truth (SSOT) Arsitektur & Perubahan Teknis Backend