Skip to content

E-Commerce Order

This example demonstrates a complete e-commerce order flow with shopping cart, checkout, payment, and inventory management.

Single Machine Version

A simplified version with one machine handling the order:

flow
machine: @order

scenario: add to cart

  given:
    @customer is logged in
    cart is empty

  on :add_item from @customer (api)
    $items: array adds $item
    $total: number increases by $item.price

    expect:
      = $items contains $item

  on :remove_item from @customer (api)
    $items removes $item
    $total decreases by $item.price

  expect:
    = $total is correct

scenario: successful checkout

  given:
    @customer is logged in
    cart contains:
      | product | price |
      | Laptop  | 1200  |
    $total: number is 1200

  on :checkout from @customer (api)
    ? cart is valid
      $order_id becomes uuid()
      emit :payment_request to @payment with:
        | field    | value     | validation                       |
        | order_id | $order_id | required, string, valid uuid     |
        | amount   | $total    | required, number, greater than 0 |
      order moves to #awaiting_payment

      reply 201 created with:
        | id     | $order_id     |
        | status | current_state |
        | total  | $total        |

    otherwise
      reply 400 bad request with:
        | error   | "CART_INVALID"           |
        | message | "Cart validation failed" |

    expect:
      = order is in #awaiting_payment
      = reply status is 201
      = reply.id equals $order_id

  on :validation_failed
    reply 400 with:
      | errors | $errors |

  on :payment_success from @payment
    order moves to #paid
    emit async :reserve_stock to @inventory

    expect:
      = order is in #paid

  on :stock_reserved from @inventory
    order moves to #fulfilled
    emit :order_confirmed to @customer
    send confirmation email

  expect:
    = order is in #fulfilled
    = @customer received :order_confirmed

scenario: out of stock checkout

  given:
    @customer is logged in
    cart contains:
      | product | price |
      | Laptop  | 1200  |
    $total: number is 1200

  on :checkout from @customer (api)
    ? cart is valid
      emit :payment_request to @payment with:
        | field    | value     |
        | order_id | $order_id |
        | amount   | $total    |
      order moves to #awaiting_payment

  on :payment_success from @payment
    order moves to #paid
    emit async :reserve_stock to @inventory

  on :out_of_stock from @inventory
    emit :refund_request to @payment
    order moves to #cancelled
    emit :order_cancelled to @customer

  expect:
    = order is in #cancelled
    = @customer received :order_cancelled

scenario: payment failed checkout

  given:
    @customer is logged in
    cart contains:
      | product | price |
      | Laptop  | 1200  |
    $total: number is 1200

  on :checkout from @customer (api)
    ? cart is valid
      emit :payment_request to @payment with:
        | field    | value     |
        | order_id | $order_id |
        | amount   | $total    |
      order moves to #awaiting_payment

  on :payment_failed from @payment
    order moves to #payment_failed
    emit :payment_error to @customer with:
      | field | value          |
      | error | $error_message |

  expect:
    = order is in #payment_failed
    = @customer received :payment_error

scenario: empty cart checkout

  given:
    @customer is logged in
    cart is empty

  on :checkout from @customer (api)
    ? cart is empty
      reply 400 bad request with:
        | error   | "CART_EMPTY"               |
        | message | "Cart is empty"            |

  expect:
    = reply status is 400
    = reply.error equals "CART_EMPTY"

Multi-Machine System

A production-ready version with separate machines for each domain:

flow
system: e-commerce checkout

machine: @order

  scenario: order lifecycle

    given:
      @customer is logged in
      cart has items
      $total: number is 1200

    on :checkout from @customer (api)
      ? cart is valid
        emit :payment_request to @payment with:
          | field    | value     | validation                       |
          | order_id | $order_id | required, string, valid uuid     |
          | amount   | $total    | required, number, greater than 0 |
        order moves to #awaiting_payment

      expect:
        = order is in #awaiting_payment

    on :validation_failed
      reply 400 with:
        | errors | $errors |

    on :payment_success from @payment
      order moves to #paid
      emit async :reserve_stock to @inventory with:
        | field    | value     |
        | order_id | $order_id |
        | items    | $items    |

      expect:
        = order is in #paid

    on :payment_failed from @payment
      order moves to #payment_failed
      emit :payment_error to @customer

    on :stock_reserved from @inventory
      order moves to #fulfilled
      emit :order_confirmed to @customer

      expect:
        = order is in #fulfilled
        = @customer received :order_confirmed

    on :out_of_stock from @inventory
      emit :refund_request to @payment
      order moves to #cancelled
      emit :order_cancelled to @customer

      expect:
        = order is in #cancelled
        = @customer received :order_cancelled

machine: @payment

  scenario: payment processing

    on :payment_request from @order
      process card with $amount
      ? card is valid
        $transaction_id: string becomes "TXN-12345"
        emit :payment_success to @order with:
          | field          | value           |
          | transaction_id | $transaction_id |
      ?
        $error_message: string becomes "Card declined"
        emit :payment_failed to @order with:
          | field | value          |
          | error | $error_message |

    on :refund_request from @order
      process refund
      emit :refund_complete to @order

machine: @inventory

  scenario: stock management

    on :reserve_stock from @order
      ? stock is available
        reserve items
        emit :stock_reserved to @order
      ?
        emit :out_of_stock to @order

State Diagram

@order#empty:add_item#cart:clear:checkout#awaiting_payment:payment_success:payment_failed#paid#payment_failed:stock_reserved:out_of_stock#fulfilled#cancelled

Lane Diagram

@customer@order@payment@inventory:checkoutvalidates cart:payment_requestprocesses card:payment_success:reserve_stockchecks stock:stock_reserved:order_confirmed

Testing Scenarios

Successful Checkout

flow
scenario: successful checkout

  given:
    @customer is logged in
    cart contains:
      | product | price |
      | Laptop  | 1200  |
    $total: number is 1200

  on :checkout from @customer (api)
    order moves to #awaiting_payment

  on :payment_success from @payment
    order moves to #paid

  on :stock_reserved from @inventory
    order moves to #fulfilled

  expect:
    = order is in #fulfilled
    = @customer received :order_confirmed

Payment Failure

flow
scenario: payment fails

  given:
    @customer is logged in
    cart has items
    payment will fail

  on :checkout from @customer (api)
    order moves to #awaiting_payment

  on :payment_failed from @payment
    order moves to #payment_failed

  expect:
    = order is in #payment_failed
    = @customer received :payment_error

Out of Stock

flow
scenario: out of stock after payment

  given:
    @customer is logged in
    cart has items
    inventory is empty

  on :checkout from @customer (api)
    order moves to #awaiting_payment

  on :payment_success from @payment
    order moves to #paid

  on :out_of_stock from @inventory
    order moves to #cancelled

  expect:
    = order is in #cancelled
    = :refund_request was emitted to @payment
    = @customer received :order_cancelled

Refund Flow

flow
scenario: process refund

  given:
    order is in #fulfilled
    $days_since_purchase: number is 5

  on :request_refund from @customer (api)

    ? days since purchase < 7
      ? order total < 100
        instant refund to wallet
        order moves to #refunded
      ? order total >= 100
        process credit card refund
        order moves to #refund_pending

    ? days since purchase < 30
      ? customer has premium status
        partial refund (75%)
        order moves to #partially_refunded
      ?
        partial refund (50%)
        order moves to #partially_refunded

    ? days since purchase < 90
      store credit only
      $credit_amount becomes $order_total
      emit :credit_issued to @customer
      order moves to #credit_issued

    otherwise
      refund denied
      emit :refund_rejected to @customer
        with:
          | reason | "Refund period expired" |

  on :refund_processed from @payment
    emit :refund_confirmation to @customer
    order moves to #refunded

  expect:
    = @customer received notification

Released under the MIT License.