[TASK-SPLIT-BILLING-202608] Implementasi Unified Split Billing, Multi-KL Bypass, & Centralized Billing Helper Modul Pengangkut Kapal
Modul: Pengangkut | Tipe: Feature | Stage Saat Ini:
STAGING| Tanggal Dokumen: 2026-09-01
1. Informasi Tugas & Referensi Bisnis
Task ID / Nomor Referensi:
TASK-SPLIT-BILLING-202608Sumber Instruksi: Penyesuaian Alur Bisnis Tagihan Pengangkut (Multi-KL Split Billing, Bypass Tagihan K/L, & Centralized Billing State Helper)
Dokumen Referensi / Lampiran: add_status_tagihan_split_billing.sql, split_billing_postman_collection.json, manual_billing_request_postman_collection.json
Task / Dependensi Terkait (Cross-Service):
- TASK-SET-EXPIRED-BILLING-V3 — Integrasi API Outbound Set Expired Billing V3 saat void tagihan
- FEAT-MANUAL-BILLING-REQUEST — Penyesuaian Alur Manual Request Billing & Idempotency Guard
Modul Terkait:
PengangkutService / Repository:
pengangkut-backendPIC Pengembang: Hermansyah Efendi
Latar Belakang & Ringkasan Kebutuhan: Implementasi arsitektur Unified Split Billing dan mekanisme Bypass Tagihan K/L secara menyeluruh untuk pengangkut kapal. Fitur ini 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. Arsitektur ini mendukung pemrosesan banyak instansi K/L sekaligus dalam satu request (Multi-KL
bypass_targets[]), penegakan guard bisnis (pencegahan total-bypass serta penolakan split pada billing yang sudah lunas), eliminasi N+1 database operations via batch transaction atomik, dan sentralisasi evaluasi status billing melalui modul helperapp/helpers/billing.helper.js(parseBillingStatedanfetchBillingDetails) sebagai Single Source of Truth (SSOT). Keseluruhan alur terintegrasi penuh dengan pemanggilan API Layanan Single BillingPOST /api/v3/set-expired-billing(idempotent), evaluasi kelayakan 5 layanan berbayar Sinkarkes Kedatangan & PHQC Keberangkatan, pemisahan array response draft vs bypassed tagihan pada endpoint inquiry, 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 (ENUM
enum_status_tagihan, kolomstatus_tagihan,keterangan,updated_by,updated_datepadapengangkut.tagihan_hdr, backfill data, dan index performa). - [x] Milestone 2: Middleware & Security Guard (
requireAdminJWT role introspectiongroupGa00,07,20). - [x] Milestone 3: Core Service Logic Split Billing (Pre-Billing Bypass dan Post-Billing Void-Reset secara atomik).
- [x] Milestone 4: Single Billing V3 Integration & Query Optimization (
POST /api/v3/set-expired-billingdengan penanganan idempotent expired, serta optimasi kuericheckDataBilling). - [x] Milestone 5: Integration with Eligibility & Inquiry (
BillingEligibilityServicedanInquiryBillingService, pemisahan arraydraft_tagihan[ACTIVE] vsbypassed_tagihan[BYPASSED]). - [x] Milestone 6: Simponi Payload Generator Update (
payloadSingleBillingV2danpayloadSingleBillingV3hanya memproses tagihanACTIVE). - [x] Milestone 7: API Controller & Strict Zod Validation (Endpoint
POST /split-billingdengan validasi skema arraybypass_targets). - [x] Milestone 8: Evaluasi Layanan Sinkarkes Kedatangan & Keberangkatan (Pemisahan 5 layanan berbayar kedatangan vs
pemberitahuan_kedatangangratis, serta kewajiban tagihan PHQC keberangkatan). - [x] Milestone 9: Multi-KL Bypass Targets & Batch Transaction (
bypass_targets[]mendukung multi-instansi dan pemisahan leg Sinkarkes kedatangan/keberangkatan, dieksekusi dalam 1 database transaction atomik). - [x] Milestone 10: Guard Proteksi Total-Bypass & Billing Paid (Penolakan split billing jika semua tagihan di-bypass atau billing sudah berstatus lunas).
- [x] Milestone 11: Centralized Billing State Helper (Pembuatan modul
app/helpers/billing.helper.jsdenganparseBillingStatedanfetchBillingDetailssebagai Single Source of Truth). - [x] Milestone 12: Comprehensive Unit Testing (Pembuatan test suite Vitest dengan 85 passed tests across 7 test suites dan 100% line coverage pada modul split billing).
- [x] Milestone 1: Database Migration & Schema Definition (ENUM
2. Deployment Lifecycle
| # | Environment | Tanggal & Waktu | PIC | Status | Catatan |
|---|---|---|---|---|---|
| 1 | Development | 2026-09-01 13:48 | Hermansyah Efendi | DEPLOYED | Branch: feat/split-billing |
| 2 | Staging (UAT) | 2026-09-09 11:34 | achmad fauzi | DEPLOYED | Release Candidate v1.18.2 via MR !2080. Co-deployed: [TASK-DRAGONFLYDB-MIGRATION, TASK-SPLIT-BILLING-202608, FEAT-MANUAL-BILLING-REQUEST, MAINT-SUPERSET-SUMMARY-SHIP-SYNC-SECURITY, REFACTOR-DEPRECATE-POST-STATUS-SERVICE-MIGRATION, TASK-PINO-LOGGER-REFACTOR, ND-304-LNSW.34-2026, ND-256-LNSW.34-2026, ND-183-LNSW.34-2026] |
| 3 | Production | — | — | PENDING | — |
Riwayat Update Log
| Tanggal | Environment | Perubahan | PIC |
|---|---|---|---|
| 2026-09-09 | Staging | Promosi ke STAGING via MR !2080 (Release Candidate v1.18.2) | Hermansyah Efendi |
| 2026-09-01 | Development | Penambahan dukungan Multi-KL bypass targets (array bypass_targets[]), eksekusi batch transaction atomik di Pre-Billing, modul helper terpusat billing.helper.js (parseBillingState & fetchBillingDetails), guard total-bypass & billing paid, pemisahan leg Sinkarkes kedatangan/keberangkatan, dan update test suites (85 passed tests across 7 test suites, 100% line coverage pada split billing) | Hermansyah Efendi |
| 2026-08-31 | Development | Penyelarasan logika evaluasi Sinkarkes 5 layanan berbayar kedatangan & PHQC keberangkatan pada BillingEligibilityService, konsolidasi alur Manual Request Billing & Idempotency, penambahan Postman Collection JSON, dan update test suites (55 passed tests) | Hermansyah Efendi |
| 2026-08-27 | Development | Pemisahan array draft_tagihan (ACTIVE) dan bypassed_tagihan (BYPASSED) pada response endpoint inquiry billing (InquiryBillingService) | Hermansyah Efendi |
| 2026-08-27 | Development | Integrasi 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-26 | Development | Inisialisasi draft audit log dari context block backend (commit d97fcdcc) | Hermansyah Efendi |
3. Analisis Teknis & Rincian Perubahan
Alur Logika & Bisnis:
- Multi-KL Payload Schema (
bypass_targets[]): Request split billing mendukung pemrosesan banyak target instansi sekaligus dalam 1 request. Tiap elemen memuatkode_kl(wajib), opsionalid_tagihan_hdr(array positive integer), dan opsionaljenis_pengajuan("kedatangan"atau"keberangkatan"untuk K/L Sinkarkes 024). - Pre-Billing Batch Execution: Eksekusi bypass sebelum billing terbit dilakukan dalam 1 transaksi database atomik (
sequelizeDb.transaction()). Menggunakan single query lookup, single query bulk update, dan single query bulk create placeholder guna mengeliminasi N+1 database operations. - Post-Billing Guard & Void-Reset:
- Guard Paid Check: Menolak split billing jika billing induk sudah lunas (
fl_paid = true). - Guard 4 (Total-Bypass Prevention): Menolak pembatalan jika seluruh tagihan dalam batch akan di-bypass (minimal 1 tagihan wajib berstatus
ACTIVEagar dapat diterbitkan billing Simponi baru). - Sinkarkes Leg Matching: Membedakan tagihan kedatangan (prefix
101) dan keberangkatan (prefix102) saat melakukan void dan penyalinan tagihan baru.
- Guard Paid Check: Menolak split billing jika billing induk sudah lunas (
- Centralized Billing Helper Module (
app/helpers/billing.helper.js): Mengonsolidasi logika penentuan status billing (hasBillingRecord,flBilling,isPaid,flPayment,isBillingExpired,isBillingGenerated,idBilling,activeKodeBilling,fetchBillingDetails) sehingga seluruh consumer file (split-billing,billing-eligibility,inquiry-billing,request-billing,superset-summary) memiliki definisi status yang seragam dan DRY. - Integrasi Single Billing V3 Set-Expired (
voidAndResetTagihan): Mengambiltrx_id_billingdari header tagihan aktif dan memanggilbillingApi.setExpiredBillingV3({ trx_id_insw: trxIdBilling, alasan })untuk menandai status expired pada Layanan Single Billing dengan penanganan idempotent jika status sudah expired. - Evaluasi Spesifik Sinkarkes Kedatangan vs Keberangkatan (
BillingEligibilityService):- Kedatangan: Backend memeriksa kolom boolean layanan pada
pengangkut.data_sinkarkes(sertifikat_air,cop,healthbook,p3k,sscec). Tagihan kedatangan hanya diwajibkan jika pemohon mengajukan minimal satu layanan berbayar. Pengajuanpemberitahuan_kedatanganmurni dikategorikan non-berbayar (gratis). - Keberangkatan: Layanan pemeriksaan keberangkatan (PHQC) selalu mewajibkan ketersediaan tagihan keberangkatan (
hasSinkarkesDepartureatausinkarkesDepartureBypassed).
- Kedatangan: Backend memeriksa kolom boolean layanan pada
- Daftar File yang Berubah:
app/helpers/billing.helper.js[Helper Module]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/services/superset-summary.service.js[Service / Business Logic]app/controllers/superset_summary_controller.js[Controller / API Route]app/controllers/v2/billing.controller.js[Controller / API Route]app/validations/billing.validation.js[Validation / Schema]app/models/billing_models.js[SQL Model Helper]tests/helpers/billing.helper.test.js[Unit Test]tests/validations/billing.validation.test.js[Unit Test]tests/controllers/v2/billing.controller.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]
- Multi-KL Payload Schema (
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 keterangan 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/split-billing- Deskripsi: Endpoint terpadu untuk memisahkan/melewati (bypass) multi-tagihan K/L dari alur billing kolektif. Backend secara otomatis mendeteksi fase Pre-Billing atau Post-Billing.
- Autentikasi / Security Guard:
Bearer TokenJWT (Wajib lolosdoIntrospectiondanrequireAdmindengangroupGa00,07, atau20). - Request Header:
Authorization:Bearer <token>Content-Type:application/json
A. Skema Validasi Request Payload (Zod Schema
SplitBillingSchema) Field Path Tipe Data Wajib/Opsional Keterangan & Aturan Validasi service_numberStringWajib Nomor aju keberangkatan (Wajib diawali prefix 102).bypass_targetsArray<Object>Wajib Array target bypass (minimal 1 item). bypass_targets[].kode_klStringWajib Enum: "022"(Inaportnet) atau"024"(Sinkarkes).bypass_targets[].id_tagihan_hdrArray<Number>Opsional Array positive integers ID header tagihan spesifik. bypass_targets[].jenis_pengajuanStringOpsional Enum: "kedatangan"atau"keberangkatan"(Spesifik Sinkarkes 024).keteranganStringWajib Alasan split billing (Minimal 5 karakter). Contoh Payload Request Multi-KL:
json{ "service_number": "10202408260001", "bypass_targets": [ { "kode_kl": "024", "jenis_pengajuan": "kedatangan" }, { "kode_kl": "022", "id_tagihan_hdr": [105] } ], "keterangan": "Tagihan Sinkarkes kedatangan dan Warta 105 dibayar mandiri oleh agen" }B. Response Pre-Billing Stage (
200 OK) json{ "data": { "stage": "PRE_BILLING", "message": "Split billing tagihan K/L berhasil dilakukan (Pre-Billing)", "results": [ { "kode_kl": "024", "jenis_pengajuan": "kedatangan", "status_tagihan": "BYPASSED", "keterangan": "Tagihan Sinkarkes kedatangan dibayar mandiri", "updated_by": "admin-1", "affected_headers": [ { "id_tagihan_hdr": 102, "kode_kl": "024", "trx_id_insw": "10102408260001", "status_tagihan": "BYPASSED" } ] }, { "kode_kl": "022", "jenis_pengajuan": null, "status_tagihan": "BYPASSED", "keterangan": "Tagihan Warta dibayar mandiri", "updated_by": "admin-1", "affected_headers": [ { "id_tagihan_hdr": 105, "kode_kl": "022", "trx_id_insw": "10202408260001", "status_tagihan": "BYPASSED" } ] } ] }, "message": "Split billing tagihan K/L berhasil dilakukan (Pre-Billing)", "statusCode": 200, "success": true }C. Response Post-Billing Stage (
200 OK) json{ "data": { "stage": "POST_BILLING", "message": "3 tagihan berhasil di-void dan di-reset", "voided_count": 3, "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 }, { "id_tagihan_hdr_lama": 103, "status_tagihan": "ACTIVE", "hdr": { "id_tagihan_hdr": 203, "status_tagihan": "ACTIVE" }, "dtl_count": 1 } ] }, "message": "3 tagihan berhasil di-void dan di-reset", "statusCode": 200, "success": true }D. Penanganan Error & Business Guards
Guard Total-Bypass Ditolak (
400 Bad Request):json{ "data": null, "message": "Tidak bisa mem-bypass seluruh tagihan. Minimal 1 tagihan harus tetap ACTIVE setelah split.", "statusCode": 400, "success": false, "errors": [] }Guard Billing Lunas Ditolak (
400 Bad Request):json{ "data": null, "message": "Billing sudah lunas, tidak dapat melakukan split billing", "statusCode": 400, "success": false, "errors": [] }Bypass Pre-Billing Tagihan Sudah Terbit Billing (
400 Bad Request):json{ "data": null, "message": "Bypass gagal. Kode billing sudah diterbitkan untuk tagihan 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": [] }Akses Ditolak (Non-Admin Role):
json{ "data": null, "message": "Akses ditolak. Endpoint ini hanya dapat diakses oleh Admin.", "statusCode": 403, "success": false }
4. Bukti Teknis (Technical Proof & Audit Trail)
- Repository:
pengangkut-backend - Branch Utama:
feat/split-billing - Merge Request / PR: MR !2080 (Release Candidate v1.18.2)
Daftar Commit Terkait (Multi-Commit Trail)
| Commit ID | Branch | Author | Tanggal & Jam | Ringkasan Perubahan |
|---|---|---|---|---|
d1f7f125834e3cf98df51dc11805135280167e49 | feat/split-billing | Hermansyah Efendi | 2026-09-01 13:48 | feat(billing): support multi-KL bypass targets and centralize billing state helper |
10f795c5c22dfd7ab84bf2efb94e770a2aa0cde9 | feat/split-billing | Hermansyah Efendi | 2026-09-01 09:34 | fix(billing): handle expired status correctly for paid and active billings |
5cdbb0be4b1e7a9cf1967bb35c9b9ac3651b1bfe | feat/split-billing | Hermansyah Efendi | 2026-08-31 18:03 | feat(billing): separate sinkarkes requirements by leg and fix bypass id priority in void reset |
d3d19610 | feat/manual-billing-request | Hermansyah Efendi | 2026-08-27 15:45 | feat(billing): separate draft_tagihan and bypassed_tagihan in inquiry response |
6e94a402 | feat/manual-billing-request | Hermansyah Efendi | 2026-08-27 15:21 | feat(billing): integrate single billing set-expired-billing v3 API and optimize query |
d97fcdcc | feat/manual-billing-request | Hermansyah Efendi | 2026-08-26 16:47 | feat(billing): implement unified split billing and bypass tagihan KL module |
Cuplikan Kode / Implementasi Inti
// 1. Modul Helper Terpusat (app/helpers/billing.helper.js)
export const parseBillingState = (billingData) => {
const row = Array.isArray(billingData) && billingData.length > 0 ? billingData[0] : null;
const hasBillingRecord = Boolean(row && row.kode_billing_induk);
const isPaid = Boolean(hasBillingRecord && row.fl_paid === true);
const isBillingExpired = Boolean(hasBillingRecord && !isPaid && row.is_expired === true);
const isBillingGenerated = Boolean(hasBillingRecord && !isBillingExpired);
const idBilling = isBillingGenerated ? row.trx_id_insw : null;
const activeKodeBilling = isBillingGenerated ? row.kode_billing_induk : null;
return {
hasBillingRecord,
flBilling: hasBillingRecord,
isPaid,
flPayment: isPaid,
isBillingExpired,
isBillingGenerated,
idBilling,
activeKodeBilling,
raw: row,
};
};
// 2. Batch Execution & Transaksi Atomik Pre-Billing (app/services/split-billing.service.js)
async bypassTagihan({ trx_ids, service_number, serviceNumberKedatangan, bypass_targets, keterangan, admin_id }) {
const t = await sequelizeDb.transaction();
try {
const existingHeaders = await TagihanHdr.findAll({
where: {
trx_id_insw: { [Op.in]: trx_ids },
status_tagihan: { [Op.ne]: STATUS_TAGIHAN.VOIDED },
},
transaction: t,
});
const idsToUpdate = new Set();
const placeholdersToCreate = [];
const results = [];
for (const item of bypass_targets) {
const { kode_kl, id_tagihan_hdr, jenis_pengajuan } = item;
const headerIds = Array.isArray(id_tagihan_hdr) && id_tagihan_hdr.length > 0
? id_tagihan_hdr.map(id => parseInt(id, 10))
: [];
let matchedHeaders = [];
if (headerIds.length > 0) {
const idSet = new Set(headerIds);
matchedHeaders = existingHeaders.filter(h => idSet.has(h.id_tagihan_hdr));
} else if (kode_kl === KODE_KL.SINKARKES && jenis_pengajuan) {
const targetTrxId = jenis_pengajuan === 'kedatangan' ? serviceNumberKedatangan : service_number;
if (!targetTrxId) {
throw new BadRequestError(`Tidak ditemukan nomor aju ${jenis_pengajuan} untuk transaksi ini.`);
}
matchedHeaders = existingHeaders.filter(h => h.kode_kl === kode_kl && h.trx_id_insw === targetTrxId);
} else {
matchedHeaders = existingHeaders.filter(h => h.kode_kl === kode_kl);
}
if (matchedHeaders.length > 0) {
if (matchedHeaders.some(h => h.fl_billing === true)) {
throw new BadRequestError('Bypass gagal. Kode billing sudah diterbitkan untuk tagihan ini.');
}
matchedHeaders.forEach(h => idsToUpdate.add(h.id_tagihan_hdr));
results.push({
kode_kl,
jenis_pengajuan: jenis_pengajuan || null,
status_tagihan: STATUS_TAGIHAN.BYPASSED,
keterangan,
updated_by: admin_id,
affected_headers: matchedHeaders.map(h => ({
id_tagihan_hdr: h.id_tagihan_hdr,
kode_kl: h.kode_kl,
trx_id_insw: h.trx_id_insw,
status_tagihan: STATUS_TAGIHAN.BYPASSED,
})),
});
} else {
let placeholderTrxId = service_number;
if (kode_kl === KODE_KL.SINKARKES && jenis_pengajuan === 'kedatangan') {
placeholderTrxId = serviceNumberKedatangan || service_number;
}
placeholdersToCreate.push({
item,
data: {
trx_id_insw: placeholderTrxId,
kode_kl,
status_tagihan: STATUS_TAGIHAN.BYPASSED,
keterangan,
updated_by: admin_id,
updated_date: new Date(),
fl_billing: false,
fl_payment: false,
total_nominal_billing: '0',
},
});
}
}
if (idsToUpdate.size > 0) {
await TagihanHdr.update(
{ status_tagihan: STATUS_TAGIHAN.BYPASSED, keterangan, updated_by: admin_id, updated_date: new Date() },
{ where: { id_tagihan_hdr: { [Op.in]: Array.from(idsToUpdate) } }, transaction: t }
);
}
if (placeholdersToCreate.length > 0) {
const createdRecords = await TagihanHdr.bulkCreate(placeholdersToCreate.map(p => p.data), { transaction: t });
createdRecords.forEach((newHdr, idx) => {
const { item } = placeholdersToCreate[idx];
results.push({
kode_kl: newHdr.kode_kl,
jenis_pengajuan: item.jenis_pengajuan || null,
status_tagihan: newHdr.status_tagihan,
keterangan: newHdr.keterangan,
updated_by: newHdr.updated_by,
affected_headers: [{ id_tagihan_hdr: newHdr.id_tagihan_hdr, kode_kl: newHdr.kode_kl, trx_id_insw: newHdr.trx_id_insw, status_tagihan: newHdr.status_tagihan }],
});
});
}
await t.commit();
return results;
} catch (err) {
await t.rollback();
throw err;
}
}5. Bukti Verifikasi Pengujian (QA / Unit Test / API Verification)
A. Verifikasi Unit Test (Development Stage)
- Test Suite File:
tests/helpers/billing.helper.test.js,tests/validations/billing.validation.test.js,tests/controllers/v2/billing.controller.test.js,tests/services/billing-eligibility.service.test.js,tests/services/inquiry-billing.service.test.js,tests/services/request-billing.service.test.js,tests/services/split-billing.service.test.js - Status Eksekusi CLI:
PASSED (85/85 tests passed, 100% line coverage on split billing)
RUN v4.1.10 D:/WORK/LNSW/pengangkut-backend
✓ tests/helpers/billing.helper.test.js (8 tests) 14ms
✓ tests/services/inquiry-billing.service.test.js (7 tests) 22ms
✓ tests/services/billing-eligibility.service.test.js (17 tests) 22ms
✓ tests/validations/billing.validation.test.js (14 tests) 17ms
✓ tests/services/request-billing.service.test.js (10 tests) 32ms
✓ tests/controllers/v2/billing.controller.test.js (5 tests) 20ms
✓ tests/services/split-billing.service.test.js (24 tests) 68ms
Test Files 7 passed (7)
Tests 85 passed (85)
Start at 13:57:03
Duration 910ms (transform 559ms, setup 0ms, import 1.43s, tests 196ms, environment 2ms)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:
Full E2E (UI + Backend) - Kebutuhan Akses / Role Akun:
Role Admin (groupGa 00, 07, 20) & Role Agen Pengangkut - Postman Collection File: split_billing_postman_collection.json, manual_billing_request_postman_collection.json
- Hasil Pengujian UAT:
PENDING - Catatan QA: Unit test lokal (85 tests across 7 test suites) telah lulus secara empiris dengan 100% line coverage pada modul split billing. Skenario uji E2E dapat dijalankan menggunakan skenario di bawah ini.
Detailed QA Playbook: Modul Split Billing (POST /split-billing)
- Endpoint:
POST /split-billing - Autentikasi / Guard:
Bearer Token(doIntrospection,requireAdmindengangroupGa00,07,20)
A. Skenario Positive (Happy Path Testing)
Skenario 1 — Pre-Billing Baseline: Bypass Sinkarkes Full (Kasus Operasional Utama)
- Tujuan: Mem-bypass seluruh tagihan Sinkarkes (024) baik kedatangan maupun keberangkatan sebelum kode billing terbit, sehingga hanya tagihan Inaport (022) yang diproses.
- Pre-kondisi Data: Aju keberangkatan
1020xterhubung dengan kedatangan1010x. Tagihan 022 dan 024 (kedatangan & keberangkatan) sudah ada di DB denganfl_billing = falsedanstatus_tagihan = 'ACTIVE'. - Trigger Request:
METHOD:POSTURL:/split-billingHEADERS:Content-Type:application/jsonAuthorization:Bearer <adminToken>
BODY:json{ "service_number": "1020x", "bypass_targets": [ { "kode_kl": "024" } ], "keterangan": "Bypass Sinkarkes penuh agar tagihan Inaport dapat terbit terlebih dahulu" }
- Ekspektasi Response (
200 OK):stage:"PRE_BILLING"results: Array berisi 1 grup hasil bypass untuk kode K/L024.
- Verifikasi Database (
pengangkut.tagihan_hdr):- Seluruh tagihan 024 (SINKARKES) pada aju
1010xdan1020xberubah status menjadiBYPASSEDdengan kolomketerangandanupdated_byterisi. - Tagihan 022 tetap berstatus
ACTIVE.
- Seluruh tagihan 024 (SINKARKES) pada aju
Skenario 2 — Pre-Billing Placeholder: Bypass Sinkarkes Sebelum Data Landing di DB
- Tujuan: Memastikan sistem berhasil membuat baris placeholder
BYPASSEDketika admin melakukan split sebelum K/L Sinkarkes mengirimkan tagihan. - Pre-kondisi Data: Aju Keberangkatan
1020xhanya memiliki tagihan 022 (INAPORT) di DB. Belum ada baris tagihan 024 sama sekali. Billing belum terbit. - Trigger Request:
METHOD:POSTURL:/split-billingHEADERS:Content-Type:application/jsonAuthorization:Bearer <adminToken>
BODY:json{ "service_number": "1020x", "bypass_targets": [ { "kode_kl": "024" } ], "keterangan": "Pre-bypass Sinkarkes sebelum data terkirim ke DB" }
- Ekspektasi Response (
200 OK):stage:"PRE_BILLING"results[0].affected_headers: Mengembalikan placeholder header baru hasil insert.
- Verifikasi Database (
pengangkut.tagihan_hdr):- Terbentuk baris baru:
trx_id_insw: '1020x',kode_kl: '024',status_tagihan: 'BYPASSED',total_nominal_billing: '0',fl_billing: false.
- Terbentuk baris baru:
Skenario 3 — Pre-Billing Multi-KL Split: 022 & 024 Kedatangan (Variasi Lanjutan)
- Tujuan: Mem-bypass tagihan multi-instansi (Inaport 022 dan Sinkarkes Kedatangan 024) secara bersamaan sebelum billing terbit.
- Pre-kondisi Data: Aju Keberangkatan
1020xterhubung dengan kedatangan1010x. Tagihan 022 dan 024 (kedatangan & keberangkatan) ada di DB,fl_billing = false. - Trigger Request:
METHOD:POSTURL:/split-billingHEADERS:Content-Type:application/jsonAuthorization:Bearer <adminToken>
BODY:json{ "service_number": "1020x", "bypass_targets": [ { "kode_kl": "022" }, { "kode_kl": "024", "jenis_pengajuan": "kedatangan" } ], "keterangan": "Tagihan Inaport dan Sinkarkes Kedatangan dibayar mandiri" }
- Ekspektasi Response (
200 OK):stage:"PRE_BILLING"results: Array berisi 2 grup hasil bypass (022 dan 024 kedatangan).
- Verifikasi Database (
pengangkut.tagihan_hdr):- Seluruh tagihan 022 berubah menjadi
BYPASSED. - Tagihan 024 kedatangan (
101...) berubah menjadiBYPASSED. - Tagihan 024 keberangkatan (
102...) tetapACTIVE.
- Seluruh tagihan 022 berubah menjadi
Skenario 4 — Post-Billing Baseline: Void & Reset Bypass Sinkarkes Full (Kasus Operasional Utama)
- Tujuan: Membatalkan kode billing gabungan (Inaport + Sinkarkes) yang sudah terbit di Layanan Single Billing, lalu mereset tagihan sehingga hanya tagihan Inaport yang siap diterbitkan ulang.
- Pre-kondisi Data:
- Kode billing aktif sudah terbit di Layanan Single Billing untuk aju
1020x(fl_paid = false, belum expired). - DB memiliki 3 tagihan aktif: 022, 024 kedatangan, 024 keberangkatan (
fl_billing = true,status_tagihan = 'ACTIVE').
- Kode billing aktif sudah terbit di Layanan Single Billing untuk aju
- Trigger Request:
METHOD:POSTURL:/split-billingHEADERS:Content-Type:application/jsonAuthorization:Bearer <adminToken>
BODY:json{ "service_number": "1020x", "bypass_targets": [ { "kode_kl": "024" } ], "keterangan": "Void billing gabungan untuk memisahkan seluruh tagihan Karantina" }
- Ekspektasi Response (
200 OK):stage:"POST_BILLING"voided_count:3message:"3 tagihan berhasil di-void dan di-reset"
- Verifikasi Eksternal & Database:
- Layanan Single Billing: Kode billing lama berstatus
EXPIREDviabillingApi.setExpiredBillingV3. pengangkut.tagihan_hdr(Record Lama): 3 tagihan lama berubah menjadiVOIDED.pengangkut.tagihan_hdr(Record Baru Hasil Clone):- Tagihan baru 022:
status_tagihan = 'ACTIVE',fl_billing = false,fl_payment = false,trx_id_billing = null. - Tagihan baru 024 (kedatangan & keberangkatan):
status_tagihan = 'BYPASSED',keteranganterisi,fl_billing = false.
- Tagihan baru 022:
pengangkut.tagihan_dtl: Seluruh baris detail tagihan lama tersalin utuh mengarah keid_tagihan_hdrbaru.
- Layanan Single Billing: Kode billing lama berstatus
Skenario 5 — Post-Billing Void & Reset Parsial: Bypass Sinkarkes Keberangkatan (Variasi Lanjutan)
- Tujuan: Membatalkan kode billing aktif dan mereset tagihan dengan hanya mem-bypass Sinkarkes Keberangkatan, sedangkan Inaport dan Sinkarkes Kedatangan tetap diproses pada billing baru.
- Pre-kondisi Data: Kode billing aktif menggabungkan 022, 024 kedatangan, dan 024 keberangkatan.
- Trigger Request:
METHOD:POSTURL:/split-billingHEADERS:Content-Type:application/jsonAuthorization:Bearer <adminToken>
BODY:json{ "service_number": "1020x", "bypass_targets": [ { "kode_kl": "024", "jenis_pengajuan": "keberangkatan" } ], "keterangan": "Bypass Sinkarkes Keberangkatan pasca billing terbit" }
- Ekspektasi Response (
200 OK):stage:"POST_BILLING"voided_count:3
- Verifikasi Database (
pengangkut.tagihan_hdr):- 3 tagihan lama menjadi
VOIDED. - 3 tagihan baru terbentuk:
- 022:
status_tagihan = 'ACTIVE'. - 024 Kedatangan:
status_tagihan = 'ACTIVE'. - 024 Keberangkatan:
status_tagihan = 'BYPASSED'.
- 022:
- 3 tagihan lama menjadi
B. Skenario Negative (Validation, Auth, & Business Rule Testing)
Skenario 6 — Guard Total-Bypass Ditolak:
- Pre-kondisi: Billing aktif hanya memiliki tagihan 022 dan 024.
- Trigger Request:
POST /split-billingdenganbypass_targets:[{ "kode_kl": "022" }, { "kode_kl": "024" }]. - Ekspektasi Response (
400 Bad Request):json{ "data": null, "message": "Tidak bisa mem-bypass seluruh tagihan. Minimal 1 tagihan harus tetap ACTIVE setelah split.", "statusCode": 400, "success": false, "errors": [] } - Verifikasi Database: Transaksi di-rollback penuh, tidak ada tagihan yang di-void.
Skenario 7 — Guard Billing Lunas (Paid) Ditolak:
- Pre-kondisi: Billing sudah dibayar (
fl_paid: true). - Trigger Request:
POST /split-billing. - Ekspektasi Response (
400 Bad Request):json{ "data": null, "message": "Billing sudah lunas, tidak dapat melakukan split billing", "statusCode": 400, "success": false, "errors": [] }
- Pre-kondisi: Billing sudah dibayar (
Skenario 8 — Otorisasi Non-Admin (RBAC Security):
- Pre-kondisi: Token otentikasi akun non-admin (
groupGaselain00,07,20). - Ekspektasi Response (
403 Forbidden):json{ "data": null, "message": "Akses ditolak. Endpoint ini hanya dapat diakses oleh Admin.", "statusCode": 403, "success": false }
- Pre-kondisi: Token otentikasi akun non-admin (
Skenario 9 — Validasi Nomor Aju Bukan Keberangkatan (Non-102):
- Payload:
"service_number": "101202409010001"(prefix101). - Ekspektasi Response (
400 Bad Request):json{ "data": null, "message": "Split billing hanya dapat dilakukan menggunakan nomor aju keberangkatan", "statusCode": 400, "success": false, "errors": [] }
- Payload:
Bukti Response Verifikasi Staging (Diisi saat UAT)
{
"test_suites": [
"tests/helpers/billing.helper.test.js (PASS)",
"tests/validations/billing.validation.test.js (PASS)",
"tests/controllers/v2/billing.controller.test.js (PASS)",
"tests/services/billing-eligibility.service.test.js (PASS)",
"tests/services/inquiry-billing.service.test.js (PASS)",
"tests/services/request-billing.service.test.js (PASS)",
"tests/services/split-billing.service.test.js (PASS)"
],
"verification_status": "DEV_PASSED",
"note": "Menunggu deployment ke environment Staging untuk eksekusi skenario uji QA."
}