Skip to content

[ND-374/LNSW.2/2026] Penyesuaian Skema Penarikan Data Jenis Kapal pada Superset Kapal SSm Pengangkut dan Validasi Referensi Kapal Asing & Non-Trayek ​

Modul: Pengangkut | Tipe: Change Request | Stage Saat Ini: STAGING | Tanggal Dokumen: 2026-09-29

STAGING STAGE (UAT VERIFICATION)

Dokumen ini sedang berada dalam tahap STAGING. Perubahan logika backend dan validasi referensi jenis kapal telah diterapkan di lingkungan Staging (UAT) untuk proses verifikasi QA.


1. Informasi Tugas & Referensi Bisnis ​

  • Task ID / Nomor Referensi: ND-374/LNSW.2/2026

  • Sumber Instruksi: Nota Dinas Resmi ND-374/LNSW.2/2026

  • Dokumen Referensi / Lampiran:

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

  • Modul Terkait: Pengangkut

  • Service / Repository: pengangkut-backend

  • PIC Pengembang: Hermansyah Efendi

  • Latar Belakang & Ringkasan Kebutuhan: Pengisian data "Jenis Kapal" pada SSm Pengangkut sebelumnya berupa isian manual mengacu pada tabel referensi. Pada kapal Trayek berbendera Indonesia, data "Jenis Kapal" sebenarnya telah tersedia pada data trayek Inaportnet yang ditarik saat inisiasi permohonan. Perbedaan pengisian manual oleh agen pelayaran dengan data tarikan trayek kerap memicu kendala dalam permohonan warta kapal serta penerbitan PKK maupun SPB di Inaportnet. Oleh karena itu, dilakukan penyesuaian sistem: untuk kapal Trayek berbendera Indonesia, data Jenis Kapal otomatis diambil dari tarikan data trayek dan dikunci (tidak dapat diubah manual); sedangkan untuk kapal Non-Trayek (baik bendera Indonesia maupun asing) serta kapal Trayek berbendera asing, pengisian Jenis Kapal divalidasi ketat terhadap tabel referensi tr_tipe_kapal karena tidak ada penarikan data otomatis dari Inaportnet.

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

    • [x] Standardisasi elemen data: mengganti validasi legacy type menjadi type_ship pada ShipNonTrayekIdentitySchema di app/validations/ship-identity.validation.js
    • [x] Implementasi private method validasi referensi jenis kapal (#validateShipType(isNonTrayek, identity)) pada app/services/ship.service.js menggunakan cache referensi tr_tipe_kapal (REF_JENIS_KAPAL_ALL)
    • [x] Penerapan aturan kondisional validasi: kapal Non-Trayek (seluruh bendera) dan kapal Trayek asing (flag !== 'ID') wajib valid terhadap referensi; kapal Trayek bendera Indonesia (flag === 'ID') dilewati karena bersumber dari tarikan trayek Inaportnet
    • [x] Fallback kompatibilitas kolom legacy: pemetaan type dan kind otomatis mengacu pada type_ship di #prepareShipDataForSaving
    • [x] Sinkronisasi pemetaan data type_ship pada tarikan Inaportnet (kapalJenisDetail / jenisDetail), penataan payload raw, dan penguncian override trayek
    • [x] Penambahan dan pembaruan unit test komprehensif pada tests/services/ship.service.test.js dan tests/validations/ship-identity.validation.test.js
    • [x] Penyusunan skenario uji mendalam dan Postman Test Suite Collection (ND-374_penyesuaian_skema_penarikan_jenis_kapal_postman_collection.json)

2. Deployment Lifecycle ​

#EnvironmentTanggal & WaktuPICStatusCatatan
1Development2026-09-29 11:33Hermansyah EfendiDEPLOYEDBranch: development
2Staging (UAT)2026-10-03 15:55achmad fauziDEPLOYEDRelease Candidate v1.18.5 via MR !2083. Co-deployed: [ND-374-LNSW.2-2026, ND-274-LNSW.2-2026, TASK-FIX-COMMENTED-BIND-PARAMS-V1.18.4, TASK-UNIFY-DB-POOL-AND-HEALTH-OBSERVABILITY, 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-10-03StagingPromosi ke STAGING via MR !2083 (Release Candidate v1.18.5)Hermansyah Efendi
2026-09-30DevelopmentPelengkapan skenario QA Playbook (mapping route type 0–4, cURL commands) dan penambahan Postman Collection ND-374Hermansyah Efendi
2026-09-29DevelopmentPenyesuaian aturan validasi: memperluas validasi referensi untuk seluruh kapal Non-Trayek (ID & Asing) serta fallback type/kind legacyHermansyah Efendi
2026-09-29DevelopmentInisialisasi draft audit log dari context block backend (commit a180e191 s.d. 806eeb93)Hermansyah Efendi

3. Analisis Teknis & Rincian Perubahan ​

  • Alur Logika & Bisnis:

    1. Klasifikasi Route Type (Tipe Trayek): Sistem mengkategorikan rute kapal berdasarkan nilai integer route_type / id_route_type:

      KodeNama TrayekKeterangan Validasi type_ship
      0NON-TRAYEKWajib Validasi Referensi (Bendera ID maupun Asing, karena tidak ada data trayek Inaportnet)
      1LINERBendera ID: Otomatis & Terkunci; Bendera Asing: Wajib Validasi Referensi
      2TRAMPERBendera ID: Otomatis & Terkunci; Bendera Asing: Wajib Validasi Referensi
      3PKKABendera ID: Otomatis & Terkunci; Bendera Asing: Wajib Validasi Referensi
      4PPKNBendera ID: Otomatis & Terkunci; Bendera Asing: Wajib Validasi Referensi
    2. Validasi Jenis Kapal Kondisional: Private method #validateShipType(isNonTrayek, identity) di ShipService mengevaluasi kondisi:

      javascript
      const shouldValidate = isNonTrayek || (flag && flag !== 'ID');
      • Kapal Non-Trayek (isNonTrayek = true): Baik berbendera asing maupun Indonesia (flag === 'ID'), wajib divalidasi ke tabel referensi tr_tipe_kapal (kode_tipe_kapal) melalui cache Redis REF_JENIS_KAPAL_ALL (TTL 12 jam) karena penarikan data ke Inaportnet tidak dilakukan (getDataShipInaport mengembalikan data kosong).
      • Kapal Trayek (isNonTrayek = false): Jika berbendera Indonesia (flag === 'ID'), validasi referensi dilewati karena data type_ship otomatis ditarik dari Inaportnet (kapalJenisDetail / jenisDetail) dan dikunci via oldShip.type_ship. Jika berbendera asing, validasi referensi tetap diberlakukan.
    3. Backward Compatibility Database: Pada method #prepareShipDataForSaving, kolom legacy type dan kind diberi fallback ke identity.type_ship:

      javascript
      type: identity.type || identity.type_ship,
      kind: identity.kind || identity.type_ship,

      Hal ini menjaga persistensi tabel database yang masih mereferensikan kolom warisan tersebut.

    4. Penyelarasan Schema Zod: Field type pada ShipNonTrayekIdentitySchema digantikan oleh type_ship dengan sanitasi noForbiddenSymbols("Jenis Kapal").min(1).max(100).

  • Perubahan Database (DDL / DML):

    sql
    -- Tidak ada perubahan skema database (DDL/DML) untuk task ini.
    -- Penyesuaian murni pada layer service logic, schema validasi, serta konsumsi tabel referensi pengangkut.tr_tipe_kapal yang telah tersedia.
  • Kontrak API & Response Payload:

    1. GET /ref/jenis-kapal-all ​

    • Deskripsi: Mengambil seluruh daftar master data referensi jenis kapal (tr_tipe_kapal) yang dicache di Redis REF_JENIS_KAPAL_ALL (TTL 12 jam).
    • Autentikasi / Guard: Bearer Token
    • Request Header:
      • Authorization: Bearer <token>
      • Accept: application/json
    • Response Payload (200 OK):
      json
      {
        "code": "200",
        "message": "success",
        "datas": [
          {
            "id_tipe_kapal": "1",
            "kode_tipe_kapal": "1.3.8",
            "tipe_kapal": "TONGKANG MINYAK (OIL BARGE)",
            "id_map": "8",
            "enum_tipe": "tongkang"
          },
          {
            "id_tipe_kapal": "2",
            "kode_tipe_kapal": "1.4.5",
            "tipe_kapal": "TONGKANG PENGANGKUT GAS (LPG)",
            "id_map": "8",
            "enum_tipe": "tongkang"
          },
          {
            "id_tipe_kapal": "15",
            "kode_tipe_kapal": "1.7.1",
            "tipe_kapal": "KAPAL BARANG UMUM (GENERAL CARGO)",
            "id_map": "1",
            "enum_tipe": "barang"
          }
        ]
      }

    2. PUT /super-set/ship/:serviceNumber & POST /super-set/ship ​

    • Deskripsi: Simpan (POST) dan perbarui (PUT) data Superset Kapal (kedatangan & keberangkatan).
    • Autentikasi / Guard: Bearer Token (doIntrospection, checkAccessAgentByServiceNumber / checkAccessShipByServiceNumber)
    • Request Header / Params:
      • Header: Authorization: Bearer <token>, Content-Type: application/json
      • Params: serviceNumber (pada PUT)
    • Request Body Payload (Struktur Lengkap):
      json
      {
        "service_number": "101019B86C56C",
        "registration_number": "AL.203/2000/04786/25937/25",
        "trayek": "pkka",
        "identity": {
          "code": "030108",
          "name": "EVER BEFIT",
          "flag": "TW",
          "siupal": "BX-79/AL203",
          "kind": "KAPAL MUATAN PETI KEMAS / CONTAINER",
          "domicile": null,
          "call_sign": "aaaa",
          "inmarsat_number": "aaaa",
          "cso_name": null,
          "cso_phone": null,
          "voyage_number": "aaaaVVVV",
          "captain_name": "HOANG VU DAI",
          "shipping_type": "INTERNATIONAL",
          "route": "Shekou/China, Semarang/Tanjung Emas, Tanjung Priok, Kaohsiung",
          "permanent_activity_ship": null,
          "imo": "9784128",
          "type_ship": "1.3.8",
          "registration_port": "CINA",
          "mmsi": "asd"
        },
        "specification": {
          "ship_width": "32.80",
          "back_draft": "1",
          "max_draft": "1",
          "front_draft": "1",
          "air_height": null,
          "minimum_safe_manning": "1",
          "length_overall": "211.00",
          "driving_force": "24260.00",
          "TahunPembuatan": 2017,
          "deadweight_ton": "36972.00",
          "gross_ton": "32691.00"
        },
        "ports": {
          "transit_port": null,
          "unloading_port": null,
          "next_port": null,
          "before_origin_port": "TWKHH",
          "raw_route": [
            {
              "id": 1,
              "port_flag": "CN",
              "port_code": "CNSHK",
              "port_name": "Shekou/China"
            },
            {
              "id": 2,
              "port_flag": "ID",
              "port_code": "IDSRG",
              "port_name": "Semarang/Tanjung Emas"
            },
            {
              "id": 3,
              "port_flag": "ID",
              "port_code": "IDJKT",
              "port_name": "Tanjung Priok"
            },
            {
              "id": 4,
              "port_flag": "TW",
              "port_code": "TWKHH",
              "port_name": "Kaohsiung"
            }
          ],
          "origin_nation": "CN",
          "origin_port": "CNSHK",
          "destination_port": "TWKHH",
          "landing_site": "IDJKT.A01",
          "windows_status": null,
          "pandu": []
        },
        "dates": {
          "estimate_arrival": "2026-10-01 00:00",
          "actual_arrival": null,
          "unload": null,
          "load": null,
          "estimate_depature": "2026-10-02 00:00",
          "actual_depature": null
        }
      }
      (Catatan: Pada POST /super-set/ship, field id_ship dihilangkan).
    • Response Payload (200 OK / 201 Created):
      json
      {
        "data": {
          "service_number": "101019B86C56C"
        },
        "message": "Data kapal kedatangan berhasil disimpan",
        "statusCode": 200,
        "success": true
      }
    • Response Error (400 Bad Request):
      json
      {
        "data": null,
        "message": "Jenis kapal tidak valid atau tidak terdaftar di referensi!",
        "statusCode": 400,
        "success": false
      }

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

Daftar Commit Terkait (Multi-Commit Trail) ​

Commit IDBranchAuthorTanggal & JamRingkasan Perubahan
806eeb93277978d514445864f02a7a8adf878b69developmentHermansyah Efendi2026-09-29 10:50Merge branch 'development' of https://scm.insw.go.id/lnsw/pengangkut/pengangkut-backend into development
a5647c055642dbd972ecdeff9a93c8867576ceb6developmentHermansyah Efendi2026-09-29 10:41mend
1bf6595fd978004c73bf42c77dc1252aca6b30b3developmentHermansyah Efendi2026-09-29 10:41mend
a180e191610352eb26dc08640416f592c4e2d354developmentHermansyah Efendi2026-09-29 10:40feat(ship): validate ship type against reference for foreign vessels

Cuplikan Kode / Implementasi Inti ​

javascript
// app/services/ship.service.js
class ShipService {
    async #validateShipType(isNonTrayek, identity) {
        const flag = identity?.flag?.trim()?.toUpperCase();
        const shouldValidate = isNonTrayek || (flag && flag !== 'ID');

        if (shouldValidate) {
            const cacheKey = CacheKeys.REF_JENIS_KAPAL_ALL();
            const refJenisKapal = await getOrSetCache(cacheKey, REF_CACHE_TTL, () => getJenisKapalAll());
            const validCodes = new Set(
                Array.isArray(refJenisKapal)
                    ? refJenisKapal.map((item) => item.kode_tipe_kapal?.trim()).filter(Boolean)
                    : []
            );

            const typeShip = identity?.type_ship?.trim();
            if (!typeShip || !validCodes.has(typeShip)) {
                return false;
            }
            return true;
        }
        return true;
    }

    #prepareShipDataForSaving(data) {
        // ...
        const result = {
            // ...
            type: identity.type || identity.type_ship,
            kind: identity.kind || identity.type_ship,
            type_ship: identity.type_ship,
        };
        // ...
    }

    // Pemanggilan di dalam upsertSupersetShip sebelum db.transaction:
    const validateShipType = await this.#validateShipType(isNonTrayek, identity);
    if (!validateShipType) throw new BadRequestError(`Jenis kapal tidak valid atau tidak terdaftar di referensi!`);
javascript
// app/validations/ship-identity.validation.js
export const ShipNonTrayekIdentitySchema = BaseIdentitySchema.extend({
    imo: noForbiddenSymbols("IMO").min(1).max(100),
    code: noForbiddenSymbols("Code").min(1).max(100),
    name: noForbiddenSymbols("Name").min(1).max(100),
    flag: noForbiddenSymbols("Flag").min(1).max(100),
    siupal: noForbiddenSymbols("Siupal").min(1).max(100),
    type_ship: noForbiddenSymbols("Jenis Kapal").min(1).max(100),
}).superRefine(validateTongkangShipCaptain);

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

A. Verifikasi Unit Test (Development Stage) ​

  • Test Suite File:
    • tests/services/ship.service.test.js
    • tests/validations/ship-identity.validation.test.js
  • Status Eksekusi CLI: PASSED
Cuplikan Kode Unit Test ​
javascript
// tests/services/ship.service.test.js
it('skips type_ship validation when ship is trayek with flag ID', async () => {
  agentService.findOneByServiceNumber.mockResolvedValueOnce({
    id_agent: 16, registration_ship_number: 'REG7', id_route_type: 1, // Trayek (LINER)
  });
  Ship.findOne.mockResolvedValueOnce(null);
  db.query.mockResolvedValueOnce([{ estimate_locked: false, actual_locked: false }]);
  Ship.create.mockResolvedValueOnce({ id_ship: 34 });

  const result = await shipService.upsertSupersetShip({
    service_number: '101-ID-FLAG',
    identity: { flag: 'ID', type_ship: 'ANYTHING_OR_EMPTY' },
    specification: {},
    dates: {},
    ports: {
      origin_port: 'IDTPP',
      destination_port: 'IDAKR',
      raw_route: [{ port_code: 'IDTPP' }, { port_code: 'IDAKR' }],
      pandu: [],
    },
  }, { userId: 1 }, {});

  expect(result).toBeDefined();
});

it('throws BadRequestError when non-trayek ship with flag ID has invalid type_ship', async () => {
  agentService.findOneByServiceNumber.mockResolvedValueOnce({
    id_agent: 17, registration_ship_number: 'REG8', id_route_type: 0, // Non-Trayek
  });
  Ship.findOne.mockResolvedValueOnce(null);
  db.query.mockResolvedValueOnce([{ estimate_locked: false, actual_locked: false }]);

  await expect(shipService.upsertSupersetShip({
    service_number: '101-NON-TRAYEK-ID',
    identity: { flag: 'ID', type_ship: 'INVALID_CODE' },
    specification: {},
    dates: {},
    ports: { pandu: [] },
  }, { userId: 1 }, {})).rejects.toThrow('Jenis kapal tidak valid atau tidak terdaftar di referensi!');
});
Output Raw CLI Test Runner ​
text
 RUN  v4.1.11 D:/WORK/LNSW/pengangkut-backend

 ✓ tests/validations/ship-identity.validation.test.js (3 tests) 13ms
 ✓ tests/services/ship.service.test.js (47 tests) 65ms

 Test Files  2 passed (2)
      Tests  50 passed (50)
   Start at  11:30:54
   Duration  652ms (transform 285ms, setup 0ms, import 498ms, tests 78ms, 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.

Pemetaan Parameter Route Type (Trayek) ​
Nilai Integer (route_type / id_route_type)Nama RuteSifat Validasi type_ship
0NON-TRAYEKWajib Lolos Referensi (Bendera ID maupun Asing)
1LINERBendera ID: Bypass Inaportnet; Bendera Asing: Wajib Referensi
2TRAMPERBendera ID: Bypass Inaportnet; Bendera Asing: Wajib Referensi
3PKKABendera ID: Bypass Inaportnet; Bendera Asing: Wajib Referensi
4PPKNBendera ID: Bypass Inaportnet; Bendera Asing: Wajib Referensi
Pemetaan Variabel Lingkungan & Service Number (Postman Collection) ​
Kategori Rute & BenderaVariabel PostmanDefault Service Number StagingNomor Registrasi IzinEntitas Kapal Uji
Trayek TRAMPER (Flag ID)101019B86C4B4AL.103/2000/72476/90951/25VARIA USAHA 9
Trayek PKKA (Flag Non-ID / Asing)101019B86C56CAL.203/2000/04786/25937/25EVER BEFIT
NON-TRAYEK (Semua Bendera)101019B66BB12(string kosong "")MV. DOLPHIN 82

Alur Prasyarat Pengujian (Frontend) & Penanganan Kendala Gateway Inaportnet ​
  1. Inisiasi Data Permohonan Agen (Melalui Frontend / UI):

    • Sebelum melakukan simulasi penyimpanan atau update data kapal (PUT /super-set/ship/:serviceNumber), tester/QA wajib melakukan pembuatan data permohonan Agen terlebih dahulu melalui UI Frontend SSm Pengangkut.
    • Pada formulir pembuatan data Agen, pengguna wajib menginput:
      • Jenis Trayek: Pilih jenis trayek sesuai skenario uji, misalnya TRAMPER (kapal domestik) atau PKKA (kapal asing).
      • Nomor Registrasi: Masukkan nomor izin operasional yang sah, misalnya:
        • Kapal Domestik Trayek ID: AL.103/2000/72476/90951/25
        • Kapal Berbendera Asing: AL.203/2000/04786/25937/25
    • Setelah data Agen berhasil tersimpan, sistem akan membentuk nomor aju (service_number).
    • Tester selanjutnya dapat berpindah ke halaman / tab Kapal untuk mengeksekusi skenario uji (baik melalui form UI maupun menggunakan endpoint API Postman/cURL dengan menyertakan service_number yang telah terbentuk).
  2. Mitigasi Kendala Gateway Inaportnet (Transient Error 500 / TLS Socket Drop):

    • Saat inisiasi permohonan di halaman Agen, backend SSm Pengangkut melakukan komunikasi real-time ke server API Inaportnet untuk validasi izin dan penarikan data warta/trayek.
    • Dikarenakan environment development/staging Inaportnet memiliki kestabilan infrastruktur yang fluktuatif, proses pembuatan aju terkadang mengalami kegagalan dengan response HTTP 500 dan log socket TLS berikut:
      text
      "Client network socket disconnected before secure TLS connection was established"
      "D04D3E8B597F0000:error:0A0003F2:SSL routines:ssl3_read_bytes:ssl/tls alert unexpected message:../deps/openssl/openssl/ssl/record/rec_layer_s3.c:918:SSL alert number 10\n"
    • Tindakan QA / Mitigasi: Error di atas murni merupakan transient SSL/TLS handshake timeout dari gateway Inaportnet dan bukan bug internal backend SSm Pengangkut. QA disarankan melakukan submit ulang (retry) beberapa saat kemudian hingga koneksi socket Inaportnet berhasil terjalin.

Skenario Pengujian QA Detail ​
Skenario 0 — Pengambilan Master Referensi Jenis Kapal Valid ​
  • Tujuan: Mendapatkan daftar seluruh kode jenis kapal (kode_tipe_kapal) yang sah.
  • Trigger / cURL:
    bash
    curl --url 'https://api-stg.insw.go.id/api/pengangkut/ref/jenis-kapal-all' \
      -H 'Authorization: Bearer <TOKEN>' \
      -H 'Accept: application/json'
  • Ekspektasi Response: HTTP 200 OK, datas berisi array objek dengan kolom kode_tipe_kapal (contoh: "1.3.8", "1.4.5", "1.7.1").

Skenario 1 — Kapal Trayek Berbendera Indonesia (Otomatis & Terkunci) ​
  • Pre-kondisi: Permohonan AJU kapal Trayek TRAMPER (id_route_type = 2, registration_number = 'AL.103/2000/72476/90951/25', service_number = '101019B86C4B4') berbendera Indonesia (flag = 'ID'). Data warta telah tersinkronisasi dengan Inaportnet.
  • Trigger / cURL:
    bash
    curl --location --request PUT 'https://api-stg.insw.go.id/api/pengangkut/super-set/ship/101019B86C4B4' \
      --header 'Authorization: Bearer <TOKEN>' \
      --header 'Content-Type: application/json' \
      --data '{
        "service_number": "101019B86C4B4",
        "registration_number": "AL.103/2000/72476/90951/25",
        "trayek": "tramper",
        "identity": {
          "code": "2010 Ba No. 2100/L",
          "name": "VARIA USAHA 9",
          "flag": "ID",
          "siupal": "B XXV-1617/AL.58",
          "kind": "tugboat",
          "domicile": null,
          "call_sign": "asd",
          "inmarsat_number": "N/A",
          "cso_name": null,
          "cso_phone": null,
          "voyage_number": "aaaaVVVV",
          "captain_name": "Nakhoda",
          "shipping_type": "DOMESTIK",
          "route": "Asam-Asam, Biringkassi, Balikpapan, Bunati, Banjarmasin, Cirebon, Ciwandan, Cigading, Cilegon, Gresik, Bojonegara, Kintap, Kendawangan, Kumai, Lamongan, Rembang/Sluke, Pulang Pisau, Sungai Puting, Samarinda, Semarang/Tanjung Emas, Marunda, Ketapang, Boom Baru/Palembang, Tanjung Perak, Tanjung Priok, Tarjun, Labuhan Lombok, Tuban, Pontianak, Dumai, Pasang Kayu, Tanjung Redep, Pangkalpinang, Banka, Panjang, Kotabaru, Tarahan, Makassar, Satui/Sei Danau, Sampit, Teluk Adang dan Tanah Grogot, Asam-Asam, Biringkassi, Balikpapan, Bunati, Banjarmasin, Cirebon, Ciwandan, Cigading, Cilegon, Gresik, Bojonegara, Kintap, Kendawangan, Kumai, Lamongan, Rembang/Sluke, Pulang Pisau, Sungai Puting, Samarinda, Semarang/Tanjung Emas, Marunda, Ketapang, Boom Baru/Palembang, Tanjung Perak, Tanjung Priok, Tarjun, Labuhan Lombok, Tuban, Pontianak, Dumai, Pasang Kayu, Tanjung Redep, Pangkalpinang, Banka, Panjang, Kotabaru, Tarahan, Makassar, Satui/Sei Danau, Sampit, Teluk Adang dan Tanah Grogot",
          "permanent_activity_ship": null,
          "imo": "9606998",
          "type_ship": "1.7.13",
          "registration_port": "TANJUNG PRIOK",
          "mmsi": "asd",
          "tugboat": "N/A",
          "perintis_ship": null,
          "docking_ship": null,
          "tongkang_ship": null,
          "tugboat_ship": null,
          "is_coal_carrier": null,
          "is_tol_laut_ship": null,
          "is_new_ship": null,
          "is_sea_trial_ship": null,
          "is_livestock_carrier_ship": null,
          "is_short_sea_ship": null,
          "is_tobe_scrapped_ship": null
        },
        "specification": {
          "ship_width": "8.00",
          "back_draft": "1",
          "max_draft": "1",
          "front_draft": "1",
          "air_height": "1",
          "minimum_safe_manning": "1",
          "length_overall": "26.00",
          "driving_force": "1",
          "TahunPembuatan": "2010",
          "deadweight_ton": "0",
          "gross_ton": "189"
        },
        "ports": {
          "transit_port": null,
          "unloading_port": null,
          "next_port": null,
          "before_origin_port": "IDBPN",
          "raw_route": [
            { "id": 1, "port_flag": "ID", "port_code": "IDIAM", "port_name": "Asam-Asam" },
            { "id": 2, "port_flag": "ID", "port_code": "IDBIR", "port_name": "Biringkassi" },
            { "id": 3, "port_flag": "ID", "port_code": "IDBPN", "port_name": "Balikpapan" },
            { "id": 4, "port_flag": "ID", "port_code": "IDBTI", "port_name": "Bunati" },
            { "id": 5, "port_flag": "ID", "port_code": "IDBDJ", "port_name": "Banjarmasin" },
            { "id": 6, "port_flag": "ID", "port_code": "IDCBN", "port_name": "Cirebon" },
            { "id": 7, "port_flag": "ID", "port_code": "IDCIW", "port_name": "Ciwandan" },
            { "id": 8, "port_flag": "ID", "port_code": "IDCIG", "port_name": "Cigading" },
            { "id": 9, "port_flag": "ID", "port_code": "IDCGN", "port_name": "Cilegon" },
            { "id": 10, "port_flag": "ID", "port_code": "IDGRE", "port_name": "Gresik" },
            { "id": 11, "port_flag": "ID", "port_code": "IDBGA", "port_name": "Bojonegara" },
            { "id": 12, "port_flag": "ID", "port_code": "IDKNP", "port_name": "Kintap" },
            { "id": 13, "port_flag": "ID", "port_code": "IDKDW", "port_name": "Kendawangan" },
            { "id": 14, "port_flag": "ID", "port_code": "IDKUM", "port_name": "Kumai" },
            { "id": 15, "port_flag": "ID", "port_code": "IDLAM", "port_name": "Lamongan" },
            { "id": 16, "port_flag": "ID", "port_code": "IDRNE", "port_name": "Rembang/Sluke" },
            { "id": 17, "port_flag": "ID", "port_code": "IDPPS", "port_name": "Pulang Pisau" },
            { "id": 18, "port_flag": "ID", "port_code": "IDSPT", "port_name": "Sungai Puting" },
            { "id": 19, "port_flag": "ID", "port_code": "IDSRI", "port_name": "Samarinda" },
            { "id": 20, "port_flag": "ID", "port_code": "IDSRG", "port_name": "Semarang/Tanjung Emas" },
            { "id": 21, "port_flag": "ID", "port_code": "IDMRA", "port_name": "Marunda" },
            { "id": 22, "port_flag": "ID", "port_code": "IDKTG", "port_name": "Ketapang" },
            { "id": 23, "port_flag": "ID", "port_code": "IDBRU", "port_name": "Boom Baru/Palembang" },
            { "id": 24, "port_flag": "ID", "port_code": "IDSUB", "port_name": "Tanjung Perak" },
            { "id": 25, "port_flag": "ID", "port_code": "IDJKT", "port_name": "Tanjung Priok" },
            { "id": 26, "port_flag": "ID", "port_code": "IDTAR", "port_name": "Tarjun" },
            { "id": 27, "port_flag": "ID", "port_code": "IDLLO", "port_name": "Labuhan Lombok" },
            { "id": 28, "port_flag": "ID", "port_code": "IDTBN", "port_name": "Tuban" },
            { "id": 29, "port_flag": "ID", "port_code": "IDPNK", "port_name": "Pontianak" },
            { "id": 30, "port_flag": "ID", "port_code": "IDDUM", "port_name": "Dumai" },
            { "id": 31, "port_flag": "ID", "port_code": "IDAYU", "port_name": "Pasang Kayu" },
            { "id": 32, "port_flag": "ID", "port_code": "IDTRE", "port_name": "Tanjung Redep" },
            { "id": 33, "port_flag": "ID", "port_code": "IDPGK", "port_name": "Pangkalpinang, Banka" },
            { "id": 34, "port_flag": "ID", "port_code": "IDPNJ", "port_name": "Panjang" },
            { "id": 35, "port_flag": "ID", "port_code": "IDKBU", "port_name": "Kotabaru" },
            { "id": 36, "port_flag": "ID", "port_code": "IDTRH", "port_name": "Tarahan" },
            { "id": 37, "port_flag": "ID", "port_code": "IDMAK", "port_name": "Makassar" },
            { "id": 38, "port_flag": "ID", "port_code": "IDSTU", "port_name": "Satui/Sei Danau" },
            { "id": 39, "port_flag": "ID", "port_code": "IDSMQ", "port_name": "Sampit" },
            { "id": 40, "port_flag": "ID", "port_code": "IDTNB", "port_name": "Teluk Adang dan Tanah Grogot" },
            { "id": 41, "port_flag": "ID", "port_code": "IDIAM", "port_name": "Asam-Asam" },
            { "id": 42, "port_flag": "ID", "port_code": "IDBIR", "port_name": "Biringkassi" },
            { "id": 43, "port_flag": "ID", "port_code": "IDBPN", "port_name": "Balikpapan" },
            { "id": 44, "port_flag": "ID", "port_code": "IDBTI", "port_name": "Bunati" },
            { "id": 45, "port_flag": "ID", "port_code": "IDBDJ", "port_name": "Banjarmasin" },
            { "id": 46, "port_flag": "ID", "port_code": "IDCBN", "port_name": "Cirebon" },
            { "id": 47, "port_flag": "ID", "port_code": "IDCIW", "port_name": "Ciwandan" },
            { "id": 48, "port_flag": "ID", "port_code": "IDCIG", "port_name": "Cigading" },
            { "id": 49, "port_flag": "ID", "port_code": "IDCGN", "port_name": "Cilegon" },
            { "id": 50, "port_flag": "ID", "port_code": "IDGRE", "port_name": "Gresik" },
            { "id": 51, "port_flag": "ID", "port_code": "IDBGA", "port_name": "Bojonegara" },
            { "id": 52, "port_flag": "ID", "port_code": "IDKNP", "port_name": "Kintap" },
            { "id": 53, "port_flag": "ID", "port_code": "IDKDW", "port_name": "Kendawangan" },
            { "id": 54, "port_flag": "ID", "port_code": "IDKUM", "port_name": "Kumai" },
            { "id": 55, "port_flag": "ID", "port_code": "IDLAM", "port_name": "Lamongan" },
            { "id": 56, "port_flag": "ID", "port_code": "IDRNE", "port_name": "Rembang/Sluke" },
            { "id": 57, "port_flag": "ID", "port_code": "IDPPS", "port_name": "Pulang Pisau" },
            { "id": 58, "port_flag": "ID", "port_code": "IDSPT", "port_name": "Sungai Puting" },
            { "id": 59, "port_flag": "ID", "port_code": "IDSRI", "port_name": "Samarinda" },
            { "id": 60, "port_flag": "ID", "port_code": "IDSRG", "port_name": "Semarang/Tanjung Emas" },
            { "id": 61, "port_flag": "ID", "port_code": "IDMRA", "port_name": "Marunda" },
            { "id": 62, "port_flag": "ID", "port_code": "IDKTG", "port_name": "Ketapang" },
            { "id": 63, "port_flag": "ID", "port_code": "IDBRU", "port_name": "Boom Baru/Palembang" },
            { "id": 64, "port_flag": "ID", "port_code": "IDSUB", "port_name": "Tanjung Perak" },
            { "id": 65, "port_flag": "ID", "port_code": "IDJKT", "port_name": "Tanjung Priok" },
            { "id": 66, "port_flag": "ID", "port_code": "IDTAR", "port_name": "Tarjun" },
            { "id": 67, "port_flag": "ID", "port_code": "IDLLO", "port_name": "Labuhan Lombok" },
            { "id": 68, "port_flag": "ID", "port_code": "IDTBN", "port_name": "Tuban" },
            { "id": 69, "port_flag": "ID", "port_code": "IDPNK", "port_name": "Pontianak" },
            { "id": 70, "port_flag": "ID", "port_code": "IDDUM", "port_name": "Dumai" },
            { "id": 71, "port_flag": "ID", "port_code": "IDAYU", "port_name": "Pasang Kayu" },
            { "id": 72, "port_flag": "ID", "port_code": "IDTRE", "port_name": "Tanjung Redep" },
            { "id": 73, "port_flag": "ID", "port_code": "IDPGK", "port_name": "Pangkalpinang, Banka" },
            { "id": 74, "port_flag": "ID", "port_code": "IDPNJ", "port_name": "Panjang" },
            { "id": 75, "port_flag": "ID", "port_code": "IDKBU", "port_name": "Kotabaru" },
            { "id": 76, "port_flag": "ID", "port_code": "IDTRH", "port_name": "Tarahan" },
            { "id": 77, "port_flag": "ID", "port_code": "IDMAK", "port_name": "Makassar" },
            { "id": 78, "port_flag": "ID", "port_code": "IDSTU", "port_name": "Satui/Sei Danau" },
            { "id": 79, "port_flag": "ID", "port_code": "IDSMQ", "port_name": "Sampit" },
            { "id": 80, "port_flag": "ID", "port_code": "IDTNB", "port_name": "Teluk Adang dan Tanah Grogot" }
          ],
          "origin_nation": "ID",
          "origin_port": "IDBPN",
          "destination_port": "IDBPN",
          "landing_site": "IDJKT.A01",
          "windows_status": null,
          "pandu": []
        },
        "dates": {
          "estimate_arrival": "2026-10-02 00:00",
          "actual_arrival": null,
          "unload": null,
          "load": null,
          "estimate_depature": "2026-10-04 00:00",
          "actual_depature": null
        }
      }'
  • Ekspektasi Response: HTTP 200 OK / 201 Created. Validasi manual referensi dilewati.
  • Verifikasi Database:
    sql
    SELECT type_ship, type, kind FROM pengangkut.ships WHERE service_number = '101019B86C4B4';
    Nilai type_ship terkunci dan terisi sesuai data tarikan Inaportnet (kapalJenisDetail / jenisDetail), bukan dari manipulasi input agen.

Skenario 2A — Kapal Non-Trayek dengan Jenis Kapal Valid ​
  • Pre-kondisi: Permohonan AJU kapal Non-Trayek (id_route_type = 0, trayek = 'non-trayek', registration_number = ''). Seluruh kapal Non-Trayek (baik berbendera domestik maupun asing) wajib divalidasi ke tabel referensi pengangkut.tr_tipe_kapal.
  • Trigger / cURL:
    bash
    curl --location --request PUT 'https://api-stg.insw.go.id/api/pengangkut/super-set/ship/101019B66BB12' \
      --header 'Authorization: Bearer <TOKEN>' \
      --header 'Content-Type: application/json' \
      --data '{
        "service_number": "101019B66BB12",
        "registration_number": "",
        "trayek": "non-trayek",
        "identity": {
          "code": "58633-PEXT",
          "name": "MV. DOLPHIN 82",
          "flag": "PA",
          "siupal": "B X-139/AL.001",
          "kind": "tongkang",
          "domicile": null,
          "call_sign": "123",
          "inmarsat_number": null,
          "cso_name": null,
          "cso_phone": null,
          "voyage_number": "123",
          "captain_name": "123",
          "shipping_type": null,
          "route": null,
          "permanent_activity_ship": null,
          "imo": "9300556",
          "type_ship": "1.4.5",
          "registration_port": "-",
          "mmsi": "123",
          "tugboat": "N/A",
          "perintis_ship": null,
          "docking_ship": null,
          "tongkang_ship": null,
          "tugboat_ship": true,
          "is_coal_carrier": null,
          "is_tol_laut_ship": null,
          "is_new_ship": null,
          "is_sea_trial_ship": true,
          "is_livestock_carrier_ship": null,
          "is_short_sea_ship": true,
          "is_tobe_scrapped_ship": null
        },
        "specification": {
          "ship_width": "32.20",
          "back_draft": "123",
          "max_draft": null,
          "front_draft": "123",
          "air_height": null,
          "minimum_safe_manning": "123",
          "length_overall": "224.93",
          "driving_force": null,
          "TahunPembuatan": "2004",
          "deadweight_ton": "0",
          "gross_ton": "39035.00"
        },
        "ports": {
          "transit_port": null,
          "unloading_port": null,
          "next_port": null,
          "before_origin_port": "CLISG",
          "raw_route": [],
          "origin_nation": "CN",
          "origin_port": "IDMUB",
          "destination_port": "IDMUB",
          "landing_site": null,
          "windows_status": null,
          "pandu": []
        },
        "dates": {
          "estimate_arrival": "2026-09-30T07:00:00",
          "actual_arrival": null,
          "unload": null,
          "load": null,
          "estimate_depature": "2026-10-01T00:00:00",
          "actual_depature": null
        },
        "id_ship": 5811
      }'
  • Ekspektasi Response: HTTP 200 OK / 201 Created. Nilai "1.4.5" lolos verifikasi cache REF_JENIS_KAPAL_ALL.
  • Verifikasi Database: Kolom type_ship, type, dan kind terupdate dengan nilai yang sesuai.

Skenario 2B — Kapal Non-Trayek dengan Jenis Kapal Invalid ​
  • Pre-kondisi: Permohonan AJU kapal Non-Trayek (id_route_type = 0).
  • Trigger / cURL: Request sama seperti 2A, tetapi identity.type_ship = "KODE_INVALID_999" atau string kosong "".
  • Ekspektasi Response: HTTP 400 Bad Request:
    json
    {
      "data": null,
      "message": "Jenis kapal tidak valid atau tidak terdaftar di referensi!",
      "statusCode": 400,
      "success": false
    }
  • Verifikasi Database: Tidak ada perubahan data (transaksi rollback).

Skenario 3A — Kapal Berbendera Asing dengan Jenis Kapal Valid ​
  • Pre-kondisi: Permohonan kapal rute internasional / berbendera asing (contoh: flag = 'TW', trayek pkka, id_route_type = 3, registration_number = 'AL.203/2000/04786/25937/25').
  • Trigger / cURL:
    bash
    curl --location --request PUT 'https://api-stg.insw.go.id/api/pengangkut/super-set/ship/101019B86C56C' \
      --header 'Authorization: Bearer <TOKEN>' \
      --header 'Content-Type: application/json' \
      --data '{
        "service_number": "101019B86C56C",
        "registration_number": "AL.203/2000/04786/25937/25",
        "trayek": "pkka",
        "identity": {
          "code": "030108",
          "name": "EVER BEFIT",
          "flag": "TW",
          "siupal": "BX-79/AL203",
          "kind": "KAPAL MUATAN PETI KEMAS / CONTAINER",
          "domicile": null,
          "call_sign": "aaaa",
          "inmarsat_number": "aaaa",
          "cso_name": null,
          "cso_phone": null,
          "voyage_number": "aaaaVVVV",
          "captain_name": "HOANG VU DAI",
          "shipping_type": "INTERNATIONAL",
          "route": "Shekou/China, Semarang/Tanjung Emas, Tanjung Priok, Kaohsiung",
          "permanent_activity_ship": null,
          "imo": "9784128",
          "type_ship": "1.3.8",
          "registration_port": "CINA",
          "mmsi": "asd"
        },
        "specification": {
          "ship_width": "32.80",
          "back_draft": "1",
          "max_draft": "1",
          "front_draft": "1",
          "air_height": null,
          "minimum_safe_manning": "1",
          "length_overall": "211.00",
          "driving_force": "24260.00",
          "TahunPembuatan": 2017,
          "deadweight_ton": "36972.00",
          "gross_ton": "32691.00"
        },
        "ports": {
          "transit_port": null,
          "unloading_port": null,
          "next_port": null,
          "before_origin_port": "TWKHH",
          "raw_route": [
            {
              "id": 1,
              "port_flag": "CN",
              "port_code": "CNSHK",
              "port_name": "Shekou/China"
            },
            {
              "id": 2,
              "port_flag": "ID",
              "port_code": "IDSRG",
              "port_name": "Semarang/Tanjung Emas"
            },
            {
              "id": 3,
              "port_flag": "ID",
              "port_code": "IDJKT",
              "port_name": "Tanjung Priok"
            },
            {
              "id": 4,
              "port_flag": "TW",
              "port_code": "TWKHH",
              "port_name": "Kaohsiung"
            }
          ],
          "origin_nation": "CN",
          "origin_port": "CNSHK",
          "destination_port": "TWKHH",
          "landing_site": "IDJKT.A01",
          "windows_status": null,
          "pandu": []
        },
        "dates": {
          "estimate_arrival": "2026-10-01 00:00",
          "actual_arrival": null,
          "unload": null,
          "load": null,
          "estimate_depature": "2026-10-02 00:00",
          "actual_depature": null
        }
      }'
  • Ekspektasi Response: HTTP 200 OK / 201 Created. Kode "1.3.8" (Kapal Muatan Peti Kemas) valid di tr_tipe_kapal.

Skenario 3B — Kapal Berbendera Asing dengan Jenis Kapal Invalid ​
  • Pre-kondisi: Permohonan kapal berbendera asing (flag !== 'ID').
  • Trigger / cURL: Request sama seperti 3A, tetapi identity.type_ship = "INVALID_FOREIGN_CODE".
  • Ekspektasi Response: HTTP 400 Bad Request:
    json
    {
      "data": null,
      "message": "Jenis kapal tidak valid atau tidak terdaftar di referensi!",
      "statusCode": 400,
      "success": false
    }

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