Skip to content

[V1 · #467] Scheduling + revenue/earnings engine (4 buckets) #461

Open

Description

@owlstonbusiness-svg

[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 · mockup revenue-simulator.png on parent.
Labels intent: enhancement + urgent for 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.

  1. 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.
  2. Unpaid tuition (owed to you) — never conflate with MatchMax 1.5 admin fee; helper text required.
  3. 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)

  1. Off-platform after L2 → hours freeze; Active drops after 45d.
  2. Stay on calendar L3+ → 0% admin + retention/search climb.
  3. Parent/student confirm → own learning hours on CV.
  4. 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 ScheduledSession writer 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

  1. Four financial buckets header (week / month / custom term range simple month+week first).
  2. Lesson complete → Pipeline → Realized + write dual-sided #459 VerifiedHoursLedger.
  3. CEO fee math (below) using existing fee-breakdown.ts (extract/extend from live src/features/cases/sor.ts computeFeeAmountHkd) helpers.
  4. Multi-student list + hourly rate; stacked bar by student (green realized / blue pipeline / pink admin fee) matching Gemini mockup.
  5. Session states: Completed (24h auto-verify), Reschedule (preserve L1/L2 sequence), Cancel free, Late cancel (cash may count, hours = 0).
  6. Pending Tuition Collection (Unpaid) bucket.
  7. Cap.57 Independent Tutor disclaimer in UI; centres via Organization (#465).
  8. 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 (#466 owns 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

  1. Desk matches parent case → tutor (Tutor Code flow unchanged).
  2. System creates TuitionCase with billingModel=PLATFORM_MATCH_1_5, matchmaxCaseId, rate from case.
  3. Tutor opens /dashboard#lessons → sees case in multi-student list.
  4. Tutor adds ScheduledSession (datetime + duration). Pipeline updates.
  5. After lesson, tutor taps Complete → pending verify.
  6. Parent confirms on /account/cases/$caseId OR 24h auto.
  7. L1: admin=G, net=0; ledger +1 teaching / +1 learning hours × duration.
  8. 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 (#459 ParentLearnerLink).
  • Public API never returns dollar amounts from this engine (private dashboard only).
  • PDPO: financial + student identifiers stay private; public surfaces use #462 privacy tiers.
  • Timezone: persist UTC; display Asia/Hong_Kong.
  • No manual hour edits — hours only via verified sessions (#459 rule).

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/advances ScheduledSession → fee snapshot via fee-breakdown.ts → on verify writes dual-sided #459 ledger. 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 #lessons section) shows 4 buckets + Pending Unpaid for authenticated tutor.
  • Completing+verifying session updates Realized, decrements Pipeline projection, writes two #459 ledger 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 #443 rules.
  • TUTOR_SELF_IMPORTED admin always 0.
  • Cap.57 disclaimer visible for Independent Tutor.
  • Org path shows same UI under /business/ filtered by organizationId.
  • 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:

  1. 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).
  2. Fee rebate apply path (LOCKED split — keep fee-breakdown.ts consistent with [Parked · Full Epic] Gamified Retention Engine — Streaks · MaxPoints · Trophy Ladder · Rewards Vault · Fee Rebate #468): when tutor has unused ADMIN_FEE_REBATE_0_25 redemption, next new PLATFORM_MATCH_1_5 TuitionCase admin total 1.5R → 1.25R where R = one-lesson gross (hourlyRate × durationHours for 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.ts whole-HKD rules (document rounded L1/L2 in QA fixture).
    • Cap.57-safe copy. Do not invent a third split.
  3. 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 is src/features/cases/sor.ts (FEE_MULTIPLIER = 1.5, computeFeeAmountHkd). File fee-breakdown.ts is the target extract/extend name used in this epic — create it by moving/wrapping sor.ts helpers 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 lessonSequenceNext or double-write ledger. Unique constraint recommended: (tuitionCaseId, occurredAt, durationHours) where not VOIDED, plus clientRequestId UUID 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)

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)

  • logCompletedLesson is idempotent (double submit ≠ double N / double ledger).
  • Dual confirm + 24h HKT auto-verify + dispute path implemented; undo rules respected.
  • voidSession soft-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_20 does 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 /dashboard case cards.
  • EN + 繁中 for Phase A strings.
  • Fee math via single module (fee-breakdown.ts wrapping/extending sor.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#earnings case 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 → caseId is bound; no case picker inside modal.
  • Modal title shows student first name + subject + rate (from card props).
  • Only cases with matched_tutor_id = me and 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


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 iff tutors.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_audit reason tuition_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 extend case_lessons with unpaid/tuition flags + client_request_id) + any bucket views
  • Backend: logCompletedLesson, scheduleNextLesson, confirmLessonComplete (parent), autoVerifyDueSessions job, markTuitionCollected, voidSession, getEarningsBuckets
  • fee-breakdown.ts: rebate helpers + quotePlatformAdminFee; wire invoice path
  • Bridge: on verify/done → #459 creditVerifiedHoursFromCaseLesson
  • UI: modals on /dashboard case 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

  1. owlstonbusiness-svg commented on Oct 25, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    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.

  2. changed the title [-][Parked Epic] Tutor/Centre Scheduling Calendar & Revenue Forecasting Engine (ERP)[/-] [+][Parked Epic · under #459] Scheduling Calendar & Revenue Forecasting (ERP)[/+] on Oct 25, 2026
  3. owlstonbusiness-svg commented on Oct 25, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    Packed under Living-CV / Account Platform Track

  4. changed the title [-][Parked Epic · under #459] Scheduling Calendar & Revenue Forecasting (ERP)[/-] [+][V1 Phased · under #467] Scheduling + Revenue & Earnings Engine (4 Buckets)[/+] on Oct 25, 2026
  5. added
    urgentGood for newcomers
    and removed
    On HoldExtra attention is needed
    on Oct 25, 2026
  6. owlstonbusiness-svg commented on Oct 25, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    Parent #467. Body fully replaced 2026-10-25 with Phase A/B, fee locks, schemas, dashboard routes (/dashboard#lessons / #earnings, reuse fee-breakdown.ts). Prototype https://share.gemini.google/OWAm773iPg6S

  7. owlstonbusiness-svg commented on Oct 25, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    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):

    • From matched / active case (existing tutoring_cases / account case): action “Log completed lesson” and “Schedule next lesson” (single datetime + duration + rate defaults from case).
    • Creates ScheduledSession (or equivalent) with correct lessonSequenceNumber, fee snapshot via existing fee-breakdown.ts, then on complete → Pipeline→Realized + #459 VerifiedHoursLedger write.
    • Empty state copy: “No lessons logged yet — log your first completed lesson from an active case to see Realized / Pipeline fill.”

    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.

    • Label: “Unpaid tuition (owed to you)”
    • Helper under card: “MatchMax admin fee is already deducted in Realized/Pipeline net. This bucket is only parent tuition still outstanding.”
    • Empty state: “No unpaid tuition tracked.” Never show admin fee here.

    3. BYO / external — promote thin path into Phase A (ADOPT)

    Parked full BYO invite loop stays Phase B, but Phase A gets:

    • “Log external lesson (private $ only, 0% admin)” — counts in private Realized/Pipeline only; does not write public Verified Hours or Trophy until parent/student links MatchMax account (Phase B viral invite).
    • Badge on row: Private · External · 0% admin.

    AC additions (Phase A):

    • From an active matched case, tutor can log ≥1 completed lesson without Phase B calendar; buckets + ledger update.
    • Unpaid card never displays MatchMax admin fee; helper text visible.
    • External private log appears in private earnings only; public hours/Trophy unchanged.
  8. owlstonbusiness-svg commented on Oct 25, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    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.

  9. owlstonbusiness-svg commented on Oct 25, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    Gap close-out (CEO adopt · Grok batch)

    Body patched (permanent ACs, not comment-only):

    1. Explicit Phase A AC for minimal [Log completed lesson] + [Schedule next lesson] on existing matched cases — Phase A blocker; no dependency on Phase B calendar.
    2. Fee rebate apply path LOCKED for when [Parked · Full Epic] Gamified Retention Engine — Streaks · MaxPoints · Trophy Ladder · Rewards Vault · Fee Rebate #468 unparks: 1.5R → 1.25R with L1 = 1.0R (unchanged) · L2 = 0.25R (was 0.5R). HK$ example with R documented. Keep fee-breakdown.ts aligned 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.

  10. owlstonbusiness-svg commented on Oct 25, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    Later KPI (Phase D of #474): add a 4th earnings pill on the tutor/centre revenue dashboard:

    🔄 Vehicle MRR (Passive Income): HK$ X/mo

    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%).

  11. owlstonbusiness-svg commented on Oct 25, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    Cross-link — Collect Tuition tool rides Phase A unpaid bucket

    Parked #475 Phase 1 (after this issue’s Phase A + #459):

    • Collect via FPS QR / Stripe Payment Link on /dashboard#earnings (centres /business/)
    • Clears Unpaid tuition (owed to you) only — never conflates with MatchMax 1.5 admin fee
    • MatchMax does not touch the money in Phase 1
    • Hours ledger (#459) stays independent of paid/unpaid

    Phase 0 of #475 = keep manual FPS admin + notes cashier; no change to this issue’s build order.

  12. owlstonbusiness-svg commented on Oct 25, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    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.

  13. 6 remaining items

  14. owlstonbusiness-svg commented on Oct 28, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    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.

  15. owlstonbusiness-svg commented on Oct 28, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    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.

  16. owlstonbusiness-svg commented on Oct 28, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    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.

  17. owlstonbusiness-svg commented on Oct 28, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    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.

  18. changed the title [-][V1 Phased · under #467] Scheduling + Revenue & Earnings Engine (4 Buckets)[/-] [+][V1 · #467] Scheduling + revenue/earnings engine (4 buckets)[/+] on Oct 29, 2026
  19. owlstonbusiness-svg commented on Oct 29, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    Merge target: Classcard calendar/attendance/ledger epic → #530 (extend this issue in Phase 1; don’t fork a second calendar).

  20. owlstonbusiness-svg commented on Oct 29, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    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.

  21. keyframesfound commented on Oct 30, 2026

    @keyframesfound
    Owner

    Extended by #533 (#530 Phase 1) — no second calendar. case_lessons now 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).

  22. added 6 commits that reference this issue on Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesturgentGood for newcomers

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions