Repository navigation
[V1 · #467] Scheduling + revenue/earnings engine (4 buckets) #461
Description
[V1 Phased · under #467] Scheduling + Revenue & Earnings Engine (4 Buckets)
RESOLVED 2026-10-25: #444 / #469 merged — live ToS §15 = tutor-pays (
owed by the Tutor; Customers and centres do not pay MatchMax a matching fee). Build/copy against tutor-pays; admin-fee automation gate on #444 is cleared for payer identity (other #475 phase gates still apply).
Payer lock: Issues lock tutor pays 1.5-lesson admin (#464). Live ToS §15 now matches (see RESOLVED above). Build against tutor-pays. #475 Phase1 Collect Tuition (parent→tutor) remains available after
#459/#461A; automated admin collection still waits on #475 phase gates (not on #444 identity).
Parent: #467.
Foundation dep: #459 VerifiedHoursLedger must ship first / in parallel — revenue moves write ledger rows; never invent a second hours store.
Sibling: #462 Trophy Wall.
Fee track: #464 · short #443 · ToS #444.
Prototype: https://share.gemini.google/OWAm773iPg6S · mockuprevenue-simulator.pngon parent.
Labels intent:enhancement+urgentfor Phase A. Phase B stays documented below (do not drop).
Grok ADOPT — Phase A must-haves (2026-10-25)
Binding additions (see also latest comment). Phase A is not done without these.
- Minimal session writer on existing matched cases: “Log completed lesson” + “Schedule next lesson” (single slot) → fee snapshot + ledger write. Dashboard must not depend on Phase B drag-drop calendar. Explicit Phase A acceptance criteria below — not optional narrative.
- Unpaid tuition (owed to you) — never conflate with MatchMax 1.5 admin fee; helper text required.
- [Parked] Automated Fee Collection & Tutor/Centre Payout Rails (FPS → Stripe Connect → Hold-until-Confirm) #475 Phase 1 hand-off: Unpaid tuition bucket is the hand-off point for #475 Phase 1 Collect Tuition (parent→tutor). Do not invent a second unpaid store; keep [Parked] Automated Fee Collection & Tutor/Centre Payout Rails (FPS → Stripe Connect → Hold-until-Confirm) #475 On Hold / not urgent.
- External private log (0% admin) in Phase A for private $ only; no public hours/Trophy until account link (Phase B).
0. Why (lock-in)
Tutors/centres currently do mental math on WhatsApp + spreadsheets. Putting four live money buckets on the schedule makes logging every lesson immediately rewarding. Combined with Trophy Wall (#462), taking a student off-platform after L2 freezes reputation; staying on MatchMax from L3+ = 0% commission + rising retention.
Closed-loop rules (copy on parent)
- Off-platform after L2 → hours freeze; Active drops after 45d.
- Stay on calendar L3+ → 0% admin + retention/search climb.
- Parent/student confirm → own learning hours on CV.
- BYO external → private $ now; hours/trophy only after MatchMax link (Phase B).
1. Phase cut (CEO)
Phase A — V1 NOW (private tutor/centre dashboard) — BUILD
Non-blocking · future #476: the AI schedule widget will call the same
ScheduledSessionwriter as Phase A log/confirm — keep that API stable (inputs: caseId, datetime, durationHours, actors). Do not special-case a second session create path.
Ship after/with #459 ledger. Private Schedule & Earnings on authenticated workspace. Wire to existing MatchMax cases where possible.
In scope
- Four financial buckets header (week / month / custom term range simple month+week first).
- Lesson complete → Pipeline → Realized + write dual-sided
#459VerifiedHoursLedger. - CEO fee math (below) using existing
fee-breakdown.ts(extract/extend from livesrc/features/cases/sor.tscomputeFeeAmountHkd) helpers. - Multi-student list + hourly rate; stacked bar by student (green realized / blue pipeline / pink admin fee) matching Gemini mockup.
- Session states: Completed (24h auto-verify), Reschedule (preserve L1/L2 sequence), Cancel free, Late cancel (cash may count, hours = 0).
- Pending Tuition Collection (Unpaid) bucket.
- Cap.57 Independent Tutor disclaimer in UI; centres via Organization (
#465). - Brand Azure/Sky/Wash/Ink.
Out of Phase A (→ Phase B)
- Full drag-drop calendar UI polish beyond extending existing week view
- Recurrence rules + .ics / Google Calendar sync
- Prepaid package burn-down pills + invoice alert on last prepaid lesson
- Small-group / multi-seat attendance revenue
- BYO external students private revenue + viral invite for hours
- Custom academic term range picker (beyond month/week)
- WhatsApp reminder hooks beyond in-app status card (
#466owns parent WA card)
Phase B — LATER (parked in this issue — do not open a duplicate)
All items listed under “Out of Phase A” above, plus forecasting scenarios / what-if sliders for tutors (Gemini simulator sliders are QA fixtures / Storybook, not production tutor controls — see §8).
2. Website integration (exact live targets)
Repo: keyframesfound/matchmaxhk (not cuantic-lab).
| Target | Path | Implementation note |
|---|---|---|
| Primary host | /dashboard with hash #lessons (existing tutor home) |
Extend src/features/cases/tutor/tutor-home.tsx + week-view.tsx + calendar-windows-editor.tsx. Add Schedule & Earnings section above / beside week view: 4 KPI cards + stacked bars. |
| Dashboard route | src/routes/_authenticated.dashboard.tsx |
Anchor #earnings or #schedule-earnings for deep link from coach tour / emails. |
| Fee math | src/features/cases/fee-breakdown.ts |
Reuse lesson1Amount, lesson2BalanceAmount, fullAdminFeeAmount, derivePaidState. Extend for durationHours ≠ 1 and short-case 20%. |
| Tutor cases | src/features/cases/tutor/tutor-cases-section.tsx · tutor.functions.ts |
Source of matched cases → TuitionCase rows. |
| Business portal | /business/ layout · src/components/business/business-layout.tsx |
Org-scoped earnings for centres (same 4 buckets, filter by org_id). |
| Public profile | /tutors/$tutorCode |
Do not put private $ here — only Trophy KPIs (#462). |
| Parent confirm | /account/cases/$caseId |
Confirm session CTA → ledger + Realized. |
| Help copy | src/features/help/content/admin-fee-structure.md · /guide/tutors |
Link from fee tooltip. |
Do not create a separate SPA. Embed in existing dashboard / business portal.
3. CEO-CONFIRMED fee model (tutor pays — never parent-pays)
Gross lesson amount:
G = agreedHourlyRateHkd × durationHours
(group: multiply by attending seat count after Phase B; Phase A = 1:1)
Platform-matched case sequence N = 1-based verified completed lesson index on that TuitionCase (reschedules do not increment N; cancelled free do not; late-cancel policy below):
N == 1: platformAdminFee = 1.00 × G ; netTutor = 0
N == 2: platformAdminFee = 0.50 × G ; netTutor = 0.50 × G
N >= 3: platformAdminFee = 0 ; netTutor = G
Also lock
| Case | Admin |
|---|---|
Short ≤5 lessons package (#443) |
20% of package total once (not 1.5× per lesson) — BillingModel.SHORT_PACKAGE_20 |
| Pro-bono / rate = 0 | 0 |
TUTOR_SELF_IMPORTED / BYO |
0 for all N |
| Cap.57 Independent Tutor | Personal tutor billing path only |
| Centre / Organization | Org calendar; tutors under org still self-employed for Cap.57; vacancy SaaS = #413 |
Reuse group scaling pattern from fee-breakdown.ts (round total to whole HKD).
Worked examples
Example 1 — classic 1.5h @ HK$600/hr (platform match)
| Lesson | G | Admin | Net tutor | Cumulative admin |
|---|---|---|---|---|
| L1 | 900 | 900 | 0 | 900 |
| L2 | 900 | 450 | 450 | 1350 (= 1.5 × 900) |
| L3+ | 900 | 0 | 900 | 1350 |
Example 2 — Gemini mockup rates (1h lessons; Completed=3, Scheduled=4; toggle off)
Approximate portfolio used in simulator (document as QA fixture seed):
| Student | Rate HKD/hr | Status | Realized net (≈) | Pipeline net (≈) | Admin pink (≈) |
|---|---|---|---|---|---|
| A | 500 | Verified (N≥3) | 3000 | 3000 | 0 |
| B Anson Lam | 450 | Verified | 2000 | 3400 | 0 |
| C | 400 | New Match | ~400 | ~4800 | ~1200 |
| D | 450 | New Match | 0 | ~1400 | ~900 |
| E Chloe Wong | 550 | Verified | 3250 | 3250 | 0 |
Header totals in mockup: Realized HK$8,725 · Pipeline HK$15,975 · Forecast HK$24,700 · Admin Fee HK$2,100 · Trophy 562 hrs / 5 Active.
QA must reproduce these totals from the seed fixture (± rounding). Toggle “Add New Platform Match Case” adds Student F with 1.5L admin deduction — fixture only.
Example 3 — short package ≤5 @ HK$4,000 package
Admin = 0.20 × 4000 = HK$800 once; net tutor across package = 3200 (allocate per product rules in #443 — link, do not fork).
4. Data model (Supabase — Prisma-ish; implement as SQL migration)
enum BillingModel {
PLATFORM_MATCH_1_5 // default matched via MatchMax
SHORT_PACKAGE_20 // ≤5 lessons, 20% package
PRO_BONO // 0
TUTOR_SELF_IMPORTED // 0 admin forever
}
enum TuitionCollectionStatus {
NOT_APPLICABLE
UNPAID // completed; parent hasn't settled tuition with tutor
PARTIAL
PAID
WAIVED
}
enum SessionStatus {
SCHEDULED
COMPLETED_PENDING_VERIFY // waiting dual confirm / 24h auto
COMPLETED_VERIFIED // counts hours + realized
RESCHEDULED // pointer to replacement session; N not consumed
CANCELLED_FREE
CANCELLED_LATE // cash may count toward Realized; hours = 0
}
model TuitionCase {
id String @id @default(uuid())
tutorProfileId String
organizationId String? // null = independent tutor
learnerProfileId String? // #459 child / learner
matchmaxCaseId String? @unique // FK existing cases table when platform-matched
billingModel BillingModel @default(PLATFORM_MATCH_1_5)
agreedHourlyRateHkd Int
currency String @default("HKD")
subjectTag String?
schoolTag String?
curriculumTag String?
lessonSequenceNext Int @default(1) // next N to assign on verify
active Boolean @default(true)
lastActivityAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions ScheduledSession[]
}
model ScheduledSession {
id String @id @default(uuid())
tuitionCaseId String
startsAt DateTime // store timestamptz; display Asia/Hong_Kong
durationHours Decimal @db.Decimal(4,2)
status SessionStatus @default(SCHEDULED)
sequenceN Int? // set when COMPLETED_VERIFIED; null until then
grossHkd Int // snapshot G at schedule time
adminFeeHkd Int @default(0)
netTutorHkd Int @default(0)
collectionStatus TuitionCollectionStatus @default(NOT_APPLICABLE)
tutorConfirmedAt DateTime?
counterpartyConfirmedAt DateTime? // parent/learner
autoVerifiedAt DateTime?
rescheduledToId String?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Bucket queries (Phase A) for selected range [rangeStart, rangeEnd):
RealizedNet = SUM(netTutorHkd) WHERE status=COMPLETED_VERIFIED AND startsAt in range
ScheduledPipeline = SUM(netTutorHkd_projected) WHERE status=SCHEDULED AND startsAt in range
// project admin using current lessonSequenceNext as if sessions complete in chronological order
PendingUnpaid = SUM(grossHkd or net?) WHERE status in (COMPLETED_*) AND collectionStatus=UNPAID
MonthEndForecast = RealizedNet + ScheduledPipeline
AdminFeeInForecast = SUM(adminFeeHkd) for completed in range + projected admin on scheduled
Unpaid tracks tuition owed to tutor by parent (not MatchMax admin invoice). Admin fee owed by tutor to MatchMax stays in existing case_fees / desk flow (#385 helpers) — surface link, do not rebuild desk invoicing in Phase A.
5. Session state machine
SCHEDULED
├─ tutor/parent: Reschedule → RESCHEDULED (+ new SCHEDULED child); sequenceN untouched
├─ Cancel (≥ policy window) → CANCELLED_FREE; no cash, no hours, N untouched
├─ Cancel late → CANCELLED_LATE; optional cash toward Realized per case policy flag; hours = 0; N untouched
└─ Mark complete → COMPLETED_PENDING_VERIFY
├─ both confirm OR 24h auto → COMPLETED_VERIFIED
│ assign sequenceN = TuitionCase.lessonSequenceNext++
│ compute admin/net from BillingModel + N
│ write VerifiedHoursLedger (#459) tutor teaching + learner learning
│ set collectionStatus=UNPAID (if G>0) until tutor marks paid
└─ dispute → stay pending / admin path (ledger dispute fields on #459)
Invariant: Reschedule preserves L1/L2 fee sequence (same TuitionCase.lessonSequenceNext).
Hours rule: Only COMPLETED_VERIFIED writes hours. Late cancel never writes hours.
6. Flows (step-by-step)
6.1 Platform-matched case → first schedule
- Desk matches parent case → tutor (
Tutor Codeflow unchanged). - System creates
TuitionCasewithbillingModel=PLATFORM_MATCH_1_5,matchmaxCaseId, rate from case. - Tutor opens
/dashboard#lessons→ sees case in multi-student list. - Tutor adds
ScheduledSession(datetime + duration). Pipeline updates. - After lesson, tutor taps Complete → pending verify.
- Parent confirms on
/account/cases/$caseIdOR 24h auto. - L1: admin=G, net=0; ledger +1 teaching / +1 learning hours × duration.
- Repeat; L2 half; L3+ full net.
6.2 Mark tuition collected
Tutor taps “Mark paid” on completed session → collectionStatus=PAID → Pending Unpaid bucket decreases.
6.3 Cap.57 disclaimer (UI copy — Independent Tutor)
Show once per session on earnings header for personal tutors:
You operate as an independent self-employed tutor. MatchMax provides matching and scheduling tools; Cap.57 centre licensing does not apply to this Independent Tutor path. Centres use Organization accounts.
7. UI wireframe (ASCII — match Gemini simulator)
┌─ Schedule & Earnings (/dashboard#earnings) ─ Week ▾ Oct 2026 ─────────────┐
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ REALIZED NET │ │ SCHEDULED │ │ FORECAST NET │ │ TROPHY HRS │ │
│ │ HK$8,725 │ │ PIPELINE │ │ TOTAL │ │ 562 hrs │ │
│ │ Completed │ │ HK$15,975 │ │ HK$24,700 │ │ 5 Active │ │
│ │ (green) │ │ Upcoming net │ │ Admin HK$2.1k│ │ → #462 KPI │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ + Pending Unpaid: HK$1,200 [Review] │
│ │
│ Cash Flow by Student (HKD) │
│ Student A ████████░░░░░░░░ green|blue │
│ B Anson Lam ██████░░░░░░░░░░ │
│ Student C █░░░░░░░░░░░░███ ... + pink admin │
│ Student D ░░░░░░███ │
│ E Chloe Wong ████████░░░░░░░░ │
│ Legend: ■ Realized ■ Pipeline ■ Admin fee (1.5L) │
│ │
│ [Week view calendar — existing week-view.tsx] │
│ Mon ... sessions chips → Complete | Reschedule | Cancel │
└────────────────────────────────────────────────────────────────────────────┘
Production does not show Hourly Rate Multiplier / Completed Lessons sliders — those are Storybook/QA fixtures mirroring Gemini. Production edits rate on case settings.
Roster strip “Verified / New Match / Fee -HK$” cards live primarily on #462 mockup; Phase A may show a compact list under bars with same labels for consistency.
8. RLS / Cap.57 / PDPO
- Tutor reads/writes only own
TuitionCase/ sessions (auth.uid()→ tutor profile). - Org members with role ≥ manager read/write org-scoped cases.
- Parents confirm sessions for linked learner only (
#459ParentLearnerLink). - Public API never returns dollar amounts from this engine (private dashboard only).
- PDPO: financial + student identifiers stay private; public surfaces use
#462privacy tiers. - Timezone: persist UTC; display
Asia/Hong_Kong. - No manual hour edits — hours only via verified sessions (
#459rule).
9. Acceptance criteria
Phase A
- Minimal log-completed-lesson path (REQUIRED — Phase A blocker): On an existing matched MatchMax case in
/dashboard(tutor-home.tsx/ cases section), tutor can tap [Log completed lesson] (date/time · durationHours · optional mark unpaid) without Phase B drag-drop calendar. Action creates/advancesScheduledSession→ fee snapshot viafee-breakdown.ts→ on verify writes dual-sided#459ledger. See wireframe under “Minimal log UI”. - [Schedule next lesson] single-slot CTA exists on the same case card (one datetime; no recurrence UI in Phase A).
- Migration for
TuitionCase+ScheduledSession+ enums; RLS policies green in CI. -
/dashboard#earnings(or#lessonssection) shows 4 buckets + Pending Unpaid for authenticated tutor. - Completing+verifying session updates Realized, decrements Pipeline projection, writes two
#459ledger rows (teach + learn). - Fee math matches Example 1 exactly; QA fixture reproduces mockup totals (±1 HKD rounding).
- Reschedule preserves
lessonSequenceNext; free cancel does not advance N; late cancel hours=0. - Short-case billing model delegates to
#443rules. -
TUTOR_SELF_IMPORTEDadmin always 0. - Cap.57 disclaimer visible for Independent Tutor.
- Org path shows same UI under
/business/filtered byorganizationId. - Brand tokens match DESIGN.md; bilingual EN / zh-HK strings.
- Does not change matching desk macros ownership.
Phase B (parked — check when unparking)
- Drag-drop calendar + recurrence + .ics/Google sync.
- Prepaid burn-down + last-lesson invoice alert.
- Group multi-seat attendance revenue.
- BYO external private $ + invite-to-link for hours/trophy.
- Custom academic term picker.
- WA reminder hooks (coordinate
#466, do not fork).
10. Dependencies & non-goals
Depends on: #459 · #464/#443/#444 · #465 Cap.57 org · existing fee-breakdown.ts.
Feeds: #462 Trophy KPIs / Active list · search ranking signals later.
≠ #428 matching outcome ledger.
Non-goals: parent-pays; replacing desk matching; public dollar amounts; full #460 profile chrome.
Downstream: gamification hook (#468) — do not build vault here
After Phase A session → COMPLETED_VERIFIED + #459 ledger write:
- Emit event / call so #468 can award MaxPoints + bump weekly lesson streak (when [Parked · Full Epic] Gamified Retention Engine — Streaks · MaxPoints · Trophy Ladder · Rewards Vault · Fee Rebate #468 Phase A unparked).
- Fee rebate apply path (LOCKED split — keep
fee-breakdown.tsconsistent with [Parked · Full Epic] Gamified Retention Engine — Streaks · MaxPoints · Trophy Ladder · Rewards Vault · Fee Rebate #468): when tutor has unusedADMIN_FEE_REBATE_0_25redemption, next newPLATFORM_MATCH_1_5TuitionCaseadmin total 1.5R → 1.25R where R = one-lesson gross (hourlyRate × durationHoursfor the billed lesson unit).- Base (no rebate): L1 = 1.0R (100%); L2 = 0.5R (50%); total admin = 1.5R.
- With rebate: L1 stays 1.0R (100%); L2 becomes 0.25R (25%); total admin = 1.25R.
- HK$ example: R = HK$450 → base admin HK$675 (L1 450 + L2 225); after rebate admin HK$562.50 → round per existing
fee-breakdown.tswhole-HKD rules (document rounded L1/L2 in QA fixture). - Cap.57-safe copy. Do not invent a third split.
- This issue owns session writer + 4 buckets + unpaid UX + BYO private $ lite; it does not own Rewards Vault UI.
Unpaid bucket empty-state copy (lock)
- Label: Pending Tuition Collection (owed to you by parent/student).
- Helper: “This is tuition the family owes you — not the MatchMax 1.5-lesson admin fee you owe the platform.”
- Link admin-fee help:
admin-fee-structure.md//guide/tutors.
Minimal log UI (Phase A) — wireframe
┌─ Case: Anson Lam · IB Econ · HK$450/hr · N next=3 ─────────────┐
│ [Log completed lesson] date/time · duration · mark unpaid? │
│ [Schedule next lesson] single slot (no drag-drop calendar) │
│ Last: L2 verified · Admin remaining: 0 · Net last: HK$450 │
└─────────────────────────────────────────────────────────────────┘
Human-assisted matching + post-L2 handoff unchanged; this is the thin writer so ledger/buckets are not empty.
11. Implementation deepen (CEO 2026-10-25 — zero guesswork add-ons)
Appended to close quality gaps. Does not change tutor-pays 1.5, rebate split, or Cap.57.
Live fee helper today issrc/features/cases/sor.ts(FEE_MULTIPLIER = 1.5,computeFeeAmountHkd). Filefee-breakdown.tsis the target extract/extend name used in this epic — create it by moving/wrappingsor.tshelpers so sequence N (L1/L2/L3+) and rebate 1.25R live in one module. Do not leave two conflicting calculators.
11.1 Exact server actions (Phase A — implement these names)
| Action | Actor | Input | Output / side effects |
|---|---|---|---|
logCompletedLesson |
tutor (org manager) | { tuitionCaseId | matchmaxCaseId, occurredAt, durationHours, markUnpaid?: boolean, clientRequestId } |
Creates/advances session → COMPLETED_PENDING_VERIFY; idempotent on clientRequestId or same case+occurredAt+duration within 5 min |
scheduleNextLesson |
tutor | { caseId, startsAt, durationHours } |
One SCHEDULED row; no recurrence |
confirmLessonParent |
parent (linked learner) | { sessionId } |
If tutor already complete → COMPLETED_VERIFIED + fee snapshot + two #459 ledger rows |
confirmLessonTutor |
tutor | extend existing tutor.functions.ts confirm |
Align SoR case_lessons with new machine when bridging |
autoVerifyDueSessions |
cron/edge HKT | — | PENDING older than 24h after endsAt → verified (unless disputed) |
disputeSession |
parent or tutor | { sessionId, reason } |
Stay/set disputed; no ledger credit |
undoCompleteSession |
actor who completed | { sessionId } |
Allowed only while COMPLETED_PENDING_VERIFY or within 2 hours after verify if no rebate/MaxPoints consumed; else staff-only |
voidSession |
staff or tutor (policy) | { sessionId, reason } |
Soft-void: status VOIDED; excluded from buckets & hours; row retained for audit (no hard delete) |
markTuitionPaid |
tutor | { sessionId } |
collectionStatus=PAID; unpaid bucket ↓ |
logExternalLessonPrivate |
tutor | { occurredAt, durationHours, grossHkd, label } |
BYO Phase A lite: private $ only; no ledger hours; no Trophy |
getEarningsDashboard |
tutor/org | { range } |
4 buckets + unpaid + per-student bars |
11.2 Dual-confirm + 24h auto + undo (locked)
Tutor logCompletedLesson
→ COMPLETED_PENDING_VERIFY
→ Parent confirmLessonParent ─┐
→ OR autoVerifyDueSessions ├→ COMPLETED_VERIFIED
(endsAt + 24h Asia/Hong_Kong, unflagged) ─┘
→ disputeSession → DISPUTED (no hours, no N advance until staff resolve)
→ undoCompleteSession (while pending, or ≤2h post-verify if safe) → back to SCHEDULED or VOIDED
- Timeouts: request expiry for planned SoR lessons remains existing 12h tutor-confirm rule in
sor.ts; post-complete verify grace is 24h. Do not conflate the two clocks. - Idempotency: double-tap Complete must not double-advance
lessonSequenceNextor double-write ledger. Unique constraint recommended:(tuitionCaseId, occurredAt, durationHours)where not VOIDED, plusclientRequestIdUUID from UI. - Soft void vs delete: never hard-delete sessions that touched fees/ledger;
VOIDED+ reason.
11.3 Notification matrix (who gets pinged)
| Event | Tutor | Parent | Staff | Channel |
|---|---|---|---|---|
logCompletedLesson |
— (actor) | In-app + push/WA-template hook if already in bus.ts |
— | Prefer extend src/features/cases/bus.ts |
| Parent confirm / 24h auto verify | In-app | — | — | |
| Dispute | In-app | In-app | In-app flag | |
| Unpaid tuition aging >7d | In-app reminder | Optional soft WA (copy: tuition to tutor, not MatchMax fee) | — | |
| MatchMax admin fee (L1/L2) | Existing desk fee bus (admin_fee.*) |
Not as parent-owed matching fee | Desk | Keep tutor-pays |
Coordinate #466: parent status card may say “Confirm lessons on your case page” only after this Phase A ships — never promise calendar.
11.4 Short-case (≤5 lessons, 20% package) × sequence N × #468 rebate
| Billing model | Admin math | lessonSequenceNext |
#468 100h fee rebate eligibility |
|---|---|---|---|
PLATFORM_MATCH_1_5 |
L1=1.0R, L2=0.5R (rebate→L2=0.25R), L3+=0 | Advances on verify | Yes — rebate applies to next new PLATFORM_MATCH_1_5 case only |
SHORT_CASE_20 |
20% of agreed package (once / per #443) — not L1/L2 split | May still count verified hours to ledger | Hours count for Azure/rebate hours gate; redeem still requires next 1.5 match (rebate SKU does not rewrite short-case 20%) |
TUTOR_SELF_IMPORTED / BYO |
Admin 0 | Private $; hours only after link (Phase B) | Hours after link count; rebate still only on next platform 1.5 case |
| Pro-bono $0 | Admin 0 | Hours may still verify | Hours count; no admin to rebate |
11.5 BYO external log privacy (Phase A lite — clear)
- UI label: “Log external lesson (private $ only)”.
- Helper: “Does not add Verified Hours or Trophy until the student links MatchMax ([V1 · #467] Scheduling + revenue/earnings engine (4 buckets) #461 Phase B invite).”
- Not shown on public profile.
- Does not emit [Parked · Full Epic] Gamified Retention Engine — Streaks · MaxPoints · Trophy Ladder · Rewards Vault · Fee Rebate #468 MaxPoints.
11.6 Mobile vs desktop placement (/dashboard)
| Viewport | Placement |
|---|---|
| Mobile | Case card list: sticky primary [Log completed lesson]; secondary sheet for datetime/duration; earnings 4 buckets in horizontal scroll chips under header |
| Desktop | Same CTAs on case row; earnings grid 4 cards + stacked bars (#earnings); week-view chips get Complete | Reschedule | Cancel when Phase B calendar present — Phase A OK without drag-drop |
Files: src/routes/_authenticated.dashboard.tsx · src/features/cases/tutor/tutor-home.tsx · week-view.tsx · tutor-home-model.ts · tutor.functions.ts.
11.7 i18n
All new UI strings EN + 繁中 via existing react-i18next namespaces (extend sor.* / new earnings.*). Hard-fail staging if missing 繁中 for Phase A CTAs.
11.8 Analytics (light — park detail)
Emit (posthog/existing): log_lesson, verify_lesson, mark_tuition_paid, log_external_private, dispute_lesson. Full funnel dashboards parked.
11.9 Parent learning-hours CV teaser
Park on #460 / learner CV — Phase A may show parent a single line on case page: “Learning hours this case: X (Verified)” linking future CV. No Instagram grid here.
11.10 Correct live label debt
tutor-home.tsx historically labelled admin fee as parent’s fee — wrong under CEO lock. Any touch to fee cards on /dashboard must say tutor pays MatchMax admin (link admin-fee-structure.md).
11.11 Additional Phase A acceptance criteria (add)
-
logCompletedLessonis idempotent (double submit ≠ double N / double ledger). - Dual confirm + 24h HKT auto-verify + dispute path implemented; undo rules respected.
-
voidSessionsoft-void only — no hard delete of fee-touched rows. - Notification matrix events wired via
bus.ts(or documented stub list in PR). - Short-case
SHORT_CASE_20does not use L1/L2 1.5 split; rebate cannot rewrite short-case math. - BYO private log cannot credit hours/Trophy/MaxPoints.
- Mobile + desktop CTAs present on
/dashboardcase cards. - EN + 繁中 for Phase A strings.
- Fee math via single module (
fee-breakdown.tswrapping/extendingsor.ts) — QA fixture Example 1 (±1 HKD). - Reminder: after log, parent status ([Urgent · V1] Case/Tutor Form Progressive Disclosure + Parent Match-Status Card + Admin Filters #466) can show confirm CTA — no calendar promise.
12. Zero-guesswork implementation ticket — Phase A (CEO quality pass 2026-10-25)
Fills Grok gaps: exact Log-lesson UI, concurrent races, pipeline pseudocode, unpaid copy + Mark paid lock, empty state, fee-breakdown rebate hook signature, impl checklist, RLS matrix, i18n. Phase B body above stays parked and must not be deleted.
12.0 Live code baseline
| Concern | Live | Phase A action |
|---|---|---|
| Fee helpers | src/features/cases/fee-breakdown.ts already exists (LESSON1_MULTIPLIER=1.0, LESSON2_BALANCE_MULTIPLIER=0.5, FULL_ADMIN_MULTIPLIER=1.5, Math.round whole HKD). Tests in tests/cases/fee-breakdown.test.ts |
Extend — do not create a second module. Add rebate-aware API (§12.8). sor.ts computeFeeAmountHkd / FEE_MULTIPLIER=1.5 remain; extract callers toward fee-breakdown over time |
| Tutor home | src/features/cases/tutor/tutor-home.tsx + tutor-cases-section.tsx + tutor.functions.ts |
Add CTAs on case cards; earnings strip |
| Mark done | markLessonDoneTutor({ lessonId, privateNote? }) — sets done, no fee snapshot / ledger / unpaid bucket |
Either wrap with logCompletedLesson for matched cases or have mark-done call into shared complete pipeline |
| Admin log L1/L2 | lessonAdminLog / lessonAdminMark (sequence 1–2 only) |
Keep; when done, must hit same ledger credit as #459 bridge |
| Dual slot confirm | confirmLessonTutor / confirmLessonParent |
Unchanged for schedule; complete-verify is separate state on ScheduledSession / extended lesson |
| Active window | ACTIVE_STUDENT_WINDOW_DAYS = **30** in sor.ts |
Product closed-loop lock is 45 days (#462). Phase A revenue code must import shared constant once updated — do not hardcode 30 in new dashboard queries |
| Label debt | Tutor-home copy still implies parent fee in places | Fix to tutor-pays 1.5 admin wording |
12.1 Log completed lesson UI — LOCKED
Placement
- Route:
/dashboard(tutor home). - Component: case card inside tutor cases list (
tutor-cases-section.tsx/ home case row) — same card that already shows lesson N / fee chips. - Mobile: full-width buttons under case meta; Desktop: button row on card right.
- Also show on
/dashboard#earningscase list rows if split view exists.
Interaction: Modal (not inline expand) — LOCKED
Why: needs date/time/duration validation + unpaid toggle without growing every card.
Case card (matched PLATFORM case):
[Log completed lesson] → opens <LogCompletedLessonModal caseId=…>
[Schedule next lesson] → opens <ScheduleNextLessonModal caseId=…> (single slot; NOT full calendar)
How case is selected
- CTA is on the case card →
caseIdis bound; no case picker inside modal. - Modal title shows student first name + subject + rate (from card props).
- Only cases with
matched_tutor_id = meand SoR status allowing lessons (e.g. matched / in progress — mirror existing tutor case list filter).
Form fields — Log completed lesson modal
| Field | Required? | Default | Notes |
|---|---|---|---|
occurredAt date (HKT) |
Yes | today HKT | <input type=date> |
occurredAt time (HKT) |
Yes | now rounded to 15m | |
durationMin |
Yes | case default / 60 | int 15–480; step 15 |
markTuitionUnpaid |
No | false |
If true → session contributes to Unpaid bucket |
privateNote |
No | empty | max 500; tutor+admin only |
clientRequestId |
hidden | crypto.randomUUID() |
idempotency |
Submit → logCompletedLesson({ caseId, occurredAtIso, durationMin, markTuitionUnpaid, privateNote?, clientRequestId }).
Schedule next lesson modal (thin)
| Field | Required? |
|---|---|
| date + time HKT | Yes |
| durationMin | Yes (default 60) |
| locationOrLink | Optional |
→ scheduleNextLesson({ caseId, startsAtIso, durationMin, locationOrLink?, clientRequestId })
Creates next case_lessons / ScheduledSession with sequence = max(sequence)+1 (see races). No drag-drop week grid in Phase A.
No rate yet — LOCKED behaviour
If hourly_rate / case rate is null or ≤0:
- Block submit on Log completed lesson.
- Modal error copy EN: “This case has no hourly rate yet. Ask MatchMax desk to set the rate before logging lessons.”
- 繁中: 「此個案尚未設定時薪。請先聯絡 MatchMax 專員設定收費,再記錄課堂。」
- Secondary link: contact desk / open WhatsApp help — not a silent HK$0 fee snapshot.
- Admin may still
lessonAdminLog/ set rate on desk (existing).
After success
- Toast: “Lesson logged — pending parent confirm” (or “Verified” if auto path N/A).
- Invalidate
["sor","tutor-cases"]+ earnings query. - Parent status card ([Urgent · V1] Case/Tutor Form Progressive Disclosure + Parent Match-Status Card + Admin Filters #466) may show confirm CTA — no calendar promise.
12.2 Concurrent confirm race — LOCKED
Two different clocks (do not merge):
A) Schedule dual-confirm (existing): parent/tutor set confirmed_by_*. Last successful UPDATE wins timestamps; status becomes confirmed when both non-null. Concurrent clicks: both writes OK; idempotent if already confirmed on that side (IF confirmed_by_tutor_at IS NOT NULL → no-op).
B) Complete → verify (#461 session / extended done path):
State: COMPLETED_PENDING_VERIFY
Parent confirmLessonComplete(sessionId) ──┐
OR autoVerify job (endsAt + 24h HKT) ──┼→ COMPLETED_VERIFIED (once)
Dispute ──┘→ DISPUTED
Race rule:
Use single SQL transition:
UPDATE … SET status='COMPLETED_VERIFIED', verified_at=now()
WHERE id=$1 AND status='COMPLETED_PENDING_VERIFY'
RETURNING *
Zero rows returned → no-op success (already verified or disputed).
Who "wins": first committer; second is idempotent no-op.
lessonSequenceNext / N advance happens ONLY in the winning verify transaction (or at log time if you advance on log — LOCK: advance N at LOG time for fee tranche; hours credit at VERIFY — document in code comment).
CEO lock for Phase A fee N:
- Admin fee L1/L2 accrual follows sequence at **log** (snapshot fee lines).
- Ledger VERIFIED_COMPLETED hours credit at **verify** (or immediate if staff force-verify).
- Double submit same clientRequestId → same session row (UPSERT / unique).
Idempotency key: clientRequestId UUID unique per tutor (or global) on scheduled_sessions.client_request_id; also UNIQUE(case_lesson_id) on ledger; UNIQUE(tuition_case_id, sequence) where not VOIDED.
Parent + tutor both click complete: treated as two confirmations toward verify if design is dual-complete; if Phase A is tutor-logs + parent-confirms, tutor click = create pending, parent click = verify. Tutor second click = no-op. Phase A lock: tutor initiates log; parent confirms (or 24h auto); tutor cannot “confirm” own log as the parent side.
12.3 Reschedule / cancel × sequence — with races
| Action | sequence N | Fee snapshot | Ledger |
|---|---|---|---|
scheduleNextLesson |
N = max(existing.sequence)+1 in transaction with advisory lock or INSERT … ON CONFLICT |
none yet | none |
| Reschedule (change starts_at only) | N unchanged | unchanged | if was SCHEDULED only, update occurred/start |
| Cancel before verify | N not reused; row VOIDED/CANCELLED; next schedule still max+1 | open fee lines staff-handled | no VERIFIED credit |
| Late cancel | same | per existing cancelActionFor |
no credit |
| Concurrent scheduleNext ×2 | both compute max+1 → use DB unique (case_id, sequence); loser retries max+1 | — | — |
12.4 Scheduled Pipeline projection — pseudocode (whole HKD)
Bucket = sum of projected remaining MatchMax admin fee on future/scheduled lessons that are not yet verified paid, for PLATFORM_MATCH_1_5 cases only.
// Single module: fee-breakdown.ts (extend). All money Math.round to whole HKD.
function projectedAdminRemainingHkd(caseCtx: {
rateHkd: number; // must be > 0
studentCount?: number; // default 1
feeModel: 'PLATFORM_MATCH_1_5' | 'SHORT_PACKAGE_20' | …;
sequenceNext: number; // next lesson index to be taught
l1Paid: boolean; // from derivePaidState / fee rows
l2Paid: boolean;
rebateApplied: boolean; // this case already consumed FEE_REBATE_0_25
}): number {
if (caseCtx.feeModel !== 'PLATFORM_MATCH_1_5') {
// SHORT_PACKAGE_20 etc.: project per that model (no L1/L2 split); omit from L1/L2 logic
return projectShortOrOther(caseCtx); // existing/ short-case helper
}
const r = caseCtx.rateHkd;
const n = caseCtx.studentCount;
const l1 = lesson1Amount(r, n); // 1.0R
const l2 = caseCtx.rebateApplied
? lesson2BalanceAmountWithRebate(r, n) // 0.25R — NEW
: lesson2BalanceAmount(r, n); // 0.5R
let remaining = 0;
if (!caseCtx.l1Paid && caseCtx.sequenceNext <= 1) remaining += l1;
// If sequenceNext == 1 but L1 already invoiced/paid, don't double-count
if (!caseCtx.l1Paid && caseCtx.sequenceNext > 1) remaining += l1; // taught past 1 but unpaid admin — still pipeline/unpaid-admin separate
if (!caseCtx.l2Paid && caseCtx.sequenceNext <= 2) remaining += l2;
if (!caseCtx.l2Paid && caseCtx.sequenceNext > 2) remaining += l2;
// L3+ : remaining admin for PLATFORM_MATCH is 0 once L1+L2 settled
if (caseCtx.l1Paid && caseCtx.l2Paid) return 0;
// Prefer deriving from fee rows + sequence rather than double-entry:
return clampNonNegative(Math.round(remaining));
}
// Dashboard Scheduled Pipeline KPI =
// SUM over my open PLATFORM cases of projectedAdminRemainingHkd(case)
// Unpaid tuition bucket is SEPARATE (tuition owed TO tutor, not admin).Rounding rule: every displayed HKD amount uses the same Math.round helpers as lesson1Amount / lesson2BalanceAmount — never format floats in UI.
12.5 Unpaid bucket — copy + Mark paid LOCKED
Label EN: Pending Tuition Collection (owed to you by parent/student)
Label 繁中: 待收學費(家長/學生欠你的學費)
Helper EN: “This is tuition the family owes you — not the MatchMax 1.5-lesson admin fee you owe the platform.”
Helper 繁中: 「這是家長/學生欠你的學費,並非你欠 MatchMax 的 1.5 堂行政費。」
Mark paid permission — Phase A LOCK: tutor-only.
- CTA
markTuitionCollected({ sessionId })allowed ifftutors.user_id = auth.uid()for that case. - No parent confirmation required in Phase A (tuition is peer-to-peer; MatchMax is not escrow).
- Parent UI may show “Tutor marked tuition received” read-only later — optional.
- Audit: write
case_auditreasontuition_marked_collected.
12.6 Empty state — zero matched cases
When getTutorCases returns [] (already partly handled in tutor-home.tsx):
┌─ Your earnings ─────────────────────────────────────┐
│ No matched MatchMax cases yet. │
│ When desk matches you, log lessons here to track │
│ admin fee, tuition, and verified hours. │
│ [Browse open cases] (existing apply route) │
└─────────────────────────────────────────────────────┘
KPIs show HK$0 / 0h — not skeletons that look like loading forever. EN+繁中 keys in §12.10.
12.7 Session / sequence state machine (Phase A condensed)
scheduleNextLesson → SCHEDULED (sequence N assigned)
logCompletedLesson → COMPLETED_PENDING_VERIFY (+ optional unpaid flag)
→ parent confirm OR +24h HKT auto → COMPLETED_VERIFIED
→ creditVerifiedHoursFromCaseLesson / twin with tuition_session_id
→ dispute → DISPUTED (staff)
undo ≤2h post-verify (if safe) / voidSession → VOIDED (soft)
12.8 Exact fee-breakdown.ts rebate hook (call site for #468)
Live file already exports lesson1Amount, lesson2BalanceAmount, fullAdminFeeAmount, derivePaidState.
Add (do not break existing tests):
/** Rebate: L2 tranche becomes 0.25R; L1 stays 1.0R; full = 1.25R */
export const LESSON2_REBATE_MULTIPLIER = 0.25;
export const FULL_ADMIN_WITH_REBATE_MULTIPLIER = 1.25;
export function lesson2BalanceAmountWithRebate(rateHkd: number, studentCount?: number): number {
return Math.round(rateHkd * normalizeStudentCount(studentCount) * LESSON2_REBATE_MULTIPLIER);
}
export function fullAdminFeeAmountWithRebate(rateHkd: number, studentCount?: number): number {
return Math.round(rateHkd * normalizeStudentCount(studentCount) * FULL_ADMIN_WITH_REBATE_MULTIPLIER);
}
export type AdminFeeQuoteInput = {
rateHkd: number;
studentCount?: number;
/** When true, next PLATFORM_MATCH_1_5 uses 1.25R split */
applyFeeRebate0_25?: boolean;
};
export function quotePlatformAdminFee(input: AdminFeeQuoteInput): {
l1Hkd: number; l2Hkd: number; totalHkd: number; multiplier: number;
} {
const l1 = lesson1Amount(input.rateHkd, input.studentCount);
const l2 = input.applyFeeRebate0_25
? lesson2BalanceAmountWithRebate(input.rateHkd, input.studentCount)
: lesson2BalanceAmount(input.rateHkd, input.studentCount);
const total = l1 + l2; // equals fullAdmin* helpers; prefer sum for clarity
return {
l1Hkd: l1,
l2Hkd: l2,
totalHkd: total,
multiplier: input.applyFeeRebate0_25 ? 1.25 : 1.5,
};
}One-time-use flag on next match (state): store on tutor or redemption row #468 RewardRedemption; when creating/invoicing next PLATFORM_MATCH_1_5 case_fees, call quotePlatformAdminFee({ …, applyFeeRebate0_25: true }), set case_fees.multiplier = 1.25, mark redemption FULFILLED + tuition_case_id in same transaction. Second invoice attempt must see redemption already FULFILLED → quote at 1.5.
QA: extend tests/cases/fee-breakdown.test.ts — R=450 → l1=450, l2=113 (round 112.5→113) or document banker’s vs Math.round — lock: use same Math.round as existing (450*0.25=112.5→113 in JS Math.round). Fixture assert total 563. Base tests for 0.5 L2 must remain green.
Cross-link: eligibility / claim UI owned by #468; apply call site owned here.
12.9 Implementation checklist — Phase A
- Migration:
scheduled_sessions(or extendcase_lessonswith unpaid/tuition flags + client_request_id) + any bucket views - Backend:
logCompletedLesson,scheduleNextLesson,confirmLessonComplete(parent),autoVerifyDueSessionsjob,markTuitionCollected,voidSession,getEarningsBuckets - fee-breakdown.ts: rebate helpers +
quotePlatformAdminFee; wire invoice path - Bridge: on verify/done →
#459 creditVerifiedHoursFromCaseLesson - UI: modals on
/dashboardcase cards; 4 KPI cards; unpaid helper; empty state; tutor-pays label fix - QA: Example 1 fee fixture ±1 HKD; double-submit idempotent; concurrent sequence unique; RLS matrix; EN+繁中 keys
12.10 i18n keys (Phase A)
earnings.kpi.collected
earnings.kpi.pipeline
earnings.kpi.unpaid
earnings.kpi.byo_private
earnings.unpaid.label
earnings.unpaid.helper
earnings.unpaid.mark_paid
earnings.empty.title
earnings.empty.body
earnings.log_lesson.cta
earnings.log_lesson.title
earnings.log_lesson.no_rate
earnings.schedule_next.cta
earnings.schedule_next.title
earnings.toast.logged_pending
earnings.toast.verified
earnings.cap57.disclaimer
12.11 RLS / authz matrix (Phase A)
| Actor | Action | Allow |
|---|---|---|
| Tutor A | log/schedule/mark paid on A’s matched cases | ✅ |
| Tutor A | same on Tutor B’s case | ❌ |
| Parent P | confirm complete on own case | ✅ |
| Parent P | mark tuition paid / see BYO private $ | ❌ |
| Parent P | see admin fee amounts owed by tutor | ❌ (tutor-pays; parent sees lesson confirm only) |
| Staff | void / force verify / set rate | ✅ |
| Anon | earnings APIs | ❌ |
12.12 Additional Phase A ACs (quality pass)
- Log modal is modal, bound to case card
caseId, fields per §12.1. - No rate → block + locked copy.
- Concurrent verify idempotent; sequence unique under race.
- Pipeline uses §12.4 pseudocode only.
- Unpaid Mark paid = tutor-only.
- Zero-case empty state live.
-
quotePlatformAdminFee+ rebate tests added; base 1.5 tests still pass. - No Phase B calendar UI shipped.
Activity
owlstonbusiness-svg commentedon Oct 25, 2026
Parent foundation: urgent V1 #459. Fee locks: tutor-pays 1.5 (#252/#444) · short ≤5 20% package #443 · Cap.57/#412/#442 B2B. Credits hours into #459 ledger on complete.
[-][Parked Epic] Tutor/Centre Scheduling Calendar & Revenue Forecasting Engine (ERP)[/-][+][Parked Epic · under #459] Scheduling Calendar & Revenue Forecasting (ERP)[/+]owlstonbusiness-svg commentedon Oct 25, 2026
Packed under Living-CV / Account Platform Track
[-][Parked Epic · under #459] Scheduling Calendar & Revenue Forecasting (ERP)[/-][+][V1 Phased · under #467] Scheduling + Revenue & Earnings Engine (4 Buckets)[/+]owlstonbusiness-svg commentedon Oct 25, 2026
Parent #467. Body fully replaced 2026-10-25 with Phase A/B, fee locks, schemas, dashboard routes (
/dashboard#lessons/#earnings, reusefee-breakdown.ts). Prototype https://share.gemini.google/OWAm773iPg6Sowlstonbusiness-svg commentedon Oct 25, 2026
ADOPT — Grok gaps (CEO 2026-10-25)
1. Minimal “log / complete known lesson” path (Phase A — REQUIRED)
Human-assisted matching still hands off after L2; without a thin session writer the 4-bucket dashboard stays empty until Phase B calendar.
Phase A must include (no full drag-drop calendar):
tutoring_cases/ account case): action “Log completed lesson” and “Schedule next lesson” (single datetime + duration + rate defaults from case).ScheduledSession(or equivalent) with correctlessonSequenceNumber, fee snapshot via existingfee-breakdown.ts, then on complete → Pipeline→Realized +#459VerifiedHoursLedgerwrite.2. Unpaid bucket — crystal-clear UI (Phase A)
Pending Tuition Collection= money parent/student owes the tutor/centre (tuition), NOT MatchMax 1.5 admin fee.3. BYO / external — promote thin path into Phase A (ADOPT)
Parked full BYO invite loop stays Phase B, but Phase A gets:
Private · External · 0% admin.AC additions (Phase A):
owlstonbusiness-svg commentedon Oct 25, 2026
Hook for later: session complete → MaxPoints (#468)
Parked retention/rewards child: #468.
Once Phase A has the minimal log/complete-lesson → ledger writer, a thin follow-on hook can award MaxPoints (and feed the weekly lesson streak) on
VERIFIED_COMPLETED. Do not build streaks/vault inside this issue — keep Phase A focused on 4-bucket dashboard + session writer + unpaid-bucket clarity.Gamification reads ledger hours; it must not invent session rows.
owlstonbusiness-svg commentedon Oct 25, 2026
Gap close-out (CEO adopt · Grok batch)
Body patched (permanent ACs, not comment-only):
fee-breakdown.tsaligned with [Parked · Full Epic] Gamified Retention Engine — Streaks · MaxPoints · Trophy Ladder · Rewards Vault · Fee Rebate #468 §4.Build order unchanged: #459 → #466∥ → #461A → #462A → #468A → rest parked.
owlstonbusiness-svg commentedon Oct 25, 2026
Later KPI (Phase D of #474): add a 4th earnings pill on the tutor/centre revenue dashboard:
Sit beside the existing 1-on-1 tutoring buckets. Not Phase A of #461 — only after Vehicles Phase A/B billing exists. Cap.57 / tutor-pays 1.5 unchanged; Vehicle fees are separate content/SaaS revenue (default platform cut 12%).
owlstonbusiness-svg commentedon Oct 25, 2026
Cross-link — Collect Tuition tool rides Phase A unpaid bucket
Parked #475 Phase 1 (after this issue’s Phase A +
#459):/dashboard#earnings(centres/business/)#459) stays independent of paid/unpaidPhase 0 of #475 = keep manual FPS admin + notes cashier; no change to this issue’s build order.
owlstonbusiness-svg commentedon Oct 25, 2026
Payer-conflict loop closed (2026-10-25 Asia/Hong_Kong)
#444 closed · #469 merged — live ToS §15 = tutor-pays (
owed by the Tutor; Customers and centres do not pay MatchMax a matching fee).Body top updated: RESOLVED note + payer lock refreshed. Build/copy against tutor-pays. Admin-fee automation identity gate on #444 cleared; other #475 phase gates / On Hold status unchanged. Urgent labels untouched.
6 remaining items
owlstonbusiness-svg commentedon Oct 28, 2026
CEO integrate (2026-10-28) — Grok Expert CORRECTED split
P1 this sprint: Calendar top-bar tab immediately after Cases + signed-out → Google (nav discoverability). Icon = navy + cyan binder (CEO art on issue).
P2 / follow-on: full Schedule & Earnings hub (upcoming lessons, students one place, unpaid/collect, dual confirm, month forecast, prior-month history).
Grok had Calendar as P2 end-to-end; CEO elevates nav entry only to P1.
owlstonbusiness-svg commentedon Oct 28, 2026
CEO re-check (2026-10-28) — Grok wanted P2; KEEP P1 tab
Grok re-check: still no Calendar after Cases; ranked full Calendar as P2.
CEO KEEP: Calendar top-bar tab after Cases stays P1. Full Schedule & Earnings hub remains Phase A / P2.
owlstonbusiness-svg commentedon Oct 28, 2026
CEO re-check ~14:33 HKT — Grok Expert CORRECTED
Still no Calendar after Cases.
ADOPT sprint order: Calendar tab = P2 if capacity (after P0 three + P1 #415/#501). Placement lock unchanged: binder icon immediately after Cases; signed-out → Google. Hub Phase A can follow the tab.
owlstonbusiness-svg commentedon Oct 28, 2026
Cross-link — ratings + End Case (#503)
CEO 2026-10-28: after dual-confirm / done lesson, ratings unlock (carousel UX). Editable while case active. Bilateral End Case freezes ratings and counts the case on parent + tutor profiles (#459 / #387). Prefer wiring End Case through this complete/verify machine — don’t invent a second lesson status system.
[-][V1 Phased · under #467] Scheduling + Revenue & Earnings Engine (4 Buckets)[/-][+][V1 · #467] Scheduling + revenue/earnings engine (4 buckets)[/+]owlstonbusiness-svg commentedon Oct 29, 2026
Merge target: Classcard calendar/attendance/ledger epic → #530 (extend this issue in Phase 1; don’t fork a second calendar).
owlstonbusiness-svg commentedon Oct 29, 2026
Hybrid foundation: This issue is the calendar/scheduling foundation for optional MatchMax Managed (#530 / Phase 1 #533), not only pure-matching availability. Extend here — do not fork a second calendar. Pure-matching tutors still get lightweight availability only.
keyframesfound commentedon Oct 30, 2026
Extended by #533 (#530 Phase 1) — no second calendar.
case_lessonsnow carries attendance (present / late / left early / absent excused / absent billable), a soft note gate and, for opt-in Managed cases only, the #532 credit ledger (HOLD at book → CONSUME / RELEASE). Pure-matching tutors keep lightweight availability. Admin master calendar:/admin/academy. Details:docs/academy-ledger.md(main c05c5fb).feat(#533): attendance + soft note gate + Managed credit ledger on th…
feat(#461): tutor earnings buckets on /dashboard (Phase A 3a)
feat(#461): pending tuition tracking + lesson soft-void (Phase A 3b)
feat(#461): tutor 'Schedule next lesson' + fix 12h expiry cancelling …
feat(#461): centre lesson summary on /business (Phase A 3d)
Merge feat/461-earnings-3a: #461 Phase A earnings, tuition, schedule …