Skip to main content

Chapter 24 — Inside the Cart: Holds, Retries & Self-Healing

Attend onboarding guide · ~8 min read · ↑ Back to contents

Chapter 22 gave you the cart's philosophy: mirror the vendor, never disagree. This chapter is the promised deep-dive — what the four cart tables actually do, what happens when a hold fails, and why some "obvious" machinery (like an abandoned-cart cleanup job) deliberately doesn't exist.

The four tables, watched live

Create a cart on a sandbox store and watch the database react. The sequence:

  1. Pick three events, click continue → a tbl_carts row appears: active, $0, the program ID, any pack/package. At the same moment, three tbl_cart_events rows appear — all pending. No seats, no vendor calls yet. (This early creation is the abandoned-cart analytics from Ch 22.)
  2. Hold a suite → a tbl_cart_items row: the vendor's cart ID, the hold status, the price, and retryCount. Plus tbl_cart_seats rows underneath with the exact sections/rows/seat ranges.
  3. Go back a step → the event flips back to pending, the item flips to cancelled, the cart total drops. Both sides — ours and the vendor's — released.

Two details people miss:

  • One item can own several seat-blocks. A big suite came back from TM as three blocks (one per order line item) — that's why seats live in their own table instead of columns on the item.
  • retryCount counts requests that needed retries, not attempts. A hold that failed outright (no alternative existed) shows retryCount: 0 — the failed attempts inside one request never become rows.

When a hold fails: the retry ladder

For seat-level inventory, vendors offer two APIs: seat-hold-specific ("give me exactly section 5, row C, seats 1–4") and best-available ("give me 4 seats, here are my codes — you pick"). The retry ladder walks from precise to desperate:

  1. Exact seats (hold-specific).
  2. Best-available, same section + same price code — "couldn't get my seats; same price, same section, any seats."
  3. Best-available, same price code, any section — "anywhere in the stadium at my price."
  4. The Hail Mary — best-available on ticket types only (price code dropped too) — "anything with my ticket type."

If even the Hail Mary fails, the fan sees the failure modal. If a later step succeeds with different seats than requested, the fan gets a consent modal first — e.g. "Box 29 wasn't available; we found Box 30 — OK?"

Suites have no best-available — suites are Attend's invention, the vendor doesn't know they exist. Instead, the backend runs a mini get-inventory (fetchSuiteAlternatives) and looks for a replacement: same size or bigger, never smaller, sorted cheapest-first (capped at 3 alternatives).

Self-correcting, not just validating

Validate doesn't only detect drift — it repairs the harmless kind:

  • Price drift: the team changed a price on the vendor side after the seats were held ($300 → $310, say). Validate re-syncs the database prices. (Known UI bug: the displayed total may not refresh on the spot even though the backend re-synced — the team is aware.)
  • Seat drift (seats missing, extra, or different): that's not repairable — the fan gets the "your cart is incorrect" modal, everything is cancelled on both sides, back to the home page. This happens in practice, not just in theory.

Why there's no hold-cancelling abandoned-cart cron

You'd expect a job that cancels stale carts. There deliberately isn't one that cancels holds, and the reason is a classic vendor quirk: TM Archtics cancels holds by seat block — section, row, seats — not by cart ID. (A separate daily job does snapshot abandoned carts into tbl_abandoned_carts for analytics — it just never cancels anything.)

Walk the failure case: a fan's hold silently expired on the vendor side (TM clears old holds every ~30 minutes). Those exact seats are now in another fan's cart. If our cron now said "release section 5, row C, seats 1–4", it would release the second fan's hold. The vendor API has no way to say whose hold to cancel.

So the system leans on self-healing instead: the vendor expires holds on its own schedule, and the inventory rebuild puts the seats back on sale within ~20 minutes. On a periodic cron (~20 min) the admin/config side (cortex-backend) rebuilds the base inventory tables (tbl_prices, tbl_seats, tbl_sections, tbl_seat_price_mapping) from the vendor — deleting + re-inserting per event + program (not a whole-table truncate); the fan backend only reads them. Out-of-sync states can't live longer than one rebuild cycle.

Two side-effects worth knowing:

  • Old active carts in the database are normal, not a bug.
  • A fan can have two active carts at once — they're fully independent; there's no global session constraint, and both can even hold the same events.

Auto-assign (backend ready, UI in progress)

For big game packs (10+ events), picking seats per event is tedious. Auto-assign: the fan holds seats for the first event, a modal offers "want us to match the rest?", and the backend finds the same or closest inventory location across the other events — then calls manage cart internally, which means it inherits the retry ladder, forced add-ons, and the standard response shape for free. The backend is built; the UI integration is still being worked on.

Recap

  • Cart → cart_events → cart_items → cart_seats; items can own multiple seat blocks; retryCount is per-request.
  • Retry ladder: exact → same section+price → same price anywhere → Hail Mary (ticket types only); suite fallback = same size or bigger, closest price; surprising substitutions ask the fan first.
  • Validate repairs price drift, but seat drift = cancel everything.
  • No abandoned-cart cron by design — Archtics cancels by seat block, so cancelling stale holds could release someone else's seats. The ~20-minute full inventory rebuild self-heals instead.
  • Auto-assign exists server-side and reuses manage cart; UI is coming.

Next → Chapter 25 — The Two Ticketmasters & the Host Checkout