Primitive spec
Elliot
The logistics primitive. Elliot moves goods from A to B across modes — parcel carriers, your own last-mile fleet, and recurring B2B freight routes — behind one Shipment model and one mode-agnostic carrier-driver contract. Adding a carrier or a mode is a new driver class, not a new system.
CarrierTender lifecycle. Signature-verified tracking webhooks ingest carrier status and stream it to Palver, a Hober order.closed auto-creates a draft shipment, and an X12 EDI layer connects freight carriers that run on EDI rather than REST. A delivery can also gate dispatch on payment through the Daneel needs ledger (payment-to-dispatch). Still phase-2: real geocoding / route optimization, ZPL labels, customs forms, and DoorDash/Uber webhook tracking.
What it owns
Elliot owns the movement: the shipment, its packages, the rate quotes, the bought label, and — for owned vehicles — the route stops and their progress. One Shipment aggregate spans all three modes; a single carrier-driver contract (quote / book / cancel) covers a parcel carrier, your own van, and a freight lane alike.
Elliot does not own the order that triggers a shipment (that is Hober), the recipient's identity (Terminus), the vehicle as a bookable resource (Trantor), the shipping charge on the ledger (Mallow), or the tracking notification (Speaker). It carries opaque references to them and stays import-free.
Concepts
- Shipment
- The universal movement aggregate. Carries a
carrierType(EASYPOST,SHIPPO,UPS,FEDEX,DOORDASH_DRIVE,UBER_DIRECT,FREIGHT_BROKER,OWN_FLEET, plus aMOCKtest driver), from/to address snapshots, anidempotencyKey(replay-safe quoting), and opaque refs to a Hober order and a Terminus recipient. Status follows the mode: parcel runsDRAFT → RATED → PURCHASED; own-fleet runsASSIGNED → EN_ROUTE_PICKUP → PICKED_UP → EN_ROUTE_DROPOFF → DELIVERED; webhooks driveIN_TRANSIT/DELIVERED/EXCEPTION. - Parcel, Rate & Label
- A Shipment holds one-or-more
Parcels (weight + dimensions + declared value). A quote returns immutableRatelines (carrier, service,amountCents, est. delivery days); buying the chosen rate produces aLabel— the permanent record of the purchase with atrackingCodeand a printable artifact (PDF/PNG/ZPL). - Carrier-driver seam
- Every mode maps to one contract:
CarrierDriverInterfacewithquote/book/cancel. Parcel-only concepts (label URL, format, tracking code) are nullable fields on the result, not method names — so an own-fleet driver simply returns null for them. Drivers auto-register by tag and resolve bycarrierType. Eight ship today, each making real calls to its carrier API:EasyPostandShippo(multi-carrier parcel), directUPSandFedEx,DoorDash DriveandUber Direct(on-demand last mile), a genericFreightBroker, plusOwnFleetand aMockfor tests. A new carrier is a new driver class, never a core change. - Carrier configuration
CarrierConfigurationis a tenant's account with an upstream carrier — type, active flag, apriorityfor default routing, and asettingsblob whose credentials are encrypted at the Doctrine boundary. The Shipment denormalizescarrierTypeso a deleted config never orphans history.- Own-fleet: drivers, stops & zones
- For the
OWN_FLEETmode, aDriveris an operational projection over a Terminus Person and a Trantor vehicle resource. Booking creates pickup/dropoffStops sequenced by aRoutePlanner(a naiveManualRoutePlannertoday; matrix / 2-opt deferred), and the driver advances each stop. A quote is gated on serviceability:Zonepolygons (SERVICE_AREA/SURCHARGE_BAND/EXCLUSION) are evaluated by a pure-PHP ray-castZoneResolver— no PostGIS. - Freight: templates, runs, loads & tendering
- Recurring B2B distribution. A
RouteTemplatecarries an RFC 5545rruleand ordered stops; a generator materializes oneRouteRunper occurrence (idempotent on tenant + template + service date). Shipments consolidate onto aLoadwith advisory capacity tracking across weight, volume, and pallets. For LTL/FTL, aCarrierTendershops every active broker config for a shipment, then awards and tenders one rate under a lock —PENDING → TENDERED → ACCEPTED(the broker's PRO/BOL lands on accept), withREJECTEDandCANCELLEDbranches. - Freight EDI (X12)
- Carriers that run on EDI rather than REST map onto the same tender lifecycle through a
CarrierType::EDIdriver — the driver registry made it additive, no core rework. A pure-PHP X12 codec (ISA/GS/ST…SE/GE/IEA, version 00401) encodes a204Load Tender and997Functional Ack and decodes inbound990(tender response),214(status), and997. Tendering is async:awardAndTenderemits a 204 and parks the tender inTENDEREDuntil an inbound 990 resolves it (accept →ACCEPTED+ shipmentPURCHASED). Inbound documents land at a shared-secret-gated endpoint; the transport seam (EdiTransportInterface) defaults to a durable spool, with AS2 / SFTP a drop-in. - Tracking & live status
- Carriers call back to a per-config webhook (
POST /elliot/v1/webhooks/{carrierConfigurationId}); the driver verifies the carrier's HMAC signature, parses the events, and a forward-only gate advances theShipmentstatus. Each change writes an audit event in the same transaction, so it streams to Palver's per-shipment and tenant channels for a live tracking board. - From a Hober order
- When a Hober order closes, an outbox subscriber auto-creates a draft shipment — ship-to from the order's delivery address, one parcel per billable line using the Hardin variant's ship weight and dimensions. If the tenant has a default carrier configured and the data is complete, the shipment lands
RATEDwith live rates; otherwise it waits as aDRAFTfor an operator to finish. Idempotent on the order id. - Place & geocoding
- A
Placeis a reusable geocoded point (address + lat/lng +geocodeStatus). AGeocoderInterfaceresolves addresses to coordinates; the defaultNoOpGeocoderleaves pointsMANUALuntil a real provider (Google / Mapbox / HERE) is wired. Places are the shared geography that own-fleet zones and Seldon field-service both read.
API surface
All endpoints are versioned under /elliot/v1/, return RFC 7807 problem details on error, and read tenantId from the bearer token. State-flipping paths (buy label, dispatch, stop transitions, run advance) take pessimistic locks, mirroring Payments' refund discipline.
Quick reference
| Method | Path | Purpose |
|---|---|---|
| POST / GET | /elliot/v1/shipments | Create a shipment and quote carriers (returns rates), or list shipments. |
| POST | /elliot/v1/shipments/{id}/labels | Buy a label against the selected rate (parcel). |
| POST | /elliot/v1/shipments/{id}/dispatch | Book an own-fleet movement — creates pickup/dropoff stops, flips to ASSIGNED. |
| PATCH | /elliot/v1/shipments/{id}/stops/{stopId} | Advance a stop and roll its status up onto the shipment. |
| POST / GET | /elliot/v1/drivers | Manage own-fleet drivers (status, assigned vehicle). |
| POST / GET | /elliot/v1/carrier-configurations | Manage tenant carrier accounts (encrypted credentials). |
| POST / GET | /elliot/v1/zones | Manage service-area / surcharge / exclusion polygons. |
| POST / GET | /elliot/v1/route-templates | Define recurring freight routes (RRULE + ordered stops). |
| GET / PATCH | /elliot/v1/route-runs/{id} | Read a materialized route run; advance its status under a lock. |
| POST | /elliot/v1/loads/{id}/shipments | Consolidate a shipment onto a load; returns an advisory capacity report. |
| POST | /elliot/v1/freight/shipments | Create a freight shipment and rate-shop every active broker. |
| POST / GET | /elliot/v1/shipments/{id}/tenders | Award + tender a freight rate to its broker, or list a shipment's tenders. |
| POST | /elliot/v1/edi/inbound | Inbound X12 freight documents (990 / 214 / 997) — shared-secret gated, partner + tenant resolved from the ISA ids. |
| POST | /elliot/v1/webhooks/{carrierConfigurationId} | Carrier tracking ingress — HMAC-verified, advances shipment status, streams to Palver. |
Example: quote, then buy a label
Create a parcel shipment; the carrier driver returns rate options:
POST /elliot/v1/shipments
Content-Type: application/json
Authorization: Bearer <token>
{
"carrierType": "MOCK",
"fromAddress": { "name": "Main Store", "street1": "1 Market St", "city": "Austin", "region": "TX", "postalCode": "78701", "countryCode": "US" },
"toAddress": { "name": "A. Customer", "street1": "500 Oak St", "city": "Dallas", "region": "TX", "postalCode": "75201", "countryCode": "US" },
"parcels": [ { "weightGrams": 1200, "lengthMm": 300, "widthMm": 200, "heightMm": 100 } ]
}
→ 201 Created shp_01JB… status: RATED
{
"rates": [
{ "id": "rt_01JB…", "carrier": "USPS", "service": "Priority", "amountCents": 899, "estDeliveryDays": 2 },
{ "id": "rt_01JC…", "carrier": "UPS", "service": "Ground", "amountCents": 1149, "estDeliveryDays": 3 }
]
}
Buy the chosen rate to get a tracking code and a printable label:
POST /elliot/v1/shipments/shp_01JB…/labels
Content-Type: application/json
Authorization: Bearer <token>
{ "rateId": "rt_01JB…", "labelFormat": "PDF" }
→ 201 Created status: PURCHASED
{
"label": { "trackingCode": "9400…", "labelUrl": "https://…/label.pdf", "amountCents": 899 }
}
How it fits with the rest
flowchart LR
Ho[Hober order] -. deferred .-> El(Elliot)
Ho[Hober order.closed] -- auto-shipment --> El(Elliot)
El -- quote / book / cancel --> Drv[Carrier driver]
Drv --> Ext[EasyPost / Shippo / UPS / FedEx]
Drv --> OnD[DoorDash / Uber Direct]
Drv --> Fr[Freight broker]
Drv --> Own[Own-fleet]
Carrier[Carrier webhooks] -- status --> El
El -- live status --> Pv[Palver]
El -. vehicle ref .-> T[Trantor]
El -. recipient .-> Te[Terminus]
El -- Place + ZoneResolver --> S[Seldon field-service]
Elliot follows the platform's opaque-reference contract in every direction. A Hober order close now auto-creates a draft Shipment; Trantor owns the vehicle as a resource; Terminus owns the recipient; and carrier tracking webhooks stream status to Palver for a live board. Seldon field-service reads Elliot's Place and ZoneResolver to gate appointments to a service area — and, in a later phase, to turn route drive-times into the travel buffer it reserves around a technician's job.