Skip to content

Commerce Foundations is our pre-built Craft Commerce store. See what it includes

Craft CMS plugins

Shipments

A Craft CMS plugin that adds first-class shipments to Craft Commerce, splitting each completed order into one or more shipments via an extensible rules engine.

Events

Every event the plugin exposes, what fires it, what’s on the payload, and how to listen.

Shipments::EVENT_BEFORE_CREATE_SHIPMENTS#

Fires: inside Shipments::createFor(), after the rules engine produced plans, before the plugin persists them. Useful for mutating the plan list (append a plan, drop a plan, redistribute qty).

Payload: CreateShipmentsEvent

Property Type Notes
order Order The Commerce order that just completed.
plans list<ShipmentPlan> The rules engine’s output. Mutate to change what gets persisted.

Listen:

Event::on(
    Shipments::class,
    Shipments::EVENT_BEFORE_CREATE_SHIPMENTS,
    static function (CreateShipmentsEvent $event): void {
        // Drop any plan that's empty.
        $event->plans = array_values(array_filter(
            $event->plans,
            static fn (ShipmentPlan $plan): bool => $plan->lineItemQtys !== [],
        ));
    },
);

Common use: mutate the plan list before persistence (append, drop, or redistribute quantities).

Shipments::EVENT_AFTER_CREATE_SHIPMENTS#

Fires: inside Shipments::createFor(), after the persistence transaction commits. The saved Shipment elements are exposed on $event->shipments, in plan order.

Payload: CreateShipmentsEvent. Same as the BEFORE fire plus a populated shipments list:

Property Type Notes
order Order The Commerce order that just completed.
plans list<ShipmentPlan> The plans that were persisted (post-BEFORE mutation).
shipments list<Shipment> The saved Shipment elements, in plan order. Empty on BEFORE.

Listen:

Event::on(
    Shipments::class,
    Shipments::EVENT_AFTER_CREATE_SHIPMENTS,
    static function (CreateShipmentsEvent $event) use ($integrationId): void {
        foreach ($event->shipments as $shipment) {
            Craft::$app->getQueue()->push(new PushShipmentJob([
                'shipmentId' => $shipment->id,
                'integrationId' => $integrationId,
            ]));
        }
    },
);

Common use: queue integration pushes, send internal notifications.

Shipments::EVENT_SHIPMENT_STATUS_CHANGED#

Fires: inside applyTransition (every transition) and on initial creation (with fromCode = null). Fires inside the DB transaction, Craft’s default queue backend is DB-backed, so queue pushes from listeners ride the same transaction.

Payload: ShipmentStatusChangedEvent

Property Type Notes
shipment Shipment The post-transition state.
fromCode ?Status null on creation.
toCode Status What it just transitioned to.
history ShipmentStatusHistory The just-saved history row.
user ?User Who initiated (null for queue / webhook).
message ?string Optional note on the history row.
sourceIntegration ?Integration Which integration drove the change (null for CP actions).
sourceExternalCode ?string The raw external code from the integration.

Common use: queue a PushShipmentJob when specific transitions happen.

Event::on(
    Shipments::class,
    Shipments::EVENT_SHIPMENT_STATUS_CHANGED,
    static function (ShipmentStatusChangedEvent $event) use ($integrationId): void {
        if ($event->toCode !== Status::Shipped) {
            return;
        }

        // Don't re-push if the change came *from* this integration.
        if ($event->sourceIntegration?->id === $integrationId) {
            return;
        }

        Craft::$app->getQueue()->push(new PushShipmentJob([
            'shipmentId' => $event->shipment->id,
            'integrationId' => $integrationId,
        ]));
    },
);

ShipmentLineItems::EVENT_RESOLVE_SHIPPABLE_UNITS#

Fires: inside ShipmentLineItems::shippableUnitsFor(), after the map is seeded with each line item’s cart qty, before pool and overflow math read it. Lets you report a different shippable unit count for a line whose cart qty does not equal its physical units, such as a single summary or kit line that stands for many units. All coverage math reads the resulting map, so the reported count stays consistent everywhere.

Payload: ResolveShippableUnitsEvent

Property Type Notes
order Order The order whose line items are being resolved.
shippableUnits array<int, int> Shippable units keyed by Commerce line item id, seeded with cart qty. Overwrite entries to change the count.

Listen:

Event::on(
    ShipmentLineItems::class,
    ShipmentLineItems::EVENT_RESOLVE_SHIPPABLE_UNITS,
    static function (ResolveShippableUnitsEvent $event): void {
        // A summary line (cart qty 1) that ships as 30 physical units.
        foreach ($event->order->getLineItems() as $lineItem) {
            if ($lineItem->id !== null && $lineItem->sku === 'KIT-30') {
                $event->shippableUnits[$lineItem->id] = 30;
            }
        }
    },
);

Common use: override shippable units for kit or summary lines whose cart qty differs from the physical unit count.

Listeners overwrite existing entries; do not unset a key. A missing entry reads as zero shippable units, which silently drops the line from coverage. The event fires once per order per request; the resolved map is cached, so a listener that does database work runs only once per order.

Integrations::EVENT_REGISTER_INTEGRATIONS#

Fires: during Integrations::getSelectableProviderTypes() (called when rendering the integration edit page’s provider dropdown).

Payload: RegisterIntegrationsEvent

Property Type Notes
types list<class-string<ProviderInterface>> Push your provider FQCN onto this list to make it selectable.

Listen:

Event::on(
    Integrations::class,
    Integrations::EVENT_REGISTER_INTEGRATIONS,
    static function (RegisterIntegrationsEvent $event): void {
        $event->types[] = ExampleErpProvider::class;
    },
);

Common use: register your custom provider class from a site module’s init().

Rules::EVENT_REGISTER_RULES#

Fires: during Rules::allRules() (called when rendering the grouping source settings + running planFor).

Payload: RegisterShipmentRulesEvent

Property Type Notes
rules list<ShipmentRuleInterface> Push instances implementing ShipmentRuleInterface onto this list.

Listen:

Event::on(
    Rules::class,
    Rules::EVENT_REGISTER_RULES,
    static function (RegisterShipmentRulesEvent $event): void {
        $event->rules[] = new MyCustomRule();
    },
);

Common use: register a custom rule implementation. See custom rules.

Provider::EVENT_BEFORE_SEND#

Fires: inside Provider::sendShipmentWithEvents(), before the provider’s sendShipment() runs. Set $event->isValid = false to skip the send.

Payload: SendIntegrationPayloadEvent

Property Type Notes
integration ProviderInterface The provider about to run.
shipment Shipment The shipment being sent.
order Order The Commerce order the shipment belongs to.
isValid bool Set to false to skip the send. Default true.

Listen:

Event::on(
    Provider::class,
    Provider::EVENT_BEFORE_SEND,
    static function (SendIntegrationPayloadEvent $event): void {
        // inspect $event->shipment / $event->order; set $event->isValid = false to skip.
    },
);

Common use: veto a send based on shipment/order state, log outbound attempts, mutate provider state before send.

Provider::EVENT_AFTER_SEND#

Fires: inside Provider::sendShipmentWithEvents(), after sendShipment() completes successfully. Does not fire if EVENT_BEFORE_SEND set isValid = false, or if sendShipment() threw.

Payload: SendIntegrationPayloadEvent. Same shape as the BEFORE fire; isValid is unused on AFTER.

Common use: record a successful integration push, fan-out notifications, snapshot the remote reference.

Provider::EVENT_BEFORE_CANCEL#

Fires: inside Provider::cancelShipmentWithEvents(), before the provider’s cancelShipment() runs. Set $event->isValid = false to skip the cancel.

Payload: CancelIntegrationPayloadEvent

Property Type Notes
integration ProviderInterface The provider about to run.
shipment Shipment The shipment being cancelled.
order Order The Commerce order the shipment belongs to.
isValid bool Set to false to skip the cancel. Default true.

Listen:

Event::on(
    Provider::class,
    Provider::EVENT_BEFORE_CANCEL,
    static function (CancelIntegrationPayloadEvent $event): void {
        // inspect $event->shipment / $event->order; set $event->isValid = false to skip.
    },
);

Common use: veto a cancel when the remote already shipped, log cancel attempts.

Provider::EVENT_AFTER_CANCEL#

Fires: inside Provider::cancelShipmentWithEvents(), after cancelShipment() completes successfully. Does not fire if EVENT_BEFORE_CANCEL set isValid = false, or if cancelShipment() threw.

Payload: CancelIntegrationPayloadEvent. Same shape as the BEFORE fire; isValid is unused on AFTER.

Common use: record a successful integration cancel, notify downstream systems.

Debugging#

Set your site’s log level to info and all status transitions log their source + target to the shipments log category. Increase to debug for rules-engine trace output.

We can take it from here.

We take on the Craft and Craft Commerce sites you built, so you can get back to building. Introduce a client who signs a management contract and you get a $3,000 partnership fee.

How the Dev Partnership Program works
See if we’re a fit