BMP
Business Management Platform
Project Blueprint & Design Documentation
Dokumen Gabungan — v2.3
Table of Contents
Dua “Architecture” yang Berbeda Level 12
Latar Belakang — Analisis 12 ERP Open Source 12
Chapter 1: Design Foundation 14
1.5 Shadow & Elevation System (baru) 18
1.6 Spacing, Radius & Grid (baru) 19
1.8 Tiga Prinsip Meta Visual 22
Chapter 2: Interaction Structure 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.1 Prinsip Pengorganisasian 26
2.3 Navigation & Information Structure 27
2.3.1 Hierarki Halaman (3 Tingkat) 28
2.3.3 Pembagian Peran Penanda 28
2.3.5 Pola Sidebar (Final: Flat + Trigger Klik) 29
2.3.7 Global Navigation vs Local Navigation 29
2.3.8 Lima Hal Spesifik Navigasi ERP/BMP 30
2.4.1 Prinsip Pengorganisasian 31
2.4.2 Conditional Field vs Progressive Disclosure 33
2.5 Search & Command Palette 33
2.5.3 Empty State & Halaman Alat 33
2.6 Notification & Approval Workflow 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.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
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.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.15 Hierarki Pemilihan Dependency (kebijakan resmi) 52
Addendum: Mekanisme Sync-Agent — Penjelasan Detail 53
3. Alur Lengkap: Laptop ke Server Kantor 54
Fase 5 — Konfirmasi & State Update 55
Fase 6 — Sync Balik (Server → Laptop) 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.2 accounts (Chart of Accounts) 58
4.1.6 items (Master Produk) 60
4.1.13 uoms (Unit of Measure) 63
4.1.18 numbering_series ➕ v1.6 (BARU) 64
4.1.19 departments ➕ v1.7 (BARU) 64
4.2.4 stock_ledger_entries (SLE) — Immutable 66
4.2.6 stock_reservations ➕ v1.6 (BARU) 67
4.3.4 purchase_receipt_items 69
4.3.6 purchase_invoice_items 70
4.3.9 landed_cost_voucher_items 71
4.4.1 boms (Bill of Materials) 71
4.4.5 work_order_operations 73
4.4.7 production_plan_items 73
4.5.1.4 delivery_note_items 75
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.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.1 modes_of_payment — TABEL BARU 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.1 taxes_templates (Sales & Purchase) 83
4.6.3.3 tax_withholding_categories 83
4.6.3.4 tax_withholding_entries — TABEL BARU 83
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.8.3 asset_depreciation_schedules 87
4.8.7 asset_maintenance_logs 88
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.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.2 Aturan Penegakan (Konvensi Implementasi) 97
Perubahan Tipe Data / Refactor Struktural 100
Chapter 5: Development Conventions 102
5.0 Development Invariants 103
5.1 Bahasa & Naming Convention 103
5.1.2 UI Component Naming (LiveView / HEEx) 104
5.2 Project Structure & Folder Convention 105
5.6 Public Action vs Internal Implementation 108
5.9 State Machine Convention 109
5.12 Money & Numeric Precision 109
5.18 Logging & Sensitive Data 110
5.20 UI State vs Application Data 111
5.22 Performance Convention 112
5.25 Property-Based Testing 113
5.28 Documentation Convention 114
5.30 Transaction Convention 115
5.32 SQLite Operational Convention 115
5.35.1 Semantic Versioning 116
5.35.2 Changelog Convention 116
5.35.3 Release Packaging & Asset Fingerprinting 116
5.40 Development Decision Hierarchy 118
5.42 Laporan Fiskal & Template Output Bahasa Indonesia 119
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.
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 |
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 bab dalam dokumen ini disusun berdasarkan ketergantungan — apa yang harus mapan dulu sebelum yang lain bisa dibangun konsisten di atasnya, bukan sekadar urutan sembarang:
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.
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:
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.
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:
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.
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.
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:
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).
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.
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.
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.
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.
| # | 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. |
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.
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.
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.
| 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.
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.
| 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.
| 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) | ❌ |
Item pertama = Overview (= Workspace; jalur bagi mode collapsed dan jalur alternatif mode expanded).
Grup ber-header legend divider: label 10px uppercase duduk di atas garis tipis yang menembusnya — terbaca pemisah, bukan item menu.
Tanpa policy di flyout bisnis — policy hidup di Administration (Settings Hub).
Panel: min-width 224px, radius 8px, shadow level 2, overlay tanpa layout shift, max-height calc(100vh − 16px) + scroll internal.
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).
(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).
| 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.
📎 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.
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.
📎 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.
| 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.
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.
| 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.
📎 command palette dengan hasil terkelompok: Action, Document (dengan badge status), dan Report.
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).
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).
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.
Tiga lapisan yang dipisah tegas:
📎 role default per fungsi (Sales/Warehouse/Finance) sebagai template admin, dengan contoh override personal dan opsi reset ke default.
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.
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.
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.
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.
Jangan membangun satu layout "responsive universal" — rancang 2–3 mode spesifik untuk peran yang memang membutuhkan:
Karena berjalan lewat LAN via browser, tidak perlu aplikasi native terpisah — cukup breakpoint CSS untuk mode touch.
📎 perbandingan prioritas informasi: tabel padat untuk Desktop staf akuntansi vs target sentuh besar untuk Tablet staf gudang.
| 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.
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:
| # | 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 |
| 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 |
| 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 |
Bab penutup Chapter 2 — justifikasi empiris untuk seluruh keputusan di atas. Format: Temuan → Diagnosis → Koreksi.
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.
Diagnosis: infinite scroll cocok untuk konsumsi konten (social media), merusak spatial memory dan total konteks pada data operasional.
Koreksi: Aturan #2 — pager tradisional wajib.
Diagnosis: User dan Email Account (domain System) masuk ke sidebar Organization — melanggar Single Ownership, menciptakan beban kognitif ganda.
Koreksi: Aturan #3 — Strict UI Ownership.
Diagnosis: Framework (Platform), Organization (Master), dan Settings (Policy) ditampilkan sebagai ikon setara — meruntuhkan taksonomi.
Koreksi: Aturan #4 — Settings Hub tunggal, platform invisible.
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.
Diagnosis: Tombol hitam "Set User Permissions" adalah tautan ke halaman lain, bukan aksi create — mengkhianati muscle memory.
Koreksi: Aturan #6 — slot primary = aksi dominan.
Diagnosis: Permission Manager terbuka dengan dua input kosong + kalimat perintah — tidak mengajar atau menawarkan langkah pertama.
Koreksi: Aturan #7 — setiap halaman membuka berguna.
Diagnosis: Like, komentar, dan hati di list User — noise di konteks bisnis; semantik "suka" menyesatkan.
Koreksi: Aturan #8 — tanpa pola sosial di list operasional.
Diagnosis: "Manufactur...", "Subcontrac..." — navigasi adalah tempat terakhir yang boleh mengorbankan keterbacaan.
Koreksi: Aturan #9 — tidak pernah truncate.
Diagnosis: Kolom "Is Group" di Department — user berpikir "induk/anak", bukan "flag group".
Koreksi: Aturan #10 — konsep bisnis, bukan mekanisme.
Diagnosis: "Accounts − PSGA" — metadata menempel ke identitas, membuat list bising.
Koreksi: Aturan #11 — badge/kolom terpisah.
Diagnosis: Satu menu berisi navigasi, preferensi, sesi, dan aksi sistem tanpa pengelompokan.
Koreksi: Aturan #12 — dikelompokkan per jenis, role-gated.
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.
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.
Prinsip yang mengikat seluruh keputusan di bawah — bukan sekadar filosofi tersirat, tapi aturan yang tidak boleh dilanggar modul manapun.
| 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.
| 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). |
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 |
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)
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.
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.
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.
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.
AshArchival + AshPaperTrail, dengan empat aturan wajib:
Lapis keamanan lain (tidak berubah): AshAuthentication (model akun terpisah) → Ash.Policy.Authorizer (otorisasi) → AshCloak (enkripsi field sensitif) → AshRateLimiter (endpoint publik).
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.
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:
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.
Menjelaskan bagaimana font & ikon (disebut Chapter 1) di-build dan di-deploy sebagai bundle offline.
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. |
| 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.
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:
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.
Diagram 1 — Komponen Sync-Agent
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.
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.
Bertanggung jawab mengirim dan menerima changeset antar node.
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.
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.
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.
Diagram 2 — Alur Lengkap Sync (Laptop → Server → Laptop)
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.
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.
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.
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.
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.
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.
Diagram 3 — Conflict Resolution per Jenis Tabel
Diagram 4 — Failure Mode & Recovery
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).
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.
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.
| 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 |
| 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 |
| 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.
| 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 |
| 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.
| 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 |
| 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 |
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 |
| 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 |
| 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 |
| 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 |
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 |
| 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 |
| 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 |
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 |
| 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 |
| 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).
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 |
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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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).
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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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:
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
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 |
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 |
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 |
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).
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
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 |
| 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.)
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| Kolom | Tipe | Deskripsi | Status |
|---|---|---|---|
| id | UUID | Primary key | Standar |
| 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.
| 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.
| 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 |
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 |
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 |
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 |
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 |
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 |
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.
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)
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.
| 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 |
| 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). |
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.
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
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:
Aturan berikut bersifat mengikat dan menjadi batas implementasi bagi seluruh chapter di bawah:
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 ✅.
| 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 |
| 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).
| 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 |
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:
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.
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.
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.
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.
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.
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.
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
}
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.
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.
BMP menggunakan SQLite sebagai satu-satunya RDBMS pada architecture saat ini.
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.
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.
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.
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).
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.
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.
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.
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.
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.
| 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.
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).
| 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.
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:
Aturan test LiveView:
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.
| 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 |
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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 |
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.
Repository menggunakan branch sederhana: main, feature/*, fix/*.
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.
Sebuah feature dianggap selesai apabila:
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.
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.
Aturan:
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 |
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.