Skip to main content

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:

OrderRouteWhat the fan did
1POST /cart/initializelanded on inventory (cart exists before any hold — abandoned-cart analytics)
2PATCH /cart/:cartId/eventspicked their games
3POST /cart/:cartId/manageselected seats → vendor holds happen here
4PATCH /cart/:cartId/bindlogged 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:

  1. The atomic lock: cart flips to processing with a race guard, and a finally block always resets it.
  2. 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.
  3. The order insert happens only after payment succeeds — then the cart flips to paid.
  4. 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.