The Big Picture: How Cart Taxes Work in This Codebase

Welcome back! 👋 In the last lesson, you built a Tax Rates Configuration API so the backend can store and manage tax rates per country. Now we’re going to apply those rates where they matter: inside the shopping cart totals.

In this lesson, you’ll make carts “tax-aware” by storing a cart’s tax_country, resolving an appropriate tax_rate_bps (configured rate or a default), and recalculating totals every time the cart is fetched. You’ll also add a focused PATCH endpoint that lets clients update the cart’s tax country and immediately see refreshed totals.

At a high level, the flow looks like this:

A cart stores an optional tax_country (like "US"). When we fetch the cart, the repository computes subtotal_cents from the cart item snapshot prices, resolves a tax_rate_bps using the stored tax_country (falling back to DEFAULT_TAX_RATE_BPS when needed), and calculates tax_cents + total_cents. Separately, a small route handler lets clients set the cart’s tax_country via PATCH /api/carts/:id/tax, and the service ensures the cart exists and is still open before saving.

Storing Tax Country on the Cart

This project treats tax_country as cart-level configuration: it’s stored on the carts table and used later when totals are computed. You can see this reflected in the cart row type and in how the cart domain object is built.

The repository definitions live in src/lib/repositories/cartsRepo.ts.

  • tax_country is explicitly nullable, which matches real usage: a user might not have provided a country yet, but the cart still needs to work.
  • Keeping tax_country on the cart means the totals computation can be deterministic: it always uses the cart’s stored value rather than “guessing” from headers, IP, or frontend defaults.
  • This structure also makes the “set tax country” endpoint simple: it updates a single column and then returns a refreshed cart view.
Resolving the Tax Rate: Configured or Default

The cart totals code must always use a stable tax rate. The key rule in this project is:

Totals should not guess the rate — they should either use a configured tax rate or a default.

That logic is centralized in resolveTaxRateBps(...), which lives in src/lib/repositories/cartsRepo.ts.

  • The first guard (if (!countryCode)) ensures carts without a selected country still get consistent totals by using DEFAULT_TAX_RATE_BPS.
  • The query is parameterized ($1) to keep it safe and consistent with the rest of the repo layer.
  • If the country code is set but no tax_rates row exists, we still fall back to DEFAULT_TAX_RATE_BPS. This keeps totals stable even when configuration is incomplete.
  • This function is intentionally “boring”: it does not validate country codes or throw 404s. Validation happens in services/routes; the repo just resolves “best available” for totals.
Computing Totals in getCartById

Cart totals are computed inside getCartById(id) so every cart fetch returns a complete, ready-to-display cart object. This is where snapshot pricing matters: subtotal is derived from cart item snapshots (unit_price_cents), not from current product prices.

The totals computation is in src/lib/repositories/cartsRepo.ts.

  • The cart is built in two layers: base is the cart row fields, then items are loaded and enriched with product metadata for display purposes.
  • The subtotal uses quantity * unit_price_cents from cart_items. That’s an intentional snapshot rule: cart totals shouldn’t change just because a product price changes later.
  • taxRateBps is derived from the stored on the cart. If the country is missing or the rate isn’t configured, we fall back to through .
Persisting tax_country: updateCartTaxCountry

To let clients change where taxes apply, we need a repository function that updates the cart’s tax_country. This function lives in src/lib/repositories/cartsRepo.ts and returns null when no row is updated (meaning the cart didn’t exist).

  • The query is parameterized and updates updated_at = now() so the database remains the source of truth for timestamps.
  • Returning null when rows[0] is missing is important: services can turn that into a 404 without needing extra queries.
  • The function returns a Cart without computed totals, which is consistent with repository layering: totals are computed in getCartById, not in update helpers.
  • Accepting countryCode: string | null keeps the function flexible for future behavior (like clearing the tax country).
Validating the Tax Country Payload

The tax update endpoint accepts JSON like:

Validation for this payload lives in src/lib/services/cartsService.ts. It expects an object with country_code, then normalizes and validates it using the shared country-code validator from the tax rates service.

  • The validator accepts unknown so it can safely handle any JSON input shape without crashing.
  • It specifically looks for country_code and passes it through validateCountryCode, which trims, uppercases, and enforces exactly two uppercase letters.
  • By reusing validateCountryCode, the codebase ensures country validation rules are consistent across tax endpoints and cart tax updates.
  • The result is a simple { ok, value/message } shape, which routes can use to return clean 400 validation responses.
Enforcing Cart Rules: setCartTaxCountryService

Setting the tax country is not just an update—it’s a business action with a key rule: you can only update open carts. This logic lives in src/lib/services/cartsService.ts, and it uses a shared guard helper (ensureCartOpen) to return proper error codes.

  • This helper turns two common checks into one reusable rule: missing carts become NOT_FOUND (404), non-open carts become CONFLICT (409).
  • Returning a ServiceResult keeps route handlers thin: they don’t re-implement these rules or statuses.
  • The rule is intentionally strict: once a cart is checked out (or abandoned), we no longer allow mutation like changing tax configuration.

Now here is the service function that applies that rule, persists the tax country, and returns a refreshed cart with updated totals:

  • The service starts by loading the cart and enforcing “open cart” rules via . That’s what produces the correct 404 vs 409 outcomes.
Route Handler: PATCH /api/carts/:id/tax

Finally, we expose this feature via a focused endpoint. The route lives at src/app/api/carts/[id]/tax/route.ts and follows the project’s standard structure: validate path params, parse JSON safely, validate payload, delegate to service, forward the service result.

  • const { id } = await context.params; is the codebase’s pattern for reading dynamic params because params is typed as a Promise.
  • isUUID(id) is a fast guardrail that ensures malformed IDs return a clean 400 VALIDATION_ERROR before any DB work happens.
  • parseJson(req) ensures invalid JSON becomes a controlled 400 error response rather than throwing and falling into a generic 500.
  • validateTaxCountryInput(...) enforces the expected payload shape and country code rules, returning another clean 400 when input is wrong.
Recap

In this lesson, you connected your tax configuration system to cart totals in a way that’s consistent, deterministic, and easy for clients to use:

  • Carts store tax_country, and getCartById computes totals using snapshot prices plus a tax rate resolved from configuration or DEFAULT_TAX_RATE_BPS.
  • The repository resolves tax rates with a safe fallback rule—no guessing—and updates tax_country with a parameterized UPDATE ... RETURNING *.
  • The service enforces cart state rules (must exist and be open), persists the tax country, then returns a refreshed cart so totals are immediately correct.
  • The PATCH /api/carts/:id/tax route stays thin: it validates input and forwards the service result using consistent success / error envelopes.

You’re now ready for the practices where you’ll implement and reinforce these exact pieces: payload validation, open-cart enforcement, repository update logic, tax rate fallback resolution, and the thin PATCH route handler.

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