Orders & fulfilment
Fulfillment Status
Generated from docs/plan/core/fulfillment-status/PLAN.md in the project's planning notes, which are not part of the public repository. Nothing described below is implemented — it is the written plan for it.
An idea, not a commitment. It has a written plan but no place in the build order and no date. What is committed.
Merchants can’t mark orders as fulfilled, so they don’t know which orders are ready to ship or already shipped. This feature adds a fulfillment workflow state to orders.
The problem
I don’t have a way to say ‘this order is ready to ship’ or ‘this order is fulfilled’. I manually track readiness in my head or notes. I need a status that shows fulfillment progress.
What it does
- Add fulfillment_status enum to orders: unfulfilled, partially_fulfilled, fulfilled, cancelled
- Auto-update fulfillment_status based on shipments: if all items shipped, status → fulfilled
- Allow manual status override: staff can mark order as fulfilled even if no shipment created
- Display fulfillment status as badge in order list and detail (different colors: gray unfulfilled, yellow partial, green fulfilled)
- Filter order list by fulfillment status (e.g., show only unfulfilled orders)
- Show fulfillment % in order detail: (items_shipped / total_items) * 100
- Support bulk status change: select multiple orders and mark as fulfilled at once
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Fulfillment workflow automation (e.g., ‘when payment captured, auto-set status to ready’) — that is automation, separate feature
- Backorder or pre-order fulfillment states — that is inventory system, separate feature
- Fulfillment notifications to warehouse (e.g., print pick list) — that is warehouse system, separate feature
Data model
orders.fulfillment_status (enum: unfulfilled, partially_fulfilled, fulfilled, cancelled). orders.fulfillment_updated_at (timestamp). No migration if orders schema is extensible; otherwise migration to add fields.
API
- PATCH /api/admin/orders/:orderId/fulfillment-status — update fulfillment status
- GET /api/admin/orders?fulfillment_status=unfulfilled — filter orders by fulfillment status
- POST /api/admin/orders/bulk-action — bulk update fulfillment status
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Order list includes ‘Fulfillment’ column showing status badge. Order detail shows fulfillment status badge and % progress bar. Button ‘Mark as Fulfilled’ changes status. Bulk actions sidebar: select multiple orders, dropdown ‘Bulk Action’ → ‘Mark Fulfilled’. Filter sidebar: checkbox ‘Unfulfilled’, ‘Partially Fulfilled’, ‘Fulfilled’.
The seam — why this is core
Core owns fulfillment status field, workflow, and UI. No paid seam — fulfillment tracking is infrastructure.
Core owns the interface + honest fulfillment tracker; order workflow is infrastructure, not a support commitment or credential.
Dependencies
- Existing orders collection (needs fulfillment_status field)
- shipment-management-tracking (for auto-updating status based on shipments)
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- Create order with 5 items and 1 shipment with 2 items; confirm fulfillment_status=‘partially_fulfilled’ and progress=40%
- Add second shipment with remaining 3 items; confirm fulfillment_status auto-updates to ‘fulfilled’
- Manually mark order as fulfilled; confirm fulfillment_status=‘fulfilled’ regardless of shipments
- Filter order list by fulfillment_status=‘unfulfilled’ and confirm only unfulfilled orders are shown
- Bulk-select 5 orders and mark as fulfilled; confirm all 5 orders update status
Risks
Auto-updating fulfillment_status based on shipments can create race conditions if shipment and fulfillment updates happen concurrently. Use database transactions to ensure consistency. Bulk status updates on large order sets (1000+) can timeout; implement async batch processing.
Commercial context
| Suggested price | free (core) |
| Rival anchor | Shopify: included; Magento: included |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and CONTRIBUTING.md for how to work on this repository.