| Kapabilitas | A2X | LabaBersih |
|---|---|---|
| Accounting (COA, jurnal, laporan) | Bridge ke Xero | Built-in |
| Per-fee Reconciliation + Suspense | Yes | Yes |
| Platform | Shopify, Amazon | TikTok, Shopee, Mengantar |
| Inventory + FEFO | No | Yes |
| Order Management + Packing | No (pakai Shopify) | Built-in |
| RTS/Returns + Inbound | No | Yes |
| PO/Purchasing | No | Yes |
| Target Market | Global (English) | Indonesia (Bahasa, Rupiah, WIB) |
| Layer | Tech |
|---|---|
| Bahasa | Elixir |
| Framework | Phoenix 1.8 |
| UI | LiveView + Tailwind v4 |
| Database | PostgreSQL (Fly.io) |
| Auth | Guardian (JWT) + bcrypt |
| Jobs | Oban |
| Hosting | Fly.io (~$10-15/bulan) |
| Domain | Phase 1 Sekarang | Phase 2 Nanti | Phase 3 Scale |
|---|---|---|---|
| Master Data | Bundle + unit conv, courier mapping, reserved_stock | Multi-warehouse enforcement | — |
| Accounting | Daily summary journal (Oban) | Recurring expenses, aged receivables, financial health | — |
| Order Intake | XLSX auto-import, SKU importer | API staging pipeline (TikTok/Shopee/Mengantar) | Settlement XLSX |
| Fulfillment | Keep current + reserved_stock + courier_mapping | FO → PickList → Ship → Handover, GRN | PackingIncident QC |
| Returns | Done | Customer refund/replacement | — |
| Intelligence | — | — | Demand, ABC-XYZ, spike, reorder |
Store N──1 Legal Entity
Store 1──8 Account (FK, auto-generated)
Store N──1 Warehouse (default)
Order N──1 Store
Order Item N──1 Product
Journal Entry Line.order_id → Order (per-order tracking)
Journal Entry.batch_date + batch_type (daily summary)
Legal Entity 1──N Tax Record
Legal Entity 1──N Purchase Order
Legal Entity 1──N Inventory Lot
Designed Phase 1
stok = readonly, HANYA berubah lewat dokumen (PO Receive / Adjustment / RTS Baik)simple (punya stok) dan bundle (virtual, dari komponen)-- ALTER products
ADD COLUMN type VARCHAR NOT NULL DEFAULT 'simple' -- simple / bundle
ADD COLUMN harga_jual DECIMAL -- untuk margin report
ADD COLUMN reserved_stock INTEGER NOT NULL DEFAULT 0 -- committed qty
-- NEW table: bundle_items (BOM)
bundle_items:
id BINARY_ID PK
bundle_product_id FK → products (type=bundle)
component_product_id FK → products (type=simple)
quantity INTEGER NOT NULL -- berapa komponen per 1 bundle
UNIQUE(bundle_product_id, component_product_id)
Stok bundle = floor(MIN(komponen.stok / bundle_item.quantity))
HPP bundle = SUM(komponen.hpp × bundle_item.quantity)
Saat bundle dijual:
Order: BNDL02 × 2
Lookup: BNDL02 = FRB01 × 3
Consume: FRB01 × 6 (FEFO)
HPP = sum lot costs dari 6 unit FRB01
On Hand = product.stok (physical stock in warehouse)
Committed = product.reserved_stock (claimed by orders, not yet shipped)
Available = stok - reserved_stock (can be sold)
reserve_stock(product_id, qty) → saat order masuk
release_reservation(product_id, qty) → saat ship atau cancel
Designed Phase 1
unit_conversions:
id BINARY_ID PK
product_id FK → products
unit_name VARCHAR NOT NULL -- "kardus", "lusin", "pak"
factor DECIMAL NOT NULL -- 1 unit = factor × base unit
UNIQUE(product_id, unit_name)
| 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 |
| Display | — | "180 pcs (10 kardus)" |
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 1L 42 2 kardus 2 botol ← sisa
Partial Phase 1
Gudang Kediri (warehouse)
├── Zona Frozen (location, parent=null)
│ ├── Rak A (parent=Zona Frozen, entity=PT Bestari)
│ └── Rak B (parent=Zona Frozen, entity=CV Maju)
└── Zona Packing (location, parent=null)
Designed Phase 1
courier_mappings:
org_id FK → organizations
raw_name VARCHAR -- "Shopee Cargo JNE" (dari marketplace)
courier VARCHAR -- "JNE" (induk kurir)
UNIQUE(org_id, raw_name)
| Raw Name (dari marketplace) | Courier (induk) |
|---|---|
| Shopee Hemat | J&T |
| Shopee Cargo JNE | JNE |
| JNE REG / JNE ECO / JNE Cargo | JNE |
| SiCepat REG / SiCepat Halu | SiCepat |
| SPX Express | SPX |
Decided
orders.customer_name -- "Budi"
orders.customer_phone -- "081234" (nullable)
Implemented
Store:
├── org_id → Organization
├── legal_entity_id → Legal Entity
├── default_warehouse_id → Warehouse (Phase 1 tambah)
├── platform: "tiktok" / "shopee" / "mengantar" / "pos"
├── fee_mapping: [{label, accountCode, percent, flatAmount}]
└── 8 Account FKs (auto-generated):
├── piutang_account_id → "12-201 Piutang Shopee Bestari"
├── revenue_account_id → "40-101 Pendapatan Shopee Bestari"
├── kas_account_id → "11-311 Saldo Shopee Bestari"
├── hpp_account_id → "50-100"
├── persediaan_account_id → "13-100"
├── retur_account_id → "41-101 Retur Shopee Bestari"
├── sample_account_id → nullable
└── transfer_kas_account_id → nullable (Mengantar only)
Implemented
Legal Entity:
├── name: "PT Bestari Jaya"
├── entity_type: "pt" / "cv" / "personal"
├── tax_status: "non_pkp" / "pkp"
├── npwp: "01.234.567.8-901.000"
├── is_default: true (1 per org, auto-create saat register)
└── Owns: stores, POs, lots, stock_transactions, tax_records
Implemented
Level 1: Type header → 12 Piutang (is_header=true, source_type="template")
Level 2: Platform header → 12-2 Piutang Shopee (source_type="platform")
Level 3: Store leaf → 12-201 Piutang Shopee Bestari (source_type="store")
1. Org baru → seed COA template (level 1 headers)
2. Toko baru →
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 leaf → platform → type
Designed Phase 1
SAAT 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: "Penjualan Shopee Bestari — 30 Mar 2026"
→ Lines per order:
Dr. Piutang (PS2603-00001) Rp 184.000
Dr. Admin (PS2603-00001) Rp 6.000 ← fee estimasi
Dr. Komisi (PS2603-00001) Rp 10.000
Cr. Revenue (PS2603-00001) Rp 200.000
→ 1 jurnal HPP summary per toko per hari
SETTLEMENT (7-14 hari kemudian):
→ Per-fee matching (estimasi vs aktual)
→ Known fee → auto-adjust ke akun yang benar
→ Unknown fee → suspense account (61-999)
Implemented
Alur:
1. Validate order.store_id == attrs.store_id (ENFORCED)
2. Calculate estimated piutang dari fee mapping × gross
3. Per fee: match estimated vs actual
4. Known fee dengan selisih → adjustment ke akun fee
5. Unknown fee → suspense account 61-999
Cocok (selisih=0): Dr. Kas / Cr. Piutang
Fee underestimated: Dr. Kas + Dr. Adjustment / Cr. Piutang
Fee overestimated: Dr. Kas / Cr. Piutang + Cr. Adjustment
Unknown fee: → Suspense 61-999 (user kategorikan manual)
Implemented
Per legal entity per bulan:
gross_revenue = SUM revenue jurnal entity bulan itu
tax_payable = gross_revenue × 0.5%
NON-PKP flow:
1. Akhir bulan → auto-generate tax_record
2. Status: draft → filed → paid
3. PKP threshold warning: omzet YTD / Rp 4.8M
PKP flow: Structure ready (nullable ppn_output/ppn_input), implement nanti.
Dr. Piutang {platform} = NET (gross - fees)
Dr. Biaya Admin Platform = fee (dari fee mapping)
Dr. Biaya Komisi = fee
Cr. Pendapatan {platform} = GROSS (totalHarga)
Dr. HPP = FEFO lot cost × quantity
Cr. Persediaan = FEFO lot cost × quantity
Cocok: Dr. Kas / Cr. Piutang
Under: Dr. Kas + Dr. Adjustment / Cr. Piutang
Over: Dr. Kas / Cr. Piutang + Cr. Adjustment
Dr. Retur Penjualan = GROSS
Cr. Piutang {platform} = NET
Cr. Biaya Admin = fee (reverse)
Dr. Persediaan = hppTotal
Cr. HPP = hppTotal
Dr. Beban Kerugian Barang Rusak (64-700) = hppTotal
Cr. Persediaan (13-100) = hppTotal
Dr. Piutang Klaim Kurir (12-400) = hppTotal
Cr. Persediaan (13-100) = hppTotal
Dr. Kas (11-100) = amount
Cr. Piutang Klaim Kurir (12-400) = amount
Dr. Beban Kerugian Piutang (64-600) = amount
Cr. Piutang Klaim Kurir (12-400) = amount
Dr. Persediaan (13-100) = total
Cr. Kas (11-100) = total
Dr. Persediaan (13-100) = total
Cr. Hutang Usaha (20-100) = total
Dr. Hutang Usaha (20-100) = amount
Cr. Kas (11-100) = amount
Dr. Kas (11-100) = total
Cr. Persediaan (13-100) = total
Dr. Hutang Usaha (20-100) = total
Cr. Persediaan (13-100) = total
Shopee/TikTok: Dr. Retur = GROSS, Cr. Piutang = NET, Cr. Fees
Mengantar: Dr. Retur = GROSS, Cr. Piutang = GROSS (tanpa fee)
1. Dr. Pendapatan / Cr. Ikhtisar Laba Rugi (30-400)
2. Dr. Ikhtisar Laba Rugi / Cr. Beban
3. Dr/Cr Ikhtisar ↔ Laba Tahun Berjalan (30-500)
Per toko, grouped by umur piutang:
0-7 hari: normal (baru kirim)
8-14 hari: perhatian (harusnya mulai cair)
15-30 hari: WARNING
> 30 hari: BAHAYA
User setup biaya tetap sekali → auto-jurnal setiap bulan via Oban.
"Sewa Gudang Rp 5jt setiap tanggal 1" → auto Dr. Beban / Cr. Kas
7 metrics:
1. Piutang Ratio (piutang/omzet %)
2. Cash Gap (masuk - keluar)
3. Days to Cash (avg ship → settlement)
4. Burn Rate (biaya tetap/bulan)
5. RTS Rate (rts/total order %)
6. Margin per Toko ((pendapatan-beban)/pendapatan %)
7. PKP Threshold (omzet YTD / 4.8M %)
Designed Phase 1
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")
├── Duplikat (nomor_pesanan exists) → skip
├── SKU unknown → order masuk, item flagged
└── Invalid data → reject, log error
5. Toast: "285 pesanan masuk, 10 duplikat, 5 SKU unknown"
| Platform | Format | Revenue Formula |
|---|---|---|
| TikTok | 63 kolom, multi-row per order | SKU Subtotal - Seller Discount |
| Shopee | 49 kolom, multi-row per order | Total Pembeli + Diskon Shopee - Voucher - Paket Diskon |
| Mengantar | HTML-disguised .xls | COD: productValue - estimatedPrice Non-COD: productValue |
Designed Phase 2
API Sync (TikTok/Shopee/Mengantar)
→ Parser → staging_orders (upsert, dedup by platform_order_id)
→ User preview di /app/staging
→ Approve → production orders
→ Reject → skip
Designed Phase 1
Cascade:
1. Exact SKU match → link product_id
2. Not found → item.product_id = nil, flagged "SKU unknown"
TIDAK ADA fuzzy matching di Phase 1.
90% kasus = exact match (seller SKU konsisten).
1. Upload XLSX pesanan 7 hari terakhir
2. System parse → extract SKU unik + nama produk
3. Tabel: SKU | Nama | HPP (user isi) | Harga Jual (user isi)
4. User bulk-fill
5. Confirm → create semua produk (stok = 0)
6. Stok masuk nanti lewat PO
Phase 1
Current flow (KEEP):
Packing Session → scan barcode → ship_order()
ship_order = atomic: status + stok FEFO + jurnal + audit
Enhancement Phase 1:
+ reserved_stock saat order masuk
+ courier_mapping untuk normalize ekspedisi
+ release_reservation saat ship atau cancel
Phase 2
Order masuk → reserve stock
↓
FulfillmentOrder (batch orders → 1 dokumen)
→ generate PickList (group by Area + SKU)
↓
Kepala Gudang approve (TTD digital)
↓
Picker ambil barang → confirm qty_picked
↓
Packer scan → verify items → tempel resi → pack
→ TRACK SIAPA YANG PACK (PIC packer)
↓
Sort by ekspedisi (via courier_mapping)
↓
Courier Handover (manifest TTD staff + driver)
↓
Side effects: stock deduction + journal + audit
PO Created → Supplier deliver ke gudang
↓
Goods Receipt Note (GRN):
→ Staff cek + hitung fisik
→ Input: qty_received, qty_rejected per item
→ TTD staff gudang + TTD sopir
↓
Stock In:
→ Lot created (FEFO: cost + expiry)
→ Journal: Dr. Persediaan / Cr. Hutang atau Kas
| Table | Purpose |
|---|---|
| fulfillment_orders | Batch document (FO2603-00012) |
| fulfillment_order_items | Orders in batch |
| pick_lists | Pick instruction (PL2603-00012) |
| pick_list_items | SKUs to pick, grouped by area |
| shipments | Per-order ship record (SHP-260331-00412) |
| courier_handovers | Manifest per courier (HO-260331-JNT-001) |
| packing_incidents | QC: wrong item, wrong qty, damaged |
| goods_receipt_notes | Enhanced PO receive (GRN-260331-001) |
| grn_items | Ordered vs received vs rejected |
Implemented
Order dikirim → detect RTS → return_record
↓
RTS Inbound Session:
├── Baik → stok masuk + lot restore + reverse piutang (A3)
├── Rusak → jurnal kerugian (A4)
└── Hilang → klaim kurir (A5)
↓
Klaim Kurir:
├── Diterima → A6: Dr. Kas / Cr. Piutang Klaim
└── Ditolak → A7: Dr. Beban Kerugian / Cr. Piutang Klaim
├── Cash PO → A11: Dr. Kas / Cr. Persediaan
└── Utang PO → A12: Dr. Hutang Usaha / Cr. Persediaan
Phase 3
| Feature | Apa |
|---|---|
| Demand Snapshots | Daily per SKU per warehouse, source breakdown per platform |
| ABC-XYZ | A=top 80% revenue, X=stable demand → AX=auto-reorder |
| Spike Detection | > 2× 14-day MA → flag + auto-response |
| Campaign Planning | Estimated volume → trigger PO, post-campaign accuracy |
| Reorder Suggestion | ROP = daily_demand × lead_time + safety_stock |
| New Product Tracking | Sell-through benchmark → signal hot/normal/slow |
| Shopee & TikTok | Mengantar | |
|---|---|---|
| Model | Marketplace escrow | COD offline |
| Fee | % dari GROSS | Flat (COD fee inc VAT) |
| Piutang | NET (GROSS - fees) | NET (sudah potong ongkir) |
| RTS ongkir | Platform absorb | Seller TIDAK kena lagi |
| Payment | Selalu "unpaid" (escrow) | COD="paid", transfer="unpaid" |
| Platform | Formula | JANGAN |
|---|---|---|
| TikTok | payment.sub_total | JANGAN kurangi seller_discount (sudah included!) |
| Shopee | Total Pembeli + Diskon Shopee - Voucher - Paket Diskon | — |
| Mengantar COD | productValue - estimatedPrice | JANGAN pakai productValue langsung |
| Mengantar Non-COD | productValue | — |
| ScaleV | product_price | JANGAN pakai gross_revenue |
| Session | Scope | Steps |
|---|---|---|
| A | Master Data Migration | Migration + schema + bundle + unit conv + reservation + courier |
| B | Order Intake | XLSX dependency + 3 parsers + import + SKU importer |
| C | Daily Journal + Fulfillment | Oban worker + backfill + reservation in ship/cancel |
| D | UI Polish | Produk form + courier page + order list enrichment |
| E | Style Refactor (LAST) | Shopify Polaris + Stripe + Xero visual |
| Session | Scope |
|---|---|
| F | Full Fulfillment (FO, PickList, Ship, Handover, GRN) |
| G | Accounting Enhance (recurring, aged receivables, health) |
| H | API Integration (TikTok/Shopee/Mengantar sync) |
| I | Settlement XLSX parser |
| Session | Scope |
|---|---|
| J | Intelligence (demand, ABC-XYZ, spike, campaign, reorder) |
{:ok, result} atau {:error, reason}org_id di setiap query — multi-tenant enforcedRepo.transaction untuk multi-step — atomic