BMP

Business Management Platform

Project Blueprint & Design Documentation

Dokumen Gabungan — v2.3

Table of Contents

Introduction 11

Peta Keseluruhan & Status 11

Dua “Architecture” yang Berbeda Level 12

Urutan Pengerjaan 12

Latar Belakang — Analisis 12 ERP Open Source 12

Chapter 1: Design Foundation 14

1.1 Theme 14

1.2 Color System 15

1.3 Typography 16

1.4 Iconography 17

1.5 Shadow & Elevation System (baru) 18

1.6 Spacing, Radius & Grid (baru) 19

1.7 Contoh Penerapan 21

1.8 Tiga Prinsip Meta Visual 22

Chapter 2: Interaction Structure 23

2.1 Landing Page & Layout 23

2.1.1 Rail Sidebar (atas → bawah) 24

2.1.2 Header (page-head 44px) 24

2.1.3 Isi Landing (My Attention) 24

2.1.4 Perilaku Icon Sync (5 State) 25

2.2 List / Table View 26

2.2.1 Prinsip Pengorganisasian 26

2.2.2 Elemen List 26

2.3 Navigation & Information Structure 27

2.3.1 Hierarki Halaman (3 Tingkat) 28

2.3.2 Peta Gestur → Efek 28

2.3.3 Pembagian Peran Penanda 28

2.3.4 Struktur Flyout 28

2.3.5 Pola Sidebar (Final: Flat + Trigger Klik) 29

2.3.6 Keselamatan Gestur 29

2.3.7 Global Navigation vs Local Navigation 29

2.3.8 Lima Hal Spesifik Navigasi ERP/BMP 30

2.4 Form & Detail View 31

2.4.1 Prinsip Pengorganisasian 31

2.4.2 Conditional Field vs Progressive Disclosure 33

2.5 Search & Command Palette 33

2.5.1 Dua Mode Pemicu 33

2.5.2 Hasil Pencarian 33

2.5.3 Empty State & Halaman Alat 33

2.6 Notification & Approval Workflow 34

2.6.1 Feed "My Attention" 34

2.6.2 Pola Fork-Join Checker Lintas Divisi 35

2.7 Personalization & Account Model 35

2.7.1 Model Akun, bukan Persona Gabungan 36

2.7.2 Admin vs Business Home 36

2.8 Consistency Patterns 37

2.9 Responsive & Usage Context 38

2.10 Konvensi Bahasa UI Sistem 39

2.11 Tangga Intensitas Overlay (5 Level) 39

2.12 Kamus 12 Aturan BMP (dari Studi Kasus ERPNext v16) 40

2.13 Pola Keyboard, Touch & A11y 40

2.13.1 Peta Keyboard Global 40

2.13.2 Touch, Focus & Guard 41

2.14 Studi Kasus: Anti-Pattern ERPNext v16 41

Temuan #1 — Search Modal Gelap 41

Temuan #2 — Load More di List Bisnis 41

Temuan #3 — Sidebar Campur Domain 41

Temuan #4 — Launcher Menyamakan Kategori 41

Temuan #5 — Breadcrumb Tidak Konsisten 41

Temuan #6 — Slot Primary untuk Navigasi 41

Temuan #7 — Halaman Alat Kosong Mati 41

Temuan #8 — Ornamen Konsumer di List 42

Temuan #9 — Label Navigasi Truncate 42

Temuan #10 — Flag Teknis di List Bisnis 42

Temuan #11 — Suffix Company 42

Temuan #12 — Dropdown Campur Jenis 42

Catatan Implementasi Teknis Chapter 2 42

Log Keputusan Final (dari 3 Konflik Hasil Penggabungan) 43

Chapter 3: Architecture 44

3.0 Architecture Invariants 44

3.1 Fondasi: Phoenix — Ash — Event-driven 45

3.2 Async & Data Processing 45

3.3 Architecture Mapping untuk UI/UX System 45

3.4 Performance Budget & Caching Strategy 46

3.5 Cloak & Pemisahan Tanggung Jawab IT 47

3.6 Data Layer — Full SQLite 47

3.6.1 Search Engine: FTS5 (Full-Text Search 5) 47

3.6.2 Pagination: Offset/Limit Optimization 48

3.7 Audit, Retensi & Kepatuhan (final) 48

3.8 Ledger Khusus 48

3.9 Cache & State 48

3.10 Real-time Event Architecture untuk UI 49

3.11 Observability, I18n, Storage 49

3.12 Asset Pipeline & Offline Bundle 50

3.13 Testing & Static Analysis 50

3.14 Deployment Profiles 51

Peran node di Profile 2 51

3.15 Hierarki Pemilihan Dependency (kebijakan resmi) 52

Addendum: Mekanisme Sync-Agent — Penjelasan Detail 53

1. Gambaran Besar 53

2. Komponen Utama 53

2.1 Session Manager 53

2.2 Flush Scheduler 53

2.3 Transport Layer 53

2.4 Apply Engine 54

2.5 State Manager 54

2.6 Telemetry Publisher 54

3. Alur Lengkap: Laptop ke Server Kantor 54

Fase 1 — Capture 54

Fase 2 — Flush 54

Fase 3 — Transport 54

Fase 4 — Apply di Server 54

Fase 5 — Konfirmasi & State Update 55

Fase 6 — Sync Balik (Server → Laptop) 55

4. Conflict Resolution 55

5. Failure Mode & Recovery 55

6. Catatan Penting Operasional 55

Penguatan (v2.0) — konteks offline-first UI: 56

Anchor ID — Deteksi Konflik Non-LWW (v2.0): 56

Chapter 4: Data Model & Table Specifications 57

4.1. Master Data 57

4.1.1 companies 57

4.1.2 accounts (Chart of Accounts) 58

4.1.3 cost_centers 58

4.1.4 fiscal_years 59

4.1.5 accounting_periods 59

4.1.6 items (Master Produk) 60

4.1.7 item_groups 60

4.1.8 customers 61

4.1.9 customer_groups 61

4.1.10 suppliers 61

4.1.11 supplier_groups 62

4.1.12 warehouses 62

4.1.13 uoms (Unit of Measure) 63

4.1.14 uom_conversions 63

4.1.15 price_lists 63

4.1.16 item_prices 63

4.1.17 sales_persons 64

4.1.18 numbering_series ➕ v1.6 (BARU) 64

4.1.19 departments ➕ v1.7 (BARU) 64

4.2 Inventory & Stock 65

4.2.1 batches 65

4.2.2 stock_entries 65

4.2.3 stock_entry_items 66

4.2.4 stock_ledger_entries (SLE) — Immutable 66

4.2.5 bins (Agregasi Stok) 67

4.2.6 stock_reservations ➕ v1.6 (BARU) 67

4.3 Purchase 68

4.3.1 purchase_orders 68

4.3.2 purchase_order_items 68

4.3.3 purchase_receipts 69

4.3.4 purchase_receipt_items 69

4.3.5 purchase_invoices 69

4.3.6 purchase_invoice_items 70

4.3.7 material_requests 70

4.3.8 landed_cost_vouchers 70

4.3.9 landed_cost_voucher_items 71

4.4 Manufacturing 71

4.4.1 boms (Bill of Materials) 71

4.4.2 bom_items 72

4.4.3 operations 72

4.4.4 work_orders 72

4.4.5 work_order_operations 73

4.4.6 production_plans 73

4.4.7 production_plan_items 73

4.5 Sales 74

4.5.1 Core Sales Tables 74

4.5.1.1 sales_orders 74

4.5.1.2 sales_order_items 75

4.5.1.3 delivery_notes 75

4.5.1.4 delivery_note_items 75

4.5.1.5 sales_invoices 76

4.5.1.6 sales_invoice_items 76

4.5.2 Fee Components & Configuration 77

5.2.1 customer_fee_components (Master Jenis Biaya) 77

5.2.2 customer_fee_configs (Konfigurasi Biaya per Customer) 77

4.5.3 Fee Snapshots 78

4.5.3.1 sales_order_fees 78

4.6 Accounting 78

4.6.1 Core Accounting 78

4.6.1.1 journal_entries 78

4.6.1.2 journal_entry_items 79

4.6.1.3 gl_entries (General Ledger) — Immutable 79

4.6.1.4 payment_ledger_entries 79

4.6.1.5 payment_terms_templates — TABEL BARU (Hide) 80

4.6.1.6 payment_schedules — TABEL BARU (Hide) 80

4.6.2 Payment & Banking 80

4.6.2.1 modes_of_payment — TABEL BARU 80

4.6.2.2 payment_entries 80

4.6.2.3 payment_entry_deductions — TABEL BARU 81

4.6.2.3a payment_entry_references — BARU 81

4.6.2.4 banks — TABEL BARU (Hide) 82

6.2.5 bank_accounts — TABEL BARU (Hide) 82

4.6.2.6 bank_transactions — TABEL BARU (Hide) 82

4.6.3 Tax & Withholding 83

4.6.3.1 taxes_templates (Sales & Purchase) 83

4.6.3.2 tax_template_items 83

4.6.3.3 tax_withholding_categories 83

4.6.3.4 tax_withholding_entries — TABEL BARU 83

4.6.4 Budget & Period 84

4.6.4.1 budgets — TABEL BARU (Hide) 84

6.4.2 budget_items — TABEL BARU (Hide) 84

6.4.3 monthly_distributions — TABEL BARU (Hide) 84

4.7 Quality 85

4.7.1 quality_inspections 85

4.7.2 quality_templates 85

4.7.3 non_conformances 85

4.8 Asset 86

4.8.1 asset_categories 86

4.8.2 assets 86

4.8.3 asset_depreciation_schedules 87

4.8.4 asset_movements 87

4.8.5 asset_repairs 87

4.8.6 asset_maintenances 88

4.8.7 asset_maintenance_logs 88

4.9 HR (Minimalis) 89

4.9.1 employees 89

4.9.2 attendances 89

4.9.3 leave_types 90

4.9.4 leave_allocations 90

4.9.5 leave_applications 90

4.9.6 salary_structures 90

4.9.7 payroll_entries 91

4.9.8 salary_slips 91

4.10 System & Framework 92

4.10.1 users 92

4.10.2 permissions 92

4.10.3 access_logs 92

4.10.4 modules ➕ v1.6 (BARU) 93

4.10.5 workspaces ➕ v1.6 (BARU) 93

4.10.6 settings ➕ v1.6 (BARU) 93

4.10.7 schema_metadata ➕ v1.6 (BARU) 94

4.10.8 roles ➕ v1.7 (BARU) 94

4.10.9 user_roles ➕ v1.7 (BARU) 94

4.11 Relasi Antar Tabel (Tree Structure) 95

4.12 Cloak Registry (Keamanan Tiga Lapis) 97

4.12.1 Grup Cloak 97

4.12.2 Aturan Penegakan (Konvensi Implementasi) 97

4.13 Catatan Implementasi 98

Changelog v1.7 (vs v1.6) 99

Penambahan Tabel Baru 99

Perubahan Tipe Data / Refactor Struktural 100

Catatan 100

Changelog v1.6 (vs v1.5) 101

Penambahan Tabel Baru 101

Penambahan Kolom Baru 101

Perubahan Tipe Data 101

Perubahan Konvensi 101

Penambahan Akun Beban 101

Penghapusan 101

Chapter 5: Development Conventions 102

Catatan Merge (K + O + Q) 102

5.0 Development Invariants 103

5.1 Bahasa & Naming Convention 103

5.1.1 Elixir & Ash Naming 104

5.1.2 UI Component Naming (LiveView / HEEx) 104

5.1.3 File Naming 104

5.2 Project Structure & Folder Convention 105

5.3 Domain Ownership 106

5.4 Resource Convention 107

5.5 Action Convention 107

5.5.1 Generic CRUD 107

5.6 Public Action vs Internal Implementation 108

5.7 Event Convention 108

5.7.1 Event Payload 108

5.8 Anchor ID 108

5.9 State Machine Convention 109

5.10 Database Convention 109

5.11 UUID Convention 109

5.12 Money & Numeric Precision 109

5.13 Tree Data Convention 110

5.14 Archive vs Delete 110

5.15 Async Job Convention 110

5.16 Reactor Convention 110

5.17 Sync-Agent Boundary 110

5.18 Logging & Sensitive Data 110

5.19 LiveView Convention 111

5.20 UI State vs Application Data 111

5.21 Query Convention 111

5.22 Performance Convention 112

5.23 Testing Strategy 112

5.24 Testing Libraries 113

5.25 Property-Based Testing 113

5.26 Mocking Convention 113

5.27 Static Analysis 114

5.28 Documentation Convention 114

5.29 Error Convention 115

5.30 Transaction Convention 115

5.31 Migration Convention 115

5.32 SQLite Operational Convention 115

5.33 Import Convention 116

5.34 Numbering Convention 116

5.35 Release & Versioning 116

5.35.1 Semantic Versioning 116

5.35.2 Changelog Convention 116

5.35.3 Release Packaging & Asset Fingerprinting 116

5.36 Git Convention 117

5.37 CI Pipeline 117

5.38 Definition of Done 117

5.39 Anti-Patterns 118

5.40 Development Decision Hierarchy 118

5.41 Dependency Rule 118

5.42 Laporan Fiskal & Template Output Bahasa Indonesia 119

5.43 Final Principle 122

Introduction

Dokumen ini adalah blueprint proyek BMP (Business Management Platform) secara utuh — menggabungkan seluruh keputusan desain dan arsitektur yang telah disepakati ke dalam satu struktur dokumentasi resmi, menggantikan catatan sesi yang sebelumnya tersebar di beberapa file terpisah.

Peta Keseluruhan & Status

Seluruh cakupan proyek dipetakan ke dalam empat kelompok besar berikut, masing-masing dengan status pengerjaan saat dokumen ini disusun:

Cluster Item Status
UI Design Foundation (Theme, Color System, Typography, Iconography) Selesai — Chapter 1
UI Interaction Structure (Layout, Nav, Dashboard, Components, Workflow) Selesai — Chapter 2
UI System UI/UX (Module toggle, Permission, Role/Account, Workspace, Settings) Belum — Chapter 6
Architecture Phoenix, Ash, event-driven, sync-agent, deployment profile Selesai — Chapter 3
Data Model Skema database & tabel lengkap untuk Ash Framework (97 tabel) Selesai — Chapter 4
Development Coding Standard, Naming, Folder Convention, Generator, Testing, CI/CD, Extension SDK, Migration, Versioning Belum — Chapter 5
Modul ERP Administration, System & Utilities, Accounting, Inventory, Purchase, Sales, Manufacturing, HR, Asset Mgmt, Quality, Project Mgmt (Core) Belum — Chapter 8
Modul ERP POS, E-commerce, Website, Helpdesk (Official); Fleet, Rental, Healthcare, Agriculture, Hospitality, dll. (Community, togglable) Belum — Chapter 9
Umum Report & Workflow lintas modul Belum — Chapter 7

Dua “Architecture” yang Berbeda Level

Penting dipisahkan agar tidak rancu: “System UI/UX” (Chapter 6) dan “Architecture” (Chapter 3) sama-sama menyentuh topik Module/Permission/Role/Workspace, tapi dari sudut pandang berbeda.

Chapter Sudut pandang Contoh
5. System UI/UX Bagaimana konsep ini terlihat & terasa oleh user/admin Halaman kelola Role, toggle modul di UI, tampilan Workspace switcher
3. Architecture Bagaimana itu benar-benar berjalan di level infrastruktur Ash.Policy.Authorizer menegakkan Permission, Reactor menjalankan Workflow

Urutan Pengerjaan

Urutan bab dalam dokumen ini disusun berdasarkan ketergantungan — apa yang harus mapan dulu sebelum yang lain bisa dibangun konsisten di atasnya, bukan sekadar urutan sembarang:

  1. Design Foundation — token visual (warna, tipografi, ikonografi) yang jadi dasar semua komponen bersama; ditunda akan berarti banyak yang harus diubah retroaktif.
  2. Interaction Structure — pola perilaku layar (layout, navigasi, list, form, report, workflow) yang dibangun di atas Design Foundation.
  3. Architecture — pondasi teknis (Phoenix, Ash, event-driven, sync-agent, deployment) sebelum konvensi kode ditulis.
  4. Data_Model_&_Table_Specifications — skema database lengkap untuk implementasi
    Ash Framework
  5. Development Conventions — bergantung pada keputusan Architecture di atas.
  6. System UI/UX — formalisasi Module/Permission/Role/Workspace/Settings sebagai spesifikasi resmi, karena modul-modul di bawah akan mengacu ke sini.
  7. Report & Workflow — struktur lintas modul yang dipakai oleh seluruh Modul ERP.
  8. Modul ERP Core — memakai seluruh fondasi di atas.
  9. Modul Optional/Community — paling akhir, dapat didorong ke kontributor komunitas.

Latar Belakang — Analisis 12 ERP Open Source

Sebelum menyusun prinsip desain BMP, dilakukan analisis terhadap 12 aplikasi ERP open source sebagai pembanding: ERPNext, Dolibarr, Apache OFBiz, metasfresh, iDempiere, Tryton, OpenPetra, Axelor, inoERP, LedgerSMB, BlueSeer, dan WebERP.

Kesimpulan utama: hampir semua ERP tersebut membangun UI dengan mengikuti struktur backend (skema database, metadata resource), bukan mengikuti alur kerja/pengalaman user. Pola kesalahan yang berulang:

Prinsip inti yang menjadi fondasi seluruh dokumen ini:

UI yang nyaman selalu menjawab “apa yang sedang ingin dicapai user saat ini” — bukan “bagaimana data ini disimpan di backend”.

BMP (Business Management Platform) secara sadar dirancang untuk UMKM yang sedang berkembang menuju skala menengah — bukan enterprise. Konsekuensinya: default harus cukup sederhana untuk dipakai tanpa training/konsultan, approval bertingkat bersifat opsional (bukan default wajib), dan satu orang boleh memegang lebih dari satu akun fungsi sesuai kebutuhan bisnis kecil yang masih hemat sumber daya.

Chapter 1: Design Foundation

Bab ini mendefinisikan fondasi visual, tipografi, filosofi ukur, dan prinsip meta yang menjadi landasan mutlak bagi seluruh antarmuka BMP. Setiap komponen UI yang dibangun di Chapter 2 (Interaction Structure) dan Chapter 6 (System UI/UX) harus mematuhi token dan batasan yang ditetapkan di sini — makin lama ditunda, makin banyak yang harus diubah retroaktif.

Status: 🔒 Final / Mengikat (v2.0) Dasar: Spec Navigasi & Landing (Mockup Final v9) · UI Reference Prinsip v1 Tujuan: Menyatukan bahasa visual, mencegah anti-pattern konsumer, dan mengunci dimensi spasial antarmuka.

Changelog v2.0:

1.1 Theme

Tema visual BMP adalah Operational Minimalist. Arah visualnya profesional tapi tidak mengintimidasi — berbeda dari kesan “software korporat kaku” ala ERP enterprise yang membuat pemilik UMKM merasa perlu training dulu sebelum berani memakainya. Berbeda juga dari aplikasi konsumer yang memprioritaskan engagement dan infinite scrolling — BMP dirancang memaksimalkan Konteks Operasional dan Kejelasan Data.

1.2 Color System

Token warna bersifat semantik (berdasarkan fungsi), bukan sekadar warna brand. Satu token = satu makna, berlaku konsisten di seluruh komponen. Aksen dijaga sangat langka supaya fokus pengguna tetap di data operasional.

Token Fungsi Nilai
--color-accent Aksi utama, elemen aktif/terpilih #16A34A (hijau)
--color-accent-bg Latar lembut untuk elemen accent #F0FDF4
--color-success / --bg-success Status positif (disetujui, berhasil) #15803D / #F0FDF4
--color-warning / --bg-warning Butuh perhatian, belum kritis #B45309 / #FFFBEB
--color-danger / --bg-danger Kritis, destruktif, overdue #B91C1C / #FEF2F2
--text-primary Teks utama #292524
--text-secondary Teks pendukung #57534E
--text-muted Teks tersier / placeholder #A8A29E
--border Garis pemisah standar #E7E5E4
--surface-1 Latar sekunder (card, panel) #FAFAF9
--surface-2 Latar utama (kanvas) #FFFFFF

Token navigasi (turunan dari surface-1, dipakai khusus di rail & area non-kanvas):

Token Fungsi Nilai
--rail Latar sidebar rail #FAFAF9
--canvas Latar kanvas utama #FAFAF9
--card Latar card di atas kanvas #FFFFFF
--line Garis tipis divider internal, legend divider flyout #E7E5E4
--hover Latar hover baris/navigasi #EDEDEC (sedikit lebih gelap dari surface-1)

Aturan Warna:

  1. Hijau (--color-accent) sangat langka — hanya untuk logo, pager aktif, focus ring, dan CTA absolut. Tidak boleh dipakai untuk teks biasa atau latar kartu; penggunaannya dibatasi supaya mata pengguna langsung tertarik saat warna ini muncul.
  2. Diferensiasi elemen (mis. tombol hover, collapse toggle) dilakukan lewat perbedaan tone (kecerahan) pada permukaan yang sama, bukan dengan memperkenalkan hue baru.
  3. Warna status (amber/merah/hijau) hanya untuk badge status, angka KPI, dan indikator overdue — bukan dekorasi umum.

1.3 Typography

Dua kebutuhan khusus aplikasi bertipe ERP/BMP yang sering diabaikan desain UI generik:

Font terpilih: Inter — open source, mendukung tabular numerals bawaan, teruji untuk UI aplikasi data-berat. Karena aplikasi berjalan offline/LAN-only, font wajib di-bundle lokal (.woff2 di priv/static/fonts/), tidak boleh bergantung pada Google Fonts CDN.

Label & header tabel: wajib UPPERCASE, ukuran kecil (10–11px), warna muted — membedakan secara instan “label sistem” dari “data inputan user”.

Skala Ukuran Pemakaian
text-xs 11px Caption, label kolom tabel (uppercase)
text-sm 13px Body dasar (list, form)
text-base 14px Teks utama form / paragraf
text-lg 16px Judul dokumen / card
text-xl 20px Judul halaman / section
text-2xl 24px Angka KPI besar (dashboard)

Berat font dibatasi 3: Regular (400) untuk body, Medium (500) untuk judul/emphasis, Semibold (600) khusus text-2xl (angka KPI besar butuh bobot lebih tegas) — mencegah developer bebas memakai Bold/Black secara tidak konsisten.

1.4 Iconography

Satu sumber ikon untuk seluruh aplikasi, bukan campuran beberapa icon set.

— widget: "design_foundation_styleguide"
Lembar acuan visual: swatch warna, contoh type scale dengan tabular numerals, dan sampel ikon dua ukuran.

1.5 Shadow & Elevation System (baru)

Elevation di BMP bukan dekorasi — ia mengkomunikasikan kedalaman interaksi dan intensitas interupsi. Sistem ini menjadi fondasi Tangga Intensitas Overlay di Chapter 2.11.

Level Token CSS Shadow Pemakaian
0 --shadow-0 none Flat surface, card biasa di kanvas
1 --shadow-1 0 1px 2px rgba(0,0,0,.05) Hover state baris tabel, dropdown sederhana
2 --shadow-2 0 4px 6px -1px rgba(0,0,0,.05), 0 2px 4px -1px rgba(0,0,0,.03) Flyout menu, anchored search result, popover
3 --shadow-3 0 10px 15px -3px rgba(0,0,0,.08), 0 4px 6px -2px rgba(0,0,0,.04) Slide-over drawer, side panel detail
4 --shadow-4 0 20px 25px -5px rgba(0,0,0,.1), 0 10px 10px -5px rgba(0,0,0,.04) Modal + backdrop gelap (level 5)

Aturan Elevation:

  1. Elevation naik = interupsi naik. Level 4 hanya untuk modal keputusan destruktif/bloking — jangan pakai untuk flyout atau dropdown.
  2. Backdrop gelap hanya menyertai level 4. Level 1–3 tidak menggunakan backdrop gelap — elevation cukup mengkomunikasikan kedalaman.
  3. Semua shadow pakai rgba(0,0,0,...) — tidak ada shadow berwarna.

1.6 Spacing, Radius & Grid (baru)

BMP mengikuti filosofi “chrome fix, kanvas fluid”, terinspirasi efisiensi ruang kerja profesional: semua ketebalan bingkai (chrome — sidebar, header, baris, tombol) berukuran tetap; yang cair (fluid) hanya sisa ruang kanvas.

Aturan mutlak:

Tabel Dimensi Kunci (terkunci — terikat Spec v9, tidak boleh diubah tanpa revisi Blueprint):

Kategori Elemen Dimensi Sifat
Chrome Fix Rail expanded 224px Tetap
Chrome Fix Rail collapsed 48px Tetap
Chrome Fix Page-head 48px Tetap
Chrome Fix Brand logo expanded 36px Tetap
Chrome Fix Brand logo collapsed 30px Tetap
Chrome Fix Search input 34px tinggi Tetap
Chrome Fix Row modul rail ≈28px Padding 6px, gap 0
Chrome Fix Chevron Visual 14px Hit area ≥28×28px
Chrome Fix Handle collapse 24px Bottom ≈36px dari bawah rail
Chrome Fix Avatar user 26px Tetap
Chrome Fix Tombol sekunder header 30px tinggi Tetap
Chrome Fix Content padding 16px Semua sisi kanvas
Chrome Fix KPI value 24px Tabular numerals
Chrome Fix KPI label 11px Uppercase, muted
Chrome Fix Baris tabel 44px (comfortable) / 36px (compact) Pilih salah satu per user
Chrome Fix Pager circle 28px Tetap
Chrome Fix Flyout min-width 224px Tetap
Fluid Lebar content area calc(100% - rail) Mengikuti sisa ruang
Fluid-isi Tinggi dropdown/flyout max-height: calc(100vh - 16px) Mengikuti viewport + scroll internal

Radius

Token Nilai Pemakaian
--radius-sm 6px Input, badge, tombol kecil
--radius-md 8px Card, panel, dropdown, flyout
--radius-lg 10px Logo brand, avatar besar

Spacing Scale

Token Nilai Pemakaian
--space-1 4px Gap ikon-teks, padding mikro
--space-2 8px Gap item kecil, padding tombol
--space-3 12px Gap komponen medium
--space-4 16px Content padding, gap card
--space-5 24px Section gap, margin blok
--space-6 32px Jarak antar section besar

Aturan Grid: tidak ada 12-column grid yang ketat — layout BMP berbasis panel dan zona (rail, head, kanvas), bukan grid kolom.

Density & Personalisasi: dua tingkat kepadatan didukung tanpa merusak tata letak — Comfortable (44px), baris/input lebih renggang, cocok layar sentuh atau keterbacaan santai; Compact (36px), baris lebih rapat, cocok power-user yang butuh lebih banyak baris data per layar. Pilihan density, status rail (expanded/collapsed), dan pin landing page disimpan di level user session (bukan global).

1.7 Contoh Penerapan

Dua contoh berikut menunjukkan token di atas diterapkan pada tampilan halaman web yang mendekati bentuk final (bukan wireframe kotak-kotak).

Contoh 1 — Home Dashboard

— widget: "bmp_home_high_fidelity"
Halaman Home dengan sidebar ikon, search bar, feed My Attention, dan 3 KPI card dengan tabular numerals.

Contoh 2 — List View

— widget: "bmp_list_view_high_fidelity"
Tabel Sales Order dengan tab saved-view, badge status berwarna, dan kolom nilai rata kanan tabular numerals.

1.8 Tiga Prinsip Meta Visual

Tiga prinsip ini menjadi batu uji setiap keputusan visual di BMP — menjelaskan mengapa token dan komponen di atas dirancang begitu, dan wajib dipegang tim UI/UX & Developer saat merancang komponen baru. BMP secara sadar menolak pola interaksi aplikasi konsumer (media sosial, e-commerce B2C) yang dirancang untuk retensi adiktif.

  1. Proporsionalitas Interupsi — intensitas overlay (Chapter 1.5, 2.11) harus sebanding dengan pentingnya peristiwa. Jangan pernah pakai modal gelap (level 4/5) untuk hasil pencarian (level 2) atau form cepat. Implikasi: Shadow level 4 hanya untuk modal. Search anchored level 2 tanpa gelap. Tombol danger hanya untuk aksi destruktif.
  2. Konteks Operasional — data bisnis adalah data operasional, bukan konten konsumsi. User butuh total konteks, posisi data, dan batasan waktu — bukan infinite feed. Pager tradisional lebih dihargai daripada Load More untuk data master/transaksi (detail di 2.2). Implikasi: Typography dioptimalkan untuk ukuran kecil dan tabular numerals. Spacing cukup rapat untuk kepadatan data. Chrome fix agar konteks tidak bergeser.
  3. Kemurnian Konteks (Bounded UI) — setiap elemen UI mencerminkan satu kepemilikan arsitektural. Jangan campur konsep Platform, Master Data, dan Policy/Settings di satu permukaan yang sama. Implikasi: Rail sidebar murni milik domain (Strict UI Ownership). Settings punya hub tersendiri (6.4). Zona settings di rail hanya terlihat role admin.

Chapter 2: Interaction Structure

Status: 🔒 Final / Mengikat (v2.0)

Dasar: Spec Navigasi & Landing (Mockup Final v9) · UI Reference v1 · Temuan UX (Studi Kasus ERPNext v16) · mockup visual (draf C)

Tujuan: Mengunci perilaku spasial, hierarki navigasi 3 tingkat, dan mencegah anti-pattern interupsi yang merusak flow operasional.

Bagian ini mendefinisikan bagaimana tiap jenis layar disusun secara konsisten di seluruh modul BMP — mengikuti prinsip inti: alur kerja user, bukan struktur backend. Semua pola di bawah dibangun di atas token visual Chapter 1.

Catatan penggabungan (editorial, baca sebelum lanjut): dokumen ini menggabungkan tiga draf — K (paling lengkap: 2.1–2.14 + studi kasus penuh), Q (revisi "final/mengikat" yang lebih ringkas, 2.1–2.8), dan C (draf visual dengan 18 referensi mockup/widget bergambar, disematkan langsung di bagian terkait). Isinya sebagian besar identik lintas ketiganya; struktur K dipakai sebagai kerangka utama karena paling lengkap dan paling dekat dengan apa yang sudah "terkunci" di Blueprint master. Tiga konflik sempat ditemukan (footer, trigger flyout, jumlah level tangga overlay) — ketiganya sudah diputuskan final oleh user dan diterapkan langsung di badan dokumen ini; log keputusannya ada di penutup dokumen.

2.1 Landing Page & Layout

Masalah pada ERP pembanding: landing page berupa "app launcher" (grid ikon besar) menyisakan ruang kosong besar saat modul sedikit; semua ikon modul diberi bobot visual sama padahal frekuensi pemakaian berbeda jauh; tidak menjawab "apa yang perlu dikerjakan hari ini"; kolom mengikuti urutan tabel database, bukan keputusan bisnis.

Prinsip pengorganisasian (C):

Arsitektur halaman BMP (Zona):

Zona Ukuran Sifat Isi
Rail sidebar 210px expanded / 48px collapsed Fix Navigasi global, search, brand
Page-head 44px tinggi, lebar fluid Fix Breadcrumb (kiri), home + icon sync (kanan)
Kanvas Sisa ruang Fluid Konten halaman operasional

Tanpa footer di halaman manapun (final — lihat log keputusan di penutup dokumen). Status sinkronisasi data (relevan karena BMP local-first dengan sync-agent) tidak hilang fungsinya — dipindah jadi icon sync di header kanan (5 state + popover detail, lihat §2.1.4), sementara identitas aplikasi ("Powered by Phoenix · Ash") sudah punya tempat di baris akun bawah rail (§2.1.1 poin 7). Tanpa greeting. Tanpa launcher grid. Rail + flyout + Ctrl+K menutup long tail navigasi. Tanpa title-block di content — identitas halaman hanya ada di breadcrumb; mengulang judul (H1) di kanvas adalah redundansi visual.

2.1.1 Rail Sidebar (atas → bawah)

# Komponen Spec
1 Brand bar Logo 36px (radius 10) + teks "BMP". Klik brand = home (Landing My Attention).
2 Search Input 34px tinggi, dropdown anchored di bawah input, tanpa backdrop gelap. Mode collapsed: tombol bulat 30px; klik = restore rail + fokus input. Ctrl+K dari mana pun.
3 9 modul bisnis Accounting, Sales, Purchase, Manufacturing, Inventory, HR, Asset, Quality, Project — ikon 16px + label + chevron. Klik label = navigasi ke Workspace modul; klik chevron = flyout (dua zona klik, dua arti).
4 Divider tipis Pemisah zona bisnis ↔ zona settings.
5 3 modul settings System & Utility (platform) · Administration (policy bisnis) · Organization (master struktur). Role-gated: hanya role admin melihat zona ini.
6 Handle collapse Lingkaran 24px di seam (tepi kanan rail); hover-reveal (always-visible di touch); klik = toggle 210↔48; chevron rotasi 180° saat collapsed.
7 Powered by Teks "Powered by Phoenix · Ash" 10px muted center, rapat edge bawah. Akses akun hanya via avatar di header.

2.1.2 Header (page-head 44px)

2.1.3 Isi Landing (My Attention)

Landing page menjawab "apa yang perlu dikerjakan hari ini", bukan "di mana letak modulnya".

📎 hasil konsolidasi landing page (feed My Attention, KPI ringkas, dokumen terakhir), dibanding versi awal yang terlalu ramai.

2.1.4 Perilaku Icon Sync (5 State)

Icon sync di header kanan bukan sekadar tombol refresh generik — ini satu-satunya sinyal di UI bahwa BMP bersifat local-first (data bisa "beda" sesaat antar laptop sebelum tersinkron), jadi harus membawa informasi state yang nyata, bukan cuma on/off.

State Kapan Terjadi Warna Animasi Badge
Tersinkron Normal, semua data sudah terkirim neutral-500 (netral) Diam Tidak ada
Sedang sync Proses aktif kirim/terima data primary-600 (hijau) Muter (spin) Tidak ada
Menunggu sync Ada perubahan lokal belum terkirim (jeda antar siklus sync, bukan error) warning (amber) Diam Angka kecil, mis. "3"
Offline Sync-agent putus dari server neutral-400 (muted) Diam — tidak muter, karena memang tidak sedang mencoba sync Tidak ada
Konflik anchor_id bentrok, butuh review manual (Chapter 3, Architecture Invariant #11) danger (merah) Diam Titik merah

Offline dan Konflik sengaja dipisah, tidak digabung jadi satu warna merah yang sama — keduanya beda secara fundamental: offline artinya "tidak apa-apa, nanti otomatis lanjut begitu koneksi kembali"; konflik artinya "ada yang perlu manusia putuskan sekarang". Kalau disamakan, user tidak bisa membedakan mana yang harus segera ditindaklanjuti.

Interaksi klik: membuka popover kecil anchored di bawah icon — pola yang sama persis dengan dropdown avatar/search (Level 2 di tangga overlay §2.11, tanpa backdrop gelap), bukan cuma memicu animasi tanpa isi. Isi popover:

Popover ini adalah satu-satunya jalan masuk dari UI ke mekanisme anchor_id (deteksi konflik non-LWW, Chapter 3 §Sync-Agent Addendum). Tanpa jalan masuk ini, fitur deteksi-konflik di backend ada tapi tidak kepakai secara praktis oleh user — sync icon jadi jembatan yang menghubungkan arsitektur sync ke lapisan interaksi.

2.2 List / Table View

Masalah pada ERP pembanding: kolom mengikuti urutan tabel database; status hanya teks polos tanpa sinyal visual; filter tersembunyi di panel terpisah; bulk action sulit ditemukan; "Load More" merusak spatial memory; ornamen konsumer (like/komentar) menambah noise; flag teknis bocor ke list bisnis.

2.2.1 Prinsip Pengorganisasian

2.2.2 Elemen List

Elemen Aturan
Filter pill Dismissible (tombol X), text-sm, --surface-1 background — memberi visibility filter aktif tanpa memakan ruang sidebar.
Aksi baris Hover-reveal (View/Process) — muncul dengan differensiasi minimal (tone+border+shadow), menegaskan clickability hanya pada hover.
Angka Rata kanan, tabular numerals.
Ornamen sosial ❌ tidak ada — tidak ada like, komentar, atau hati di list operasional. Kolaborasi hidup di timeline dokumen.
Flag teknis ❌ tidak ada — tidak ada kolom "Is Group" atau checkbox tree di list bisnis. Mekanisme tree diekspresikan lewat indentasi/expand.
Suffix metadata ❌ tidak ada — multi-company diekspresikan lewat badge/kolom/filter company global, tidak pernah suffix nama.

📎 List view Sales Order versi high-fidelity, dapat dipakai ulang sebagai referensi visual bagian ini.

2.3 Navigation & Information Structure

Koreksi konsep penting: navigasi bukan flowchart (satu jalur linear). Model yang tepat adalah peta kedalaman dengan panah dua arah di tiap level, ditambah jalur pintas (search) yang dapat melompati level tengah.

📎 diagram lima level kedalaman navigasi (Home → Workspace → List → Detail → Sub-record) dengan panah dua arah dan jalur pintas via search.

2.3.1 Hierarki Halaman (3 Tingkat)

Tingkat Halaman Cara Masuk Breadcrumb Baris Aktif Rail
0 — Landing My Attention Klik logo Home / My Attention Tidak ada
1 — Workspace Overview per modul (12: 9 bisnis + 3 settings) Klik label modul, atau item flyout "Overview" Home / Accounting Modul tsb
2 — Sub-halaman List / report / form (Sales Invoice, Stock Balance, dll.) Klik item flyout Home / Accounting / Sales Invoice Modul tsb

Halaman utilitas (My Profile, Preferences) diperlakukan seperti landing: di luar workspace, tanpa baris aktif.

2.3.2 Peta Gestur → Efek

Gestur Efek Navigasi?
Klik logo ke My Attention
Klik label/baris modul ke Workspace modul; baris aktif; flyout menutup
Klik chevron baris flyout buka/tutup; halaman & state aktif tidak berubah
Klik item flyout navigasi; baris aktif; flyout menutup
Klik luar / Esc flyout menutup
Klik handle seam rail collapse/expand
Ctrl+K search
Mode collapsed: klik ikon membuka flyout (item pertama = Overview)

2.3.3 Pembagian Peran Penanda

2.3.4 Struktur Flyout

Aturan produksi flyout: click-to-pin, tutup pada klik-luar/Esc; hanya satu flyout terbuka (membuka satu = lain menutup otomatis); clamp viewport (flyout yang melewati batas bawah bergeser naik otomatis).

2.3.5 Pola Sidebar (Final: Flat + Trigger Klik)

(Prinsip "sidebar flat" berasal dari draf C, tidak ada di K/Q.) Tree bersarang (expand/collapse permanen) tidak dipakai karena mencerminkan struktur database, bukan frekuensi pemakaian. Sidebar flat (1 level) adalah baseline — ini sudah otomatis konsisten dengan struktur rail K/Q yang memang flat sejak awal.

Keputusan final: trigger flyout tetap KLIK (chevron), bukan hover. Alasan utama bukan cuma keamanan gestur (2.3.6), tapi BMP eksplisit mendukung mode Tablet touch-first untuk staf gudang/kasir (§2.9) — hover tidak eksis di touchscreen, jadi trigger hover akan berperilaku beda per device dan melanggar konsistensi (§2.8). Usulan hover dari draf C ditolak; bagian flat-sidebar-nya diserap karena memang sudah sejalan dengan rail yang ada.

📎 ilustrasi dari draf C: perbandingan sidebar flat dengan flyout munculan saat hover. Konsep flat-nya dipakai; trigger hover-nya ditolak (lihat catatan final di atas).

2.3.6 Keselamatan Gestur

2.3.7 Global Navigation vs Local Navigation

Global Navigation Local Navigation
Definisi Tidak berubah di manapun user berada Berubah total tergantung level/konteks saat ini
Isi App switcher, search universal, notifikasi, identitas akun, shortcut Home Breadcrumb, tab dalam record, related-list/link
Aturan Isi minim Maksimal 5–6 tab; breadcrumb maksimal 3–4 level

📎 kombinasi rak ikon (global nav) identik di semua halaman dengan breadcrumb + tab (local nav) yang berubah sesuai konteks.

2.3.8 Lima Hal Spesifik Navigasi ERP/BMP

📎 rantai dokumen dengan status kini disorot.

📎 Settings Hub sebagai jalur discovery config/master data, dikelompokkan per kategori fungsional.

📎 satu halaman penuh menggabungkan global nav, local nav, status-aware action bar, dan document trail sekaligus.

2.4 Form & Detail View

Masalah pada ERP pembanding: field diurutkan sesuai skema tabel; tabel item/baris sulit dinavigasi keyboard; validasi baru muncul setelah submit; field read-only tidak dibedakan visual.

2.4.1 Prinsip Pengorganisasian

📎 Form Sales Order tersusun Who→What→When/How dengan validasi inline dan field read-only bertanda visual.

📎 Advanced Details terbuka, termasuk contoh conditional field metode pembayaran.

2.4.2 Conditional Field vs Progressive Disclosure

Progressive Disclosure Conditional Field
Siapa kontrol User (klik untuk buka) Sistem (otomatis berdasar nilai field lain)
Kapan muncul Kapan saja user mau lihat Hanya saat kondisi tertentu terpenuhi
Tujuan Menyembunyikan yang jarang dipakai Menyembunyikan yang tidak relevan di konteks itu

Aturan desain conditional field: jangan bikin layout "lompat" kasar; validasi harus ikut kondisi aktif (field tersembunyi tidak boleh dianggap wajib); data lama tidak boleh hilang diam-diam saat kondisi berubah; jangan berantai lebih dari 2 tingkat.

2.5 Search & Command Palette

Masalah pada ERP pembanding: search tercecer per-modul; hasil tidak dikelompokkan; tidak ada "command" (search hanya bisa navigasi); kotak kosong tidak menawarkan apa-apa; klik search membuka modal tengah gelap (level 5) padahal ekspektasi user level 2.

2.5.1 Dua Mode Pemicu

Pemicu Posisi Intensitas
Klik input search Anchored dropdown di bawah input Level 2 (tanpa backdrop gelap)
Ctrl+K Palette terpusat di tengah layar Level 2–4 (dipanggil sengaja)

Mesin pencarian yang sama melayani kedua pemicu — perbedaan hanya posisi dan konteks pemanggilan.

2.5.2 Hasil Pencarian

2.5.3 Empty State & Halaman Alat

📎 command palette dengan hasil terkelompok: Action, Document (dengan badge status), dan Report.

2.6 Notification & Approval Workflow

Masalah pada ERP pembanding: sebagian besar tidak punya sistem notifikasi terpadu; notifikasi terpecah ke banyak sumber; harus buka dokumen penuh untuk approve hal sederhana; tidak ada progres approval bertingkat yang terlihat; approval yang didiamkan lama diperlakukan sama seperti yang baru masuk.

Terminologi resmi alur approval BMP: Submitter → Checker (paralel, lintas divisi) → Approver (final).

2.6.1 Feed "My Attention"

Satu feed terpadu menggabungkan task + notifikasi + alert, diurutkan berdasarkan urgensi — bukan dipisah ke banyak sumber. Item actionable (butuh tindakan user) dipisahkan visual dari info-only.

📎 feed notifikasi terpadu (actionable vs info-only) dan contoh progres approval bertingkat linear (stepper 2 tingkat).

2.6.2 Pola Fork-Join Checker Lintas Divisi

Untuk kasus seperti Purchase Order yang membutuhkan verifikasi dari lebih dari satu divisi (contoh: Manager Production & Manager Accounting) sebelum sampai ke Approver akhir (Direktur/GM), alur tidak lagi linear melainkan fork-join:

Catatan implementasi: pola ini secara alami dipetakan ke Reactor (fan-out/fan-in dengan compensation saat salah satu step gagal) — dibahas lebih lanjut di Chapter 3 Architecture.

📎 diagram fork-join: Purchasing submit → dua Checker paralel (Manager Production, Manager Accounting) → Approver terkunci sampai kedua Checker selesai.

2.7 Personalization & Account Model

Tiga lapisan yang dipisah tegas:

📎 role default per fungsi (Sales/Warehouse/Finance) sebagai template admin, dengan contoh override personal dan opsi reset ke default.

2.7.1 Model Akun, bukan Persona Gabungan

Keputusan desain BMP: satu orang dapat memiliki lebih dari satu akun terpisah (contoh: budi.kasir dan budi.pemilik), bukan satu akun dengan banyak peran digabung otomatis. Personalisasi dan permission melekat ke akun, bukan ke individu fisiknya.

📎 daftar akun tersimpan sesinya, badge notifikasi per akun tanpa menggabungkan datanya.

2.7.2 Admin vs Business Home

Akun IT/Sysadmin murni (tanpa keterlibatan transaksi bisnis) mendapat home berupa System Console (status job, backup, permintaan akses user) yang berbeda kategori sepenuhnya dari home operasional bisnis — bukan sekadar varian KPI dari template yang sama.

📎 perbandingan Home akun bisnis (Sales) dengan Home akun IT Admin (System Console), dua kategori informasi berbeda total.

2.8 Consistency Patterns

Kamus perilaku — satu pola untuk satu situasi, dipakai persis sama di seluruh aplikasi, dipaksakan lewat komponen bersama (bukan sekadar dokumentasi yang harus diingat manual).

Situasi Pola BMP
Hierarki tombol Satu warna = satu makna (primary/secondary/danger), berlaku di semua layar
Toast / feedback Posisi tetap, hilang otomatis
Konfirmasi destruktif Selalu modal level 5, selalu menjelaskan konsekuensi konkret — bukan "Apakah Anda yakin?" generik
Empty state Selalu punya CTA yang mengarahkan ke aksi berikutnya
Loading state Skeleton yang meniru bentuk akhir, bukan spinner generik

Cara menjaga konsistensi secara teknis: shared Phoenix function components (<.modal>, <.toast>, <.button variant="...">, <.empty_state>) dengan attr/slot untuk kontrak yang dicek saat compile; katalog visual komponen (PhoenixStorybook); test khusus untuk komponen bersama karena dampak perubahannya luas.

📎 lembar acuan pola interaksi: hierarki tombol, toast, modal konfirmasi destruktif, empty state, loading skeleton.

2.9 Responsive & Usage Context

Untuk konteks kantor lokal (LAN, offline), isu klasik field usage (sinyal putus-putus, offline sync) tidak relevan. Yang tetap relevan: kemungkinan ada titik pemakaian fisik berbeda dalam satu lokasi — bukan device berbeda karena di luar kantor, tapi konteks fisik berbeda dalam satu gedung/toko.

📎 perbandingan prioritas informasi: tabel padat untuk Desktop staf akuntansi vs target sentuh besar untuk Tablet staf gudang.

2.10 Konvensi Bahasa UI Sistem

Konsep Label UI (English)
Feed perhatian utama di Home My Attention
Saved view — dokumen milik user Assigned to Me
Saved view — menunggu approval Pending Approval
Riwayat dokumen terakhir dibuka Recent Activity
Bagian field yang di-collapse Advanced Details
Tombol simpan tanpa submit Save Draft
Hub konfigurasi/master data Settings Hub

Catatan kecil: C menyebut baris terakhir sebagai "Settings" saja, K menyebut "Settings Hub" — dipakai "Settings Hub" karena konsisten dengan istilah yang sama di 2.3.4 dan 2.3.8.

2.11 Tangga Intensitas Overlay (5 Level)

Elevation di Chapter 1 adalah apa (shadow). Tangga di sini adalah kapan (aturan pemakaian). Intensitas overlay harus proporsional dengan pentingnya peristiwa. Versi final ini memakai 5 level — selaras dengan Chapter 1 §1.5 (Shadow & Elevation) yang sudah lebih dulu dikunci dengan 5 level shadow mengikuti tangga overlay ini persis; Toast/snackbar masuk ke Level 3, dan Fullscreen jadi pola situasional tanpa nomor level tersendiri (bukan level ke-6).

Level Pola Backdrop Pemakaian BMP
1 Inline change / Tooltip Tanpa Edit cell tabel, toggle switch, penjelasan mikro
2 Panel anchored / dropdown (flyout, hasil search) Tanpa Eksplorasi, navigasi, pemilihan cepat
3 Dropdown / Popover / Toast & snackbar (avatar, filter, notifikasi sistem) Tanpa Info perifer tak menghalangi; pemilihan dari daftar
4 Slide-over panel / drawer Opsional tipis Detail record tanpa meninggalkan konteks list
5 Modal + backdrop gelap Gelap Hanya keputusan destruktif, bloking, atau konfirmasi kritis

Fullscreen (print preview, data import wizard) tetap ada sebagai pola situasional — dipakai untuk pergantian mode sungguhan, tapi tidak diberi nomor level tersendiri karena bukan soal "seberapa mengganggu", melainkan mode kerja yang sepenuhnya berbeda.

Aturan:

2.12 Kamus 12 Aturan BMP (dari Studi Kasus ERPNext v16)

# Temuan (Sumber) Aturan BMP
1 Klik search modal gelap tengah (UX #1) Panel hasil search selalu anchored di bawah input, tanpa backdrop gelap; gelap hanya level 5+
2 "Load More" di list operasional (UX #2) Pager tradisional wajib untuk list master/transaksi; Load More hanya untuk Timeline/Activity/Audit
3 Sidebar dicampuri lintas domain (UX #3) Strict UI Ownership: sidebar hanya entitas milik domain; lintas domain via Ctrl+K / Settings Hub
4 Launcher menyamakan Platform/Master/Policy (UX #4) Satu Settings Hub; platform tak terlihat user bisnis; master data masuk modul pemiliknya; launcher hanya modul operasional
5 Breadcrumb tak mencerminkan hierarki (UX #5) Breadcrumb digenerate dari pohon navigasi yang sama dengan sidebar; semua level klikabel; kedalaman konsisten
6 Slot primary dipakai navigasi (UX #6) Slot primary = aksi dominan halaman (biasanya create); navigasi = sekunder/menu
7 Halaman alat membuka kosong-mati (UX #7) Setiap halaman membuka berguna (default/terakhir/terpandu); empty state wajib memuat aksi berikutnya
8 Ornamen konsumer di list operasional (UX #8) Kolom hanya yang relevan keputusan; kolaborasi di timeline dokumen; pola sosial tidak diimpor
9 Label navigasi ter-truncate (UX #9) Tidak pernah truncate; wrap dua baris atau kecilkan ikon
10 Kolom teknis bocor ke list bisnis (UX #10) List menampilkan konsep bisnis; tree via indentasi/expand, bukan flag teknis
11 Suffix company mencemari nama (UX #11) Identitas terpisah dari metadata; badge/filter, bukan suffix
12 Satu dropdown mencampur jenis (UX #12) Dropdown dikelompokkan per jenis (navigasi/preferensi/sesi); aksi sistem disembunyikan per role

2.13 Pola Keyboard, Touch & A11y

2.13.1 Peta Keyboard Global

Key Aksi
Ctrl+K Membuka Palette Search (Level 4)
Esc Menutup Flyout, Dropdown, Drawer; tidak menutup Modal level 5 (butuh aksi eksplisit)
Enter (pada baris tabel/modul) Navigasi ke detail record / Workspace modul
Enter (pada chevron) Buka/tutup flyout
Tab Fokus berpindah ke elemen interaktif berikutnya (Chevron adalah elemen Tab terpisah)
↑ / ↓ Navigasi di dalam dropdown/flyout/palette

2.13.2 Touch, Focus & Guard

Aspek Aturan
Focus-visible Ring warna --color-accent untuk semua elemen interaktif
Touch Handle collapse always-visible; chevron hit area ≥28×28px; tombol aksi ≥44×44px
Dirty-form guard Muncul hanya pada gestur navigasi eksplisit (klik label modul, item flyout, logo, browser back). Tidak pada klik chevron, buka dropdown, atau Ctrl+K search

2.14 Studi Kasus: Anti-Pattern ERPNext v16

Bab penutup Chapter 2 — justifikasi empiris untuk seluruh keputusan di atas. Format: Temuan → Diagnosis → Koreksi.

Temuan #1 — Search Modal Gelap

Diagnosis: klik search di level 2 (eksplorasi) tetapi ditampilkan dengan level 5 (modal gelap). Memutus kontinuitas spasial, menyembunyikan konteks, meninggalkan attention residue.

Koreksi: Aturan #1 — search anchored, tanpa gelap.

Temuan #2 — Load More di List Bisnis

Diagnosis: infinite scroll cocok untuk konsumsi konten (social media), merusak spatial memory dan total konteks pada data operasional.

Koreksi: Aturan #2 — pager tradisional wajib.

Temuan #3 — Sidebar Campur Domain

Diagnosis: User dan Email Account (domain System) masuk ke sidebar Organization — melanggar Single Ownership, menciptakan beban kognitif ganda.

Koreksi: Aturan #3 — Strict UI Ownership.

Temuan #4 — Launcher Menyamakan Kategori

Diagnosis: Framework (Platform), Organization (Master), dan Settings (Policy) ditampilkan sebagai ikon setara — meruntuhkan taksonomi.

Koreksi: Aturan #4 — Settings Hub tunggal, platform invisible.

Temuan #5 — Breadcrumb Tidak Konsisten

Diagnosis: Breadcrumb di-generate dari route page, bukan dari pohon workspace sidebar — dua sumber kebenaran yang bertentangan.

Koreksi: Aturan #5 — satu source of truth untuk IA.

Temuan #6 — Slot Primary untuk Navigasi

Diagnosis: Tombol hitam "Set User Permissions" adalah tautan ke halaman lain, bukan aksi create — mengkhianati muscle memory.

Koreksi: Aturan #6 — slot primary = aksi dominan.

Temuan #7 — Halaman Alat Kosong Mati

Diagnosis: Permission Manager terbuka dengan dua input kosong + kalimat perintah — tidak mengajar atau menawarkan langkah pertama.

Koreksi: Aturan #7 — setiap halaman membuka berguna.

Temuan #8 — Ornamen Konsumer di List

Diagnosis: Like, komentar, dan hati di list User — noise di konteks bisnis; semantik "suka" menyesatkan.

Koreksi: Aturan #8 — tanpa pola sosial di list operasional.

Temuan #9 — Label Navigasi Truncate

Diagnosis: "Manufactur...", "Subcontrac..." — navigasi adalah tempat terakhir yang boleh mengorbankan keterbacaan.

Koreksi: Aturan #9 — tidak pernah truncate.

Temuan #10 — Flag Teknis di List Bisnis

Diagnosis: Kolom "Is Group" di Department — user berpikir "induk/anak", bukan "flag group".

Koreksi: Aturan #10 — konsep bisnis, bukan mekanisme.

Temuan #11 — Suffix Company

Diagnosis: "Accounts − PSGA" — metadata menempel ke identitas, membuat list bising.

Koreksi: Aturan #11 — badge/kolom terpisah.

Temuan #12 — Dropdown Campur Jenis

Diagnosis: Satu menu berisi navigasi, preferensi, sesi, dan aksi sistem tanpa pengelompokan.

Koreksi: Aturan #12 — dikelompokkan per jenis, role-gated.

Catatan Implementasi Teknis Chapter 2

Log Keputusan Final (dari 3 Konflik Hasil Penggabungan)

  1. Tangga overlay (2.11) — 5 level, bukan 6. Versi K (6 level) sempat dipakai di draf sebelumnya, tapi ternyata tidak selaras dengan Chapter 1 §1.5 (Shadow & Elevation) yang sudah lebih dulu terkunci dengan 5 level shadow mengikuti tangga overlay ini. Diputuskan pindah ke versi Q (5 level): Toast masuk ke Level 3, Fullscreen jadi pola situasional tanpa nomor level.
  2. Dimensi rail & header — dikonfirmasi ulang oleh user berdasarkan mockup HTML yang sudah berjalan: rail sidebar 210px expanded / 48px collapsed (bukan 224px seperti draf awal), page-head 44px (bukan 48px). Sudah diterapkan ke seluruh tabel/spec di dokumen ini.

Selain empat hal di atas, isi ketiga draf pada dasarnya identik — perbedaan Q terutama gaya penulisan yang lebih ringkas tanpa kehilangan substansi, dan C menambahkan 18 referensi visual yang sekarang disematkan langsung di tiap bagian terkait.

Chapter 3: Architecture

Status: 🔒 Final / Mengikat (v2.0)

Perubahan v2.0: menambahkan 6 sub-bab baru (Architecture Mapping untuk UI/UX, Performance Budget & Caching, Cloak & Pemisahan Tanggung Jawab IT, FTS5/Pagination sebagai sub-bab Data Layer, Real-time Event Architecture, Asset Pipeline & Offline Bundle) + klarifikasi ETS vs Persisted State + penguatan Addendum Sync-Agent untuk konteks offline-first UI. Seluruh sub-bab lama dipertahankan, hanya diberi nomor baru.

3.0 Architecture Invariants

Prinsip yang mengikat seluruh keputusan di bawah — bukan sekadar filosofi tersirat, tapi aturan yang tidak boleh dilanggar modul manapun.

  1. Business logic tidak pernah bergantung pada topologi deployment (Standalone → Office Local-First).
  2. Domain logic hidup di Ash Resource/Action, bukan di Phoenix controller.
  3. Baca lintas domain lewat public action; efek samping lintas domain lewat event.
  4. Transaksi lintas domain yang butuh kepastian “semua-atau-tidak” memakai Reactor.
  5. Replikasi (sync-agent) tidak pernah memanggil Ash action.
  6. Mutasi hasil replikasi harus aman di level constraint database (CHECK, UNIQUE) — ini last line of defense, bukan pengganti validasi Ash.
  7. Data ledger transaksional bersifat append-only (insert-only, tidak pernah UPDATE/DELETE).
  8. Derived state (mis. stok saat ini) tidak pernah jadi source of truth — selalu rebuildable dari data ledger.
  9. Data finansial/audit tidak pernah dihapus otomatis.
  10. Sync infrastructure (sync-agent) berada di luar domain model Ash — infrastructure layer, bukan business/service layer BMP.
  11. Setiap event/transaksi yang punya dokumen penyerta di dunia nyata (invoice supplier, nomor produksi, surat jalan, dst.) wajib punya anchor_id — hash pendek dari (entity_type + nomor dokumen penyerta + tanggal event). Konflik sync dideteksi lewat UNIQUE(anchor_id), bukan last-write-wins (detail di Addendum Sync-Agent).

3.1 Fondasi: Phoenix — Ash — Event-driven

Elemen Peran
Ash.Domain Bounded context per modul (DDD) — Sales, Inventory, Accounting, dst.
Ash Resource Model — data, validasi, business rule, policy.
LiveView (default) / LiveState (khusus POS/kiosk) View + Controller. LiveView wajib pakai komponen bersama; LiveState untuk titik interaksi yang butuh respons sangat cepat di client atau harus tahan koneksi kurang stabil.
Ash.Notifier + AshEvents Event bus antar domain. Baca data lintas domain = panggilan langsung ke public action; efek samping akibat suatu aksi = event.
Reactor Orkestrasi transaksi “semua-atau-tidak” lintas domain (fork-join Checker, Sales Order → reservasi stok → jurnal).

DDD sebagai struktur makro (satu Ash.Domain = satu bounded context), tiap domain punya Model + Domain Logic sendiri, tetapi View tetap mengikuti Design System terpusat — bukan MVC penuh yang berdiri sendiri per domain. Folder structure per-domain (lib/bmp/sales/), bukan per-layer teknis.

3.2 Async & Data Processing

Library Dipakai untuk
Reactor Transaksi butuh rollback/compensation eksplisit.
Oban Job async reliable, terjadwal (eskalasi urgensi approval, dst).
AshOban Job yang terikat lifecycle satu Resource — diprioritaskan di atas Oban mentah bila relevan.
Broadway Ingest data volume/stream dari luar (batch harian POS/E-commerce).
NimblePool Worker pool untuk operasi resource-terbatas (generate PDF/laporan besar, panggilan API eksternal terbatas concurrency).

3.3 Architecture Mapping untuk UI/UX System

Chapter 3 sebelumnya berbicara ke pembaca backend saja. Tapi keputusan UI/UX di Chapter 1, 2, dan 6 punya implikasi arsitektural yang perlu eksplisit didokumentasikan — supaya developer tahu mengapa komponen tertentu dipilih, bukan cuma apa.

Kebutuhan UI (Chapter) Komponen Arsitektur Implementasi
Search universal Ctrl+K (2.5) Ash Query + ETS cache + SQLite FTS5 Endpoint tunggal /api/search menggabungkan Action/Document/Report/Setting; cache master data yang sering dipakai
Pager tradisional (2.2) Ash Query offset-limit + count aggregate page & count wajib di tiap list API; tidak ada cursor-based pagination untuk master/transaksi
Saved views & personalization (2.3, 6.5) Tabel user_settings + saved_views (Data Model §10.6) State persist per user: rail, density, kolom tabel, filter aktif
Account Switcher (2.7) Multitenancy per company + user_roles (§10.9) Session scope company; switch = ganti tenant context tanpa re-login
Field sensitif (Cloak) Ash + AshCloak extension Field BOM rate/COGS/payroll/bank dienkripsi at-rest; akses server dibatasi lewat pemisahan tanggung jawab IT, bukan lewat exclusion role di level aplikasi
Fork-Join Approval (2.6) Reactor + Event Bus Step per checker; event checker_approved/checker_rejected; join gate menunggu N checker
My Attention real-time (2.1) Phoenix PubSub + Presence Channel user:<id>:notifications; push event saat approval masuk
Dirty-form guard (2.13) LiveView form tracking + beforeunload hook Client-side state is_dirty; guard hanya pada navigasi eksplisit (route change)
KPI strip Landing (2.1) Materialized view / scheduled aggregate Refresh interval; cache di ETS; fallback ke query langsung saat cache miss
Flyout menu (2.2) Static menu tree (Elixir map) + role filter Menu tree di-compile; difilter saat render berdasarkan user_roles; tidak hit database per request

3.4 Performance Budget & Caching Strategy

UI/UX responsif (chrome fix, search cepat, list tanpa jeda) butuh janji performa dari architecture.

Operasi UI Target Response Strategi
Initial page load (Landing) < 500ms Inline critical CSS; font preloaded; skeleton screen
Search Ctrl+K (first char) < 100ms ETS cache master data (items, customers, accounts); debounce 150ms
Search Ctrl+K (full result) < 300ms FTS5 untuk dokumen; prefix match untuk nomor dokumen
List view (page load) < 200ms Offset-limit + count; ETS cache untuk filter options
Form load (detail view) < 300ms Eager load relasi via Ash load; tidak N+1
KPI strip refresh < 100ms (cached) ETS TTL 60s; background job update
Flyout menu render < 50ms Menu tree static; role-filter di memory

Caching Hierarchy: - L1 — ETS (in-memory, per node): master data, menu tree, user session — volatile, cepat - L2 — SQLite query cache (Ash built-in): query berulang - L3 — Disk / Sync-Agent: data historis besar

Larangan: - Tidak ada N+1 query di list view (dicek via test) - Tidak ada full table scan untuk filter/search (harus pakai index) - Tidak ada query tanpa LIMIT untuk dropdown/flyout (maks 100 item)

3.5 Cloak & Pemisahan Tanggung Jawab IT

Compliance & keputusan bisnis kritis dari Peta Menu Rev3: field rahasia (resep/twist_notes, margin, payroll, rekening bank) harus terlindung dari staf IT meski mereka administrator sistem.

3.6 Data Layer — Full SQLite

Karena hanya satu data layer, tidak ada lagi kebutuhan “test matrix lintas database” — menyederhanakan CI dan menghilangkan kelas bug “jalan di SQLite, gagal di Postgres” (atau sebaliknya) yang sebelumnya jadi risiko di desain dual-layer.

3.6.1 Search Engine: FTS5 (Full-Text Search 5)

SQLite FTS5 dipakai untuk search dokumen/nomor — bagian dari implementasi Search Ctrl+K di §3 (Architecture Mapping UI/UX) di atas. Index FTS5 dibangun di atas kolom yang sering dicari (nomor dokumen, nama item/customer), disinkronkan via trigger SQLite saat insert/update.

3.6.2 Pagination: Offset/Limit Optimization

Konfirmasi arah yang sudah diputuskan di §3: pagination pakai offset/limit + count aggregate, bukan cursor-based — konsisten dengan Aturan BMP #2 (Chapter 2.12, Pager Tradisional). Optimisasi: index pada kolom sort default (biasanya posting_date DESC atau created_at DESC) supaya OFFSET besar tidak jadi full-scan.

3.7 Audit, Retensi & Kepatuhan (final)

AshArchival + AshPaperTrail, dengan empat aturan wajib:

  1. Tidak ada purge otomatis untuk Resource finansial/audit (PO, Invoice, Journal Entry, Approval/Checker-Approver record). Archive bersifat permanen — tidak ada job terjadwal yang menghapus data ini bahkan setelah retention period (10 tahun untuk pajak Indonesia) terlewati. Storage murah, kehilangan bukti hukum mahal.
  2. Aksi archive itu sendiri wajib tercatat AshPaperTrail — setiap “penghapusan” (arsip) harus punya jejak siapa/kapan/kenapa, sebagai meta-audit atas aksi audit-nya sendiri.
  3. Document Trail dan tab Archived wajib menampilkan record yang diarsipkan secara utuh — query di kedua tempat ini harus eksplisit menyertakan data ter-archive (include_archived?: true), tidak boleh ikut tersaring oleh filter default AshArchival.
  4. Archive adalah state transition (ACTIVE → ARCHIVED), bukan deletion secara konsep maupun implementasi — baris tetap ada secara fisik di database, hanya ditandai archived_at. Mental model ini yang membuat aturan 1–3 di atas konsisten.

Lapis keamanan lain (tidak berubah): AshAuthentication (model akun terpisah) → Ash.Policy.Authorizer (otorisasi) → AshCloak (enkripsi field sensitif) → AshRateLimiter (endpoint publik).

3.8 Ledger Khusus

TigerBeetle (via TigerBeetlex) — bukan pengganti RDBMS BMP Core, hanya untuk POS & E-commerce standalone. Scope sempit: cuma Account/Transfer (ledger kas) — katalog/sesi/cart tetap SQLite. Terhubung ke BMP Core lewat batch sync harian (Oban scheduled job, konfigurable), longgar — bukan transaksi real-time lintas sistem.

3.9 Cache & State

Dengan topologi disederhanakan menjadi SQLite-per-node, kebutuhan shared cache/pub-sub lintas node (Redix) tidak lagi relevan — tiap node (server kantor maupun laptop) berjalan sebagai instance mandiri.

Komponen Kapan dipakai
ETS Default satu-satunya — cache in-process ringan, nol dependency tambahan, dipakai di semua node.

Mnesia dan Redix tidak masuk stack — keduanya menjawab kebutuhan cluster/multi-node yang sudah tidak ada di topologi ini.

Klarifikasi (v2.0) — ETS vs Persisted State: ETS tidak persisten saat server restart, padahal state UI per user (rail collapsed, density, landing pin) perlu bertahan. Maka:

3.10 Real-time Event Architecture untuk UI

Menjelaskan bagaimana event backend sampai ke UI (My Attention, notifikasi approval, status dokumen berubah).

Phoenix PubSub Topics: - user:{user_id}:notifications — approval masuk, task assigned - company:{company_id}:document_updates — dokumen yang sedang dibuka user lain berubah status - company:{company_id}:kpi_refresh — trigger refresh KPI strip

Event-to-UI Mapping:

Event Backend Efek UI
approval_requested Toast + badge counter My Attention + push notifikasi browser (jika diizinkan)
document_status_changed Update badge status di list view yang sedang terbuka
checker_rejected Modal konfirmasi (level 5) jika user sedang di halaman dokumen yang sama
sync_completed Update timestamp “Last synced” di header

Backpressure: user offline (mobile laptop) → event di-queue di Oban → saat online, di-flush batch (maks 50 event/burst) → UI tampilkan “N notifikasi baru”, bukan toast individual per event.

3.11 Observability, I18n, Storage

3.12 Asset Pipeline & Offline Bundle

Menjelaskan bagaimana font & ikon (disebut Chapter 1) di-build dan di-deploy sebagai bundle offline.

3.13 Testing & Static Analysis

3.14 Deployment Profiles

Dua profil, disederhanakan dari desain sebelumnya setelah PostgreSQL/cloud dihapus dari scope.

Profile Topologi Node & peran
1. Standalone Satu mesin, satu SQLite Tidak ada node lain — dipakai UMKM sekecil apa pun (1 komputer/laptop).
2. Office + Mobile Laptop (Local-First Sync) Satu server kantor on-premise + PC kantor (thin client) + sejumlah laptop mobile Lihat rincian peran per node di bawah.

Peran node di Profile 2

Node Instalasi Sync
Server kantor Ash app penuh + SQLite (source of truth kantor) Menjalankan sync-agent, menunggu koneksi dari laptop mobile
PC kantor (desktop, tidak keluar kantor) Thin client — connect langsung ke server kantor lewat LAN (browser/LiveView biasa) Tidak perlu — tidak ada SQLite lokal, tidak ada sync-agent
Laptop mobile (kadang di luar kantor) Ash app penuh + SQLite lokal Menjalankan sync-agent — connect ke localhost saat di luar kantor, ke IP server saat di kantor

Ini koreksi penting dari desain sebelumnya: bukan semua komputer perlu instalasi penuh + sync-agent. Hanya laptop yang benar-benar berpindah lokasi (dan karenanya butuh kemampuan offline) yang menjalankan SQLite lokal dan sync-agent. PC kantor yang selalu berada di LAN cukup jadi client biasa ke server kantor — persis seperti aplikasi web internal pada umumnya, tanpa kompleksitas sync sama sekali.

Detail lengkap mekanisme sync-agent (komponen, alur, protokol, failure mode, keamanan) dibahas di Addendum.

3.15 Hierarki Pemilihan Dependency (kebijakan resmi)

Tier 1 Ash/Phoenix → Tier 2 Elixir umum → Tier 3 Erlang/OTP langsung. Di mana pun Ash punya ekstensi resmi (Ash*), itu selalu dipilih di atas library generik non-Ash. Non-Ash hanya dipakai untuk hal di luar cakupan Ash (Phoenix itu sendiri, LiveState, Swoosh, Waffle, sync-agent) — semuanya tetap murni ekosistem Erlang/Elixir/Rust resmi tanpa dependency berat di luar itu.

Rincian Library Tier 2 (Elixir Umum) yang Dikunci untuk BMP:

  1. PDF Engine: tincture (pure Elixir). Menghasilkan PDF berkualitas tipografi tinggi (PDF/A untuk arsip, AES-256 untuk slip gaji) tanpa binary eksternal seperti wkhtmltopdf atau Chrome headless. Template PDF disimpan sebagai data XML, bukan HTML.
  2. QR Code: eqrcode (pure Elixir, zero dependencies). Dipakai untuk men-generate QR code Batch ID pada label traceability CPKB (Modul Manufacturing & Sales). Output SVG-nya langsung di-embed ke dalam template tincture.
  3. XLSX Export: exceed (pure Elixir, stream-oriented). Dipakai untuk mengekspor laporan berskala besar (misal: GL drill-down ribuan baris di Accounting, atau Sales Analytics tahunan) langsung ke format Excel .xlsx tanpa menyebabkan memory blow-up.
  4. CSV Import/Export: nimble_csv (pure Elixir). Dipakai sebagai jalur utama ingest data massal, seperti import rekap CSV harian dari marketplace (Sales) dan import statement bank (Accounting).
  5. Image Processing: DITOLAK. Kebutuhan manipulasi foto produk (resize, crop) dilayani di client-side (browser) atau tidak diperlukan sama sekali. Library populer seperti image (yang membungkus NIF libvips dari bahasa C) ditolak agar stack BMP tetap 100% pure BEAM tanpa risiko segmentation fault atau keharusan mengompilasi binary C di server produksi.
  6. Email (Swoosh): DITUNDA. Sesuai keputusan arsitektur, pengiriman dokumen (Invoice/DN) di Fase 1 dilakukan manual (Save PDF → Gmail/WA). Integrasi SMTP otomatis ditunda ke Fase 2.

Addendum: Mekanisme Sync-Agent — Penjelasan Detail

1. Gambaran Besar

Sync-agent adalah proses Rust kecil yang berjalan di server kantor dan di tiap laptop mobile (bukan di PC kantor — lihat bagian 9). Tugasnya satu: memastikan perubahan data di SQLite laptop dan SQLite server kantor selalu sinkron, tanpa Ash app perlu tahu apa pun.

2. Komponen Utama

Diagram 1 — Komponen Sync-Agent

2.1 Session Manager

Bertanggung jawab atas capture perubahan dari SQLite: membuat SQLite Session via rusqlite (feature session), attach ke tabel-tabel yang perlu disinkronkan, merekam setiap INSERT/UPDATE/DELETE yang dilakukan Ash app, dan menghasilkan changeset (blob biner) saat flush dipanggil. Session Manager tidak mengintervensi Ash app sama sekali — Ash menulis ke SQLite seperti biasa, Session Extension merekam efeknya di belakang layar.

2.2 Flush Scheduler

Bertanggung jawab memutuskan kapan changeset diambil dan dikirim. Model koneksi: begitu laptop pertama kali terhubung ke server kantor, seluruh perubahan tertunda langsung di-flush dan disinkronkan penuh (initial full sync). Setelah itu, koneksi TCP+TLS dijaga tetap terbuka (persistent) — setiap ada perubahan baru, flush dipicu segera (event-driven), bukan menunggu timer. Timer berkala (5–10 detik) tetap dipertahankan sebagai heartbeat/fallback (menjaga koneksi hidup dan menangkap perubahan yang mungkin terlewat), bukan sebagai mekanisme utama.

2.3 Transport Layer

Bertanggung jawab mengirim dan menerima changeset antar node.

2.4 Apply Engine

Bertanggung jawab menerapkan changeset yang diterima ke SQLite lokal: memanggil sqlite3changeset_apply() via rusqlite, menjalankan conflict handler per baris per tabel, dan mengirim ACK ke pengirim setelah berhasil.

2.5 State Manager

Bertanggung jawab menyimpan status sync — high_water_mark (offset changeset terakhir yang berhasil di-ACK, berfungsi sebagai sync cursor), schema_version (versi schema SQLite saat ini), last_synced_at, dan peer_status (reachable/unreachable). Disimpan di file lokal kecil terpisah (bukan di SQLite utama) agar tidak tercampur dengan data bisnis.

2.6 Telemetry Publisher

Mempublikasikan metrik ke Elixir lewat named pipe/Unix socket — event :telemetry (sync.changeset.sent, sync.changeset.applied, sync.conflict.resolved, sync.lag_seconds) yang dikonsumsi System Console BMP untuk menampilkan status sync per node.

3. Alur Lengkap: Laptop ke Server Kantor

Diagram 2 — Alur Lengkap Sync (Laptop → Server → Laptop)

Fase 1 — Capture

Ash app (exqlite) menulis ke SQLite (WAL mode) seperti biasa — INSERT/UPDATE/DELETE apa pun. Session Manager (rusqlite + session feature) merekam semua perubahan secara otomatis di belakang layar.

Fase 2 — Flush

Dipicu oleh perubahan baru (event-driven, koneksi persistent), timer heartbeat, peer baru terdeteksi, atau manual trigger. Session Manager memanggil sqlite3session_changeset() dan menghasilkan changeset blob biner.

Fase 3 — Transport

Transport Layer di laptop mengecek peer reachable, membuka (atau memakai) koneksi TCP+TLS ke server kantor, mengirim schema_version untuk verifikasi, mengirim changeset dalam frame length-prefixed, lalu menunggu ACK.

Fase 4 — Apply di Server

Server menerima frame, memverifikasi schema_version (tidak cocok → tolak koneksi, kirim error), lalu meneruskan ke Apply Engine yang memanggil sqlite3changeset_apply() dengan conflict handler:

Berhasil → kirim ACK. Gagal → kirim NACK + alasan.

Fase 5 — Konfirmasi & State Update

Laptop menerima ACK → update high_water_mark, update last_synced_at, publish telemetry (sync.changeset.sent + applied). Menerima NACK → log error, retry dengan backoff eksponensial, publish telemetry sync.conflict.manual bila perlu.

Fase 6 — Sync Balik (Server → Laptop)

Setelah ACK terkirim, server melakukan hal yang sama: flush session server (perubahan dari user kantor), kirim changeset ke laptop, laptop apply lalu kirim ACK balik. Ini yang membuat sync dua arah berjalan dalam satu koneksi persistent yang sama — bukan dua koneksi terpisah.

4. Conflict Resolution

Diagram 3 — Conflict Resolution per Jenis Tabel

5. Failure Mode & Recovery

Diagram 4 — Failure Mode & Recovery

6. Catatan Penting Operasional

WAL Mode wajib — semua SQLite yang terlibat harus dikonfigurasi:

PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; Tanpa ini, Ash app dan sync-agent bisa saling block saat menulis bersamaan.

CHECK Constraint wajib — karena apply changeset bypass Ash action, business rule kritikal harus ada di level SQLite, bukan hanya di validasi Ash: CHECK (qty >= 0) CHECK (amount > 0)

Schema Version wajib — setiap migrasi Ash harus menaikkan schema_version di metadata database. Sync-agent membaca ini sebelum menerima koneksi apa pun, dan menolak sync jika versi laptop dan server tidak cocok (urutan upgrade: server kantor dulu, baru laptop — selama laptop belum di-upgrade, sync berhenti sementara, bukan corrupt).

Penguatan (v2.0) — konteks offline-first UI:

Anchor ID — Deteksi Konflik Non-LWW (v2.0):

Menggantikan last-write-wins untuk seluruh tabel event/transaksional (bukan cuma draft) — setiap event yang di dunia nyata punya dokumen penyerta (invoice supplier, nomor produksi, surat jalan, dsb.) wajib mencatat identitas itu lewat 1 kolom tambahan.

Formula: anchor_id = short_hash(entity_type + nomor_dokumen_penyerta + tanggal_event). Contoh: barang masuk dari invoice INV-88214 tanggal 12 Agu 2026 → short_hash("goods_receipt|INV-88214|2026-08-12"); produksi batch PRD-0451 tanggal 15 Agu 2026 → short_hash("production|PRD-0451|2026-08-15").

Ditegakkan lewat UNIQUE(anchor_id) per tabel (Invariant #11) — dua laptop offline yang mencatat kejadian bisnis yang sama akan menghasilkan anchor_id identik, sehingga tabrakan kelihatan saat sync (bukan salah satu diam-diam kalah lewat timestamp).

Penanganan tabrakan: di-flag sebagai Sync Conflict untuk direview manual (Manager/Admin terkait) — tidak ditolak otomatis dan tidak ditimpa diam-diam, konsisten dengan prinsip “yang pengecualian dibuat terlihat” (BMP Reason).

Cakupan: seluruh tabel event/transaksional yang punya dokumen penyerta di dunia nyata — bukan cuma goods receipt dan production yang jadi contoh; setiap resource baru wajib dicek apakah dia kandidat anchor_id saat didesain.

Chapter 4: Data Model & Table Specifications

Bab ini menjawab pertanyaan "apa" — skema database lengkap untuk implementasi
Ash Framework. Rasional di balik tiap keputusan besar (kenapa Customer = channel,
kenapa stok tidak boleh negatif, kenapa IT tidak bisa baca field rahasia, dst) ada
di dokumen pendamping BMP Reason*. Konvensi penulisan kode yang mengikuti
skema ini ada di Chapter 5 (Development Conventions).*

Status: 🔒 Final / Mengikat
Dasar: Chapter 3 (Architecture) · BMP Reason · Data Model v1.6
Tujuan: Blueprint tunggal untuk implementasi database dan resource Ash Framework
Versi: 1.7 (Upgrade dari v1.6 — menuntaskan 2 refactor struktural dari Cross-Check v1.5 yang sempat terlewat)

Changelog v1.7 (vs v1.6):

1. ➕ Tabel baru departments (§1.19) — master departemen (Finance & Accounting, HR & GA, Production, Sales, Marketing, Technical Support, IT Support, Quality), menggantikan string bebas.

2. 🔧 employees.department (string) → employees.department_id (UUID → departments).

3. ➕ Tabel baru roles (§10.8) dan user_roles (§10.9, junction many-to-many) — menggantikan users.roles (JSONB).

4. 🔧 permissions.role (string bebas) → permissions.role_id (UUID → roles).

5. 🔧 users — kolom roles (JSONB) dihapus, digantikan relasi lewat user_roles.

4.1. Master Data

4.1.1 companies

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
name string Nama perusahaan Standar
default_currency string IDR (fixed, tidak nullable) Standar
country string Indonesia (fixed, tidak nullable) Standar
enable_perpetual_inventory boolean Always TRUE (BMP policy, hardcoded) Standar (diadaptasi)
default_bank_account_id UUID Relasi ke accounts Standar
default_receivable_account_id UUID Relasi ke accounts Standar
default_payable_account_id UUID Relasi ke accounts Standar
default_inventory_account_id UUID Relasi ke accounts Standar
default_cost_center_id UUID Relasi ke cost_centers ➕ v1.5
default_round_off_account_id UUID Relasi ke accounts — akun pembulatan selisih (§4.10) v1.6
tax_id string NPWP (dienkripsi — Cloak Grup E) Standar
bpom_license string Nomor Izin Edar BPOM (dienkripsi — Cloak Grup E) Standar
fiscal_year_start_month integer 1 (Januari, fixed untuk Indonesia) v1.6
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.1.2 accounts (Chart of Accounts)

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
account_number string Kode akun (mis. "1-1000") Standar
account_name string Nama akun — Bahasa Indonesia untuk output fiskal (§4.13) Standar
account_name_internal string Nama internal/kode — English (§4.9) v1.6
account_type string Bank, Cash, Receivable, Payable, Stock, Tax, Round Off, dll. Standar
root_type string Asset, Liability, Equity, Income, Expense Standar
parent_id UUID Relasi ke accounts (tree) Standar
is_group boolean True = parent/folder Standar
is_frozen boolean True = tidak bisa diposting Standar
company_id UUID Relasi ke companies Standar
currency string IDR Standar
lft integer Nested set left (untuk tree query cepat) v1.6
rgt integer Nested set right (untuk tree query cepat) v1.6
old_parent_id UUID Untuk reparenting tree v1.6
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

Akun Beban Wajib untuk Komponen Fee:

Kode Nama Akun (Indonesia) Nama Internal (English) Root Type Account Type
5-1000 Beban Marketplace Fee Marketplace Fee Expense Expense Expense
5-2000 Beban Komisi Reseller Reseller Commission Expense Expense Expense
5-3000 Beban Diskon Penjualan Sales Discount Expense Expense Expense
5-4000 Beban Promosi Promotion Expense Expense Expense
5-5000 Beban Pembulatan Round Off Expense Expense Round Off

4.1.3 cost_centers

Kolom Tipe Deskripsi Status
id UUID Primary key ➕ v1.5
name string Nama cost center (mis. "Perusahaan", "Produksi", "Marketing") ➕ v1.5
parent_id UUID Relasi ke cost_centers (tree) ➕ v1.5
is_group boolean True = parent/folder ➕ v1.5
company_id UUID Relasi ke companies ➕ v1.5
is_active boolean ➕ v1.5
lft integer Nested set left v1.6
rgt integer Nested set right v1.6
created_at timestamp UTC ➕ v1.5
updated_at timestamp UTC ➕ v1.5

Catatan: Cost Center hide dari UI untuk user operasional. Hanya Owner/Finance yang melihatnya.

4.1.4 fiscal_years

Kolom Tipe Deskripsi Status
id UUID Primary key ➕ v1.5
name string "2026" ➕ v1.5
year_start_date date Tanggal mulai (1 Januari) ➕ v1.5
year_end_date date Tanggal akhir (31 Desember) ➕ v1.5
is_active boolean True = tahun berjalan ➕ v1.5
company_id UUID Relasi ke companies ➕ v1.5
created_at timestamp UTC ➕ v1.5
updated_at timestamp UTC ➕ v1.5

4.1.5 accounting_periods

Kolom Tipe Deskripsi Status
id UUID Primary key ➕ v1.5
name string "Januari 2026" ➕ v1.5
period_start_date date Tanggal mulai ➕ v1.5
period_end_date date Tanggal akhir ➕ v1.5
is_closed boolean True = sudah ditutup ➕ v1.5
fiscal_year_id UUID Relasi ke fiscal_years ➕ v1.5
company_id UUID Relasi ke companies ➕ v1.5
created_at timestamp UTC ➕ v1.5
updated_at timestamp UTC ➕ v1.5

Catatan: Accounting Period hide dari UI sampai ada kebutuhan tutup buku formal.

4.1.6 items (Master Produk)

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
item_code string Kode unik (mis. "BLUEGRAY-01") Standar
item_name string Nama produk Standar
item_group_id UUID Relasi ke item_groups Standar
uom_id UUID Satuan dasar Standar
is_stock_item boolean True = barang fisik Standar
has_batch_no boolean BMP: True untuk semua produk Standar (diadaptasi)
has_serial_no boolean BMP: False (pakai batch) ❌ Dihapus
has_shelf_life boolean BMP: False untuk parfum (expiry compliance only) Standar (diadaptasi)
valuation_method string BMP: Moving Average default Standar (diadaptasi)
default_warehouse_id UUID Relasi ke warehouses Standar
supplier_id UUID Default supplier Standar
lead_time_days integer Lead time pembelian/produksi Standar
reorder_level decimal Level stok minimum Standar
safety_stock decimal Stok pengaman Standar
is_active boolean Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.1.7 item_groups

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
name string FG, Bulk, RM, Packaging Standar
parent_id UUID Relasi ke item_groups (tree) Standar
is_group boolean Standar
default_income_account_id UUID Relasi ke accounts Standar
default_expense_account_id UUID Relasi ke accounts Standar
default_cogs_account_id UUID Relasi ke accounts Standar
default_inventory_account_id UUID Relasi ke accounts Standar
lft integer Nested set left v1.6
rgt integer Nested set right v1.6
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.1.8 customers

Catatan: Customer di BMP = Channel Marketplace (Shopee, Tokopedia, TikTok Shop). Setiap Customer/channel memiliki 1 Sales Person penanggung jawab (lihat 1.17 sales_persons).

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
customer_code string Kode channel (CUST-SHOPEE) Standar (diadaptasi)
customer_name string Nama channel Standar (diadaptasi)
customer_group_id UUID Relasi ke customer_groups Standar
sales_person_id UUID Relasi ke sales_persons — PIC channel ini ➕ v1.5
default_price_list_id UUID Relasi ke price_lists (1 price list) Standar
default_sales_taxes_template_id UUID Relasi ke taxes_templates Standar
default_receivable_account_id UUID Relasi ke accounts Standar
default_cost_center_id UUID Relasi ke cost_centers ➕ v1.5
is_active boolean Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.1.9 customer_groups

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
name string Marketplace, Retail Fisik, Distributor B2B Standar
parent_id UUID Relasi ke customer_groups (tree) Standar
is_group boolean Standar
lft integer Nested set left v1.6
rgt integer Nested set right v1.6
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.1.10 suppliers

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
supplier_code string Kode supplier Standar
supplier_name string Nama supplier Standar
supplier_group_id UUID Relasi ke supplier_groups Standar
default_currency string IDR Standar (diadaptasi)
default_payable_account_id UUID Relasi ke accounts Standar
default_purchase_taxes_template_id UUID Relasi ke taxes_templates Standar
tax_withholding_category_id UUID Relasi ke tax_withholding_categories Standar
is_active boolean Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.1.11 supplier_groups

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
name string Importir Oil, Supplier Alkohol, Supplier Kemasan, Jasa Ekspedisi Standar
parent_id UUID Relasi ke supplier_groups (tree) Standar
is_group boolean Standar
lft integer Nested set left v1.6
rgt integer Nested set right v1.6
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.1.12 warehouses

Warehouse Zoning BMP:

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
name string Nama gudang Standar
warehouse_type string Storage, Work In Progress, Rejected, Scrap, Transit Standar
parent_id UUID Relasi ke warehouses (tree) Standar
is_group boolean Standar
company_id UUID Relasi ke companies Standar
is_active boolean Standar
lft integer Nested set left v1.6
rgt integer Nested set right v1.6
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.1.13 uoms (Unit of Measure)

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
uom_code string ml, L, kg, pcs Standar
uom_name string Milliliter, Liter, Kilogram, Pieces Standar
is_active boolean Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.1.14 uom_conversions

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
from_uom_id UUID Relasi ke uoms Standar
to_uom_id UUID Relasi ke uoms Standar
conversion_factor decimal Presisi 9 desimal (§4.10) Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.1.15 price_lists

Catatan Penting: BMP hanya menggunakan 1 Price List ("Harga Dasar — IDR"). Semua variasi harga (diskon, markup) ditangani oleh Fee Components (lihat bagian 5.2).

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
name string Hanya 1: "Harga Dasar — IDR" Standar
currency string IDR Standar
is_active boolean Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.1.16 item_prices

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
item_id UUID Relasi ke items Standar
price_list_id UUID Relasi ke price_lists Standar
price decimal Harga per unit — s.d. 4 desimal internal (§4.10) Standar
effective_date date Tanggal berlaku Standar
is_active boolean Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.1.17 sales_persons

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
employee_id UUID Relasi ke employees, wajib & unik — nama/departemen di-resolve via join ➕ Revisi v1.5
is_active boolean Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

Catatan: sales_persons = pandangan "fungsi Sales" atas karyawan; identitas tunggal tetap di employees. 1 Customer/channel → 1 sales_person_id; sales_orders.sales_person_id menyimpan snapshot PIC saat transaksi (histori kontribusi tahan rotasi).

4.1.18 numbering_series ➕ v1.6 (BARU)

Catatan: Numbering series terdefinisi kode per instalasi (§4.11). Tidak ada DDL runtime. Dokumen cancel menyimpan nomornya (tidak dipakai ulang).

Kolom Tipe Deskripsi Status
id UUID Primary key v1.6
prefix string Prefix series (mis. "SO-SHOPEE", "BLG") v1.6
doctype string Dokumen yang dilayani (SalesOrder, PurchaseOrder, Batch, dll.) v1.6
current integer Nomor terakhir yang terpakai v1.6
pad_length integer Panjang padding (default 3 → 001) v1.6
suffix string Suffix opsional (mis. "-2026") v1.6
company_id UUID Relasi ke companies v1.6
is_active boolean v1.6
created_at timestamp UTC v1.6
updated_at timestamp UTC v1.6

4.1.19 departments ➕ v1.7 (BARU)

Catatan: Master departemen — formalisasi dari org chart (8 departemen: Finance & Accounting, HR & GA, Production, Sales, Marketing, Technical Support, IT Support, Quality). Semua departemen melapor langsung ke Direktur (§ org chart), jadi tidak butuh parent_id/tree.

Kolom Tipe Deskripsi Status
id UUID Primary key v1.7
department_code string Kode departemen (mis. "SALES", "PROD") v1.7
department_name string Nama departemen v1.7
is_active boolean v1.7
created_at timestamp UTC v1.7
updated_at timestamp UTC v1.7

4.2 Inventory & Stock

4.2.1 batches

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
batch_id string Nomor batch (mis. "BLG-2026-001") Standar
item_id UUID Relasi ke items Standar
warehouse_id UUID Relasi ke warehouses Standar
qty decimal Jumlah stok batch Standar
manufacturing_date date Tanggal produksi Standar
expiry_date date Tanggal kedaluwarsa (compliance) Standar
supplier_id UUID Relasi ke suppliers Standar
supplier_reference string Referensi lot dari supplier Standar
supplier_drum_code string BMP Custom: Kode rahasia drum (Cloak Grup E) ➕ v1.5
maturity_start_date date BMP Custom: Tanggal mulai maturing ➕ v1.5
maturing_days integer BMP Custom: Lama maturing aktual ➕ v1.5
parent_batch_id UUID BMP Custom: Batch induk (FG ← Bulk) ➕ v1.5
quality_status string BMP Custom: QC Pending / Maturing / Accepted / Rejected / Consumed / Exhausted ➕ v1.5
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.2.2 stock_entries

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
stock_entry_code string Nomor dokumen Standar
stock_entry_type string Material Receipt, Material Issue, Material Transfer, Manufacture, Repack Standar
work_order_id UUID Relasi ke work_orders Standar
purchase_receipt_id UUID Relasi ke purchase_receipts Standar
delivery_note_id UUID Relasi ke delivery_notes Standar
posting_date date Tanggal transaksi Standar
posting_time time Waktu transaksi (UTC, render Asia/Jakarta) Standar
company_id UUID Relasi ke companies Standar
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
is_active boolean Standar
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.2.3 stock_entry_items

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
stock_entry_id UUID Relasi ke stock_entries Standar
item_id UUID Relasi ke items Standar
batch_id UUID Relasi ke batches Standar
qty decimal Jumlah Standar
rate decimal Harga per unit — s.d. 4 desimal (§4.10) Standar
source_warehouse_id UUID Relasi ke warehouses Standar
target_warehouse_id UUID Relasi ke warehouses Standar
additional_costs JSONB BMP Custom: Biaya tambahan (overhead) ➕ v1.5
is_scrap boolean BMP Custom: True = by-product ➕ v1.5
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.2.4 stock_ledger_entries (SLE) — Immutable

Kolom Tipe Deskripsi Status
id UUIDv7 Primary key — time-ordered untuk lokalitas index SQLite (§4.11) v1.6
item_id UUID Relasi ke items Standar
warehouse_id UUID Relasi ke warehouses Standar
batch_id UUID Relasi ke batches Standar
voucher_type string Stock Entry, Purchase Receipt, Delivery Note, Sales Invoice Standar
voucher_no string Nomor dokumen sumber Standar
qty_change decimal Perubahan kuantitas Standar
qty_after_transaction decimal Saldo setelah transaksi Standar
valuation_rate decimal Harga per unit — s.d. 4 desimal Standar
posting_date date Tanggal transaksi Standar
posting_time time Waktu transaksi (UTC) Standar
company_id UUID Relasi ke companies Standar
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
created_at timestamp UTC Standar

4.2.5 bins (Agregasi Stok)

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
item_id UUID Relasi ke items Standar
warehouse_id UUID Relasi ke warehouses Standar
actual_qty decimal Stok aktual Standar
projected_qty decimal Stok + incoming - outgoing Standar
reserved_qty decimal Stok yang direservasi Standar
valuation_rate decimal Harga rata-rata — Cloak Grup A Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

MATERIALIZED VIEW (v1.5): bins adalah derived state — diisi ulang dari stock_ledger_entries + order terbuka; bukan source of truth dan tidak menerima write langsung. actual_qty = SUM(qty_change) per item+warehouse; valuation_rate = rate SLE terakhir; projected_qty = actual + incoming terbuka − outgoing terbuka; reserved_qty = reservasi aktif. Rebuildable kapan pun via aksi Repost / job Oban (konsisten Architecture Invariant #8: derived state selalu rebuildable dari ledger).

4.2.6 stock_reservations ➕ v1.6 (BARU)

Catatan: Tabel untuk reservasi stok aktif. Dihitung dalam projected_qty bins.

Kolom Tipe Deskripsi Status
id UUID Primary key v1.6
item_id UUID Relasi ke items v1.6
warehouse_id UUID Relasi ke warehouses v1.6
batch_id UUID Relasi ke batches (nullable = semua batch) v1.6
voucher_type string Sales Order, Work Order, dll. v1.6
voucher_id UUID ID dokumen sumber v1.6
qty decimal Jumlah direservasi v1.6
status string Active, Consumed, Released, Expired v1.6
created_at timestamp UTC v1.6
updated_at timestamp UTC v1.6

4.3 Purchase

4.3.1 purchase_orders

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
po_code string Nomor PO Standar
supplier_id UUID Relasi ke suppliers Standar
material_request_id UUID Relasi ke material_requests Standar
transaction_date date Tanggal PO Standar
reference_usd_rate decimal BMP Custom: Kurs USD-IDR saat PO ➕ v1.5
total_qty decimal Standar
total_amount decimal Rupiah bulat (§4.10) Standar
currency string IDR Standar
status string Draft, Submitted, Partially Received, Completed, Cancelled Standar
company_id UUID Relasi ke companies Standar
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.3.2 purchase_order_items

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
purchase_order_id UUID Relasi ke purchase_orders Standar
item_id UUID Relasi ke items Standar
qty decimal Jumlah pesan Standar
rate decimal Harga per unit (dalam IDR) — s.d. 4 desimal Standar
reference_usd_price decimal BMP Custom: Harga dalam USD (dari katalog) ➕ v1.5
uom_id UUID Relasi ke uoms Standar
discount decimal Standar
total decimal Standar
received_qty decimal Jumlah yang sudah diterima Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.3.3 purchase_receipts

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
pr_code string Nomor PR Standar
purchase_order_id UUID Relasi ke purchase_orders Standar
supplier_id UUID Relasi ke suppliers Standar
posting_date date Tanggal terima Standar
posting_time time Waktu terima (UTC) Standar
total_qty decimal Standar
total_amount decimal Rupiah bulat Standar
status string Draft, Submitted, Partially Accepted, Completed, Cancelled Standar
is_return boolean True = retur ke supplier Standar
company_id UUID Relasi ke companies Standar
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.3.4 purchase_receipt_items

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
purchase_receipt_id UUID Relasi ke purchase_receipts Standar
item_id UUID Relasi ke items Standar
batch_id UUID Relasi ke batches Standar
qty_received decimal Jumlah diterima Standar
qty_accepted decimal Jumlah lulus QC Standar
qty_rejected decimal Jumlah ditolak QC Standar
rate decimal Harga per unit (dalam IDR) — s.d. 4 desimal Standar
reference_usd_price decimal BMP Custom: Harga dalam USD (dari katalog) ➕ v1.5
uom_id UUID Relasi ke uoms Standar
supplier_drum_code string BMP Custom: Kode rahasia drum (Cloak Grup E) ➕ v1.5
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.3.5 purchase_invoices

Kolom Tipe Deskripsi Status
id UUID Primary key ➕ v1.5
pi_code string Nomor PI ➕ v1.5
purchase_order_id UUID Relasi ke purchase_orders ➕ v1.5
purchase_receipt_id UUID Relasi ke purchase_receipts ➕ v1.5
supplier_id UUID Relasi ke suppliers ➕ v1.5
posting_date date Tanggal invoice ➕ v1.5
total_gross decimal Total sebelum PPN — Rupiah bulat ➕ v1.5
tax_amount decimal PPN Masukan ➕ v1.5
tax_withholding_amount decimal PPh 23 (jika ada) ➕ v1.5
total_net decimal Total + PPN - PPh — Rupiah bulat ➕ v1.5
purchase_price_variance decimal Selisih antara PO rate dan PI rate ➕ v1.5
status string Draft, Submitted, Paid, Cancelled ➕ v1.5
company_id UUID Relasi ke companies ➕ v1.5
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
created_by UUID User ID ➕ v1.5
created_at timestamp UTC ➕ v1.5
updated_at timestamp UTC ➕ v1.5

4.3.6 purchase_invoice_items

Kolom Tipe Deskripsi Status
id UUID Primary key ➕ v1.5
purchase_invoice_id UUID Relasi ke purchase_invoices ➕ v1.5
item_id UUID Relasi ke items ➕ v1.5
batch_id UUID Relasi ke batches ➕ v1.5
qty decimal Jumlah ➕ v1.5
rate decimal Harga per unit (IDR) — s.d. 4 desimal ➕ v1.5
reference_usd_price decimal BMP Custom: Harga dalam USD (dari katalog) ➕ v1.5
uom_id UUID Relasi ke uoms ➕ v1.5
discount decimal ➕ v1.5
total decimal ➕ v1.5
created_at timestamp UTC ➕ v1.5
updated_at timestamp UTC ➕ v1.5

4.3.7 material_requests

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
mr_code string Nomor MR Standar
material_request_type string Purchase, Material Transfer, Material Issue, Manufacture Standar
item_id UUID Relasi ke items Standar
qty decimal Jumlah diminta Standar
required_date date Tanggal dibutuhkan Standar
warehouse_id UUID Relasi ke warehouses Standar
status string Pending, Ordered, Transferred, Issued, Received Standar
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.3.8 landed_cost_vouchers

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
lcv_code string Nomor LCV Standar
purchase_receipt_id UUID Relasi ke purchase_receipts Standar
distribution_method string Qty, Amount, Weight Standar
total_cost decimal Total biaya tambahan Standar
status string Draft, Submitted, Cancelled Standar
company_id UUID Relasi ke companies Standar
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.3.9 landed_cost_voucher_items

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
landed_cost_voucher_id UUID Relasi ke landed_cost_vouchers Standar
expense_account_id UUID Relasi ke accounts Standar
amount decimal Biaya tambahan Standar
description string Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.4 Manufacturing

4.4.1 boms (Bill of Materials)

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
bom_code string Kode BOM Standar
item_id UUID Relasi ke items Standar
qty_output decimal Jumlah output standar Standar
is_active boolean Standar
is_default boolean True = default BOM Standar
rm_cost_as_per string Valuation / Price List Standar
with_operations boolean True = ada biaya overhead Standar
maturing_days integer BMP Custom: SOP maturing ➕ v1.5
formulation_code string BMP Custom: Kode internal formulasi (Cloak Grup B) ➕ v1.5
twist_notes text BMP Custom: Catatan "twist" rahasia (dienkripsi — Cloak Grup B) ➕ v1.5
company_id UUID Relasi ke companies Standar
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.4.2 bom_items

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
bom_id UUID Relasi ke boms Standar
item_id UUID Relasi ke items Standar
qty decimal Jumlah komponen Standar
rate decimal Harga per unit (snapshot) — Cloak Grup B Standar
uom_id UUID Relasi ke uoms Standar
source_warehouse_id UUID Relasi ke warehouses Standar
operation_id UUID Relasi ke operations Standar
is_scrap boolean BMP Custom: True = by-product ➕ v1.5
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.4.3 operations

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
operation_name string Mixing, Filling, Capping, Labeling, Packaging Standar
workstation string Mixer 200L, Filling Line 1 Standar
time_in_mins decimal Durasi standar Standar
hour_rate decimal Biaya overhead per jam Standar
company_id UUID Relasi ke companies Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.4.4 work_orders

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
wo_code string Nomor WO Standar
production_item_id UUID Relasi ke items Standar
bom_id UUID Relasi ke boms Standar
qty_to_produce decimal Jumlah target Standar
qty_produced decimal Jumlah realisasi Standar
fg_warehouse_id UUID Relasi ke warehouses Standar
wip_warehouse_id UUID Relasi ke warehouses Standar
source_warehouse_id UUID Relasi ke warehouses Standar
planned_start_date date Tanggal mulai rencana Standar
planned_end_date date Tanggal selesai rencana Standar
actual_start_date date Tanggal mulai aktual Standar
actual_end_date date Tanggal selesai aktual Standar
status string Draft, Submitted, Not Started, In Process, Completed, Stopped, Cancelled Standar
allow_overproduction decimal Toleransi (%) Standar
company_id UUID Relasi ke companies Standar
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.4.5 work_order_operations

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
work_order_id UUID Relasi ke work_orders Standar
operation_id UUID Relasi ke operations Standar
planned_time decimal Durasi rencana Standar
actual_time decimal Durasi aktual Standar
started_at timestamp Waktu mulai aktual (UTC) Standar
finished_at timestamp Waktu selesai aktual (UTC) Standar
status string Pending, In Progress, Completed Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.4.6 production_plans

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
plan_code string Nomor plan Standar
plan_date date Tanggal plan Standar
plan_horizon integer Horizon perencanaan Standar
status string Draft, Released, Completed, Cancelled Standar
company_id UUID Relasi ke companies Standar
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.4.7 production_plan_items

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
production_plan_id UUID Relasi ke production_plans Standar
item_id UUID Relasi ke items Standar
source_type string Sales Order, Material Request, Forecast Standar
source_id UUID ID dokumen sumber Standar
qty_demand decimal Kebutuhan Standar
qty_planned decimal Jumlah direncanakan Standar
qty_produced decimal Jumlah realisasi Standar
required_date date Tanggal dibutuhkan Standar
status string Pending, Released, Completed, Cancelled Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.5 Sales

4.5.1 Core Sales Tables

4.5.1.1 sales_orders

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
so_code string Nomor SO Standar
customer_id UUID Relasi ke customers (Channel) Standar (diadaptasi)
sales_person_id UUID Relasi ke sales_persons — PIC saat transaksi dibuat ➕ v1.5
posting_date date Tanggal rekap Standar
total_gross decimal Total sebelum diskon — Rupiah bulat Standar
total_fee_amount decimal Total semua biaya (fee, komisi, diskon) ➕ v1.5
net_total_after_fee decimal Gross revenue - total_fee_amount — Rupiah bulat ➕ v1.5
reseller_name string BMP Custom: Nama reseller (dari Google Sheets) ➕ v1.5
channel_order_ref string BMP Custom: Referensi order dari marketplace ➕ v1.5
dedup_hash string Hash unik (customer_id + item_id + posting_date) untuk pencegahan double import CSV ➕ v1.6
status string Draft, Submitted, Partially Delivered, Completed, Cancelled Standar
company_id UUID Relasi ke companies Standar
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

Catatan Dedup Key:

4.5.1.2 sales_order_items

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
sales_order_id UUID Relasi ke sales_orders Standar
item_id UUID Relasi ke items Standar
batch_id UUID BMP Custom: Batch yang dijual (traceability CPKB) ➕ v1.5
qty decimal Jumlah Standar
price decimal Harga per unit — s.d. 4 desimal Standar
discount decimal Diskon per baris Standar
total decimal Total per baris — Rupiah bulat Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.5.1.3 delivery_notes

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
dn_code string Nomor DN Standar
sales_order_id UUID Relasi ke sales_orders Standar
customer_id UUID Relasi ke customers Standar
posting_date date Tanggal kirim Standar
posting_time time Waktu kirim (UTC) Standar
total_qty decimal Standar
total_amount decimal Rupiah bulat Standar
status string Draft, Submitted, Completed, Cancelled Standar
is_return boolean True = retur customer Standar
return_reason_code string Wajib bila is_return = true — "Wangi tidak cocok" / "Botol bocor" / "Lainnya" v1.6
company_id UUID Relasi ke companies Standar
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.5.1.4 delivery_note_items

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
delivery_note_id UUID Relasi ke delivery_notes Standar
sales_order_item_id UUID Relasi ke sales_order_items Standar
item_id UUID Relasi ke items Standar
batch_id UUID Relasi ke batches Standar
qty decimal Jumlah dikirim Standar
rate decimal Harga per unit — s.d. 4 desimal Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.5.1.5 sales_invoices

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
si_code string Nomor SI Standar
sales_order_id UUID Relasi ke sales_orders Standar
delivery_note_id UUID Relasi ke delivery_notes Standar
customer_id UUID Relasi ke customers Standar
posting_date date Tanggal invoice Standar
total_gross decimal Total sebelum PPN — Rupiah bulat Standar
tax_amount decimal PPN 11-12% Standar
total_net decimal Total + PPN — Rupiah bulat Standar
status string Draft, Unpaid, Paid, Overdue, Cancelled Standar
is_return boolean True = credit note Standar
company_id UUID Relasi ke companies Standar
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.5.1.6 sales_invoice_items

Kolom Tipe Deskripsi Status
id UUID Primary key ➕ v1.5
sales_invoice_id UUID Relasi ke sales_invoices ➕ v1.5
item_id UUID Relasi ke items ➕ v1.5
batch_id UUID Relasi ke batches ➕ v1.5
qty decimal Jumlah ➕ v1.5
price decimal Harga per unit — s.d. 4 desimal ➕ v1.5
discount decimal Diskon per baris ➕ v1.5
total decimal Total per baris — Rupiah bulat ➕ v1.5
cogs decimal Cost of Goods Sold per baris (dienkripsi — Cloak Grup A) ➕ v1.5
gross_profit decimal total - cogs (dienkripsi — Cloak Grup A) ➕ v1.5
created_at timestamp UTC ➕ v1.5
updated_at timestamp UTC ➕ v1.5

4.5.2 Fee Components & Configuration

Filosofi Desain:

BMP menggunakan 1 Price List ("Harga Dasar — IDR"). Semua variasi harga (diskon, markup) ditangani oleh Fee Components.

Konsep Jumlah Fungsi Contoh
Price List 1 saja Menentukan harga jual dasar produk Bluegray = Rp 1.000.000
Komponen Biaya Banyak Mendefinisikan jenis biaya FEE-SHOPEE, COMMISSION
Konfigurasi Biaya Banyak Menentukan nilai biaya per customer dengan effective date Shopee = 3% + Rp 2.000

5.2.1 customer_fee_components (Master Jenis Biaya)

Catatan: Tabel ini adalah master data yang mendefinisikan jenis biaya. Tidak memiliki relasi langsung ke customers karena satu jenis biaya bisa digunakan oleh banyak customer (reusable).

Kolom Tipe Deskripsi Status
id UUID Primary key ➕ v1.5
component_code string Kode unik komponen (FEE-SHOPEE, COMMISSION) ➕ v1.5
component_name string Nama komponen (Fee Shopee, Komisi Reseller) ➕ v1.5
component_type string Fee Marketplace, Diskon, Komisi, Lainnya ➕ v1.5
account_id UUID Relasi ke accounts (akun beban di CoA) ➕ v1.5
is_active boolean ➕ v1.5
created_at timestamp UTC ➕ v1.5
updated_at timestamp UTC ➕ v1.5

5.2.2 customer_fee_configs (Konfigurasi Biaya per Customer)

Catatan: Tabel ini adalah pivot table yang menghubungkan Customer dengan Komponen Biaya dan menentukan nilai biaya yang berlaku untuk customer tersebut. Relasi langsung ke customers ada di sini.

Kolom Tipe Deskripsi Status
id UUID Primary key ➕ v1.5
customer_id UUID Relasi ke customers ➕ v1.5
fee_component_id UUID Relasi ke customer_fee_components ➕ v1.5
calculation_type string Percentage, Fixed, Percentage + Fixed ➕ v1.5
percentage_value decimal Nilai persentase ➕ v1.5
fixed_amount decimal Nilai tetap ➕ v1.5
minimum_amount decimal Minimum biaya (opsional) ➕ v1.5
maximum_amount decimal Maksimum biaya (opsional) ➕ v1.5
calculation_order integer Urutan perhitungan (1, 2, 3, ...) ➕ v1.5
effective_date_from date Tanggal mulai berlaku ➕ v1.5
effective_date_to date Tanggal berakhir berlaku (null = tanpa batas) ➕ v1.5
is_active boolean ➕ v1.5
created_at timestamp UTC ➕ v1.5
updated_at timestamp UTC ➕ v1.5

4.5.3 Fee Snapshots

Catatan: sales_order_fees adalah tabel snapshot yang menyimpan biaya aktual pada saat Sales Order dibuat. Ini menjaga histori tetap akurat meskipun ada perubahan fee di masa depan (prinsip immutability akuntansi).

4.5.3.1 sales_order_fees

Kolom Tipe Deskripsi Status
id UUID Primary key ➕ v1.5
sales_order_id UUID Relasi ke sales_orders ➕ v1.5
fee_component_id UUID Relasi ke customer_fee_components ➕ v1.5
calculation_type string Snapshot cara hitung ➕ v1.5
percentage_value decimal Snapshot persentase ➕ v1.5
fixed_amount decimal Snapshot nilai tetap ➕ v1.5
base_amount decimal Dasar perhitungan biaya ➕ v1.5
fee_amount decimal Jumlah biaya aktual — Rupiah bulat ➕ v1.5
description string Keterangan ➕ v1.5
created_at timestamp UTC ➕ v1.5
updated_at timestamp UTC ➕ v1.5

4.6 Accounting

4.6.1 Core Accounting

4.6.1.1 journal_entries

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
je_code string Nomor JE Standar
voucher_type string Journal Entry, Bank Entry, Cash Entry, Contra, Credit Note, Debit Note, Opening Entry Standar
posting_date date Tanggal posting Standar
total_debit decimal Total debit — Rupiah bulat Standar
total_credit decimal Total credit — Rupiah bulat Standar
is_opening boolean True = saldo awal Standar
company_id UUID Relasi ke companies Standar
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.6.1.2 journal_entry_items

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
journal_entry_id UUID Relasi ke journal_entries Standar
account_id UUID Relasi ke accounts Standar
debit decimal Rupiah bulat Standar
credit decimal Rupiah bulat Standar
party_type string Customer, Supplier, Employee Standar
party_id UUID ID party Standar
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.6.1.3 gl_entries (General Ledger) — Immutable

Kolom Tipe Deskripsi Status
id UUIDv7 Primary key — time-ordered (§4.11) v1.6
account_id UUID Relasi ke accounts Standar
debit decimal Rupiah bulat Standar
credit decimal Rupiah bulat Standar
party_type string Customer, Supplier Standar
party_id UUID ID party Standar
voucher_type string Journal Entry, Payment Entry, Sales Invoice, Purchase Invoice Standar
voucher_no string Nomor dokumen sumber Standar
posting_date date Tanggal posting Standar
is_opening boolean True = saldo awal Standar
company_id UUID Relasi ke companies Standar
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
created_at timestamp UTC Standar

4.6.1.4 payment_ledger_entries

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
party_type string Customer, Supplier Standar
party_id UUID ID party Standar
voucher_type string Sales Invoice, Purchase Invoice, Payment Entry Standar
voucher_no string Nomor dokumen Standar
invoice_amount decimal Nilai invoice — Rupiah bulat Standar
paid_amount decimal Sudah dibayar — Rupiah bulat Standar
outstanding_amount decimal Sisa tagihan — Rupiah bulat Standar
due_date date Jatuh tempo Standar
company_id UUID Relasi ke companies Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.6.1.5 payment_terms_templates — TABEL BARU (Hide)

Kolom Tipe Deskripsi Status
id UUID Primary key 👁️ v1.5
name string "Net 30", "Net 60" 👁️ v1.5
description text 👁️ v1.5
is_active boolean 👁️ v1.5
created_at timestamp UTC 👁️ v1.5
updated_at timestamp UTC 👁️ v1.5

4.6.1.6 payment_schedules — TABEL BARU (Hide)

Kolom Tipe Deskripsi Status
id UUID Primary key 👁️ v1.5
voucher_type string Sales Invoice, Purchase Invoice 👁️ v1.5
voucher_no string Nomor dokumen 👁️ v1.5
due_date date Tanggal jatuh tempo 👁️ v1.5
payment_amount decimal Jumlah termin — Rupiah bulat 👁️ v1.5
paid_amount decimal Sudah dibayar — Rupiah bulat 👁️ v1.5
outstanding_amount decimal Sisa termin — Rupiah bulat 👁️ v1.5
created_at timestamp UTC 👁️ v1.5
updated_at timestamp UTC 👁️ v1.5

4.6.2 Payment & Banking

4.6.2.1 modes_of_payment — TABEL BARU

Kolom Tipe Deskripsi Status
id UUID Primary key ➕ v1.5
name string "Transfer Bank", "Tunai", "QRIS" ➕ v1.5
type string Bank, Cash, Wallet ➕ v1.5
default_account_id UUID Relasi ke accounts (akun Bank/Cash default) ➕ v1.5
is_active boolean ➕ v1.5
company_id UUID Relasi ke companies ➕ v1.5
created_at timestamp UTC ➕ v1.5
updated_at timestamp UTC ➕ v1.5

4.6.2.2 payment_entries

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
pe_code string Nomor PE Standar
payment_type string Receive, Pay, Internal Transfer Standar
party_type string Customer, Supplier Standar
party_id UUID ID party Standar
mode_of_payment_id UUID Relasi ke modes_of_payment ➕ v1.5
posting_date date Tanggal settlement Standar
gross_amount decimal BMP Custom: Total dari marketplace — Rupiah bulat ➕ v1.5
net_amount decimal BMP Custom: Jumlah yang benar-benar diterima — Rupiah bulat ➕ v1.5
settlement_reconciliation_status string Pending / Reconciled / Discrepancy — status rekonsiliasi settlement marketplace v1.6
status string Draft, Submitted, Paid, Cancelled Standar
paid_from_account_id UUID Relasi ke accounts — akun sumber dana, snapshot saat submit ➕ v1.5
paid_to_account_id UUID Relasi ke accounts — akun tujuan dana, snapshot saat submit ➕ v1.5
company_id UUID Relasi ke companies Standar
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.6.2.3 payment_entry_deductions — TABEL BARU

Catatan: Pengganti JSONB deductions untuk memudahkan agregasi laporan.

Kolom Tipe Deskripsi Status
id UUID Primary key ➕ v1.5
payment_entry_id UUID Relasi ke payment_entries ➕ v1.5
fee_component_id UUID Relasi ke customer_fee_components ➕ v1.5
account_id UUID Relasi ke accounts ➕ v1.5
amount decimal Nilai deduksi — Rupiah bulat ➕ v1.5
description string Keterangan ➕ v1.5
created_at timestamp UTC ➕ v1.5
updated_at timestamp UTC ➕ v1.5

4.6.2.3a payment_entry_references — BARU

Kolom Tipe Deskripsi Status
id UUID Primary key ➕ v1.5
payment_entry_id UUID Relasi ke payment_entries ➕ v1.5
reference_type string Sales Invoice, Purchase Invoice, Journal Entry ➕ v1.5
reference_id UUID ID dokumen yang dialokasikan ➕ v1.5
allocated_amount decimal Jumlah alokasi — Rupiah bulat ➕ v1.5
created_at timestamp UTC ➕ v1.5
updated_at timestamp UTC ➕ v1.5

(payment_entry_deductions §6.2.3 tetap — kini satu-satunya rumah deduksi.)

4.6.2.4 banks — TABEL BARU (Hide)

Kolom Tipe Deskripsi Status
id UUID Primary key 👁️ v1.5
name string Nama bank 👁️ v1.5
swift_code string Kode SWIFT 👁️ v1.5
created_at timestamp UTC 👁️ v1.5
updated_at timestamp UTC 👁️ v1.5

6.2.5 bank_accounts — TABEL BARU (Hide)

Kolom Tipe Deskripsi Status
id UUID Primary key 👁️ v1.5
bank_id UUID Relasi ke banks 👁️ v1.5
account_number string Nomor rekening (Cloak Grup D) 👁️ v1.5
account_name string Nama pemilik 👁️ v1.5
account_id UUID Relasi ke accounts (GL) 👁️ v1.5
company_id UUID Relasi ke companies 👁️ v1.5
is_active boolean 👁️ v1.5
created_at timestamp UTC 👁️ v1.5
updated_at timestamp UTC 👁️ v1.5

4.6.2.6 bank_transactions — TABEL BARU (Hide)

Kolom Tipe Deskripsi Status
id UUID Primary key 👁️ v1.5
bank_account_id UUID Relasi ke bank_accounts 👁️ v1.5
transaction_date date Tanggal transaksi 👁️ v1.5
reference string Referensi bank 👁️ v1.5
description text Deskripsi 👁️ v1.5
deposit decimal Debit (masuk) — Rupiah bulat 👁️ v1.5
withdrawal decimal Kredit (keluar) — Rupiah bulat 👁️ v1.5
is_reconciled boolean 👁️ v1.5
payment_entry_id UUID Relasi ke payment_entries 👁️ v1.5
created_at timestamp UTC 👁️ v1.5
updated_at timestamp UTC 👁️ v1.5

4.6.3 Tax & Withholding

4.6.3.1 taxes_templates (Sales & Purchase)

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
name string "PPN 11%" Standar
type string Sales, Purchase Standar
company_id UUID Relasi ke companies Standar
is_active boolean Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.6.3.2 tax_template_items

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
tax_template_id UUID Relasi ke taxes_templates Standar
account_id UUID Relasi ke accounts Standar
rate decimal Persentase pajak Standar
charge_type string On Net Total, On Previous Row, Actual Standar
is_inclusive boolean True = harga sudah termasuk pajak Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.6.3.3 tax_withholding_categories

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
name string PPh 23 (Jasa) Standar
rate decimal Persentase potongan Standar
threshold_amount decimal Ambang batas kumulatif Standar
threshold_period string Monthly, Yearly Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.6.3.4 tax_withholding_entries — TABEL BARU

Kolom Tipe Deskripsi Status
id UUID Primary key ➕ v1.5
purchase_invoice_id UUID Relasi ke purchase_invoices ➕ v1.5
supplier_id UUID Relasi ke suppliers ➕ v1.5
withholding_category_id UUID Relasi ke tax_withholding_categories ➕ v1.5
taxable_amount decimal Jumlah kena pajak — Rupiah bulat ➕ v1.5
withholding_amount decimal Jumlah potongan — Rupiah bulat ➕ v1.5
period_from date Periode mulai ➕ v1.5
period_to date Periode akhir ➕ v1.5
created_at timestamp UTC ➕ v1.5
updated_at timestamp UTC ➕ v1.5

4.6.4 Budget & Period

4.6.4.1 budgets — TABEL BARU (Hide)

Kolom Tipe Deskripsi Status
id UUID Primary key 👁️ v1.5
name string "Budget 2026" 👁️ v1.5
budget_against string Cost Center, Project 👁️ v1.5
fiscal_year_id UUID Relasi ke fiscal_years 👁️ v1.5
company_id UUID Relasi ke companies 👁️ v1.5
status string Draft, Submitted, Completed 👁️ v1.5
created_by UUID User ID 👁️ v1.5
created_at timestamp UTC 👁️ v1.5
updated_at timestamp UTC 👁️ v1.5

6.4.2 budget_items — TABEL BARU (Hide)

Kolom Tipe Deskripsi Status
id UUID Primary key 👁️ v1.5
budget_id UUID Relasi ke budgets 👁️ v1.5
account_id UUID Relasi ke accounts 👁️ v1.5
cost_center_id UUID Relasi ke cost_centers 👁️ v1.5
budget_amount decimal Jumlah anggaran — Rupiah bulat 👁️ v1.5
spent_amount decimal Realisasi — Rupiah bulat 👁️ v1.5
created_at timestamp UTC 👁️ v1.5
updated_at timestamp UTC 👁️ v1.5

6.4.3 monthly_distributions — TABEL BARU (Hide)

Kolom Tipe Deskripsi Status
id UUID Primary key 👁️ v1.5
budget_id UUID Relasi ke budgets 👁️ v1.5
month integer 1-12 👁️ v1.5
percentage decimal Persentase alokasi (total 100%) 👁️ v1.5
created_at timestamp UTC 👁️ v1.5
updated_at timestamp UTC 👁️ v1.5

4.7 Quality

4.7.1 quality_inspections

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
inspection_code string Nomor inspeksi Standar
item_id UUID Relasi ke items Standar
batch_id UUID Relasi ke batches Standar
template_id UUID Relasi ke quality_templates Standar
reference_type string Purchase Receipt, Stock Entry Manufacture Standar
reference_id UUID ID dokumen sumber Standar
inspected_by UUID User ID Standar
inspection_date date Tanggal inspeksi Standar
status string Pending, Accepted, Rejected Standar
result JSONB Hasil per parameter Standar
notes text Catatan tambahan Standar
company_id UUID Relasi ke companies Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.7.2 quality_templates

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
name string Template A: RM Cairan, Template B: Packaging, Template C: FG Standar
item_type string RM, Packaging, FG Standar
parameters JSONB Daftar parameter uji Standar
is_active boolean Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.7.3 non_conformances

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
nc_code string Nomor NC Standar
source_type string Manufacturing, Sales Return, Quality Inspection Standar
source_id UUID ID dokumen sumber Standar
batch_id UUID Relasi ke batches Standar
subject string Subjek deviasi Standar
description text Kronologi Standar
corrective_action text Tindakan kompensasi Standar
preventive_action text Tindakan pencegahan Standar
status string Open, In Progress, Closed Standar
company_id UUID Relasi ke companies Standar
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.8 Asset

4.8.1 asset_categories

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
name string Mesin Produksi, Kendaraan, Peralatan Kantor Standar
depreciation_method string Straight Line, Written Down Value Standar
useful_life_years integer Umur ekonomis Standar
salvage_value decimal Nilai sisa Standar
frequency string Monthly, Quarterly, Yearly Standar
fixed_asset_account_id UUID Relasi ke accounts Standar
accumulated_depreciation_account_id UUID Relasi ke accounts Standar
depreciation_expense_account_id UUID Relasi ke accounts Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.8.2 assets

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
asset_name string Nama aset Standar
asset_category_id UUID Relasi ke asset_categories Standar
location string Lokasi fisik Standar
custodian string Penanggung jawab Standar
gross_purchase_amount decimal Nilai perolehan — Rupiah bulat Standar
purchase_date date Tanggal beli Standar
available_for_use_date date Tanggal mulai dipakai Standar
status string Draft, In Use, Sold, Scrapped, Cancelled Standar
company_id UUID Relasi ke companies Standar
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.8.3 asset_depreciation_schedules

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
asset_id UUID Relasi ke assets Standar
depreciation_date date Tanggal depresiasi Standar
depreciation_amount decimal Nilai depresiasi — Rupiah bulat Standar
accumulated_depreciation decimal Akumulasi — Rupiah bulat Standar
book_value decimal Nilai buku — Rupiah bulat Standar
is_posted boolean True = sudah di-JE Standar
journal_entry_id UUID Relasi ke journal_entries Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.8.4 asset_movements

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
movement_code string Nomor movement Standar
asset_id UUID Relasi ke assets Standar
from_location string Lokasi asal Standar
to_location string Lokasi tujuan Standar
from_custodian string Penanggung jawab asal Standar
to_custodian string Penanggung jawab tujuan Standar
movement_date date Tanggal pindah Standar
status string Draft, Submitted, Completed Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.8.5 asset_repairs

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
repair_code string Nomor repair Standar
asset_id UUID Relasi ke assets Standar
failure_date date Tanggal rusak Standar
description text Deskripsi kerusakan Standar
repair_cost decimal Biaya perbaikan — Rupiah bulat Standar
capitalize boolean True = tambah nilai aset Standar
stock_items JSONB Spare part yang dikonsumsi Standar
status string Pending, Completed, Cancelled Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.8.6 asset_maintenances

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
asset_id UUID Relasi ke assets Standar
maintenance_type string Preventive, Calibration Standar
periodicity string Daily, Weekly, Monthly, Quarterly, Yearly Standar
assign_to string Penanggung jawab Standar
next_due_date date Jadwal berikutnya Standar
is_active boolean Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.8.7 asset_maintenance_logs

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
asset_maintenance_id UUID Relasi ke asset_maintenances Standar
asset_id UUID Relasi ke assets Standar
done_by string Pelaksana Standar
done_date date Tanggal eksekusi Standar
notes text Catatan Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.9 HR (Minimalis)

4.9.1 employees

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
employee_code string NIK karyawan Standar
first_name string Nama depan Standar
last_name string Nama belakang Standar
email string Email (dienkripsi — Cloak Grup C) Standar
phone string No HP (dienkripsi — Cloak Grup C) Standar
bank_account string Rekening bank (dienkripsi — Cloak Grup C) Standar
bank_name string Nama bank (dienkripsi — Cloak Grup C) Standar
tax_id string NPWP (dienkripsi — Cloak Grup C) Standar
address text Alamat (dienkripsi — Cloak Grup C) Standar
date_of_joining date Tanggal masuk Standar
date_of_leaving date Tanggal keluar Standar
department_id UUID Relasi ke departments (§1.19) 🔧 v1.7
status string Active, Left Standar
user_id UUID Relasi ke users Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.9.2 attendances

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
employee_id UUID Relasi ke employees Standar
attendance_date date Tanggal Standar
status string Present, Absent, Half Day, On Leave, Work From Home Standar
leave_application_id UUID Relasi ke leave_applications Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.9.3 leave_types

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
name string Cuti Tahunan, Cuti Sakit, Cuti Melahirkan Standar
is_paid boolean True = dibayar Standar
encashable boolean True = bisa dicairkan Standar
max_days integer Maksimum hari Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.9.4 leave_allocations

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
employee_id UUID Relasi ke employees Standar
leave_type_id UUID Relasi ke leave_types Standar
allocation_date date Tanggal jatah Standar
total_days integer Total jatah Standar
used_days integer Sudah dipakai Standar
remaining_days integer Sisa Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.9.5 leave_applications

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
employee_id UUID Relasi ke employees Standar
leave_type_id UUID Relasi ke leave_types Standar
from_date date Tanggal mulai Standar
to_date date Tanggal selesai Standar
total_days integer Jumlah hari Standar
reason text Alasan Standar
status string Pending, Approved, Rejected, Cancelled Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.9.6 salary_structures

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
name string Struktur gaji Standar
employee_id UUID Relasi ke employees Standar
effective_date date Tanggal berlaku Standar
is_active boolean Standar
components JSONB Daftar komponen gaji (dienkripsi — Cloak Grup C) Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.9.7 payroll_entries

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
pe_code string Nomor payroll Standar
from_date date Awal periode Standar
to_date date Akhir periode Standar
total_gross decimal Total gaji kotor — Rupiah bulat Standar
total_net decimal Total gaji bersih — Rupiah bulat Standar
status string Draft, Submitted, Paid, Cancelled Standar
journal_entry_id UUID Relasi ke journal_entries Standar
company_id UUID Relasi ke companies Standar
cost_center_id UUID Relasi ke cost_centers ➕ v1.5
created_by UUID User ID Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.9.8 salary_slips

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
payroll_entry_id UUID Relasi ke payroll_entries Standar
employee_id UUID Relasi ke employees Standar
gross_pay decimal Gaji kotor — Rupiah bulat Standar
total_deduction decimal Total potongan — Rupiah bulat Standar
net_pay decimal Gaji bersih — Rupiah bulat Standar
components JSONB Detail komponen (dienkripsi — Cloak Grup C) Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

4.10 System & Framework

4.10.1 users

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
email string Email unik Standar
password_hash string Hash password Standar
first_name string Standar
last_name string Standar
is_active boolean Standar
last_login timestamp UTC Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

Catatan v1.7: kolom roles (JSONB) dihapus — daftar role sebuah user sekarang lewat relasi user_roles (§10.9), bukan array di dalam baris users.

4.10.2 permissions

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
role_id UUID Relasi ke roles (§10.8) 🔧 v1.7
resource_type string Item, SalesOrder, dll. Standar
action string Read, Write, Create, Delete, Submit, Cancel, Export Standar
conditions JSONB Filter tambahan (mis. employee_id = current_user) Standar
created_at timestamp UTC Standar
updated_at timestamp UTC Standar

Catatan v1.7: role (string bebas) diganti role_id (FK ke roles) supaya nama role konsisten & tidak typo-prone.

Catatan v1.6: permission_level dihapus (v1.5). Kerahasiaan data = Cloak (enkripsi at-rest untuk field finansial/PII/resep); kewenangan aksi = Ash.Policy.Authorizer (role x resource x action, tabel ini); visibilitas menu/UI = konfigurasi workspace (Chapter 5). Tiga lapis, tidak saling tumpang tindih.

4.10.3 access_logs

Kolom Tipe Deskripsi Status
id UUID Primary key Standar
user_id UUID Relasi ke users Standar
action string Login, View, Create, Update, Delete, Export Standar
resource_type string Item, SalesOrder, dll. Standar
resource_id UUID ID resource Standar
ip_address string IP client Standar
metadata JSONB Filter atau parameter Standar
created_at timestamp UTC Standar

4.10.4 modules ➕ v1.6 (BARU)

Catatan: Tabel untuk module enablement per instalasi (§5.6.5). Project/R&D = 👁️ default hide.

Kolom Tipe Deskripsi Status
id UUID Primary key v1.6
module_code string Kode modul (Accounting, Sales, Purchase, Manufacturing, Inventory, Quality, Asset, HR, Project) v1.6
module_name string Nama modul v1.6
is_enabled boolean True = aktif v1.6
is_hidden boolean True = hide dari UI (bisa dinyalakan) v1.6
display_order integer Urutan tampilan v1.6
created_at timestamp UTC v1.6
updated_at timestamp UTC v1.6

4.10.5 workspaces ➕ v1.6 (BARU)

Catatan: Tabel untuk workspace configuration (§5.2). 12 workspace = 9 bisnis + 3 settings.

Kolom Tipe Deskripsi Status
id UUID Primary key v1.6
workspace_code string Kode workspace v1.6
workspace_name string Nama workspace v1.6
module_id UUID Relasi ke modules v1.6
is_default boolean True = default untuk role tertentu v1.6
allowed_roles JSONB Daftar role yang boleh akses v1.6
display_order integer Urutan tampilan v1.6
created_at timestamp UTC v1.6
updated_at timestamp UTC v1.6

4.10.6 settings ➕ v1.6 (BARU)

Catatan: Tabel untuk policy global per instalasi (§5.6.3). Scalar instalasi (seri nomor, locale) di sini.

Kolom Tipe Deskripsi Status
id UUID Primary key v1.6
setting_key string Kunci unik (mis. "frozen_date", "default_valuation_method") v1.6
setting_value string Nilai v1.6
setting_type string Tipe data (string, integer, boolean, decimal, date) v1.6
module_code string Modul pemilik (Accounting, Inventory, Sales, dll.) v1.6
description text Penjelasan efek setting v1.6
is_editable boolean True = bisa diubah via Settings Hub v1.6
company_id UUID Relasi ke companies (nullable = global) v1.6
created_at timestamp UTC v1.6
updated_at timestamp UTC v1.6

4.10.7 schema_metadata ➕ v1.6 (BARU)

Catatan: Metadata untuk sync-agent (§4.5). Setiap migrasi menaikkan schema_version.

Kolom Tipe Deskripsi Status
id UUID Primary key v1.6
schema_version integer Versi schema saat ini v1.6
last_migration_at timestamp Waktu migrasi terakhir (UTC) v1.6
migration_name string Nama file migrasi terakhir v1.6
created_at timestamp UTC v1.6
updated_at timestamp UTC v1.6

4.10.8 roles ➕ v1.7 (BARU)

Catatan: Master role — set tetap & kecil (Owner, Finance, Production, Warehouse, Sales, IT, QA, dll.), bukan role bebas-tambah oleh user biasa (perubahan role = perubahan kode/deploy, bukan input UI).

Kolom Tipe Deskripsi Status
id UUID Primary key v1.7
role_code string Kode role (mis. "OWNER", "IT_MANAGER") v1.7
role_name string Nama role v1.7
is_active boolean v1.7
created_at timestamp UTC v1.7
updated_at timestamp UTC v1.7

4.10.9 user_roles ➕ v1.7 (BARU)

Catatan: Junction many-to-many, pengganti users.roles (JSONB). 1 user bisa punya lebih dari 1 role (mis. Manager IT yang juga Super Admin).

Kolom Tipe Deskripsi Status
id UUID Primary key v1.7
user_id UUID Relasi ke users v1.7
role_id UUID Relasi ke roles v1.7
created_at timestamp UTC v1.7

Unique constraint: (user_id, role_id) — 1 user tidak boleh punya role yang sama dobel.

4.11 Relasi Antar Tabel (Tree Structure)

Company
├── Account (CoA)
│ ├── Akun Beban (Marketplace Fee, Komisi, Diskon, Promosi, Round Off)
│ └── Akun Pajak (PPN, PPh)
├── FiscalYear
├── AccountingPeriod
├── CostCenter
├── Budget
├── NumberingSeries
├── Item
│ ├── ItemGroup
│ ├── UOM
│ ├── PriceList (HANYA 1)
│ ├── Batch
│ └── BOM
├── Customer (Channel)
│ ├── CustomerGroup
│ ├── CustomerFeeConfig (relasi langsung ke customer)
│ │ └── CustomerFeeComponent (master, tidak langsung ke customer)
│ └── SalesOrder
│ ├── SalesOrderItem
│ │ └── DeliveryNoteItem
│ ├── SalesOrderFee (via SO, snapshot)
│ │ └── CustomerFeeComponent (master, snapshot)
│ ├── DeliveryNote
│ │ └── DeliveryNoteItem
│ └── SalesInvoice
│ └── SalesInvoiceItem
├── Supplier
│ ├── SupplierGroup
│ ├── TaxWithholdingCategory
│ └── PurchaseOrder
│ ├── PurchaseOrderItem
│ └── PurchaseReceipt
│ ├── PurchaseReceiptItem
│ │ └── QualityInspection
│ └── PurchaseInvoice
│ ├── PurchaseInvoiceItem
│ └── TaxWithholdingEntry
├── Warehouse
├── Asset
│ ├── AssetCategory
│ ├── AssetDepreciationSchedule
│ └── AssetMovement
├── Employee
│ ├── Department (department_id) ➕ v1.7
│ ├── SalesPerson (employee_id)
│ ├── Attendance
│ ├── LeaveAllocation
│ ├── LeaveApplication
│ └── SalaryStructure
└── JournalEntry
├── JournalEntryItem
└── GLEntry \

User ➕ v1.7
└── UserRole (junction)
└── Role \

PaymentEntry
├── PaymentLedgerEntry
├── PaymentEntryReference (child table, pengganti JSONB references)
├── PaymentEntryDeduction (child table, pengganti JSONB)
└── ModeOfPayment \

Bank
└── BankAccount
└── BankTransaction
└── PaymentEntry \

TaxTemplate
└── TaxTemplateItem \

TaxWithholdingCategory
└── TaxWithholdingEntry \

StockReservation (baru v1.6)
└── Item + Warehouse + Batch \

Modules (baru v1.6)
└── Workspaces (baru v1.6)
└── Settings (baru v1.6) \

SchemaMetadata (baru v1.6)

4.12 Cloak Registry (Keamanan Tiga Lapis)

Lapis 1 — Cloak (at-rest encryption): Field dienkripsi di database. Saat serialize API, field grup Cloak dibuang bila role bukan pembaca — UI tidak pernah menerima nilainya, bukan sekadar disembunyikan.

Lapis 2 — Ash.Policy.Authorizer: Kewenangan aksi (role x resource x action). Matriks di §5.4 Chapter 5.

Lapis 3 — Workspace Config: Visibilitas menu/UI. Hide UI ≠ security.

4.12.1 Grup Cloak

Grup Field Pembaca Runtime
A — Margin/COGS sales_invoice_items.cogs, sales_invoice_items.gross_profit, stock_ledger_entries.valuation_rate, bins.valuation_rate Owner/Finance
B — Resep boms.twist_notes, boms.formulation_code, bom_items.qty, bom_items.rate Owner, Prod/R&D
C — Payroll/PII employees.{email,phone,bank_account,bank_name,tax_id,address}, salary_structures.components, salary_slips.* Owner/Finance
D — Bank bank_accounts.account_number, bank_transactions.* Owner/Finance
E — Rahasia Entitas companies.tax_id, companies.bpom_license, batches.supplier_drum_code, purchase_receipt_items.supplier_drum_code tax_id: Owner/Finance · bpom_license: +BPOM/QA · drum_code: Owner, Prod/R&D, Wh/Purch

4.12.2 Aturan Penegakan (Konvensi Implementasi)

  1. Cloak dua pintu: terenkripsi at-rest untuk semua; saat serialize API, field grup Cloak dibuang bila role bukan pembaca — UI tidak pernah menerima nilainya, bukan sekadar disembunyikan.
  2. Log sanitizer: field ter-Cloak (margin/COGS, payroll, bank, resep, PII) tidak pernah muncul di log/telemetry/Sentry (§4.12).
  3. Export = aksi terpisah. Tanpa Export, tombol xlsx/csv tidak render dan endpoint diblokir; export role R• men-strip kolom yang ditolak.
  4. Konfigurasi workspace/flyout hanya presentasi. Setiap route & LiveView tetap dijaga policy — deep-link tidak menembus apa pun (hide ≠ security).

4.13 Catatan Implementasi

Aspek Catatan
Primary Key Semua tabel menggunakan UUID sebagai primary key. Tabel ledger (SLE, GL) menggunakan UUIDv7 untuk time-ordering dan lokalitas index SQLite (§4.11).
Timestamp Semua tabel memiliki created_at dan updated_at (kecuali SLE/GL yang immutabel). Waktu disimpan UTC; UI render Asia/Jakarta (§4.10).
Soft Delete Tidak ada soft delete. Gunakan Ash.Archival untuk soft delete/archive. Data keuangan wajib disimpan 10 tahun.
JSONB Digunakan untuk data dinamis: components, parameters, result, additional_costs, conditions, metadata. Tidak lagi untuk references, deductions (dipindah ke child table), maupun roles (dipindah ke user_roles, v1.7).
Tree accounts, item_groups, customer_groups, supplier_groups, warehouses, cost_centers menggunakan parent_id + nested set (lft/rgt) untuk query cepat (v1.6).
Batch Traceability batches memiliki parent_batch_id untuk inheritance FG ← Bulk.
Price List HANYA 1 Price List ("Harga Dasar — IDR"). Semua variasi harga ditangani oleh Fee Components.
Sales Person sales_persons.employee_id → employees; snapshot per transaksi di sales_orders.sales_person_id.
Departments (v1.7) Master departments (§1.19), 8 departemen tetap, semua lapor langsung ke Direktur (tanpa parent_id/tree). employees.department_id → departments (sebelumnya string bebas).
Roles (v1.7) Master roles (§10.8) + junction user_roles (§10.9) menggantikan users.roles (JSONB). Role adalah set tetap & kecil, ditambah lewat kode/deploy, bukan lewat UI. permissions.role_id → roles (sebelumnya string bebas).
Dedup Key (Sales Order) (customer_id, item_id, posting_date) — cukup untuk mencegah double import CSV. v1.6: ditambahkan dedup_hash SHA-256.
Fee Components Master jenis biaya (customer_fee_components) TIDAK punya relasi ke customer. Konfigurasi per customer (customer_fee_configs) yang punya relasi ke customer.
Effective Date customer_fee_configs memiliki effective_date_from dan effective_date_to untuk menangani perubahan fee.
Snapshot sales_order_fees menyimpan snapshot fee saat transaksi terjadi (prinsip immutability akuntansi).
Akun Snapshot Pembayaran paid_from/paid_to_account_id di payment_entries = snapshot akun saat submit; rekonsiliasi bank memfilter PE via kolom ini. modes_of_payment.default_account_id tetap ada sebagai resolver saat submit.
Cloak (Enkripsi) Field rate, amount, twist_notes, dan field finansial/PII dienkripsi at-rest dengan Cloak (menggantikan Chinese Wall). Akses server oleh IT dibatasi lewat pemisahan tanggung jawab organisasi (staf IT per perangkat, hanya Manager IT pegang Server App).
Security Layers Cloak (at-rest) + Ash.Policy.Authorizer (role_id x resource x action) + konfigurasi workspace (UI); permission_level tidak dipakai.
Akun Beban Wajib Beban Marketplace Fee, Beban Komisi Reseller, Beban Diskon Penjualan, Beban Promosi, Beban Pembulatan harus ada di CoA.
Derived State bins = materialized view rebuildable dari SLE. stock_reservations = tabel baru v1.6 untuk projected_qty.
Referensi USD reference_usd_rate dan reference_usd_price di Purchase untuk memisahkan selisih kurs vs kenaikan harga.
Child Table Deductions payment_entry_deductions sebagai pengganti JSONB deductions untuk memudahkan agregasi di laporan. payment_entry_references sebagai pengganti JSONB references.
Archival Gunakan Ash.Archival untuk memenuhi kewajiban penyimpanan data keuangan 10 tahun.
Oban background_jobs dihapus dari spec. Oban membawa tabelnya sendiri.
Uang & Presisi AshMoney untuk uang. Tampilan & total dokumen = Rupiah bulat (tanpa desimal). Rate satuan internal s.d. 4 desimal. conversion_factor = 9 desimal. Selisih pembulatan → akun Round Off (§4.10).
Bahasa Satu bahasa English untuk sistem: kode, identifier, komentar, string UI, istilah teknis & proper noun. Bahasa Indonesia hanya untuk prosa naratif dan output fiskal (§4.9, §4.13).
Numbering Series Terdefinisi kode per instalasi (tanpa DDL runtime). Dokumen cancel menyimpan nomornya (tidak dipakai ulang) (§4.11).
Event Payload Payload event membawa ID + fakta immutabel minimal; konsumen yang butuh data lengkap membaca via public action (tanpa salinan basi di payload) (§4.11).
Async Jobs Semua job idempoten; job membawa ID dan membaca data terkini saat run; scheduled job terdaftar di satu manifest dan tampil di System Console (§4.12).
Sync-Agent Schema version wajib — setiap migrasi menaikkan schema_version di schema_metadata. Sync-agent menolak koneksi bila versi node tidak cocok. Urutan upgrade: server kantor dulu, baru laptop (§4.5).
CHECK Constraint Business rule kritikal sebagai CHECK constraint SQLite (qty >= 0, amount > 0) — last line of defense replikasi (Invarian #6).
Pragma SQLite journal_mode=WAL; busy_timeout=5000 (wajib per node).
Module Enablement Modul togglable per instalasi via tabel modules. Project/R&D = 👁️ default hide (§5.6.5).
Settings Hub Satu permukaan untuk semua policy. Policy = scalar di tabel settings; master data tetap di modul pemiliknya (§5.6).
Return Reason Code Delivery Note retur wajib isi reason_code: "Wangi tidak cocok" (subjektif, CS) / "Botol bocor" (objektif, trigger QC) / "Lainnya" (§BMP Reason #5).
Settlement Reconciliation payment_entries.settlement_reconciliation_status = Pending / Reconciled / Discrepancy untuk tracking settlement marketplace (v1.6).

Changelog v1.7 (vs v1.6)

Penambahan Tabel Baru

Perubahan Tipe Data / Refactor Struktural

Catatan

Kedua refactor ini sudah disetujui sejak review Cross-Check Data Model v1.5, tapi sempat terlewat waktu penyusunan v1.6. v1.7 menuntaskan keduanya — tidak ada perubahan keputusan lain di luar ini.

Changelog v1.6 (vs v1.5)

Penambahan Tabel Baru

Penambahan Kolom Baru

Perubahan Tipe Data

Perubahan Konvensi

Penambahan Akun Beban

Penghapusan

Chapter 5: Development Conventions

Chapter ini mendefinisikan aturan implementasi yang wajib diikuti ketika keputusan Blueprint (Chapter 1–4) diterjemahkan menjadi kode. Architecture (Chapter 3) menjawab bagaimana sistem bekerja; Data Model (Chapter 4) menjawab bagaimana data terstruktur; chapter ini menjawab bagaimana kode itu sendiri harus ditulis, diorganisasikan, diuji, dan dipelihara. Semua konvensi di bawah dipaksakan lewat tooling (CI, generator, static analysis) sejauh mungkin — bukan sekadar dokumentasi yang harus diingat manual; yang tidak bisa dicek otomatis, ditulis sebagai doctest.

Status: 🔒 Mengikat

Catatan Merge (K + O + Q)

Chapter ini digabung dari tiga draf paralel — K (naming convention rinci + folder tree granular + boilerplate generator), O (draf paling lengkap dan sistematis: invariant + 41 subbab menyeluruh dari domain ownership sampai anti-pattern), dan Q ("mengikat" v2, berisi tambahan krusial yang tidak ada di K maupun O: Laporan Fiskal & Template Output Bahasa Indonesia). Beberapa keputusan diambil secara eksplisit, bukan dipilih diam-diam:

  1. Kerangka bab mengikuti O. O jauh lebih lengkap dan konsisten secara struktural (bernomor 5.0–5.41, tiap subbab merujuk balik ke invariant Architecture/Data Model) dibanding draf sebelumnya di memori kerja (yang baru berisi 4.1–4.4 lama). O dipakai sebagai kerangka utama; K dan Q disisipkan ke subbab yang relevan.
  2. Extension SDK — TIDAK dimasukkan. K (§5.8) dan Q (§4.7) sama-sama mengusulkan bab Extension SDK formal (plugin architecture, hook system, API contract bervensi). Ini bertentangan dengan keputusan yang sudah dikunci sebelumnya: Extension SDK dropped entirely — kebutuhan kustom cukup lewat dependency Elixir biasa, tanpa SDK formal. Chapter ini mempertahankan keputusan lama; bagian Extension SDK dari K/Q tidak diadopsi. 🚩 Perlu konfirmasi eksplisit dari pengguna bila keputusan ini ingin diubah.
  3. Target coverage % — TIDAK dijadikan gate. K (§5.4.1) mengusulkan target coverage eksplisit per lapis test (Unit ≥80%, Integration ≥60%, UI Component ≥70%). Ini bertentangan dengan keputusan yang sudah dikunci: tidak ada coverage % gate. Angka dari K dipertahankan sebagai arah kualitatif (bukan gate CI yang memblokir merge) di §5.23; ini juga sebuah flag, bukan keputusan diam-diam.
  4. Struktur folder — digabung, tidak konflik. K memberi rincian subfolder (components/core, layout/, navigation/, hooks/, storybook/) yang tidak ada di O; digabung ke §5.2 karena melengkapi, bukan bertentangan.
  5. Release & Versioning — diisi dari K. Ini adalah bagian yang sebelumnya masih kosong di memori kerja (direncanakan sebagai §5.5 lama); K memberi isi konkret (SemVer, changelog, asset fingerprinting) yang konsisten dengan keputusan Chapter 3/4 (single binary Burrito, nol CDN). Diadopsi sebagai §5.35.
  6. Laporan Fiskal & Template Output (§5.42) — diambil utuh dari Q. Ini satu-satunya bagian besar yang sama sekali tidak ada di K maupun O, dan sifatnya wajib secara hukum (UU KUP Pasal 28 ayat 2). Tetap dalam Bahasa Indonesia sesuai isinya sendiri (§5.1 mengizinkan output fiskal berbahasa Indonesia).
  7. Naming convention — digabung. Tabel bahasa/konteks di O (§5.1) dan tabel naming Elixir/Ash + UI Component + File Naming di K (§5.1.1–5.1.4) saling melengkapi; digabung jadi satu §5.1 yang lebih lengkap.

5.0 Development Invariants

Aturan berikut bersifat mengikat dan menjadi batas implementasi bagi seluruh chapter di bawah:

  1. Business logic tidak ditempatkan di Phoenix LiveView sebagai sumber kebenaran.
  2. Business logic berada di Ash Resource, Action, Policy, atau domain service yang memiliki alasan arsitektural yang jelas.
  3. Phoenix LiveView bertanggung jawab atas interaction state, rendering, navigation, dan orchestration UI.
  4. Query lintas domain menggunakan public action milik domain pemilik.
  5. Efek samping lintas domain menggunakan event.
  6. Transaksi lintas domain yang membutuhkan atomicity menggunakan Reactor.
  7. Async job harus idempotent.
  8. Sync-agent tidak pernah memanggil Ash Action.
  9. Constraint database tetap wajib meskipun validasi sudah dilakukan di Ash.
  10. Resource finansial dan audit tidak boleh mempunyai jalur penghapusan destruktif biasa.
  11. Derived state harus dapat dibangun kembali dari source data.
  12. Semua kode yang masuk repository harus melewati formatter, static analysis, dan test yang relevan.

5.1 Bahasa & Naming Convention

BMP menggunakan English sebagai bahasa sistem.

Area Bahasa
Module name, domain name, resource name English
Attribute name, action name, event name English
Database table, database column English
Error code, UI label English
Source-code comment English
Documentation narrative (chapter, BMP-Kenapa) Indonesia diperbolehkan
Output fiskal / laporan resmi Bahasa Indonesia (lihat §5.42)

Contoh: SalesOrder, SalesOrderLine, DeliveryNote, PaymentEntry, stock_reservations, settlement_reconciliation_status. Nama bisnis Indonesia tidak diterjemahkan secara literal apabila istilah English sudah menjadi terminologi standar ERP.

Larangan: tidak ada campuran bahasa dalam satu identifier — tanggal_posting ❌, posting_date ✅.

5.1.1 Elixir & Ash Naming

Elemen Konvensi Contoh
Module PascalCase, domain prefix Bmp.Sales.Order, Bmp.Accounting.JournalEntry
Function snake_case create_order/2, calculate_total/1
Private function snake_case, prefix _ _validate_dedup_hash/1
Atom / enum value snake_case :submitted, :pending_approval
Database table snake_case, plural sales_orders, journal_entries
Ash resource singular, PascalCase Bmp.Sales.Order → tabel sales_orders
Ash action snake_case, verb-first :create, :submit, :cancel, :approve
Ash calculation snake_case, noun :total_amount, :outstanding_balance
Ash policy snake_case, deskriptif :owner_can_read_all, :it_blocked_from_group_e

5.1.2 UI Component Naming (LiveView / HEEx)

Elemen Konvensi Contoh
Shared component module PascalCase, prefix BmpWeb. BmpWeb.Components.Button, BmpWeb.Components.Modal
Component function snake_case <.button>, <.modal>, <.badge>
Component variant atom <.button variant={:primary}>, <.button variant={:danger}>
Slot snake_case <:title>, <:footer>, <:action>
CSS class kebab-case, prefix bmp- bmp-btn-primary, bmp-kpi-card
Design token CSS custom property --color-accent, --radius-md

Larangan: tidak ada hardcode hex/px di template individual. Semua warna, ukuran, dan shadow harus melalui design token Chapter 1 — dipaksakan lewat Credo custom rule (§5.27).

5.1.3 File Naming

Tipe File Pola Contoh
Ash Resource {resource_name}.ex lib/bmp/sales/order.ex
Ash Domain {domain_name}_domain.ex lib/bmp/sales/sales_domain.ex
LiveView {page_name}_live.ex lib/bmp_web/live/sales/order_list_live.ex
LiveView template {page_name}_live.html.heex lib/bmp_web/live/sales/order_list_live.html.heex
Shared component {component_name}.ex lib/bmp_web/components/button.ex
Component template {component_name}.html.heex lib/bmp_web/components/button.html.heex
Test {module_name}_test.exs test/bmp/sales/order_test.exs
Migration {timestamp}_{description}.exs priv/repo/migrations/20260115120000_add_user_roles.exs

5.2 Project Structure & Folder Convention

Struktur kode mengikuti bounded context (Domain-Driven Design — satu Ash.Domain = satu bounded context), bukan mengikuti tipe file global.

lib/
├── bmp/
│ ├── application.ex # OTP Application
│ ├── repo.ex # AshSqlite.Repo
│ │
│ ├── sales/ # Bounded Context: Sales
│ │ ├── sales_domain.ex # Ash.Domain
│ │ ├── order.ex # Ash.Resource
│ │ ├── order_item.ex
│ │ ├── invoice.ex
│ │ ├── calculations/
│ │ │ └── total_amount.ex
│ │ └── policies/
│ │ └── order_policy.ex
│ │
│ ├── accounting/ # Bounded Context: Accounting
│ ├── inventory/ # Bounded Context: Inventory
│ ├── manufacturing/ # Bounded Context: Manufacturing
│ ├── purchase/ # Bounded Context: Purchase
│ ├── quality/ # Bounded Context: Quality
│ ├── asset/ # Bounded Context: Asset
│ ├── hr/ # Bounded Context: HR
│ ├── project/ # Bounded Context: Project
│ ├── administration/ # Bounded Context: Administration
│ ├── organization/ # Bounded Context: Organization
│ ├── system/ # Bounded Context: System & Framework
│ │ ├── user.ex
│ │ ├── role.ex
│ │ ├── permission.ex
│ │ ├── module.ex
│ │ ├── workspace.ex
│ │ ├── setting.ex
│ │ └── cloak/
│ │ └── registry.ex
│ │
│ └── shared/ # Cross-domain utilities
│ ├── money_types.ex
│ └── validators.ex

├── bmp_web/
│ ├── components/ # Shared UI components (kontrak compile-time, Ch 1 & 2)
│ │ ├── core/ # button, badge, modal, toast, table, form, input
│ │ ├── layout/ # app.html.heex (3-zone: rail/head/kanvas), rail, page_head, breadcrumb
│ │ └── navigation/ # flyout, search, command_palette
│ │
│ ├── live/ # LiveView per domain — TIPIS, tanpa logika bisnis
│ │ ├── sales/
│ │ ├── accounting/
│ │ ├── inventory/
│ │ └── system/
│ │
│ ├── hooks/ # JS hooks untuk LiveView
│ │ ├── dirty_form_guard.js
│ │ ├── focus_visible.js
│ │ └── rail_collapse.js
│ │
│ └── storybook/ # PhoenixStorybook / katalog visual komponen

├── priv/
│ ├── repo/migrations/ # migrasi Ash + bump schema_version
│ ├── static/fonts/ # Inter .woff2 (bundle lokal)
│ ├── print_templates/ # template XML Tincture per dokumen
│ └── print_templates/fiscal/ # template fiskal Bahasa Indonesia (§5.42)

└── agent/ # crate Rust sync-agent — DI LUAR app Ash

Nama domain harus mencerminkan bounded context pada Architecture — Bmp.Sales, Bmp.Inventory, Bmp.Accounting. Struktur global seperti Bmp.Models / Bmp.Services / Bmp.Helpers / Bmp.BusinessLogic tidak digunakan karena akan mengaburkan ownership domain.

Aturan folder:

  1. Folder domain (lib/bmp/<domain>/) adalah satu-satunya rumah logika bisnis; bmp_web/ hanya presentasi — tidak boleh ada controllers/ (semua interaksi via LiveView atau public action Ash) maupun models/ (model hidup di domain).
  2. Komponen UI bersama wajib di components/core/ — tidak boleh duplikat per domain.
  3. Lintas domain hanya lewat public actions + events, tidak pernah import resource domain lain langsung (lihat §5.6).
  4. agent/ tidak boleh memanggil aksi Ash; mutasi replikasi aman dijaga di level constraint database (Invariant #9, §5.17).

5.3 Domain Ownership

Setiap Resource mempunyai satu domain owner.

Resource Owner
SalesOrder Sales
DeliveryNote Inventory / Delivery context
Item Inventory
Customer Sales
Supplier Purchase
Account Accounting
Employee HR
Company Organization

Domain owner bertanggung jawab atas: schema Resource, validation, public actions, authorization policy, state transition, event yang berasal dari Resource, dan test domain tersebut. Domain lain tidak boleh mengakses internal implementation Resource.

5.4 Resource Convention

Resource Ash harus mendefinisikan secara eksplisit: identity, attributes, relationships, validations, actions, policies, calculations/aggregates bila diperlukan, data layer, dan multitenancy bila berlaku.

defmodule Bmp.Sales.SalesOrder do
use Ash.Resource,
domain: Bmp.Sales,
data_layer: AshSqlite.DataLayer \

...
end

Resource tidak boleh menjadi tempat menaruh kode UI, dan tidak boleh mengetahui: LiveView socket, HEEx, browser event, CSS, route, atau Phoenix controller.

Boilerplate resource baru wajib memiliki (dihasilkan generator, §5.35): UUID primary key, timestamps, stub policy, stub doctest, dan migrasi yang menaikkan schema_version.

5.5 Action Convention

Action adalah public API domain. Nama action harus menggunakan kata kerja yang menjelaskan intent bisnis: create, update, submit, approve, reject, cancel, archive, reconcile, reserve, release, post, unpost.

Hindari action generik seperti process, execute, handle, do_it, update_status — jika intent bisnis sebenarnya dapat disebutkan secara eksplisit.

5.5.1 Generic CRUD

CRUD hanya digunakan apabila operasi tersebut memang merupakan operasi bisnis yang valid. Untuk dokumen transaksional, action bisnis lebih diutamakan (create_draft, submit, approve, reject, cancel) daripada update_status generik. Perubahan state harus merepresentasikan transisi bisnis yang valid.

5.6 Public Action vs Internal Implementation

Domain A

└── public action

Domain B

Domain B tidak boleh membaca tabel/domain A secara langsung untuk menghindari public API. Untuk kebutuhan data → public action → read. Untuk efek samping → event → consumer. Untuk atomic transaction lintas domain → Reactor → multiple public actions. Aturan ini mengikuti Architecture invariant bahwa baca lintas domain menggunakan public action dan efek samping lintas domain menggunakan event.

5.7 Event Convention

Event digunakan untuk menyampaikan fakta yang sudah terjadi, bukan perintah.

Benar: sales_order_submitted, payment_entry_posted, delivery_note_completed, checker_approved, checker_rejected.
Hindari: submit_sales_order, post_payment, complete_delivery.

Event harus menggunakan bentuk past-tense/fact.

5.7.1 Event Payload

Payload membawa: ID Resource, tenant/company identifier bila diperlukan, fakta immutable minimum, dan correlation/anchor identifier (§5.8) bila diperlukan. Payload tidak membawa seluruh object Resource — konsumen yang membutuhkan data lengkap harus membaca melalui public action.

%{
sales_order_id: order.id,
company_id: order.company_id,
anchor_id: order.anchor_id
}

5.8 Anchor ID

Setiap event atau transaksi yang memiliki dokumen penyerta wajib memiliki anchor_id — hash pendek dari entity_type + nomor dokumen dunia-nyata (invoice/production/surat-jalan) + tanggal event, dienforce lewat UNIQUE(anchor_id). anchor_id menjaga hubungan Document → Event → Ledger / Audit / Attachment, dan menjadi mekanisme deteksi konflik sync non-LWW (Architecture Invariant #11). anchor_id bukan pengganti primary key — primary key tetap digunakan untuk identitas Resource.

5.9 State Machine Convention

Status dokumen tidak boleh diubah secara bebas melalui field update apabila perubahan tersebut mempunyai konsekuensi bisnis. Gunakan AshStateMachine untuk state transition resmi.

DRAFT → SUBMITTED → APPROVED → POSTED → COMPLETED

Jalur invalid harus ditolak — misalnya COMPLETED → DRAFT tidak boleh tersedia hanya karena caller melakukan update pada attribute status.

5.10 Database Convention

BMP menggunakan SQLite sebagai satu-satunya RDBMS pada architecture saat ini.

5.11 UUID Convention

Default: UUID. Khusus ledger dan GL: UUIDv7, dipakai untuk Resource yang membutuhkan karakteristik ordering temporal yang lebih baik (lokalitas index SQLite). Jangan mengubah tipe primary key hanya untuk konsistensi kosmetik.

5.12 Money & Numeric Precision

Semua nilai uang menggunakan AshMoney. Currency BMP = IDR. Tampilan uang dan total dokumen = Rupiah bulat (tanpa desimal); rate/unit price internal dapat menyimpan hingga 4 desimal; conversion_factor (konversi UOM) hingga 9 desimal; selisih pembulatan masuk ke akun Round Off. Jangan menggunakan floating-point untuk nilai uang — gunakan Money/Decimal/AshMoney, bukan float/double.

5.13 Tree Data Convention

Resource yang memiliki struktur tree menggunakan parent_id, is_group, serta nested set lft/rgt untuk query tree. Berlaku untuk: Account, Cost Center, Item Group, Customer Group, Supplier Group, Warehouse.

5.14 Archive vs Delete

Untuk Resource finansial, audit, dan dokumen transaksional: Archive ≠ Delete. Archive adalah state transition (ACTIVE → ARCHIVED), bukan DELETE FROM .... Tidak boleh ada purge otomatis untuk data finansial/audit. AshPaperTrail mencatat perubahan; AshArchival menangani archival (retensi 10 tahun).

5.15 Async Job Convention

Semua Oban job harus idempotent. Job tidak menyimpan snapshot data bisnis sebagai payload — payload job hanya membawa identifier (%{document_id: id}), lalu saat dijalankan: load current data → validate current state → execute. Bukan menggunakan stale snapshot.

5.16 Reactor Convention

Reactor hanya digunakan apabila satu business operation melibatkan beberapa domain dan membutuhkan atomicity atau compensation (mis. Sales Order → Reserve Stock → Create Delivery → Create Ledger Entry). Jangan menggunakan Reactor untuk satu action sederhana, query biasa, operasi UI, atau job yang tidak membutuhkan orchestration.

5.17 Sync-Agent Boundary

Sync-agent adalah infrastructure terpisah: Sync-agent → SQLite changeset → SQLite, bukan Sync-agent → Ash Action. Sync-agent tidak mengetahui business workflow Ash; business invariant tetap dijaga oleh SQLite constraint. Schema version wajib diverifikasi sebelum changeset diterapkan.

5.18 Logging & Sensitive Data

Field yang dilindungi AshCloak (margin/COGS, payroll, bank, resep/formula, PII) tidak pernah muncul dalam application log, telemetry, Sentry, exception message, atau debug output. Log hanya boleh membawa identifier dan metadata operasional: sales_order_id, company_id, action, event, duration_ms, result — bukan tax_id, bpom_license, atau nilai terenkripsi.

5.19 LiveView Convention

LiveView adalah default UI layer BMP, bertanggung jawab atas rendering, user interaction, temporary UI state, navigation, loading state, optimistic/pending presentation, dan PubSub subscription. LiveView tidak menjadi tempat business rule.

User → LiveView → Ash Action → Domain → Event → PubSub → LiveView update

Bukan LiveView → Repo/SQL → Business calculation.

5.20 UI State vs Application Data

Jenis Storage
Business data SQLite
Ledger SQLite / AshDoubleEntry
Audit history SQLite / AshPaperTrail
Persistent UI preference SQLite user_settings
Saved view SQLite
Query cache, master-data cache, menu tree cache ETS
Temporary LiveView state LiveView process

ETS tidak digunakan sebagai source of truth — hanya cache volatile.

5.21 Query Convention

Query harus menggunakan Ash Query: filter sedekat mungkin dengan database (jangan ambil seluruh tabel lalu filter di Elixir), gunakan pagination untuk list, gunakan eager loading untuk relationship yang diketahui diperlukan, hindari N+1 query (maksimal 2 query per list view — lihat aturan test §5.23), dan index harus mengikuti query yang benar-benar digunakan. Pagination standar BMP: offset + limit + count (traditional numbered pager, sesuai Chapter 2).

5.22 Performance Convention

Operasi Target
Initial Landing < 500 ms
Ctrl+K first character < 100 ms
Ctrl+K full result < 300 ms
List page < 200 ms
Form/detail < 300 ms
Cached KPI refresh < 100 ms

Target berasal dari performance budget Chapter 3. Developer tidak boleh mengorbankan correctness hanya untuk memenuhi target ini; optimisasi dilakukan setelah bottleneck terukur.

5.23 Testing Strategy

Testing BMP dibagi menjadi piramida: Unit → Domain/Action → Integration → LiveView → End-to-End, dengan prioritas pada domain dan business rule, bukan sekadar coverage UI.

Level Tool Cakupan Arah kualitatif*
Unit ExUnit + Ash.Generator calculation, validation, pure function, business rule sederhana, parser/formatter tinggi
Resource/Action ExUnit + Ash.Generator create, update, state transition, policy, validation, constraint, event emission tinggi
Integration ExUnit + Bypass/Mox Reactor, Oban, PubSub, multi-domain workflow, database constraint, sync boundary menengah
LiveView Phoenix.LiveViewTest + Storybook navigation, interaction, form, loading, error presentation, permission-aware rendering menengah–tinggi
E2E Playwright (opsional) critical user journey (Login → My Attention → Create SO → Submit) 5–10 skenario

*Bukan gate CI yang memblokir merge (keputusan lama: tidak ada coverage % gate) — draf K sempat mengusulkan angka gate eksplisit (Unit ≥80%, Integration ≥60%, UI Component ≥70%); angka itu dipertahankan sebagai arah kualitatif saja, lihat Catatan Merge poin 3.

Aturan test Ash:

  1. Doctest wajib untuk semua public function — dokumentasi = test.
  2. Ash.Generator untuk data seed — tidak boleh ada fixture hardcode.
  3. Test policy terpisah — setiap policy Cloak harus punya test eksplisit (role X bisa/tidak bisa membaca field Y).
  4. Tidak ada N+1 — test query dengan Ash.Query.load/2 harus lolos inspect Ecto query (maksimal 2 query per list view).

Aturan test LiveView:

  1. Test component isolate — render komponen tanpa mount LiveView penuh untuk validasi struktur HTML.
  2. Test dirty-form guard — simulasi navigasi saat form belum disimpan, assert modal konfirmasi muncul.
  3. Test keyboard navigation — assert Tab order, Enter behavior, Esc behavior.
  4. Test responsive breakpoint — assert class CSS sesuai mode (desktop vs tablet).

Test data layer: satu data layer SQLite — tidak ada test matrix lintas database; WAL mode aktif di test environment; database per test (async: true) via Ecto.Adapters.SQL.Sandbox.

5.24 Testing Libraries

Library Fungsi
ExUnit Test framework
Doctest Executable documentation
Bypass HTTP boundary testing
Mox Behaviour mocking
StreamData Property-based testing
Oban.Testing Oban job testing
Ash.Generator Test data generation

5.25 Property-Based Testing

StreamData digunakan terutama untuk invariant yang mempunyai banyak kombinasi input: qty >= 0, amount > 0, conversion factor > 0, numbering sequence monotonic, Σdebit = Σcredit, Neraca seimbang (TOTAL ASET = TOTAL LIABILITAS + EKUITAS). Property test harus menguji invariant, bukan sekadar contoh input tertentu.

5.26 Mocking Convention

Mox hanya digunakan pada boundary yang benar-benar perlu diisolasi. Jangan melakukan mock terhadap Resource internal, function sederhana, atau database hanya agar test terlihat cepat. Preferensi: real domain logic + controlled external boundary. Mock digunakan terutama untuk external system atau behaviour boundary.

5.27 Static Analysis

Setiap perubahan kode harus melewati:

mix format --check-formatted
mix credo --strict
mix dialyzer
mix test

Dialyzer/Dialyxir menemukan type inconsistency dan unreachable/error-prone path — wajib bersih di boundary public API antar domain (bukan Extension SDK — lihat Catatan Merge poin 2), opsional/tidak memblokir berlebihan di internal domain. Credo menjaga consistency dan code quality, termasuk custom rule untuk compliance Design System (larangan hardcode hex/px, wajib pakai design token — §5.1.2).

5.28 Documentation Convention

Dokumentasi kode harus menjelaskan why, bukan mengulang what.

Tidak perlu:

# Increment current number by one
current = current + 1

Lebih berguna:

# Numbering is monotonic because cancelled documents
# must never reuse a previously issued number.

5.29 Error Convention

Error harus dapat dibedakan menjadi: validation error, authorization error, business rule violation, state transition error, infrastructure error, synchronization error. Error bisnis harus dapat diterjemahkan menjadi pesan UI yang bisa ditindaklanjuti ("Sisa budget Rp X", bukan "Invalid input").

Jangan expose: SQL error mentah, stack trace, internal module name, encrypted value, atau server path.

5.30 Transaction Convention

Jangan menggunakan transaction hanya karena operasi melibatkan beberapa query — gunakan ketika beberapa mutation harus mempunyai atomicity. Single-domain transaction → Ash Action. Cross-domain transaction → Reactor. Async continuation → Event/Oban. Pemilihan mekanisme mengikuti ownership dan failure semantics, bukan preferensi programmer.

5.31 Migration Convention

1. Migrasi hanya via mix ash.codegen — tidak ada SQL manual di production.

2. Migrasi wajib idempoten — bisa di-run ulang tanpa error.

3. Setiap migrasi menaikkan schema_version di schema_metadata (kontrak sync-agent); agent menolak koneksi bila versi node tidak cocok.

4. Urutan upgrade: server kantor dulu, baru laptop — sync berhenti sementara, tidak corrupt.

5. Non-destruktif untuk tabel finansial: tambah kolom nullable dulu; "hapus" = AshArchival, tidak pernah DROP data.

6. Tanpa DDL runtime: dimensi/numbering series custom = kolom/definisi kode per instalasi, terversioning (menolak "unlimited" ala ERPNext).

7. Schema version adalah bagian dari sync protocol; test wajib untuk perubahan constraint penting.

5.32 SQLite Operational Convention

Setiap node BMP wajib menjalankan:

PRAGMA journal_mode=WAL;
PRAGMA busy_timeout=5000;

WAL dan busy_timeout wajib. Constraint database tidak boleh dinonaktifkan hanya untuk membuat proses sync atau import berhasil.

5.33 Import Convention

Bulk import harus: parsing → validation → deduplication → mutation → error report. Import tidak boleh langsung memasukkan data mentah ke tabel; semua business invariant tetap berlaku pada import.

5.34 Numbering Convention

Numbering series didefinisikan per instalasi (tanpa runtime DDL) — mis. SO-SHOPEE-2026-W32, BLG-2026-001. Nomor yang sudah diterbitkan tidak pernah digunakan kembali; dokumen cancelled tetap menyimpan nomor aslinya.

5.35 Release & Versioning

5.35.1 Semantic Versioning

BMP mengikuti SemVer: MAJOR.MINOR.PATCH.

Jenis Perubahan Versi Contoh
Breaking change (API, schema, policy) MAJOR 2.0.0
Fitur baru, modul baru, workspace baru MINOR 1.5.0
Bug fix, patch security, hotfix PATCH 1.4.3

5.35.2 Changelog Convention

Format changelog mengikuti prinsip Keep a Changelog — setiap rilis mencatat Added/Changed/Fixed/Removed secara eksplisit; changelog ditulis sebagai dokumentasi, bukan auto-generate dari commit message mentah.

5.35.3 Release Packaging & Asset Fingerprinting

5.36 Git Convention

Repository menggunakan branch sederhana: main, feature/*, fix/*.

5.37 CI Pipeline

Checkout → Dependencies → Compile → Format Check → Credo → Dialyzer → Tests → Build Release

CI menggunakan SQLite sebagai satu-satunya data layer, sesuai Architecture — tidak ada environment CI yang diam-diam menggunakan database lain hanya untuk testing.

5.38 Definition of Done

Sebuah feature dianggap selesai apabila:

5.39 Anti-Patterns

5.40 Development Decision Hierarchy

Jika developer menghadapi pilihan implementasi, urutan keputusan adalah:

1. Architecture invariant

2. Domain ownership

3. Ash convention

4. Existing BMP convention

5. Simplest implementation

6. Performance optimization

Performance tidak boleh mengalahkan domain ownership atau correctness.

5.41 Dependency Rule

Pemilihan dependency mengikuti hierarchy resmi: Tier 1 — Ash/Phoenix → Tier 2 — Elixir ecosystem → Tier 3 — Erlang/OTP direct. Library baru tidak boleh ditambahkan hanya karena lebih nyaman. Sebelum menambah dependency harus dipastikan: (1) kebutuhan tidak dapat dipenuhi oleh Tier 1, (2) tidak dapat dipenuhi oleh standard library/OTP, (3) dependency mempunyai maintenance yang layak, (4) dependency tidak bertentangan dengan offline/self-contained architecture, (5) dependency tidak menggandakan fungsi library yang sudah digunakan.

Library Tier 2 yang telah dikunci: tincture, eqrcode, exceed, nimble_csv. Image processing library ditolak; Swoosh ditunda.

5.42 Laporan Fiskal & Template Output Bahasa Indonesia

Aturan:

  1. Output fiskal = laporan wajib Bahasa Indonesia menurut UU KUP Pasal 28 ayat (2) & lampiran SPT Badan: Laporan Posisi Keuangan (Neraca), Laporan Laba Rugi, Laporan Arus Kas, Neraca Saldo, Buku Besar, Bukti Potong PPh. Faktur Pajak mengikuti format standar DJP (BMP hanya memasok data).
  2. Semua output fiskal dirender dari template Tincture di priv/print_templates/fiscal/*.xml — terversioning, tanpa eksekusi kode; perubahan template = perubahan dokumen (wajib changelog).
  3. Seluruh string template diambil hanya dari Glosarium Fiskal (bawah). Tidak ada string Indonesia hard-coded di kode; CI mengeceknya (§5.23/§5.37).
  4. CoA account_name wajib diisi Bahasa Indonesia ("Beban Marketplace Fee"); account_number = kode angka. Nama internal/kode tetap English.
  5. Angka: Rupiah penuh, bulat, pemisah ribuan titik; kepala memuat "(Dalam Rupiah penuh, kecuali dinyatakan lain)".
  6. Export xlsx lampiran SPT (via exceed) memakai header kolom yang sama persis dengan template print — satu glosarium, dua wujud.
  7. CI: smoke-render semua template fiskal per build; gagal bila placeholder tak terisi atau string di luar glosarium.

Kepala & kaki bersama (semua template):

{NAMA_PERUSAHAAN}
{ALAMAT_LENGKAP}
NPWP : {NPWP}
──────────────────────────────────────────────
{JUDUL_LAPORAN} ← uppercase, dari glosarium
{PERIODE} ← "Per 31 Desember 2026" / "Untuk Tahun yang Berakhir 31 Desember 2026"
(Dalam Rupiah penuh, kecuali dinyatakan lain)

Kaki: Dicetak dari BMP : {tanggal} {pencetak} · Halaman {n} dari {N} · blok tanda tangan: Disusun oleh, (______) {Nama – Finance} · Mengetahui, (______) {Nama – Owner}.

Template 1 — LAPORAN POSISI KEUANGAN (NERACA) (invarian: TOTAL ASET = TOTAL LIABILITAS DAN EKUITAS)

ASET
ASET LANCAR
Kas dan Setara Kas {n}
Piutang Usaha {n}
Persediaan {n}
Pajak Dibayar di Muka {n}
TOTAL ASET LANCAR {n}
ASET TIDAK LANCAR
Aset Tetap {n}
Akumulasi Penyusutan ({n})
TOTAL ASET TIDAK LANCAR {n}
TOTAL ASET {n} \

LIABILITAS DAN EKUITAS
LIABILITAS JANGKA PENDEK
Utang Usaha {n}
Utang Pajak {n}
Pendapatan Diterima di Muka {n}
TOTAL LIABILITAS JANGKA PENDEK {n}
LIABILITAS JANGKA PANJANG {n}
TOTAL LIABILITAS {n}
EKUITAS
Modal Disetor {n}
Laba Ditahan {n}
Laba (Rugi) Berjalan {n}
TOTAL EKUITAS {n}
TOTAL LIABILITAS DAN EKUITAS {n}

Template 2 — LAPORAN LABA RUGI DAN PENGHASILAN KOMPREHENSIF LAIN

PENDAPATAN USAHA {n}
BEBAN POKOK PENJUALAN ({n})
LABA KOTOR {n}
BEBAN OPERASIONAL
Beban Marketplace Fee {n}
Beban Komisi Reseller {n}
Beban Diskon Penjualan {n}
Beban Promosi {n}
Beban Gaji dan Kesejahteraan {n}
Beban Penyusutan {n}
Beban Umum dan Administrasi {n}
TOTAL BEBAN OPERASIONAL ({n})
LABA OPERASIONAL {n}
PENDAPATAN (BEBAN) LAIN-LAIN {n}
LABA SEBELUM PAJAK {n}
BEBAN PAJAK PENGHASILAN ({n})
LABA BERSIH {n}

Template 3 — LAPORAN ARUS KAS (metode tidak langsung) (invarian: KAS AKHIR = saldo akun Bank+Cash di Neraca)

ARUS KAS DARI AKTIVITAS OPERASI
Laba Bersih {n}
Penyesuaian: Beban Penyusutan {n}
Perubahan Piutang Usaha ({n}/{n})
Perubahan Persediaan ({n}/{n})
Perubahan Utang Usaha & Pajak {n}
KAS BERSIH DARI OPERASI {n}
ARUS KAS DARI AKTIVITAS INVESTASI
Perolehan Aset Tetap ({n})
KAS BERSIH DARI INVESTASI ({n})
ARUS KAS DARI AKTIVITAS PENDANAAN
Setoran Modal {n}
KAS BERSIH DARI PENDANAAN {n}
KENAIKAN (PENURUNAN) BERSIH KAS {n}
KAS DAN SETARA KAS AWAL PERIODE {n}
KAS DAN SETARA KAS AKHIR PERIODE {n}

Template 4 — NERACA SALDO

Kode Akun Nama Akun Debit Kredit
1-1000 Kas dan Setara Kas {n}
JUMLAH {Σ} {Σ}

Template 5 — BUKU BESAR (satu akun per blok)

Kepala blok: {KODE} — {NAMA AKUN}

Tanggal Keterangan Ref Debit Kredit Saldo
{tgl} {voucher_no + party} {JE/SI/PI/PE} {n} {n} {n}

Template 6 — BUKTI POTONG PPh 23 (mengikuti struktur e-Bupot; sumber: tax_withholding_entries + purchase_invoices)

Blok Field
Dokumen Nomor & seri bukti potong · Tanggal · Masa Pajak & Tahun Pajak
Pemotong Pajak Nama · NPWP · Alamat
Penerima Penghasilan Nama · NPWP · Alamat
Transaksi Uraian penghasilan (Jasa / Sewa / Dividen / Royalti) · Jumlah Bruto
Perhitungan Tarif (%) · PPh yang Dipotong
Pernyataan "Demikian bukti potong ini dibuat dengan sebenarnya" + tanda tangan pemotong

Glosarium Fiskal — sumber string tunggal

Internal (kode/identifier) Label fiskal (Indonesia)
Assets Aset
Current Assets Aset Lancar
Non-current Assets Aset Tidak Lancar
Liabilities Liabilitas
Equity Ekuitas
Cash & Bank Kas dan Setara Kas
Receivables Piutang Usaha
Inventory Persediaan
Fixed Assets Aset Tetap
Accumulated Depreciation Akumulasi Penyusutan
Payables Utang Usaha
Tax Payable Utang Pajak
Paid-in Capital Modal Disetor
Retained Earnings Laba Ditahan
Revenue Pendapatan Usaha
Cost of Goods Sold Beban Pokok Penjualan
Gross Profit Laba Kotor
Operating Profit Laba Operasional
Net Profit Laba Bersih
Trial Balance Neraca Saldo
General Ledger Buku Besar
Cash Flow Arus Kas
Fiscal Year Tahun Pajak
Withholding (PPh 23) Pemotongan Pajak

5.43 Final Principle

Development BMP mengikuti prinsip:

Domain first, invariant first, correctness first, UI second, optimization last.

Kode harus mencerminkan arsitektur. Jika sebuah implementasi lebih mudah dibuat tetapi menyebabkan domain ownership kabur, business rule masuk ke UI, cross-domain coupling, data finansial dapat dihapus, event menjadi command, async job tidak idempotent, atau sync-agent mengetahui business logic — maka implementasi tersebut ditolak, meskipun secara teknis dapat berjalan.

Chapter 5 menjadi kontrak antara Blueprint dan source code: setiap feature baru harus dapat ditelusuri kembali ke domain, Resource, Action, invariant, event, persistence, test, dan deployment model yang telah ditetapkan dalam Chapter 3 dan Chapter 4.