Orders Must Not Change

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.

The Order Mapping Must Include Snapshot Fields

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_country and tax_rate_bps are 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_cents and total_cents are 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.
List and Fetch Must Return the Same Snapshot Fields

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 includes tax_country and tax_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 listOrders queries tax_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 the orders table and should remain unchanged.
Checkout Must Snapshot Tax Inside the Transaction

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 selects tax_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.
Lock Items and Products So Inventory and Totals Are Stable

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 UPDATE so 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.”
Inventory Checks and Decrement Are Part of the Same Atomic Flow

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_items table enforces UNIQUE (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_INVENTORY is 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.
Snapshotting Tax Rate at Checkout (The Core of the Lesson)

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 taxRateBps follows the exact rule your practices will enforce:

    • If tax_country is set → attempt to load rate_bps from tax_rates.
    • If no row exists (or tax_country is unset) → fall back to DEFAULT_TAX_RATE_BPS.
  • 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.

Inserting the Order With Snapshot Fields

This is where “snapshotting” becomes real: we store tax metadata and computed totals directly in the orders row.

  • tax_country and tax_rate_bps are stored on the order so future reads can show “what rate was applied” without looking anywhere else.
  • tax_cents and total_cents are 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 using mapOrderRow, ensuring a consistent response shape.
  • The order status is set to 'pending' at creation, matching the lifecycle you implemented earlier.
Snapshotting Line Items and Finalizing the Cart

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 Most Important Rule: Never Recompute Tax When Reading Orders

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).
Recap and Course Finish

You’ve now completed the full tax journey:

  • You built configurable tax rates.
  • You applied taxes dynamically to carts using stored tax_country and 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.” 💪

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal