XPrivate · Plan Implementasi

Monthly Package + Generic Transaction Model

Geser bisnis flow: order per-sesi sekarang jadi paket bulanan prepaid, ditopang tabel transactions yang generik biar produk lain (misal bank soal) bisa nyambung tanpa rombak skema lagi nanti.

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

Pencatatan + visualisasi, bukan enforcement

Sistem ini punya dua 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.

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. Paket bulanan prepaid. Satu paket = satu billing month (tanggal 1 → akhir bulan). Murid bayar sekali di awal bulan untuk sejumlah sesi. alur lengkap →
  2. Verifikasi 1× per murid per bulan. Admin cek satu payment proof per paket, lalu sesi-sesinya dijadwalkan dalam bulan itu. journey admin →
  3. Harga otomatis dari grade + cadence. Murid pilih grade, lalu pilih kartu cadence yang tersedia; harga langsung muncul (prorated kalau daftar mid-month). requirement →
  4. Reschedule tetap dalam billing month. Sesi yang di-reschedule (murid request atau admin langsung) wajib jatuh di bulan tagihan yang sama. aturan →
  5. Struktur transaksi generik. Header transaksi siap menampung produk lain (mis. bank soal) di masa depan tanpa rombak skema. latar belakang →
  6. Sesi 90 menit + 15 menit grace. Durasi standar 90 menit, overtime sampai 15 menit gratis (cap 105 menit total). konstanta →
  7. Kontrol kehadiran ada di guru. Cuma guru yang tap confirm start/end (bisa kapan saja — sistem tidak ngecek waktu, cuma urutan: end butuh start dulu). Murid tidak confirm. Foto les bareng murid wajib di-upload sebelum sesi bisa closed. alur sesi →
  8. Audit log + timeline view untuk rekonsiliasi dispute. Tiap aksi (confirm, upload, reschedule, verify) tercatat. Admin punya halaman timeline per-sesi buat lihat kronologi lengkapnya pas ada perselisihan. timeline endpoint →
  9. Offline only untuk sekarang. Online di-defer; slot enum sudah disiapkan untuk fase berikutnya. scope →

Kenapa Ini Ada

Setelah review produk, ada pergeseran besar di bisnis flow. Order yang tadinya per-sesi sekarang jadi paket bulanan prepaid (per kalender, dari tanggal 1 sampai akhir bulan). Tabel orders yang sekarang juga sekalian di-rename 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
Kelas murid nentuin price list Price list cuma di-key sama grade (12 level: SD-1SMA-3). Gak pakai dimensi subject / teacher / mode.
Harga otomatis muncul pas pilih order Murid pilih grade dulu → server balikin price_list_items yang active → murid pilih kartu cadence → harga langsung kebaca.
Bayar les sebulan sekali (upload 1× per bulan) Prepaid bulanan, jendela per kalender (tanggal 1 → akhir bulan). Satu payment proof per transaction.
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). Enforce-nya soft — cuma di-flag di presence.
Les offline / online Defer online ke fase berikutnya. Sekarang offline doang. Slot enum-nya udah disiapin (mode: offline | online) biar gak rombak skema lagi nanti.
Konfirmasi mulai / selesai sesi Cuma guru yang tap confirm start & end. Konfirmasi dari murid dihapus — di lapangan sering lupa / males, jadi operasi mandek. Status sesi sekarang gerak berdasarkan input guru aja. Tidak ada cek waktu: guru bisa tap kapan saja. Cuma urutan yang dijaga — tombol "selesai" baru muncul setelah tombol "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).

Business Flow

Alur dari murid pilih grade sampai sesi closed. Garis putus-putus = jalur reschedule (bisa dari murid lewat approval, atau admin langsung).

flowchart TD
    A([Murid pilih grade]) --> B[Pilih cadence
dari price_list_items] B --> C[Server itung totalPrice
+ prorated kalau mid-month] C --> D[(transactions + lesson_packages
status: pending)] D --> E[Murid upload payment_proof] E --> F[(transactions
status: paid)] F --> G{Admin review} G -- verify --> H[(transactions
status: verified
package aktif)] G -- reject --> X[(transactions
status: cancelled)] H --> I[Admin bikin lesson_sessions
dalam billingMonth] I --> J[Guru tap
confirm start] J --> K[Sesi jalan
90 + 15 menit grace] K --> L[Guru tap
confirm end] L --> P[Guru upload foto les
bareng murid - WAJIB] P --> M[Guru submit presence
flag withinGraceWindow] M --> N([Sesi closed]) I -. reschedule .-> R{Jalur reschedule} R -- murid request --> S[Admin approve / reject] R -- admin langsung --> T[adminReschedule] S -- approved --> I T --> I 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 D,F,H state; class N done; class X bad;

Lifecycle transaksi → package → lesson_sessions, lengkap dengan jalur reschedule.

User Journey

Dua perspektif: pengalaman murid (booking & ikut les) dan pengalaman admin (operasi bulanan). Angka 1–5 = level happiness murid/admin di tiap step.

Journey Murid

journey
    title Murid: pesan paket les bulanan
    section Pilih paket
      Buka aplikasi: 5: Murid
      Pilih grade: 5: Murid
      Lihat cadence: 5: Murid
      Pilih cadence: 5: Murid
    section Bayar
      Lihat total: 4: Murid
      Transfer: 3: Murid
      Upload bukti: 4: Murid
      Tunggu verify: 2: Murid
    section Les jalan
      Lihat jadwal: 5: Murid
      Datang ke les: 5: Murid
      Ikut les 90m: 5: Murid, Guru
      Difoto bareng guru: 4: Murid, Guru
    section Reschedule
      Request: 3: Murid
      Tunggu approve: 2: Murid
      Lihat jadwal baru: 4: Murid
					

Perspektif murid dari pesan paket sampai sesi jalan.

Journey Admin

journey
    title Admin: operasi bulanan
    section Price list
      Bikin price list: 4: Admin
      Aktifkan cadence: 4: Admin
    section Verifikasi
      Buka antrian: 4: Admin
      Cek bukti bayar: 4: Admin
      Verify/reject: 5: Admin
    section Schedule sesi
      Pilih package: 4: Admin
      Assign teacher: 4: Admin
      Set jam sesi: 4: Admin
    section Reschedule
      Lihat request: 3: Admin
      Approve/reject: 4: Admin
      Edit jam langsung: 4: Admin
    section Presence
      Cek laporan: 4: Admin
      Cek grace flag: 4: 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 id, grade (enum), cadencePerWeek, sessionsPerMonth, monthlyPrice, isActive, timestamps
transactions Rename dari orders: id, transactionNo (TXN-YYYYMMDD-XXXX), studentId, productType enum (lesson_package sekarang; reserved: question_bank, …), totalPrice, status (pendingpaidverifiedcancelled), billingMonth (YYYY-MM, nullable buat produk yang gak bulanan), notes, timestamps
lesson_packages New id, transactionId, studentId, priceListItemId, billingMonth, sessionsTotal, sessionsScheduled, sessionsCompleted, proratedFromDay (nullable; null = sebulan penuh)
payment_proofs Repoint ganti orderIdtransactionId; satu proof per transaction
lesson_sessions Reshape tambah packageId (ganti orderId langsung); subject tetep di sini (pilih per sesi); tambah mode enum (offline default; online reserved); durationMs diambil dari konstanta; tambah rescheduledFromSessionId. Drop studentConfirmStartAt & studentConfirmEndAt — yang tinggal cuma teacherConfirmStartAt / teacherConfirmEndAt.
session_evidence Tighten tabel-nya udah ada (opsional dulu). Sekarang wajib: minimal satu row sebelum lesson_sessions.status bisa pindah ke completed / closed. Aturan di app-level (cek di endpoint submitPresence).
lesson_session_events New Audit log. id, sessionId, actorUserId, eventType (confirm_start | confirm_end | evidence_uploaded | presence_submitted | reschedule_requested | reschedule_approved | reschedule_executed | status_changed), fromState, toState, payload (json, nullable), createdAt. Append-only — gak pernah di-update / di-delete.
reschedule_requests New id, sessionId, initiatedBy (student | admin), requestedAt, proposedScheduledAt, status (pending | approved | rejected | executed), reviewedByAdminId, reviewedAt, reason
orders Drop belum live, belum ada data — diganti transactions + lesson_packages

Konstanta

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

API Routes (oRPC, apps/api)

Frontend (apps/web)

Migration

Karena belum live (belum ada data production):

  1. Bikin satu migration baru: drop orders, bikin tabel-tabel baru, dan reshape lesson_sessions + payment_proofs.
  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. Formula prorate — usulnya proratedPrice = monthlyPrice × (remainingDaysInMonth / totalDaysInMonth), round up. sessionsTotal = ceil(sessionsPerMonth × ratio).
  2. Enforce overtime — soft (warning + flag di presence) atau hard (block confirm end lewat 105 menit)? Defaultnya soft.
  3. Window notice reschedule — wajib request ≥ 24 jam sebelum jadwal, atau gak ada minimum?
  4. KYC gatekycStatus = 'verified' wajib sebelum transaction pertama, atau cukup di-flag di queue admin?
  5. Cancellation / refund — kalau murid cancel mid-month, sesi sisanya gimana?
  6. Subject per sesi — free text, atau enum yang udah di-seed (Matematika, IPA, Bahasa, dst)?

Urutan Pengerjaan (PR kecil-kecil)

  1. Schema + migration + konstanta / enum di packages/shared
  2. Price list: admin CRUD + read public
  3. Bikin transactions + lesson_packages, payment proof, admin verify
  4. Bikin lesson session di atas package yang udah verified (admin)
  5. Reschedule: request + admin approve + admin langsung
  6. Form presence: flag grace overtime
  7. Update docs ditarik bareng setiap PR yang nyentuh permukaannya