Product Overview
Unified Service Model
Section titled “Unified Service Model”Every offering is modeled as a Service classified by 3 orthogonal dimensions:
| Dimension | Options | Example |
|---|---|---|
| deliveryMode | live, async, hybrid | Video call vs self-paced |
| groupType | individual, group, open | 1:1 vs capped vs unlimited |
| structure | single, package, course, subscription | One session vs N credits vs ordered sequence vs recurring |
Core flow: Service → Enrollment → Session
Enrollment Session Accounting
Section titled “Enrollment Session Accounting”sessionsTotal = total sessions purchasedsessionsScheduled = booked/reserved (not yet completed)sessionsCompleted = completedsessionsCancelled = cancelled (credit returned)sessionsForfeited = forfeited (no-show, late cancel — credit consumed)remaining = sessionsTotal - sessionsScheduled - sessionsCompleted - sessionsCancelled - sessionsForfeitedCore Flows
Section titled “Core Flows”Flow A: Trial Session Booking (Public)
Section titled “Flow A: Trial Session Booking (Public)”Student visits /:teacher-slug → Auto-detect timezone → See service catalog → Select trial service → See available slots → Fill form (name, email, phone?, level, goals, notes)
IF trial is FREE: → Create Session(pending_confirmation) → Email teacher: "New trial request" → Teacher confirms or rejects → IF confirmed: Session(scheduled), create calendar event, email student
IF trial is PAID: → Create Session(hold) + holdExpiresAt = now + 15min → Create Stripe Checkout Session (on teacher's connected account) → Webhook: checkout.session.completed → Session(scheduled)Flow B: Service Enrollment (Buy then book)
Section titled “Flow B: Service Enrollment (Buy then book)”Student visits /:teacher-slug → See services tab → Select service → See pricing + availability preview
IF first purchase (no account): → Magic link auth → Accept legal documents → Redirect to Stripe CheckoutELSE: → Check if legal docs need re-acceptance → Redirect to Stripe Checkout
Webhook: checkout.session.completed → Enrollment(active), credit GRANT → Student books sessions from portalFlow C: Session Booking (Authenticated Student)
Section titled “Flow C: Session Booking (Authenticated Student)”Student portal → /student/book → Select enrollment (package with remaining credits) → Select eligible service → See available slots (preferred hours highlighted) → Confirm booking
Backend: → Check remaining credits >= 1 → Check policy engine (max active bookings, min notice) → Check slot available (race condition protection) → CreditLedger: RESERVE -1 → Create Session(scheduled) → Create Google Calendar event → Trigger Drive auto-copy for linked resourcesFlow D: Session Completion
Section titled “Flow D: Session Completion”BullMQ job at: session.endsAt + gracePeriod
IF teacher.completionMode == AUTO_COMPLETE: → Session(completed), CreditLedger: CONSUME
IF teacher.completionMode == PENDING_REVIEW: → Session(pending_review), notify teacher
Teacher can override: COMPLETED / NO_SHOW_STUDENT / NO_SHOW_TEACHER / CANCELEDFlow E: Cancellation
Section titled “Flow E: Cancellation”Student or teacher requests cancellation
PolicyEngine.evaluateCancellation(): → Resolves policy: service-specific → teacher default → legal docs → allow all → Evaluates rules in order (first match wins) → Actions: full_refund (RELEASE), partial_penalty (CONSUME), forfeit (CONSUME), block (not allowed), allow_free (RELEASE)
→ Session status updated→ Credits adjusted via SessionCreditHandler→ Calendar event cancelled→ Waitlist: if slot freed → auto-notify next personFlow F: Waitlist
Section titled “Flow F: Waitlist”Service at capacity (active enrollments >= maxParticipants)
Student joins waitlist: → WaitlistEntry(waiting, position=N) → Partial UNIQUE ensures one active entry per student per service
When slot opens (enrollment cancelled / session cancelled): → WaitlistService.onSlotFreed() checks capacity → Auto-notifies next person: WaitlistEntry(notified), email sent → 48-hour offer window (expiresAt)
If student enrolls: WaitlistEntry(enrolled)If offer expires: WaitlistEntry(expired), notify next person → BullMQ hourly cron job expires stale entries
Teacher can also manually notify specific entries.Slot Engine (Availability Calculation)
Section titled “Slot Engine (Availability Calculation)”Inputs
Section titled “Inputs”availability_schedules+availability_rules— Named weekly schedules with date-range activationsavailability_overrides— One-off time off or extra availabilityservices.sessionDurationMinutes— Slot durationteacher.bufferMinutes— Gap between sessionsteacher.minNoticeHours— Minimum advance booking- Google Calendar Free/Busy — Busy blocks from selected calendars
- Existing sessions in DB (scheduled/hold statuses)
viewer_timezone— Student’s IANA timezone for displaypreferredHours— Student’s preferred time range + days (optional)
Algorithm
Section titled “Algorithm”1. Resolve which schedule applies for each date via activations2. Load rules for active schedules + overrides3. Generate candidate slots (teacher TZ → UTC)4. Filter: remove slots where start < now + minNoticeHours5. Fetch Google Calendar Free/Busy6. Fetch existing DB sessions7. Merge busy blocks (Google + DB + buffer)8. Remove overlapping slots9. Convert to viewer timezone10. Mark isPreferred based on student's preferred hours11. Return: [{ startUtc, endUtc, startLocal, endLocal, isPreferred }]Review System
Section titled “Review System”Sources
Section titled “Sources”- PinTeach: Students submit reviews via in-app prompts after sessions
- External: Teachers import from Preply, Italki, Verbling, Google Business, Trustpilot
Teacher Dashboard (/teacher/reviews)
Section titled “Teacher Dashboard (/teacher/reviews)”5 tabs: Overview (KPIs + recent), All (searchable, filterable), Requests (send solicitations), Import (external reviews), Settings (auto-request, auto-approve, display).
Widgets
Section titled “Widgets”Embeddable review widgets with 4 layouts (grid/carousel/list/wall), configurable via /teacher/widgets.
Resource Library & Materials
Section titled “Resource Library & Materials”Structure
Section titled “Structure”material_folders: Hierarchical folder system (self-referential parentId)lesson_templates: Reusable lesson blueprints with meet link, default contentteacher_resources: Resources (links, Drive files, YouTube, Vimeo) with provider + kind
Google Drive Integration
Section titled “Google Drive Integration”- Drive file picker for adding resources
- Auto-copy: when Drive resources are linked to a session,
DriveCopyServicequeues BullMQ jobs to copy files into student folders - Tracked in
drive_file_copiestable (pending → copying → copied → failed)
Key Decisions
Section titled “Key Decisions”| Decision | Choice | Rationale |
|---|---|---|
| Service model | 3-dimension unified | Replaces old split of 5+ entities |
| Payment model | Buy service first, book later | Simpler, no HOLD on purchase |
| Credits | Reserve on book, consume on complete | Flexibility for changes/no-shows |
| Stripe | Connect (teacher collects) | We don’t handle funds |
| Student auth | Magic link | Zero friction |
| Teacher auth | Google OAuth | Already uses Google |
| Auto-complete | Default ON, grace 30min | Less clicks, with manual override |
| DB dates | UTC + IANA timezone | Zero DST errors |
| Google Calendar | Integration, not dependency | Core works without Google |
| Cancellation policies | Per-service JSONB rules | Flexible, template-based |
| Resources | Normalized tables + junctions | NOT JSONB arrays |
| Soft-delete | 7 tables with deletedAt | Reversible, audit-friendly |
| Audit log | Append-only, fire-and-forget | Never blocks primary operations |
| Waitlist | Auto-notify on slot freed | Reduces teacher manual work |
| Preferred hours | Visual only, no filtering | Students see all slots, preferred highlighted |