Lewati ke konten utama

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 pola email_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 -qo selaras 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:

ProdukAsal modul KeslesStatus
Polqoinfra notifikasi (FCM/WA/email, multi-tenant)DB sudah tenant_id-ready
SqileUMKM Academy → content_servicesudah full service extraction
Modelarymenu 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:

  • PlanningSection sengaja dipisah dari ReportsSection "supaya seluruh modul bisa di-lift-and-shift ke produk standalone (Modelary) tanpa nyangkut ke operational reports" — komentar di planning_section.dart.
  • Schema DB planning dipisah dari report "agar gampang dipindah ke db_planning kalau menu Planning jadi project sendiri" — komentar di dashboard_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.dartMerchantDashboardMenu.planning + label "Planning"/"Perencanaan" + planningRead permission gating.
  • home/.../models/dashboard_rbac.dart — permission planningRead, planningUpdate (super-admin only).
  • home/.../widgets/menu/dashboard_sidebar.dart — render menu Planning.
  • services/dashboard_home_api.dart — method downloadPlanningDefaultsTemplate, listGrowthProjectionScenarios, getGrowthProjectionScenario, dst.

2.2 Empat model (math)

#PanelInti modelPersistensi
1Growth Projection3 revenue stream (device margin one-off + daily fee recurring + MDR share recurring) × 5 tahun, dengan churn & active basereport.growth_projection_scenarios
2Deployment Ramp-UpJadwal rollout bulanan (Active Unit = Installation × Active Base %)report.deployment_ramp_scenarios
3Break-evenRevenue model (3 stream) vs cost model (capital + opex + CAC + variable) → break-even year + payback❌ sandbox only
4Pricing SensitivitySweep 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:

  1. FK ke iam.users SUDAH diputus — mig v1/059_drop_iam_fk_constraints.sql drop owner_user_id/created_by/updated_by (scenarios) + user_id (planning_defaults) sebagai bagian ekstraksi db_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.
  2. Schema split belum tuntasplanning_defaults sudah di schema planning, tapi 2 tabel scenario masih di schema report. Catatan: schema report murni berisi 2 tabel scenario ini (bukan tercampur operational reports — diverifikasi via grep kode + migrasi), jadi setelah cutover schema report jadi kosong dan bisa di-DROP dari core DB.

Objek yang dipindah (lengkap)

Sumber di db_kesles_merchantTujuan di db_kesles_merchant_planningIsi
report.growth_projection_scenariosplanning.growth_projection_scenariosskenario panel Growth Projection
report.deployment_ramp_scenariosplanning.deployment_ramp_scenariosskenario panel Deployment Ramp
planning.planning_defaultsplanning.planning_defaultsprefill 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, tambah planning.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_planning belum di-scaffold. Ini akan jadi DB domain ke-11, mengikuti konvensi db_kesles_merchant_{domain}. Bukan sekadar pindah schema di dalam core DB — tabel planning dipindah ke database Postgres terpisah (mirror db_kesles_merchant_content untuk 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.

SisiField modelSumber API (existing)Status
HPP (cost)unitCost, landedCostGET /api/dashboard/reports/device-wacwac_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, marginPercentGET /api/dashboard/master-data/device-offers (registration_device_offers) + /device-modelsdevice_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)

CheckTarget
go build ./... (planning_service + dashboard_api)PASS
Kontrak 6 endpoint (URL + JSON shape)Identik — frontend tidak rebuild
RBAC super-admin enforcementIdentik (reportsPlanningRoles)
Scaffold db_kesles_merchant_planning (init + migrations v1)Apply clean di DB kosong
Cutover data scenario + defaults → DB baruRow 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.

#TahapAksiGate lulus → lanjutRollback
0Pre-flightScaffold services/planning_service/ + audit kontrak 6 endpoint + freeze JSON shapeBuild PASS, kontrak terdokumentasiBuang skeleton (belum ada efek)
1DB scaffoldcreatedb db_kesles_merchant_planning + apply init/ + migrations/v1/ (schema planning: 2 scenarios + defaults) di DB kosongMigrasi apply clean; schema_migrations tercatatdropdb (DB baru, data lama utuh)
2Backfill + verifyCopy 3 tabel core → DB baru (COPY/pg_dump --data-only); scenario report.* → schema planningRow count match per tabel; checksum parameters sampleTruncate DB baru, ulang
3Dual-writeplanning_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 windowStop dual-write, core tetap SOT
4Reader cutoverArahkan dashboard_api baca via HTTP planning_service (pasang alias route lama → baru)Smoke 4 panel via service; zero error 24–48 jamBalik baca ke core (flag)
5Writer cutoverplanning_service = sole writer; stop tulis ke coreSemua save/load lewat service; core read-onlyRe-enable dual-write
6SoakPantau prod (default 14 hari, atau ≥48 jam zero legacy read karena low-risk)Zero legacy read + zero error
7Legacy DROPDROP SCHEMA report, planning di db_kesles_merchant (3 tabel kosong)Wajib konfirmasi user eksplisitRestore 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_id di 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 run tanpa 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:

  1. Entity — unit yang diproyeksikan, label konfigurabel (merchant / customer / subscriber / outlet). Menjawab "user customer bukan merchant": yang berubah cuma label + dimensi, bukan struktur engine.
  2. 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 deklarasi per_txn_share.
  3. Cost components — bertipe: fixed_opex, per_acquisition (CAC), per_active_variable, hpp_per_unit. Tiap komponen punya binding sumber (manual | csv | push_api | connector) — lanjutan BaselineProvider (§4.3, §4.6). Kesles WAC = satu implementasi connector, 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

  1. Phase 1.5 (di Kesles): refactor 4 panel baca dari model_definition yang nilainya = preset Kesles sekarang. Output identik, perilaku tak berubah — tapi engine sudah driver-based di belakang layar.
  2. 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:

ProdukPositioningPricing modelRelevansi ke Modelary
CausalModeling engine + spreadsheet-like, scenario bull/base/bear simultan, dashboard investor-readySubscription per-bulan, mulai ~$50–250/blnPaling dekat — named-variable scenario + chart. Cocok jadi acuan UX panel.
RunwayFP&A modern, kolaboratif, real-timeUnlimited seats, tier per jumlah/jenis integrasi (bukan per-seat)Model pricing menarik untuk SaaS UMKM: jangan per-seat, tapi per-integrasi/volume.
JiravDriver-based planning + report templates, onboarding cepat non-teknisFlat tahunan ($10k–15k/thn Starter–Pro)Acuan untuk template siap-pakai (Modelary punya 4 model template).
PigmentVisual, kolaboratif, real-time scenarioing lintas timEnterprise, customAcuan visualisasi; tapi learning curve tinggi (~5 bln) → Modelary harus lebih simpel.
MosaicSaaS metrics, ARR, cohort; standar Series C+ (diakuisisi Hibob Feb 2025)EnterpriseAcuan untuk metric-driven dashboard, segmen lebih besar.
CubeExcel/Sheets front-end + governed data layerPer-bulanAcuan "spreadsheet-first" — Modelary bisa tawarkan export XLSX (sudah ada).

Insight positioning untuk Modelary:

  1. 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.
  2. 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.
  3. 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.
  4. 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 / bulanGET /api/dashboard/merchants/registration
Revenue daily feeGET /api/dashboard/reports/daily-fee
MDR shareGET /api/dashboard/reports/daily-settlement
Revenue per merchant / partnerGET /api/dashboard/reports/revenue-by-merchant · -by-partner
Volume transaksiGET /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

AspekDampak 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/ (mirror content_service) — DONE 2026-06-11, port 8097, go build PASS
  • 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/ dengan Deps{Auth, Subject} (decouple dari dashboard_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.sh dieksekusi — 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 ke planningDB; aktif setelah deploy dashboard_api dengan PLANNING_POSTGRES_DSN di .env.
  • DB tahap 4+5 — reader+writer cutover DONE 2026-06-11: planningReadDB() + planningWriteDB() — saat PLANNING_POSTGRES_DSN di-set, semua planning reads & writes routing ke db_kesles_merchant_planning; fallback ke s.db otomatis kalau DSN kosong. Deploy dashboard_api untuk aktifkan.
  • DB tahap 6 — SOAK AKTIF 2026-06-11 00:58: log planning_db_connected dual_write=enabled ✅; 0 planning_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.sql applied — DROP SCHEMA IF EXISTS report CASCADE; DROP SCHEMA IF EXISTS planning CASCADE di db_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, systemd planning-service.service active 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 — konstanta report.* diubah ke planning.* (post mig 106, planningReadDB() connect ke db_kesles_merchant_planning schema planning).
  • .env.example diupdate 2026-06-11: tambah PLANNING_POSTGRES_DSN + PLANNING_SERVICE_BASE_URL.
  • Tambah entry db_kesles_merchant_planning ke shared/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_definition JSONB (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_id semua tabel planning.* (FK iam sudah diputus, §4.2)
  • Putuskan user model Modelary (iam.users Kesles vs identitas sendiri) (§4.2)
  • Abstraksi BaselineProvider per-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:

OpsiAlasanEffort
ModelarySudah di kode (planning_section.dart, dashboard_menu.dart). Tema "financial model".Zero rework
ProyqoSuffix -qo selaras Polqo → trio brand rapi (Polqo · Sqile · Proyqo). Akar "proyeksi".Update referensi "Modelary" di kode
Foresa / SkenaroAlternatif (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.


  • 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) — konvensi db_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.