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 |
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
lib/andutils/both hold domain logic (lib/booking/*,lib/appointments/*,utils/slotAllocation/*,utils/appointmentlock.ts,utils/timeSlotsProcessing.ts,utils/schedule/*)lib/booking/{availability,allocation,checkout,lifecycle,locks,occupancy,participants,transitions};utils/keeps only pure helpers with no Prisma importscripts/<area>/x.ts+jobs/<area>/x.ts+app/api/cleanup/x/route.tsfor every croncleanupRoute()factoryjobs/<area>/x.tsis 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 toutils/slotAllocation/*lib/booking/availability(server) +features/scheduling/{hooks,components}(client) ; names match the server conceptlib/data/*reads next tolib/api/scope/*and hand-rolledorganizationIdpins inlib/data/needs-you.ts,staff-appointments.ts,consultant-dashboard.tslib/datatakes aScopeargument; noorganizationIdliteral outsidelib/api/scopeactions/*(server actions) duplicatingapp/api/*handlers for the same mutation (stream, maintenance)app/api, actions call it (decision 2026-07-17 stands: Route Handlers)__tests__/{booking-algorithm,booking,payments,maintenance,collaborators,enterprise}+tests/typescript/race-conditions__tests__/booking/**,__tests__/payments/**; the race suite under__tests__/booking/race/**prisma/sql/{check-constraints,ledger-triggers}.sql+prisma/sql/one-off/*booking-exclusion.sql,ledger-invariants.sql) and index them from oneprisma/sql/README.md(the reset runbook already points there)2. API surface
/api/bookings/{consultations,subscriptions,webinars,classes}/...,/api/appointments/[id]/{cancel,reschedule,...},/api/slots/**,/api/trials/**,/api/checkout,/api/participants/{class,webinar}/[id]/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 underrequestswithkind=TRIALcrud-with-planin the path (/api/bookings/webinars/crud-with-plan)/api/booking/events(create with plan is the only create)availability-with-allocation/[consultantId]/api/booking/availability/[consultantId]/grid[id]/allocate/route.ts(54% duplicated per SonarCloud on #1329)POST /api/booking/requests/[id]/allocatewithmodein the body; the service already switches on event typerequest-for-approval(verb-noun),reschedule/respond,reschedule/withdraw,cancel/previewrequests/[id]/approve,appointments/[id]/reschedule/{respond,withdraw},appointments/[id]/cancel/preview(keep),requestsPOST replacesrequest-for-approval/api/cleanup/*for cron HTTP twins/api/jobs/[job]from the job registry (see §1)3. Model and enum naming (schema, post-reset only)
SlotOfAvailabilityWeekly,SlotOfAvailabilityCustom,SlotOfAppointmentAvailabilityWindow(weekly rule),AvailabilityException(dated custom),SessionAtom(booked 30-minute row); "slot" reserved for the UI's bookable unitAppointmentwith four nullable FKs (consultationId,subscriptionId,webinarId,classId) +trialSessionBooking(kind + onerequestId) withBookingRequest/BookingEventtables, participation viaAppointmentParticipant(#1322)AppointmentStatus,EventStatus,TrialSessionStatus) andSlotCompletionStatusBookingStatusHistorystores them as strings for this reasonBookingStatusafter the aggregate merge; until then keep the CAS helpers as the only writersisTentativeoverloaded: unpaid hold AND being rescheduledholdState(HELD / CONFIRMED) on the atom +RescheduleRequestowning the "moving" statebookingSource(DIRECT_CHECKOUT/REQUEST_SUBMITTED)entryModeConsultantProfile.scheduleType(WEEKLY xor CUSTOM)AvailabilityModeenum named for what it is4. Module and function naming
lib/payments/operations/checkout.ts(3,666 LOC),utils/slotAllocation/SlotAllocationService.ts(3,901 LOC)checkout/{validate,locks,commit,handlers/<type>}.ts,allocation/{occupancy-read,candidate-search,commit}.ts; the 46-scenario race suite is the oraclevalidateSlotAvailabilityexists twice with different contracts (checkout.ts,app/api/trials/[trialId]/route.ts)assertWithinPublishedWindow+assertNoOccupancyConflict, both inlib/booking/occupancymergeConsecutiveSlots(generator),mergeAdjacentWeeklyRows/mergeAdjacentCustomRows(storage),app/explore/experts/[consultantId]/utils/mergeSlots.ts(display, tolerant)coalesceAvailabilityRows(storage, exact adjacency),mergeBookableSlots(generator),groupForDisplay(UI, tolerant) ; the filemergeAdjacentWeeklyRows.tsnow also holds custom logic and needs renaming tocoalesceAvailabilityRows.tsdeleteExistingAppointments(allocator)releaseUnpaidTentativeRowswith the Payment guard in the name's contractreconnectEnrolledUsers,connectAttendeeToEventSlots(#1331),recordParticipants(#1322)seatAttendee/unseatAttendeeon the participants module, the join-table connect as an implementation detailisOccupiedByLiveAppointment+buildOccupiedAppointmentFilter+buildDeadHoldFilter+buildConsultantOccupancyWhereoccupancymodule exportingisLive(row)andliveWhere()with the JS/SQL parity test beside themslot-booking:,consultee-booking:,event-checkout:,auto-allocate:,consultation-approval:,subscription-approval:,approval-payment-mint:,appointment:lib/booking/locks.tsexporting a typedLockAtomunion with the order encoded as a rank, so a mis-ordered acquisition fails at compile timetransitionConsultationRequest,transitionSubscriptionRequest,transitionWebinarEvent,transitionClassEvent,transitionRescheduleRequest,transitionSlotCompletion,transitionTrialSessiontransition(entity, id, to, opts)over a per-entity allowed-from table, after the enum mergehandleConsultationCheckout/handleSubscriptionCheckout/ ... vscreateConsultation/createSubscription(webhook fallback)createBookingRows(kind, input)used by both paths; the webhook fallback becomes a thin caller5. Docs, prompts, skills
docs/booking/**is banded but15-checklist.md,06-dependency-graphs.mdandREADME.mdstill 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 ofinitialAllocation,Idempotency-Key,reschedule/respond,cancel/preview,orgScope, partial allocation. A regeneration from code is its own PR..claude/skills/booking-doctrinecarries 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.