Skip to content

[TASK-SPLIT-BILLING-202608] Implementasi Unified Split Billing dan Bypass Tagihan K/L Modul Pengangkut Kapal

Modul: Pengangkut | Tipe: Feature | Stage Saat Ini: DEVELOPMENT | Tanggal Dokumen: 2026-08-26


1. Informasi Tugas & Referensi Bisnis

  • Task ID / Nomor Referensi: TASK-SPLIT-BILLING-202608

  • Sumber Instruksi: ND-256/LNSW.34/2026 & WA-20260824-split-billing

  • Dokumen Referensi / Lampiran: add_status_tagihan_split_billing.sql

  • Task / Dependensi Terkait (Cross-Service): TASK-SET-EXPIRED-BILLING-V3 — Integrasi API Outbound Set Expired Billing V3 saat void tagihan

  • Modul Terkait: Pengangkut

  • Service / Repository: pengangkut-backend

  • PIC Pengembang: Hermansyah Efendi

  • Latar Belakang & Ringkasan Kebutuhan: Implementasi arsitektur Unified Split Billing dan mekanisme Bypass Tagihan K/L untuk pengangkut kapal. Memungkinkan admin memproses tagihan K/L yang dibayar secara mandiri/terpisah (bypass) sebelum kode billing Simponi terbit (Pre-Billing Bypass) maupun melakukan pembatalan dan penerbitan ulang billing setelah billing terbentuk (Post-Billing Void-Reset) secara atomik dalam satu transaksi database, terintegrasi dengan pemanggilan API Layanan Single Billing POST /api/v3/set-expired-billing (idempotent), aman, serta selaras dengan validasi kelayakan (eligibility) dan kalkulasi inquiry Single Billing.

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

    • [x] Milestone 1: Database Migration & Schema Definition (Penambahan ENUM enum_status_tagihan, kolom status_tagihan, alasan_split, updated_by, updated_date pada pengangkut.tagihan_hdr, backfill data lama, serta index performa).
    • [x] Milestone 2: Middleware & Security Guard (Implementasi middleware requireAdmin untuk memverifikasi role admin via JWT token groupGa 00, 07, 20).
    • [x] Milestone 3: Core Service Logic Split Billing (Implementasi SplitBillingService dengan dukungan Pre-Billing Bypass dan Post-Billing Void-Reset secara atomik dalam 1 database transaction).
    • [x] Milestone 4: Single Billing V3 Integration & Query Optimization (Integrasi pemanggilan API Layanan Single Billing POST /api/v3/set-expired-billing saat void tagihan dengan penanganan idempotent already-expired, serta optimasi kueri checkDataBilling menggunakan WHERE no_aju_layanan = $1).
    • [x] Milestone 5: Integration with Eligibility & Inquiry (Pembaruan BillingEligibilityService dan InquiryBillingService untuk mengenali status BYPASSED, pemisahan array response draft_tagihan hanya untuk ACTIVE dan bypassed_tagihan untuk BYPASSED beserta metadata split).
    • [x] Milestone 6: Simponi Payload Generator Update (Penyelarasan payloadSingleBillingV2 dan payloadSingleBillingV3 agar hanya memproses tagihan berstatus ACTIVE).
    • [x] Milestone 7: API Controller & Strict Zod Validation (Penyediaan endpoint POST /v2/billing/split-billing dan validasi skema Zod SplitBillingSchema dengan tipe array positive integer id_tagihan_hdr & prefix validasi 102).
    • [x] Milestone 8: Unit Testing & Verification (Pembuatan 1-to-1 Vitest unit test suites dengan 100% code coverage pada modul split billing, client API, controller, validation, dan inquiry billing).

2. Deployment Lifecycle

#EnvironmentTanggal & WaktuPICStatusCatatan
1Development2026-08-26 16:30Hermansyah EfendiDEPLOYEDBranch: feat/split-billing
2Staging (UAT)PENDING
3ProductionPENDING

Riwayat Update Log

TanggalEnvironmentPerubahanPIC
2026-08-27DevelopmentPemisahan array draft_tagihan (ACTIVE) dan bypassed_tagihan (BYPASSED) pada response endpoint inquiry billing (InquiryBillingService)Hermansyah Efendi
2026-08-27DevelopmentIntegrasi Single Billing API v3 set-expired-billing saat void tagihan, penanganan idempotent expired, optimasi kueri checkDataBilling, dan update test suites (commit 6e94a402, 16b2bcda)Hermansyah Efendi
2026-08-26DevelopmentInisialisasi draft audit log dari context block backend (commit d97fcdcc)Hermansyah Efendi

3. Analisis Teknis & Rincian Perubahan

  • Alur Logika & Bisnis:

    • Unified Split Billing Entry-Point (SplitBillingService.splitBilling): Memvalidasi nomor pengajuan layanan wajib ber-prefix 102 (keberangkatan), me-resolve pasangan nomor aju kedatangan (101...) 1x di entry-point, dan merutekan otomatis ke bypassTagihan (Pre-Billing) jika belum ada billing aktif, atau ke voidAndResetTagihan (Post-Billing) jika kode billing aktif sudah terbit.
    • Integrasi Single Billing V3 Set-Expired (voidAndResetTagihan): Mengambil trx_id_billing dari header tagihan aktif dan memanggil billingApi.setExpiredBillingV3({ trx_id_insw: trxIdBilling, alasan }) untuk menandai status expired pada Layanan Single Billing. Diterapkan penanganan idempotent (jika respon menyatakan status sudah expired, proses void lokal tetap dilanjutkan) serta rollback transaksi database atomik jika terjadi failure API lainnya.
    • Optimalisasi Kueri Single Billing (checkDataBilling): Mengganti operator ILIKE '%' || $1 || '%' menjadi perbandingan eksak WHERE no_aju_layanan = $1 untuk mencegah false match/collision dan mengoptimalkan performa Index Scan.
    • Eligibility & Inquiry Array Separation: Tagihan berstatus BYPASSED dihitung memenuhi syarat kelayakan (completed), diecualikan dari total nominal billing Simponi. Response endpoint inquiry billing memisahkan struktur data menjadi dua array: draft_tagihan (hanya menampung tagihan ACTIVE yang masuk ke billing Simponi) dan bypassed_tagihan (menampung tagihan BYPASSED lengkap dengan metadata alasan_split, bypassed_by, dan bypassed_at).
    • Admin Guard & Strict Validation: Endpoint split billing diproteksi oleh middleware requireAdmin (verifikasi token groupGa 00, 07, 20) dan divalidasi oleh Zod SplitBillingSchema (id_tagihan_hdr array positive integer).
    • Simponi Payload Filtering: Filter payloadSingleBillingV2 dan payloadSingleBillingV3 disesuaikan agar hanya menyertakan tagihan berstatus ACTIVE.
    • Daftar File yang Berubah:
      • migration/add_status_tagihan_split_billing.sql [DDL/DML]
      • app/constant/billing.constant.js [Constant]
      • app/schemas/tagihan-hdr.schema.js [Schema / Model]
      • app/schemas/tagihan-dtl.schema.js [Schema / Model]
      • app/models/billing_models.js [Model / SQL Helper]
      • app/api/billing.api.js [API Client / Outbound HTTP]
      • app/middlewares/require-admin.js [Middleware]
      • app/validations/billing.validation.js [Validation]
      • app/services/split-billing.service.js [Service / Business Logic]
      • app/services/billing-eligibility.service.js [Service / Business Logic]
      • app/services/inquiry-billing.service.js [Service / Business Logic]
      • app/services/request-billing.service.js [Service / Business Logic]
      • app/controllers/v2/billing.controller.js [Controller / API Route]
      • app/routers/billing_route.js [Controller / API Route]
      • tests/controllers/v2/billing.controller.test.js [Unit Test]
      • tests/api/billing.api.test.js [Unit Test]
      • tests/middlewares/require-admin.test.js [Unit Test]
      • tests/services/split-billing.service.test.js [Unit Test]
      • tests/services/billing-eligibility.service.test.js [Unit Test]
      • tests/services/inquiry-billing.service.test.js [Unit Test]
      • tests/services/request-billing.service.test.js [Unit Test]
      • tests/validations/billing.validation.test.js [Unit Test]
      • CHANGELOG.md [Documentation]
  • Perubahan Database (DDL / DML):

    sql
    -- Step 1: Buat ENUM Type untuk status_tagihan (Strict Type)
    DO $$
    BEGIN
      IF NOT EXISTS (
        SELECT 1 FROM pg_type t 
        JOIN pg_namespace n ON n.oid = t.typnamespace 
        WHERE t.typname = 'enum_status_tagihan' AND n.nspname = 'pengangkut'
      ) THEN
        CREATE TYPE pengangkut.enum_status_tagihan AS ENUM ('ACTIVE', 'BYPASSED', 'VOIDED');
      END IF;
    END$$;
    
    -- Step 2: Tambah kolom baru ke tagihan_hdr menggunakan ENUM type
    ALTER TABLE pengangkut.tagihan_hdr
      ADD COLUMN IF NOT EXISTS status_tagihan pengangkut.enum_status_tagihan DEFAULT 'ACTIVE',
      ADD COLUMN IF NOT EXISTS updated_by     varchar(50)  NULL,
      ADD COLUMN IF NOT EXISTS updated_date   timestamptz(0) NULL,
      ADD COLUMN IF NOT EXISTS alasan_split   text         NULL;
    
    -- Step 3: Backfill data lama agar tidak NULL
    UPDATE pengangkut.tagihan_hdr
    SET status_tagihan = 'ACTIVE'
    WHERE status_tagihan IS NULL;
    
    -- Step 4: Index performa untuk query lookup split billing
    CREATE INDEX IF NOT EXISTS idx_tagihan_hdr_trx_status 
    ON pengangkut.tagihan_hdr (trx_id_insw, status_tagihan);
  • Kontrak API & Response Payload:

    1. Endpoint Split Billing (Admin Action)

    POST /v2/billing/split-billing
    • Deskripsi: Endpoint terpadu untuk memisahkan/melewati (bypass) tagihan K/L tertentu dari alur billing kolektif. Backend secara otomatis mendeteksi apakah transaksi berada pada fase Pre-Billing atau Post-Billing.
    • Autentikasi / Security Guard: Bearer Token JWT (Wajib lolos doIntrospection dan requireAdmin dengan groupGa 00, 07, atau 20).
    • Request Header / Content-Type:
      • Authorization: Bearer <token>
      • Content-Type: application/json
    A. Skema Validasi Request Payload (Zod Schema)
    FieldTipe DataWajib/OpsionalAturan Validasi / Keterangan
    service_numberStringWajibNomor pengajuan layanan keberangkatan (Wajib diawali prefix 102).
    kode_klStringKondisionalEnum: "022" (Inaportnet) atau "024" (Sinkarkes).
    id_tagihan_hdrArray<Number>KondisionalArray positive integers daftar ID header tagihan spesifik yang ingin di-bypass (misal: [105]).
    alasan_splitStringWajibKeterangan/alasan pengajuan split (Minimal 5 karakter).

    Aturan Refinement: Request wajib menyertakan minimal salah satu dari kode_kl ATAU id_tagihan_hdr.

    Contoh Payload Request:

    json
    {
      "service_number": "10202408260001",
      "kode_kl": "024",
      "id_tagihan_hdr": [105],
      "alasan_split": "K/L Sinkarkes melakukan pembayaran mandiri di instansi"
    }
    B. Kondisi 1: Pre-Billing (Saat Kode Billing BELUM Terbit)
    • Trigger: Belum ada kode billing aktif pada billing.subbiller_hdr untuk nomor aju ini.
    • Aksi Internal: Mengubah status record tagihan_hdr eksisting menjadi BYPASSED (atau membuat header placeholder BYPASSED jika tagihan K/L belum terkirim sama sekali).
    • HTTP Status: 200 OK
    • Response Payload:
      json
      {
        "data": {
          "stage": "PRE_BILLING",
          "message": "Split billing tagihan K/L berhasil dilakukan (Pre-Billing)",
          "service_number": "10202408260001",
          "kode_kl": "024",
          "status_tagihan": "BYPASSED",
          "alasan_split": "K/L Sinkarkes melakukan pembayaran mandiri di instansi",
          "updated_by": "admin-user",
          "affected_headers": [
            {
              "id_tagihan_hdr": 105,
              "kode_kl": "024",
              "status_tagihan": "BYPASSED"
            }
          ]
        },
        "message": "Split billing tagihan K/L berhasil dilakukan (Pre-Billing)",
        "statusCode": 200,
        "success": true
      }
    C. Kondisi 2: Post-Billing (Saat Kode Billing SUDAH Terbit / Void & Reset)
    • Trigger: Kode billing Simponi sudah terbit (subbiller_hdr.kode_billing_induk ada & belum expired), namun belum dibayar (fl_paid = false).
    • Aksi Internal (Atomic DB Transaction & External Outbound):
      1. Memanggil Single Billing API POST /api/v3/set-expired-billing menggunakan trx_id_billing tagihan aktif.
      2. Menangani respon idempotent jika billing sudah berstatus expired.
      3. Void seluruh tagihan aktif dalam batch transaksi (status_tagihan = 'VOIDED').
      4. Duplikasi data header & detail tagihan baru.
      5. Tagihan target yang di-split diset status_tagihan = 'BYPASSED'.
      6. Tagihan lainnya diset status_tagihan = 'ACTIVE' dengan fl_billing = false agar siap di-generate ulang kode billing Simponi barunya.
    • HTTP Status: 200 OK
    • Response Payload:
      json
      {
        "data": {
          "stage": "POST_BILLING",
          "message": "Tagihan aktif berhasil di-void dan di-reset. Silakan lakukan generate billing ulang.",
          "voided_count": 2,
          "reset_tagihan": [
            {
              "id_tagihan_hdr_lama": 101,
              "status_tagihan": "ACTIVE",
              "hdr": { "id_tagihan_hdr": 201, "status_tagihan": "ACTIVE" },
              "dtl_count": 1
            },
            {
              "id_tagihan_hdr_lama": 102,
              "status_tagihan": "BYPASSED",
              "hdr": { "id_tagihan_hdr": 202, "status_tagihan": "BYPASSED" },
              "dtl_count": 1
            }
          ]
        },
        "message": "Tagihan aktif berhasil di-void dan di-reset. Silakan lakukan generate billing ulang.",
        "statusCode": 200,
        "success": true
      }
    D. Penanganan Error & Business Guards
    • Tagihan Sudah Dibayar (fl_payment = true):
      json
      {
        "data": null,
        "message": "Void tidak dapat dilakukan. Terdapat tagihan yang sudah dibayarkan dalam batch ini.",
        "statusCode": 400,
        "success": false,
        "errors": []
      }
    • Validasi Nomor Aju (Prefix Non-102):
      json
      {
        "data": null,
        "message": "Split billing hanya dapat dilakukan menggunakan nomor aju keberangkatan",
        "statusCode": 400,
        "success": false,
        "errors": []
      }
    • Tagihan Target Tidak Ditemukan:
      json
      {
        "data": null,
        "message": "id_tagihan_hdr 999 tidak ditemukan dalam daftar tagihan aktif transaksi ini.",
        "statusCode": 400,
        "success": false,
        "errors": []
      }
    • Akses Ditolak (Non-Admin Role):
      json
      {
        "status": 403,
        "error": "Forbidden",
        "error_description": "Endpoint ini hanya dapat diakses oleh Admin"
      }

    2. Integrasi API Eksternal (Outbound to Single Billing)

    POST /api/v3/set-expired-billing
    • Deskripsi: Membatalkan / menandai status expired atas billing induk yang sudah terbit di Layanan Single Billing saat eksekusi void tagihan pada fase Post-Billing.
    • Request Header:
      • Content-Type: application/json
    • Request Body:
      json
      {
        "trx_id_insw": "TRX-BILLING-20260827-001",
        "alasan": "K/L Sinkarkes melakukan pembayaran mandiri di instansi"
      }
    • Penanganan Khusus (Idempotency): Status 400 Bad Request dengan pesan error "Billing sudah dalam status expired" atau "sudah expired" diabaikan (handled idempotently), sehingga alur void dan reset tagihan database lokal tetap dilanjutkan.

    3. Endpoint Inquiry Billing & Eligibility (Updated)

    GET /api/inquiry-billing
    • Deskripsi: Mengambil ringkasan status billing, evaluasi kelayakan (eligibility check), rincian tagihan aktif, serta status split billing dengan pemisahan array tagihan aktif dan bypassed.
    • Autentikasi / Security Guard: Bearer Token JWT (doIntrospection, checkAccessAgentByServiceNumber).
    • Query Parameters:
      • serviceNumber (String, Wajib): Nomor pengajuan layanan (Kedatangan 101... atau Keberangkatan 102...).
    • HTTP Status: 200 OK
    • Response Payload:
      json
      {
        "data": {
          "billing_status": {
            "is_billing": true,
            "is_generated": false,
            "is_expired": false,
            "id_billing": null,
            "is_paid": false,
            "total_tagihan": 50000,
            "is_split_billing": true,
            "payment_detail": null,
            "billing_detail": null
          },
          "eligibility": {
            "is_eligible": true,
            "message": "Semua syarat terpenuhi (Split Billing: Tagihan Sinkarkes)",
            "requirements": {
              "lk3": {
                "required": true,
                "completed": true,
                "name": "LK3",
                "description": "Laporan Kedatangan dan Keberangkatan Kapal"
              },
              "inaport": {
                "required": true,
                "completed": true,
                "status": "ACTIVE",
                "name": "Tagihan Warta",
                "description": "Kementerian Perhubungan"
              },
              "sinkarkes": {
                "required": true,
                "completed": true,
                "status": "BYPASSED",
                "name": "Tagihan Sinkarkes",
                "description": "Kementerian Kesehatan"
              }
            }
          },
          "draft_tagihan": [
            {
              "id_tagihan_hdr": 101,
              "trx_id_insw": "10202408260001",
              "jenis_pengajuan": "KEBERANGKATAN",
              "kode_kl": "022",
              "uraian_kl": "KEMENTERIAN PERHUBUNGAN",
              "uraian_satker": "Kantor Distrik Navigasi Palembang",
              "jenis_satker": "NONBLU",
              "nama_jasa": "Jasa Rambu",
              "nominal_tarif_pnbp": 50000,
              "volume": 1,
              "satuan_tarif": "Per Kapal",
              "total_tarif_per_record": "50000",
              "tgl_tagihan": "2026-08-26",
              "status_tagihan": "ACTIVE"
            }
          ],
          "bypassed_tagihan": [
            {
              "id_tagihan_hdr": 102,
              "trx_id_insw": "10202408260001",
              "jenis_pengajuan": "KEBERANGKATAN",
              "kode_kl": "024",
              "uraian_kl": "KEMENTERIAN KESEHATAN",
              "uraian_satker": "BKK Palembang",
              "jenis_satker": "NONBLU",
              "nama_jasa": "Penerbitan COP",
              "nominal_tarif_pnbp": 30000,
              "volume": 1,
              "satuan_tarif": "Dokumen",
              "total_tarif_per_record": "30000",
              "tgl_tagihan": "2026-08-26",
              "status_tagihan": "BYPASSED",
              "alasan_split": "K/L Sinkarkes melakukan pembayaran mandiri di instansi",
              "bypassed_by": "admin-1",
              "bypassed_at": "2026-08-27 15:30:00"
            }
          ]
        },
        "message": "Data billing ditemukan",
        "statusCode": 200,
        "success": true
      }
    Rincian Perubahan Field Response (Field Delta Matrix)
    Field PathTipe DataStatus PerubahanDeskripsi & Dampak Fungsional
    data.billing_status.is_split_billingBooleanBaru (Added)Bernilai true jika terdapat minimal satu tagihan K/L berstatus BYPASSED. Digunakan frontend untuk menampilkan badge / indikator split billing.
    data.billing_status.is_expiredBooleanBaru (Added)Menandakan apakah kode billing Simponi yang pernah terbit sudah melewati tanggal kedaluwarsa atau fl_expired = true.
    data.billing_status.total_tagihanNumberDiubah (Modified Logic)Kalkulasi nominal tagihan kini hanya menjumlahkan tagihan ACTIVE. Tagihan BYPASSED dan VOIDED otomatis dikecualikan dari total.
    data.eligibility.requirements.inaport.statusString | nullBaru (Added)Status eksplisit tagihan Warta Kemenhub ("ACTIVE", "BYPASSED", atau null).
    data.eligibility.requirements.sinkarkes.statusString | nullBaru (Added)Status eksplisit tagihan Sinkarkes Kemenkes ("ACTIVE", "BYPASSED", atau null).
    data.eligibility.messageStringDiubah (Modified Format)Format pesan kelayakan kini menyertakan info split jika ada tagihan di-bypass (misal: "Semua syarat terpenuhi (Split Billing: Tagihan Sinkarkes)").
    data.draft_tagihan[]Array<Object>Diubah (Modified Structure)Hanya berisi array tagihan berstatus ACTIVE yang berkontribusi terhadap total_tagihan dan diproses ke Simponi Single Billing.
    data.bypassed_tagihan[]Array<Object>Baru (Added Array)Berisi array tagihan yang di-bypass (status_tagihan: "BYPASSED") lengkap dengan metadata split (alasan_split, bypassed_by, bypassed_at).

4. Bukti Teknis (Technical Proof & Audit Trail)

  • Repository: pengangkut-backend
  • Branch Utama: feat/split-billing
  • Merge Request / PR: -

Daftar Commit Terkait (Multi-Commit Trail)

Commit IDBranchAuthorTanggal & JamRingkasan Perubahan
16b2bcdafeat/split-billingHermansyah Efendi2026-08-27 15:23docs(changelog): update unreleased entries
6e94a402feat/split-billingHermansyah Efendi2026-08-27 15:21feat(billing): integrate single billing set-expired-billing v3 API and optimize query
1319246cfeat/split-billingHermansyah Efendi2026-08-27 11:56fix(billing): enforce positive integer validation for id_tagihan_hdr in split billing schema
90e79bb1feat/split-billingHermansyah Efendi2026-08-26 17:34refactor(billing): rename rebilling terminology to voidAndResetTagihan and reset_tagihan
d97fcdccfeat/split-billingHermansyah Efendi2026-08-26 16:47feat(billing): implement unified split billing and bypass tagihan KL module

Cuplikan Kode / Implementasi Inti

javascript
// 1. Entry point terpadu split billing pada app/services/split-billing.service.js
async splitBilling({ service_number, kode_kl, id_tagihan_hdr = [], alasan_split, admin_id }) {
  const prefix = (service_number || '').slice(0, 3);
  if (prefix !== '102') {
    throw new BadRequestError('Split billing hanya dapat dilakukan menggunakan nomor aju keberangkatan');
  }
  const serviceNumberKedatangan = await checkAjuKedatangan(service_number);
  const trx_ids = serviceNumberKedatangan
    ? [service_number, serviceNumberKedatangan]
    : [service_number];
  const existingBilling = await checkDataBilling(service_number);
  const isBillingGenerated = existingBilling.length > 0 && existingBilling[0].is_active;
  if (!isBillingGenerated) {
    const result = await this.bypassTagihan({
      trx_ids,
      service_number,
      kode_kl,
      id_tagihan_hdr,
      alasan_split,
      admin_id,
    });
    return { stage: 'PRE_BILLING', ...result };
  } else {
    const result = await this.voidAndResetTagihan({
      trx_ids,
      service_number,
      kode_kl,
      bypassed_tagihan: id_tagihan_hdr,
      alasan_split,
      admin_id,
      existingBilling,
    });
    return { stage: 'POST_BILLING', ...result };
  }
}

// 2. Integrasi Set Expired Billing V3 saat void tagihan pada voidAndResetTagihan
// Guard: Ambil trx_id_billing dari active tagihan headers
const trxIdBilling = activeTagihanList.find(h => !!h.trx_id_billing)?.trx_id_billing || billingData[0]?.trx_id_insw;
if (!trxIdBilling) {
  logger.warn({ service_number }, 'Void rejected: No trx_id_billing found in active tagihan headers');
  throw new BadRequestError('Tidak ditemukan trx_id_billing pada tagihan aktif untuk transaksi ini.');
}

// Step 1: Set status billing menjadi expired di Layanan Single Billing
logger.debug({ service_number, trx_id_billing: trxIdBilling }, 'Calling billingApi.setExpiredBillingV3');
try {
  await billingApi.setExpiredBillingV3({
    trx_id_insw: trxIdBilling,
    alasan: alasan_split || null,
  });
} catch (error) {
  const isAlreadyExpired = error?.message?.toLowerCase().includes('sudah dalam status expired')
    || error?.message?.toLowerCase().includes('sudah expired');
  if (isAlreadyExpired) {
    logger.info(
      { service_number, trx_id_billing: trxIdBilling, error_message: error.message },
      'Billing is already marked as expired in Single Billing. Proceeding with void and reset'
    );
  } else {
    throw error;
  }
}

// Step 2: Void semua tagihan aktif dalam batch di database lokal
const [voidedCount] = await TagihanHdr.update(
  {
    status_tagihan: STATUS_TAGIHAN.VOIDED,
    alasan_split:   alasan_split,
    updated_by:     admin_id,
    updated_date:   new Date(),
  },
  {
    where: { id_tagihan_hdr: { [Op.in]: Array.from(activeIds) } },
    transaction: t,
  }
);

// 3. Pemisahan struktur data draft_tagihan & bypassed_tagihan pada app/services/inquiry-billing.service.js
let draftTagihan = [];
let bypassedTagihan = [];
if (serviceNumberKedatangan && serviceNumberKeberangkatan) {
  const rawDraftTagihan = await dataDraftTagihan(serviceNumberKedatangan, serviceNumberKeberangkatan);
  (rawDraftTagihan || []).forEach(({ id_tagihan_dtl, status_tagihan, bypassed_by, bypassed_at, alasan_split, ...item }) => {
    if (status_tagihan === STATUS_TAGIHAN.BYPASSED) {
      bypassedTagihan.push({
        ...item,
        status_tagihan: STATUS_TAGIHAN.BYPASSED,
        alasan_split:   alasan_split || null,
        bypassed_by:    bypassed_by || null,
        bypassed_at:    bypassed_at || null,
      });
    } else {
      draftTagihan.push({
        ...item,
        status_tagihan: STATUS_TAGIHAN.ACTIVE,
      });
    }
  });
}
return {
  billing_status: { ... },
  eligibility: { ... },
  draft_tagihan:    draftTagihan,
  bypassed_tagihan: bypassedTagihan,
};

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

A. Verifikasi Unit Test (Development Stage)

  • Test Suite File: tests/validations/billing.validation.test.js, tests/api/billing.api.test.js, tests/controllers/v2/billing.controller.test.js, tests/services/split-billing.service.test.js, tests/services/inquiry-billing.service.test.js
  • Status Eksekusi CLI: PASSED
text
 RUN  v4.1.10 D:/WORK/LNSW/pengangkut-backend

 ✓ tests/validations/billing.validation.test.js (15 tests) 20ms
 ✓ tests/api/billing.api.test.js (24 tests) 49ms
 ✓ tests/controllers/v2/billing.controller.test.js (5 tests) 33ms
 ✓ tests/services/split-billing.service.test.js (20 tests) 72ms
 ✓ tests/services/inquiry-billing.service.test.js (6 tests) 16ms

 Test Files  5 passed (5)
      Tests  70 passed (70)
   Duration  1.05s

B. Verifikasi Integrasi Staging / Production

Diisi saat naik ke Staging atau Production.

json
{
  "test_suites": [
    "tests/validations/billing.validation.test.js (PASS)",
    "tests/api/billing.api.test.js (PASS)",
    "tests/controllers/v2/billing.controller.test.js (PASS)",
    "tests/services/split-billing.service.test.js (PASS)",
    "tests/services/inquiry-billing.service.test.js (PASS)"
  ],
  "verification_status": "DEV_PASSED"
}
  • Hasil Pengujian UAT: PENDING
  • Catatan QA: Unit test lokal (70 tests across 5 test suites) telah lulus secara empiris. Menunggu jadwal deployment dan verifikasi staging.

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