Skip to content

[TRACKER] Booking + payments code architecture: folder layout, API surface, model and function naming #1332

Description

@teetangh

Why this issue exists

Wave 5 (#1319) touched most of the booking, payments and maintenance code and kept running into the same structural friction: two god modules, three parallel places for one job, several names for one concept, and routes whose names describe their history rather than their contract. This issue is a tracker only. Nothing here is scheduled; it collects the naming and layout decisions so they can be made once, deliberately, after the pre-MVP reset, instead of being re-litigated PR by PR. Items were gathered from the wave-5 audits and PR reviews; no fresh exploration was run for this draft, so line counts and paths are as of 2026-09-02.

1. Folder layout

Today Problem Proposed
lib/ and utils/ both hold domain logic (lib/booking/*, lib/appointments/*, utils/slotAllocation/*, utils/appointmentlock.ts, utils/timeSlotsProcessing.ts, utils/schedule/*) "utils" is where the allocator, the lock module and the slot generator live; nothing about them is a utility One domain root per bounded context: lib/booking/{availability,allocation,checkout,lifecycle,locks,occupancy,participants,transitions} ; utils/ keeps only pure helpers with no Prisma import
scripts/<area>/x.ts + jobs/<area>/x.ts + app/api/cleanup/x/route.ts for every cron Three files per job; the HTTP twin drifted from the job wrapper (guards, response shapes) until #1324's cleanupRoute() factory jobs/<area>/x.ts is the only entry; the HTTP twin is generated from the same registration; scripts/ keeps one-off operator scripts only (scripts/db/coalesce-*, scripts/db/sidecar-objects.ts)
lib/scheduling/*, hooks/scheduling/*, components/scheduling/* next to utils/slotAllocation/* "scheduling" (client) and "slotAllocation" (server) are the same concept with two names; the deleted client allocator's successors kept the old file names lib/booking/availability (server) + features/scheduling/{hooks,components} (client) ; names match the server concept
lib/data/* reads next to lib/api/scope/* and hand-rolled organizationId pins in lib/data/needs-you.ts, staff-appointments.ts, consultant-dashboard.ts Org scoping has a single source but readers bypass it Every read under lib/data takes a Scope argument; no organizationId literal outside lib/api/scope
actions/* (server actions) duplicating app/api/* handlers for the same mutation (stream, maintenance) Two entry points, one guarded, one not One handler per mutation in app/api, actions call it (decision 2026-07-17 stands: Route Handlers)
__tests__/{booking-algorithm,booking,payments,maintenance,collaborators,enterprise} + tests/typescript/race-conditions Suite placement decides whether CI-only failures happen (PR 4's collaborators suite was not in the folders the executor ran) Mirror the domain roots: __tests__/booking/**, __tests__/payments/**; the race suite under __tests__/booking/race/**
prisma/sql/{check-constraints,ledger-triggers}.sql + prisma/sql/one-off/* Correctness backstops live outside the schema and outside the migration story Keep, but name by concern (booking-exclusion.sql, ledger-invariants.sql) and index them from one prisma/sql/README.md (the reset runbook already points there)

2. API surface

Today Problem Proposed
/api/bookings/{consultations,subscriptions,webinars,classes}/..., /api/appointments/[id]/{cancel,reschedule,...}, /api/slots/**, /api/trials/**, /api/checkout, /api/participants/{class,webinar}/[id] Five roots for one aggregate; a client cannot tell from the path which of the five booking shapes it is talking to /api/booking/requests/[id] (consultation + subscription), /api/booking/events/[id] (webinar + class), /api/booking/appointments/[id]/{cancel,reschedule}, /api/booking/availability/**, /api/booking/checkout; trials fold under requests with kind=TRIAL
crud-with-plan in the path (/api/bookings/webinars/crud-with-plan) Describes an implementation detail from 2024 /api/booking/events (create with plan is the only create)
availability-with-allocation/[consultantId] The grid endpoint's name lists its two joins /api/booking/availability/[consultantId]/grid
Four near-identical [id]/allocate/route.ts (54% duplicated per SonarCloud on #1329) Same body, four files One POST /api/booking/requests/[id]/allocate with mode in the body; the service already switches on event type
request-for-approval (verb-noun), reschedule/respond, reschedule/withdraw, cancel/preview Mixed verb styles Sub-resource verbs consistently as the last segment: requests/[id]/approve, appointments/[id]/reschedule/{respond,withdraw}, appointments/[id]/cancel/preview (keep), requests POST replaces request-for-approval
/api/cleanup/* for cron HTTP twins "cleanup" names one of eight job kinds /api/jobs/[job] from the job registry (see §1)

3. Model and enum naming (schema, post-reset only)

Today Problem Proposed
SlotOfAvailabilityWeekly, SlotOfAvailabilityCustom, SlotOfAppointment "Slot" means three things: a published window, a bookable 30-minute atom, and a booked row AvailabilityWindow (weekly rule), AvailabilityException (dated custom), SessionAtom (booked 30-minute row); "slot" reserved for the UI's bookable unit
Appointment with four nullable FKs (consultationId, subscriptionId, webinarId, classId) + trialSession Polymorphism by nullable columns; every reader does a five-arm OR Keep for launch; post-reset candidate: Booking (kind + one requestId) with BookingRequest / BookingEvent tables, participation via AppointmentParticipant (#1322)
Three status enums (AppointmentStatus, EventStatus, TrialSessionStatus) and SlotCompletionStatus Three state machines for one lifecycle; BookingStatusHistory stores them as strings for this reason One BookingStatus after the aggregate merge; until then keep the CAS helpers as the only writers
isTentative overloaded: unpaid hold AND being rescheduled Two meanings, one boolean holdState (HELD / CONFIRMED) on the atom + RescheduleRequest owning the "moving" state
bookingSource (DIRECT_CHECKOUT / REQUEST_SUBMITTED) Reads as origin, is used as a lifecycle discriminator (#1328 hold expiry) entryMode
ConsultantProfile.scheduleType (WEEKLY xor CUSTOM) Exclusive by convention only (verified in prod: no consultant has both) CHECK constraint in the sidecar + one AvailabilityMode enum named for what it is

4. Module and function naming

Today Problem Proposed
lib/payments/operations/checkout.ts (3,666 LOC), utils/slotAllocation/SlotAllocationService.ts (3,901 LOC) Every wave patches the same two files; the class has ~40 issue-numbered patches Split by extraction after #1322 lands: checkout/{validate,locks,commit,handlers/<type>}.ts, allocation/{occupancy-read,candidate-search,commit}.ts; the 46-scenario race suite is the oracle
validateSlotAvailability exists twice with different contracts (checkout.ts, app/api/trials/[trialId]/route.ts) Same name, one checks the published window, the other only conflicts assertWithinPublishedWindow + assertNoOccupancyConflict, both in lib/booking/occupancy
Three merges: mergeConsecutiveSlots (generator), mergeAdjacentWeeklyRows / mergeAdjacentCustomRows (storage), app/explore/experts/[consultantId]/utils/mergeSlots.ts (display, tolerant) Three "merge" verbs with three semantics coalesceAvailabilityRows (storage, exact adjacency), mergeBookableSlots (generator), groupForDisplay (UI, tolerant) ; the file mergeAdjacentWeeklyRows.ts now also holds custom logic and needs renaming to coalesceAvailabilityRows.ts
deleteExistingAppointments (allocator) Deletes only never-paid tentative rows but is named as a general delete (rule 2) releaseUnpaidTentativeRows with the Payment guard in the name's contract
reconnectEnrolledUsers, connectAttendeeToEventSlots (#1331), recordParticipants (#1322) Three verbs for "seat this user" seatAttendee / unseatAttendee on the participants module, the join-table connect as an implementation detail
isOccupiedByLiveAppointment + buildOccupiedAppointmentFilter + buildDeadHoldFilter + buildConsultantOccupancyWhere Four functions define "busy" One occupancy module exporting isLive(row) and liveWhere() with the JS/SQL parity test beside them
Lock names slot-booking:, consultee-booking:, event-checkout:, auto-allocate:, consultation-approval:, subscription-approval:, approval-payment-mint:, appointment: Fine individually; the global order (event/consultant → consultee → slot; approval → mint) lives in comments lib/booking/locks.ts exporting a typed LockAtom union with the order encoded as a rank, so a mis-ordered acquisition fails at compile time
transitionConsultationRequest, transitionSubscriptionRequest, transitionWebinarEvent, transitionClassEvent, transitionRescheduleRequest, transitionSlotCompletion, transitionTrialSession Seven helpers, one pattern One generic transition(entity, id, to, opts) over a per-entity allowed-from table, after the enum merge
handleConsultationCheckout / handleSubscriptionCheckout / ... vs createConsultation / createSubscription (webhook fallback) Two creators per type with different shapes (#1331 aligned them) One createBookingRows(kind, input) used by both paths; the webhook fallback becomes a thin caller

5. Docs, prompts, skills

  • docs/booking/** is banded but 15-checklist.md, 06-dependency-graphs.md and README.md still describe retired mechanisms (tracked for PR 11 of [UMBRELLA] Booking + maintenance productionization, wave 5 — verified residuals, HLD/LLD verdict, 11-PR train #1319).
  • prompts/booking-algorithm-tests/ cites /api/events/* (renamed 2026-06-12) and none of initialAllocation, Idempotency-Key, reschedule/respond, cancel/preview, orgScope, partial allocation. A regeneration from code is its own PR.
  • .claude/skills/booking-doctrine carries seven rules in one file; rule 6 is stale. Candidate split: booking-doctrine (rules 1–5, 7), booking-concurrency (locks, CAS, sidecars, retry budgets, function ceiling), booking-availability (rows, atoms, coalescing, union validation, scheduleType), booking-money-boundary (refund front doors, holds, dead-hold rule, parity), booking-verification (dev-server recipe, mock payments, chaos suite, which jest folders to run).

Out of scope for this tracker

Behavioural changes, the pre-MVP reset itself (runbook in #1322), the QStash migration (#1010), the DST implementation (#872). Anything here that needs a schema change waits for the reset.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bookingBooking, scheduling, slots, reschedule, cancellationlaunch: post-mvpFirst 90 days after launch — coverage, polish, operational maturitytech-debtRefactors, structure, dependency upgrades, cleanup

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions