Repository navigation
Case becomes the system of record #201
Description
Issue 1 — Case becomes the system of record
Summary
Move every match off the spreadsheet and WhatsApp-only workflow onto a Case. The case is the parent object for the 1.5-lesson admin fee, later lessons, reviews, and the calendar. Concierge still introduces people in WhatsApp. The website is where lesson 1, lesson 2, and the fee are recorded.
Schema must allow a centre to own a case later. No centre UI in this issue.
Why
We already have 120 tutors and 200+ matched cases, tracked by hand. The public site is about to take real traffic. If new matches still start only in a WhatsApp group, we cannot invoice cleanly or attach a calendar later. We do not collect ongoing tuition. Parents pay tutors directly after the intro. We only record and collect the 1.5-lesson admin fee.
Users
- Concierge / admin: creates the case, logs lessons, marks the FPS fee paid, chases unpaid fees.
- Parent: sees the match, confirms lesson 1 and 2, sees the fee status.
- Tutor: sees the match, confirms lesson 1 and 2, sees whether the admin fee is still owed.
Scope
In:
- Case record for every active and recent match, including a one-time import.
- Lesson rows for lesson 1 and lesson 2 only.
- Admin fee of 1.5 × agreed hourly rate.
- Admin unpaid queue.
- Explicit status transitions and an audit row.
- Required organisation link.
- Organisation member table.
- Tutor list.
Out:
- Ongoing scheduling (Issue 3).
- Notes shop (Issue 2).
- Reviews and automated reminders (Issue 4).
- Tutor payout of lesson money. We never hold lesson fees.
- Automatic FPS reconciliation.
- Centre pages, staff assignment UI, recruitment, ads.
- Any screen that asks “are you a centre?” during signup.
Data
Organisation, created silently for every tutor:
- id, kind (individual, centre), name, slug unique, created_at
- kind defaults to individual
- slug is reserved, not routed publicly in this epic
- no centre can be created from the product UI; admin may insert one by hand
OrganisationMember:
- organisation_id, account_id, role (owner, tutor)
- unique on (organisation_id, account_id)
- every current tutor gets one row, role owner
- no screen
Case:
- id, created_at, source (whatsapp, website_form, referral, import)
- organisation_id, required
- assigned_tutor_id, required
- parent_account_id, student_display_name, student_school optional, curriculum, subject, level
- hourly_rate_hkd decimal, lesson_mode (f2f, online), station / area
- status: draft, introduced, lesson_1_scheduled, lesson_1_done, lesson_2_scheduled, lesson_2_done, fee_invoiced, fee_paid, active, replaced, closed, waived
- concierge_id
- whatsapp_group_url, admin-only, never shown to the other party
- replacement_of_case_id nullable
- internal_note
Owner is organisation_id. The person teaching is assigned_tutor_id. For every current tutor these are the same human.
Lesson, sequence 1 and 2 only:
- case_id, sequence, starts_at, duration_min, location_or_link
- status: planned, confirmed, done, no_show, cancelled
- confirmed_by_parent_at, confirmed_by_tutor_at, marked_done_by, done_at
AdminFee:
- case_id is not unique
- rate_hkd, multiplier default 1.5, amount_hkd
- status: not_due, invoiced, paid, waived, refunded
- fps_reference, due_at, paid_at, marked_paid_by, waive_reason
- only one AdminFee per case may be not_due, invoiced, or paid
- a waived or refunded row may be followed by a new invoice
- a custom one-lesson invoice stores multiplier 1 and a reason
Audit:
- case_id, actor_id, from_status, to_status, reason, created_at
- an illegal jump writes nothing
Allowed transitions
- draft → introduced, closed
- introduced → lesson_1_scheduled, closed
- lesson_1_scheduled → lesson_1_done, closed
- lesson_1_done → lesson_2_scheduled, fee_invoiced, closed
- lesson_2_scheduled → lesson_2_done, closed
- lesson_2_done → fee_invoiced
- fee_invoiced → fee_paid, waived
- fee_paid → active
- active → replaced, closed
- waived → closed
A no-show or cancel on lesson 1 or 2 does not move the case to fee_invoiced. Admin override requires a reason and an audit row. Illegal status jumps fail and write no audit row.
Flow
- Admin creates a case before the intro is treated as a match.
- If admin passes an existing organisation_id, do not create an individual organisation. Create an OrganisationMember row only if that tutor has none on that organisation.
- If no organisation_id is passed, create the individual organisation and the owner member row in the same transaction, then store that id on the case.
- Admin sets assigned tutor, rate, subject, station, and the admin-only WhatsApp link.
- Admin logs lesson 1. Parent and tutor each confirm. Both confirms set the lesson to confirmed. Admin may confirm for a side; the audit says admin.
- Admin or either party marks it done. No-show and cancel do not count toward the fee.
- Repeat for lesson 2.
- When both are done, create the fee: hourly_rate × 1.5, status invoiced, due in 3 days. Admin may invoice earlier, or waive, with a reason.
- Admin sends FPS details manually. Parent pays. Admin pastes the reference and marks paid. Status becomes fee_paid, then active.
- Unpaid queue: invoiced fees past due, oldest first, with parent, tutor, amount, and the admin-only WhatsApp link.
Import
CSV: parent name, phone, tutor, subject, rate, lesson dates, fee paid yes/no.
- source=import. Phones are admin-only.
- Create the organisation and member row before the case.
- A rerun import reuses the organisation and does not insert a second owner row or a second slug.
Permissions
- Tutor list: student first name, subject, next lesson time, station, fee status. No parent phone. No WhatsApp link.
- Parent: own cases, tutor public card, lesson times, fee amount and status. No tutor phone.
- Admin: everything.
- Opening a case does not publish the tutor’s real name. No public organisation page.
Acceptance
- A concierge can create a case in under 2 minutes.
- Every live case has organisation_id.
- Passing an existing organisation_id does not spawn an individual organisation.
- Import rerun creates no duplicate organisation, slug, or owner row.
- Fee is not created from a no-show or cancel unless admin overrides with a reason.
- Fee amount is 1.5 × the rate on the case at invoice time, unless the invoice is edited and audited.
- Only one open fee exists per case.
- Parent and tutor cannot see each other’s phone or WhatsApp.
- Replacing a tutor closes the old case as replaced and opens a new case. A failed first match is a waive or a new invoice, not an automatic second charge.
- An admin-inserted kind=centre organisation can be attached without a code change. No UI for this.
Edge cases
- Rate changed after lesson 1: fee uses the rate at invoice time. Admin can lock the rate when lesson 1 is marked done.
- Only one lesson happened: no automatic fee. Admin can invoice multiplier 1 with a reason.
- Duplicate parent phone: attach to the existing parent account.
Activity
owlstonbusiness-svg commentedon Oct 7, 2026
The parent owes the admin fee, not the tutor. The tutor sees only whether MatchMax has marked it paid. The parent sees the amount and the status.
Short-term, as already defined in closed #98: if the parent asked for 5 lessons or fewer, the fee is 20% of the scheduled package instead of 1.5 lessons. Admin sets that on the case. It is still created only when the intro lessons are done, unless admin invoices earlier with a reason. A case marked short-term that continues past 5 lessons is an admin review, not an automatic second charge.
Orders groundwork: delivered-order gating + grant-safe payout export (#…
SoR batch: case system of record, calendar, reviews + notification bu…