Snapshotting Tax at Checkout

Congratulations on making it to the final lesson in this path! 🎉 You have come a long way—from building foundational APIs, to managing carts and orders, to applying country-based taxes before checkout. This last step brings everything together in one of the most important moments in an e-commerce backend: turning a live cart into a permanent order snapshot.

In this lesson, we will focus on preserving tax history at checkout time. That means the order should store the exact tax_country, tax_rate_bps, tax_cents, and total_cents that were true at the moment checkout happened. Even if tax rates change later, old orders must not change with them. By the end of this lesson, you will see how the checkout transaction captures those values, why reads must return the stored snapshot instead of recomputing tax, and how the repository keeps order data consistent across both list and get-by-id reads.

Previously, we made carts tax-aware by allowing a cart to store a tax_country and by deriving cart totals from the current tax_rates configuration. That was the right behavior for a cart, because carts are still editable and should reflect current pricing rules. Orders are different: once checkout succeeds, their financial values must become permanent.

Why Checkout Needs a Tax Snapshot

A cart is allowed to change. A user can add items, remove items, switch quantities, or even change the country used for tax calculation. Because of that, cart totals should always reflect the current state of the cart and the current tax configuration.

An order is not like that. Once checkout succeeds, the order becomes a historical record of what the customer actually agreed to pay. If a tax rate for US changes tomorrow, that should affect future carts and future checkouts, but it should never rewrite an existing order from yesterday.

That is why checkout must snapshot tax data. Instead of storing only the final tax_cents, we also persist the tax_country and the exact tax_rate_bps used during checkout. That gives us both the result and the context behind the result, which is useful for receipts, order history, debugging, and accounting.

The Order Row Must Include Tax Snapshot Fields

The first place this lesson touches is src/lib/repositories/ordersRepo.ts. Since orders are supposed to preserve tax details, the repository’s database row type must include those fields explicitly.

This is important because the repository is the layer that converts raw database rows into domain objects. If the row type omits tax_country or tax_rate_bps, those values will never make it into the Order returned by the rest of the application.

  • This row type mirrors the important persisted columns in the orders table, including both tax_country and tax_rate_bps. That matters because a checkout snapshot is only useful if the repository actually reads those stored values back out of the database.

  • Including tax_country gives clients the geographic context used for tax resolution at checkout time. Including tax_rate_bps gives clients the exact rate that produced the stored tax_cents, which is especially helpful when explaining old orders after tax policies have changed.

  • These fields are not optional extras in this workflow. They are part of the order’s financial record, and keeping them in the row type ensures the repository remains aligned with the database schema and the domain model.

Mapping Stored Tax Fields into the Order Domain Object

Once OrderRow includes the snapshot fields, the repository needs to carry them forward into the returned Order. That work happens inside mapOrderRow(...) in src/lib/repositories/ordersRepo.ts.

This mapping step is where the repository turns a raw SQL result into a stable domain object used elsewhere in the app. If the mapping forgets the tax snapshot fields, clients will lose access to that data even though it exists in the database.

  • The mapping copies the stored tax_country and tax_rate_bps directly from the database row into the Order domain object. That is exactly what we want for snapshot behavior: the order should reflect what was persisted, not some recalculated or inferred value.

  • Notice that tax_cents and total_cents are also copied directly from the row. This reinforces the accounting rule for orders: reads should expose the stored financial values as-is, because those are the values that were finalized during checkout.

  • This mapper is also important for consistency between different repository readers. Both list-based reads and single-order reads depend on , so once the tax snapshot fields are mapped here, they can flow through the rest of the order API uniformly.

Listing Orders Must Return the Snapshot Fields Too

It is not enough for getOrderById(...) to expose the tax snapshot. The list endpoint should also return the same important tax context, especially if the UI displays order summaries in a table or dashboard.

That is why listOrders(...) in src/lib/repositories/ordersRepo.ts should simply query orders and map them through the same row mapper.

  • This query fetches paginated orders directly from the orders table and maps each one through mapOrderRow(...). Because the mapper already includes tax_country and tax_rate_bps, the list response automatically carries those snapshot fields too.

  • That consistency matters for clients. A list view and a detail view should not disagree about which financial fields are important, especially when those fields help explain how totals were calculated at checkout time.

  • Returning the tax snapshot in list reads also helps with debugging and operations. For example, if support staff are reviewing recent orders, they can see the stored tax context immediately without needing to fetch every order individually.

Reading a Single Order Must Not Recompute Tax

The most important reading rule in this lesson is simple: orders must never recompute tax from the current tax table. A cart can do that because it is still “live.” An order cannot.

The single-order reader in src/lib/repositories/ordersRepo.ts gets this right by selecting the order row, mapping it, and then attaching the order items. It does not go back to tax_rates, and it does not rerun tax calculations during reads.

  • The function reads the order row as persisted and immediately maps it with mapOrderRow(...). That means tax_cents, total_cents, tax_country, and tax_rate_bps all come from the stored order snapshot, not from the current tax configuration.

  • This is a critical accounting guarantee. If the code recomputed tax during reads using the latest tax_rates entry, old orders could silently drift over time and stop matching what the customer was actually charged.

  • The only additional read performed here is for order_items, which are also stored snapshots of the purchased items. That keeps the whole order response historically accurate: both the order-level tax values and the per-item unit prices represent what was true at checkout time.

Checkout Must Resolve Tax Inside the Same Transaction

The central checkout logic lives in createOrderFromCart(...) inside src/lib/repositories/ordersRepo.ts. This function is where the cart stops being temporary and becomes a permanent order.

The most important idea here is that inventory validation, tax resolution, total calculation, order insertion, order item insertion, and cart status update all happen inside the same transaction. Snapshotting only works if the stored tax details and stored totals all come from the same locked moment in time.

  • Wrapping checkout in withTransaction(...) ensures the entire workflow is atomic. Either every step succeeds together, or the whole checkout rolls back and leaves the system unchanged.

  • This matters a lot for financial integrity. You do not want a half-finished checkout where inventory was reduced but the order was not created, or where an order row exists but the cart never got marked as checked out.

The first transactional step is locking and loading the cart.

  • The FOR UPDATE clause locks the cart row for the duration of the transaction. That prevents concurrent checkout flows or other updates from changing the cart while this checkout is finalizing its snapshot.

  • Notice that the query selects tax_country from the cart row. That is essential because the checkout logic needs the cart’s chosen country in order to resolve the correct rate and persist that context onto the order.

Resolving and Snapshotting the Tax Rate at Checkout

Now we reach the tax-specific heart of the checkout flow. At this point, the transaction has a locked cart, locked items, and validated inventory. That means it can safely compute the final order totals for this exact checkout moment.

This logic also needs a fallback: if the cart has no tax_country, or if the chosen country does not exist in tax_rates, checkout should use DEFAULT_TAX_RATE_BPS.

  • The subtotal is derived from the locked cart item snapshot, not from any frontend calculation. That keeps the financial result deterministic and ensures checkout totals always come from persisted server-side data.

  • If cart.tax_country exists, the transaction looks up rate_bps in tax_rates. If no row is found, or if the cart never had a country set, the code falls back to DEFAULT_TAX_RATE_BPS, which lets checkout continue with a predictable default rate instead of failing unexpectedly.

  • The resolved rate is then used to compute tax_cents with computeTaxCents(...), and the final is produced by adding subtotal and tax. These are the exact values that must be stored onto the order and preserved forever.

Inserting the Order with the Tax Snapshot

Once the checkout transaction has resolved the correct tax values, it persists them directly into the orders table. This is the moment the snapshot becomes permanent.

The order insert in src/lib/repositories/ordersRepo.ts explicitly stores both tax_country and tax_rate_bps, along with the computed tax and total amounts.

  • The insert statement persists all the important order-level financial fields in one row, including the two tax snapshot columns: tax_country and tax_rate_bps. This ensures future reads do not need to ask the current tax configuration what happened during this checkout.

  • The tax values are inserted using the data already resolved inside this same transaction. That is exactly what makes snapshotting correct: the stored totals and the stored tax metadata all come from the same locked checkout state.

  • RETURNING * immediately gives the repository the newly inserted order row, which is then mapped into the domain object. This is a useful pattern because it lets the repository keep working with the authoritative saved values instead of reconstructing them separately.

Snapshotting the Purchased Items and Finalizing Checkout

After creating the order row, checkout copies each cart item into order_items and then marks the cart as checked out. These steps complete the transition from a temporary cart to a permanent order snapshot.

  • Each order_items row copies the product_id, quantity, and unit_price_cents from the locked cart item snapshot. Just like the order-level tax fields, these are meant to preserve what was true when checkout happened, even if product pricing changes later.

  • Updating the cart status to "checked_out" finalizes the source cart so it cannot continue being treated as an editable shopping cart. This is an important state transition because it prevents the same cart from being checked out repeatedly.

  • All of this still happens inside the same transaction. That is the key guarantee of the whole lesson: inventory changes, order creation, item snapshotting, tax snapshotting, and cart finalization either all happen together or none of them happen at all.

Why Future Tax Changes Must Not Affect Existing Orders

This lesson closes an important conceptual loop in the course.

Earlier, we made cart totals depend on the current tax setup. That was correct, because carts are live, editable objects. But after checkout, the order must stop following current tax configuration and start representing historical truth.

That is why this lesson insists on two complementary rules:

  1. Checkout resolves and stores tax snapshot fields during the transaction.
  2. Order reads return the stored values exactly as persisted, without recomputing tax.

Together, those rules create the right behavior for a production-style commerce system. Tax updates remain useful because they affect future customers and future carts, but they do not rewrite financial history.

Final Recap

You made it to the end of the path—great work. 🎉 In this final lesson, we pulled together the cart, tax, and order workflows into a proper checkout snapshot.

In src/lib/repositories/ordersRepo.ts, we saw how OrderRow and mapOrderRow(...) include tax_country and tax_rate_bps, so those fields flow into the Order domain object consistently. We also confirmed that both listOrders(...) and getOrderById(...) return orders with the stored tax context, which keeps list views and detail views aligned.

Most importantly, we studied createOrderFromCart(...) and saw how it locks the cart and products, validates inventory, resolves the tax rate from tax_country, falls back to DEFAULT_TAX_RATE_BPS when needed, computes tax_cents and total_cents, and inserts the order with both tax_country and tax_rate_bps persisted. Because all of that happens inside one transaction, the order snapshot reflects the exact truth of the checkout moment.

That is the final piece of the system: carts can change, tax rates can change, inventory can change—but a completed order should not.

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