Congratulations on making it to the final lesson of this course 🎉 You’ve built a full tax configuration system, wired it into carts, and you can now see cart totals change as the tax country changes. That’s a huge backend milestone.
Previously, in “Applying Cart Taxes”, you made carts tax-aware by storing tax_country, resolving a tax_rate_bps (configured or default), and computing totals every time a cart is fetched. That was perfect for a dynamic cart. But orders aren’t dynamic—orders are receipts. This lesson is about making that receipt immutable and trustworthy.
Tax rates change. Sometimes frequently. If your system recalculates tax for old orders using “today’s” rate, you’ll corrupt your financial history.
The key behavior we want is:
- Updating a tax rate affects future carts and future checkouts.
- Updating a tax rate must not change existing orders.
That’s what “snapshotting tax at checkout” means: at the moment checkout happens, we store tax_country, tax_rate_bps, tax_cents, and total_cents on the order. After that, reads (listOrders, getOrderById) must simply return those stored values—no recomputation.
All of the work in this lesson lives in src/lib/repositories/ordersRepo.ts.
Before we even talk checkout, the simplest way to see “snapshotting” is in the order mapping: if a field exists in the DB row and we want it to be part of the domain order object, mapOrderRow(...) must include it.
This function is the foundation for both listing and fetching orders, because both functions map rows through it.
tax_countryandtax_rate_bpsare the “snapshot metadata” that explain why an order’s tax looks the way it does. Without them, the order would have numbers but no context.tax_centsandtotal_centsare mapped directly from the row and should be treated as final values once stored.- This mapping function is intentionally “dumb”: it doesn’t compute totals or call out to other tables. That’s a critical theme of this lesson—reads should not recompute.
Both list and fetch call mapOrderRow(...), which means once mapping is correct, both endpoints become consistent automatically.
- Listing orders returns the snapshot fields because the SQL selects
*and the mapping includestax_countryandtax_rate_bps. - This is exactly what you want for an orders list UI: it can show totals and the rate used without additional DB lookups.
- Nothing in
listOrdersqueriestax_rates, which preserves the snapshot guarantee.
Fetching one order by ID should behave the same way—plus items.
- The order-level amounts (
subtotal_cents,tax_cents,total_cents) come straight from theorderstable and should remain unchanged.
The heart of this lesson is createOrderFromCart(...). Checkout is the one moment where it’s valid to look at current state (cart, items, inventory, tax configuration) and convert it into a permanent order snapshot.
This function already uses withTransaction, and that’s non-negotiable: inventory checks + inventory decrement + totals + inserts must be consistent.
- The cart row is locked with
FOR UPDATE, and importantly it selectstax_country. That ensures the checkout sees a stable tax country during the transaction. - If the cart doesn’t exist, checkout throws a
CheckoutError("CART_NOT_FOUND"). This is a business outcome handled upstream (service layer maps it). - If the cart isn’t open, we throw
CART_NOT_OPEN. That prevents checkout from running twice or from modifying a finalized cart.
Next, checkout locks cart items and the relevant product rows. This prevents race conditions like two checkouts trying to buy the last unit at the same time.
- Cart items are selected with
FOR UPDATE, which ensures the item snapshot used for subtotal (quantity × unit_price_cents) cannot change mid-checkout. - Products are also locked with
FOR UPDATEso inventory checks and decrements are safe from race conditions. - Notice that item rows include
unit_price_cents. That’s the same snapshot principle you used earlier for carts: checkout uses snapshot item prices, not “current product price.”
Checkout gathers required quantities per product, verifies availability, then decrements inventory. All of this happens inside the same transaction so it’s “all or nothing”.
- The first loop aggregates quantities per product before performing inventory checks. In this codebase, the
cart_itemstable enforcesUNIQUE (cart_id, product_id), and the add-to-cart logic increments the existing row rather than inserting duplicates. That means a cart normally has only one row per product. The aggregation loop is still a useful defensive pattern: it ensures the inventory check operates per product ID even if the query shape changes in the future or if multiple rows somehow appear (for example, after a schema change or a bulk import). It keeps the checkout logic correct without relying too heavily on assumptions about the cart structure. INSUFFICIENT_INVENTORYis thrown if the product is missing or doesn’t have enough stock. Because this is inside a transaction, nothing is decremented if the check fails.- The decrement is done after all checks pass, which reduces the chance of partial state changes. Combined with the transaction, it ensures consistency.
Now we compute the subtotal, resolve the tax rate basis points once, compute tax_cents, and compute total_cents. These values are then inserted into the orders table as permanent snapshot fields.
-
Subtotal uses item snapshot prices (
unit_price_cents), so the receipt reflects what was actually in the cart at checkout time. -
Resolving
taxRateBpsfollows the exact rule your practices will enforce:- If
tax_countryis set → attempt to loadrate_bpsfromtax_rates. - If no row exists (or tax_country is unset) → fall back to
DEFAULT_TAX_RATE_BPS.
- If
-
computeTaxCents(subtotal, taxRateBps)ensures consistent basis-point math and rounding behavior across the codebase. -
Currency is derived from one of the products, with
"USD"as the fallback. This is consistent with how the project normalizes currency elsewhere.
This is where “snapshotting” becomes real: we store tax metadata and computed totals directly in the orders row.
tax_countryandtax_rate_bpsare stored on the order so future reads can show “what rate was applied” without looking anywhere else.tax_centsandtotal_centsare stored as snapshot amounts. Once written, they must not be recomputed during reads.RETURNING *lets us immediately map the inserted order row into the domain object usingmapOrderRow, ensuring a consistent response shape.- The order status is set to
'pending'at creation, matching the lifecycle you implemented earlier.
The order isn’t complete without snapshotting purchased line items and marking the cart as checked out—still inside the same transaction.
- Each order item stores
unit_price_cents, which is the item-level snapshot that prevents price changes from affecting past orders. - The cart status update to
"checked_out"is the final “commit” of checkout. Since we’re still in the transaction, we only reach this line if all prior steps succeed. - Returning the order at the end gives the caller the snapshot totals and metadata immediately.
The practices intentionally include starter code that tries to recompute tax in getOrderById(...) by querying the current tax rate. That is exactly what we must not do.
The correct behavior is what you see in the current repository reads:
listOrders(...)selects orders and maps them. No tax lookups.getOrderById(...)maps the order row and loads items. No tax lookups.mapOrderRow(...)simply maps stored snapshot fields.
If a tax rate is updated in tax_rates, it should affect:
- carts (because carts resolve tax dynamically), and
- future checkouts (because snapshotting uses current config at checkout time),
…but it must not affect:
- existing orders (because orders must remain immutable receipts).
You’ve now completed the full tax journey:
- You built configurable tax rates.
- You applied taxes dynamically to carts using stored
tax_countryand a safe default. - And now you’ve ensured checkout snapshots tax permanently into orders so historical receipts never drift.
This final step is what separates “it works today” from “it’s correct forever.” 💪
