XPrivate · admin-MVP-1 · Design Plan (Output 1+2) ALIGNED aligned 2026-05-31
IA handover index → admin-mvp-1-ia-handover.html · Stack: React 19 + TanStack Start (SPA) · shadcn amber b1MJ9vRwY6

XPrivate v2 — Admin-MVP-1 Design Plan

Iter scopeAdmin/ops backoffice refactor (greenfield rebuild)
Ship target2026-06-30 (5-week sprint)
Hard deadline driverTahun Ajaran Jul 2026 enrollment
Stack core (locked 2026-05-26)React 19 + TanStack Start + shadcn/ui + Tailwind v4 · TanStack Form/Query/Router/Table · Cloudflare Workers (TS) · Hono + @hono/zod-openapi · Better Auth + Google OAuth + Negation ACL RBAC · Neon Postgres 18 + CF Hyperdrive · Drizzle ORM · Cloudflare R2 (presigned URL, private ACL) · SigNoz · Paraglide JS (BI iter-1, EN iter-2) · Bun Workspaces monorepo
AudienceEng partner (Engineering Director) — Ralph loop Claude Code execution
Doc versionv1.3 (2026-05-25)
Authority boundary Founder owns: product scope, domain decisions, business policies (cancel policy, pricing model, role split, deadlines, kill criteria).
Eng Partner owns: ALL engineering implementation — ORM, routing, validation, monorepo tool, repo structure, deployment pipeline, testing strategy, code style, Definition of Done, RTO/RPO, infra detail.
Implementation suggestions di doc ini = inspiration, bukan mandate. Partner overrides freely.
🎯 EXECUTIVE SUMMARY (30-second read)

Table of Contents

0. READ THIS FIRST — Context & Background

Untuk partner Engineering Director Section ini kasih konteks lengkap sebelum lu dive ke epic + diagram + schema. Skip kalo udah familiar — tapi sangat recommended baca dulu biar gak bingung "why" di belakang setiap design decision.

0.1 Tentang XPrivate (product context)

XPrivate = Indonesian les-privat (private tutoring) marketplace. Operational platform existing dengan 4 sub-systems:

Customer base: orangtua + siswa (SD - Mahasiswa - Profesional), tutor (independent contractor, golongan tier 1-4 + non-gol). Postpaid monthly billing. Sesi di rumah siswa atau online (Zoom manual). Indonesia Jakarta-region primarily.

0.2 Why greenfield rewrite (bukan selective refactor)

Existing platform punya tech debt yang block fitur baru + ops scale:

Hard deadline driver: Tahun Ajaran Jul 2026 enrollment. Pricing matrix wajib aktif sebelum siswa daftar ulang TA baru. Selip = kehilangan enrollment cycle.

Decision lock: greenfield rebuild full, BUKAN selective refactor. Legacy 4 repos di-deprecate. Data migration deferred (fresh schema v2, bulk-import reference data only — Tutor, Siswa, Subject, Tingkat, Config — transactional history archive read-only).

0.3 Why admin-first iter

Iter 1 = admin/ops backoffice ONLY. Customer-facing (siswa/tutor self-service) defer iter berikutnya. Reasoning:

  1. De-risk foundation — operator catch bugs sebelum customer kena impact
  2. Smaller user surface — admin = 1-5 orang vs customer = ratusan/ribuan. Iterate fast tanpa support load
  3. Validate data model — pricing matrix + state machine v2 + snapshot pattern proven jalan untuk ops sebelum expose ke customer
  4. Industry pattern — Stripe, Shopify, Square = backoffice-first then customer-facing

0.4 Why this stack (locked)

Engineering implementation micro-decisions (ORM, routing, validation lib, monorepo tool, repo structure, deployment pipeline, testing strategy) = partner authority (see Section 3).

0.5 Why Option (b) cancellation policy (financial double-down)

Multi-voice council review (Architect + Skeptic + Pragmatist + Critic premortem + Forecaster + Red-Teamer) recommended Option (a) — industry standard zero pay + 3-strike access sanction (mirror Italki / Preply / Cambly pattern).

Founder override pilih Option (b) — zero pay + additional 1-sesi denda graduated (100%/50%/25% berdasar lead time). Rationale:

Full council voices + decision log: companion file 2026-05-25-xprivate-tutor-cancel-policy-v1.md (Downloads). Wajib baca kalo lu mau understand WHY policy keras ini chosen against council recommendation.

0.6 Existing platform reference state (legacy)

Legacy 4 sub-repos tetap accessible buat reference behavior current. Lu udah ada akses (per founder confirm 2026-05-25):

RepoStackPurpose in iter 1
xp-backendSpring Boot 3.1.10Business logic source-of-truth current. Reference saat butuh "behavior current apa" tapi JANGAN port logic langsung — rewrite cleanly di TS Workers.
xp-appAngular 20 PWAConsumer UX reference. Iter 1 admin-only = gak di-rebuild, tetap legacy. Iter 2 customer-facing = rewrite React + TanStack Start (Capacitor SPA mode for mobile).
xp-backofficeAngular 20 Clarity v17Admin UX reference. Total rewrite ke React 19 + TanStack Start + shadcn/ui + Tailwind v4 di iter 1.
xp-landingpageNext.js 15Marketing site. Keep separate, gak touch iter 1.

Detailed code-level archaeology available on-demand: file 2026-05-23-business-flow-overview.md (~51KB, 600 lines). Cover 9 domain modules, endpoint inventory, state machine current behavior, file storage pipeline, security gaps. Kontak founder kalo lu butuh reference deep current behavior selama rebuild.

0.7 Working methodology

0.8 Indonesia-specific context

Legal entity status (penting buat finance + compliance):

Legal context cancellation policy:

Tutor relationship: independent contractor (perjanjian kerja sama, NOT UU Ketenagakerjaan employment). Contract clause WAJIB explicit untuk deduction enforce. Founder schedule legal counsel review (~1-2h consultation, budget Rp 1-3M, target Jun week 2) sebelum tutor re-sign.

Cultural context: tutor relationship lebih personal dari transactional di Indonesia formal tutoring. Strict policy bisa lose tutor faster. Goodwill belasungkawa budget (off-system, finance authority) untuk severe FM cases (keluarga inti meninggal, kecelakaan parah).

0.9 Founder commitment context

Founder originally pure PM role (75% product, 25% product-eng). Untuk meet hard deadline 2026-06-30, founder commit full-time eng + PM (~40h/week dev + 5-10h/week PM decisions) selama sprint. Combined dengan partner part-time (~20h/week) + AI augment = ~100-130 productive hours/week capacity. 5-week intensive sprint sustainable, beyond risky.

0.10 Pre-launch tutor pilot

Sebelum full v2 rollout (post 2026-06-30 ship), founder pilot Option (b) cancel policy ke 5 candidate tutors. Re-onboarding conversation + Option (b) policy explanation + observe reaction. Kill criterion: ≥2 refuse re-sign citing cancel policy → revisit Option (b) → (a). Pilot target: Jun week 3-4. Founder owns.

0.11 What's already done vs pending

ItemStatus
Spec design v1.3 (brainstorm + reviewer + RAT)✓ Done
Pricing matrix RAT (operator role-play, 5D keep)✓ Done
Council review cancel policy✓ Done
Multi-agent review (product + feature concept reviewer)✓ Done
Stack lock (React 19 + TanStack Start + shadcn/ui + CF Workers + Neon 18 + R2 + SigNoz, full list)✓ Done 2026-05-26 (revised per partner alignment)
Data migration strategy lock✓ Done
Existing repo access partner✓ Done
Information Architecture + design handover⏳ Coming
Jira tickets w/ detailed acceptance criteria⏳ Coming
Legal counsel cancel policy review⏳ Founder, Jun wk2
Pre-launch tutor pilot 5 tutors⏳ Founder, Jun wk3-4
Partner Sprint 0 (repo bootstrap + infra)⏳ Partner, before 2026-05-26

0.12 How to navigate this doc

  1. Read Section 0 (this) — context
  2. Skim diagrams (5-10): journey × 3 + state machine + pricing model + cancel flow
  3. Domain glossary (Section 4) — lock terms
  4. Epic List (Section 11) — table + full story detail
  5. Data Model (Section 12) — schemas (Drizzle illustration, partner pick ORM)
  6. Workflows narrative (11.5) — text complement diagrams
  7. Roadmap + Kill Criteria (Section 13) — sprint plan
  8. UX critical affordances (Section 14) — must-haves UI
  9. Open questions (Section 17) — resolve di writing-plans phase

Companion docs di same Downloads folder:

On-demand reference (kontak founder kalo butuh):


1. Overview

XPrivate adalah Indonesian private tutoring (les-privat) marketplace. Existing platform (Spring Boot 3.1.10 + Angular 20 + Next.js + PostgreSQL) di-deprecate. v2 = full greenfield rebuild.

Iter 1 fokus: admin/ops backoffice ONLY. Customer-facing (siswa/tutor self-service signup, payment gateway, auto-notif) defer ke iter berikutnya.

Why admin-first? De-risk foundation (operator catch bugs sebelum customer), smaller user surface (1-5 admin vs ratusan customer), validate data model, common pattern (Stripe / Shopify / Square backoffice-first).

2. Goals + Non-goals

Goals (iter 1)

Non-goals (defer iter 2+)

Known-risk skips (explicit, with mitigation)

SkipRationaleMitigation
2FA admin (TOTP)Rely on Google OAuth login w/ admin's Google account 2FA enabledPolicy/training enforce admin Google account 2FA on. Add to admin onboarding checklist.
Login audit + account lockoutDefer iter 1.5Mitigate: rate-limit at Cloudflare layer sebagai cheap workaround.
Indonesia invoice format (PPN, kuitansi)Skip — belum berbadan hukum, belum PKPSimple invoice format OK. Add formal compliance saat BH + PKP.
PDP UU 27/2022 formal complianceSkip detailed — basic consent capture only in admin formAdd formal compliance + DSAR endpoint iter 2 saat customer-facing.
Detailed PPh withholdingIter 1 = placeholder field only, handle deduction off-system manualFormalize at finance system level + iter 2 honor breakdown.

3. Tech Stack

Stack update 2026-05-26 Stack revisi major dari v1.3 spec (Svelte → React + TanStack universe) per partner architecture alignment. Full decision log: ~/Documents/Obsidian Vault/Decisions/2026-05-26-xprivate-admin-mvp-architecture-alignment.md. Partner architecture page: xp-mvp-architecture.pages.dev. Partner-discussion doc: ~/Workspaces/XPrivate/handover/2026-05-26-partner-discussion-rbac-cron-invite.md (v3).

Locked — founder authority (product/UX scope)

LayerPick
Frontend frameworkReact 19 + TanStack Start (Vite-based fullstack React, CF Workers via Nitro adapter)
UI componentsshadcn/ui (radix variant) + Tailwind CSS v4
ThemeLight mode only iter-1 (dark mode defer iter-2 if demand surface)
Form libraryTanStack Form + Zod standard-schema validator
Data fetchingTanStack Query (server-side pagination for schedule/audit/billing/payouts/students/tutors; client-side for bounded master data + pricing matrix per-subject)
RoutingTanStack Router
TablesTanStack Table
i18nParaglide JS (BI iter-1, EN iter-2) — all UI strings via m.session_approve() style
Auth strategyBetter Auth + Google OAuth + Workspace domain restrict (@xprivate.education). Effective MFA via Google account 2FA
RBAC modelPermission-list-based + Negation ACL resolution (union → dedupe → negation, cached in WhoAmI). 3 preset role bundles hardcoded iter-1 (ADMIN superuser + OPS + FINANCE)
Storage ACLFull private R2 + S3 presigned URL iter-1 (signed URL endpoint, 15-min expiry, per-photo-type auth check matrix). PDP UU 27/2022 compliance step
Naming conventionssnake_case (data) + camelCase (code) + kebab-case (files). No translate layer (data fields snake_case end-to-end)
Cron timezoneWIB (Asia/Jakarta UTC+7) for all scheduled jobs
Permission key format<resource>:<action> lowercase, wildcard *, negation ! prefix, resource singular

Locked — partner authority (engineering implementation, locked 2026-05-26)

LayerPick
Backend runtimeCloudflare Workers (TypeScript) + Wrangler
API layerHono /api/* mounted via Nitro adapter on TanStack Start backend
API spec@hono/zod-openapi auto-generated OpenAPI from Zod schemas
DatabaseNeon Postgres 18 + CF Hyperdrive (edge-cached connection pool)
ORMDrizzle ORM + Drizzle Kit (migrations + schema management)
UUID strategyuuidv7() PG18 native
ValidationZod snake_case DTOs
Object storageCloudflare R2 + S3 presigned URL (single source of truth, no public ACL)
ObservabilitySigNoz OTEL telemetry-js
Unit testsVitest in CI
Integration testsVitest + Manual (MVP) + Neon Branch for test DB
E2E testsPlaywright
CI/CDCloudflare Workers CI
Monorepo toolingBun Workspaces (NOT runtime — Wrangler runtime CF Workers, Bun for dev/build only)
Repo structureapps/web (TanStack Start + Hono) · apps/cron-<task-name> (CF Cron Workers) · packages/db (Drizzle schema + migrations + client) · packages/service (business logic + Zod) · packages/auth (RBAC resolution + middleware)

Pending partner Sprint 0 (still partner authority)

4. Domain Glossary (LOCKED)

Use these exact terms in code + UI Naming inconsistency = downstream bugs. Lock di Sprint 1.
TermMeaning
SesiUnit pengajaran (1 lesson, default 60-90 menit, default 1:1)
TingkatTingkat pendidikan siswa (SD 1-6, SMP 7-9, SMA 10-12, Mahasiswa, Umum, Profesional)
SegmentasiTier layanan (REGULER, REGULER_PLUS, INTERNASIONAL)
KategoriGrouping subject (REGULER_KELAS, PROGRAM_KHUSUS, SERTIFIKASI, BAHASA_ASING, MUSIK, KOMPUTER, ...)
SubjectMata pelajaran konkret (e.g. "Bahasa Mandarin", "Piano", "CPNS")
SubjectLevelOptional child per Subject (e.g. HSK 1-6 under Mandarin, Grade 1-8 under Piano)
GolonganTier tutor (GOL_1, GOL_2, GOL_3, GOL_4, NON_GOLONGAN; 1 tertinggi)
Tahun AjaranJul-Jun (e.g. 2026/2027 = Jul 2026 - Jun 2027)
TagihanStudent-side invoice per bulan (postpaid)
HonorariumTutor-side payout per bulan (internal — siswa never sees)
SlotAvailable time window dari tutor

5. Admin Journey

flowchart LR subgraph LOG[Login] direction TB L1[Open backoffice] --> L2[Google OAuth login] end subgraph MD[Master Data] direction TB M1[CRUD Tutor] --> M2[CRUD Siswa] --> M3[Edit Pricing Matrix] --> M4[Edit Cancellation Policy] end subgraph AQ[Approval Queue] direction TB Q1[Review REQUESTED sesi] --> Q2[Bulk approve / reject] --> Q3[Handle FM claims L1] end subgraph REC[Reconciliation] direction TB R1[Mark Settlement PAID bulk] --> R2[Mark Honor PAID bulk] --> R3[Refund / credit note] end subgraph REP[Reports] direction TB P1[Generate monthly reports] --> P2[Export Excel] --> P3[Period close checklist] end LOG --> MD --> AQ --> REC --> REP style LOG fill:#dbeafe,stroke:#1e40af style MD fill:#fef3c7,stroke:#b45309 style AQ fill:#d1fae5,stroke:#10b981 style REC fill:#fee2e2,stroke:#ef4444 style REP fill:#e9d5ff,stroke:#7e22ce

6. Tutor Journey

Note Tutor app refactor defer iter customer-facing. Iter 1 tutor interactions = manual WhatsApp + existing tutor app (legacy).
flowchart LR subgraph BR[Booking Received] direction TB B1[Notif via WhatsApp from admin] --> B2[Review sesi detail] end subgraph PRE[Pre-Sesi] direction TB P1[Confirm availability] --> P2[Plan materi] end subgraph SES[Sesi] direction TB S1[Travel to lokasi] --> S2[Start sesi - IN_PROGRESS] --> S3[Teach] --> S4[Log materi + foto report] --> S5[Confirm completion] end subgraph POST[Post-Sesi] direction TB PO1[Wait admin validation] --> PO2[Sesi to COMPLETED] end subgraph MON[Monthly] direction TB M1[Receive honor slip] --> M2[Verify bank transfer] end BR --> PRE --> SES --> POST --> MON style BR fill:#dbeafe,stroke:#1e40af style PRE fill:#fef3c7,stroke:#b45309 style SES fill:#d1fae5,stroke:#10b981 style POST fill:#fee2e2,stroke:#ef4444 style MON fill:#e9d5ff,stroke:#7e22ce

7. Siswa / Parent Journey (admin-led iter 1)

flowchart LR subgraph INT[Intake operator-led] direction TB I1[Lead via WhatsApp] --> I2[Operator log siswa + parent data] --> I3[Receive credentials via WhatsApp] end subgraph BOOK[Booking] direction TB B1[Request via WhatsApp] --> B2[Admin create sesi] --> B3[Admin approve] end subgraph SES[Sesi] direction TB S1[Tutor datang ke rumah] --> S2[Belajar] --> S3[Tutor log materi] end subgraph BILL[Billing postpaid] direction TB BI1[Receive invoice bulanan] --> BI2[Bayar via bank transfer] --> BI3[Upload bukti bayar] --> BI4[Operator verify mark PAID] end subgraph PROG[Progress] direction TB PR1[Receive monthly progress report] end INT --> BOOK --> SES --> BILL --> PROG style INT fill:#dbeafe,stroke:#1e40af style BOOK fill:#fef3c7,stroke:#b45309 style SES fill:#d1fae5,stroke:#10b981 style BILL fill:#fee2e2,stroke:#ef4444 style PROG fill:#e9d5ff,stroke:#7e22ce

8. Schedule State Machine (M3)

7 states + 2 sub-states Sub-states: NO_SHOW_STUDENT, NO_SHOW_TUTOR (COMPLETED variants with billing rules).
stateDiagram-v2 [*] --> REQUESTED: create sesi REQUESTED --> APPROVED: admin approve REQUESTED --> REJECTED: admin reject (terminal) APPROVED --> IN_PROGRESS: tutor start APPROVED --> CANCELLED: cancel (terminal) APPROVED --> RESCHEDULED: reschedule IN_PROGRESS --> COMPLETED: tutor confirm + admin validate (2-step) RESCHEDULED --> REQUESTED: new linked sesi CANCELLED --> [*] REJECTED --> [*] COMPLETED --> [*]
StateMeaningTrigger by
REQUESTEDSesi diminta, nunggu admin approveAuto on create
APPROVEDAdmin approve, slot di-lock, snapshot pricing configAdmin only
REJECTEDAdmin tolak (terminal)Admin only
IN_PROGRESSSesi mulaiTutor (Start)
COMPLETEDSesi selesai + materi confirmedTutor confirm + admin validate
CANCELLEDDibatalin post-APPROVED, w/ punishment (terminal)Siswa/tutor/admin
RESCHEDULEDOld sesi closed, new linked sesi createdSiswa/tutor/admin

9. Pricing Matrix Model (M2)

5D matrix complexity Pricing matrix tutor = Subject × SubjectLevel? × Tingkat × Segmentasi × Golongan. Atomic clone+bump UI + drill-down navigation mandatory (per RAT 2026-05-25 finding).
erDiagram KATEGORI ||--o{ SUBJECT : contains SUBJECT ||--o{ SUBJECT_LEVEL : has SUBJECT ||--o{ PRICING_MATRIX : refs SUBJECT_LEVEL ||--o{ PRICING_MATRIX : refs TINGKAT ||--o{ PRICING_MATRIX : refs SEGMENTASI ||--o{ PRICING_MATRIX : refs GOLONGAN ||--o{ PRICING_MATRIX : tutor_only SUBJECT { int id PK string nama int kategori_id FK bool has_levels string status } SUBJECT_LEVEL { int id PK int subject_id FK string nama string kode int sort_order } PRICING_MATRIX { int id PK int subject_id FK int subject_level_id FK int tingkat_id FK int segmentasi_id FK string audience int golongan_id FK decimal amount date effective_from date effective_until string status } KATEGORI { int id PK string nama } TINGKAT { int id PK string nama int sort_order } SEGMENTASI { int id PK string nama } GOLONGAN { int id PK string nama int tier_rank }

Snapshot semantic

Pricing matrix di-snapshot ke schedule.pricing_matrix_id_student + schedule.pricing_matrix_id_tutor saat sesi APPROVED. Schedule immune dari pricing change kemudian. Tahun ajaran transition = bulk-clone + bump, atomic operation (1 click).

10. Cancellation Flow

flowchart TD A[Tutor cancel sesi] --> B{Lead time} B -->|>4h| Z1[No penalty] B -->|≤4h| C{Force majeure?} C -->|Yes| FM[Claim FM + evidence] FM --> L1{L1 review by operator} L1 -->|Approve| Z2[No penalty] L1 -->|Reject| BD[Banding 14d window] BD --> L2{L2 review by senior admin} L2 -->|Overturn| Z3[Refund penalty] L2 -->|Uphold| ESC1[Final - penalty stands] C -->|No| D{Self-sub success?} D -->|Yes| Z4[NO penalty - clean] D -->|No - escalate xprivate| E[Penalty applied] E --> E1[Zero pay sesi cancelled] E --> E2[Denda graduated 100% / 50% / 25%] E --> E3[+1 strike] E3 --> SC{Strike count in 90d} SC -->|1| S1[Warning] SC -->|2| S2[Suspend 7 days] SC -->|3| S3[Terminate kerjasama] style Z1 fill:#d1fae5,color:#000 style Z2 fill:#d1fae5,color:#000 style Z3 fill:#d1fae5,color:#000 style Z4 fill:#d1fae5,color:#000 style E fill:#fee2e2,color:#000 style S3 fill:#fee2e2,color:#000

Denda graduated (CONFIG, runtime-editable)

Lead timeDendaStrike
≤ 1 jam100% of 1 sesi cost+1
≤ 2 jam50%+1
≤ 4 jam25%+1
> 4 jam0% (no penalty)0

Siswa cancel rules

Lead timeCharge siswaHonor tutor
< 1h100%100% (no transport)
< 2h50%50%
< 4h25%25%
≥ 4h00

11. Epic List (8 epics)

EpicOutcomeSprint
M1 — User Management (Admin) Operator login (Google OAuth + Workspace restrict) + CRUD admin + role/privilege Sprint 1
A1 — Audit Trail Immutable audit middleware + 7y retention + entity search Sprint 1
M2 — Master Data Management Tutor, Siswa, Subject+Level, Tingkat, Segmentasi, Kategori, Pricing Matrix 5D, Cancellation Policy Editor, Config, audit, validation Sprint 2 (2wk)
M3 — Schedule Management State machine v2 + approval queue + cancel calc engine + FM workflow + substitute matching + reschedule + materi log + calendar view + conflict detection + no-show + bulk reschedule Sprint 3
M4 — Reporting Student progress + tagihan siswa + honorarium tutor + bulk mark-paid + outstanding aging + materi visibility + reminder broadcast + reconciliation export Sprint 4
M6 — HR Discipline + Termination Strike escalation + suspend + terminate + final settlement + access revoke + archive + reactivation Sprint 4
M7 — Finance Workflow Refund/credit note + period close + outstanding aging + PPh placeholder + honor payment slip + backup/DR Sprint 4
A2 — Notification Trigger Manual welcome email trigger dari M1/M2 forms Sprint 4

Epic Stories — Full Detail

M1 — User Management (Admin)

M2 — Master Data Management

  1. Tutor CRUD — profile (nama, golongan, kontak, alamat, lat/long, bio, foto, kontrak PDF upload + signed flag), subject list, slot mgmt
  2. Siswa + Parent CRUD — profile (nama, tingkat, segmentasi, parent fields, alamat, lat/long, foto), preferred tutor (TeacherMatch)
  3. Subject + SubjectLevel — parent+child schema, has_levels flag
  4. Tingkat CRUD — stable individual rows
  5. Segmentasi CRUD — operator-managed
  6. Kategori CRUD — operator-managed
  7. Pricing Matrix (Student) — 5D versioned per Tahun Ajaran, mid-flight override OK, sparse allowed
  8. Pricing Matrix (Tutor) — same + Golongan dim
  9. Cancellation Policy Editor — bracket threshold + denda % runtime configurable
  10. App Config — transport bracket, session default duration, strike decay, FM appeal window, dll
  11. Pricing audit log — every matrix change (who, when, from-to, reason)
  12. Bulk operations — copy row, copy column, %-bump, atomic clone+bump Tahun Ajaran
  13. Coverage gap detector — warn "Subject X has 60% combos populated"
  14. Soft-delete + cascade rules — Subject/Tingkat delete = ARCHIVED, FK preserved
  15. Data validation rules — phone, email, lat/long, required fields
  16. Duplicate detection — siswa/tutor by phone/email → soft-warn
  17. Pricing matrix sanity check — negative amount prevent, abnormally high flag

M3 — Schedule Management

M4 — Reporting

A1 — Audit Trail (cross-cutting)

A2 — Notification Trigger (narrowed)

M6 — HR Discipline + Termination

M7 — Finance Workflow

Refund / credit note flow:

Financial period close:

Outstanding aging integration: reuse M4 dashboard + bad debt write-off flow (senior approval).

Reconciliation: export template (Excel finance-familiar) + adjustment audit trail.

PPh withholding placeholder: pph_withheld_amount field per honor batch, operator input manual.

Honor payment slip: tutor receive slip w/ breakdown (base + transport + extra-time + adjustments + PPh). Export PDF per tutor per month.

Backup + DR strategy: daily backup procedure + restore drill cadence + retention policy. RTO/RPO targets + implementation: partner authority.

11.5 Workflows Narrative

Booking happy path

1. Admin create sesi → REQUESTED
2. Admin review approval queue → APPROVED (or REJECTED)
3. Tutor mark Start → IN_PROGRESS (validation: only from APPROVED)
4. Tutor mark attendance + materi + foto report
5. Admin validate completion → COMPLETED (2-step)
6. Settlement auto-created (UNPAID, snapshot pricing dari APPROVED config)
7. Monthly billing cycle (M4):
   - Tagihan siswa generated
   - Siswa bayar offline, upload bukti
   - Operator mark Settlement PAID (bulk capable)
8. Monthly honorarium cycle (M4):
   - Honorarium tutor generated
   - Finance batch transfer offline
   - Operator mark SettlementTeacher PAID (bulk capable)

Force Majeure workflow

Tutor claim FM (via WhatsApp ke admin iter 1)
  ↓
Admin entry claim di backoffice + evidence
  ↓
L1 Operator review → APPROVE (no penalty) atau REJECT (normal escalate)
  ↓ (if REJECT)
Tutor banding dalam 14d window
  ↓
L2 Admin review → UPHOLD (final) atau OVERTURN (refund penalty)
  ↓
Audit log all events
Optional comp_notes field — finance off-system bayar belasungkawa

Substitute matching (admin-side, iter 1)

Tutor X cancel via WhatsApp → admin info
  ↓
Admin open schedule detail X → klik [Cari Sub]
  ↓
List candidates sorted by distance ascending:
  Filter: subject match + slot bebas + status ACTIVE
  Display: nama, distance km, golongan, last_active, [Pilih]
  ↓
Admin pilih candidate Z → dispatch via WhatsApp manual
  ↓
Z accept via WhatsApp → admin update sesi assignment ke Z
  ↓
Sesi jalan dengan Z

12. Data Model (key schemas)

Schema notation Examples below pakai Drizzle syntax sebagai illustration. Partner authority: pick ORM + adjust syntax. Schema shape + semantics = locked (founder via business rules). Implementation = partner.

Subject + SubjectLevel (parent + nullable child)

// Example syntax (Drizzle illustration — partner pick ORM)
const subject = pgTable('subject', {
  id: serial('id').primaryKey(),
  nama: text('nama').notNull(),
  kategoriId: integer('kategori_id').references(() => kategori.id),
  hasLevels: boolean('has_levels').default(false),
  deskripsi: text('deskripsi'),
  sortOrder: integer('sort_order').default(0),
  status: text('status').default('ACTIVE'),
});

const subjectLevel = pgTable('subject_level', {
  id: serial('id').primaryKey(),
  subjectId: integer('subject_id').references(() => subject.id).notNull(),
  nama: text('nama').notNull(),
  kode: text('kode').notNull(),
  sortOrder: integer('sort_order').default(0),
  status: text('status').default('ACTIVE'),
});

Pricing Matrix

const pricingMatrix = pgTable('pricing_matrix', {
  id: serial('id').primaryKey(),
  subjectId: integer('subject_id').references(() => subject.id).notNull(),
  subjectLevelId: integer('subject_level_id').references(() => subjectLevel.id),
  tingkatId: integer('tingkat_id').references(() => tingkat.id).notNull(),
  segmentasiId: integer('segmentasi_id').references(() => segmentasi.id).notNull(),
  audience: text('audience').notNull(), // STUDENT | TUTOR
  golonganId: integer('golongan_id').references(() => golongan.id),
  amount: numeric('amount', { precision: 20, scale: 4 }).notNull(),
  effectiveFrom: date('effective_from').notNull(),
  effectiveUntil: date('effective_until'),
  status: text('status').default('ACTIVE'),
});

Schedule (key fields)

const schedule = pgTable('schedule', {
  id: serial('id').primaryKey(),
  status: text('status').notNull(), // 7 states
  subjectId: integer('subject_id').references(() => subject.id).notNull(),
  subjectLevelId: integer('subject_level_id').references(() => subjectLevel.id),
  tutorId: integer('tutor_id').references(() => tutor.id).notNull(),
  siswaId: integer('siswa_id').references(() => siswa.id).notNull(),
  sesiDateTime: timestamp('sesi_date_time').notNull(),
  sesiMode: text('sesi_mode').notNull(),

  // Pricing snapshot (immune from later changes)
  pricingMatrixIdStudent: integer('pricing_matrix_id_student').references(() => pricingMatrix.id),
  pricingMatrixIdTutor: integer('pricing_matrix_id_tutor').references(() => pricingMatrix.id),
  amountStudent: numeric('amount_student'),
  amountTutor: numeric('amount_tutor'),
  transportFee: numeric('transport_fee'),

  // Cancellation policy snapshot
  cancellationPolicyConfigId: integer('cancellation_policy_config_id'),

  // Lifecycle
  createdAt: timestamp('created_at').defaultNow(),
  approvedAt: timestamp('approved_at'),
  startedAt: timestamp('started_at'),
  completedAt: timestamp('completed_at'),
  cancelledAt: timestamp('cancelled_at'),
  cancelReason: text('cancel_reason'),
  cancelInitiator: text('cancel_initiator'),
  cancelLeadTimeHours: numeric('cancel_lead_time_hours'),
  cancelDendaAmount: numeric('cancel_denda_amount'),
  cancelDendaAmountOverride: numeric('cancel_denda_amount_override'),

  // Linked
  rescheduledToScheduleId: integer('rescheduled_to_schedule_id'),
});

Cancellation Policy Config + Bracket

const cancellationPolicyConfig = pgTable('cancellation_policy_config', {
  id: serial('id').primaryKey(),
  policyType: text('policy_type').notNull(), // TUTOR_CANCEL | SISWA_CANCEL
  effectiveFrom: date('effective_from').notNull(),
  effectiveUntil: date('effective_until'), // NULLABLE = current
  status: text('status').default('ACTIVE'),
  notes: text('notes'),
});

const cancellationBracket = pgTable('cancellation_bracket', {
  id: serial('id').primaryKey(),
  configId: integer('config_id').references(() => cancellationPolicyConfig.id).notNull(),
  sortOrder: integer('sort_order').notNull(),
  thresholdHours: numeric('threshold_hours', { precision: 5, scale: 2 }).notNull(),
  chargePctSiswa: numeric('charge_pct_siswa', { precision: 5, scale: 2 }), // SISWA_CANCEL
  honorPctTutor: numeric('honor_pct_tutor', { precision: 5, scale: 2 }), // SISWA_CANCEL
  substitutionDendaPct: numeric('substitution_denda_pct', { precision: 5, scale: 2 }), // TUTOR_CANCEL
  basePenaltyEnabled: boolean('base_penalty_enabled').default(true),
  strikeAdded: integer('strike_added').default(0),
  notes: text('notes'),
});

Force Majeure (claim + review + appeal)

const forceMajeureClaim = pgTable('force_majeure_claim', {
  id: serial('id').primaryKey(),
  scheduleId: integer('schedule_id').references(() => schedule.id).notNull(),
  tutorId: integer('tutor_id').references(() => tutor.id).notNull(),
  category: text('category').notNull(), // SAKIT | DUKA | DARURAT | BENCANA | LAINNYA
  reasonText: text('reason_text'),
  status: text('status').notNull(), // PENDING_L1 | APPROVED_L1 | REJECTED_L1 | PENDING_L2 | UPHELD | OVERTURNED
  compNotes: text('comp_notes'), // optional, operator catat info comp manual
  createdAt: timestamp('created_at').defaultNow(),
});

const forceMajeureReview = pgTable('force_majeure_review', {
  id: serial('id').primaryKey(),
  claimId: integer('claim_id').references(() => forceMajeureClaim.id).notNull(),
  reviewerUserId: integer('reviewer_user_id').notNull(),
  reviewLevel: text('review_level').notNull(), // L1 | L2
  decision: text('decision').notNull(), // APPROVE | REJECT | UPHOLD | OVERTURN
  reasonText: text('reason_text'),
  createdAt: timestamp('created_at').defaultNow(),
});

const forceMajeureAppeal = pgTable('force_majeure_appeal', {
  id: serial('id').primaryKey(),
  claimId: integer('claim_id').references(() => forceMajeureClaim.id).notNull(),
  tutorId: integer('tutor_id').references(() => tutor.id).notNull(),
  additionalReason: text('additional_reason'),
  submittedAt: timestamp('submitted_at').defaultNow(),
  deadline: timestamp('deadline').notNull(), // claim.review.created_at + 14d
});

12.5 App Config Keys (runtime-editable)

All editable via admin UI, audit-tracked. Defaults locked, runtime override allowed.

KeyDefaultNotes
tutor_cancel.self_sub_window_hours4Window di mana tutor bisa cari pengganti sendiri
tutor_cancel.no_penalty_threshold_hours4Cancel >X hours = no penalty
siswa_cancel.no_penalty_threshold_hours4Same for siswa cancel
strike_decay_days90Rolling window untuk strike count
strike_threshold_terminate3Strike count untuk terminate kerjasama
force_majeure.approval_levels2L1 + L2 banding
force_majeure.appeal_window_days14Days tutor bisa appeal post-reject
transport.bracket_short_km20< 20km = flat Rp 10k
transport.bracket_short_amount10000IDR for short bracket
transport.bracket_long_amount12000IDR for >20km bracket
session.default_duration_minutes90Default 1.5 jam per sesi

13. Roadmap + Kill Criteria

SprintScopeDurationTarget
Sprint 1 — FoundationM1 + A11 week2026-06-01
Sprint 2 — Master DataM2 (full 5D + atomic clone+bump + drill-down + coverage detector)2 weeks2026-06-15
Sprint 3 — Schedule CoreM3 (state machine + approval + cancel + FM + sub matching + reschedule + materi + calendar + conflict + no-show + bulk reschedule)1.5 weeks2026-06-25
Sprint 4 — Reporting + HR + Finance + PolishM4 + M6 + M7 + A2 + honor slip + hardening5 days2026-06-30
Mid-sprint health gate: 2026-06-16 Mandatory retro + scope cut option if >20h behind plan. Burnout signal triggers immediate re-plan.

Kill Criteria (Duke states + dates)

  1. 2026-06-01: Sprint 1 belum done → retro + scope cut (drop Google OAuth, email/pwd only) OR push deadline
  2. 2026-06-15: Sprint 2 belum done → defer Cancellation Policy Editor + Tutor contract upload ke iter 1.5
  3. 2026-06-25: Sprint 3 belum done → defer FM workflow + substitute matching UI ke iter 1.5
  4. 2026-06-30: Sprint 4 belum done → defer M6 + M7 ke iter 1.5 (post-Jul)
  5. 2026-07-15: Full scope belum done + burnout signal → mandatory pause, re-plan, escalate
  6. Pre-launch tutor pilot 5 tutors: ≥2 refuse re-sign citing cancel policy → revisit Option (b) → (a)

14. Critical UX Affordances (don't skip)

Per pricing matrix RAT 2026-05-25 findings Skip these = operator confusion + data corruption at launch.
  1. Atomic "Clone Tahun Ajaran" button — handles BOTH effective_until update on old rows + insert bumped new rows in single op (NOT 2 manual steps)
  2. Hierarchical drill-down navigation — Subject → Level → matrix per audience. NOT flat 6-column entry.
  3. Coverage gap detector — warn "Subject X has 60% combos populated (12/20)" with link to fix
  4. has_levels auto-show/hide — operator only sees Level picker when relevant
  5. CSV import — bulk entry path mandatory
  6. Per-row validation — surface errors immediately at input

15. Pre-Sprint-1 Checklist

ItemStatusOwner / Date
Pricing matrix RAT executed✓ Done2026-05-25 (kept 5D, Sprint 2 extended)
Stack locked (revised 2026-05-26)✓ DoneReact 19 + TanStack Start + shadcn/ui + Tailwind v4 · CF Workers · Neon Postgres 18 + Hyperdrive · R2 (presigned URL) · SigNoz · TanStack Form/Query/Router/Table · Better Auth + Negation ACL · Drizzle · Hono + @hono/zod-openapi · Paraglide JS · Bun Workspaces monorepo
Data migration strategy locked✓ DoneFresh schema + bulk-import reference
Legal counsel cancel policy review⏳ TBDFounder, Jun week 2
Pre-launch tutor pilot plan⏳ TBDFounder, Jun week 3-4
Google Workspace OAuth domain restrict⏳ Sprint 1 (M1)Eng partner
Mid-sprint health gate calendar block⏳ TBD2026-06-16

16. Repo Structure + DoD + Sprint 1 Kickoff

Repo structure — Partner authority

Founder gak prescribe folder layout. Partner pick monorepo tool + structure per familiar patterns + AI-augmented workflow.

Definition of Done — Partner authority

Partner define per team standards + ops capacity. Founder hanya require:

Sprint 1 Kickoff

Founder commits (this doc + companion markdown + IA handover — TBD):

Partner commits (Sprint 0, before 2026-05-26 kickoff):

Sprint 1 kickoff: 2026-05-26 — pair sync on Sprint 1 stories M1 + A1.

17. Open Questions (resolve di writing-plans phase)