Skip to main content

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, CarrierShipment entity, API endpoints.
  • Carrier packages (packages/carrier-<provider>/) — each carrier implements ShippingAdapter and 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

StatusMeaning
label_createdLabel generated, not yet picked up
picked_upCarrier has collected the package
in_transitPackage is moving through carrier network
out_for_deliveryFinal delivery attempt in progress
deliveredSuccessfully delivered
failed_deliveryDelivery attempt failed
returnedPackage returned to sender
cancelledShipment 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:

  1. Carrier sends POST /api/shipping_carriers/webhook/{provider}.
  2. Signature verified, ShippingWebhookEvent constructed.
  3. Event enqueued to carrier-specific worker queue.
  4. Worker updates CarrierShipment status and emits domain events.

Core service methods

The ShippingCarrierService (resolved via DI as shippingCarrierService) provides:

MethodPurpose
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 orderId from 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 created
  • shipping_carriers.shipment.status_changed — tracking status updated
  • shipping_carriers.shipment.delivered — package delivered
  • shipping_carriers.shipment.cancelled — shipment cancelled
  • shipping_carriers.webhook.received — webhook processed
  • shipping_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.