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.
Sistem ini punya dua peran:
Yang dijaga sistem cuma:
closed setelah ada foto evidence + presence. Bukan blokir
waktu, tapi blokir state.
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.
| Requirement | Keputusan |
|---|---|
| Kelas murid nentuin price list |
Price list cuma di-key sama grade (12 level:
SD-1…SMA-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). |
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.
Dua perspektif: pengalaman murid (booking & ikut les) dan pengalaman admin (operasi bulanan). Angka 1–5 = level happiness murid/admin di tiap step.
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
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.
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
(pending→paid→verified→cancelled),
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 orderId → transactionId; 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
|
File baru: packages/shared/src/constants/lessons.ts
LESSON_DURATION_MIN = 90LESSON_GRACE_MIN = 15 — overtime free, max 105 menit total
SD-1…SD-6,
SMP-1…SMP-3, SMA-1…SMA-3
apps/api)pricelist.list (public), pricelist.upsert /
pricelist.deactivate (admin)
transactions.create (murid): pilih grade +
price_list_item → server itung prorated kalau mid-month → bikin
transaction + lesson_package
transactions.uploadProof, transactions.verify
(admin), transactions.cancel
lessonPackages.list (murid liat punyanya sendiri; admin liat
semua, filter per bulan)
lessonSessions.create (admin; cuma buat package yang udah
verified; scheduledAt harus dalam billing month-nya)
lessonSessions.requestReschedule (murid),
lessonSessions.adminReschedule (admin langsung),
reschedule.approve / reschedule.reject (admin)
proposedScheduledAt wajib di
billingMonth yang sama.
lessonSessions.teacherConfirmStart — cuma guru,
tidak ada cek waktu (bisa kapan saja).
lessonSessions.teacherConfirmEnd — cuma guru,
cuma aktif kalau teacherConfirmStartAt sudah ada
(urutan logis, bukan time-based).
lessonSessions.uploadEvidence (guru) — wajib minimal 1
foto sebelum sesi bisa closed
lessonSessions.submitPresence (guru): nge-block kalau
belum ada evidence. Field tetep, tambah withinGraceWindow
(read-only, true kalau durationActualMs ≤ 105 min).
lesson_session_events (append-only). Source of truth buat
"siapa nekan apa kapan".
lessonSessions.getTimeline (admin) — return semua event
buat satu sesi, urut by createdAt, dengan info actor
user-nya. Ini yang nge-power timeline view di backoffice.
apps/web)billingMonth →
masuk antrian admin
price_list_items
reschedule_requests dengan approve / reject
adminReschedule (gak perlu approval)
lesson_sessions → lihat kronologi event-nya berurutan
(siapa, kapan, transisi state apa). Data dari
lesson_session_events. Tool utama buat rekonsiliasi
dispute.
Karena belum live (belum ada data production):
orders, bikin tabel-tabel
baru, dan reshape lesson_sessions + payment_proofs.
apps/docs/overview/product.md — section order diganti ke
package model; online dikeluarin dari MVP
apps/docs/technical/user-flow.md — rewrite order flow,
tambahin reschedule flow
apps/docs/technical/database.md — tabel-tabel baru +
catatan soal productType buat bank soal nanti
.agents/database.md kalau di sana ada konvensi skema yang
kena
proratedPrice = monthlyPrice × (remainingDaysInMonth / totalDaysInMonth),
round up. sessionsTotal = ceil(sessionsPerMonth × ratio).
kycStatus = 'verified' wajib
sebelum transaction pertama, atau cukup di-flag di queue admin?
packages/sharedtransactions + lesson_packages, payment
proof, admin verify