Shipping Carriers
The shipping_carriers module provides a provider-agnostic hub for shipment management. Each carrier (InPost, DPD, FedEx, etc.) is a separate package implementing the ShippingAdapter interface.
Architecture
Mirrors the payment gateway pattern:
- Core module (
packages/core/src/modules/shipping_carriers/) — adapter contract, status machine, webhook routing,CarrierShipmententity, API endpoints. - Carrier packages (
packages/carrier-<provider>/) — each carrier implementsShippingAdapterand registers via the adapter registry.
ShippingAdapter interface
import type { ShippingAdapter } from 'packages/core/src/modules/shipping_carriers/lib/adapter'
const myCarrier: ShippingAdapter = {
providerKey: 'my_carrier',
async calculateRates(input) {
// Return available shipping rates
return [
{ serviceCode: 'standard', serviceName: 'Standard', amount: 9.99, currencyCode: 'USD', estimatedDays: 5 },
{ serviceCode: 'express', serviceName: 'Express', amount: 19.99, currencyCode: 'USD', estimatedDays: 2 },
]
},
async createShipment(input) {
// Generate label and tracking number
return { shipmentId: '...', trackingNumber: '...', labelUrl: '...' }
},
async getTracking(input) {
// Fetch tracking status and events
return { trackingNumber: '...', status: 'in_transit', events: [] }
},
async cancelShipment(input) {
return { status: 'cancelled' }
},
async verifyWebhook(input) {
return { eventType: 'tracking.updated', eventId: '...', idempotencyKey: '...', data: {}, timestamp: new Date() }
},
mapStatus(carrierStatus) {
return 'in_transit'
},
}
Unified shipment status
| Status | Meaning |
|---|---|
label_created | Label generated, not yet picked up |
picked_up | Carrier has collected the package |
in_transit | Package is moving through carrier network |
out_for_delivery | Final delivery attempt in progress |
delivered | Successfully delivered |
failed_delivery | Delivery attempt failed |
returned | Package returned to sender |
cancelled | Shipment cancelled before pickup |
Status transitions are monotonic — terminal statuses (delivered, returned, cancelled) cannot regress.
Registering adapters
import { registerShippingAdapter } from 'packages/core/src/modules/shipping_carriers/lib/adapter-registry'
import { registerShippingWebhookHandler } from 'packages/core/src/modules/shipping_carriers/lib/adapter-registry'
export const setup: ModuleSetupConfig = {
async onTenantCreated() {
registerShippingAdapter(myCarrierAdapter)
registerShippingWebhookHandler('my_carrier', verifyMyCarrierWebhook, { queue: 'my-carrier-webhook' })
},
}
Webhook processing
Same async pattern as payment gateways:
- Carrier sends
POST /api/shipping_carriers/webhook/{provider}. - Signature verified,
ShippingWebhookEventconstructed. - Event enqueued to carrier-specific worker queue.
- Worker updates
CarrierShipmentstatus and emits domain events.
Core service methods
The ShippingCarrierService (resolved via DI as shippingCarrierService) provides:
| Method | Purpose |
|---|---|
calculateRates(input) | Get available rates from a carrier |
createShipment(input) | Create shipment, generate label, persist CarrierShipment |
getTracking(input) | Fetch tracking info and update stored status |
cancelShipment(input) | Cancel shipment and update status |
Admin UI entry points
The carrier shipment flow is intentionally order-driven:
- operators start from Sales → Orders
- the carrier module injects a Create shipment row action for a specific order
- the wizard reads
orderIdfrom the route and pre-fills shipment context from that order
This means the standalone /backend/shipping-carriers/create page is an implementation route, not a first-class sidebar destination. The supported operator flow is to launch shipment creation from an order row action or another order-scoped entry point.
Practical implications:
- a carrier shipment can be created without first assigning a sales shipping method on the order
- the wizard still requires an order context
- at least one configured carrier provider must be available for the flow to be useful
Events
shipping_carriers.shipment.created— new shipment createdshipping_carriers.shipment.status_changed— tracking status updatedshipping_carriers.shipment.delivered— package deliveredshipping_carriers.shipment.cancelled— shipment cancelledshipping_carriers.webhook.received— webhook processedshipping_carriers.webhook.failed— webhook verification failed
Mock adapter
A mock shipping adapter is included in the example module for development:
import { registerShippingAdapter } from 'packages/core/src/modules/shipping_carriers/lib/adapter-registry'
import { mockShippingAdapter } from './lib/mock-shipping-adapter'
registerShippingAdapter(mockShippingAdapter)
The mock adapter uses in-memory storage with fixed rates and simulated tracking events.