Skip to content

[FEAT-MANUAL-BILLING-REQUEST] Penyesuaian Alur Bisnis Tagihan Pengangkut (Manual Request Billing & Idempotency) ​

Modul: Pengangkut | Tipe: Feature | Stage Saat Ini: STAGING | Tanggal Dokumen: 2026-08-31


1. Informasi Tugas & Referensi Bisnis ​

  • Task ID / Nomor Referensi: FEAT-MANUAL-BILLING-REQUEST

  • Sumber Instruksi: Penyesuaian Alur Bisnis Tagihan Pengangkut (Manual Request Billing & Idempotency)

  • Dokumen Referensi / Lampiran: manual_billing_request_postman_collection.json

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

  • Modul Terkait: Pengangkut

  • Service / Repository: pengangkut-backend

  • PIC Pengembang: Hermansyah Efendi

  • Latar Belakang & Ringkasan Kebutuhan: Penyesuaian alur bisnis penerbitan Single Billing pada modul Pengangkut Kapal. Sebelumnya, pembuatan billing dipicu secara otomatis (auto-billing) pada handler POST /tagihan dan POST /warta-kapal/send/putResponse. Alur ini diubah menjadi manual billing request yang dipicu secara eksplisit oleh pengguna/agen setelah semua prasyarat kelayakan (eligibility) terpenuhi (dokumen LK3 disetujui, tagihan Inaportnet aktif/lengkap, dan tagihan Sinkarkes aktif/lengkap). Untuk mencegah duplicate billing requests atau race conditions saat penekanan tombol berulang, diimplementasikan mekanisme Token-Bound Idempotency Key berbasis Redis dengan validasi x-idempotency-key berdurasi 5 menit yang terikat pada hash sesi token autentikasi pengguna.

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

    • [x] Penghapusan pemicu auto-billing otomatis pada handler POST /tagihan dan POST /warta-kapal/send/putResponse.
    • [x] Implementasi reusable BillingEligibilityService untuk verifikasi status LK3, Tagihan Inaportnet, dan Tagihan Sinkarkes.
    • [x] Implementasi endpoint GET /inquiry-billing untuk pengecekan status kelayakan dan rincian draft/bypassed tagihan.
    • [x] Implementasi Idempotency Service & Middleware berbasis Redis token session hash (POST /generate-idempotency-key).
    • [x] Implementasi endpoint POST /request-billing dilindungi Idempotency Guard (x-idempotency-key).
    • [x] Penulisan Vitest unit test suite komprehensif untuk seluruh billing service v2 dan idempotency service.

2. Deployment Lifecycle ​

#EnvironmentTanggal & WaktuPICStatusCatatan
1Development2026-06-25 17:01Hermansyah EfendiDEPLOYEDBranch: development (Merged from feat/manual-billing-request)
2Staging (UAT)2026-09-09 11:34achmad fauziDEPLOYEDRelease 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]
3Production——PENDING—

Riwayat Update Log ​

TanggalEnvironmentPerubahanPIC
2026-09-09StagingPromosi ke STAGING via MR !2080 (Release Candidate v1.18.2)Hermansyah Efendi
2026-08-31DevelopmentInitial log created from backend context blockHermansyah Efendi

3. Analisis Teknis & Rincian Perubahan ​

  • Alur Logika & Bisnis:

    • Decoupling Auto-Billing: Menghapus pemanggilan otomatis pembuatan billing dari alur warta kapal dan penerimaan tagihan K/L. Tagihan sekarang hanya dikonsolidasi dan menunggu perintah eksplisit dari pengguna.
    • Reusable Billing Eligibility Service (BillingEligibilityService): Layanan terpusat untuk memverifikasi prasyarat penerbitan billing. Mengecek kelengkapan dan persetujuan dokumen LK3 (doc=7, code=2), ketersediaan tagihan Warta Kemenhub (Inaportnet), serta tagihan Sinkarkes Kemenkes. Mendukung integrasi status tagihan ACTIVE dan BYPASSED (split billing).
    • Inquiry Billing (InquiryBillingService): Endpoint GET /inquiry-billing mengevaluasi kelayakan pengajuan aju kapal (resolusi nomor aju keberangkatan 102 dan kedatangan 101), mengembalikan status billing (is_billing, is_generated, is_expired, is_paid), status kelayakan (is_eligible, requirements), serta rincian draft_tagihan dan bypassed_tagihan.
    • Token-Bound Idempotency Guard (IdempotencyService & validateIdempotency): Menggantikan mekanisme captcha lama dengan token-bound idempotency key system. Kunci dihasilkan via POST /generate-idempotency-key, disimpan di Redis dengan TTL 5 menit (300 detik), dan terikat pada SHA-256 hash dari Bearer Token pengguna serta service number. Saat endpoint POST /request-billing dipanggil dengan header x-idempotency-key, middleware mengonsumsi (single-use) kunci tersebut secara atomik untuk mencegah double billing generation.
    • Manual Request Billing (RequestBillingService): Endpoint POST /request-billing yang memvalidasi kelayakan via BillingEligibilityService, mengonsumsi idempotency key, dan memicu pembuatan kode billing Simponi via Single Billing Service.
    • Daftar File yang Berubah:
      • app/routers/billing_route.js [Router]
      • app/routers/idempotency.router.js [Router]
      • app/controllers/v2/billing.controller.js [Controller]
      • app/controllers/v2/idempotency.controller.js [Controller]
      • app/services/inquiry-billing.service.js [Service]
      • app/services/request-billing.service.js [Service]
      • app/services/billing-eligibility.service.js [Service]
      • app/services/idempotency.service.js [Service]
      • app/middlewares/validate-idempotency.js [Middleware]
      • app/validations/billing.validation.js [Validation]
      • app/validations/idempotency.validation.js [Validation]
      • app/models/billing_models.js [Model]
      • tests/services/inquiry-billing.service.test.js [Unit Test]
      • tests/services/request-billing.service.test.js [Unit Test]
      • tests/services/billing-eligibility.service.test.js [Unit Test]
      • tests/services/idempotency.service.test.js [Unit Test]
  • Perubahan Database (DDL / DML):

    sql
    -- Tidak ada migration file DDL/DML yang terdeteksi untuk task ini.
    -- Seluruh state session dan kunci idempotency dikelola secara in-memory/cache pada Redis cluster.
  • Kontrak API & Response Payload:

    1. GET /inquiry-billing ​

    • Deskripsi: Mengambil status kelayakan (eligibility) pengajuan billing beserta rincian draft tagihan aktif dan tagihan yang di-bypass.
    • Autentikasi / Guard: Bearer Token (doIntrospection, checkAccessAgentByServiceNumber)
    • Request Query Params:
      • serviceNumber: string (Nomor Aju Keberangkatan / Kedatangan, misal 102... atau 101...)
    • Response Payload (200 OK):
      json
      {
        "data": {
          "billing_status": {
            "is_billing": true,
            "is_generated": false,
            "is_expired": false,
            "id_billing": null,
            "is_paid": false,
            "total_tagihan": 1500000,
            "is_split_billing": false,
            "payment_detail": null,
            "billing_detail": null
          },
          "eligibility": {
            "is_eligible": true,
            "message": "Semua syarat terpenuhi",
            "requirements": {
              "lk3": { "required": true, "completed": true, "name": "LK3" },
              "inaport": { "required": true, "completed": true, "status": "ACTIVE", "name": "Tagihan Warta" },
              "sinkarkes": { "required": true, "completed": true, "status": "ACTIVE", "name": "Tagihan Sinkarkes" }
            }
          },
          "draft_tagihan": [],
          "bypassed_tagihan": []
        },
        "message": "Data billing ditemukan",
        "statusCode": 200,
        "success": true
      }
    • Response Error (400 Bad Request):
      json
      {
        "data": null,
        "message": "Data kedatangan tidak ditemukan untuk service number ini",
        "statusCode": 400,
        "success": false,
        "errors": []
      }

    2. POST /generate-idempotency-key ​

    • Deskripsi: Membuat single-use idempotency key berdurasi 5 menit yang terikat pada hash token otentikasi user dan service number.
    • Autentikasi / Guard: Bearer Token (doIntrospection, checkAccessAgentByServiceNumber)
    • Request Body:
      json
      {
        "type": "GENERATE_BILLING",
        "serviceNumber": "102202608310001"
      }
    • Response Payload (200 OK):
      json
      {
        "data": {
          "idempotency_key": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
          "expires_in": 300,
          "valid_until": "2026-08-31T09:02:00.000Z"
        },
        "message": "Idempotency key berhasil dibuat",
        "statusCode": 200,
        "success": true
      }
    • Response Error (400 Bad Request):
      json
      {
        "data": null,
        "message": "Validation Failed",
        "statusCode": 400,
        "success": false,
        "errors": [
          { "field": "body.type", "message": "Tipe idempotency tidak valid" }
        ]
      }

    3. POST /request-billing ​

    • Deskripsi: Mengajukan pembuatan tagihan Single Billing secara manual setelah kelayakan terpenuhi dan validasi idempotency lolos.
    • Autentikasi / Guard: Bearer Token (doIntrospection, validateIdempotency('GENERATE_BILLING'), checkAccessAgentByServiceNumber)
    • Request Header:
      • x-idempotency-key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
    • Request Body:
      json
      {
        "serviceNumber": "102202608310001"
      }
    • Response Payload (200 OK):
      json
      {
        "data": {
          "kode_billing": "820260831000123",
          "trx_id_insw": "102202608310001"
        },
        "message": "Proses generate billing berhasil dilakukan",
        "statusCode": 200,
        "success": true
      }
    • Response Error (400 Bad Request):
      json
      {
        "data": null,
        "message": "Idempotency key tidak valid atau sudah kedaluwarsa, silakan muat ulang halaman",
        "statusCode": 400,
        "success": false,
        "errors": []
      }

4. Bukti Teknis (Technical Proof & Audit Trail) ​

Daftar Commit Terkait (Multi-Commit Trail) ​

Commit IDBranchAuthorTanggal & JamRingkasan Perubahan
9a670759feat/manual-billing-requestHermansyah Efendi2026-06-25 17:01refactor(billing): use double negation for parent billing code validation
fe12d383feat/manual-billing-requestHermansyah Efendi2026-06-25 16:06fix(billing): require parent billing code for generated billing status
81399b5ffeat/manual-billing-requestHermansyah Efendi2026-06-25 10:51feat(billing): allow dynamic billing version switching via environment variable
2cb9de06feat/manual-billing-requestHermansyah Efendi2026-06-08 15:35feat(idempotency): add idempotency validation for warta-kapal and cop submission
fcefa795feat/manual-billing-requestHermansyah Efendi2026-06-04 19:32feat(idempotency): replace captcha with token-bound idempotency key system
450a18b1feat/manual-billing-requestHermansyah Efendi2026-06-04 15:00feat(billing): add inquiry-billing endpoint and billing eligibility service

Cuplikan Kode / Implementasi Inti ​

javascript
// app/services/inquiry-billing.service.js
import connection from '../connection.js';
import { BadRequestError } from '../error/response-error.js';
import { checkAjuKedatangan, checkDataBilling, dataDraftTagihan } from '../models/billing_models.js';
import { billingEligibilityService } from './billing-eligibility.service.js';

class InquiryBillingService {
  async getByServiceNumber(serviceNumber) {
    let serviceNumberKeberangkatan = serviceNumber;
    let serviceNumberKedatangan = null;

    if (serviceNumber.substr(0, 3) === "102") {
      serviceNumberKedatangan = await checkAjuKedatangan(serviceNumber);
      if (!serviceNumberKedatangan) {
        throw new BadRequestError("Data kedatangan tidak ditemukan untuk service number ini");
      }
    }

    // 1. Cek kelayakan billing via reusable service
    const eligibility = await billingEligibilityService.checkEligibility(serviceNumber);

    // 2. Cek billing status
    const dataSingleBilling = serviceNumberKeberangkatan ? await checkDataBilling(serviceNumberKeberangkatan) : [];
    const isBillingActive = eligibility.requirements.inaport.required || eligibility.requirements.sinkarkes.required;
    const hasBillingRecord = dataSingleBilling.length > 0 && !!dataSingleBilling[0].kode_billing_induk;
    const isBillingExpired = hasBillingRecord && dataSingleBilling[0].is_expired === true;
    const isPaid = hasBillingRecord && dataSingleBilling[0].fl_paid === true;

    return {
      billing_status: {
        is_billing: isBillingActive,
        is_generated: hasBillingRecord && !isBillingExpired,
        is_expired: isBillingExpired,
        id_billing: (hasBillingRecord && !isBillingExpired) ? dataSingleBilling[0].trx_id_insw : null,
        is_paid: isPaid
      },
      eligibility: {
        is_eligible: eligibility.is_eligible,
        message: eligibility.message,
        requirements: eligibility.requirements
      }
    };
  }
}

export const inquiryBillingService = new InquiryBillingService();

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

A. Verifikasi Unit Test (Development Stage) ​

  • Test Suite File: tests/services/inquiry-billing.service.test.js, tests/services/request-billing.service.test.js, tests/services/billing-eligibility.service.test.js, tests/services/idempotency.service.test.js
  • Status Eksekusi CLI: PASSED
text
 RUN  v4.1.10 D:/WORK/LNSW/pengangkut-backend

 ✓ tests/services/request-billing.service.test.js (9 tests) 23ms
 ✓ tests/services/billing-eligibility.service.test.js (17 tests) 16ms
 ✓ tests/services/inquiry-billing.service.test.js (6 tests) 14ms
 ✓ tests/services/idempotency.service.test.js (11 tests) 15ms

 Test Files  4 passed (4)
      Tests  43 passed (43)
   Start at  08:57:11
   Duration  661ms (transform 269ms, setup 0ms, import 696ms, tests 69ms, environment 1ms)

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 Agen Pengangkut / PPJK
  • Postman Collection File: manual_billing_request_postman_collection.json
  • Hasil Pengujian UAT: PENDING
  • Catatan QA: Unit test suite (43 tests) telah lulus secara empiris pada local test runner. Menunggu deployment ke environment Staging untuk pengujian E2E menggunakan Postman Collection & UI Flow.

A. Konfigurasi Variabel Postman Collection ​
Variable NameDefault Value / TemplateFungsi & Deskripsi
baseUrlhttps://demo-backend.insw.go.id/pengangkut/apiBase URL utama service Pengangkut Core
singleBillingBaseUrlhttps://demo-backend.insw.go.id/single-billingBase URL service Single Billing
apiKeydSGZSMBOtdh4dWLYwW7qT38nVroknMHnAPI Key pengangkut
bearerTokenKZ6IlAA4NxocmCozKC71xaYymS7sMAgM_94h0gftFnGBearer token otentikasi user
singleBillingToken9kuq3cU3AXa5XGPV2Pyec1kGZesDJPxXBearer token otentikasi API DJA Simponi / Single Billing
inswKemenhubKeyxxxKey untuk header insw-kemenhub-key pada respon LK3
inswKemenkesKeyxxxKey otentikasi integrasi Kemenkes
billingKemenhubKeyxxxKey untuk simponi_req.key (Tagihan Inaportnet Kemenhub KL 022)
billingKemenkesKeyxxxKey untuk simponi_req.key (Tagihan Sinkarkes Kemenkes KL 024)
serviceNumber102019B83694CNomor Aju / Service Number Pengangkut valid (Keberangkatan)
serviceNumberKedatangan101019543877BNomor Aju / Service Number Pengangkut valid (Kedatangan)
invalidServiceNumber1029999999999Service Number buatan untuk uji data tidak ditemukan
idempotencyKey""Diisi otomatis oleh skrip pengujian Postman saat Key Gen
kodeBilling820260710740418Kode billing target untuk simulasi callback pelunasan Simponi DJA

B. Skenario Simulasi Injeksi Data Prasyarat (Data Seeding) ​

Sebelum menguji alur kelayakan billing, jalankan 5 cURL berikut untuk mengisi data prasyarat di database:

1. Simulasi Approval LK3 Kemenhub (POST /warta-kapal/send/putResponse) ​
bash
curl --location 'https://demo-backend.insw.go.id/pengangkut/api/warta-kapal/send/putResponse' \
--header 'Content-Type: application/json' \
--header 'insw-kemenhub-key: xxx' \
--data '{
  "no_aju": "102019B83694C",
  "tipe": "LK3",
  "nomor_layanan": "BDN.IDMAK.2609.000140",
  "status": 2,
  "message": "",
  "nomor_produk": "SL003.IDMAK.0926.000142",
  "waktu_approve": "2026-09-09 11:10:22",
  "link_produk": "https://simpadu-inaportnet.dephub.go.id/index.php/document/pdfs/lk3_nakhoda/SL003.DN.IDMAK.2609.000139",
  "kode_dermaga": ""
}'
2. Simulasi Tagihan Inaportnet Kemenhub (POST /tagihan) ​
bash
curl --location 'https://demo-backend.insw.go.id/pengangkut/api/tagihan' \
--header 'Content-Type: application/json' \
--data '{
    "simponi_req": {
        "method": "billingcode",
        "key": "xxx",
        "data": [
            {
                "header": [
                    "102019B83694C",
                    "INV.IDMAK.2609.000003",
                    "",
                    null,
                    "022",
                    "04",
                    "413439",
                    "F",
                    "1",
                    834660,
                    "Labuh-PU-PT. JELAJAH LAUT NUSANTARA",
                    "413439",
                    "014844831042001"
                ],
                "detail": [
                    [
                        "Labuh-PU-PT. JELAJAH LAUT NUSANTARA",
                        "000401",
                        "201615C",
                        "425513",
                        90,
                        9274,
                        "Per GT/15 Hari",
                        834660,
                        "19",
                        "51",
                        0
                    ]
                ]
            },
            {
                "header": [
                    "102019B83694C",
                    "INV.IDMAK.2609.000009",
                    "",
                    null,
                    "022",
                    "04",
                    "287473",
                    "F",
                    "1",
                    175000,
                    "VTS-PU-IDMAK-PT. JELAJAH LAUT NUSANTARA",
                    "287473",
                    "014844831042001"
                ],
                "detail": [
                    [
                        "VTS-PU-IDMAK-PT. JELAJAH LAUT NUSANTARA",
                        "000930",
                        "201615C",
                        "425517",
                        175000,
                        1,
                        "Per Kapal",
                        175000,
                        "19",
                        "51",
                        0
                    ]
                ]
            }
        ]
    }
}'
3. Simulasi Tagihan Sinkarkes Kemenkes Keberangkatan (POST /tagihan) ​
bash
curl --location 'https://demo-backend.insw.go.id/pengangkut/api/tagihan' \
--header 'Content-Type: application/json' \
--data '{
    "simponi_req": {
        "method": "billingcode",
        "key": "xxx",
        "data": {
            "header": [
                "102019B83694C",
                "58830",
                "SIxxxxx",
                "2026-09-08 07:23:44",
                "024",
                "05",
                "416145",
                "F",
                "1",
                85000,
                "PT JELAJAH LAUT NUSANTARA",
                "416145",
                "014844831042001"
            ],
            "detail": [
                [
                    "PT JELAJAH LAUT NUSANTARA",
                    "001111",
                    "2024045",
                    "425314",
                    85000,
                    1,
                    "Per Pemeriksaan Per Kapal",
                    85000,
                    "19",
                    "51"
                ]
            ]
        }
    }
}'
4. Simulasi Tagihan Sinkarkes Kemenkes Kedatangan (POST /tagihan) ​
bash
curl --location 'https://demo-backend.insw.go.id/pengangkut/api/tagihan' \
--header 'Content-Type: application/json' \
--data '{
    "simponi_req": {
        "method": "billingcode",
        "key": "xxx",
        "data": {
            "header": [
                "101019543877B",
                "58830",
                "SIxxxxx",
                "2026-09-08 07:23:44",
                "024",
                "05",
                "416145",
                "F",
                "1",
                85000,
                "PT JELAJAH LAUT NUSANTARA",
                "416145",
                "014844831042001"
            ],
            "detail": [
                [
                    "PT JELAJAH LAUT NUSANTARA",
                    "001111",
                    "2024045",
                    "425314",
                    85000,
                    1,
                    "Per Pemeriksaan Per Kapal",
                    85000,
                    "19",
                    "51"
                ]
            ]
        }
    }
}'
5. Simulasi Notifikasi Pembayaran Lunas Simponi DJA ke Single Billing (POST /single-billing/dja/activeinquiry_v2) ​
bash
curl --location 'https://demo-backend.insw.go.id/single-billing/dja/activeinquiry_v2' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer 9kuq3cU3AXa5XGPV2Pyec1kGZesDJPxX' \
--data '{
    "simponi_req": {
        "method": "activeinquiry_v2",
        "data": [
            "8186790",             // [0] ID Tagihan / Trx Simponi DJA
            "2026090915908818403", // [1] Trx ID INSW / ID Billing Single Billing
            "820260710740418",     // [2] Kode Billing Induk Simponi
            "FBDC361QVML892TK",    // [3] Nomor Transaksi Penerimaan Negara (NTPN)
            "630523721197",        // [4] Nomor Transaksi Bank (NTB) / Ref Bank
            "2026-09-09 14:44:42", // [5] Tanggal & Jam Pembayaran (YYYY-MM-DD HH:mm:ss)
            "520008000990",        // [6] Nomor Rekening Kas Negara / Bank Persepsi
            "7015",                // [7] Kode Channel Bayar / Kode Bank Persepsi
            "2026-09-09 00:00:00"  // [8] Tanggal Buku / Tanggal Setor Kas Negara
        ]
    }
}'

C. Matriks Pengujian Fisik (13 Skenario Detailed) ​
1. Positive Cases (Happy Path) ​
  1. GET /inquiry-billing (Status Eligible):

    • Trigger / Request: Query serviceNumber=102019B83694C sesudah Data Seeding diselesaikan.
    • Ekspektasi Response: HTTP 200 OK, eligibility.is_eligible = true, eligibility.message = "Semua syarat terpenuhi".
    • Verifikasi Database / Log: Seluruh persyaratan LK3, Tagihan Warta, dan Tagihan Sinkarkes berstatus completed = true.
  2. POST /generate-idempotency-key (Valid Key Generation):

    • Trigger / Request: Body {"type":"GENERATE_BILLING", "serviceNumber":"102019B83694C"}.
    • Ekspektasi Response: HTTP 200 OK, menghasilkan UUID idempotency_key dengan expires_in: 300 (5 menit).
    • Verifikasi Database / Log: Kunci tersimpan di Redis dengan namespace token hash sesi user.
  3. POST /request-billing (Eksekusi Billing Berhasil):

    • Trigger / Request: Header x-idempotency-key: <UUID>, Body {"serviceNumber":"102019B83694C"}.
    • Ekspektasi Response: HTTP 200 OK, message = "Proses generate billing berhasil dilakukan", Kode Billing Simponi terbit.
    • Verifikasi Database / Log: Idempotency key terkonsumsi dan terhapus dari Redis; record subbiller_hdr berhasil terbuat.
2. Negative Cases — Validation & Authorization Errors ​
  1. GET /inquiry-billing (Missing Query serviceNumber):

    • Trigger / Request: Dipanggil tanpa query parameter serviceNumber.
    • Ekspektasi Response: HTTP 400 Bad Request, message = "Validation Failed", error detail pada field query.serviceNumber.
  2. GET /inquiry-billing (Data Service Number Tidak Ditemukan):

    • Trigger / Request: Query serviceNumber=1029999999999.
    • Ekspektasi Response: HTTP 400 Bad Request, message = "Data kedatangan tidak ditemukan untuk service number ini".
  3. POST /generate-idempotency-key (Action Type Tidak Valid):

    • Trigger / Request: Body {"type":"INVALID_ACTION_TYPE", "serviceNumber":"102019B83694C"}.
    • Ekspektasi Response: HTTP 400 Bad Request, message = "Validation Failed".
  4. POST /generate-idempotency-key (Tanpa Header Authorization):

    • Trigger / Request: Request dikirim tanpa header Authorization: Bearer <token>.
    • Ekspektasi Response: HTTP 401 Unauthorized / 400 Bad Request, message = "Token otentikasi tidak ditemukan".
3. Negative Cases — Ineligibility & Business Logic Errors ​
  1. POST /request-billing (Service Number Belum Eligible / Belum LK3):

    • Trigger / Request: Request billing pada serviceNumber yang belum disetujui LK3-nya.
    • Ekspektasi Response: HTTP 400 Bad Request, message = "LK3 Belum Terselesaikan".
  2. POST /request-billing (Duplicate Submission / Kode Billing Masih Aktif):

    • Trigger / Request: Request billing diulang ketika kode billing aktif sudah terbit di database.
    • Ekspektasi Response: HTTP 400 Bad Request, message = "Billing atas nomor aju layanan 102019B83694C belum kedaluwarsa atau masih berlaku. Silahkan cek data tagihan atas billing yang sudah ada.".
4. Negative Cases — Idempotency & Security Boundary Errors ​
  1. POST /request-billing (Missing Header x-idempotency-key):

    • Trigger / Request: Request billing dikirim tanpa header x-idempotency-key.
    • Ekspektasi Response: HTTP 400 Bad Request, message = "Header x-idempotency-key wajib disertakan".
  2. POST /request-billing (Key Palsu / Expired):

    • Trigger / Request: Header x-idempotency-key: 99999999-9999-9999-9999-999999999999.
    • Ekspektasi Response: HTTP 400 Bad Request, message = "Idempotency key tidak valid atau sudah kedaluwarsa, silakan muat ulang halaman".
  3. POST /request-billing (Payload Mismatch / Service Number Berbeda):

    • Trigger / Request: Key dibentuk untuk serviceNumber A, tetapi dikirim dengan body {"serviceNumber": "serviceNumber B"}.
    • Ekspektasi Response: HTTP 400 Bad Request, message = "Idempotency Key tidak valid!".
  4. POST /request-billing (Replay Attack / Re-use Key yang Sudah Dikonsumsi):

    • Trigger / Request: Mengirimkan request billing dua kali berturut-turut menggunakan x-idempotency-key yang sama.
    • Ekspektasi Response: Request pertama 200 OK (key terhapus dari Redis), Request kedua 400 Bad Request (Idempotency key tidak valid atau sudah kedaluwarsa...).

Bukti Response Verifikasi Staging (Diisi saat UAT) ​
json
{
  "status": "PENDING",
  "note": "Menunggu deployment ke environment Staging untuk eksekusi skenario uji QA."
}

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