Skip to content

[UMBRELLA] Booking + maintenance productionization — master audit and 10-PR train #1169

Description

@teetangh

This is the master tracker for the booking + maintenance productionization train. It consolidates a five-lens audit (Prisma schema + allocation engine, reschedule/cancel lifecycle + cron fleet + heatmap, B2B/org booking + guests/collaborators + docs, GitHub-issues archaeology with closed-fix regression checks, and a full frontend/UX journey walk) run 2026-08-13/14 against dev. Every finding below was verified against source with file:line evidence; the load-bearing P0s were re-verified by hand before this issue was filed.

1. Context and goal

The booking subsystem (consultation / subscription / webinar / class + trials; auto / manual / requested allocation — the mode formerly named preAllocate; request-for-approval and direct checkout; B2C and org-funded payment rails; the reschedule/cancel lifecycle) and the maintenance subsystem (maintenance mode OFF/DEGRADED/OFFLINE plus the 61-workflow scheduled fleet) are MVP-grade. The goal of this train is 100% production readiness for both, delivered as ten PRs in four serial waves cut from dev, with one coordinated db push at the end (gated on #1045) and the docs/prompts corpus brought back in sync.

Three prior hardening waves left the money paths in good shape, and the regression check confirmed every closed booking fix still holds (#1003, #1004, #1005, #1012, #1071 via PR #1091; #829; #830; #835 via #854; #440; #476; #503 items 1–5). This train is new work, not re-work.

2. Findings register

P0 — money loss / integrity

ID Finding Evidence
P0-1 Maintenance freeze hard-deletes appointments whose only Payment is PENDING (the include is filtered to SUCCEEDED, the guard checks payment.length === 0), cascade-destroying the Payment row; the capture webhook then lands on nothing actions/maintenance/freeze-appointments.ts:148,371
P0-2 Freeze refunds payment.amount gross via raw createRefund, bypassing the refundable-balance clamp; org_* and free_ intents throw UNKNOWN_GATEWAY, leaving no wallet/ledger/seat reversal freeze-appointments.ts:390-394
P0-3 Freeze skips every CAS (it can resurrect COMPLETED→CANCELLED), hard-deletes slots, and never closes open RescheduleRequests; refundWholeEventPayments is explicitly non-idempotent, so freeze + manual cancel double-refunds every attendee freeze-appointments.ts:361-372, lib/payments/operations/event-refunds.ts:40-43
CORE-1 Trial slots are created without consultantProfileId, so they fall outside the slot_no_confirmed_overlap exclusion constraint — a trial and a consultation can confirm on the same consultant-minute (#1093 §1) app/api/trials/[trialId]/route.ts:378-398
CORE-2 Four disjoint Redis lock namespaces guard the same physical resource, and keys are start-instant-granular rather than interval-granular; the trial path never serializes against checkout or allocation utils/appointmentlock.ts:391,540,588,639
CORE-3 createAppointmentForConsultation fabricates a confirmed slot at now+1h with no availability/conflict/lock check, no consultantProfileId, one long slot instead of 30-minute atoms — using the global Prisma client from inside a Serializable transaction, so the row survives the transaction's rollback app/api/bookings/consultations/[consultationId]/route.ts:913-970
UX-1..5 The paid-trial modal never shows the price; the consultant's "propose times" toast claims the consultee has been asked to accept when no consultee surface renders proposals at all; remove-attendee is one-click, unconfirmed, and discards the refund summary; the cancel dialog asks for consent without a refund preview; abandoning the Razorpay modal dead-ends (no ondismiss) TrialBookingModal.tsx:189-236, useConsultantEventActions.ts:106-111, participants page :86-91,182-191, CancelConfirmationDialog.tsx:82-95, RazorpayCheckout.tsx:152-214

Two further money-integrity items from the enterprise audit's private advisory (the withheld sections referenced by #1132) are absorbed by wave 1; their details are deliberately not restated here.

P1 (the full list rides in the PR descriptions)

The reschedule response loop was never built — ACCEPTED/DECLINED/COUNTERED are dead or side-effect-only enum members, the one DECLINED writer bypasses the CAS helper, and the withdraw endpoint has zero UI callers (#1163). Org admins can neither cancel nor reschedule org-funded bookings, and the approval flow drops org sponsorship entirely (#1166). Subscription cancellation after any delivered session refunds ₹0 into a MANUAL_REVIEW queue nobody drains (#1006, now unmasked). expire-unpaid-trials races the capture webhook and skips the tombstone — and the live cron entrypoints are jobs/**, with scripts/ twins that are stale diverged copies to delete. Roughly 21 scheduled jobs run without withCronLock, several of them FINANCIAL_JOB_NAMES members that the doctrine says must fail closed. The 1-hour reminder window is 30 minutes wide on a fleet measured to fire ~every 2.75 hours (#866). The checkout Serializable transaction is not retried on P2034, so hot-webinar contention surfaces as "Something Went Wrong". The approval path holds a gateway round-trip inside its Serializable transaction, re-confirms RESCHEDULED slots, and mints on a divergent gateway (#1165). The free_ referral-credit refund rail dead-ends (#1161). Abandoned-checkout cleanup cascades away BookingUtilization while engagementsUsed stays incremented, shrinking org seat caps (#1132 §08). The org/personal scoping seam has nine violations (#1166). The constraint sidecar is applied by no CI or deploy step, so a future db push would silently drop the double-booking backstop (#1092). Razorpay success skips the checkout-success reconciliation poll that the Stripe path gets. The subscription reschedule grid is unclamped (no period window, no weekly caps), and the trial reschedule page renders fully then 403s at submit.

P2

Unbounded janitor scans (cleanup-tentative-slots has no take; the freeze scan runs a 5-level include in one 60-second transaction) against the good MAX_EVENTS_PER_RUN = 5000 pattern; webinar/class tentative slots are excluded from reconcile and never self-heal; the cancel audit log collapses platform actors to consultant|consultee; cancelled/refunded attendees keep Stream channel access; the grid footer names only the browser timezone while caps bucket in schedulingTimezone (#1076); org surfaces hardcode Asia/Kolkata while personal surfaces render viewer-local; assorted toast/empty-state/pagination polish.

Verified good — do not "fix"

Interactive cancel paths are CAS-guarded with frozen refund snapshots (100/50/0 tiers; consultant-initiated = 100%). The allocation engine's core is sound: idempotent replay via allocationIdempotencyKey checked both pre-lock and in-lock, consistent consultant→consultee lock ordering, stale-tab CAS via expectedTentativeSlotCount, in-transaction conflict revalidation plus pg_advisory_xact_lock, and 23P01/23505 mapped to 409s. Auto-confirm re-validates proposals through the full allocator under wideLock — nothing is trusted from the client, and the ~4,600-line client engine is already demoted to display + test oracle. The org scope helpers fail closed with no cross-tenant IDOR. The payout service holds its own fail-closed distributed lock (the comment in system-jobs/run is accurate). The outbox drains are locked and bounded, org invitations are complete end-to-end, and Waitlist is the newsletter by design — event capacity has no waitlist, and a full event is simply sold out.

3. The PR train (10 PRs, 4 serial waves, nothing stacked)

# Branch Thesis Carries
1 fix/booking-slot-integrity Trials enter the same lock/constraint/validation guarantees as everything else; lock keys become interval-granular CORE-1, CORE-2, trial validation, delete-branch guard, breaker gating, capacity include-trap, deterministic org-payment pick — Part of #1093, #676
2 fix/approval-path-correctness Approval can no longer invent appointments, drop org context, or hold a gateway call inside a transaction CORE-3, ORG-9, closes #1165, RESCHEDULED exclusion, withSerializableRetry on checkout, tentative-hold visibility in the conflict check
3 fix/refund-doctrine-and-freeze One refund front door all rails actually enter; freeze obeys the doctrine P0-1/2/3, closes #1161 and #1162, wallet-receivable decision, #676 B1 reconcile-by-UUID, event-refund idempotency, webhook idempotency-key precedence, maintenance-key TTL (#697 INF-1)
4 fix/reschedule-cancel-lifecycle The proposal loop closes; lifecycle authorization, proration, and audit stop lying Closes #1163, #448 (rescoped), #1006, #1085 in-repo residue; org-admin cancel/reschedule; program-window awareness; Stream channel cleanup
5 fix/org-scoping-seam Org and personal contexts stop bleeding into each other Closes #1166 (ORG-1..8; ORG-9 rides PR 2)
6 fix/cron-fleet-hygiene Every job locked, bounded, doctrine-compliant, and able to notice its own death Locks on ~21 jobs, bounded scans, expire-unpaid-trials fix, reminder windows, #1132 §08, heartbeat dead-man workflow, minimal SystemJobExecution writes, stale scripts/ twin deletion — Part of #866, #697, #1132
7 fix/booking-ux-money-truth Every destructive or paid action shows its price first; no payment flow dead-ends UX-1..5, closes #1167, checkout-success parity, capacity re-check, subject clamps, cache invalidation
8 perf/scheduling-freshness-and-diet The grid stops being permanently stale; the dead client engine is deleted Closes #1164 and #1076 (mitigation half; full fix is #1168), real-service parity test, PENDING-list reshape — Part of #997
9 fix/db-booking-constraints Every documented-but-absent DB guarantee becomes real, mapped to one push Closes #1093 (§2 decision recorded: receivable); §3 server-minted idempotency keys, §4 partial uniques, §5 exclusion (staged), indexes, sidecar-drift CI guard — Part of #1092
10 docs/booking-maintenance-refresh The residual docs debt no code PR owns Closes #1013; docs/collaborators de-drift, prompts/ index + seed-cohort reconciliation + refreshed booking test prompts

Post-train: fix/tz-grid-scheduling-timezone delivers #1168. Merge order is strictly serial 1→10; the one coordinated db push (then npm run db:sidecars) happens after PR 9 merges and #1045 has merged.

4. Decisions recorded (2026-08-13)

Heatmap freshness = client polling (~60s + focus refetch) with s-maxage on the availability endpoints; SSE rejected on Netlify's function timeout; Supabase Realtime assessed viable-but-not-now (Broadcast-mode invalidation pings are the upgrade path once a live "seats left" surface justifies quotas, after #481). No server-action conversion — the Route Handler + useMutation convention stands. Wallet floor stays >= 0; the top-up-refund shortfall books an org receivable. #1076 ships the explain-the-bucket mitigation now, #1168 later. Approval links unify on RAZORPAY. Org context is carried end-to-end through approvals. The train-end push is additive; NOT-NULL flips and clean-data constraints stay staged for the pre-MVP reset.

5. Existing-issue dispositions

#676 is superseded by this tracker (A1–A4 shipped via #847 and live-verified; B1's reconcile-by-UUID half closes via PR 3; docs drift folds into #1013/PR 10; the AE/CN/CA/RV/A11 addendum items get re-triaged after the train). #837's checkboxes are decorative — its comments are the record — and its one remaining item, the staging chaos go/no-go, merges with #874 as this train's exit gate. #309 closes as a duplicate of #997. #448 is rescoped to its audit-trail and staff-blind-spot halves and closes via PR 4. #1092 keeps the observability build-out; PR 9 carries only the drift-guard slice. #866 keeps the QStash decision with #1010 as the execution issue; PR 6 carries the heartbeat and locks. #697 keeps INF-3/4/5; INF-1 and INF-2-minimal ride PRs 3 and 6. #872 owns the DST root cause and builds on #1168. #1132 keeps its auth/SSRF/GST items; PR 6 carries §08 and wave 1 absorbs its two withheld booking/money items. #867's booking-subset items are absorbed here; the dashboard-wide remainder stays. #472, #1058, #1089, and #1128 are linked as adjacent, not carried.

6. Verification gate

Per-PR: tsc, eslint (warnings block), targeted jest, and a background dev server exercised via mock-data API scripts. New suites: freeze-doctrine, trial-slot-integrity, accept/decline transitions, free_-rail + event-refund idempotency, org attendee-arm + list/detail parity, cron-lock registry drift, checkout price parity, and a real-service grid parity test replacing the self-mocking one. Train-wide: chaos scenarios 1/2/4 plus a new trial-vs-checkout same-minute race script after wave 1; npm run test:chaos:api plus scenario 5 after wave 2; the coordinated push + Supabase advisors after PR 9; and the full 8-scenario staging chaos run (the #837/#874 go/no-go) — extended with freeze-during-pending-capture and freeze-then-manual-cancel scenarios — as the train exit gate.

7. Deferral register (explicitly out of this train)

QStash execution (#1010); the full #997 engine port; DST (#872) and #1168 (post-train); SystemJobExecution dashboards (#1092); INF-3/4/5; no-show marking UI; guest/magic-link access; event seat-waitlist; the trial-reschedule feature (PR 7 hides the broken entry); collaborator permission enforcement (#768); the org trials dead API surface (delete-or-build pending); #906 and the cold-start cluster (#932/#937/#1117/#1120/#1124 — a separate infra train); #1132 §05 GST credit notes; MAX_CANDIDATE_STARTS_PER_ROW; remaining UX P2 polish. #481 and #874 run as launch gates at train exit rather than as train content.

PR checklist

Related: #1161 #1162 #1163 #1164 #1165 #1166 #1167 #1168 · #676 #837 #866 #697 #997 #1006 #1013 #1076 #1092 #1093 #1132 #448 #1085 #1010 #872 #874 #481 · Part of #1072.

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, cancellationcron jobsfinancePayments, refunds, earnings, payouts, invoicing, ledgerlaunch: pre-mvpGates launch — money, data, or a failure we would not detectproductionProduction deployment and readiness

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions