Skip to content

[TASK-NON-BLOCKING-BILLING-DISPATCH-AND-RETRY-CONFIG] Isolasi Error Dispatcher Downstream INAPORT/SINKARKES Menggunakan Promise.allSettled dan Konfigurasi Percobaan Ulang Dinamis ​

Modul: Single Billing | Tipe: Bugfix | Stage Saat Ini: PRODUCTION | Tanggal Dokumen: 2026-10-13

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.


1. Informasi Tugas & Referensi Bisnis ​

  • Task ID / Nomor Referensi: TASK-NON-BLOCKING-BILLING-DISPATCH-AND-RETRY-CONFIG

  • Sumber Instruksi: CHANGELOG.md (Release v1.9.2) & Internal Hardening

  • Dokumen Referensi / Lampiran: Tidak ada

  • Task / Dependensi Terkait (Cross-Service): Tidak ada

  • Modul Terkait: Single Billing

  • Service / Repository: single-billing-BE

  • PIC Pengembang: Hermansyah Efendi

  • Latar Belakang & Ringkasan Kebutuhan: Pada alur penerbitan single billing SIMPONI, kegagalan transmisi ke downstream pihak ketiga (INAPORT dan SINKARKES) sebelumnya menggunakan Promise.all sehingga me-reject seluruh proses rekap tagihan meskipun kode billing SIMPONI telah berhasil terbit. Selain itu, nilai maksimum percobaan/retry ke downstream masih bersifat hardcoded (5 untuk Inaport dan 3 untuk Sinkarkes) sehingga menyulitkan penyesuaian latensi di environment non-produksi, serta ditemukan transitive security vulnerability akibat dependensi nodemon. Oleh karena itu, dilakukan isolasi dispatcher dengan Promise.allSettled, penambahan konfigurasi retry dinamis via environment variable, pembersihan dependensi dev, serta rilis versi v1.9.2.

  • Ruang Lingkup & Rincian Sub-Pekerjaan (Technical Scope & Milestones):

    • [x] Penyelarasan payload INAPORT agar menggunakan kode billing induk pada skenario single billing terpadu (d800d37)
    • [x] Isolasi error dispatcher INAPORT dan SINKARKES dengan safe wrapper dan Promise.allSettled pada orchestrator tagihan (1ba23df)
    • [x] Penyertaan catatan kegagalan parsial pada response pengirimanDataBilling tanpa merusak status HTTP 200 (1ba23df)
    • [x] Pembersihan dependensi nodemon dan mitigasi vulnerability via npm audit fix (d2a90a6, c539815)
    • [x] Bump versi rilis v1.9.2 pada package.json, package-lock.json, dan CHANGELOG.md (ce44197)
    • [x] Penambahan konfigurasi dinamis INAPORT_MAX_ATTEMPTS dan SINKARKES_MAX_RETRIES via skema ENV_SCHEMA (ed0ebd8)
    • [x] Penambahan unit test Vitest untuk safe dispatching, retry env, dan validator skema (1ba23df, ed0ebd8)

2. Deployment Lifecycle ​

#EnvironmentTanggal & WaktuPICStatusCatatan
1Development2026-10-13 10:57Hermansyah EfendiDEPLOYEDBranch: development
2Staging (UAT)2026-10-14 13:00Hermansyah Efendi / QADEPLOYEDRelease Candidate via MR Staging !231. 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-10-13DevelopmentInisialisasi draft audit log isolasi error downstream billing dispatcher dan konfigurasi retry dinamis (release v1.9.2)Hermansyah Efendi

3. Analisis Teknis & Rincian Perubahan ​

  • Alur Logika & Bisnis:

    1. Isolasi Kegagalan Downstream (dispatchBillingNotifications): Memisahkan pemanggilan transmisi downstream safePostBillingInaport dan safePostBillingSinkarkes menggunakan Promise.allSettled. Jika salah satu atau kedua target eksternal mengalami timeout atau error jaringan, promise tidak melempar rejection yang membatalkan transaksi utama, sehingga status rekap tagihan dan kode billing SIMPONI yang telah terbit tetap tersimpan serta dikembalikan ke pemanggil (caller).
    2. Observabilitas Status Pengiriman: Objek response pengirimanDataBilling menyertakan indikator boolean toInaport dan toSinkarkes. Apabila ada target yang gagal terkirim, response secara transparan menyertakan pesan pada field catatan (contoh: "Pengiriman data billing ke INAPORT gagal") dengan tetap mempertahankan status HTTP 200 OK.
    3. Mapping Kode Billing Induk INAPORT: Memastikan payload yang dikirimkan ke INAPORT pada skenario single billing terpadu menggunakan kode billing induk SIMPONI hasil generate orkestrator, menjaga konsistensi identifikasi pembayaran lintas platform.
    4. Konfigurabilitas Percobaan Ulang Dinamis (Environment Configuration): Menghilangkan nilai konstan hardcoded (maxAttempts = 5 di Inaport dan maxRetries = 3 di Sinkarkes). Transmisi ke downstream kini dikendalikan secara dinamis via variabel lingkungan:
      • INAPORT_MAX_ATTEMPTS: Mengatur batas maksimal iterasi percobaan request ke INAPORT (fungsi loginAsINAPORT, sendBillingInaport, dan sendFlagLunasInaport) dengan nilai default 5.
      • SINKARKES_MAX_RETRIES: Mengatur batas maksimal pengulangan (retry count) ke SINKARKES (fungsi sendBillingSinkarkes dan sendFlagLunasSinkarkes) dengan nilai default 3.
      • Defensive Type Parsing: Helper getMaxAttempts dan getMaxRetries mengimplementasikan fungsi ekstraksi defensif parseInt(..., 10). Apabila nilai variabel lingkungan tidak didefinisikan, bernilai bukan integer, atau <= 0, helper secara otomatis melakukan fallback ke nilai default (5 untuk Inaport, 3 untuk Sinkarkes).
      • Skema Validasi Lingkungan (ENV_SCHEMA): Kedua variabel didaftarkan pada app/utils/env-validator.js dengan opsi non-blocking { required: false, default: "5" } dan { required: false, default: "3" }, menjamin tersedianya nilai fallback yang aman tanpa memicu exception startup aplikasi.
    5. Hardening Keamanan & Dependensi: Menghapus dependensi nodemon yang menyisakan import tak terpakai pada model tracking, menyelesaikan kerentanan transitive dependencies melalui npm audit fix, serta penyesuaian versi rilis v1.9.2.
  • Rincian Konfigurasi Environment Variable:

    Nama VariabelTipe DataStatusDefault (Prod)Rekomendasi StagingKonsumen / HelperDeskripsi & Dampak Operasional
    INAPORT_MAX_ATTEMPTSIntegerOpsional (required: false)"5""1"app/helpers/inaport.js (getMaxAttempts)Batas maksimal perulangan request otentikasi login, penerbitan tagihan, dan flag lunas ke service INAPORT. Pada Staging diset 1 agar fail-fast tanpa overhead retry perulangan.
    SINKARKES_MAX_RETRIESIntegerOpsional (required: false)"3""1"app/helpers/sinkarkes.js (getMaxRetries)Batas maksimal retry loop pengiriman data tagihan dan flag lunas ke service SINKARKES saat terjadi timeout/downstream down. Pada Staging diset 1 (nilai integer positif minimal).
  • Perubahan Database (DDL / DML):

    sql
    -- Tidak ada perubahan skema database (DDL/DML) untuk task ini.
    -- Perubahan murni pada layer service logic (tagihan_helper), helper dispatcher downstream (inaport/sinkarkes), dan konfigurasi runtime schema.
  • Kontrak API & Response Payload:

    POST /api/v2/terima-rekap-tagihan & POST /api/v3/terima-rekap-tagihan ​

    • Deskripsi: Endpoint orkestrasi rekap data tagihan dan penerbitan single billing ke SIMPONI, INAPORT, dan SINKARKES.
    • Autentikasi / Guard: Bearer Token (authMiddleware)
    • Request Header:
      • Authorization: Bearer <token>
      • Content-Type: application/json
    • Request Body: Sesuai kontrak tagihan eksisting (multi-satker / single satker).
    • Response Payload Sukses Penuh (200 OK):
      json
      {
        "status": 200,
        "kode_billing": {
          "kodeBillingPnbp": "820261012000001"
        },
        "pengirimanDataBilling": {
          "toInaport": true,
          "toSinkarkes": true
        },
        "message": "Berhasil merekap tagihan INSW dan meminta kode billing ke SIMPONI"
      }
    • Response Payload Sukses dengan Kegagalan Parsial Downstream (200 OK):
      json
      {
        "status": 200,
        "kode_billing": {
          "kodeBillingPnbp": "820261012000001"
        },
        "pengirimanDataBilling": {
          "toInaport": false,
          "toSinkarkes": true,
          "catatan": "Pengiriman data billing ke INAPORT gagal"
        },
        "message": "Berhasil merekap tagihan INSW dan meminta kode billing ke SIMPONI"
      }

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: !231

Daftar Commit Terkait (Multi-Commit Trail) ​

Commit IDBranchAuthorTanggal & JamRingkasan Perubahan
ce44197ff85514430e9f8f3414ce10d708f937eddevelopmentHermansyah Efendi2026-10-13 10:57chore(release): v1.9.2
ed0ebd8ac1a080a05ba3b806beb23f853c6fb368developmentHermansyah Efendi2026-10-12 20:41feat(helpers): make inaport and sinkarkes retry counts configurable via env
c539815ecbe9f9a516a055697bb73b2526a9ef09developmentHermansyah Efendi2026-10-12 19:02fix(tracking): remove unused nodemon import
2c62c0a4513cd6fa0f4a6bc69976eb440c73fdb6developmentHermansyah Efendi2026-10-12 18:29chore(release): v1.9.1 (superseded by v1.9.2)
d2a90a680a1e571da9ebca7bcea5423da470e893developmentHermansyah Efendi2026-10-12 18:25chore(deps): remove nodemon and resolve security audit vulnerabilities
1ba23df1774402f5f205ea428d45f9408f1201bedevelopmentHermansyah Efendi2026-10-12 18:25fix(tagihan): prevent inaport and sinkarkes dispatch errors from stopping orchestrator
d800d3739d289fc79be307352f7f3c4c9dea6e9cdevelopmentHermansyah Efendi2026-09-30 19:27fix(billing): use parent billing code for inaport payload

Cuplikan Kode / Implementasi Inti ​

javascript
// Cuplikan safe dispatcher pada app/helpers/tagihan_helper.js
const dispatchBillingNotifications = async (payloadInaport, payloadSinkarkes, trxIdInsw) => {
  const [inaportSettled, sinkarkesSettled] = await Promise.allSettled([
    safePostBillingInaport(payloadInaport, trxIdInsw),
    safePostBillingSinkarkes(payloadSinkarkes, trxIdInsw),
  ]);

  const sendToInaport = inaportSettled.status === "fulfilled" ? inaportSettled.value : false;
  const sendToSinkarkes = sinkarkesSettled.status === "fulfilled" ? sinkarkesSettled.value : false;

  if (sendToInaport) {
    await updateFlInaport(trxIdInsw);
  }
  if (sendToSinkarkes) {
    await updateFlSinkarkes(trxIdInsw);
  }

  const pengirimanDataBilling = {
    toInaport: sendToInaport,
    toSinkarkes: sendToSinkarkes,
  };

  const failedTargets = [];
  if (!sendToInaport) failedTargets.push("INAPORT");
  if (!sendToSinkarkes) failedTargets.push("SINKARKES");

  if (failedTargets.length > 0) {
    pengirimanDataBilling.catatan = `Pengiriman data billing ke ${failedTargets.join(" dan ")} gagal`;
    logger.warn("Dispatched billing notifications with partial failure", {
      trxIdInsw,
      failedTargets,
      toInaport: sendToInaport,
      toSinkarkes: sendToSinkarkes,
    });
  }

  return {
    sendToInaport,
    sendToSinkarkes,
    pengirimanDataBilling,
  };
};
javascript
// Cuplikan parsing defensif INAPORT_MAX_ATTEMPTS pada app/helpers/inaport.js
const getMaxAttempts = () => {
  const parsed = parseInt(process.env.INAPORT_MAX_ATTEMPTS, 10);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : 5;
};
javascript
// Cuplikan parsing defensif SINKARKES_MAX_RETRIES pada app/helpers/sinkarkes.js
const getMaxRetries = () => {
  const parsed = parseInt(process.env.SINKARKES_MAX_RETRIES, 10);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : 3;
};
javascript
// Cuplikan registrasi skema default pada app/utils/env-validator.js
export const ENV_SCHEMA = {
  // ...
  INAPORT: { required: true },
  INAPORT_API: { required: true },
  INAPORT_MAX_ATTEMPTS: { required: false, default: "5" },
  // ...
  SINKARKES_API: { required: true },
  SINKARKES_MAX_RETRIES: { required: false, default: "3" },
  // ...
};

5. Bukti Verifikasi Pengujian (QA / Unit Test / API Verification) ​

A. Verifikasi Unit Test (Development Stage) ​

  • Test Suite File: tests/helpers/tagihan_helper.test.js, tests/helpers/sinkarkes.test.js, tests/utils/env-validator.test.js
  • Status Eksekusi CLI: PASSED
javascript
// Cuplikan dari tests/helpers/tagihan_helper.test.js
it("should continue successfully with partial warning when inaport dispatch fails", async () => {
  mockPostBillingInaport.mockRejectedValue(new Error("Inaport downstream timeout"));
  mockPostBillingSinkarkes.mockResolvedValue(true);

  const res = createMockResponse();
  await processRekapTagihanOrchestrator(validReq, res, next, "v3");

  expect(res.statusCode).toBe(200);
  expect(res.body.pengirimanDataBilling.toInaport).toBe(false);
  expect(res.body.pengirimanDataBilling.toSinkarkes).toBe(true);
  expect(res.body.pengirimanDataBilling.catatan).toContain("INAPORT gagal");
});

// Cuplikan verifikasi skema default dari tests/utils/env-validator.test.js
it("should have INAPORT_MAX_ATTEMPTS and SINKARKES_MAX_RETRIES configured with default values", () => {
  expect(ENV_SCHEMA.INAPORT_MAX_ATTEMPTS).toBeDefined();
  expect(ENV_SCHEMA.INAPORT_MAX_ATTEMPTS.required).toBe(false);
  expect(ENV_SCHEMA.INAPORT_MAX_ATTEMPTS.default).toBe("5");

  expect(ENV_SCHEMA.SINKARKES_MAX_RETRIES).toBeDefined();
  expect(ENV_SCHEMA.SINKARKES_MAX_RETRIES.required).toBe(false);
  expect(ENV_SCHEMA.SINKARKES_MAX_RETRIES.default).toBe("3");
});
text
 RUN  v4.1.11 D:/WORK/LNSW/single-billing-BE

 ✓ tests/utils/env-validator.test.js (11 tests) 21ms
 ✓ tests/helpers/sinkarkes.test.js (17 tests) 28ms
 ✓ tests/helpers/tagihan_helper.test.js (17 tests) 50ms

 Test Files  3 passed (3)
      Tests  45 passed (45)
   Start at  10:43:58
   Duration  561ms (transform 396ms, setup 255ms, import 400ms, tests 98ms, environment 0ms)

B. Panduan Pengujian QA & Verifikasi Staging (QA Playbook) ​

Diisi saat development sebagai panduan eksekusi pengujian QA, kemudian dilengkapi bukti response JSON saat verifikasi di Staging.

  • Cakupan / Surface Pengujian: API / Postman Only & B2B Webhook + UI Verification
  • Kebutuhan Akses / Role Akun: Token Bearer Pengangkut / Akses Database Single Billing (tabel billing & tracking)
  • Hasil Pengujian UAT: PASSED
  • Catatan QA: Pengujian verifikasi end-to-end UAT dan isolasi downstream dispatcher telah lolos.
Skenario Pengujian QA (Test Scenarios) ​
  1. Skenario 1 — Happy Path Penerbitan Billing Terpadu:

    • Pre-kondisi: Data tagihan warta valid dan belum pernah diterbitkan billing. Service SIMPONI, INAPORT, dan SINKARKES dapat diakses normal.
    • Trigger / Request: POST /api/v3/terima-rekap-tagihan dengan payload rekap valid.
    • Ekspektasi Response: HTTP 200 OK, kode_billing terisi, pengirimanDataBilling.toInaport bernilai true, pengirimanDataBilling.toSinkarkes bernilai true, tanpa field catatan.
    • Verifikasi Database / Log: Kolom fl_inaport dan fl_sinkarkes pada tabel billing bernilai 1/true.
  2. Skenario 2 — Partial Downstream Failure (INAPORT Down):

    • Pre-kondisi: Endpoint mock/staging INAPORT dimatikan atau disimulasikan timeout, sementara SIMPONI dan SINKARKES normal.
    • Trigger / Request: POST /api/v3/terima-rekap-tagihan dengan payload rekap valid.
    • Ekspektasi Response: HTTP 200 OK, kode billing SIMPONI tetap terbit, pengirimanDataBilling.toInaport bernilai false, pengirimanDataBilling.catatan berbunyi "Pengiriman data billing ke INAPORT gagal".
    • Verifikasi Database / Log: Log Winston/Loki mencatat level warn (Dispatched billing notifications with partial failure), tracking record 214 tercatat pada tabel tracking.
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