Skip to content

[Big Pickle][booking] Overall Verdict on the Booking Subsystem (HLD + LLD) — P0-P3 indexed backlog, backend core + UI (B2C + B2B), 10 new findings #1433

Description

@teetangh

Big Pickle — Overall Verdict on the Booking Subsystem (HLD + LLD)

Companion to #1421 [Big Pickle][finance] Overall Verdict on the Financial Subsystem. This is the booking verdict in the same series. Together #1421 + this issue cover the whole bookable-deliverable + money surface.

Parents/umbrellas referenced: #1319 (booking + maintenance wave-5 umbrella, OPEN), #1332 (booking + payments architecture tracker, OPEN). #837 (B2C state-machine hardening) and #687 (invoice-funded credit-risk) are CLOSED/merged — their live threads live on in the issues they spawned.

Scope rule (per product): DST / IANA-TZID work is excluded from launch — most customers are Indian before foreigners. It stays as #872 (post-MVP) + #1168 (schedulingTimezone grid, post-MVP). Not re-scoped here.


1. Executive verdict

The booking subsystem is architecturally sound and launch-safe for its core booking path:

  • Three-layer concurrency defense is in place and reviewed: Redis lock → GiST EXCLUDE constraint → Serializable SSI with retry. This is the right shape for a double-booking-prone system.
  • Hold-then-confirm two-phase (tentative slots held at booking, confirmed on payment) is correct and matches how the availability/occupancy model is taught in docs/booking/03-slot-math.
  • CAS state machine is the single source of truth (lib/booking/transitions.ts) and is applied across most transitions.
  • Refund front doors and explicit org scoping are enforced per the booking-doctrine.

What remains is NOT a rebuild — it is a set of pre-launch P0/P1 correctness + display items and a P2/P3 backlog. The most important patterns:

  1. Money/consent/wallet failures are invisible to customers (P1, UI): WALLET_FROZEN, CONSENT_REQUIRED, CONSENT_WITHDRAWN degrade to a generic "Something Went Wrong"; wallet-freeze has no org-side banner. This is the single biggest launch risk in the customer-facing layer.
  2. Two crons bypass the CAS/audit discipline (P1, backend): the expire-stale and reconcile-slot scripts use raw updateMany. They can silently overwrite a concurrent writer's state and leave NO audit trail.
  3. Tentative (held-pending-payment) state is mis-rendered (P1, UI): SessionTimeline drops all tentative sessions, so a held booking looks unscheduled with no urgency CTA.

Nothing in this verdict changes the previously-frozen acceptance posture: no db push against the shared Supabase project; verify against the seeded dev-server + mock payments (see booking-verification runbook).


2. Verified-correct posture (do NOT regress)

These were re-confirmed against the current code and are considered correct by design:


3. P0 — launch blockers

Nothing in this audit is a new P0 of its own beyond what is already filed. The existing P0s stand:

# Item Scope
#1340 Reschedule auto-confirm races the allocation commit — can leave openForAppointmentId dangling backend, B2C
#1341 Cron lock is fail-open — concurrent unlocked runs double-refund/double-notify during Redis outage backend, cron
#1347 Credit-funded B2C bookings break sum(legs) === Payment.amount money invariant backend, money (finance)

4. P1 — must fix before/at launch

4.1 Backend core

# Item B2C/B2B
#1423 (NEW) expire-stale crons raw updateMany bypass the CAS state machine AND the audit trail; APPROVED→EXPIRED is undocumented in transitions.ts both
#1424 (NEW) reconcile-slot-availability clears isTentative with a blind updateMany — TOCTOU, no parent-status re-check both
#1344 Auto-allocation reads availability outside the commit transaction B2C
#1355 (CLOSED→track) Payment concurrency: consultee-calendar relies on Redis lock, not Serializable; consultee-side missing GiST B2C
#1402 Checkout double-click creates two orders before CAS B2C

4.2 UI — customer-facing (money/consent/wallet/tentative)

# Item B2C/B2B
#1426 (NEW) WALLET_FROZEN / CONSENT_REQUIRED / CONSENT_WITHDRAWN → generic "Something Went Wrong" (classification + toast map) both
#1427 (NEW) Wallet-freeze invisible on org billing + no admin visibility; moneyMoveBlocked ignores it B2B
#1428 (NEW) SessionTimeline hides ALL tentative sessions → held booking looks unscheduled, no countdown, TENTATIVE needs-action unsurfaced B2C/B2B
#1345 Consultant Home "Pending Requests" badge count contradicts NeedsYou card + Requests page B2C (consultant)
#1346 EventCard/heatmap show wrong start date for classes B2C

4.3 Availability / slot segmentation (B2C)

# Item
#1415 Pricing/duration toggle shows EMPTY/wrong-length windows (no fallback)
#1416 Merge-tolerance asymmetry: exact-adjacency booking vs 1-minute display merge
#1417 Default 1h vs authored 2h block — never offers the combined session
#1343 startDay semantics inconsistent across write/read/display/validation
#1342 Availability grid timezone/day-shift mismatch
#1326 Weekly rows can freeze wrong utcOffsetMinutes (UTC-3 on IST consultant)

5. P2 — first 90 days

5.1 Backend core

# Item B2C/B2B
#1425 (NEW) mintRefundCreditNote XNOR guard inverted (!refundId === !disputeId) — latent finance/B2B
#1431 (NEW) calculateRequiredSlots silently defaults consultation/webinar to 2 atoms on missing duration (class throws) B2C
#1348 Onboarding/allocation residuals (offset=0 default, partial-allocation top-up, allocator tie-break) B2C
#1206 Product decision: partial allocation when availability falls short (PENDING) B2C
#997 Calendar performance: slow PENDING lists + move allocation engine server-side B2C
#1356 Transactional outbox — post-payment side-effects fire-and-forget both
#1092 DB correctness/observability: sidecar apply, constraint attribution both
#1411 Cleanup-abandoned-payments crashes without isolation — concurrent cron run risk cron
#1413 GH Actions crons missing concurrency groups cron

5.2 UI — customer-facing

# Item B2C/B2B
#1429 (NEW) Trial UI: consultant mapper forces isTentative=false (party asymmetry); detail "Pay Now" skips branded checkout; slot-picker lacks TRIAL type B2C
#1430 (NEW) No dunning-suspend banner / consent pre-flight on org-funded surfaces — blocks only appear at failed checkout (P1 the moment ENABLE_DUNNING_SUSPEND ships) B2B
#1394 Seeded consultants lack SESSION_BOOKING consent → seeded checkout 403s both (dev)
#833 7-day tentative-slot expiration UX (reduced to 24h via #845-#848; open arg for rebooking friction) B2C
#1085 Novu template renders "from X to Y" for a reschedule with no new time B2C
#849 User-facing cancel-pending-booking action for tentative holds B2C
#448 Reschedule UX: no notifications, no audit trail, staff blind spots B2C

5.3 B2B / org-funded

# Item
#1357 Ledger, tax & GST compliance gaps (Stripe refunds bypass GST/TDS; overage transitions in same Serializable tx)
#1372 CREDIT_POOL refund-back-to-pool not acceptance-tested
#835 Org-wallet-funded B2C refunds invisible to org audit surface
#1128 Invoice-refund wallet credit labelled "guaranteed bookkeeping" but has no durability guarantee
#1003 Group-event (webinar/class) cancellation refunds/notifies nobody; attendee earnings never reversed

5.4 Backlog items not yet filed (tracker candidates)

  • Collaborator TDS split across HOST orgs — revenue split vs statutory TDS deduction for a collaborator belonging to a different org than the plan host: open question, needs a decision (B2B, consider filing separately with a design doc).

6. P3 / tech-debt / deferred

# Item Notes
#1432 (NEW) Money & credit display consistency (pre-discount+subtotal; GSTIN "Optional" vs e-invoice required; org credits no per-booking cost) UI polish
#1418 Pre-#1320 unmerged availability rows stay split data migration
#1419 Verify every view re-merges N 30-min atoms into one run hardening
#1420 Terminology consistency: unify slot/session/appointment/event docs/tech-debt
#1332 Booking + payments architecture tracker (renames, folder layout, API surface) post-reset
#1375 Split the two booking god modules (SlotAllocationService 4357 LOC, checkout.ts 3808 LOC) blocked on participant model
#1058 Consultant planner has no archive path for booked plans enhancement
#472 Session overrun detection and conflict prevention post-MVP
#1168 Draw slot grid in schedulingTimezone post-MVP (prereq for #872)
#872 DST / IANA-TZID implementation (post-MVP, per scope rule) deferred by product
#1338 Handoff doc — wave-5 status, open residuals, next moves documentation

7. Backend-core vs UI split (executive view)

Backend core is healthy; the highest-risk defect class is in the UI/display layer, specifically where a backend business-rejection or state is not surfaced:

Recommended emphasis: schedule the four P1 UI items (#1426, #1427, #1428, #1429) together as one "customer-facing booking fidelity" workstream, and the two cron bypasses (#1423, #1424) together with #1341/#1411/#1413 as the "cron correctness" workstream.


8. New findings filed in this verdict (this series)

These are the 10 findings this audit added, all now open as individual issues (per the #1421 [Big Pickle] pattern):

# Severity Layer Finding
#1423 P1 backend/core expire-stale crons bypass CAS + audit trail; APPROVED→EXPIRED undocumented
#1424 P1 backend/core reconcile-slot-availability blind isTentative clear, TOCTOU
#1425 P2 backend/money mintRefundCreditNote inverted XNOR guard
#1426 P1 UI WALLET_FROZEN/CONSENT_* errors → generic toast
#1427 P1 UI/B2B wallet-freeze invisible on org billing + admin
#1428 P1 UI tentative sessions hidden; no countdown / TENTATIVE CTA
#1429 P2 UI trial mapper asymmetry; Pay-Now skips branded checkout; no TRIAL slot-picker type
#1430 P2 UI/B2B no dunning-suspend banner / consent pre-flight
#1431 P2 backend/core calculateRequiredSlots silent 2-atom default (consultation/webinar)
#1432 P3 UI/money money & credit display consistency

9. Prioritized remediation index (P0 → P3)

  1. P0: [booking][P0] Reschedule confirmation: the allocator's supersede sweep DECLINED the very proposal being confirmed (booking moved, audit trail said refused) — fixed by #1515 #1340, [booking][P0 CRITICAL] Cron lock is fail-open — concurrent unlocked runs double-refund / double-notify during a Redis outage #1341, [finance][P1 HIGH] Credit-funded B2C bookings break the sum(legs) === Payment.amount money invariant #1347
  2. P1 backend: [booking][P1 HIGH] expire-stale-requests crons bypass the CAS state machine and the audit trail (raw updateMany, APPROVED->EXPIRED undocumented) #1423, [booking][P1 HIGH] reconcile-slot-availability clears isTentative with a blind updateMany — no parent-status/CAS re-check (TOCTOU on paid-tentative slots) #1424, [booking][P1 HIGH] Auto-allocation reads availability outside the commit transaction — push exclusivity to the DB #1344
  3. P1 UI (booking fidelity workstream): [booking][P1 HIGH] WALLET_FROZEN / CONSENT_REQUIRED / CONSENT_WITHDRAWN checkout errors fall through to a generic 'Something Went Wrong' toast #1426, [booking][P1 HIGH] Org wallet-freeze state is invisible on billing/dashboard surfaces — 'moneyMoveBlocked' ignores it, no banner (org discovers at failed booking) #1427, [booking][P1 HIGH] SessionTimeline filters out ALL tentative sessions — held-pending-payment bookings render as 'Awaiting schedule confirmation' with no CTA or countdown #1428
  4. P1 availability: [booking][P1 HIGH] Pricing/duration toggle shows EMPTY or wrong-length windows for authored availability blocks (breakDownSlotsPreservingStatus sliding-window has no fallback) #1415, [booking][P1 HIGH] Merge-tolerance asymmetry: exact-adjacency booking merge vs 1-minute display merge cause display/bookability divergence #1416, [booking][P1 HIGH] Default booking length (1h) vs authored block (2h): heatmap/dialog show fragmented 1-hour slots, never the combined 2h session #1417, [booking][P1 HIGH] startDay semantics inconsistent across weekly-availability write, read, display and validation #1343, [booking][P1 HIGH] Availability grid timezone / day-shift mismatch between read-display, validation and the booking generator #1342, Weekly availability rows can freeze a wrong utcOffsetMinutes (UTC−3 found on an IST consultant) — audit + pin the launch rule #1326
  5. P1 dashboard: [dashboard][P1 HIGH] Consultant Home 'Pending Requests' badge count contradicts the NeedsYou card and the Requests page #1345, [dashboard][P1 HIGH] EventCard and request heatmap show the wrong start date for classes (schedulingPeriodStartsAt vs actual slot) #1346
  6. P2 backend: [booking][P2 MEDIUM] mintRefundCreditNote guard is inverted — !refundId === !disputeId never throws when exactly one key is set (latent) #1425, [booking][P2 MEDIUM] calculateRequiredSlots silently defaults consultation/webinar to 2 atoms on missing duration — can under-allocate vs the priced session (class throws) #1431, [booking][P2 MEDIUM] Onboarding / allocation-mode residuals — freeze-offset zero default, partial-allocation top-up, and allocator edge cases #1348, Product decision: should consultants be able to PARTIALLY allocate recurring events when availability falls short? #1206, Request Calendar performance: slow PENDING list endpoints + move client-side allocation engine server-side #997, [P2 MEDIUM] Transactional Outbox & Payment Side-Effect Reliability #1356, DB-layer correctness and observability: automate the sidecar apply, attribute constraint violations, and settle the one remaining trigger #1092, [payments][P1 HIGH] Cleanup-Abandoned-Payments Crashes Without ISOLATION LEVEL — Concurrent Run Risk #1411, [payments][P1 HIGH] GitHub Actions Crons Missing concurrency Groups — 56 UnGuarded Scheduled Workflows (20 Money Crons) #1413
  7. P2 UI: [booking][P2 MEDIUM] Trial UI gaps: consultant mapper forces isTentative=false (party asymmetry), detail 'Pay Now' skips branded checkout, slot-picker lacks TRIAL type #1429, [booking][P2 MEDIUM] No dunning-suspend banner or consent pre-flight on org-funded booking surfaces — blocks surface only at failed checkout #1430, [seed][P2] Consent artifacts are skipped for everyone when the org seed early-returns on a thin user set #1394, [B2C][UX] 7-day tentative-slot expiration locks users out of rebooking for days #833, Novu template still renders "from X to Y" for a reschedule with no new time — dashboard edit needed to finish #1083 #1085, [B2C][UX] User-facing cancel-pending-booking action for tentative holds #849, Reschedule Flow: Missing notifications, no audit trail, staff/admin blind spots, and UX gaps #448
  8. P2 B2B/org: [P1 HIGH] Ledger, Tax & GST Compliance Gaps #1357, [finance][P1 HIGH] CREDIT_POOL Refund-Back-to-Pool Not Acceptance-Tested #1372, [SEAM][AUDIT] Org-wallet-funded B2C refunds are invisible to the org audit surface #835, Invoice-refund wallet credit is called "guaranteed bookkeeping" but has no durability guarantee #1128, Group-event cancellation refunds and notifies nobody (audit C3/M2) #1003
  9. P3 / deferred: [booking][P3 LOW] Money & credit display consistency: checkout shows pre-discount+subtotal, GSTIN 'Optional' but e-invoice-required, org credits no per-booking cost #1432, [booking][P2 MEDIUM] Pre-#1320 unmerged availability rows stay split unless the coalesce migration scripts are run #1418, [booking][P2 MEDIUM] Verify appointment/session views always re-merge N 30-minute atoms into one contiguous run (hardening + regression) #1419, [booking][P2 MEDIUM] Terminology consistency: unify slot / session / appointment / event across code, comments, docs and issues #1420, [TRACKER] Booking + payments code architecture: folder layout, API surface, model and function naming #1332, [EPIC] Split the two booking god modules by extraction: SlotAllocationService (4,357 LOC) and checkout.ts (3,808 LOC) #1375, Consultant planner has no archive path for booked plans #1058, [RESILIENCE] Session overrun detection and conflict prevention #472, Draw the slot grid in schedulingTimezone so the columns match the caps being enforced #1168, DST / IANA-TZID: reader flip over the dual-written local availability columns (post-MVP) #872, [HANDOFF] Booking productionization wave 5 → next session: status, what is correct, open defects, recommended next moves (2026-09-03) #1338

10. Verification posture

Per the booking-verification runbook: changes to any P0/P1 item above must be proven against the seeded dev-server + mock payments (never db push against the shared Supabase project), with the jest suites under __tests__/booking-algorithm, __tests__/booking, __tests__/payments, and __tests__/enterprise as the correctness gate. The UI findings (#1426-#1430) additionally need a visual check on the affected surfaces (appointment detail, org billing, checkout).

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, cancellationlaunch: 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