Extraction Plan
Status: Phase 1 SHIPPED 2026-06-11 — historical reference
Plan ini ditulis 2026-06-10 saat Phase 1 masih in-progress. Phase 1 sekarang SUDAH SELESAI dan deployed. State terkini ada di Service Status. Konten di bawah dipertahankan sebagai referensi historis lengkap (731 baris, 10 section termasuk Phase 2 SaaS yang masih parked).
Status: 🟡 IN PROGRESS (ditulis 2026-06-10, update 2026-06-11) — Step 7 DONE (mig 106 applied, schemas dropped). HTTP proxy code DONE, binary dashboard_api belum di-deploy ke VM. Gate tersisa: build + deploy dashboard_api baru → purchasing 500 fix + planning proxy aktif.
Tracker: Rencana lift-and-shift menu Planning dashboard → service mandiri → produk SaaS independen, mengikuti pola yang sudah dipakai Polqo (notifikasi) & Sqile (UMKM Academy).
Tujuan: Mempersiapkan modul Planning supaya bisa di-lift ke produk
modelary (financial scenario modeling) tanpa coupling ke dashboard_api /
merchant_dashboard lainnya. Dua tahap:
- Phase 1 — jadikan service mandiri (
services/planning_service/) di dalam Kesles Merchant, mirror polaemail_service/firebase_service/content_service. - Phase 2 — lift ke SaaS multi-tenant (
modelary.com), parked sampai tenant model jelas.
Catatan nama. Working name = Modelary (sudah disebut di komentar kode:
planning_section.dart,dashboard_menu.dart). Alternatif branding Proyqo (suffix-qoselaras Polqo) masih terbuka — keputusan final lihat §9. Sepanjang dokumen ini, "Modelary" = placeholder yang konsisten dengan kode saat ini.
1. Konteks
Per 2026-06-10 diputuskan menu Planning di dashboard akan berdiri sendiri, sejalan dengan dua produk sibling yang sudah jalan di repo ini:
| Produk | Asal modul Kesles | Status |
|---|---|---|
| Polqo | infra notifikasi (FCM/WA/email, multi-tenant) | DB sudah tenant_id-ready |
| Sqile | UMKM Academy → content_service | sudah full service extraction |
| Modelary | menu Planning (4 model finansial) | plan ini |
Modul Planning adalah forward-looking strategic module — empat model finansial yang murni kalkulatif (client-side math) + persistensi skenario. Tidak ada operational data path; satu-satunya dependency adalah baseline tarif (daily fee, MDR split) yang di-resolve dari resolver dashboard. Ini bikin Planning kandidat ekstraksi paling bersih dari semua menu dashboard.
Kode sudah sengaja didesain untuk ekstraksi sejak awal:
PlanningSectionsengaja dipisah dariReportsSection"supaya seluruh modul bisa di-lift-and-shift ke produk standalone (Modelary) tanpa nyangkut ke operational reports" — komentar diplanning_section.dart.- Schema DB
planningdipisah darireport"agar gampang dipindah kedb_planningkalau menu Planning jadi project sendiri" — komentar didashboard_planning_defaults.go.
Bukan tujuan (Phase 1):
- Ubah kontrak API atau JSON shape — frontend dashboard tidak boleh rebuild khusus.
- Multi-tenant / drop FK
iam.users(id)— ditunda ke Phase 2. - Ganti math model atau parameter panel — ekstraksi = lift, bukan rewrite.
2. Current state inventory (Phase 0 — audit 2026-06-10)
2.1 Frontend (apps/merchant_dashboard)
lib/dashboard/features/planning/
├── domain/entities/report_scenario_entities.dart (394 LOC)
├── presentation/planning_section.dart router submenu → panel
├── presentation/forms/report_scenario_save_dialog.dart save/load dialog
└── presentation/panels/
├── planning_growth_projection_panel.dart (3115 LOC)
├── planning_deployment_ramp_panel.dart (2451 LOC)
├── planning_breakeven_panel.dart (2040 LOC)
├── planning_pricing_sensitivity_panel.dart (1292 LOC)
└── settings_planning_defaults_panel.dart (2282 LOC)
Total ~11.5k LOC. Self-contained — import hanya ke core/ (i18n, theme),
home/.../common/ (shared widgets), dan services/dashboard_home_api.dart.
Wiring eksternal (yang harus di-track saat ekstraksi):
home/.../models/dashboard_menu.dart—MerchantDashboardMenu.planning+ label "Planning"/"Perencanaan" +planningReadpermission gating.home/.../models/dashboard_rbac.dart— permissionplanningRead,planningUpdate(super-admin only).home/.../widgets/menu/dashboard_sidebar.dart— render menu Planning.services/dashboard_home_api.dart— methoddownloadPlanningDefaultsTemplate,listGrowthProjectionScenarios,getGrowthProjectionScenario, dst.
2.2 Empat model (math)
| # | Panel | Inti model | Persistensi |
|---|---|---|---|
| 1 | Growth Projection | 3 revenue stream (device margin one-off + daily fee recurring + MDR share recurring) × 5 tahun, dengan churn & active base | ✅ report.growth_projection_scenarios |
| 2 | Deployment Ramp-Up | Jadwal rollout bulanan (Active Unit = Installation × Active Base %) | ✅ report.deployment_ramp_scenarios |
| 3 | Break-even | Revenue model (3 stream) vs cost model (capital + opex + CAC + variable) → break-even year + payback | ❌ sandbox only |
| 4 | Pricing Sensitivity | Sweep tiap lever ±spread → tornado chart 5-year cumulative revenue | ❌ sandbox only |
Gap penting: hanya 2 dari 4 panel punya tabel skenario. Break-even & Pricing Sensitivity = client-side sandbox (cuma punya prefill via
planning_defaults). Saat ekstraksi, putuskan apakah keduanya butuh tabel skenario sendiri (lihat checklist §8).
2.3 Backend (services/dashboard_api)
internal/app/
├── dashboard_planning_defaults.go GET/PUT/DELETE /api/dashboard/planning/defaults
├── dashboard_planning_defaults_export.go GET /api/dashboard/planning/defaults/export (XLSX)
├── dashboard_planning_defaults_payloads.go validator shape parameters
└── dashboard_reports_scenarios_store.go CRUD store (growth + deployment ramp)
Routes (server.go):
/api/dashboard/planning/defaults (GET/PUT/DELETE, ?kind=...)
/api/dashboard/planning/defaults/export (GET, XLSX)
/api/dashboard/reports/growth-projections[/] (CRUD scenario)
/api/dashboard/reports/deployment-ramps[/] (CRUD scenario)
RBAC (rbac.go):
reportsPlanningRoles = []string{"superadmin"} — super-admin only.
Catatan path inkonsisten: scenario CRUD masih di prefix
/api/dashboard/reports/...(legacy mig 061), sedangkan defaults di/api/dashboard/planning/.... Saat ekstraksi, namespace harus diseragamkan ke/planning/...(lihat §3.2).
2.4 Database — current state di db_kesles_merchant
schema report (shared dengan operational reports):
├── report.growth_projection_scenarios owner_user_id (UUID, no FK), parameters jsonb
└── report.deployment_ramp_scenarios (idem)
schema planning (sudah terisolasi):
└── planning.planning_defaults singleton per (user_id, panel_kind)
panel_kind: shared_core | growth_projection |
deployment_ramp | breakeven | pricing_sensitivity
Migrations: v1/019_system_settings_sequences_planning.sql
(snapshot), legacy 061_create_report_scenarios.sql, 088_planning_defaults.sql,
094_planning_defaults_extend_panel_kinds.sql.
Dua fakta yang menentukan strategi ekstraksi:
- FK ke
iam.usersSUDAH diputus — migv1/059_drop_iam_fk_constraints.sqldropowner_user_id/created_by/updated_by(scenarios) +user_id(planning_defaults) sebagai bagian ekstraksidb_kesles_merchant_auth(iam.* sudah DROPPED dari core DB, sole SOT = auth_service). Jadi semua kolom user sekarang plain UUID tanpa FK → tabel planning sudah cross-DB ready. - Schema split belum tuntas —
planning_defaultssudah di schemaplanning, tapi 2 tabel scenario masih di schemareport. Catatan: schemareportmurni berisi 2 tabel scenario ini (bukan tercampur operational reports — diverifikasi via grep kode + migrasi), jadi setelah cutover schemareportjadi kosong dan bisa di-DROPdari core DB.
Objek yang dipindah (lengkap)
Sumber di db_kesles_merchant | Tujuan di db_kesles_merchant_planning | Isi |
|---|---|---|
report.growth_projection_scenarios | planning.growth_projection_scenarios | skenario panel Growth Projection |
report.deployment_ramp_scenarios | planning.deployment_ramp_scenarios | skenario panel Deployment Ramp |
planning.planning_defaults | planning.planning_defaults | prefill singleton per (user_id, panel_kind) |
Kolom *_scenarios (identik 2 tabel): id (uuid PK), name (varchar 120),
description, owner_user_id (uuid, eks-FK iam.users → plain UUID),
is_shared (bool), parameters (jsonb), parameters_version (smallint),
created_by_user_id, updated_by_user_id, created_at, updated_at,
deleted_at. Index: unik (owner_user_id, lower(name)) where not deleted, idx
(owner_user_id, updated_at desc), idx shared (updated_at desc) where is_shared.
Kolom planning_defaults: id (uuid PK), user_id (uuid, eks-FK iam.users),
panel_kind (varchar 40 ∈ shared_core|growth_projection|deployment_ramp|breakeven|pricing_sensitivity),
parameters (jsonb), parameters_version (smallint), created_at, updated_at.
Index unik (user_id, panel_kind).
Catatan:
- Tidak ada view / sequence / type yang ikut — hanya 3 tabel ini.
- Semua kolom user sudah plain UUID (FK diputus mig 059) → copy 1:1 tanpa ubah kolom.
- Break-even & Pricing Sensitivity belum punya tabel skenario sendiri (sandbox
client-side, state via
planning_defaults). Kalau mau bisa di-save, tambahplanning.breakeven_scenarios+planning.pricing_sensitivity_scenarios(§2.2).
2.5 Target DB-per-domain: db_kesles_merchant_planning (BELUM ADA)
Repo ini sudah pakai pola satu DB per domain — per database-plan v15
(2026-06-10) ada 10 database live: db_kesles_merchant (core) +
_auth, _partner, _notification, _inventory, _content, _payment,
_marketing, _order, _poslite. Belum ada DB planning.
Jawaban audit:
db_kesles_merchant_planningbelum di-scaffold. Ini akan jadi DB domain ke-11, mengikuti konvensidb_kesles_merchant_{domain}. Bukan sekadar pindah schema di dalam core DB — tabel planning dipindah ke database Postgres terpisah (mirrordb_kesles_merchant_contentuntuk Sqile).
Cross-DB Policy (wajib, sudah jadi standar semua domain DB): Postgres tidak
support cross-DB FK. Tidak ada FK ke iam.*/merchant.* — user_id plain UUID,
validasi di app-layer via auth_service (GET /internal/users/{id}). Karena FK
sudah diputus (§2.4 poin 1), planning tidak butuh perubahan kolom untuk ini.
2.6 Sumber data input panel (HPP via WAC API + harga jual via offers API)
Panel Planning tidak join DB master langsung — input device-nya datang dari 2 API antar-service yang sudah ada. Ini penting karena pola "data via API, bukan join DB" justru yang bikin ekstraksi Modelary bersih.
| Sisi | Field model | Sumber API (existing) | Status |
|---|---|---|---|
| HPP (cost) | unitCost, landedCost | GET /api/dashboard/reports/device-wac → wac_amount, unit_cost_avg, landed_cost_avg + breakdown per-batch (customs+VAT+inbound shipping). Sumber: inventory_service (db_kesles_merchant_inventory). | ✅ Live — fitur "Use current WAC" di panel Growth Projection (Ship-11.a.5) |
| Harga jual (non-HPP) | devicePrice, shippingFee, promo, marginPercent | GET /api/dashboard/master-data/device-offers (registration_device_offers) + /device-models → device_price_amount, shipping_fee_amount, promo_amount, base_unit_price, margin_percent. | 🟡 API ada, tapi belum auto-ditarik ke panel (masih input manual) |
Model margin: margin = harga jual (offers API) − HPP (WAC API). Hierarki
resolve yang sudah didesain di kode: planning_defaults (manual per-user) >
resolver baseline (WAC/offers API) > hardcoded fallback (669.630 / 60.000).
Penambahan yang relevan (Phase 1+): wiring device-offers/device-models
sebagai baseline harga jual — simetris dengan WAC yang sudah jadi baseline HPP.
Setelah itu, panel Pricing Sensitivity bisa sweep harga jual real (bukan asumsi).
3. Phase 1 — Service mandiri (services/planning_service/)
Target: planning jadi binary/port sendiri, mirror content_service. Frontend
dashboard tetap manggil endpoint yang sama (via gateway / reverse proxy), zero
behavior change.
3.1 Struktur service
services/planning_service/
├── cmd/server/main.go
├── internal/
│ ├── planning/
│ │ ├── defaults_store.go (dari dashboard_planning_defaults.go)
│ │ ├── scenarios_store.go (dari dashboard_reports_scenarios_store.go)
│ │ ├── payloads.go (validator shape, frozen contract)
│ │ └── export.go (XLSX template)
│ ├── config.go Config{SchemaPlanning, SchemaIAM, Timezone}
│ └── transport/http/
│ ├── auth.go SubjectResolver + AuthMiddleware + *Error
│ ├── handlers_defaults.go
│ ├── handlers_scenarios.go
│ └── routes.go Register() — semua endpoint /planning/*
├── migrations/ snapshot SQL portable schema planning
└── README.md sumber kebenaran kontrak
Module tidak import package internal dashboard_api. Error type sendiri
(shape JSON identik {"error":..., "code":...}). Host inject Deps{Auth, Subject}.
3.2 Scaffold db_kesles_merchant_planning (DB-per-domain)
Buat DB domain ke-11 mengikuti struktur db_kesles_merchant_content /
_partner:
merchant_database/db_kesles_merchant_planning/
├── init/
│ ├── 001_create_extensions.sql pgcrypto, uuid-ossp
│ └── 002_create_schemas.sql schema: planning
├── migrations/v1/
│ ├── 001_schema_migrations_tracker.sql
│ ├── 002_scenarios.sql planning.growth_projection_scenarios +
│ │ planning.deployment_ramp_scenarios
│ │ (dari report.*, semua di schema planning)
│ └── 003_planning_defaults.sql planning.planning_defaults
├── seeds/master/ (kalau ada baseline default)
└── README.md SOT kontrak + cross-db notes
Konsolidasi: 2 tabel scenario yang sekarang di schema report pindah ke schema
planning di DB baru — sekalian merapikan schema split yang belum tuntas (§2.4).
Kolom user sudah plain UUID (FK diputus mig 059), jadi copy 1:1 tanpa ubah kolom.
Pola cutover (mirror notification DB): scaffold DB baru lengkap dulu,
data tetap di db_kesles_merchant sampai schema stabil; cutover migration
(copy data + flip connection string planning_service) belakangan setelah
soak. Bukan big-bang.
3.3 Konsolidasi namespace API (breaking-safe)
- Pindahkan scenario CRUD dari
/api/dashboard/reports/{growth-projections,deployment-ramps}→/api/dashboard/planning/scenarios/{growth-projection,deployment-ramp}. Pasang redirect/alias dari path lama selama ≥1 release biar frontend lama tetap jalan (frontend di-update di release yang sama, alias dihapus setelah soak).
3.4 Validasi Phase 1 (gate sebelum merge)
| Check | Target |
|---|---|
go build ./... (planning_service + dashboard_api) | PASS |
| Kontrak 6 endpoint (URL + JSON shape) | Identik — frontend tidak rebuild |
| RBAC super-admin enforcement | Identik (reportsPlanningRoles) |
Scaffold db_kesles_merchant_planning (init + migrations v1) | Apply clean di DB kosong |
| Cutover data scenario + defaults → DB baru | Row count match, zero loss |
flutter analyze (merchant_dashboard) | No issues |
| Smoke 4 panel (save/load growth + ramp, prefill breakeven + pricing, export XLSX) | Manual saat deploy lokal |
3.5 Tahapan migrasi DB (runbook)
Mengikuti pola cutover domain DB lain (mirror db_kesles_merchant_partner
§migration phases), disederhanakan karena planning: super-admin only, write
volume rendah (scenario sesekali), satu consumer (dashboard_api), FK ke iam
sudah diputus (mig 059), dan cuma 3 tabel. Tiap tahap punya gate; tahap
berikut tidak mulai sebelum gate hijau.
| # | Tahap | Aksi | Gate lulus → lanjut | Rollback |
|---|---|---|---|---|
| 0 | Pre-flight | Scaffold services/planning_service/ + audit kontrak 6 endpoint + freeze JSON shape | Build PASS, kontrak terdokumentasi | Buang skeleton (belum ada efek) |
| 1 | DB scaffold | createdb db_kesles_merchant_planning + apply init/ + migrations/v1/ (schema planning: 2 scenarios + defaults) di DB kosong | Migrasi apply clean; schema_migrations tercatat | dropdb (DB baru, data lama utuh) |
| 2 | Backfill + verify | Copy 3 tabel core → DB baru (COPY/pg_dump --data-only); scenario report.* → schema planning | Row count match per tabel; checksum parameters sample | Truncate DB baru, ulang |
| 3 | Dual-write | planning_service jadi writer ke DB baru; dashboard_api tetap tulis ke core dan mirror ke service (transitional) | Tulisan baru muncul identik di 2 DB selama window | Stop dual-write, core tetap SOT |
| 4 | Reader cutover | Arahkan dashboard_api baca via HTTP planning_service (pasang alias route lama → baru) | Smoke 4 panel via service; zero error 24–48 jam | Balik baca ke core (flag) |
| 5 | Writer cutover | planning_service = sole writer; stop tulis ke core | Semua save/load lewat service; core read-only | Re-enable dual-write |
| 6 | Soak | Pantau prod (default 14 hari, atau ≥48 jam zero legacy read karena low-risk) | Zero legacy read + zero error | — |
| 7 | Legacy DROP | DROP SCHEMA report, planning di db_kesles_merchant (3 tabel kosong) | Wajib konfirmasi user eksplisit | Restore dari backup pre-drop |
Catatan:
- Tahap 3 (dual-write) bisa dilewati kalau deploy dilakukan saat maintenance window singkat — karena write volume rendah, cukup: freeze → backfill (tahap 2) → cutover langsung (tahap 4+5) → soak. Pilih jalur ini kalau downtime detik-an bukan masalah untuk menu super-admin.
- Tahap 7 mengikuti aturan repo: DROP schema legacy butuh konfirmasi eksplisit (mirror order Phase 10E, partner Phase 6).
- Setiap tahap update
shared/docs/database-plan.md(status DB #11).
4. Phase 2 — SaaS modelary.com (parked)
Lanjut setelah Phase 1 stabil & tenant model diputuskan. Mirror keputusan yang ditunda di plan Sqile.
4.1 Multi-tenant
Tambah tenant_id di semua tabel planning.* (mirror Polqo readiness). Default
00000000-...-0001 untuk Kesles single-tenant; tenant lain diversify saat onboard.
4.2 User model & validasi (FK sudah diputus)
Tidak perlu drop FK lagi — owner_user_id + planning_defaults.user_id sudah
plain UUID tanpa FK sejak mig 059 (§2.4). Validasi user lewat auth_service
(GET /internal/users/{id}) ala domain DB lain. Yang tersisa untuk Phase 2:
putuskan apakah Modelary pakai user model iam.users Kesles atau identitas
sendiri (tenant-based/email). Default: pending sampai tenant model jelas.
4.3 Decouple baseline resolver
Saat ini baseline tarif (daily fee, MDR split) di-resolve dari resolver Kesles.
Modelary multi-tenant butuh baseline per-tenant — abstraksi BaselineProvider
interface, Kesles inject implementasi resolver-nya, tenant lain pakai config sendiri.
4.4 Lift ke Go module / repo terpisah
git.kesles.com/modelary/planning — merchant import via go.mod, Modelary API
import module yang sama. Satu bug fix landed di 2 produk.
4.5 Frontend Modelary
Extract features/planning/ jadi Flutter app / Dart package mandiri
(packages/kesles_planning/) untuk Modelary web/app.
4.6 Modelary sebagai API publik — tenant eksternal (perusahaan/UMKM)
Skenario: perusahaan/UMKM lain pakai Modelary API untuk modeling bisnis mereka sendiri (bukan via dashboard Kesles). Ini target akhir SaaS. Tiga isu yang harus dipecahkan:
1. Sumber data input per-tenant (bring-your-own cost/price).
Saat ini HPP datang dari device-wac (inventory_service Kesles) dan harga jual
dari device-offers Kesles. Tenant eksternal tidak punya inventory_service
Kesles. Solusi: abstraksi BaselineProvider (§4.3) jadi per-tenant dengan 3 mode:
- Manual / CSV import — tenant input HPP & harga produk sendiri (paling simpel, default untuk UMKM kecil).
- Push API — tenant kirim data biaya/harga ke Modelary (
POST /v1/tenants/{id}/cost-data), Modelary simpan sebagai baseline tenant. - Connector — tenant yang pakai sistem inventory/akuntansi (mis. Accurate, Jurnal, atau ERP) connect via konektor; Modelary pull HPP berkala. Kesles sendiri = satu implementasi konektor (inventory_service).
2. Generalisasi model (driver-based, bukan device-specific). 4 model sekarang niche ke bisnis akuisisi merchant payment — revenue = device margin + daily fee + MDR share. Untuk UMKM generik (warung, F&B, retail), driver itu tidak relevan. Pilihan:
- Opsi A — tetap niche. Modelary = "FP&A untuk bisnis akuisisi merchant / agen payment / distributor device". Tidak generalisasi; jual ke segmen yang model bisnisnya mirip Kesles. Paling cepat, fokus (lihat benchmark insight #1, §5).
- Opsi B — driver configurable. Abstraksi revenue stream jadi konfigurasi
(
one_off_margin,recurring_fee,per_txn_share, dst.) yang tenant petakan ke bisnisnya. Lebih powerful, tapi mendekati Causal/Jirav (kompetisi langsung, butuh modeling engine umum). Risiko over-engineering.
Rekomendasi: mulai Opsi A (niche, leverage diferensiasi Kesles), buka ke Opsi B hanya kalau ada demand tenant non-payment yang konkret. Desain teknis Opsi B (meta-schema + contoh JSON multi-bisnis) di §4.7.
3. Kontrak API publik + auth + kuota. API publik butuh: API key per-tenant (bukan JWT dashboard internal), rate-limit
- kuota per-plan (mirror Runway/Causal yang pricing-nya per-volume/integrasi —
§5), versioning (
/v1/), dan isolasi data per-tenant (tenant_iddi semua tabel, §4.1). Endpoint inti:POST /v1/scenarios/{model}/run(stateless compute),CRUD /v1/scenarios(persisted),PUT /v1/baselines(cost/price data tenant).
Catatan compute: 4 model itu murni kalkulatif (client-side math sekarang). Untuk API publik, math harus dipindah ke server (Go) supaya tenant bisa
runtanpa frontend Kesles — refactor pure-function math jadi package Go yang dipakai bareng UI Flutter (via FFI/port) dan API. Ini prasyarat §4.6.
4.7 Meta-schema model — antisipasi multi-bisnis
Masalah: parameter sekarang flat & terkunci ke ontologi payment-merchant —
merchantTargets, dailyFee, mdrTotalPct, mdrKeslesPct, devicePrice,
unitCost, cacPerMerchant, dst. Setiap field mengasumsikan "merchant + device
- MDR". UMKM lain beda total: tidak ada MDR share, HPP & variable cost beda, dan entity-nya customer, bukan merchant.
Prinsip: jangan tambah cabang if business_type == ... (tidak scale).
Pisahkan model bisnis (data/konfigurasi) dari engine hitung (kode). Model
bisnis disimpan sebagai model_definition (JSONB) berisi primitif bertipe;
engine generik tapi bounded.
Tiga elemen model_definition:
- Entity — unit yang diproyeksikan, label konfigurabel (
merchant/customer/subscriber/outlet). Menjawab "user customer bukan merchant": yang berubah cuma label + dimensi, bukan struktur engine. - Revenue streams — daftar stream bertipe (tertutup):
one_off_margin(device margin),recurring_fee(daily fee),per_txn_share(MDR share),product_sale(harga − COGS, untuk retail). UMKM tanpa MDR = tidak deklarasiper_txn_share. - Cost components — bertipe:
fixed_opex,per_acquisition(CAC),per_active_variable,hpp_per_unit. Tiap komponen punya binding sumber (manual|csv|push_api|connector) — lanjutanBaselineProvider(§4.3, §4.6). Kesles WAC = satu implementasiconnector, bukan asumsi universal.
Kunci keseimbangan: ~5 tipe stream + ~5 tipe cost yang tertutup & ketat, bukan bahasa formula bebas. Cukup konfigurabel untuk multi-bisnis, tapi tetap bounded (gampang divalidasi, tenant tidak perlu jadi modeling engineer) — beda dari over-engineering jadi Causal/Anaplan (§5 insight #4). Engine evaluator hidup di package Go (prasyarat §4.6).
Contoh — preset Kesles (payment merchant)
{
"entity": { "key": "merchant", "label": "Merchant" },
"horizon_years": 5,
"acquisition_targets": [300, 1000, 3000, 6000, 10000],
"retention": { "churn_pct": 0.10, "active_base_pct": 0.85 },
"revenue_streams": [
{ "type": "one_off_margin", "key": "device_margin",
"selling": { "source": "connector", "ref": "device-offers" },
"hpp": { "source": "connector", "ref": "device-wac" } },
{ "type": "recurring_fee", "key": "daily_fee",
"amount": 2000, "period": "day", "op_days": 360 },
{ "type": "per_txn_share", "key": "mdr_share",
"share_pct": 0.003, "avg_txn": 50000, "op_days": 360 }
],
"costs": [
{ "type": "per_acquisition", "key": "cac", "amount": 150000 },
{ "type": "per_active_variable","key": "support", "amount": 5000, "period": "month" },
{ "type": "fixed_opex", "key": "opex", "amount": 50000000, "growth_pct": 0.10 }
]
}
Contoh — preset UMKM retail/F&B (tanpa MDR, entity customer)
{
"entity": { "key": "customer", "label": "Pelanggan" },
"horizon_years": 3,
"acquisition_targets": [500, 1500, 4000],
"retention": { "churn_pct": 0.25, "active_base_pct": 0.60 },
"revenue_streams": [
{ "type": "product_sale", "key": "penjualan",
"avg_price": 35000, "avg_qty_per_active": 4, "period": "month",
"hpp": { "source": "manual", "amount": 20000 } }
],
"costs": [
{ "type": "per_acquisition", "key": "promo", "amount": 10000 },
{ "type": "fixed_opex", "key": "sewa", "amount": 8000000, "growth_pct": 0.05 }
]
}
Perhatikan: preset UMKM tidak punya per_txn_share (MDR), hpp bindingnya
manual (bukan WAC connector), dan entity = customer. Semua perbedaan =
konfigurasi, nol kode baru. Engine yang sama meng-evaluasi keduanya.
Pisahkan dua "user"
- Tenant user = yang login ke Modelary (owner/finance) → auth/identity (§4.2).
- Modeled entity = noun yang diproyeksikan (merchant/customer/...) → dimensi
di
model_definition, bukan user login. Dipisah → tidak ada konflik.
Sekuensing aman
- Phase 1.5 (di Kesles): refactor 4 panel baca dari
model_definitionyang nilainya = preset Kesles sekarang. Output identik, perilaku tak berubah — tapi engine sudah driver-based di belakang layar. - Phase 2 (SaaS): expose template picker + custom definition + binding sumber
data untuk tenant eksternal; tambah
tenant_id(§4.1).
Refactor ini invisible untuk Kesles tapi membuka semua dinamika multi-bisnis.
5. Benchmark — produk sejenis di pasar
Modelary masuk kategori FP&A / financial scenario modeling SaaS. Pasar ini sudah matang (3rd-gen FP&A tools), jadi positioning & pricing Modelary bisa nyontek pemain berikut:
| Produk | Positioning | Pricing model | Relevansi ke Modelary |
|---|---|---|---|
| Causal | Modeling engine + spreadsheet-like, scenario bull/base/bear simultan, dashboard investor-ready | Subscription per-bulan, mulai ~$50–250/bln | Paling dekat — named-variable scenario + chart. Cocok jadi acuan UX panel. |
| Runway | FP&A modern, kolaboratif, real-time | Unlimited seats, tier per jumlah/jenis integrasi (bukan per-seat) | Model pricing menarik untuk SaaS UMKM: jangan per-seat, tapi per-integrasi/volume. |
| Jirav | Driver-based planning + report templates, onboarding cepat non-teknis | Flat tahunan ($10k–15k/thn Starter–Pro) | Acuan untuk template siap-pakai (Modelary punya 4 model template). |
| Pigment | Visual, kolaboratif, real-time scenarioing lintas tim | Enterprise, custom | Acuan visualisasi; tapi learning curve tinggi (~5 bln) → Modelary harus lebih simpel. |
| Mosaic | SaaS metrics, ARR, cohort; standar Series C+ (diakuisisi Hibob Feb 2025) | Enterprise | Acuan untuk metric-driven dashboard, segmen lebih besar. |
| Cube | Excel/Sheets front-end + governed data layer | Per-bulan | Acuan "spreadsheet-first" — Modelary bisa tawarkan export XLSX (sudah ada). |
Insight positioning untuk Modelary:
- Segmen kosong = UMKM / fintech akuisisi merchant. Pemain di atas semua target startup SaaS / mid-market US. Modelary lahir dari model bisnis konkret (device margin + daily fee + MDR share akuisisi merchant QRIS) — ini niche yang belum dilayani tool generik. Jadikan diferensiasi: "FP&A untuk bisnis akuisisi merchant / agen payment", bukan FP&A generik.
- Pricing: hindari per-seat. Tren 2026 jelas — Causal/Runway/Jirav tidak pakai per-seat per-bulan murni. Untuk pasar UMKM Indonesia, model per-tenant + tier volume/fitur (mirror Runway) lebih cocok daripada per-seat.
- Template-first = keunggulan. Modelary sudah datang dengan 4 model jadi (Growth, Ramp, Break-even, Pricing). Pemain lain jual "engine kosong" yang butuh berminggu setup. Onboarding instan = nilai jual utama untuk non-teknis.
- Simplicity > power. Pigment/Anaplan kalah di learning curve. Modelary harus tetap di sisi Causal/Jirav (cepat dipakai), jangan over-engineer jadi modeling engine umum.
6. Roadmap fitur — gap saat ini
Planning sekarang baru separuh siklus FP&A: 4 model forward-looking (plan)
tanpa loop balik ke realita. Scenario tersimpan = sandbox; "target" yang ada
(merchant_targets: [300,1000,3000,6000,10000]) cuma input asumsi, bukan
target yang dilacak vs aktual. Data aktualnya sudah tersedia via API dashboard
— tinggal disambung. Berikut gap berurutan prioritas.
6.1 Target → Actual loop (paling krusial)
Mengubah Planning dari kalkulator jadi sistem FP&A. Empat sub-fitur:
a. Commit scenario jadi "the plan". Tandai satu scenario sebagai active
budget/target per periode (mis. "Target 2026"), lock + versioning. Tabel baru
planning.committed_targets (scenario_id, period, locked_at, locked_by).
b. Tarik aktual & hitung variance. Pasangkan angka target ke aktual dari API yang sudah ada (tidak perlu bikin sumber baru):
| Target (dari planning) | Aktual (API existing) |
|---|---|
| Merchant onboarded / bulan | GET /api/dashboard/merchants/registration |
| Revenue daily fee | GET /api/dashboard/reports/daily-fee |
| MDR share | GET /api/dashboard/reports/daily-settlement |
| Revenue per merchant / partner | GET /api/dashboard/reports/revenue-by-merchant · -by-partner |
| Volume transaksi | GET /api/dashboard/reports/transaction-summary · summary/txn-trend |
Tampilkan variance % + status (on-track / behind / ahead) per bulan per stream.
c. Alokasi target ke bawah. Pecah target tahunan → bulanan (kurva deployment ramp sudah ada) → per-partner / per-sales-rep / per-region. Tanpa ini target cuma angka global tanpa PIC.
d. Alert saat melenceng. Kalau aktual tertinggal target > threshold, kirim notifikasi via notification service (Polqo). Planning jadi alat operasional, bukan dokumen statis.
6.2 Rolling forecast / re-forecast
Proyeksi sekarang statis 5 tahun. Tambahkan re-forecast tengah periode = aktual-to-date + sisa rencana, supaya angka selalu update.
6.3 Variance bridge (waterfall "kenapa meleset")
Saat aktual ≠ target, dekomposisi penyebab per-driver (volume vs harga vs churn vs HPP) lewat waterfall. Model sudah driver-based — tinggal didekomposisi.
6.4 Cash flow & runway
Break-even sudah ada, tapi belum ada timeline arus kas / runway (kapan kas habis,
kebutuhan modal kerja). Aktual AR/AP sudah tersedia (reports/ap-invoice-aging,
reports/po-aging). Ini fitur inti Causal/Runway yang belum dipunya.
6.5 Probabilistik (bull/base/bear berbobot + Monte Carlo)
Scenario sekarang deterministik. Tambahkan probability-weighted (bull/base/bear) dan opsional Monte Carlo untuk rentang keyakinan.
6.6 Cohort-based churn
Churn sekarang satu angka %. Retensi per-cohort dari data transaksi aktual jauh lebih akurat untuk proyeksi recurring revenue.
6.7 Kolaborasi & approval
Scenario → review → approve jadi budget, plus komentar. Relevan begitu lebih dari satu orang pakai (benchmark Pigment/Runway punya ini).
6.8 Export board-ready
Sekarang cuma export XLSX defaults. Untuk board/investor perlu export PDF/PPTX ringkas (plan + variance + grafik).
Catatan sekuensing: §6.1 (target loop) bisa mulai sebelum ekstraksi penuh — nilai bisnisnya tinggi dan datanya sudah ada. Sisanya (§6.2–6.8) menyusul. Beberapa fitur (probabilistik, cash flow) juga jadi nilai jual Modelary saat SaaS (§4.6).
7. Dampak ke produk lain
| Aspek | Dampak Phase 1 |
|---|---|
Frontend merchant_dashboard | ✅ Zero behavior change kalau alias route dipasang. Update import dashboard_home_api base path. |
dashboard_api | 🟡 Pindah 4 file handler keluar + wiring proxy/gateway ke planning_service. |
DB db_kesles_merchant | 🟡 Scaffold DB baru db_kesles_merchant_planning (#11) + cutover 3 tabel (2 scenario + defaults); drop dari core setelah soak. |
| Operational reports lain | ✅ Tidak kena — Planning sudah terpisah dari ReportsSection. |
| Merchant launch / mobile apps | ✅ Tidak kena — Planning super-admin dashboard only. |
8. Checklist — dipisah: Persiapan Service vs Future
Dua bucket tegas. A = yang wajib untuk Planning jadi service mandiri (ekstraksi murni, zero behavior change). B = semua yang menambah kapabilitas baru (fitur FP&A, SaaS, multi-tenant, generalisasi) — di luar lingkup jadi-service.
Garis pemisah: kalau sebuah task mengubah perilaku/menambah fitur, dia masuk B (future). Kalau cuma memindah kode/DB tanpa ubah output, dia masuk A.
8.A — Persiapan jadi service mandiri (wajib, sekarang)
Lingkup: lift menu Planning → services/planning_service/ + DB sendiri, kontrak
& output identik. Tidak ada fitur baru.
- Buat skeleton
services/planning_service/(mirrorcontent_service) — DONE 2026-06-11, port 8097,go buildPASS - Pindah
dashboard_planning_defaults*.go+dashboard_reports_scenarios_store.go— DONE 2026-06-11 (store_defaults.go + store_scenarios.go + payloads.go) - Bikin
transport/http/denganDeps{Auth, Subject}(decouple daridashboard_api) — DONE 2026-06-11 (helpers + module + 4 handler files) - Konsolidasi route ke namespace
/api/dashboard/planning/*— DONE 2026-06-11 (6 route, scenario path dipindah dari/reports/ke/planning/scenarios/) - DB migrasi bertahap per runbook §3.5 — tahap 1 (scaffold) DONE 2026-06-10: init + migrations v1/001–003 applied di
db_kesles_merchant_planning - DB tahap 2 — backfill DONE 2026-06-11:
scripts/backfill_planning_db.shdieksekusi —planning_defaults src=5 tgt=5 OK; growth/ramp 0 rows (belum ada skenario tersimpan di dev). - DB tahap 3 — dual-write DONE 2026-06-11:
dashboard_planning_mirror.go— 8 write ops di-mirror async keplanningDB; aktif setelah deploydashboard_apidenganPLANNING_POSTGRES_DSNdi.env. - DB tahap 4+5 — reader+writer cutover DONE 2026-06-11:
planningReadDB()+planningWriteDB()— saatPLANNING_POSTGRES_DSNdi-set, semua planning reads & writes routing kedb_kesles_merchant_planning; fallback kes.dbotomatis kalau DSN kosong. Deploydashboard_apiuntuk aktifkan. - DB tahap 6 — SOAK AKTIF 2026-06-11 00:58: log
planning_db_connected dual_write=enabled✅; 0planning_mirror_write_failed. Soak selesai sebelum 2026-06-13. - DB tahap 7 — DROP legacy DONE 2026-06-11: mig
v1/106_drop_legacy_planning_schemas.sqlapplied —DROP SCHEMA IF EXISTS report CASCADE; DROP SCHEMA IF EXISTS planning CASCADEdidb_kesles_merchant. 3 tabel lama sudah hilang. - planning_service VM deploy DONE 2026-06-11: binary di
/home/enalfarid/kesles_merchant/merchant_planning/planning-service, systemdplanning-service.serviceactive running port 8097. Health check{"service":"planning-service","status":"ok"}✅. - HTTP proxy cutover (#2) code DONE 2026-06-11:
planning_service_proxy.go+server.go(6 route if/else proxy) +config.go(PlanningServiceBaseURL) +.env.production(PLANNING_SERVICE_BASE_URL=http://127.0.0.1:8097). Binary belum di-deploy ke VM. - Schema fix DONE 2026-06-11:
dashboard_reports_scenarios_store.go— konstantareport.*diubah keplanning.*(post mig 106, planningReadDB() connect ke db_kesles_merchant_planning schema planning). -
.env.examplediupdate 2026-06-11: tambahPLANNING_POSTGRES_DSN+PLANNING_SERVICE_BASE_URL. - Tambah entry
db_kesles_merchant_planningkeshared/docs/database-plan.md(v17, DB #11, soak aktif) - Build + deploy dashboard_api binary baru — menyelesaikan: purchasing 500 fix (INVENTORY_PURCHASING_READS_ENABLED) + planning proxy aktif + schema fix live
- Putuskan: Break-even & Pricing Sensitivity butuh tabel skenario sendiri? (§2.2)
- Update komentar kode yang refer
docs/architecture/planning-extraction.md→ arahkan ke file ini - Validasi gate §3.4 (build + kontrak + scaffold + smoke 4 panel)
- Update mirror
repo_exports/(backend + dashboard + docs)
Catatan: device-offers/device-models sebagai baseline harga jual (§2.6)
boleh masuk A hanya jika disepakati sebagai bagian parity ekstraksi; default-nya
fitur → masuk B.
8.B — Future (fitur FP&A + SaaS, di luar jadi-service)
Tidak menghalangi ekstraksi; bisa jalan paralel/menyusul. Kelompok terurut:
B1 — Fitur FP&A (masih single-tenant Kesles, §6):
- Target → Actual loop §6.1:
planning.committed_targets+ variance vs API aktual + alokasi + alert - Rolling forecast §6.2 · Variance bridge §6.3 · Cash flow/runway §6.4
- Probabilistik §6.5 · Cohort churn §6.6 · Approval §6.7 · Export PDF/PPTX §6.8
B2 — Generalisasi engine (prasyarat multi-bisnis, §4.6–4.7):
- Pindah math 4 model dari Flutter → package Go (compute server-side) (§4.6)
- Putuskan generalisasi model: niche (Opsi A) vs driver configurable (Opsi B) (§4.6)
- Refactor parameter flat →
model_definitionJSONB (preset Kesles = output identik) (§4.7) - Engine evaluator generik di Go: ~5 tipe stream + ~5 tipe cost (bounded) (§4.7)
- Template library archetype (payment / retail-UMKM / SaaS / distributor) (§4.7)
B3 — SaaS multi-tenant & API publik (§4.1–4.6):
- Final nama produk (§9) → rename referensi kode kalau bukan "Modelary"
- Tambah
tenant_idsemua tabelplanning.*(FK iam sudah diputus, §4.2) - Putuskan user model Modelary (iam.users Kesles vs identitas sendiri) (§4.2)
- Abstraksi
BaselineProviderper-tenant: manual/CSV · push API · connector (§4.3, §4.6) - Kontrak API publik
/v1/*+ API key per-tenant + rate-limit/kuota per-plan (§4.6) - Lift ke Go module
git.kesles.com/modelary/planning - Extract Dart package
packages/kesles_planning/
9. Keputusan nama (pending)
Working name Modelary dipakai di kode saat ini. Final branding:
| Opsi | Alasan | Effort |
|---|---|---|
| Modelary | Sudah di kode (planning_section.dart, dashboard_menu.dart). Tema "financial model". | Zero rework |
| Proyqo | Suffix -qo selaras Polqo → trio brand rapi (Polqo · Sqile · Proyqo). Akar "proyeksi". | Update referensi "Modelary" di kode |
| Foresa / Skenaro | Alternatif (foresight / scenario). Lebih lemah — tidak nyambung tema sibling. | Update referensi |
Rekomendasi: Modelary (hemat effort, konsisten kode) atau Proyqo (keselarasan branding). Keputusan diambil sebelum Phase 1 merge supaya rename, kalau ada, sekali jalan.
10. Related
- Plan sibling:
umkm-academy-module-extraction-plan.md(di docs/plans/) — pola ekstraksi Sqile (template plan ini) - Frontend:
apps/merchant_dashboard/lib/dashboard/features/planning/ - Backend defaults:
services/dashboard_api/internal/app/dashboard_planning_defaults.go - Backend scenarios store:
services/dashboard_api/internal/app/dashboard_reports_scenarios_store.go - DB migration:
merchant_database/db_kesles_merchant/migrations/v1/019_system_settings_sequences_planning.sql - DB FK strip:
merchant_database/db_kesles_merchant/migrations/v1/059_drop_iam_fk_constraints.sql - DB inventory SOT:
shared/docs/database-plan.md(shared/docs/database-plan.md) — konvensidb_kesles_merchant_{domain}, 10 DB live (planning = #11) - Pola DB domain acuan:
merchant_database/db_kesles_merchant_content/(Sqile),merchant_database/db_kesles_merchant_notification/(pola scaffold-then-cutover) - Benchmark sumber: Causal/Runway/Jirav/Pigment/Mosaic/Cube — FP&A market Q2 2026 (lihat ringkasan §5)
Dokumen ini adalah tracker rencana lift-and-shift menu Planning → Modelary.
Phase 0 (audit current state) selesai 2026-06-10. Phase 1 (service mandiri
services/planning_service/) siap dieksekusi. Phase 2 (SaaS multi-tenant
modelary.com) ditunda sampai tenant model & nama final diputuskan.