Implemented · 280 tests pass · Updated 1 April 2026
Pipeline & Side Effects
3-track status model, event-driven side effects, RTS management.
Prinsip #1: 3 Track Status Independen
Setiap track jalan paralel. Gak saling tunggu.
ORDER STATUS (business lifecycle):
dibuat → diproses → selesai / dibatalkan
FULFILLMENT STATUS (warehouse + pengiriman):
pending → ready_to_ship → picking → packed → shipped → in_transit
│
┌───────┴────────┐
▼ ▼
delivered rts_detected
│
┌────────┼────────┐
▼ ▼ ▼
hold_requested retrying returned
│ │
└───┬────┘
▼
delivered (SAVED!)
PAYMENT STATUS (uang):
unpaid → settled → refunded
Definisi Fulfillment Status
| Status | Artinya | Kapan |
pending | Order baru masuk, belum ada resi | API sync order baru |
ready_to_ship | Ada resi, siap diproses | XLSX import / resi masuk dari platform |
picking | Sedang dipick di gudang | Staff mulai pick |
packed | Sudah dikemas, nunggu kurir | Staff selesai kemas |
shipped | Kurir sudah ambil | Ship event (scan/API/XLSX) |
in_transit | Dalam perjalanan | API tracking update |
delivered | Sampai ke buyer | Kurir confirm / API |
rts_detected | Gagal deliver, terdeteksi RTS | API notify / XLSX |
hold_requested | Minta kurir tahan paket | Admin request hold |
retrying | Kurir coba kirim ulang | Admin record retry |
returned | Paket balik ke seller (RUGI) | Final — gagal deliver |
Contoh Kombinasi Status
| Skenario | Order | Fulfillment | Payment |
| Order baru dari API (belum ada resi) | dibuat | pending | unpaid |
| Order dari XLSX (ada resi) | dibuat | ready_to_ship | unpaid |
| Sudah dikirim, uang belum cair | diproses | shipped | unpaid |
| Sampai, uang belum cair | diproses | delivered | unpaid |
| Uang cair dari marketplace | diproses | delivered | settled |
| Semua beres | selesai | delivered | settled |
| Dibatalkan sebelum kirim | dibatalkan | pending | unpaid |
| RTS terdeteksi | diproses | rts_detected | unpaid |
| RTS di-hold, retry berhasil | diproses | delivered | unpaid |
| RTS gagal, paket balik | diproses | returned | unpaid |
Order "selesai" = derived. Bukan di-set manual. Otomatis selesai kalau fulfillment = delivered DAN payment = settled.
Prinsip #2: Side Effects = Reaksi dari Event
| Event | Trigger | Side Effects |
| Order Masuk | XLSX / API / manual | Reserve stok |
| Ship | Staff scan / API / XLSX | Potong stok (FEFO) + HPP + piutang + audit |
| Delivered | Kurir confirm / API | Update status only. Gak ada side effect keuangan. |
| Settlement | XLSX rekon / API settlement | Jurnal Dr.Kas / Cr.Piutang. Payment = settled. |
| RTS Detected | API / XLSX / manual | Create RTS ticket. Estimate cost. |
| RTS Returned | Hold gagal / timeout | Reverse stok + reverse jurnal + klaim kurir |
| RTS Saved | Hold + retry berhasil | Cancel RTS ticket. Order → delivered. |
| Cancel | Internal / external | Release reservation only (sebelum ship) |
Prinsip #3: 3 Source, 1 Function
Semua trigger source masuk ke function yang SAMA. Gak ada logic terpisah per source.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Staff Scan │ │ API Sync │ │ XLSX Import │
│ (real-time) │ │ (background)│ │ (catch-up) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└─────────────────┼─────────────────┘
▼
┌─────────────────────┐
│ ship_order() │
│ │
│ • Potong stok (FEFO)│
│ • HPP calculation │
│ • Create piutang │
│ • Audit trail │
│ • Journal (event dt)│
└─────────────────────┘
Prinsip #4: Journal Date = Event Date
IMPLEMENTED. Semua journal date = order.shipped_at (event date), BUKAN tanggal proses.
TikTok shipped 28 Maret
Nelly upload XLSX 1 April
→ Jurnal tanggal 28 Maret ✅
→ Laporan Maret: revenue BENAR
Prinsip #5: Idempotent
| Skenario | Behavior |
| Ship order 2x | SKIP (:already_shipped) |
| XLSX import order yang sudah ada | SKIP (by nomor_pesanan) |
| Detect RTS 2x untuk order sama | SKIP (return existing ticket) |
| Daily journal generate 2x hari sama | SKIP (by batch_date + store_id) |
| Rekon order yang sudah settled | REJECT (already reconciled) |
RTS Management — CORE FEATURE
Cost per RTS: Rp 80.000 - 125.000+
CAC Rp 60.000 + fulfillment Rp 5.000/pcs + ongkir balik Rp 15.000 + stok rusak (jika).
Scale: 300 order/hari × 5% RTS = 15 RTS/hari = Rp 45.000.000/bulan RUGI.
Flow: Early Intervention
RTS DETECTED (dari API/XLSX)
│
Eligible untuk hold?
│
┌────┴────┐
│ YES │ NO
▼ ▼
HOLD REQ. RETURNED 💀 (langsung rugi)
│
▼
RETRYING (kurir coba lagi)
│
┌────┴────┐
│ SUKSES │ GAGAL
▼ ▼
DELIVERED ✅ RETURNED 💀
(SAVED!) (rugi)
Functions (Implemented)
| Function | Deskripsi |
detect_rts/1 | Create ticket + update order. Idempotent. |
request_hold/1 | Minta kurir tahan paket |
record_retry/1 | Record retry (increment count) |
resolve_saved/1 | ORDER SAVED — paket sampai |
resolve_returned/1 | Final returned — RUGI |
dashboard_metrics/2 | Save rate, active count, estimated loss |
Value Proposition: Save rate 50% = 7.5 saved/hari × Rp 100.000 = Rp 22.500.000/bulan diselamatkan. Ini alasan seller bayar LabaBersih.
XLSX Import — Smart Routing
1 upload, sistem yang routing berdasarkan status platform:
| Status Platform | Action | Fulfillment Status |
| Perlu Dikirim / AWAITING_COLLECTION / CREATE | Import → ready_to_ship | ready_to_ship |
| Sedang Dikirim / IN_TRANSIT | Skip (status update dari API) | — |
| Selesai / DELIVERED / COMPLETED | Skip (status update dari API) | — |
| RTS / RETURNED | Skip (status update dari API) | — |
| Dibatalkan / CANCELLED | Import → dibatalkan | pending |
XLSX = import order BARU (siap kirim). Status update (shipped → delivered → RTS) nanti dari API sync. Bukan dari XLSX.
Platform Status Mapping
Setiap platform punya 10-20 status. Kita cuci jadi ~10 status internal yang seragam.
| Platform Status | Internal Fulfillment |
| UNPAID, ON_HOLD, AWAITING_SHIPMENT, Belum Bayar | pending |
| AWAITING_COLLECTION, Perlu Dikirim, CREATE/PICKUP | ready_to_ship |
| IN_TRANSIT, Sedang Dikirim, ON_DELIVERY | in_transit |
| DELIVERED, Selesai, COMPLETED | delivered |
| RTS, RETURNED, SHIPMENT_RETURN | rts_detected |
| CANCELLED, Dibatalkan | (order_status: dibatalkan) |
Raw platform status disimpan di marketplace_data untuk referensi.
Full Pipeline
ORDER MASUK (3 sources)
XLSX Import / API Sync / Manual Input
│
▼
┌──────────────────┐
│ CREATE ORDER │ → reserve stok
│ order: dibuat │
│ fulfillment: │
│ pending / │ (API: pending, XLSX: ready_to_ship)
│ ready_to_ship │
│ payment: unpaid │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ FULFILLMENT │ (internal gudang)
│ picking → packed │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ SHIP EVENT │ → potong stok + HPP + piutang + audit
│ 3 trigger: │ journal date = shipped_at (event date)
│ • staff scan │
│ • API sync │ fulfillment: shipped → in_transit
│ • XLSX catch-up │
└────────┬─────────┘
│
┌────┴──────────────────────┐
▼ ▼
DELIVERED ✅ RTS DETECTED ⚠️
fulfillment: delivered fulfillment: rts_detected
│ │
▼ ┌────┴────┐
SETTLEMENT ▼ ▼
Dr.Kas / Cr.Piutang HOLD REQ. RETURNED 💀
payment: settled │
│ ▼
▼ RETRYING
ORDER SELESAI │
(delivered + settled) ┌───┴───┐
▼ ▼
DELIVERED RETURNED 💀
(SAVED!) (rugi)
CANCEL (before ship):
order: dibatalkan → release reservation
Implementation Status
| Step | Apa | Status |
| 1 | Migration: split status → 3 track (order/fulfillment/payment) | ✅ Done |
| 2 | Schema: Order 3-track + RtsTicket | ✅ Done |
| 3 | Shared ship function (do_ship_order) | ✅ Done |
| 4 | RTS Management: detect → hold → retry → saved/returned | ✅ Done (13 tests) |
| 5 | XLSX import smart routing by status | ✅ Done |
| 6 | Update all callers + tests | ✅ Done (280 tests pass) |
| 7 | UI: fulfillment tabs + RTS ticket actions | ✅ Done |
| 8 | Daily journal: shipped_at as event date | ✅ Done |
| 9 | Fulfillment status split: pending + ready_to_ship | ✅ Done |