CONTOH — Sistem, nama, log, dan angka di laporan ini fiktif. Laporan untuk klien mengikuti struktur dan tingkat detail yang sama.

Klien (fiktif) PT Contoh Niaga Digital, toko online dengan backend Laravel 10 dan MySQL
Masalah yang dilaporkan Sebagian order kartu kredit diproses dua kali; sebagian order virtual account tetap pending walaupun pembayarannya sukses
Akses yang dipakai Repository (read-only), log aplikasi 14 hari terakhir, dashboard Midtrans sandbox, environment staging
Waktu terpakai 7 dari 8 jam

1. Ringkasan

Kedua masalah terkonfirmasi dan berasal dari satu tempat: handler notifikasi Midtrans. Handler ini memperlakukan setiap notifikasi sebagai pembayaran baru, dan mengerjakan semua pekerjaan berat (reservasi stok ke API gudang, email konfirmasi) sebelum membalas Midtrans. Rekomendasinya adalah opsi B: membuat pemrosesan pembayaran idempotent, memindahkan pekerjaan berat ke queue, dan menambahkan rekonsiliasi berkala ke API status Midtrans. Harga tetap Rp8.400.000, sekitar 1,5 minggu kalender.

2. Gejala dan dampak

  • Order diproses dua kali: 37 order kartu kredit dalam 30 hari terakhir. Stok terpotong dua kali dan pelanggan menerima dua email konfirmasi.
  • Order tertahan pending: 112 order virtual account dalam 30 hari terakhir, padahal dashboard Midtrans menunjukkan pembayaran sukses. Tim CS mengubah statusnya secara manual, sekitar 2 jam per hari.

3. Scope investigasi

  • Diperiksa: PaymentNotificationController, OrderService, konfigurasi queue, log aplikasi, riwayat notifikasi di dashboard Midtrans, dan URL notifikasi yang terdaftar.
  • Di luar scope: frontend checkout, integrasi kurir, dan performa database secara umum.

4. Temuan

Penyebab 1 (terkonfirmasi): status capture dan settlement sama-sama diproses sebagai “lunas”

Untuk pembayaran kartu kredit, Midtrans mengirim notifikasi capture saat pembayaran berhasil, lalu notifikasi lain saat statusnya berubah menjadi settlement. Handler memanggil markPaid() untuk kedua status, dan markPaid() tidak memeriksa apakah order sudah lunas.

// PaymentNotificationController.php (disederhanakan)
if (in_array($notif->transaction_status, ['capture', 'settlement'])) {
    $this->orders->markPaid($order); // memotong stok dan mengirim email, setiap kali dipanggil
}

Bukti (log disanitasi):

2026-02-11 10:02:14 midtrans.notify  order_id=INV-20481 transaction_status=capture fraud_status=accept
2026-02-11 10:02:15 order.paid       order_id=INV-20481 stock_reserved=3
2026-02-12 09:15:40 midtrans.notify  order_id=INV-20481 transaction_status=settlement
2026-02-12 09:15:41 order.paid       order_id=INV-20481 stock_reserved=3

Reproduksi: dua notifikasi di atas dikirim ulang ke staging. Test baru test_paid_order_is_processed_once gagal di kode saat ini.

Penyebab 2 (terkonfirmasi): handler memanggil API gudang sebelum membalas Midtrans

Handler memanggil API reservasi stok secara sinkron. Saat API gudang lambat atau error, handler membalas 500. Menurut dokumentasi Midtrans, notifikasi yang dibalas 500 hanya dicoba ulang satu kali. Jika percobaan ulang itu juga gagal, notifikasi tidak dikirim lagi, dan tidak ada mekanisme lain yang mengecek status pembayaran. Order pun tertahan pending.

2026-02-18 13:40:02 midtrans.notify    order_id=INV-21007 transaction_status=settlement
2026-02-18 13:40:12 warehouse.reserve  order_id=INV-21007 error="timeout after 10s"
2026-02-18 13:40:12 http.response      path=/payment/notify status=500
2026-02-18 13:42:13 midtrans.notify    order_id=INV-21007 transaction_status=settlement
2026-02-18 13:42:23 warehouse.reserve  order_id=INV-21007 error="timeout after 10s"
2026-02-18 13:42:23 http.response      path=/payment/notify status=500

Setelah dua kegagalan itu, tidak ada notifikasi lain untuk INV-21007. Pola yang sama ditemukan di 104 dari 112 order pending.

Yang sudah dikesampingkan

  • Signature key tidak valid: 40 sampel notifikasi lolos verifikasi signature_key.
  • URL notifikasi salah: URL di dashboard Midtrans sesuai, dan notifikasi untuk order lain diterima normal.
  • Gangguan di sisi Midtrans: riwayat pengiriman notifikasi di dashboard tercatat lengkap, termasuk percobaan ulangnya.

5. Opsi perbaikan

Opsi Pendekatan Estimasi Risiko Yang tidak diselesaikan
A markPaid() dibuat idempotent (status pembayaran per order dan kunci unik per transaksi); handler langsung membalas 200, reservasi stok dan email dipindah ke queue dengan retry 12 jam Rendah Order yang notifikasinya gagal sebelum perbaikan ini tetap pending
B Opsi A, ditambah job rekonsiliasi setiap 15 menit yang mengecek order pending lebih dari 30 menit ke API status Midtrans, dan alert jika pemrosesan notifikasi gagal berulang 20 jam Rendah —
C Menulis ulang modul pembayaran 80+ jam Tinggi Tidak diperlukan: masalahnya ada di dua titik yang spesifik

6. Rekomendasi

Opsi B, harga tetap Rp8.400.000, sekitar 1,5 minggu kalender. Jika Anda lanjut dalam 30 hari, biaya diagnosis Rp2.500.000 dipotong penuh, sehingga yang dibayar Rp5.900.000: DP Rp2.950.000, pelunasan Rp2.950.000 saat acceptance.

Draf acceptance criteria:

  1. Notifikasi capture lalu settlement untuk order yang sama membuat order diproses satu kali (test otomatis).
  2. Notifikasi yang sama dikirim dua kali tidak menimbulkan efek ganda (test otomatis).
  3. Handler membalas 200 dalam waktu di bawah 1 detik walaupun API gudang lambat; reservasi stok berjalan di queue dengan retry (test dan demo di staging).
  4. Order yang pending lebih dari 30 menit dan sudah sukses di Midtrans otomatis menjadi lunas lewat job rekonsiliasi (test dan demo di staging).
  5. Alert terkirim jika ada 3 atau lebih kegagalan pemrosesan notifikasi dalam 15 menit.

7. Risiko lain yang teramati

Di luar scope dan tidak diinvestigasi lebih jauh:

  • Server key Midtrans pernah ter-commit di riwayat repository lama. Sebaiknya key dirotasi.
  • Tidak ada monitoring untuk queue yang menumpuk atau macet.

8. Langkah selanjutnya

  1. Setujui opsi dan acceptance criteria di atas.
  2. SOW perbaikan dikirim dalam 1 hari kerja setelah persetujuan.
  3. Biaya diagnosis dipotong penuh dari harga perbaikan jika Anda lanjut dalam 30 hari.

Untuk menyimpan halaman ini sebagai PDF, gunakan tombol "Cetak / simpan PDF" di atas atau menu Cetak di browser Anda.