XPrivate · Plan Implementasi

Postpaid Slot Subscription + Pricing Matrix

Subscription bulanan bayar belakangan, harga ditarik dari matriks 4-axis (grade × segmentasi × kategori × subject), slot bisa diisi item beda-beda per sesi. Online via Zoom masuk ke MVP. Invoice otomatis di-generate cron akhir bulan dari sesi yang benar-benar jalan. PWA installable dengan push reminder + offline read cache, kamera lewat browser API.

Status: Proposed Belum live · belum ada data Domain: backoffice
Filosofi sistem

Pencatatan + visualisasi, bukan enforcement

Sistem ini punya empat peran:

  1. Alat pencatatan untuk operasi yang sudah jalan offline — bukan gatekeeper. Tidak ada pembatasan waktu: guru boleh tap "confirm start" kapan saja, bahkan jam-jaman setelah sesi sebenarnya.
  2. Alat rekonsiliasi untuk admin — kalau ada dispute (mis. murid bilang gak ada les, guru bilang ada), admin buka satu sesi dan langsung lihat kronologi lengkap: kapan guru confirm start, kapan upload foto, kapan presence di-submit, dst.
  3. Alat monitoring progress — admin (dan ke depan: guru) bisa lihat perkembangan murid lewat data sesi: hadir/absen ritme, materi yang sudah dibahas (materialsCovered), catatan progress per murid (studentProgressNotes), homework yang di-assign. Pas murid stuck atau jarang masuk, kelihatan dari data — bukan cuma dari memori guru.
  4. Alat metrik bisnis — agregasi dari data operasi: mata pelajaran/kategori mana yang laku (sesi terbanyak), demographic murid (grade × segmentasi × kategori) dan guru (subject yang dipegang × volume sesi × honorarium), konversi subscribe → renewal, dropout pattern. Datanya sudah ada di tabel-tabel inti — tinggal query agregat.

Yang dijaga sistem cuma:

  1. Urutan logis tombol — "Confirm End" baru muncul/aktif setelah "Confirm Start" pernah ditekan. Tujuan: trail data konsisten.
  2. Syarat state transition — sesi baru bisa ke closed setelah ada foto evidence + presence. Bukan blokir waktu, tapi blokir state.
  3. Audit log kuat — semua aksi (confirm, upload, reschedule, verify) tercatat dengan timestamp, actor, dan transisi state-nya. Ini titik beratnya, karena ini yang admin pakai pas rekonsiliasi.
Sistem ngecek apakah datanya konsisten, bukan apakah operasinya patuh jadwal. Operasi offline tetap punya keleluasaan, sistem cuma memastikan jejaknya rapi — dan jejak itu gampang dibaca pas dibutuhkan.
Executive Summary
  1. Matriks harga 4-axis. Tiap kombinasi grade × segmentasi × kategori × subject = satu baris di price_list_items, lengkap dengan harga dan honorariumGuru. Murid cuma lihat total gabungan, bukan breakdown. tabel →
  2. Slot subscription, slot bebas. Murid beli paket cadence (mis. 8 slot/bulan, Senin & Rabu). Tiap slot bisa diisi item matrix berbeda — UI nyediain template "tiap Senin = item X" biar gak fill manual. alur lengkap →
  3. Postpaid, invoice dari cron. Tidak ada bayar di awal. Cron tanggal 1 bulan berikutnya jalan → sum semua sesi closed di bulan sebelumnya → bikin invoice draft. Admin review → issue → murid bayar. cron endpoint →
  4. Online (Zoom) masuk MVP. Tiap sesi punya mode = offline | online. Kalau online, zoomLink wajib (MVP: admin/guru isi manual; auto-generate via Zoom API di-defer). requirement →
  5. Tambah sesi mid-month → admin queue. Murid request, admin approve, sesi extra langsung masuk subscription bulan itu dan otomatis ke-include di invoice akhir bulan. slotAdd flow →
  6. Reschedule tetap dalam billing month. Murid request atau admin langsung, semua harus jatuh di billing month yang sama. aturan →
  7. Sesi 90 menit + 15 menit grace. Soft enforcement (presence flag withinGraceWindow). konstanta →
  8. Kontrol kehadiran ada di guru. Cuma guru yang tap confirm start/end (bisa kapan saja — sistem cuma ngecek urutan, bukan waktu). Murid tidak confirm. Foto les bareng murid wajib di-upload sebelum sesi bisa closed. alur sesi →
  9. Audit log + timeline view. Tiap aksi tercatat. Admin punya halaman timeline per-sesi buat lihat kronologi lengkap pas ada dispute. timeline endpoint →
  10. Struktur transaksi generik. Tabel transactions punya productType discriminator — siap buat bank soal dan produk lain ke depan tanpa rombak skema. latar belakang →
  11. PWA: push reminder + offline read cache. Web manifest + service worker, push notifications buat reminder pre-sesi (T-30 & T-15), guru telat confirm, invoice issued, dan status reschedule. Offline: data terakhir tetap kebaca (TanStack Query persister di IndexedDB) dengan indikator Last synced: Xm ago per screen. Camera lewat browser API (HTML media capture), bukan native wrapper. scope PWA →

Kenapa Ini Ada

Update bisnis flow yang signifikan setelah review operasi nyata:

  1. Harga ditarik dari matriks 4-axis — grade × segmentasi × kategori × subject. Tiap cell punya dua angka: harga (income XPrivate) dan honorariumGuru (income guru). Yang ditampilkan ke murid cuma jumlah keduanya — breakdown disembunyikan.
  2. Slot subscription dengan slot-flex — murid beli cadence package (N slot/bulan), tapi tiap slot bisa diisi item matrix yang beda. Realitas operasi: "tiap Senin Matematika, tiap Rabu Bahasa Inggris" — itu dua item matrix yang beda. UI bantu dengan weekly template biar tidak perlu fill manual tiap minggu.
  3. Postpaid — tidak ada bayar di awal bulan. Sesi jalan dulu sebulan, lalu cron akhir bulan generate invoice dari sesi yang benar-benar closed. Admin review, lalu issue ke murid.
  4. Online (Zoom) masuk ke MVP di sebelah offline.
  5. PWA masuk MVP — installable, push notification buat reminder operasional, offline read cache (data terakhir tetap kebaca), last synced indicator di tiap screen. Camera lewat browser API (HTML media capture).

Tabel orders yang lama sekalian dirombak jadi transactions — header generik buat semua produk, biar nanti pas mau jualan bank soal atau produk lain, gak perlu rombak skema lagi.

Belum live. Belum ada data production yang perlu di-migrate. Ini momen paling murah buat ganti skema dengan bersih.

Requirement Bisnis → Keputusan

Requirement Keputusan
Harga 4-axis (grade × segmentasi × kategori × subject) price_list_items di-key sama empat axis itu. Tiap baris punya harga + honorariumGuru (int IDR). Yang ditampilin ke murid = harga + honorariumGuru sebagai satu angka. Matrix sparse — admin gak perlu fill semua kombinasi, cuma yang valid.
Slot ↔ item matrix Per-slot bebas. Tiap lesson_sessions punya priceListItemId sendiri. UI nyediain weekly template helper ("tiap Senin = item X") biar gak perlu fill manual tiap minggu.
Segmentasi (Reguler / Reguler-Plus / Internasional) Per slot/sesi (bagian dari priceListItemId). Murid bisa campur antar sesi di satu bulan.
Cadence package cadence_packages = catalog admin: nama, slotsPerMonth, weeklyPattern (json opsional). Murni container — tidak ada harga di sini, semua harga datang dari matrix per slot.
Les online via Zoom Masuk MVP. lesson_sessions.mode = offline | online; kalau online, zoomLink wajib. MVP: admin/guru isi manual. Defer: auto-generate via Zoom API.
Bayar les sebulan (postpaid) Cron invoicing.monthlyClose jalan tanggal 1 bulan berikutnya. Sum semua sesi closed di bulan tagihan tsb (snapshot harga + honorarium per sesi) → bikin transactions status=draft. Admin review → issue → murid bayar.
Tambah sesi mid-month slot_add_requests (murid → admin). Approved = admin lanjut bikin lesson_sessions baru di subscription tsb. Otomatis ter-include di invoice akhir bulan.
Menu reschedule Dua jalur: (a) murid request → admin approve; (b) admin langsung reschedule. Tetep harus jatuh di billing month yang sama.
Durasi les 90 menit Tetep, gak variabel.
Jam tambahan free 15 menit Grace overtime: 90 + 15 bebas (sampai 105 menit total). Soft enforcement — cuma di-flag di presence.
Konfirmasi mulai / selesai sesi Cuma guru yang tap confirm start & end. Murid tidak confirm — di lapangan sering lupa / males, jadi operasi mandek. Tidak ada cek waktu: guru bisa tap kapan saja. Cuma urutan yang dijaga — tombol "selesai" baru muncul setelah "mulai" pernah ditekan.
Bukti foto les Wajib. Guru harus upload minimal satu foto les bareng murid sebelum sesi bisa closed. Foto ini sekaligus jadi kontrol kehadiran (gantiin fungsi murid-confirm).
PWA installable Web manifest + service worker. Install prompt diberi onboarding ringan — terutama iOS, karena push notifications cuma jalan kalau "Add to Home Screen" (iOS 16.4+).
Push notifications (reminder) Web Push API + VAPID. Triggers: (a) reminder pre-sesi T-30 & T-15 menit ke guru + murid; (b) guru: H+2 jam dari scheduledAt tapi belum confirm end / belum upload foto; (c) status update ke murid (invoice issued, reschedule / slot-add approve/reject); (d) admin: notif pas cron monthlyClose selesai.
Camera (upload foto evidence) Browser API via <input type="file" accept="image/*" capture="environment">. Buka native camera device, balik sebagai File, upload ke R2. Fallback ke galeri kalau permission ditolak. Native wrapper (Capacitor) di-skip — overkill.
Offline scope App shell + read cache. Service worker cache static assets + foto evidence yang sudah upload. Data API persisted via TanStack Query (@tanstack/query-async-storage-persister ke IndexedDB), SWR + refetchOnReconnect. Tidak ada write queue di MVP.
Stale data UI Tiap screen yang nge-render dari cache nampilin Last synced: Xm ago (dari query.dataUpdatedAt). Lewat threshold (5 menit untuk data day-of, 30 menit untuk catalog) → tampil badge "Data mungkin belum terbaru" + tombol refresh manual. Banner offline global pas navigator.onLine = false.

Business Flow

Alur dari murid subscribe sampai invoice akhir bulan diverifikasi. Garis putus-putus = jalur reschedule. Cron month-end di kanan-bawah yang ngumpulin sesi closed jadi invoice draft.

flowchart TD
    A([Murid pilih cadence package]) --> B[Subscribe
student_subscriptions] B --> C[Materialize sessions
dari weeklyPattern] C --> D[Slot fill: pilih item matrix
per sesi - subject + grade
+ segmentasi + kategori + mode] D --> E[(lesson_sessions
priceSnapshot + honorariumSnapshot
terkunci)] E --> F[Guru tap
confirm start] F --> G[Sesi jalan
90 + 15 menit grace
online: Zoom, offline: ketemu] G --> H[Guru tap
confirm end] H --> P[Guru upload foto les
bareng murid - WAJIB] P --> M[Guru submit presence
flag withinGraceWindow] M --> N([Sesi closed]) E -. reschedule .-> R{Jalur reschedule} R -- murid request --> S[Admin approve/reject] R -- admin langsung --> T[adminReschedule] S -- approved --> E T --> E E -. tambah sesi .-> Q[Murid request slot] Q --> QA[Admin approve] QA --> C N --> CR[Cron tanggal 1
bulan berikutnya] CR --> INV[(transactions
status: draft
sum harga + honorarium)] INV --> ADM{Admin review} ADM -- issue --> IS[(transactions
status: pending)] ADM -- cancel --> XC[(transactions
status: cancelled)] IS --> PAY[Murid upload bukti bayar] PAY --> VF[(transactions
status: paid)] VF --> AV{Admin verify} AV -- verify --> OK[(transactions
status: verified)] AV -- reject --> XR[(transactions
status: cancelled)] classDef state fill:#fef3c7,stroke:#b45309,color:#1c1917; classDef done fill:#dcfce7,stroke:#16a34a,color:#1c1917; classDef bad fill:#fee2e2,stroke:#b91c1c,color:#1c1917; class E,INV,IS,VF,OK state; class N done; class XC,XR bad;

Subscribe → materialize → slot fill → sesi jalan → cron month-end → invoice → pembayaran. Audit log (lesson_session_events) ngerekam tiap transisi.

User Journey

Tiga perspektif: murid (subscribe + ikut les + bayar invoice), guru (jalanin sesi + lihat honorarium), admin (operasi bulanan + invoicing). Angka 1–5 = level kepuasan di tiap step.

Journey Murid

journey
    title Murid: subscribe & bayar postpaid
    section Subscribe
      Buka aplikasi: 5: Murid
      Pilih cadence: 5: Murid
      Set weekly template: 4: Murid
      Confirm subscribe: 5: Murid
    section Les jalan
      Lihat jadwal: 5: Murid
      Ikut sesi 90m: 5: Murid, Guru
      Difoto bareng guru: 4: Murid, Guru
    section Tambah/reschedule
      Request tambah sesi: 3: Murid
      Request reschedule: 3: Murid
      Tunggu approve: 2: Murid
    section Bayar akhir bulan
      Terima invoice: 4: Murid
      Lihat total: 4: Murid
      Transfer: 3: Murid
      Upload bukti: 4: Murid
					

Murid: subscribe di awal bulan, bayar setelah bulan jalan.

Journey Guru

journey
    title Guru: jalanin sesi & lihat honorarium
    section Persiapan
      Lihat jadwal: 5: Guru
      Lihat zoom link: 4: Guru
    section Sesi jalan
      Tap confirm start: 5: Guru
      Ajar 90m: 5: Guru, Murid
      Tap confirm end: 5: Guru
      Upload foto: 4: Guru
      Submit presence: 4: Guru
    section Akhir bulan
      Lihat honorarium: 5: Guru
      Lihat agregat: 4: Guru
					

Guru: confirm + foto + presence per sesi, lihat honorarium snapshot.

Journey Admin

journey
    title Admin: operasi bulanan + invoicing
    section Catalog
      CRUD matrix harga: 4: Admin
      CRUD cadence package: 4: Admin
    section Operasi harian
      Approve slot-add: 3: Admin
      Approve reschedule: 4: Admin
      Cek session timeline: 5: Admin
    section Invoicing
      Cron jalan: 5: Admin
      Review draft invoice: 4: Admin
      Issue ke murid: 5: Admin
      Verify bukti bayar: 4: Admin
    section Payout
      Lihat aggregate guru: 4: Admin
      Export CSV: 3: Admin
					

Operasi admin sepanjang satu billing month.

Domain Model

Tabel baru / berubah (di packages/db/src/schema/)

Tabel Aksi Field penting
price_list_items New (matrix) id, grade (enum 15), segmentasi (enum 3), kategori (enum), subject (enum), harga (int IDR), honorariumGuru (int IDR), isActive, timestamps. Unique (grade, segmentasi, kategori, subject). Sparse — gak perlu isi semua kombinasi, cuma yang valid.
cadence_packages New id, name, slotsPerMonth, weeklyPattern (json nullable; per-weekday default mis. [{dow:1, defaultPriceListItemId:"…"}]), isActive, timestamps.
student_subscriptions New id, studentId, cadencePackageId, billingMonth (YYYY-MM), status (active | cancelled), timestamps. Unique (studentId, billingMonth). Replace lama lesson_packages.
lesson_sessions Reshape orderIdsubscriptionId; tambah priceListItemId, priceSnapshot (int), honorariumSnapshot (int) — locked saat session creation; subject tetap di sini (denormalized snapshot dari item matrix buat fast read); tambah mode enum (offline default, online), zoomLink (text, required kalau online); tambah rescheduledFromSessionId; drop studentConfirm*.
session_evidence Tighten Sudah ada. Sekarang wajib: minimal 1 row sebelum lesson_sessions.status bisa pindah ke completed/closed. Aturan di app-level (cek di submitPresence).
lesson_session_events New (audit) Audit log. id, sessionId, actorUserId, eventType (confirm_start | confirm_end | evidence_uploaded | presence_submitted | slot_filled | reschedule_requested | reschedule_approved | reschedule_executed | status_changed), fromState, toState, payload (json, nullable), createdAt. Append-only.
reschedule_requests New id, sessionId, initiatedBy (student | admin), requestedAt, proposedScheduledAt, status, reviewedByAdminId, reviewedAt, reason. Aturan: stay in same billingMonth.
slot_add_requests New id, subscriptionId, studentId, reason, status (pending | approved | rejected), reviewedByAdminId, reviewedAt, createdAt. Approve = admin lanjut bikin sesi baru.
transactions Rename + postpaid dari orders: id, transactionNo (TXN-YYYYMMDD-XXXX), studentId, productType enum (lesson_subscription sekarang; reserved: question_bank, …), subscriptionId (nullable buat produk non-lesson), totalAmount (int, locked saat issue), status (draftpendingpaidverified | cancelled), billingMonth, notes, issuedAt, paidAt, verifiedAt, timestamps. Cron-generated as draft.
payment_proofs Repoint orderIdtransactionId; satu proof per transaction.
orders Drop belum live, belum ada data — diganti transactions + student_subscriptions
push_subscriptions New id, userId, endpoint (unique), p256dh, auth, userAgent (nullable), createdAt, lastSeenAt. Index on userId. Satu row per device user yang opt-in ke push.

Konstanta

File baru: packages/shared/src/constants/lessons.ts

API Routes (oRPC, apps/api)

Catalog

Subscription + slot fill

Reschedule

Day-of session

Postpaid invoicing

Audit & timeline

PWA + push notifications

Frontend (apps/web)

PWA shell (semua role)

Murid

Guru

Admin

Migration

Karena belum live (belum ada data production):

  1. Bikin satu migration baru: drop orders + lama lesson_packages (kalau ada); bikin price_list_items (matrix), cadence_packages, student_subscriptions, lesson_session_events, reschedule_requests, slot_add_requests; reshape lesson_sessions + payment_proofs; bikin transactions fresh.
  2. Jalanin di D1 yang masih kosong — gak perlu backfill.

Dokumentasi yang Perlu Di-update (di PR yang sama)

Yang Masih Kebuka (non-blocking)

  1. Cron timezone & cutoff — UTC+7 tanggal 1 bulan berikutnya, tapi seberapa toleran kalau sesi closed telat (guru baru tap end keesokan harinya, dst)?
  2. Default grade per murid — di-lock dari profile (admin assign), atau pickable per slot? Draft sekarang: pickable, UI default ke profile grade kalau ada.
  3. Zoom API integration — manual link di MVP. Kapan upgrade? Single shared link per guru acceptable interim?
  4. Teacher payout disbursement — out of MVP scope. Admin export CSV?
  5. Slot-add cap — limit per bulan?
  6. Bad debt — kalau invoice bulan X belum lunas, block subscription bulan X+1?
  7. Kategori & subject lists — enum fixed vs seeded categories / subjects table? Subject list cenderung paling sering nambah.
  8. Reschedule notice window — min notice (24h)?
  9. Cancel subscription mid-month — sesi closed tetap masuk invoice; scheduled sesi di-cancel.
  10. Grace overtime enforcement — soft (warning + flag) vs hard. Default: soft.
  11. iOS A2HS onboarding — gimana cara nge-push UX supaya murid + guru iOS install PWA dulu? Banner edukasi di onboarding? Fallback reminder via email kalau belum installed?
  12. Reminder copy — final copy bahasa Indonesia per push event (mis. "Sesi Matematika kamu mulai 30 menit lagi 📚"). Detail bahas saat implementasi.
  13. Notification opt-out per channel — user bisa pilih push reminder, email, atau dua-duanya?
  14. VAPID key rotation — simpan di Cloudflare secret; rotasi setahun sekali?

Roadmap Post-MVP

Item yang sudah dipertimbangkan tapi sengaja di-defer karena cakupan implementasinya besar dan bukan blocker buat MVP.

Integrasi Google Calendar

Urutan Pengerjaan (PR kecil-kecil)

  1. Schema + migration + konstanta/enum di packages/shared (matrix, cadence_packages, student_subscriptions, lesson_session_events, slot_add_requests; transactions reshape; lesson_sessions reshape)
  2. Admin: matrix CRUD + cadence packages CRUD
  3. Murid: subscribe + slot fill UI + weekly template helper
  4. Session materialization dari template + slot editor
  5. Reschedule + slot-add request flows
  6. Day-of: guru confirm, evidence wajib, presence + grace flag
  7. Cron monthlyClose + admin invoice queue + payment proof flow
  8. Teacher payout aggregate view
  9. Online (Zoom) mode UI + zoomLink editor
  10. PWA setup: manifest + service worker + TanStack Query persister + offline banner + Last synced indicator
  11. Push notifications: push_subscriptions schema + subscribe/unsubscribe endpoints + permission flow + crons (upcomingSessions, unclosedSessions) + inline push triggers
  12. iOS A2HS onboarding flow (banner + edu screen + email fallback)
  13. Update docs ditarik bareng setiap PR yang nyentuh permukaannya