Tutorial: Trace a Purchase End-to-End
A useful rule of thumb: if these four APIs are good, we are good — get catalog → get inventory → manage cart → checkout. Everything else in the fan backend is supporting machinery around them.
In this tutorial you'll follow one fan's purchase through those four APIs in the actual code
of flex-v3-backend. By the end you'll know which module owns each step, where pricing gets
decided, and what really happens between "add to cart" and "order confirmed".
What you need: the flex-v3-backend repo cloned and open in your editor, and Chapters
21 and 22 read (we'll lean on both).
You won't run anything — this is a guided code-reading, about 30–40 minutes. Every path
below is relative to the repo root; if a function is hard to find, search the filename first,
then the function name within it. Don't try to understand every line — each step tells you
what to look for, and that's enough. A runnable version (real requests against a sandbox
schema) will be added once local environments are set up.
Step 1 — Get the catalog
"What can I buy?"
Open src/modules/catalog/catalog.controller.ts.
The fan app's first call is GET /catalog. The response is the active FlexProgram plus
everything needed to render the storefront: the one program-definition shape matching its
programType (game packs or preset packages or build-your-own — a switch, not all three),
the events, tags, categories, and assets.
Things to notice:
- The route sits behind the team-schema guard — every request carries
X-Team-Schema, and all queries run against that client's PostgreSQL schema. One deployment, many teams. - Only the active program is returned. Teams configure several published programs and flip which one is active — that's why the catalog is the only place program selection matters.
✅ Checkpoint: you can say what GET /catalog returns and why it never returns two programs.
Step 2 — Get inventory (pricing happens here)
"What seats can I see, at what price?"
Open src/modules/pricing-rules/services/pricing-rule-resolver.service.ts.
Before any seat is returned, the backend decides which one pricing rule this fan qualifies
for. Find resolveRuleForUser() and read the loop: rules arrive ordered by priority ASC NULLS LAST, each rule's condition rows (account group / pack / package / event count) are
checked, and the first match wins. One fan, one rule — always.
Now open src/modules/inventory/services/inventory.service.ts and find where the suite
queries branch on pricingMode (event_level / suite_level / suite_tier_level). This is
Chapter 21's "pricing is filtering" in the flesh: seats are joined with all vendor pricing,
then filtered to the winning rule's price combos.
✅ Checkpoint: you can point at the exact loop that decides a fan's price, and explain why two fans can see different prices for the same seat without anything being broken.
Step 3 — Manage the cart
"Hold those seats for me."
Open src/modules/cart/cart.controller.ts and skim the routes top to bottom — they read as
the fan's journey:
| Order | Route | What the fan did |
|---|---|---|
| 1 | POST /cart/initialize | landed on inventory (cart exists before any hold — abandoned-cart analytics) |
| 2 | PATCH /cart/:cartId/events | picked their games |
| 3 | POST /cart/:cartId/manage | selected seats → vendor holds happen here |
| 4 | PATCH /cart/:cartId/bind | logged in (anonymous cart attached to the user) |
Then open src/modules/cart/services/cart-manage.service.ts and find the hold retry ladder:
exact seats → best-available in section+price → same price any section → ticket-types only.
Four attempts before giving up. If persisting a hold fails, the vendor hold is released
immediately — the local cart and the vendor cart must never disagree. That's the
"mirror" from Chapter 22.
✅ Checkpoint: you can name the four retry levels in order, and explain why a cart exists in the database before any seat is held.
Step 4 — Validate, then checkout
"Take my money."
Open src/modules/cart/services/cart-validate.service.ts first: before payment, holds are
re-verified (Host: expiry check; Archtics: a shopping_cart call comparing seats and prices).
Then src/modules/cart/services/cart-checkout.service.ts — the most consequential file in the
fan backend. Follow checkout() and notice:
- The atomic lock: cart flips to
processingwith a race guard, and afinallyblock always resets it. - The vendor split: TM Host charges via a Stripe PaymentIntent that is only captured
after every vendor commit succeeds; TM Archtics is a native
payment_request— no Stripe. - The order insert happens only after payment succeeds — then the cart flips to
paid. - The failure paths: a partial multi-event commit queues refunds in
tbl_checkout_refunds; a payment-plan checkout force-saves the card (future installments need it).
✅ Checkpoint: you can explain when the money actually moves for TM Host vs TM Archtics, and why the order row can never exist without a successful payment.
What you now know
You can place every fan-facing request into one of four buckets, and you know the file that owns each bucket. When a bug report says "fan saw the wrong price", you know to start at the rule resolver (Step 2); "seats vanished at checkout" points at holds and validation (Steps 3–4).
Next: the lookup tables for everything you just read are in the Reference section — Pricing, Cart & Orders, Add-on Types.