AstroBaaS

Orders & fulfilment

Shipments with tracking and carrier hooks

Free — GPL coresize Lplanned, not built

Generated from docs/plan/core/shipments-with-tracking-and-carrier-hooks/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.

Introduces Shipment entity that tracks parcels through fulfillment. Multiple shipments per order allowed. Each shipment has tracking number, carrier, status, and line items. Webhooks fire at each fulfillment stage.

The problem

I ship many orders in multiple parcels per order but have no way to track individual parcels. No shipment entity, no partial shipping, no line-item visibility in fulfillment. Staff manually notes tracking numbers in comments.

What it does

  • Shipment entity: shipment_id, order_id, status (pending, packed, shipped, delivered, cancelled), carrier, tracking_number, label_id, shipped_at, delivered_at, created_at
  • Line items in shipment: shipment_line_items table linking order_line_item_id to shipment_id with quantity
  • Partial shipments: order with 3 line items can ship as: Shipment 1 (items 1+2), Shipment 2 (item 3)
  • Pick list per shipment: staff view shows line items for this shipment, qty to pick, bin location
  • Pack workflow: staff pack items into box, enter tracking (or auto-generate label), click ‘Mark as Shipped’
  • Webhooks: order.shipment_created, order.shipment_packed, order.shipment_shipped (with tracking), order.shipment_delivered
  • Admin shipments view: list all shipments, filter by status, carrier, date; bulk actions (re-ship, cancel)
  • Shipment detail: shows line items, carrier, tracking link, current status, history of status changes
  • Multi-warehouse prep: shipment can store warehouse_location for future MSI integration
  • Return shipments: returned items create new Shipment with status ‘return_in_transit’ for bookkeeping

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.

  • Multi-warehouse fulfillment orchestration: out of scope—one warehouse per shipment, no auto-routing. Reason: deferred to multi-warehouse feature.
  • Split shipment due to partial availability: out of scope—merchant manually creates shipments. Reason: backorder logic is complex; handled by order workflow.
  • Shipment weight/dimensions aggregation: out of scope—weight from order, not summed from items. Reason: items may be bundled; weight is order total.
  • Packaging slip generation: out of scope—packing list is pick list. Reason: can be added to shipping-label feature.
  • Shipment cost attribution (shipping fee per shipment): out of scope—shipping fee stays on Order. Reason: split billing is deferred to order accounting.
  • Split payment for multi-shipment orders: out of scope. Reason: payment model assumes one payment per order.

Data model

Migration: new Shipment table/collection (shipment_id, order_id, status, carrier, tracking_number, label_id, shipped_at, delivered_at, created_at). New ShipmentLineItem table/collection (id, shipment_id, order_line_item_id, quantity, created_at). Add shipment_id to Order (nullable, null if no shipments yet).

API

  • POST /orders/:id/shipments (staff, body: {line_items: [{order_line_item_id, quantity}], carrier?, warehouse_location?}) → {shipment_id}
  • GET /orders/:id/shipments → [{shipment_id, status, carrier, tracking_number, line_items: […], created_at}]
  • GET /shipments/:id → {shipment_id, order_id, status, carrier, tracking_number, line_items: […], shipped_at, delivered_at}
  • PUT /shipments/:id/status (staff, body: {status: ‘packed’ | ‘shipped’ | ‘cancelled’}) → {shipment_id, status, shipped_at?}
  • PUT /shipments/:id/tracking (staff, body: {tracking_number, carrier, label_id?}) → {shipment_id}
  • POST /shipments/:id/cancel (staff) → {shipment_id, status: ‘cancelled’}

Every route added here must also appear in src/pages/openapi.json.ts — a test fails the build if it does not.

Admin

Shipments section in order detail lists all shipments with status, carrier, tracking number (clickable). Create Shipment button opens modal to select line items and qty, set carrier. Pack view shows per-shipment pick list. Staff scan items, click Shipped, enter tracking (or auto-generate label).

The seam — why this is core

Core owns Shipment entity, line-item allocation, and status workflow. Paid pack owns: multi-warehouse routing, backorder splitting, shipment cost allocation, return shipment orchestration. Why: core provides honest single-warehouse shipments with basic line tracking; multi-location complexity is paid.

Daily operational wound, not a roadmap nicety. Must ship before any fulfillment claim.

Dependencies

  • orders (order must exist; order_line_items must be linkable)
  • shipping-label-generation (label_id references shipping label)
  • tracking-number-sync (tracking_number synced from carrier)
  • click-and-collect-in-store-pickup (pickup orders use shipment status for ‘ready’ state)

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.

  • Order with 3 line items creates Shipment 1 with items 1+2 (qty 2+1), Shipment 2 with item 3 (qty 1)
  • GET /orders/123/shipments returns [{shipment_id, status: ‘pending’, line_items: […]}]
  • POST /orders/123/shipments creates shipment; total qty in shipment = sum of line_item quantities
  • PUT /shipments/ship_456/status to ‘shipped’ with tracking_number=‘abc123’ stores both; shipped_at set
  • Webhook order.shipment_shipped fired with shipment_id, tracking_number, carrier
  • Shipment detail shows line items with original product name, price, qty in shipment
  • Deleting order with shipments does not cascade-delete shipments; shipments are kept for audit
  • PUT /shipments/ship_456/cancel sets status=‘cancelled’; inventory is NOT reverted (merchant’s concern)
  • Two shipments for same order: both can have different carriers (e.g., one UPS, one DHL)
  • Shipment with status ‘shipped’ cannot change tracking; must cancel and create new shipment

Risks

Schema: line-item linkage couples shipment to order_line_item structure; if line items are refunded, shipment line references stale item. Inventory: creating shipment does not decrement inventory; decrement happens on shipped status (or pick). Status flow: if status is ‘shipped’ but label never generated, tracking is missing; must validate before allowing shipped status. Hooks: if shipment_shipped webhook fails, order is marked shipped but external system doesn’t know.

Commercial context

Suggested priceCore
Rival anchorMagento Open Source: bundles UPS, USPS, DHL, FedEx free with live rates and PDF labels.

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 ../../AI-GUIDE.md for how to work on this repository.