Managing Order Transitions

Managing Order Transitions

Huge congrats for making it this far. 🎉 You’ve built a full “mini e-commerce backend” flow: carts → items → checkout → orders → retrieval. What’s left now is the part that makes orders feel “alive”: state transitions.

In this final lesson, you’ll implement the two most important “actions” on an order in this codebase:

  • Pay an order: POST /api/orders/:id/pay
  • Cancel an order: POST /api/orders/:id/cancel

You’ll see the same layering pattern you’ve used throughout the course:

  • Routes validate input and enforce HTTP method rules.
  • Services enforce business rules like “only pending orders can be paid.”
  • Repositories persist state changes with a safe UPDATE ... RETURNING ....

What “Order Transitions” Mean Here

Orders have a status field with a small set of allowed values:

  • pending (created at checkout)
  • paid
  • shipped
  • cancelled

This project intentionally uses action routes (/pay, /cancel) instead of a generic “update status” endpoint. That’s because transitions are not “free-form edits”—they’re business actions with rules:

  • Paying is only valid from pending.
  • Canceling is blocked if the order is already shipped or already cancelled.

Those rules belong in the service layer, so every caller follows the same policy.

Repository: Persisting a Status Change With setOrderStatus

Learners will implement setOrderStatus(id, status) in src/lib/repositories/ordersRepo.ts.

The requirements are very specific:

  • Run an update:

    • UPDATE orders SET status = $1, updated_at = now() WHERE id = $2 RETURNING *
  • If no row is returned, return null (so the service can turn it into a 404)

  • If a row is returned, map it with mapOrderRow(...) so the rest of the app sees domain data

Here’s what that looks like in this repo style:

TypeScript
// src/lib/repositories/ordersRepo.ts
export async function setOrderStatus(
  id: string,
  status: Order["status"],
): Promise<Order | null> {
  const rows = await query<OrderRow>(
    `UPDATE orders SET status = $1, updated_at = now() WHERE id = $2 RETURNING *`,
    [status, id],
  );

  return rows[0] ? mapOrderRow(rows[0]) : null;
}
  • RETURNING * is doing a lot of work for you: you don’t need to perform a second query to re-fetch the updated order, which keeps transitions fast and consistent.
  • The rows[0] ? ... : null shape is a deliberate contract: repositories don’t decide HTTP status codes; they simply report “updated” vs “not found.”
  • Mapping via mapOrderRow keeps the “no raw DB rows beyond the repo” rule intact. Services/routes never deal with OrderRow directly.
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