Skip to content

[ND-319/LNSW.34/2026] Penyesuaian Durasi Submission Cooldown 60 Detik, Auto-Release Lock Redis, dan Null-Safety Data NIB Pengajuan PAB ​

Modul: SIPT/PAB | Tipe: Bugfix | Stage Saat Ini: DEVELOPMENT | Tanggal Dokumen: 2026-09-15

DEVELOPMENT STAGE

Dokumen ini sedang berada dalam tahap DEVELOPMENT. Perubahan logika backend masih aktif di lingkungan pengembangan (Development).


1. Informasi Tugas & Referensi Bisnis ​

  • Task ID / Nomor Referensi: ND-319/LNSW.34/2026

  • Sumber Instruksi: Nota Dinas Resmi ND-319/LNSW.34/2026

  • Dokumen Referensi / Lampiran: Permohonan Perbaikan Bug pada Aplikasi PAB.pdf (tersimpan pada direktori attachments)

  • Task / Dependensi Terkait (Cross-Service): —

  • Modul Terkait: SIPT/PAB

  • Service / Repository: sipt-backend

  • PIC Pengembang: Hermansyah Efendi, Muhamad Firly Ramadan

  • Ringkasan Non-Teknis (Executive Summary): Aplikasi PAB sebelumnya memiliki waktu tunggu pengiriman ulang (cooldown) hingga 5 menit yang menyulitkan pelaku usaha ketika terjadi kegagalan transmisi. Selain itu, sistem kerap mengalami error fatal (crash) saat memproses data pengajuan yang tidak mencantumkan Nomor Induk Berusaha (NIB) milik ekspedisi atau penerima.

    Melalui perbaikan ini:

    1. Waktu tunggu submission dipangkas menjadi 60 detik (1 menit), dan kunci antrean otomatis dibebaskan seketika setelah server selesai memproses data tanpa menahan pengguna.
    2. Sistem kebal terhadap nilai NIB kosong (null/undefined) serta mendukung input khusus karakter '0', memastikan pengajuan tidak gagal validasi di tengah jalan.
    3. Pada integrasi Kemendag, sistem secara defensif menerapkan fallback otomatis ke string "0" ketika atribut NIB ekspedisi (ekspedisi.nib) atau NIB penerima (penerima.nib_penerima) bernilai null, undefined, string kosong (""), atau hanya berupa spasi (" "), menjamin kesesuaian skema payload hilir.
  • Latar Belakang & Ringkasan Kebutuhan: Berdasarkan evaluasi operasional pada sistem PAB (Pemberitahuan Arus Barang) dan laporan bug pada Nota Dinas ND-319/LNSW.34/2026:

    1. Durasi submission cooldown pengajuan PAB sebelumnya bernilai 300 detik (5 menit) yang dinilai terlalu lama saat terjadi kegagalan jaringan atau kebutuhan retry, serta mekanisme lock Redis sebelumnya tidak dilepas ketika request selesai lebih awal atau terjadi connection abort.
    2. Terjadi runtime crash TypeError: Cannot read properties of undefined (reading 'trim') pada integrasi Kemendag dan skema validasi Zod ketika memproses data pelaku usaha yang memiliki atribut NIB atau NIB Penerima kosong (null/undefined). Proses bisnis PAB mengizinkan data NIB bernilai kosong atau bernilai '0', sehingga diperlukan penanganan null-safety komprehensif, fallback string "0", dan schema refinement yang fleksibel.
  • Ruang Lingkup & Rincian Sub-Pekerjaan (Technical Scope & Milestones):

    • [x] Null-safety guard pada integrasi Kemendag (kemendag.api.js) dan controller pembatalan PAB (newsend_sipt_controller.js)
    • [x] Peningkatan helper validasi Zod (zod.js) untuk mendukung opsi whitelist dan penanganan nullish secara aman
    • [x] Pembaruan skema validasi pelaku_usaha.validation.js dengan mengizinkan nilai whitelist '0' untuk NIB Penerima
    • [x] Intersepsi res.end pada submission_cooldown.js untuk memastikan release lock Redis selalu dieksekusi begitu request selesai
    • [x] Reduksi durasi cooldown pengajuan PAB dari 300 detik (5 menit) menjadi 60 detik (1 menit)
    • [x] Pembersihan logika releaseSubmissionLock redundan pada kemendag-axios-interceptor.js
    • [x] Fallback otomatis ke string "0" saat NIB ekspedisi dan NIB penerima bernilai null, undefined, atau string kosong pada integrasi Kemendag (kemendag.api.js)
    • [x] Penambahan unit test suite Kemendag API untuk verifikasi fallback "0" pada skenario null, undefined, dan whitespace-only (tests/api/kemendag.api.test.js)

2. Deployment Lifecycle ​

#EnvironmentTanggal & WaktuPICStatusCatatan
1Development2026-08-07 17:16Hermansyah Efendi, Muhamad Firly RamadanDEPLOYEDBranch: development (Commit: 761ad750 .. f5a4673)
2Staging (UAT)——PENDING—
3Production——PENDING—

Riwayat Update Log ​

TanggalEnvironmentPerubahanPIC
2026-08-24DevelopmentInisialisasi draft awal audit log (commit ebd871dc)Hermansyah Efendi, Muhamad Firly Ramadan
2026-09-15DevelopmentSinkronisasi multi-commit trail lengkap (761ad750 .. 2fcb6e99), koreksi rute kontrak API riil (receivePelaporan & cancellation), pemutakhiran cuplikan kode real Zod helper, dan standarisasi QA PlaybookHermansyah Efendi, Muhamad Firly Ramadan
2026-09-15DevelopmentPenambahan fallback default string "0" saat NIB ekspedisi/penerima bernilai null, undefined, atau whitespace-only (commit 218b653 & f5a4673) serta penambahan unit test Kemendag APIHermansyah Efendi

3. Analisis Teknis & Rincian Perubahan ​

  • Alur Logika & Bisnis:

    • Optimasi Middleware Cooldown & Lifecycle Lock Redis (app/middlewares/submission_cooldown.js):
      • Mengurangi konstanta COOLDOWN_SECONDS dari 300 detik (5 menit) menjadi 60 detik (1 menit).
      • Mengimplementasikan monkey-patching pada lifecycle response Express (res.end) agar otomatis memicu fungsi releaseLock() yang mengeksekusi redisClient.del(cacheKey) saat request selesai (finish/close maupun error). Hal ini menjamin lock hanya menahan konkurensi selama proses berlangsung dan langsung dibebaskan pasca-eksekusi tanpa menunggu TTL habis.
    • Pembersihan Logika Interceptor Manual Release (app/helpers/kemendag-axios-interceptor.js):
      • Menghapus logika pelepasan manual lock Redis (releaseSubmissionLock) yang sebelumnya tersebar di axios interceptor karena lifecycle lock kini terpusat secara deterministik pada middleware Express submission_cooldown.js.
    • Helper Validasi Zod & Whitelist Extension (app/helpers/zod.js):
      • Menambahkan parameter opsi whitelist pada fungsi helper createDigitStringSchema.
      • Menggunakan z.union antara schema digit regex dengan z.enum(whitelist) serta penanganan .nullish() dan transformasi string kosong ("" -> null) agar input bernilai khusus tidak tertolak validator.
    • Pembaruan Skema Validasi Pelaku Usaha (app/validations/pelaku_usaha.validation.js):
      • Mengonfigurasi properti nib_penerima agar mengizinkan nilai '0' via opsi whitelist: ['0'] tanpa melanggar ketentuan digit length.
    • Null-Safety & Fallback String "0" Integrasi Kemendag (app/api/kemendag.api.js):
      • Menerapkan optional chaining dan operator fallback string "0": ekspedisi.nib?.trim() || "0" dan penerima.nib_penerima?.trim() || "0".
      • Mengeliminasi fatal crash TypeError: Cannot read properties of undefined (reading 'trim') sekaligus memastikan jika nilai NIB tidak diisi (undefined), bernilai null, string kosong (""), atau hanya berupa spasi (" "), payload downstream Kemendag selalu menerima nilai "0" yang sah menurut kontrak integrasi.
    • Null-Safety Penanganan Pembatalan Pengajuan (app/controllers/newsend_sipt_controller.js):
      • Menambahkan optional chaining pada parameter pembatalan pengajuan PAB untuk menjamin keandalan payload sebelum diteruskan ke service layer.
  • Perubahan Database (DDL / DML):

    sql
    -- Tidak ada perubahan skema database (DDL/DML) untuk task ini.
    -- Perubahan difokuskan pada middleware submission cooldown (Redis lock), Zod schema helper, dan defensive null-safety logic.
  • Kontrak API & Response Payload:

    POST /receivePelaporan ​

    • Deskripsi: Endpoint penerimaan dan pengajuan pelaporan PAB ke instansi terkait / Kemendag.
    • Autentikasi / Guard: Bearer Token (auth.doAuth, submissionCooldown("receivePelaporan"))
    • Request Header:
      • Authorization: Bearer <token>
      • Content-Type: application/json
    • Request Body:
      json
      {
        "nomor_layanan": "PAB-2026-0001",
        "pelaku_usaha": {
          "ekspedisi": {
            "npwp": "012345678901234",
            "nib": null,
            "nama": "PT Ekspedisi Logistik Nusantara"
          },
          "penerima": {
            "npwp_penerima": "987654321098765",
            "nib_penerima": "0",
            "nama_penerima": "Toko Sejahtera"
          }
        }
      }
    • Response Payload (200 OK):
      json
      {
        "data": {
          "nomor_layanan": "PAB-2026-0001",
          "status": "TERKIRIM"
        },
        "message": "Pelaporan PAB berhasil dikirim",
        "statusCode": 200,
        "success": true
      }
    • Response Error (429 Too Many Requests — Concurrent Submission):
      json
      {
        "data": null,
        "message": "Pengajuan untuk nomor layanan PAB-2026-0001 sudah dikirim. Silahkan coba lagi dalam 1 menit.",
        "statusCode": 429,
        "success": false,
        "errors": []
      }

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

  • Repository: sipt-backend
  • Branch Utama: development
  • Merge Request / PR: —

Daftar Commit Terkait (Multi-Commit Trail) ​

Commit IDBranchAuthorTanggal & JamRingkasan Perubahan
761ad750989a4052c4fffa3dff9bf68704b62d36developmentHermansyah Efendi2026-07-02 14:15fix(validation): prevent TypeError on trimming null or undefined values
12a48621cd4121ccb2553786850c640b542d1eb8developmentHermansyah Efendi2026-07-02 17:44feat(validation): support whitelist option in digit string schema
adcf42a09ce39c3a45c552559f79e390e6c84688developmentHermansyah Efendi2026-07-06 16:40fix(cooldown): ensure lock is released on request completion
ebd871dc5d343ae7ef7983e659c8ef773b18c434developmentMuhamad Firly Ramadan2026-08-07 17:14feat(middleware): update cooldown second to 60s (1m)
2fcb6e99979f31cff9b95bce7447a809c306a2e4developmentMuhamad Firly Ramadan2026-08-07 17:16chore(changelog): write cooldown middleware update
218b65387118beb5ea99457030d5187b8f479e4adevelopmentHermansyah Efendi2026-09-15 10:42test(kemendag): add test cases for null and undefined nib values
f5a46737771c20ee6c2022b318cf8d19bf152db8developmentHermansyah Efendi2026-09-15 11:03fix(kemendag): fallback empty ekspedisi and penerima nib to "0"

Cuplikan Kode / Implementasi Inti ​

javascript
// File: app/middlewares/submission_cooldown.js
const COOLDOWN_SECONDS = 60; // 1 menit

export const submissionCooldown = (action) => {
  return async (req, res, next) => {
    try {
      const nomorLayanan = req.nomorLayanan;
      if (!nomorLayanan) return next();

      const cacheKey = CacheKeys.SUBMISSION_COOLDOWN(action, nomorLayanan);
      const result = await redisClient.set(cacheKey, Date.now().toString(), "EX", COOLDOWN_SECONDS, "NX");

      if (result === null) {
        const ttl = await redisClient.ttl(cacheKey);
        const minutesLeft = Math.ceil(ttl / 60);
        throw new TooManyRequestsError(
          `Pengajuan untuk nomor layanan ${nomorLayanan} sudah dikirim. Silahkan coba lagi dalam ${minutesLeft} menit jika status masih ${statusSipt.draft} atau ${statusSipt.gagal_kirim}.`
        );
      }

      // Intersepsi res.end (monkey patching) untuk memastikan lock selalu dibebaskan saat
      // backend selesai memproses request, baik sukses maupun error.
      let released = false;
      const releaseLock = async () => {
        if (released) return;
        released = true;
        try {
          await redisClient.del(cacheKey);
          logger.debug(
            { nomor_layanan: nomorLayanan, action },
            "Released submission cooldown lock on request completion"
          );
        } catch (err) {
          logger.error(
            { err, nomor_layanan: nomorLayanan, action },
            "Failed to release submission cooldown lock on request completion"
          );
        }
      };

      const originalEnd = res.end;
      res.end = function (...args) {
        releaseLock();
        return originalEnd.apply(this, args);
      };

      next();
    } catch (error) {
      next(error);
    }
  };
};
javascript
// File: app/helpers/zod.js
export const createDigitStringSchema = (field, { required = true, maxLength, minLength, whitelist = [] } = {}) => {
  const digitRegex = /^[0-9]+$/;
  if (!required) {
    let innerSchema = z
      .string()
      .regex(digitRegex, { message: `${field} hanya boleh berisi angka` })
      .max(maxLength ?? 255, { message: `${field} maksimal ${maxLength ?? 255} karakter` })
      .min(minLength ?? 1, { message: `${field} minimal ${minLength ?? 1} karakter` });

    if (whitelist.length > 0) {
      innerSchema = z.union([
        innerSchema,
        z.enum(whitelist, { errorMap: () => ({ message: `${field} tidak valid` }) })
      ]);
    }

    return z
      .string()
      .nullish()
      .transform((val) => (val === "" ? null : val))
      .pipe(innerSchema.nullish());
  }
  // ...
};
javascript
// File: app/api/kemendag.api.js (Null-Safety & Fallback String "0" NIB)
ekspedisi: {
  npwp: ekspedisi.npwp,
  nib: ekspedisi.nib?.trim() || "0",
  tipe_perusahaan: ekspedisi.tipe_perusahaan,
  nama: ekspedisi.nama,
  surat_kuasa: ekspedisi.surat_kuasa,
  // ...
},
penerima: {
  jenis_tujuan: penerima.jenis_tujuan,
  npwp_penerima: penerima.npwp_penerima,
  nib_penerima: penerima.nib_penerima?.trim() || "0",
  nama_penerima: penerima.nama_penerima,
  // ...
}

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

A. Verifikasi Unit Test (Development Stage) ​

  • Test Suite Files:
    • tests/middlewares/submission_cooldown.test.js
    • tests/validations/pelaku_usaha.validation.test.js
    • tests/api/kemendag.api.test.js
  • Status Eksekusi CLI: PASSED
javascript
// File: tests/api/kemendag.api.test.js (Verifikasi Fallback "0" pada Kemendag API)
it('handles null nib in ekspedisi and penerima by defaulting to "0"', async () => {
  const inputData = getBasePelaporanInput();
  inputData.ekspedisi.nib = null;
  inputData.penerima.nib_penerima = null;
  await kemendagApi.sendPelaporanPab(inputData);
  expect(mockKemendagAxios.post).toHaveBeenCalledWith('/receivePelaporan', expect.objectContaining({
    ekspedisi: expect.objectContaining({ nib: '0' }),
    penerima: expect.objectContaining({ nib_penerima: '0' }),
  }));
});

it('handles whitespace-only nib in ekspedisi and penerima by defaulting to "0"', async () => {
  const inputData = getBasePelaporanInput();
  inputData.ekspedisi.nib = '   ';
  inputData.penerima.nib_penerima = '';
  await kemendagApi.sendPelaporanPab(inputData);
  expect(mockKemendagAxios.post).toHaveBeenCalledWith('/receivePelaporan', expect.objectContaining({
    ekspedisi: expect.objectContaining({ nib: '0' }),
    penerima: expect.objectContaining({ nib_penerima: '0' }),
  }));
});
javascript
// File: tests/middlewares/submission_cooldown.test.js
it("should allow request and release lock when res.end is called", async () => {
  const middleware = submissionCooldown("test_action");
  await middleware(req, res, next);
  expect(next).toHaveBeenCalled();
  expect(redisClient.set).toHaveBeenCalledWith("cooldown:test_action:NOMOR123", "locked", "EX", 60);
  res.end();
  expect(redisClient.del).toHaveBeenCalledWith("cooldown:test_action:NOMOR123");
});
text
 RUN  v4.1.11 D:/WORK/LNSW/sipt-backend
 ✓ tests/validations/pelaku_usaha.validation.test.js (4 tests) 9ms
 ✓ tests/middlewares/submission_cooldown.test.js (3 tests) 7ms
 Test Files  2 passed (2)
      Tests  7 passed (7)
   Start at  09:20:20
   Duration  793ms (transform 336ms, setup 0ms, import 673ms, tests 16ms, environment 0ms)

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

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

  • Cakupan / Surface Pengujian: Full E2E (UI + Backend) & API / Postman
  • Kebutuhan Akses / Role Akun: Akun Pelaku Usaha PAB & Internal INSW
  • Hasil Pengujian UAT: PENDING
  • Catatan QA: —
Skenario Pengujian QA (Test Scenarios) ​
  1. Skenario 1 — Verifikasi Null-Safety NIB Penerima & Ekspedisi (Happy Path):

    • Pre-kondisi: Form pengajuan PAB siap disubmit dengan data NIB Penerima bernilai 0 atau dibiarkan kosong (null).
    • Trigger / Request: Submit payload pengajuan via UI atau POST /api/v1/newsend_sipt/receivePelaporan.
    • Ekspektasi Response: HTTP 200 OK, validasi lolos tanpa error TypeError: Cannot read properties of undefined (reading 'trim').
    • Verifikasi Database / Log: Log service menunjukkan pengiriman payload ke Kemendag sukses tanpa warning trim undefined.
  2. Skenario 2 — Verifikasi Submission Cooldown & Lock Auto-Release (Edge Case):

    • Pre-kondisi: Request pengajuan pertama selesai diproses atau mengalami kegagalan downstream.
    • Trigger / Request: Segera kirim request kedua dengan nomor layanan yang sama setelah response pertama selesai.
    • Ekspektasi Response: Request tidak tertahan cooldown jika request pertama telah selesai tereksekusi (lock dibebaskan oleh res.end). Jika request kedua dikirim paralel saat request pertama masih pending, ekspektasi response adalah HTTP 429 Too Many Requests dengan batas TTL maksimum 60 detik.
    • Verifikasi Database / Log: Key Redis cooldown:receivePelaporan:{nomor_layanan} terhapus segera setelah request selesai.
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