accelerando.wiki ↗ app ↗ github

Cart & Order Lifecycle

Every state transition is a named refuse. Every inventory move is a git-log entry with a reason.

The state-machine slice of the ecommerce vertical. Where Catalog owns the merchant's SKUs, Orders owns the *funnel* — from shopper intent (Cart) through commitment (Order) through fulfillment through delivery. Payment state comes from Stripe (Slice M's boundary); the state machine here honors that boundary without depending on it.


The three views

Order dashboard — funnel counts + revenue by state, plus the abandoned-cart signal. Seven state cards (Awaiting payment / Paid / Fulfilled / Delivered / Completed / Cancelled / Returned) with count and revenue each, plus an *Open carts* and *Abandoned carts* pair below.

Orders — full queue across the funnel with status filter. Inline transition buttons per row: *Mark paid* on awaiting_payment, *Fulfill* on paid, *Delivered* on fulfilled, *Cancel* on any pre-fulfilled state, *Return* on delivered or completed. Refuse messages surface the exact rule name.

Carts — every open cart with last-activity age. Abandoned filter for the > 24h no-activity set. Click a cart to open the drawer, add lines from the variants picker, and place the order.


The state machine

Cart (open)
    │  place_order → decrement inventory, snapshot lines
    ▼
awaiting_payment
    │  mark_order_paid (Stripe webhook in Slice M)
    ▼
paid
    │  fulfill_order (tracking_number optional)
    ▼
fulfilled
    │  mark_order_delivered
    ▼
delivered
    │  auto-complete after N days (Slice N)   or   return_order → restore inventory
    ▼
completed                                          returned

Cancel is available on awaiting_payment or paid only. Once an order is fulfilled it can't be cancelled — it can be *returned*, but not cancelled. Both cancel and return restore inventory by writing an InventoryAdjustment with reason=return and a reference to the order id.


Inventory reservation: place-time, not cart-time

Add-to-cart validates against available stock but *doesn't* reserve. This is the Shopify default and the least-surprising path for merchants: many carts sit half-full for days. Reserving would starve stock.

Inventory decrements happen at place_order:

  1. Every line is re-validated against current stock. If any variant has gone out of stock since add-to-cart, the entire place_order refuses with insufficient_stock_at_checkout.
  2. If validation passes, decrement every variant by its line quantity and write an InventoryAdjustment per variant with reason=sale and reference=<order_id>.
  3. Snapshot every line into an EcomOrderLine — capturing product name, variant name, SKU, unit price at the moment of purchase. Future product edits do not silently rewrite the receipt.

The receipt is immutable. Merchant renames the product a week later? The order still shows the name it had at checkout. That's what audit-log-as-DB gets you.


The four PRIORITY 100 refuses

| Refuse | When | |---|---| | insufficient_stock | Add-to-cart with quantity that exceeds available stock (including what's already in the cart) | | cart_is_empty | place_order on a cart with no lines | | insufficient_stock_at_checkout | place_order and inventory has drained between add-to-cart and now | | order_already_fulfilled_cannot_cancel_use_return | cancel_order on an already-shipped order |

Plus every state-machine transition refuses with its own named rule (order_not_awaiting_payment, order_not_paid, order_not_fulfilled, order_not_delivered) when called out of sequence. No silent state jumps.


What's wired

| Entity | Purpose | |---|---| | Cart | Shopper intent with subtotal + last_activity_at (feeds abandonment) | | CartLine | Line items in a cart | | EcomOrder | Placed order with the full state machine | | EcomOrderLine | Snapshotted line item — captures product/variant state at purchase time |

Tools (13): list_carts, create_cart, add_to_cart, update_cart_line, remove_from_cart, place_order, list_orders, mark_order_paid, fulfill_order, mark_order_delivered, cancel_order, return_order, order_dashboard.


The chargeback-defense payoff

This is the demo moment. A customer disputes a charge:

The complete evidence bundle for a chargeback dispute is *one filesystem walk*, not a subpoena to Shopify.


What's NOT in this slice