Skip to content

[HANDOFF] Booking productionization wave 5 → next session: status, what is correct, open defects, recommended next moves (2026-09-03) #1338

Description

@teetangh

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 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 Scope State
#1330 Sonar: exclude test suites from copy-paste detection merged
#1321 PR 1: CAS everywhere, no hard deletes, EXPIRED as the one outcome, sweeps CAS-first with the money predicate merged (3 review rounds)
#1322 PR 2: AppointmentParticipant, BookingStatusHistory (written from day one), schema hygiene, sidecar parser + db:assert-sidecars chained, reset runbook merged
#1323 PR 3b: availability rows coalesce on save (weekly + custom, 12-hour cap), checkout validates the union, generator merges across rows, trials validate the window, atomic write+coalesce merged (2 rounds)
#1324 PR 5: every job guarded, cleanupRoute() factory for 36 HTTP twins, DEGRADED write-block reconciled, job-name mismatch fixed merged
#1325 PR 6: refund rail in the quote, org-scope fall-throughs, trial P2002 → 409, single-writer attendee removal, payer-admin view merged
#1328 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 merged (2 rounds)
#1329 PR 4: co-host guard in every mode, Serializable class create, #1206 partial allocation with explicit confirm and correct replay counts merged
#1327 PR 7: one price derivation, one refund quote (BigInt paise), earnings healer without age window, org receivables view merged
#1331 PR 12: every session writer produces the same 30-minute atoms; webhook fallbacks aligned to checkout; participants born HELD for event attendees merged
#1333 PR 8: booking audit trail read model + staff route + operator timeline merged
#1334 PR 9: availability grid conditional GET with the poll cost measured merged
#1335 PR 11: docs refresh, dead-code sweep, TODO retargets (+ a live fix: org MAINTAINER profile saves 403'd) merged, in the release
#1337 PR 13: five booking skills + 12 prompts, re-verified against merged dev merged, released via #1374
#1336 PR 10: k6 load-gate harness + dispatch-only workflow + runbook (built, NOT run); 18-thread review round fixed the gate so it cannot pass while measuring nothing; fixture_overrides no-op fixed (job-level env: beats $GITHUB_ENV) merged, released via #1374
#1350 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) merged, released via #1374

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):

  1. 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).
  2. Product decision: should consultants be able to PARTIALLY allocate recurring events when availability falls short? #1206 top-up mode missing. A partially allocated plan is not re-attempted; autoAllocate has no "place only the missing N, keep the rest" path, and the naive path would delete confirmed sessions. Marked // #1206 follow-up in scripts/appointments/reconcile-slot-availability.ts.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. payout-service.ts acquireLock returns null for both contention and a Redis outage. File an issue against the payout service.
  8. 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):

  1. 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.
  2. Novu workflow appointment-partially-scheduled must 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.
  3. Coalesce scripts scripts/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.
  4. Prod Resend key invalid since June (P0: production email delivery dead since 2026-06-18 — invalid RESEND_API_KEY hard-locks signup and password reset #1298): signup and password reset are dead on production; checkout is unreachable at the auth wall.
  5. 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).

4. Recommended next moves, in order

  1. Done: test(load): the #874 load gate — k6 harness for the five booking write paths, chaos 6/14c/17, dispatch-only workflow #1336 and fix(booking): the co-host guard sees live holds, and weekly-capped manual allocation locks consultant-wide #1350 merged and released via release: dev → prod 2026-09-03 tail (wave-5 #1350, #1336, #1337) #1374. The first real load-gate dispatch needs every LOAD_GATE_* secret and variable provisioned, including the new LOAD_GATE_CONSULTANT_TOKEN for the reschedule-respond leg; expect the first run to surface fixture problems before system problems, because the gate now fails on any unexpected 4xx.
  2. 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.
  3. Thread appointmentId into appendHistory at the writers (item 4): a mechanical one-PR change, no schema.
  4. Implement the Product decision: should consultants be able to PARTIALLY allocate recurring events when availability falls short? #1206 top-up mode in SlotAllocationService (item 2): a new entry that takes the existing confirmed sessions as fixed, searches only for the missing count, and never calls deleteExistingAppointments; reuse the partial-allocation result shape; trigger it from reconcile-slot-availability on availability change.
  5. 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.
  6. Schema push and release are DONE (release: dev → prod 2026-09-03 (booking + maintenance wave 5) #1339, release: dev → prod 2026-09-03 tail (wave-5 #1350, #1336, #1337) #1374). Owner decision 2026-09-03: the coalesce --apply runs 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 slug appointment-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, because db push drops 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.
  7. Post-reset epics: [TRACKER] Booking + payments code architecture: folder layout, API surface, model and function naming #1332 naming/architecture, [EPIC] Split the two booking god modules by extraction: SlotAllocationService (4,357 LOC) and checkout.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)

  • 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bookingBooking, scheduling, slots, reschedule, cancellationdocumentationImprovements or additions to documentationlaunch: pre-mvpGates launch — money, data, or a failure we would not detect

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions