# LabaBersih v2 — Master Plan

> Satu-satunya dokumen referensi untuk status development v2.
> Update terakhir: 1 April 2026
> Referensi detail: v1-audit.md, pipeline-design.md, v2-execution-blueprint.md, design-system.md

---

## 0. PRODUCT POSITIONING

**LabaBersih = Accounting for Online Commerce (A2X versi Indonesia)**

Referensi produk: A2X (Shopify/Amazon → Xero/QuickBooks)
Tapi LabaBersih LEBIH LENGKAP — all-in-one, bukan bridge.

| Kapabilitas | A2X | LabaBersih |
|---|---|---|
| Accounting (COA, jurnal, laporan, trial balance, buku besar) | ✅ Bridge ke Xero | ✅ Built-in |
| Reconciliation (per-fee matching, suspense account) | ✅ | ✅ (sedang di-improve) |
| Platform | Shopify, Amazon | TikTok, Shopee, Mengantar |
| Inventory + FEFO | ❌ | ✅ |
| Order management + packing | ❌ (pakai Shopify) | ✅ Built-in |
| RTS/Returns + inbound (baik/rusak/hilang) | ❌ | ✅ |
| PO/Purchasing | ❌ | ✅ |
| Target market | Global (English) | Indonesia (Bahasa, Rupiah, WIB) |
| Design reference | — | Shopify Polaris (structure) + Stripe (financial) + Xero (accounting) |

### Core Value Proposition
> "Seller Indonesia gak perlu pakai 3 app terpisah (marketplace + accounting + inventory).
> LabaBersih = satu dashboard, semua terurus."
> + "Reduce RTS rate → save Rp 22.5jt/bulan per seller."

### Problem yang Diselesaikan
1. **Laba kelihatan besar, ternyata saat rekonsil sisa dikit / minus** — fee transparency + per-fee reconciliation
2. **Manual import/export antar platform** — staging pipeline + API sync
3. **Gak tau HPP akurat** — FEFO lot tracking, bukan tebak-tebakan
4. **Laporan keuangan gak beres** — Trial Balance enforced, COA immutable, jurnal balanced
5. **RTS = silent killer** — Rp 80k-125k rugi per RTS, Rp 45jt/bulan. LabaBersih intervene SEBELUM RTS final (hold + retry). Indonesia-specific COD problem.

### Pipeline Architecture (KRITIS — baca pipeline-design.md)
3 status track independen: ORDER (dibuat→selesai) / FULFILLMENT (unfulfilled→delivered) / PAYMENT (unpaid→settled).
Side effects tied to events. 3 trigger sources (scan/API/XLSX) → 1 shared function.
RTS = sub-flow dengan tiket management (detect→hold→retry→saved/returned).
Detail lengkap: `rules/pipeline-design.md`

### Fundamental Accounting Principles (WAJIB DIPATUHI)
1. **Pendapatan selalu di CREDIT** — revenue accounts normal_balance = credit
2. **Beban selalu di DEBIT** — expense accounts normal_balance = debit
3. **Trial Balance HARUS balance** — total debit = total credit, ada UI untuk cek
4. **Kode akun IMMUTABLE** — sekali dibuat gak boleh diubah (seperti Elixir immutability) ✅
5. **Fee estimation ≠ fakta** — saat kirim = asumsi 80-90%, saat rekonsil = fakta terungkap
6. **Per-fee reconciliation** — setiap fee di-match individual, bukan lump sum (A2X approach)
7. **Unknown fee → suspense account** — fee tak dikenal masuk penampung, user kategorikan manual
8. **Rekonsiliasi WAJIB validasi toko** — order.store_id HARUS == selected store_id. Gak boleh cross-store.
9. **COA auto-generate per toko** — saat buat toko → auto-create akun (piutang, kas, pendapatan, retur) di bawah platform header
10. **COA structure strict** — selalu group by platform (Opsi A). Piutang Shopee → leaf per toko. Aggregate di header.

### COA Architecture (FUNDAMENTAL — harus di-implement sebelum G1)

**Current (SALAH):**
```
Store → piutang_account_code: "12-201" (STRING, manual, bisa salah)
COA → flat, gak ada hierarchy, gak ada platform grouping
Rekonsiliasi → gak validasi store ownership
```

**Target (BENAR):**
```
Store → piutang_account_id: UUID (FK ke Account, guaranteed valid)
COA → 3 level hierarchy:
  Level 1: Type header (12 Piutang)
  Level 2: Platform header (12-1 Piutang Shopee) ← auto saat platform pertama
  Level 3: Toko leaf (12-101 Piutang Shopee Store 1) ← auto saat toko dibuat
Rekonsiliasi → order.store_id == selected store_id (ENFORCED)
```

**Auto-generate flow:**
```
1. Org baru dibuat → seed COA template (level 1 headers)
2. Toko baru dibuat →
   a. Cek platform header ada? Kalau belum → create (level 2)
   b. Create leaf accounts per toko (level 3)
   c. Link store record ke accounts via FK
3. Rekonsiliasi → validasi order milik toko yang dipilih
4. Laporan → aggregate otomatis dari leaf → platform → type
```

**Migration needed:**
- Store table: string codes → FK account_ids + legal_entity_id
- Account table: tambah `source_type` ("template"/"platform"/"store"/"entity") + `source_id`
- Seed: COA template per org baru
- Existing data: migrate string codes ke FK references
- New table: `legal_entities`

### Legal Entity + Tax Reporting (FUNDAMENTAL — design dari awal)

**Problem seller Indonesia:**
- PKP threshold Rp 4.8M/tahun → lewat = wajib PPN 11%
- Seller pecah bisnis ke beberapa entitas (PT, CV, personal) untuk tax planning
- Harus tau omzet per entitas — kalau gak tracking, kena pajak lebih
- PPh Final UMKM 0.5% per entitas

**Architecture:**
```
Organization (1 akun LabaBersih)
├── Legal Entity: PT Bestari Jaya (NPWP: xxx)
│   ├── Store: Shopee Bestari
│   ├── Store: TikTok Bestari ID
│   ├── PO-001 (pembelian milik PT ini)
│   ├── Lots: milik PT ini
│   └── Persediaan: 13-1 Persediaan PT Bestari Jaya
│
├── Legal Entity: CV Maju Terus (NPWP: yyy)
│   ├── Store: TikTok Bestari EN
│   ├── Store: Mengantar Bestari
│   ├── PO-002 (pembelian milik CV ini)
│   ├── Lots: milik CV ini
│   └── Persediaan: 13-2 Persediaan CV Maju Terus
```

**Impact ke semua model:**

| Model | Sekarang | Target |
|---|---|---|
| Store | milik org | milik org + **legal_entity** |
| PO | milik org | milik org + **legal_entity** |
| Inventory Lot | milik org | milik org + **legal_entity** |
| Stock Transaction | milik org | milik org + **legal_entity** |
| Persediaan Account | 1 per org (13-100) | 1 per **legal_entity** (13-1xx) |
| HPP Account | 1 per org (50-100) | 1 per **legal_entity** (50-1xx) |
| Neraca | 1 per org | Per **legal_entity** + consolidated |
| Laba Rugi | 1 per org | Per **legal_entity** + consolidated |
| Trial Balance | 1 per org | Per **legal_entity** + consolidated |

**Fitur per Legal Entity:**
1. Omzet tracker — warning mendekati 4.8M
2. Laporan per entitas (L/R, Neraca, TB)
3. Tax summary — PPh Final 0.5% per entity
4. PKP alert threshold

**Database table baru:**
```
legal_entities:
  id (binary_id)
  org_id (FK organizations)
  name (string) — "PT Bestari Jaya"
  npwp (string) — "01.234.567.8-901.000"
  entity_type (string) — "pt" / "cv" / "personal"
  address (text)
  is_active (boolean)
  timestamps
```

**Pendekatan pragmatis:**
```
Default: 1 legal entity per org (auto-create saat daftar)
→ User baru: gak perlu mikir entity, semua jalan seperti biasa
→ User scale: aktifkan multi-entity, pisahkan toko + persediaan per entity
→ Data structure SUDAH SIAP dari awal — gak perlu restructure
```

### Warehouse Location Hierarchy (design sekarang, fitur nanti)

**Tambah di migration bersamaan:**
- `warehouse_locations` + `parent_id` (FK self-reference, nullable) — untuk hierarchy Zone → Rack → Bin
- `warehouse_locations` + `legal_entity_id` (FK, nullable) — kepemilikan stok per entity

**Kenapa sekarang:** Biaya tambah column nullable = hampir nol. Biaya restructure nanti = tinggi.

```
Gudang Kediri (warehouse)
├── Zona Frozen (location, parent=null, entity=null)
│   ├── Rak A (location, parent=Zona Frozen, entity=PT Bestari)
│   └── Rak B (location, parent=Zona Frozen, entity=CV Maju)
└── Zona Packing (location, parent=null, entity=null)
```

### Journal Architecture: Daily Summary + Per-Order Lines

**Keputusan:** 1 jurnal per toko per hari, lines per order (Opsi 4).

**Kenapa bukan per-order jurnal:**
- 300 order/hari = 600 jurnal/hari = 18.000/bulan → DB berat, gak scale
- Fee estimasi per order → harus adjustment saat rekonsil → tambah jurnal lagi

**Kenapa bukan pure summary (A2X tanpa fee):**
- Revenue 1M tanpa beban tercatat = overconfident
- Seller ambil keputusan salah karena laba kelihatan 100%
- Ini PROBLEM #1 yang LabaBersih harus SOLVE

**Approach final:**
```
SHIP (real-time per order):
  → Stok potong (FEFO lot consumption) ✓
  → HPP tercatat per order ✓
  → Jurnal fee BELUM (batch nanti)

DAILY BATCH (Oban job akhir hari, per toko):
  → 1 jurnal header: "Penjualan Shopee Bestari — 30 Mar 2026"
  → Lines per order (per-order traceable):
      Dr. Piutang (PS2603-00001)  Rp 184.000
      Dr. Admin   (PS2603-00001)  Rp   6.000  ← estimasi, LANGSUNG dicatat
      Dr. Komisi  (PS2603-00001)  Rp  10.000
      Cr. Revenue (PS2603-00001)  Rp 200.000
      ────────────────────────────────────────
      Dr. Piutang (PS2603-00002)  Rp 124.200
      ... (semua order hari itu)
  → 1 jurnal HPP summary per toko per hari

SETTLEMENT (7-14 hari kemudian):
  → Per-fee matching (estimasi vs aktual)
  → 1 jurnal adjustment per settlement
  → Known fee → auto-adjust ke akun yang benar
  → Unknown fee → suspense account
```

**Scale:**
| Order/hari | Per-order approach | Daily summary approach |
|---|---|---|
| 300 | 600 jurnal | 6 jurnal (+ 300 line sets) |
| 6.300 | 12.600 jurnal | ~30 jurnal (+ 6.300 line sets) |

**Audit:**
- Auditor minta order PS2603-00001 → filter journal lines by order reference → ketemu semua debit/credit
- Traceable per rupiah ke order spesifik ✓
- Fee per toko terpisah (gak campur antar toko) ✓
- Fee per order beda nominal (karena gross beda) → handled di per-order lines ✓

**Database:**
- `journal_entry_lines` tambah: `order_id` (FK, nullable) — tracking line ke order mana
- `journal_entries` tambah: `batch_date` (date, nullable) — tanggal batch
- `journal_entries` tambah: `batch_type` ("daily_sales"/"daily_hpp"/"settlement_adjustment")

**Bonus features dari referensi software lain:**

| # | Fitur | Dari | Status |
|---|---|---|---|
| 1 | Daily Summary Journal (per toko per hari) | A2X | Design ✓, implement nanti |
| 2 | Aged Receivables (piutang menggantung) | Xero | Design ✓ (detail di bawah) |
| 3 | Recurring Expenses (auto-jurnal bulanan) | QuickBooks | Design ✓ (detail di bawah) |
| 4 | Tax: PPh Final + PPN structure | Xero | Design ✓ — PPh Final implement sekarang, PPN structure only |
| 5 | Financial Health Dashboard | v1 + Stripe | Design ✓ (detail di bawah) |
| 6 | Per-fee reconciliation + suspense | A2X | Design ✓, implement nanti |

### Bonus #2: Aged Receivables (Piutang Menggantung)
**Referensi:** Xero Aged Receivables Report

**Apa:** Laporan piutang per toko, grouped by umur (berapa lama sudah menggantung).

**Data source:** Orders yang status = "dikirim" tapi belum "selesai" (belum rekonsil).
Umur = selisih hari dari shipped_at sampai hari ini.

**Output:**
```
Toko Shopee Bestari:
  0-7 hari:    Rp 15.000.000  (normal — baru kirim, settlement belum jatuh tempo)
  8-14 hari:   Rp  3.000.000  (perhatian — harusnya sudah mulai cair)
  15-30 hari:  Rp    500.000  (WARNING — cek ke platform, mungkin ada masalah)
  > 30 hari:   Rp    100.000  (BAHAYA — kemungkinan tertahan, dispute, atau bug)
  Total:       Rp 18.600.000

Toko TikTok Bestari ID:
  0-7 hari:    Rp 12.000.000
  > 30 hari:   Rp          0  ← bagus, semua cair
  Total:       Rp 12.000.000
```

**Backend:** Query orders WHERE status = "dikirim", GROUP BY age bucket, SUM total_harga per bucket.
**UI:** Tabel per toko, color coded (hijau/kuning/merah), bisa di halaman Laporan sebagai tab baru atau dashboard widget.
**Effort:** Low — data sudah ada, tinggal query + render.

### Bonus #3: Recurring Expenses (Biaya Tetap Otomatis)
**Referensi:** QuickBooks Recurring Transactions

**Apa:** User setup biaya tetap sekali. System auto-generate jurnal setiap bulan via Oban scheduled job.

**Database baru:**
```
recurring_expenses:
  id            : binary_id
  org_id        : FK organizations
  legal_entity_id : FK legal_entities (nullable)
  description   : string — "Sewa Gudang Kediri"
  amount        : decimal — 5.000.000
  debit_account_id : FK accounts — akun beban (62-xxx Biaya Operasional)
  credit_account_id : FK accounts — akun kas (11-100)
  schedule_day  : integer — tanggal generate (1-28)
  is_active     : boolean
  last_generated: date — terakhir auto-generate
  timestamps
```

**Flow:**
```
1. User setup: "Sewa Gudang Rp 5jt setiap tanggal 1"
2. Oban cron job: setiap hari cek apakah ada recurring yang due
3. Tanggal 1 → auto-create jurnal:
     Dr. 62-101 Beban Sewa Gudang   Rp 5.000.000
     Cr. 11-100 Kas                  Rp 5.000.000
4. Update last_generated = hari ini
5. Bulan depan tanggal 1 → repeat
```

**Benefit:** User gak perlu input manual 5+ jurnal biaya tetap setiap bulan. Setup sekali, jalan selamanya.
**Effort:** Low — Oban sudah ada, tinggal 1 table + 1 worker + 1 UI form.

### Bonus #5: Financial Health Dashboard
**Referensi:** v1 Cash Flow Health + Stripe Dashboard + Financial Ratios

**Metrics:**
```
1. Piutang Ratio
   = Piutang belum cair / Total Omzet bulan ini × 100%
   → 35% = KUNING (banyak yang belum cair)
   → > 50% = MERAH

2. Cash Gap
   = Cash masuk bulan ini - Cash keluar bulan ini
   → Positif = HIJAU (surplus)
   → Negatif = MERAH (defisit — keluar lebih banyak dari masuk)

3. Days to Cash
   = Rata-rata hari dari kirim sampai uang cair (settlement)
   → < 7 hari = HIJAU
   → > 14 hari = MERAH

4. Burn Rate
   = Total beban operasional per bulan (sewa + gaji + internet + dll)
   → "Biaya tetap Rp 15jt/bulan, harus jual minimal Rp XX untuk nutup"

5. RTS Rate
   = Jumlah RTS / Total order × 100%
   → < 3% = HIJAU (normal)
   → > 5% = MERAH (ada masalah produk/ekspedisi)

6. Margin per Toko
   = (Pendapatan - Beban) / Pendapatan × 100% per toko
   → Shopee Bestari: 18%
   → TikTok Bestari: 12%
   → Mengantar: 8% ← "toko ini perlu review"

7. PKP Threshold (dari legal entity)
   = Omzet YTD / 4.800.000.000 × 100%
   → 70% = WARNING mendekati PKP
   → > 90% = ALERT KERAS
```

**Data source:** Semua dari data yang sudah ada (orders, jurnal, rekonsil, legal entity omzet).
**UI:** Widget cards di Dashboard utama. Replace dashboard basic sekarang.
**Effort:** Medium — banyak query aggregation tapi gak ada model baru.

### Bonus #4: Tax — PPh Final + PPN (KEPUTUSAN FINAL)

**Keputusan:** PPh Final develop sekarang, PPN design structure sekarang tapi implement nanti.

**Alasan:**
- Hafish dan 90% seller Indonesia = NON-PKP → PPh Final 0.5%
- PPN (PKP) belum ada case langsung, tapi client bisa butuh nanti
- Tech debt dari structure kosong = NOL (nullable fields, no dead code)

**Pendekatan: Per Legal Entity, BUKAN per transaksi.**
Gak perlu tax code per journal line. Tax ditentukan oleh status entity.

**Database:**
```
legal_entities:
  + tax_status    : string — "non_pkp" / "pkp" (default: "non_pkp")
  + pkp_date      : date (nullable — tanggal dikukuhkan PKP, kalau ada)

tax_records (tabel baru — tracking pajak per bulan per entity):
  id              : binary_id
  org_id          : FK organizations
  legal_entity_id : FK legal_entities
  period_month    : integer (1-12)
  period_year     : integer
  tax_type        : string — "pph_final" / "ppn"
  gross_revenue   : decimal — total omzet bulan itu
  ppn_output      : decimal (nullable — hanya untuk PKP)
  ppn_input       : decimal (nullable — hanya untuk PKP)
  tax_payable     : decimal — auto-calculate
  status          : string — "draft" / "filed" / "paid"
  timestamps
```

**NON-PKP flow (implement sekarang):**
```
1. Entity status = non_pkp
2. Akhir bulan → Oban job auto-generate tax_record:
     gross_revenue = SUM semua revenue jurnal entity bulan ini
     tax_payable = gross_revenue × 0.5%
     tax_type = "pph_final"
     status = "draft"
3. User buka Tax Report → lihat:
     "PPh Final Maret 2026: Rp 2.500.000"
     "Bayar sebelum tanggal 15 April 2026"
4. User bayar → klik "Tandai Sudah Bayar" → status = "paid"
5. Riwayat lengkap per bulan
```

**PKP flow (structure sekarang, implement NANTI kalau ada client):**
```
1. Entity status = pkp
2. Akhir bulan → auto-generate tax_record:
     ppn_output = SUM revenue × 11% (dari penjualan)
     ppn_input = SUM purchase × 11% (dari PO receive)
     tax_payable = ppn_output - ppn_input
     tax_type = "ppn"
3. Tax Report: PPN Output - PPN Input = Kurang/Lebih Bayar
4. SPT Masa PPN support
```

**Kenapa BUKAN tech debt:**
- `tax_status` = 1 field, default "non_pkp", gak ada code path yang trigger PPN
- `tax_records.ppn_output/ppn_input` = nullable, gak diisi untuk non_pkp
- `tax_records.tax_type` = "pph_final" satu-satunya yang aktif
- Zero dead code, zero maintenance cost
- Kalau nanti butuh PKP: tambah logic di 1 function, structure sudah siap

**Migration (bareng refactor):**
- [ ] `legal_entities` + `tax_status` + `pkp_date`
- [ ] New table: `tax_records`

---

### COMPLETE REFACTOR CHECKLIST

**Semua yang harus dikerjakan sebelum lanjut fitur baru (G1 staging pipeline):**

#### Migration (1 migration file, atomic):
- [ ] New table: `legal_entities` (id, org_id, name, npwp, entity_type, address, is_active)
- [ ] Alter `stores`: tambah `legal_entity_id` (FK nullable) + ubah `*_account_code` string → `*_account_id` FK
- [ ] Alter `accounts`: tambah `source_type` + `source_id` (tracking asal akun)
- [ ] Alter `purchase_orders`: tambah `legal_entity_id` (FK nullable)
- [ ] Alter `inventory_lots`: tambah `legal_entity_id` (FK nullable)
- [ ] Alter `stock_transactions`: tambah `legal_entity_id` (FK nullable)
- [ ] Alter `warehouse_locations`: tambah `parent_id` (FK self) + `legal_entity_id` (FK nullable)

#### Backend:
- [ ] Schema: LegalEntity (new)
- [ ] Schema: update Account, Store, PO, Lot, StockTransaction, WarehouseLocation
- [ ] Context: auto-create default legal entity saat register_user
- [ ] Context: auto-create COA template saat org baru (header accounts)
- [ ] Context: auto-create platform header + store leaf accounts saat buat toko
- [ ] Context: link store ke accounts via FK (bukan string code)
- [ ] Context: reconciliation store validation (order.store_id == selected)
- [ ] Context: per-fee reconciliation (bukan lump sum)
- [ ] Context: suspense account untuk unknown fee

#### UI:
- [ ] COA: reflect hierarchy 3 level (type → platform → store)
- [ ] Store form: dropdown pilih akun (bukan ketik kode) atau auto-generate
- [ ] Rekonsiliasi: validasi toko, per-fee matching UI
- [ ] Settings: tab Legal Entity (NPWP, nama, tipe)
- [ ] Laporan: filter by legal entity

#### Test:
- [ ] Test auto-generate COA saat buat toko
- [ ] Test store validation di rekonsiliasi
- [ ] Test per-fee reconciliation
- [ ] Test legal entity isolation (lot, PO, persediaan)
- [ ] Existing 133 tests tetap pass

---

## 1. STATUS: Apa yang SUDAH Selesai

### Backend (Layer 0-9) ✅ COMPLETE
- 133 tests, 0 failures
- 10 context modules (Accounts, Accounting, Inventory, Sales, Orders, Returns, Reconciliation, Purchasing, Integrations, Marketing)
- 14 formula jurnal implemented
- FEFO + FIFO lot management
- Advisory lock ID generation (PS, PO, RTS, JE, PKG)
- WIB timezone enforcement
- Multi-tenant org_id
- Status transition enforcement (dikirim hanya lewat packing, selesai hanya lewat rekonsil)
- Mengantar platform-specific journal logic
- Sample order journal
- Sequential period closing
- Deployed ke Fly.io (lababersih.id)

### UI Phase 1-5 ✅ COMPLETE
- Phase 1: Fix bugs (money negatif, fmt duplikat, title bahasa, status label) ✅
- Phase 2: Shared list components (filter_bar, tab_filter, pagination, select_filter) ✅
- Phase 3: Apply ke semua 8 list pages (search, filter, pagination, stat cards) ✅
- Phase 4: Polish detail pages (dates, clickable refs, badges, hide empty) ✅
- Phase 5: Loading states (phx-disable-with), form polish, browser title ✅

### Pages yang sudah ada
| Page | List | Create | Edit | Delete | Detail | Search | Filter | Pagination |
|---|---|---|---|---|---|---|---|---|
| Dashboard | - | - | - | - | - | - | - | - |
| Pesanan | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| Packing | ✅ | ✅ | - | - | ❌ | ✅ | ✅ | - |
| RTS | ✅ | ✅ | - | - | ✅ | ✅ | ✅ | ✅ |
| Produk | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| PO | ✅ | ✅ | - | - | ✅ | ✅ | ✅ | ✅ |
| Toko | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | - |
| Bagan Akun | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | - |
| Jurnal | ✅ | ✅ | - | Void ✅ | ✅ | ✅ | ✅ | ✅ |
| Rekonsiliasi | ✅ | ✅ | - | - | ❌ | ✅ | ❌ | - |
| Laporan | ❌ | - | - | - | - | - | - | - |
| Buku Besar | ❌ | - | - | - | - | - | - | - |
| Settings | ❌ | - | - | - | - | - | - | - |

---

## 2. PENILAIAN: Kenapa Keputusan Ini Diambil

### 2a. v1 UI/UX Score (dari referensi UX)

| Framework | Score | Kesimpulan |
|---|---|---|
| Jakob Nielsen Heuristics | 5.3/10 | Fungsional tapi inkonsisten. Badge beda style tiap halaman (heuristic #4 GAGAL). |
| Laws of UX | 5.0/10 | Information overload. Dashboard 8 stat cards + chart + cash flow = Miller's Law dilanggar. |
| Don't Make Me Think | 4.8/10 | Dashboard bikin mikir. Gak ada visual hierarchy yang jelas. |
| Refactoring UI | 4.2/10 | Terlalu padat. Gak ada white space. Warna inkonsisten. |
| **RATA-RATA** | **4.8/10** | **Below average by UX standards** |

**Kesimpulan:** v1 itu "ugly but functional". User pakai karena fitur lengkap, bukan karena UX bagus.

### 2b. v1 Error Handling Score

| Kategori | Score | Detail |
|---|---|---|
| Form validation | 7/10 | Per-field, bahasa Indonesia, border merah. Tapi HPP false positive. |
| Not found | 8/10 | "Pesanan tidak ditemukan" + recovery link. BAIK. |
| 404 page | 1/10 | Default Next.js. Hitam, Inggris, gak ada nav. FATAL. |
| Error prevention | 6/10 | Button disabled ✅ tapi status dropdown misclick ✗ |
| Console errors | 2/10 | React hydration error SETIAP halaman. |
| **RATA-RATA** | **4.8/10** | **Buruk. v2 harus jauh lebih baik.** |

### 2c. v1 Feature Completeness Score

| Kategori | Score | Detail |
|---|---|---|
| Order management | 9/10 | Lengkap: CRUD, import, sync, bulk, ekspedisi, multi-platform |
| Akuntansi | 8/10 | COA tree, jurnal, laporan L/R + neraca, buku besar, rekonsil |
| Inventory | 7/10 | Produk + stok + gudang. Tapi gak ada FEFO. |
| RTS | 7/10 | Daftar + terima paket + klaim. Sub-tab workflow. |
| Dashboard | 8/10 | 8 stat cards + chart + cash flow + alert. Terlalu banyak tapi lengkap. |
| **RATA-RATA** | **7.8/10** | **Fitur lengkap, ini kenapa user tetap pakai.** |

### 2d. Keputusan Design v2

Berdasarkan penilaian di atas:

| Keputusan | Alasan |
|---|---|
| **Ambil pattern v1** (stat cards, tabs, filter, table) | Pattern-nya BAIK (score 8/10). User familiar. |
| **JANGAN copy style v1** | Style BURUK (score 4.2/10). Inkonsisten, padat. |
| **Pakai shared components** | Solve masalah #1 v1: inkonsistensi badge. 1 component = 1 kebenaran. |
| **Max 7 kolom tabel** | v1 9+ kolom = horizontal scroll. Miller's Law. |
| **Dashboard 4 stat cards, bukan 8** | v1 dashboard overwhelming. Less is more. |
| **Custom 404 + error pages** | v1 404 = 1/10. v2 harus punya branded error page. |
| **Loading states semua button** | v1 gak ada = double submit. v2 sudah implement. |
| **Status gak bisa diubah dari tabel** | v1 dropdown di tabel = misclick. v2 enforce flow. |
| **FEFO + expiry tracking** | v1 gak punya. v2 advantage. Pertahankan. |
| **Zero console errors** | v1 React error tiap halaman. Elixir compile-time = advantage. |

---

## 3. YANG HARUS DIKERJAKAN (Part by Part)

### Part A: Laporan Keuangan UI
**Prioritas:** TINGGI — Hafish cek tiap hari. Backend sudah ada.
**Scope:** Laba Rugi + Neraca (tree view). Date picker bulan. 4 stat cards.
**Backend:** `income_statement/3` ✅, `balance_sheet/2` ✅. Perlu: hierarchy grouping function.
**v1 ref:** screenshot laporan.png — 4 tab, stat cards, tree report.
**Effort:** Medium (1 session)

### Part B: Bagan Akun Hierarchy
**Prioritas:** TINGGI — v2 flat list gak bisa dipakai untuk akuntansi serius.
**Scope:** Tree view + indentation by level. Group by type. Saldo column. Stat cards.
**Backend:** Perlu `account_balances/1`, `count_accounts_summary/1`.
**v1 ref:** screenshot bagan-akun.png — tree 248 akun, indented, saldo visible.
**Effort:** Medium (1 session)

### Part C: Pesanan Table Enrichment
**Prioritas:** MEDIUM — Nelly perlu tau status Jurnal/Bayar tanpa klik detail.
**Scope:** Tambah kolom Jurnal (✓/✗), Bayar (badge), Pendapatan. Nomor pesanan sub-text di bawah ID. Max 7 kolom.
**Backend:** Cek journal existence per order. `payment_status` sudah ada.
**v1 ref:** screenshot pesanan — 9+ kolom tapi kita pilih 7 terpenting.
**Effort:** Low (1 session kecil)

### Part D: Stat Cards di Semua List Pages
**Prioritas:** MEDIUM — Konsistensi. Quick overview per halaman.
**Scope:** RTS, PO, Jurnal, Rekonsil, Produk — masing-masing 4 stat cards.
**Backend:** 5 summary functions baru.
**v1 ref:** Setiap halaman v1 punya stat cards.
**Effort:** Low (1 session)

### Part E: Buku Besar
**Prioritas:** MEDIUM — Auditor butuh. Simple UI.
**Scope:** Dropdown pilih akun → table transaksi + running saldo.
**Backend:** Perlu `general_ledger/3` function baru.
**v1 ref:** screenshot buku-besar.png — dropdown + date picker + table.
**Effort:** Medium (1 session)

### Part F: Settings UI
**Prioritas:** MEDIUM — User management.
**Scope:** Tab Profil (edit nama/phone/password) | Organisasi (nama toko) | Anggota (list + invite).
**Backend:** Semua sudah ada di Accounts context ✅.
**v1 ref:** screenshot pengaturan.png — tab-based settings.
**Effort:** Medium (1 session)

### Part G: Staging Pipeline + XLSX Import

> **ARSITEKTUR KRITIS:** Part ini bukan cuma XLSX import. Ini bangun **staging pipeline**
> yang jadi fondasi untuk SEMUA data masuk (XLSX, API sync TikTok/Shopee/Mengantar, manual).
> Service layer-nya SAMA — yang beda cuma parser/source.

**Prioritas:** TINGGI — Nelly import 300+ order/hari. Pipeline ini juga fondasi API integration.

**Flow (shared untuk semua source):**
```
Source (XLSX/API/Manual) → Parser → Staging Orders → Preview → Approve → Production Orders
```

**Sub-parts:**

#### G1: Staging Service (Backend)
**Scope:** Context functions untuk staging pipeline.
**Backend:**
- `Integrations.create_staging_orders/1` — bulk insert ke staging (upsert, skip duplikat)
- `Integrations.list_staging_orders/2` — list dengan filter (status, platform, source)
- `Integrations.preview_staging_order/1` — get 1 staging order dengan diff vs production
- `Integrations.approve_staging_orders/2` — bulk approve → create production orders via Orders.create_order
- `Integrations.reject_staging_orders/1` — bulk reject
- Dedup logic: cek `platform_order_id` unique per org
- Staging table sudah ada ✅ (`staging_orders` dari Layer 8)
**Effort:** Medium (1 session)

#### G2: Staging UI Components (Shared)
**Scope:** UI components yang dipakai oleh XLSX import DAN API sync nanti.
**Components:**
- **Staging List Page** (`/app/staging`)
  - Tab filter: Semua | Menunggu | Disetujui | Ditolak
  - Filter: source (XLSX/API/Manual), platform, tanggal
  - Table: Platform Order ID, Platform, Customer, Total, Status, Source, Tanggal
  - Bulk select (checkbox) → Approve All / Reject All buttons
- **Staging Preview** (modal/detail)
  - Side-by-side: staging data vs production data (kalau sudah ada)
  - Edit fields sebelum approve (kalau data salah)
  - Approve / Reject / Skip buttons
- **Staging Stats**
  - Stat cards: Menunggu X, Disetujui X, Ditolak X, Total X
**Effort:** Medium (1 session)

#### G3: XLSX Parser + Upload UI
**Scope:** Khusus XLSX — parser + upload interface.
**Backend:**
- Dependency: `xlsxir` atau `elixlsx` library
- `Orders.parse_order_xlsx/1` — parse file, return list of order maps
- Support format: Shopee XLSX, TikTok XLSX, Mengantar XLSX (3 format berbeda)
- Mapping: kolom XLSX → staging_order fields
**UI:**
- Upload page (`/app/pesanan/import`) — drag & drop atau file input
- Format selection (Shopee/TikTok/Mengantar/Custom)
- Preview parsed data sebelum masuk staging
- Confirm → insert ke staging_orders
- Redirect ke staging list untuk approve
**v1 ref:** Button "Import" di header Pesanan
**Effort:** High (1-2 session)

#### G4: Settlement XLSX Parser (Future — setelah G3)
**Scope:** Parser untuk rekonsiliasi XLSX settlement.
- Shopee Rekonsiliasi XLSX
- Shopee Income XLSX
- TikTok Settlement XLSX
- Flow sama: parse → staging → approve → process_reconciliation
**Effort:** Medium (1 session, setelah G3 proven)

**Total Effort Part G:** 3-4 session (G1 → G2 → G3 → optional G4)

**Dependency:**
```
G1 (staging service) → G2 (staging UI) → G3 (XLSX parser)
                                        → Future: API sync juga pakai G1+G2
```

### Part H: Error Handling Polish (dari audit)
**Prioritas:** MEDIUM
**Scope:**
- Custom 404 page (branded, bahasa Indonesia, link kembali)
- Form validation real-time (phx-change)
- Not found graceful di semua detail pages
**Effort:** Low (1 session)

---

## 4. URUTAN EKSEKUSI

```
Session 1: Part B (Bagan Akun Hierarchy) ✅ DONE
Session 2: Part A (Laporan Keuangan) ✅ DONE
Session 3: Part C + D (Pesanan Enrich + Stat Cards) ✅ DONE
Session 4: Part E (Buku Besar) ✅ DONE
Session 5: Part F (Settings) ✅ DONE
Session 6: Part H (Error Handling Polish) ✅ DONE
Session 7: Part G1 (Staging Service — backend)
Session 8: Part G2 (Staging UI Components)
Session 9: Part G3 (XLSX Parser + Upload)
Session 10: Part G4 (Settlement XLSX — optional)
```

### Setelah Part G selesai, API Integration tinggal:
```
- Buat parser per platform (TikTok, Shopee, Mengantar)
- Colok ke staging pipeline yang sudah ada (G1 + G2)
- Gak perlu bikin UI baru — staging list sudah handle semua source
```

### Per session:
1. Plan → confirm scope
2. Backend (kalau perlu function baru)
3. LiveView UI
4. Test (compile + 133 tests)
5. Deploy ke production
6. Screenshot verify via DevTools

---

## 5. RULES YANG HARUS DIIKUTI (dari semua audit)

### Dari design-system.md:
- Shared components only — DILARANG copy-paste style
- daisyUI tokens — DILARANG inline style/arbitrary colors
- Max 7 kolom tabel — Miller's Law
- Button: primary kanan, cancel kiri, danger far left — Fitts's Law
- Typography: max 2 size per section — hierarchy
- Spacing: Tailwind scale only — consistency

### Dari CLAUDE.md:
- Logic di context, BUKAN LiveView — separation of concerns
- org_id di setiap query — multi-tenant
- Repo.transaction untuk multi-step — atomic
- {:ok, result} atau {:error, reason} — no exceptions
- Error message bahasa user, bukan teknis

### Dari v1 audit:
- AMBIL: pattern (stat cards, tabs, filter, table)
- HINDARI: style inkonsisten, 9+ kolom, dashboard overwhelming
- IMPROVE: error handling (custom 404, loading states, real-time validation)
- PERTAHANKAN keunggulan v2: FEFO, shared components, tests, status enforcement

### Dari error handling audit:
- Custom 404/error page WAJIB
- Form validation real-time (phx-change)
- Loading states SEMUA button (sudah ✅)
- Konfirmasi destructive action (sudah ✅)

---

## 6. DOMAIN MAP (6 Domain)

> Detail teknis lengkap ada di `rules/v2-execution-blueprint.md`
> Master plan ini = KEPUTUSAN. Blueprint = EKSEKUSI.

| # | Domain | Scope | Phase 1 | Phase 2 | Phase 3 |
|---|---|---|---|---|---|
| 1 | **Master Data** | Product, Warehouse, Store, Legal Entity, Supplier, Courier, Customer | Bundle+unit conv, courier mapping, reserved_stock | Multi-warehouse enforcement | — |
| 2 | **Accounting** | COA, Journal, Reconciliation, Closing, Tax, Reports | Daily summary journal (Oban) | Recurring expenses, aged receivables, financial health | — |
| 3 | **Order Intake** | XLSX import, API sync, SKU matching | XLSX auto-import, SKU importer | API staging pipeline (TikTok/Shopee/Mengantar) | Settlement XLSX |
| 4 | **Fulfillment** | Packing, ship, pick, handover, GRN | Keep current + reserved_stock + courier_mapping | FO→PickList→Ship→Handover, GRN | PackingIncident QC |
| 5 | **Returns** | RTS, inbound, klaim, supplier return | ✅ Sudah implemented | Customer refund/replacement | — |
| 6 | **Intelligence** | Demand, ABC-XYZ, spike, campaign, reorder | — | — | Full implementation |

### Keputusan Arsitektur Kunci

- **Customer:** Denormalized di order (name, phone). Gak ada tabel terpisah. Future: aggregate by phone.
- **Product:** 2 tipe (simple/bundle). Bundle = BOM pattern. DB selalu base unit (pcs).
- **Journal:** Daily summary per toko per hari + per-order lines (Opsi 4). Scale: 30 jurnal vs 12.600.
- **Fulfillment Phase 1:** Keep packing session + ship_order. Tambah reserved_stock + courier_mapping saja.
- **Fulfillment Phase 2:** Replace dengan FulfillmentOrder→PickList→Ship→CourierHandover.
- **GRN Phase 2:** Replace simple receive_po dengan GRN (qty_received vs qty_rejected, TTD).

---

## 7. FASE 1: MENERIMA PESANAN (Order Intake)

> Ini fase terakhir yang dikerjakan tapi paling krusial untuk migrasi v1 → v2.
> Tanpa ini, Nelly gak bisa pindah ke v2.

### 6a. Prinsip Produk

**Product setup = master data ONLY:**
- Nama, SKU, HPP, Harga Jual, Unit
- Stok = 0 (readonly, gak bisa diisi manual)
- Stok HANYA masuk lewat dokumen: PO Receive / Stock Adjustment / RTS Baik
- Alasan: setiap stok harus punya "surat lahir" (bukti dokumen)

**SKU = milik seller, bukan LabaBersih:**
- Seller sudah punya SKU yang konsisten di semua marketplace
- LabaBersih pakai SKU yang SAMA (gak bikin SKU baru)
- Import XLSX → match by exact SKU → link ke product
- Gak perlu sku_mappings untuk 90% kasus

**Harga Jual field (BARU — belum ada di schema):**
- `products` + `harga_jual` (decimal, nullable)
- Untuk laporan margin: harga_jual - HPP = margin per produk

### 6f. Produk Bundle vs Simple (BOM Pattern)

**Best practice dari Dear Inventory / ERPNext / Cin7:**

**2 tipe produk:**
```
SIMPLE (SKU Utama):
  - Punya stok sendiri (dari PO)
  - HPP dari FEFO lot
  - Contoh: FRB01 Forbest New 1kg, stok: 100

BUNDLE:
  - TIDAK punya stok sendiri
  - Stok = dihitung dari komponen: floor(komponen_stok / qty_per_bundle)
  - HPP = sum HPP komponen
  - Contoh: BNDL02 [3Kg] Forbest = FRB01 × 3, tersedia: 33
```

**Database:**
```
products:
  + type          : string — "simple" / "bundle" (FIELD BARU)
  + harga_jual    : decimal (FIELD BARU)

bundle_items (BOM — Bill of Materials):
  id
  bundle_product_id    → FK products (type=bundle)
  component_product_id → FK products (type=simple)
  quantity             : integer (berapa komponen per 1 bundle)
```

**Saat bundle dijual:**
```
Order: BNDL19 × 2
Lookup: BNDL19 = FRB01×1 + ORGAMAX×1
Consume: FRB01×2 (FEFO) + ORGAMAX×2 (FEFO)
HPP = sum lot costs dari kedua komponen
```

**Di halaman Produk:**
```
Tab: Semua (67) | SKU Utama (16) | Bundle (51)
Bundle row: tampilkan komponen + stok tersedia (virtual)
```

### 6g. Konversi Satuan (Unit Conversion)

**Problem:** PO/receive dalam kardus, order/jual dalam pcs. Audit stok hitung kardus, database simpan pcs.

**Prinsip: database SELALU simpan base unit (pcs). Konversi di application layer.**

**Database:**
```
unit_conversions:
  id
  product_id     → FK products
  unit_name      : string ("kardus", "lusin", "pak")
  factor         : decimal (18 = 1 kardus = 18 pcs)
  timestamps
```

**Contoh data:**
```
FRB01 → kardus → 18 (1 kardus = 18 pcs)
RTN01 → kardus → 20 (1 kardus = 20 botol)
```

**Implikasi per operasi:**

| Operasi | User input | System convert & simpan |
|---|---|---|
| PO | 10 kardus @ Rp 810.000/kardus | 180 pcs, lot cost Rp 45.000/pcs |
| Receive | 10 kardus ✓ | 180 pcs masuk stok |
| Order | 3 pcs | consume 3 pcs dari lot |
| Stok display | — | "180 pcs (10 kardus)" |
| HPP | — | selalu per base unit dari lot |

**Stock Audit Report (fitur penting):**
```
SKU    Produk           Stok(pcs) Stok(kardus)  Sisa
FRB01  Forbest New 1kg  180       10 kardus     0 pcs      ✓ PAS
FRB02  Forbest 1kg       55        3 kardus     1 pcs      ← sisa
RTN01  Ritrina 1 Liter   42        2 kardus     2 botol    ← sisa
```
Auditor di gudang hitung kardus (cepat), match dengan system. Gak perlu bongkar hitung satuan.

### 6b. SKU Importer (Onboarding Produk Cepat)

**Problem:** Seller punya 67 produk. Input 1-1 = capek.

**Solusi:** Import SKU dari XLSX pesanan.
```
1. Upload XLSX pesanan 7 hari terakhir
2. System parse → extract SKU unik + nama produk
3. Tampilkan tabel: SKU | Nama | HPP (user isi) | Harga Jual (user isi)
4. User bulk-fill HPP + harga jual
5. Confirm → create semua produk sekaligus
6. Stok = 0 (nanti masuk lewat PO)
```

**SKU Trial (verifikasi):**
```
Total SKU:       67
Sudah ada HPP:   60 ✓
Belum ada HPP:    7 ← WARNING
Bundle:            5 ← perlu setup bundle mapping
```

### 6c. Halaman Pesanan (Order List) — SPESIFIKASI LENGKAP

> Hafish: "harus kaya informasi, pipeline-nya clear"
> Pattern: mirip v1 tapi improved berdasarkan audit

**Kolom tabel (informasi yang HARUS ada):**

| # | Kolom | Data | Sumber | Catatan |
|---|---|---|---|---|
| 1 | ID Pesanan | PS2603-00001 | LabaBersih generate | Clickable ke detail |
| 2 | No. Pesanan Platform | TK-583220... | Dari XLSX/API | Bisa panjang, truncate |
| 3 | Tanggal Platform | 30 Mar 2026, 14:05 | Dari XLSX/API | Kapan customer order |
| 4 | Tanggal Import | 30 Mar 2026, 15:29 | LabaBersih timestamp | Kapan masuk ke system |
| 5 | Customer | Budi / D****o | Dari platform | Masked untuk privacy |
| 6 | Platform | tiktok / shopee | Badge | Per toko |
| 7 | Status Pesanan | dibuat/dikemas/dikirim/selesai/rts | Badge | Pipeline utama |
| 8 | Status Bayar | Belum Bayar / Lunas | Badge | Dari rekonsil |
| 9 | Status Jurnal | ✓ / ✗ | Icon | Ada jurnal penjualan atau belum |
| 10 | Fulfillment | Terpenuhi / Belum | Badge/icon | Sudah ship lewat packing |
| 11 | Revenue | Rp 200.000 | total_harga | Right-aligned, mono |
| 12 | Resi | JX77099... | no_resi | Truncate, dari platform |

**Tapi Miller's Law = max 7 kolom visible.**

**Solusi: 2 level info per row (seperti v1):**
```
Row utama (7 kolom):
│ ID Pesanan   │ Customer │ Platform │ Status │ Jurnal │ Bayar │ Revenue │

Sub-row (di bawah ID, text kecil muted):
│ TK-58322... · 30 Mar 14:05 · JX77099... │
```

Ini pattern v1 — ID besar di atas, nomor platform + tanggal + resi kecil di bawah. 7 kolom tapi informasi 12 field.

**Header actions:**
```
[Upload XLSX] [Sync] [+ Manual]
```

**Stat cards (sudah ada):**
```
Dikemas: 37 | Dikirim: 2313 | Selesai: 2271 | RTS: 22
```

**Tab filter (sudah ada):**
```
Semua | Dibuat | Dikemas | Dikirim | Selesai | RTS | Dibatalkan
```

**Filter bar (sudah ada):**
```
🔍 Cari nomor pesanan, customer... | Platform ▾ | Tanggal ▾
```

**Saran improvement dari saya (bukan dari v1):**

1. **Tanggal filter** — range picker (dari-sampai), bukan cuma dropdown
2. **Bulk actions** — checkbox select → "Kirim Semua" / "Export" (nanti)
3. **Quick filter chips** — "Belum ada jurnal" / "Belum bayar" (1 klik filter)
4. **Sort** — klik header kolom untuk sort (tanggal, revenue, status)
5. **Inline resi** — resi muncul di sub-row, gak perlu klik detail
6. **Color coding revenue** — order > Rp 500k highlight subtle (high value order)

### 6d. Order Schema Update (field yang BELUM ada)

```
orders (perlu ALTER):
  + platform_order_date  : utc_datetime  ← tanggal dari platform
  + imported_at          : utc_datetime  ← tanggal masuk LabaBersih
  + fulfillment_status   : string        ← "pending" / "fulfilled"
  + source               : string        ← "xlsx" / "api" / "manual"
```

### 6e. Import XLSX Flow (Tanpa Preview, Tanpa Staging)

```
1. Klik "Upload XLSX" di header Pesanan
2. Modal: pilih file + pilih toko (dropdown)
3. System parse XLSX:
   a. Detect format (Shopee/TikTok/Mengantar) by column headers
   b. Extract orders + items
   c. Per item: match SKU ke products (exact match)
4. Auto-import:
   ├── Order baru → create (status: "dibuat")
   ├── Order duplikat (nomor_pesanan sudah ada) → skip
   ├── SKU gak dikenal → order tetap masuk, item flagged "SKU unknown"
   └── Error (data invalid) → reject, log error
5. Toast: "285 pesanan masuk, 10 duplikat, 5 SKU unknown"
6. Order langsung muncul di list
7. SKU unknown → user bisa map nanti di halaman produk
```

**Gak ada preview. Gak ada staging. Upload → masuk.**
- Zero console errors (Elixir advantage, sudah ✅)
