Subsystem 23 — Shipping (Rate Shopping & Labels)
Purpose
The physical shipping-transaction layer for a self-fulfilled order: rate shopping (which
carriers/service levels can move this parcel and for how much), label purchase (buying the
one quoted rate and getting back a real printable label + tracking number), and tracking
status lookup. Distinct from subsystem 22 (@mercatus-liber/fulfillment), which routes who
produces/ships an order line (a POD provider vs. self-fulfillment) — this subsystem is how a
self-fulfilled order physically ships once an operator has it in hand. Before this subsystem,
every shop built on this framework had zero notion of rate shopping, label purchase, or
tracking at all — an operator’s only option was logging into a carrier or reseller site by
hand with no code in this repo aware of the transaction. Added from an explicit user ask: “by
default I’d use PirateShip but no API… for mine, I’d want full automation and I believe
Shippo provides that” — this subsystem ships both halves of that statement as real, working
code: a genuinely honest zero-infra default modeling PirateShip’s real manual workflow, and a
real, credential-gated Shippo adapter for full automation.
Depends on
@mercatus-liber/core only, for Money. Declares its own independent address/parcel shapes
(ShippingAddress, Parcel) rather than reusing checkout-orders’ ShippingInfo (which
carries only a single free-text address string) — rate shopping and label purchase need real
structured fields (city/state/postalCode/country), and this subsystem owns them
independently, the same “adapters/subsystems own their own shapes, never fork or reach into
another subsystem’s types” discipline fulfillment’s OrderLookup and internal-bi’s
OrderMetricsSource already follow.
Responsibilities
ShippingAdapter: the swappable per-provider contract (mirrors@mercatus-liber/payments’PaymentAdapterand@mercatus-liber/fulfillment’sFulfillmentAdaptershape — a plain structural interface, no base class) —getRates(input),buyLabel(rateId),getTrackingStatus(trackingNumber).RateQuoteResult/LabelPurchaseResult: deliberate discriminated unions ({ available: true, rates }/{ available: false, reason, instructionsUrl }, and thepurchasedequivalent), not a bare array or a thrown error. An adapter that has no real rates to offer (the manual/PirateShip default, always; any real adapter, occasionally — e.g. an unsupported route) must be able to say so honestly: an empty array would be ambiguous (“zero carriers service this route” vs. “this adapter can’t quote rates at all”) and carries no explanation or next step for the caller — exactly the kind of quiet, misleading half-truth this subsystem’s design rejects.TrackingStatus/TrackingStatusState: coarse tracking status ("unknown" | "in_transit" | "delivered" | "exception") with"unknown"a first-class, honest value, not an error — for adapters with no live tracking-events API to consult, this subsystem reports what it actually knows (nothing beyond “here’s where to look”) rather than fabricating a plausible-looking timeline.detectedCarrier/carrierTrackingUrllet an adapter with no live API hand back a real carrier tracking-lookup URL instead.createManualShippingAdapter(): the zero-infra default (packages/shipping) — see “The manual/PirateShip default” below.
The manual/PirateShip default (createManualShippingAdapter)
Modeling PirateShip’s real, current product, confirmed via research at build time (not assumed
from the design discussion): Pirate Ship does not publish or offer any official public API for
rate shopping, label purchase, or tracking — its site and support material describe only its
own web UI and CSV/e-commerce-platform order-import integrations (Shopify, WooCommerce, Etsy),
never a documented REST/GraphQL API. The only “API” surface found is an unofficial,
community-maintained wrapper reverse-engineered against PirateShip’s undocumented internal
browser endpoints, whose own README states outright it’s undocumented and correctness “can’t be
guaranteed.” Building this package’s default adapter against that would have been exactly the
kind of dishonest-looking automation this subsystem’s design rejects — it would look like a
real integration while resting on a foundation neither documented nor supported by Pirate Ship
itself. createManualShippingAdapter() is therefore a genuine, documented manual workflow,
not a stub or a fragile scrape-based integration pretending to be one:
getRates/buyLabelalways honestly report{ available: false, reason, instructionsUrl }/{ purchased: false, reason, instructionsUrl }pointing the operator at their own real PirateShip account (PIRATESHIP_URL = "https://www.pirateship.com") — never a fabricated empty-looking success, never a thrown error that gives the caller nothing to act on.getTrackingStatusis the one method this adapter can do something genuinely useful for without any API: it recognizes a tracking number’s carrier by its well-documented, stable format (UPS/FedEx/USPS/DHL regex patterns,detectCarrierFromTrackingNumber, exported for reuse) and returns that carrier’s own real public tracking-lookup URL.statusis always"unknown"andeventsalways[]— this adapter has no live data source — but the carrier link is real and actionable, not fabricated.- This adapter stays the permanent, always-registered fallback even now that a real Shippo
adapter exists, mirroring
fulfillment’s manual adapter staying the permanent self-fulfillment default even once real POD adapters exist. If PirateShip ever ships a real, documented, supported public API, that would justify a new@mercatus-liber/adapter-pirateshippackage built against it — this one stays as-is regardless.
The Shippo adapter (@mercatus-liber/adapter-shippo)
A real, deployed ShippingAdapter implementation wrapping Shippo’s real, current REST API,
grounded in docs.goshippo.com’s own documentation (confirmed 2026-09-09, not training-data
recall) — same sibling-package shape as adapter-shopify/adapter-clerk/adapter-printful/
adapter-printify. Raw fetch via an injectable ShippoHttpClient
(packages/adapter-shippo/src/http-client.ts), no new HTTP dependency — this repo’s other
third-party adapters all use raw fetch, not a client library, and this one follows the same
convention. Auth is Shippo’s real, confirmed Authorization: ShippoToken <token> scheme (not
Bearer).
getRates— creates a real, synchronous (async: false) Shippo Shipment viaPOST /shipments/and reads back its real, confirmedratesarray. Honestly reportsavailable: false(never a fabricated empty-looking success) when the Shipment didn’t reachstatus: "SUCCESS"or came back with zero rates (e.g. an unsupported route), folding Shippo’s ownmessagesinto the reason.buyLabel— purchases the previously-quotedrateIdvia a real, synchronousPOST /transactions/. Onlystatus: "SUCCESS"maps topurchased: true; every other real, documented status (WAITING/QUEUED/ERROR/REFUNDED/REFUNDPENDING/REFUNDREJECTED) honestly reportspurchased: false. ASUCCESStransaction missinglabel_url/tracking_number/rate.provider— structurally unreachable given Shippo’s own documented contract — throws rather than fabricating those fields, a guard against trusting an inconsistent real API response.getTrackingStatus— bridges this subsystem’s carrier-lessgetTrackingStatus(trackingNumber)contract onto Shippo’s real carrier-addressedGET /tracks/{carrier}/{tracking_number}endpoint by reusing@mercatus-liber/shipping’s owndetectCarrierFromTrackingNumber(the same heuristic the manual adapter uses) to pick Shippo’s own lowercase carrier token (toShippoCarrierToken,mapping.ts). When no carrier can be confidently detected, or Shippo has no record for the number (a real 404), this honestly falls back tostatus: "unknown"with no live API call fabricated — same honesty discipline as the manual adapter, just with a real API genuinely consulted first when possible.
One real, disclosed bridging gap (toShippoParcel, packages/adapter-shippo/src/mapping.ts):
Shippo’s real Parcel object requires length/width/height, but this subsystem’s own
Parcel type deliberately makes lengthIn/widthIn/heightIn optional (some carriers rate on
weight alone for small parcels). Rather than fabricate a plausible-looking default dimension
(the same discipline adapter-printful/adapter-printify’s resolveCatalogTarget follows by
throwing instead of inventing a mapping), toShippoParcel throws a clear, actionable error when
a dimension is missing — a real deployment must supply real package dimensions to get a real
Shippo quote.
Money conversion: Shippo’s real amount is a decimal-major-unit string (e.g. "24.30"
for $24.30); toMoney rounds to the nearest cent (Math.round, not Math.floor) converting
into this subsystem’s integer-minor-unit Money, avoiding a silently-lost cent on some decimal
values a naive truncation would produce.
Tracking status mapping: Shippo’s real, confirmed tracking_status.status enum
(UNKNOWN | PRE_TRANSIT | TRANSIT | DELIVERED | RETURNED | FAILURE) maps onto this subsystem’s
narrower TrackingStatusState. PRE_TRANSIT (label created, not yet handed to the carrier)
maps to "in_transit" rather than "unknown" — Shippo genuinely knows the shipment exists and
is progressing, and "in_transit" is the closer honest fit of the four available states, not a
guess. RETURNED/FAILURE both map to "exception" — this subsystem’s contract has no
separate “returned” state.
Wiring (apps/reference-storefront/lib/services.ts)
manual (createManualShippingAdapter(), @mercatus-liber/shipping) is always registered;
SHIPPO_API_TOKEN set and truthy additionally registers createShippoShippingAdapter({ apiToken }) under "shippo" — the same additive, two-state “env var truthy adds a provider, rather than
swapping one” shape as fulfillment’s Printful/Printify branches (shipping: Record<string, ShippingAdapter>). Unlike fulfillment, there is no routing repository/service wrapper here —
shipping’s adapters are exposed directly as a map for a caller to pick from, since a shipping
transaction (rate-shop-then-buy for one specific outbound parcel) has no per-SKU routing concept
the way a fulfillment provider does. lib/adapter-info.ts’s getAdapterInfo() reports a
Shipping row: "Manual (PirateShip)" when SHIPPO_API_TOKEN is unset, `“Manual (PirateShip)
- Shippo (registered)“
when it's set, visible on/demo//admin/settings — mirroringfulfillmentInfo()'s own row exactly. As withfulfillmentInfo`, “active” in this row does not imply live-verified — see the credential-gap disclosure below.
Honest credential-gap disclosure
No real Shippo account or API token exists in this environment, checked directly: no
SHIPPO_API_TOKEN in the shell environment (env | grep -i shippo — no output) or in
.env.local (repo root; the app carries no .env.local of its own), which contains only
VERCEL_OIDC_TOKEN — same disclosed-gap posture as admin-auth-clerk (epic 27), the
Printful/Printify fulfillment adapters (epics 42/43), and the PostHog/GA4 insights adapters
(epic 46). @mercatus-liber/adapter-shippo is built and unit-tested for real correctness
against Shippo’s actual, current API shape (grounded in docs.goshippo.com, not training-data
recall), not exercised against a live Shippo API in this environment; no live rate/label/
tracking call against a real Shippo account was made or is claimed.
What is genuinely verified without a live credential:
- The
services.tsenv-var branch itself, in both states — manual-only whenSHIPPO_API_TOKENis unset, and manual+Shippo-registered when it’s set (including with a fake/test token for wiring-verification purposes only, never a live call) —apps/reference-storefront/test/adapter-info.test.ts’s twoShipping-subsystem tests cover exactly this. - The manual adapter’s own honest not-available behavior —
packages/shipping/test/ shipping.test.ts(16 tests) exercisesgetRates/buyLabelalways reportingavailable: false/purchased: falsewith the real PirateShip URL, andgetTrackingStatus’s carrier- detection heuristic across real UPS/FedEx/USPS/DHL tracking-number formats. - The Shippo adapter’s own unit test suite —
packages/adapter-shippo/test/(20 tests: 5 inhttp-client.test.ts, 15 inadapter.test.ts) exercisesgetRates/buyLabel/getTrackingStatusagainst realistically-shaped mocked Shippo API responses (including every documented transaction status, thetoShippoParcelmissing-dimension error, and theMoney/tracking-status mapping functions), plusshipping-adapter-compat.ts’s compile-time proof (checked bytscon every build, not visual inspection) thatcreateShippoShippingAdapter’s return value structurally satisfies the fullShippingAdaptercontract with zero casts. - Admin visibility of which adapter is active —
/demo/<demoSlug>/admin/settings’sShippingrow (lib/adapter-info.ts) accurately reports"Manual (PirateShip)"or"Manual (PirateShip) + Shippo (registered)"depending onSHIPPO_API_TOKEN, confirmed by the sameadapter-info.test.tssuite above.
Explicitly NOT this subsystem’s job
- Automatically rate-shopping/buying a label when an order is placed or paid — no event-bus
subscription exists (unlike
inventory/internal-bi’s own sync registration). An operator (or a future admin action) explicitly callsgetRates/buyLabelfor now; wiring this tocheckout.order.paidautomatically is a reasonable later addition, not required for this epic’s scope. - An admin UI for rate shopping/label purchase — this epic ships the subsystem and the
Shippo adapter with real, verified wiring into
services.tsand/admin/settings’s adapter visibility, but no/admin/orders(or dedicated) page yet callsgetRates/buyLabeldirectly the wayfulfillment’s admin actions callsubmitOrder/markShipped— a reasonable later addition building on this real contract, not required for this epic’s scope. - Live Shippo webhook receipt — Shippo supports webhook-driven tracking updates, but no
route in
apps/reference-storefrontreceives one and no real Shippo account exists in this environment to send a real webhook from. - Persisting purchased labels/rates anywhere —
buyLabel’s result is returned to its caller; this subsystem owns noLabelRecordpersistence layer of its own (unlikefulfillment’sFulfillmentLineRecord) — a real deployment wanting a durable purchase history would build that on top of this contract, mirroringfulfillment’s own explicit scope boundary.
Decoupling notes
packages/shipping’s only runtime dependency is @mercatus-liber/core (for Money).
@mercatus-liber/adapter-shippo’s only runtime dependency is @mercatus-liber/shipping (for
the ShippingAdapter contract and detectCarrierFromTrackingNumber). Neither package imports
@mercatus-liber/checkout-orders, @mercatus-liber/fulfillment, or any other subsystem — this
subsystem is deliberately as standalone as payments, addressed by an arbitrary
ShippingAddress/Parcel a caller constructs, never reaching into another subsystem’s order or
fulfillment-record types. Verify via grep -rn "@mercatus-liber/shipping" packages/cart packages/checkout-orders/src packages/catalog/src packages/fulfillment/src packages/promotions/src packages/bundles/src packages/recommendations/src packages/inventory/src packages/pdp/src packages/cms/src packages/analytics/src packages/advertising/src packages/internal-bi/src packages/account/src before merge — every one of those must return
zero hits; shipping (and adapter-shippo) are consumed only by apps/reference-storefront.
Open questions
- Should rate shopping/label purchase become a real admin action (e.g. on
/admin/orders, mirroringfulfillment’s “Route & submit for fulfillment” button) instead of a contract-and-wiring-only epic? Deferred — the real, tested, wiredShippingAdaptercontract and both adapters are a genuine, working default and don’t block a later admin-surface story building on top of them. - Both the manual/PirateShip default and the real Shippo adapter are now real, wired,
unit-tested code, but no live Shippo credential exists in this environment (see the
disclosure above) — the Shippo adapter’s own unit test suite, plus the
services.tsenv-var branch verified in both states, stand as the proof of swappability for v1, the same posture asfulfillment’s Printful/Printify adapters andinternal-bi’s external-BI-tool adapter. Live account verification is disclosed future work, not required for this epic’s scope.