Skip to content

Supply Chain Context

The Supply Chain context manages vendor relationships and bulk ordering for the restaurant chain. It coordinates with the Inventory context to ensure adequate stock across locations.

Purpose

A 500-location restaurant chain can't have each location ordering independently — bulk purchasing saves money and ensures consistency. The Supply Chain context handles the procurement lifecycle from order creation through approval, shipping, receipt, and issue reporting.

Interview Connection

From the Head Chef's interview:

"I source local ingredients required to prepare our menu items."

The Head Chef works at the corporate level to standardize ingredients and manage supplier relationships. The Supply Chain context formalizes this process with tracked purchase orders.

Types

type PurchaseOrderId is Id(PurchaseOrder)

type VendorId is UUID

Note the PoDisputed status — if a shipment has quality issues, the purchase order can be disputed rather than simply accepted.

Entity: PurchaseOrder

The PurchaseOrder entity has a 5-command lifecycle:

event-sourced entity PurchaseOrder as flow is {

  // An event-sourced entity OWNS the commands and events that change it, so
  // they are declared INSIDE it, and every command names the event it yields.
  command CreateBulkOrder yields event BulkOrderCreated is { purchaseOrderId: PurchaseOrderId }
  command ApproveOrder yields event OrderApproved is { purchaseOrderId: PurchaseOrderId }

  event BulkOrderCreated is { purchaseOrderId: PurchaseOrderId }
  event OrderApproved is { purchaseOrderId: PurchaseOrderId }
  event ApproveOrderRejected is { purchaseOrderId: PurchaseOrderId, rejectionReason: String(1,500) }

  record PurchaseOrderData is { purchaseOrderId: PurchaseOrderId }

  // Lifecycle phases are named STATES, not a status field: each state
  // declares the commands it accepts, so the compiler knows the machine.
  initial state ActivePurchaseOrder of record PurchaseOrderData is {
    handler ActivePurchaseOrderHandler is {
      on cmd: command ApproveOrder is {
        yield event OrderApproved(purchaseOrderId = cmd.purchaseOrderId)
      }
      // `set` and `morph` may appear ONLY in an `on event` clause here:
      // replay has to re-apply exactly the same change.
      on evt: event OrderApproved is {
        morph entity PurchaseOrder to state Approved
          with record PurchaseOrderData(purchaseOrderId = evt.purchaseOrderId)
      }
    }
  }

  state Approved of record PurchaseOrderData is {
    handler ApprovedHandler is {
      // A command this state does not accept is refused -- and the refusal
      // is PUBLISHED before it is raised, so the attempt is recorded.
      on cmd: command ApproveOrder is {
        send event ApproveOrderRejected(purchaseOrderId = cmd.purchaseOrderId,
          rejectionReason = "PurchaseOrder does not accept ApproveOrder in this state")
          to outlet PurchaseOrderEvents
        error "PurchaseOrder does not accept ApproveOrder in this state"
      }
    }
  }

  // A processor receives on its OWN inlet and publishes on its OWN outlet,
  // and a portlet's type must ADMIT everything that travels on it.
  type PurchaseOrderCommand is CreateBulkOrder | ApproveOrder
  type PurchaseOrderEvent is BulkOrderCreated | OrderApproved | ApproveOrderRejected

  inlet PurchaseOrderCommands is type PurchaseOrderCommand
  outlet PurchaseOrderEvents is type PurchaseOrderEvent
}

The lifecycle: Create → Approve → Ship → Receive → (optional) Report Issue.

The ReportIssue command can trigger a follow-up that changes the order status to PoDisputed, initiating a resolution process with the vendor.

Repository

repository PurchaseOrderRepository as flow is {
  inlet PurchaseOrderRepositoryFromPurchaseOrder is command PersistOrderApproved
  outlet PurchaseOrderRepositoryResponses is result PurchaseOrderResult

  // A repository answers with a RESULT, never an event.
  result PurchaseOrderResult is { found: Boolean }

  record StoredPurchaseOrder is { purchaseOrderId: PurchaseOrderId }

  // A repository that answers queries and declares NO index at all is a
  // sequential scan by construction, and draws a warning saying so.
  schema PurchaseOrderSchema is relational
    of rows as record StoredPurchaseOrder
      index on field StoredPurchaseOrder.purchaseOrderId

  command PersistOrderApproved is { purchaseOrderId: PurchaseOrderId }

  handler PurchaseOrderPersistence is {
    on command PersistOrderApproved is {
      do "update the stored purchaseOrder row for this purchaseOrderId"
    }
    // An inlet admitting an alternation needs a clause that receives it.
    // Handling each member is not enough -- say what ARRIVING means.
    on other is {
      do "persist whatever else arrives on this inlet"
    }
  }
}

The index on vendorId supports vendor-centric views — how many orders are outstanding with a specific vendor, what's the order history, etc.

Design Decisions

Why no adaptor to Inventory? When a shipment is received at a restaurant location, the Inventory context's ReceiveStock command is called directly by the receiving clerk. The supply chain tracks the corporate-level purchase order, while inventory tracks the location-level stock. These are different levels of abstraction that don't need tight coupling.

Why many OrderLineItem? Bulk purchase orders typically contain multiple items — different cuts of meat, sauces, packaging, etc. The many keyword models a collection of line items within a single order.

Source