You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue is the handoff prompt for the next session on the booking + maintenance productionization (umbrella #1319, plan-of-record in the 2026-09-02 wave-5 plan). It states where things are as of 2026-09-03 ~15:15 IST, what is on dev and believed correct, what is open or defective, and the recommended next moves. Read it top to bottom before touching anything; the "how to resume" section at the end is the operating recipe that worked.
1. Current status
dev is at 5f057bc0 and carries every PR of wave 5; the train is complete. Released to prod in two steps: #1339 on 2026-09-03 12:25 IST (merge commit 09daf428, 16 PRs); the schema was pushed additively to the shared database first (backup ~/Desktop/familiarise-backups/familiarise-pre-schema-push-20260903-1201.dump, phantom-column rows in phantom-columns-source-rows-20260903.sql), and #1374 (prod a69fa88a, 14:53 IST) carrying the three PRs that landed afterwards: #1337, #1336, #1350. No schema change between the two releases.
PR 3: hold expiry by predicate (status-aware), bounded lock budgets, per-attempt renewal, appointment lock + limiter on cancel/reschedule, mint atom, re-mint in place, trial scheduling CAS, org context re-asserted under the lock
PR 14: co-host guard uses the occupancy predicate (live holds block, dead holds free); manualAllocate takes the consultant-wide lock when a weekly cap applies (subscription, class)
Nothing is open on the train. All fw-* worktrees for wave-5 branches are removed; the five that remain belong to unrelated open PRs (#1148, #1213, #1229, #1266, #1268).
Tracking and decisions: #1319 (umbrella, merge log and residuals in its comments), #1332 (architecture/naming tracker, post-MVP), #1320 (slot-merge prod bug, fixed by #1323), #1326 (utcOffsetMinutes capture follow-up), #874 (load gate, never executed).
2. Implemented and believed correct (verified at line level or by green suites)
Lifecycle: seven CAS helpers in lib/booking/transitions.ts are the only status writers; SLOT_COMPLETION_ALLOWED_FROM.UNVERIFIED admits COMPLETED so the maintenance drain converges while automated completion passes fromIn: ["SCHEDULED"]; slot history is written only for rows the CAS moved (updateManyAndReturn).
Sweeps: CAS the request first, release slots second, one transaction per request; the cohort's money predicate is repeated in the CAS where because a Stripe reconcile flips a Payment without moving its request; payment expiry only from PENDING; skips counted separately from cleaned.
Holds: a PENDING direct-checkout hold is free once every payment row is dead (EXPIRED, FAILED, or PENDING past expiresAt), never by the clock alone; buildDeadHoldFilter is the SQL twin; the grid, allocator, /validate and checkout agree.
Locks: REQUEST_PATH_RETRY_CONFIG (six attempts, ~7 s) on request paths; approval lock 45 s renewed per Serializable attempt; checkout renews its slot grant per attempt; withAppointmentLock (75 s) on cancel/reschedule; the pay-link mint locks approval-payment-mint:<kind>:<id> nested under the approval lock; exhausted P2034 → 409.
Availability: ScheduleType is exclusive; coalescing is exact adjacency with a 12-hour fold cap sourced from utils/timeSlotValidation.ts; loadPublishedCoverage is scoped by scheduleType and fails closed on a soft-deleted profile; the four write routes run overlap-check + write + coalesce in one Serializable transaction.
Allocation: assertCollaboratorsAvailable in every mode and both class routes; class create Serializable with 23P01 → 409; allowPartial with placedSessions/requiredSessions/unplacedSessions/partial, idempotent replay carries the counts; assignment must be ACTIVE; engagement returned on slot removal; the approval gate compares covered half-hour atoms.
Row shape: a session is N × 30-minute atoms in one contiguous run; checkout and the webhook fallback creators share buildContiguousSlotAtomsForWindow; event attendees connect to existing session slots (lib/appointments/attendee-seats.ts) with participant rows born HELD and promoted on capture.
Money boundary: deriveCheckoutAmount (multiply before divide, negatives rejected) and quoteBookingRefund (BigInt via the constructor) are the single derivations; the earnings healer is bounded by count not age and pages once per payment per day; org receivables read in one RepeatableRead transaction; checkout re-asserts org status, membership, assignment, dunning and consent under the lock.
Maintenance: 66 jobs guarded; HTTP twins via lib/cron/cleanup-route.ts; reconcile-refunds guard name corrected; irp-uploader in FINANCIAL_JOB_NAMES with fail-closed lock.
Grid: conditional GET; marker = profile/availability/slot/request/payment updatedAt maxima + availability row count + earliest PENDING expiresAt; measured cost recorded in docs/booking/20-availability-grid-cost.md.
3. Issues, gaps and defects still open
Money and correctness (core logic):
Rate-card tiers unreachable at settlement.resolveEffectiveRateCard defines eight tiers, but resolveOrgSplit() passes only orgId, membershipOverrideId, at, so contract- and plan-scoped cards can be created and never selected. Owner decision pending (forward the scope, or refuse scoped cards at the POST route).
Two sweeps still hard-delete never-paid tentative rows (cleanup-tentative-slots, expire-stale-requests), scoped to isTentative with no Payment. Tolerated by rule 2; the last hard deletes in the subsystem.
BookingStatusHistory.appointmentId is NULL on every row: no writer passes meta.appointmentId. The timeline read model matches by polymorphic ids instead. Thread the id at cancel, reschedule respond/withdraw, checkout confirm, webhook confirm and the sweeps.
No durable reservation before the gateway call in the approval mint (Razorpay Orders has no idempotency header; Payment.paymentIntent is required and unique). Blast radius bounded by @@unique([userId, appointmentId]); needs a schema change, so post-reset.
Four money HTTP twins pin a 200 regardless of result (process-payouts, release-earnings, sweep-abandoned-overage-charges, sync-payment-earnings); the jobs/** entrypoints carry the failure signal. Contract change, own PR.
payout-service.tsacquireLock returns null for both contention and a Redis outage. File an issue against the payout service.
ChatSidebar fetch race: an org switch mid-fetch can clear loading state for the wrong scope (guard the catch/finally with the fetch key).
Operational blockers (not code):
dev's schema is ahead of the shared Supabase database (one project serves dev AND prod). Nothing has been pushed by design; the push is step one of the pre-MVP reset (docs/prisma/pre-mvp-reset-runbook.md). Until then, any deployment of dev and any local dev server against the shared database fails on the first query that selects a new column.
Coalesce scriptsscripts/db/coalesce-weekly-availability.ts and coalesce-custom-availability.ts are dry-run by default; the --apply runs (44 custom pairs across 29 consultants; weekly similar) are the owner's, with a backup first.
Load gate never executed. The harness targets a Netlify deploy preview from a GitHub-hosted runner; constraints: session-cookie auth only (pre-minted cookies, AUTH_ORIGIN = prod URL), isMockPayment inert outside development mode, per-IP limiters, shared production database.
Deferred by design (do not re-open without new evidence): the ConsultantBusyInterval read model (waits on #874 numbers), the god-module split and aggregate/enum/naming cleanup (#1332, post-reset), selection-stage holds (#1196, waits on a measured 409 rate), DST (#872, post-MVP), QStash (#1010).
Process hazards learned this wave:
Every PR appended to the same changelog file; ten parallel branches produced a changelog conflict on nearly every restack. Next train: one changelog fragment file per PR, assembled at release.
The main checkout's node_modules vanished mid-morning on 2026-09-03 (cause not found in any agent transcript); every fw-* worktree symlinks to it. Repaired with npm ci + prisma generate. Check ls node_modules/.bin/tsc before trusting a "tsc failed" from a worktree.
prompts/ is listed in .gitignore, so Prettier silently skips it locally and in CI; the files are tracked. Format with --ignore-path /dev/null or fix the ignore line.
CodeRabbit is on a rolling 7-day Fair-Use allowance: roughly one included review per 20–60 minutes, every push consumes an attempt, and "Review triggered" can be silently dropped (re-request with @coderabbitai full review).
dev requires branches to be up to date: after every merge the next PR is BEHIND; gh pr update-branch handles the clean case, a merge commit in the worktree handles conflicts (squash target, history irrelevant).
Rate-card decision TAKEN 2026-09-03: forward the scope behind a flag (wave-6 PR). The original recommendation for reference: forward programId / contractId / planId from the booking into resolveOrgSplit behind a feature flag defaulting to the current behaviour, and add a settlement parity test that a scoped card, once enabled, is the one chosen. Refusing scoped cards at the POST route is the cheaper interim if the decision is deferred.
Thread appointmentId into appendHistory at the writers (item 4): a mechanical one-PR change, no schema.
Convert the two remaining hard-deleting sweeps to soft-cancel (item 3), then add slotOfAppointment.delete* to the global forbidden list in appointment-delete-forbidden.test.ts.
Worktrees live at ~/Desktop/fw-* with node_modules and .env symlinked from the main checkout; they track origin/dev, so push explicitly (git push origin <branch>). Regenerate the Prisma client right before any tsc (the shared client is clobbered by whichever worktree generated last); clear tsconfig.tsbuildinfo (warm runs hide errors); use NODE_OPTIONS=--max-old-space-size=12288.
Review round-robin: request with @coderabbitai full review one PR at a time; triage with .claude/skills/pr-comment-triage/SKILL.md (autonomous mode approved by the owner: fix, verify, push, resolve threads without replying, post the table; pause only for money/design decisions).
Merge when clean = Lint + TypeScript green, zero unresolved threads, not BEHIND/DIRTY: gh pr merge N --squash --delete-branch --subject "<title> (#N)" --body "<PR body>", then remove the worktree.
Changelog rule: one ## Changelog: 2026-09-02 — wave 5 heading right above ## Changelog: 2026-08-14, sections in train order; /tmp/restack-changelog-final.py (recreate if lost: dev's file + missing trailing sections) resolves the recurring conflict.
Never prisma db push against the shared project; never run coalesce-* --apply; never start a dev server while coding; eslint warnings are blocking; one compact pinning test per PR at most (owner's direction: core logic over test breadth).
Memory for the assistant lives in the project memory directory (project_2026_09_02_booking_wave5.md, verify_* notes); the plan file is ~/.claude/plans/users-kaustavghosh-desktop-familiarise-stateless-crescent.md.
Purpose
This issue is the handoff prompt for the next session on the booking + maintenance productionization (umbrella #1319, plan-of-record in the 2026-09-02 wave-5 plan). It states where things are as of 2026-09-03 ~15:15 IST, what is on
devand believed correct, what is open or defective, and the recommended next moves. Read it top to bottom before touching anything; the "how to resume" section at the end is the operating recipe that worked.1. Current status
devis at5f057bc0and carries every PR of wave 5; the train is complete. Released to prod in two steps: #1339 on 2026-09-03 12:25 IST (merge commit09daf428, 16 PRs); the schema was pushed additively to the shared database first (backup~/Desktop/familiarise-backups/familiarise-pre-schema-push-20260903-1201.dump, phantom-column rows inphantom-columns-source-rows-20260903.sql), and #1374 (proda69fa88a, 14:53 IST) carrying the three PRs that landed afterwards: #1337, #1336, #1350. No schema change between the two releases.AppointmentParticipant,BookingStatusHistory(written from day one), schema hygiene, sidecar parser +db:assert-sidecarschained, reset runbookcleanupRoute()factory for 36 HTTP twins, DEGRADED write-block reconciled, job-name mismatch fixedfixture_overridesno-op fixed (job-levelenv:beats$GITHUB_ENV)manualAllocatetakes the consultant-wide lock when a weekly cap applies (subscription, class)Nothing is open on the train. All
fw-*worktrees for wave-5 branches are removed; the five that remain belong to unrelated open PRs (#1148, #1213, #1229, #1266, #1268).Tracking and decisions: #1319 (umbrella, merge log and residuals in its comments), #1332 (architecture/naming tracker, post-MVP), #1320 (slot-merge prod bug, fixed by #1323), #1326 (
utcOffsetMinutescapture follow-up), #874 (load gate, never executed).2. Implemented and believed correct (verified at line level or by green suites)
lib/booking/transitions.tsare the only status writers;SLOT_COMPLETION_ALLOWED_FROM.UNVERIFIEDadmits COMPLETED so the maintenance drain converges while automated completion passesfromIn: ["SCHEDULED"]; slot history is written only for rows the CAS moved (updateManyAndReturn).wherebecause a Stripe reconcile flips a Payment without moving its request; payment expiry only from PENDING; skips counted separately from cleaned.expiresAt), never by the clock alone;buildDeadHoldFilteris the SQL twin; the grid, allocator,/validateand checkout agree.REQUEST_PATH_RETRY_CONFIG(six attempts, ~7 s) on request paths; approval lock 45 s renewed per Serializable attempt; checkout renews its slot grant per attempt;withAppointmentLock(75 s) on cancel/reschedule; the pay-link mint locksapproval-payment-mint:<kind>:<id>nested under the approval lock; exhausted P2034 → 409.ScheduleTypeis exclusive; coalescing is exact adjacency with a 12-hour fold cap sourced fromutils/timeSlotValidation.ts;loadPublishedCoverageis scoped byscheduleTypeand fails closed on a soft-deleted profile; the four write routes run overlap-check + write + coalesce in one Serializable transaction.assertCollaboratorsAvailablein every mode and both class routes; class create Serializable with 23P01 → 409;allowPartialwithplacedSessions/requiredSessions/unplacedSessions/partial, idempotent replay carries the counts; assignment must be ACTIVE; engagement returned on slot removal; the approval gate compares covered half-hour atoms.buildContiguousSlotAtomsForWindow; event attendees connect to existing session slots (lib/appointments/attendee-seats.ts) with participant rows born HELD and promoted on capture.deriveCheckoutAmount(multiply before divide, negatives rejected) andquoteBookingRefund(BigInt via the constructor) are the single derivations; the earnings healer is bounded by count not age and pages once per payment per day; org receivables read in one RepeatableRead transaction; checkout re-asserts org status, membership, assignment, dunning and consent under the lock.lib/cron/cleanup-route.ts;reconcile-refundsguard name corrected;irp-uploaderinFINANCIAL_JOB_NAMESwith fail-closed lock.updatedAtmaxima + availability row count + earliest PENDINGexpiresAt; measured cost recorded indocs/booking/20-availability-grid-cost.md.3. Issues, gaps and defects still open
Money and correctness (core logic):
resolveEffectiveRateCarddefines eight tiers, butresolveOrgSplit()passes onlyorgId,membershipOverrideId,at, so contract- and plan-scoped cards can be created and never selected. Owner decision pending (forward the scope, or refuse scoped cards at the POST route).autoAllocatehas no "place only the missing N, keep the rest" path, and the naive path would delete confirmed sessions. Marked// #1206 follow-upinscripts/appointments/reconcile-slot-availability.ts.cleanup-tentative-slots,expire-stale-requests), scoped toisTentativewith no Payment. Tolerated by rule 2; the last hard deletes in the subsystem.BookingStatusHistory.appointmentIdis NULL on every row: no writer passesmeta.appointmentId. The timeline read model matches by polymorphic ids instead. Thread the id at cancel, reschedule respond/withdraw, checkout confirm, webhook confirm and the sweeps.Payment.paymentIntentis required and unique). Blast radius bounded by@@unique([userId, appointmentId]); needs a schema change, so post-reset.process-payouts,release-earnings,sweep-abandoned-overage-charges,sync-payment-earnings); thejobs/**entrypoints carry the failure signal. Contract change, own PR.payout-service.tsacquireLockreturns null for both contention and a Redis outage. File an issue against the payout service.ChatSidebarfetch race: an org switch mid-fetch can clear loading state for the wrong scope (guard the catch/finally with the fetch key).Operational blockers (not code):
dev's schema is ahead of the shared Supabase database (one project serves dev AND prod). Nothing has been pushed by design; the push is step one of the pre-MVP reset (docs/prisma/pre-mvp-reset-runbook.md). Until then, any deployment ofdevand any local dev server against the shared database fails on the first query that selects a new column.appointment-partially-scheduledmust exist in every environment before fix(booking): co-host guard in every allocation mode, Serializable class create, and partial allocation with explicit confirm #1329's partial notice fires; the trigger is fire-and-forget and silent when missing.scripts/db/coalesce-weekly-availability.tsandcoalesce-custom-availability.tsare dry-run by default; the--applyruns (44 custom pairs across 29 consultants; weekly similar) are the owner's, with a backup first.AUTH_ORIGIN= prod URL),isMockPaymentinert outside development mode, per-IP limiters, shared production database.Deferred by design (do not re-open without new evidence): the
ConsultantBusyIntervalread model (waits on #874 numbers), the god-module split and aggregate/enum/naming cleanup (#1332, post-reset), selection-stage holds (#1196, waits on a measured 409 rate), DST (#872, post-MVP), QStash (#1010).Process hazards learned this wave:
node_modulesvanished mid-morning on 2026-09-03 (cause not found in any agent transcript); everyfw-*worktree symlinks to it. Repaired withnpm ci+prisma generate. Checkls node_modules/.bin/tscbefore trusting a "tsc failed" from a worktree.prompts/is listed in.gitignore, so Prettier silently skips it locally and in CI; the files are tracked. Format with--ignore-path /dev/nullor fix the ignore line.@coderabbitai full review).devrequires branches to be up to date: after every merge the next PR is BEHIND;gh pr update-branchhandles the clean case, a merge commit in the worktree handles conflicts (squash target, history irrelevant).4. Recommended next moves, in order
LOAD_GATE_*secret and variable provisioned, including the newLOAD_GATE_CONSULTANT_TOKENfor the reschedule-respond leg; expect the first run to surface fixture problems before system problems, because the gate now fails on any unexpected 4xx.programId/contractId/planIdfrom the booking intoresolveOrgSplitbehind a feature flag defaulting to the current behaviour, and add a settlement parity test that a scoped card, once enabled, is the one chosen. Refusing scoped cards at the POST route is the cheaper interim if the decision is deferred.appointmentIdintoappendHistoryat the writers (item 4): a mechanical one-PR change, no schema.SlotAllocationService(item 2): a new entry that takes the existing confirmed sessions as fixed, searches only for the missing count, and never callsdeleteExistingAppointments; reuse the partial-allocation result shape; trigger it fromreconcile-slot-availabilityon availability change.slotOfAppointment.delete*to the global forbidden list inappointment-delete-forbidden.test.ts.--applyruns are DEFERRED to the pre-MVP data reset (a dry run folds 233 weekly + 84 custom rows, but checkout and slot generation already read the union, so bookability does not depend on them). Still the owner's: the Novu slugappointment-partially-scheduled, the Resend key (P0: production email delivery dead since 2026-06-18 — invalid RESEND_API_KEY hard-locks signup and password reset #1298), the post-release browser checks, and later the pre-MVP DATA reset per the runbook (which now says to capture to a file, becausedb pushdrops unknown tables). Only after test(load): the #874 load gate — k6 harness for the five booking write paths, chaos 6/14c/17, dispatch-only workflow #1336 merges: dispatch the load gate against a preview and record numbers in Capacity estimation & load-test go/no-go: concurrent→DAU→MAU model, binding ceilings, vendor-tier blow-ups #874.SlotAllocationService(4,357 LOC) andcheckout.ts(3,808 LOC) #1375 (the god-module split plan, filed 2026-09-03: SlotAllocationService 4,357 LOC and checkout.ts 3,808 LOC, extraction order with a step A0 that first converts two source-text test assertions to behavioural ones), the mint reservation (item 5), the busy-interval read model if Capacity estimation & load-test go/no-go: concurrent→DAU→MAU model, binding ceilings, vendor-tier blow-ups #874 says the query is the bottleneck.5. How to resume (operating recipe)
~/Desktop/fw-*withnode_modulesand.envsymlinked from the main checkout; they trackorigin/dev, so push explicitly (git push origin <branch>). Regenerate the Prisma client right before anytsc(the shared client is clobbered by whichever worktree generated last); cleartsconfig.tsbuildinfo(warm runs hide errors); useNODE_OPTIONS=--max-old-space-size=12288.@coderabbitai full reviewone PR at a time; triage with.claude/skills/pr-comment-triage/SKILL.md(autonomous mode approved by the owner: fix, verify, push, resolve threads without replying, post the table; pause only for money/design decisions).gh pr merge N --squash --delete-branch --subject "<title> (#N)" --body "<PR body>", then remove the worktree.## Changelog: 2026-09-02 — wave 5heading right above## Changelog: 2026-08-14, sections in train order;/tmp/restack-changelog-final.py(recreate if lost: dev's file + missing trailing sections) resolves the recurring conflict.prisma db pushagainst the shared project; never runcoalesce-* --apply; never start a dev server while coding; eslint warnings are blocking; one compact pinning test per PR at most (owner's direction: core logic over test breadth).project_2026_09_02_booking_wave5.md,verify_*notes); the plan file is~/.claude/plans/users-kaustavghosh-desktop-familiarise-stateless-crescent.md.