Skip to main content

Shipping Notes — Data Model

This page describes the tables, columns, and relationships that underpin the Shipping Notes subdomain. The terminology and conceptual model live in the overview; it assumes the reader already knows what an SN is and how it relates to order, UP, and payment.

Overview

The subdomain runs on three tables: shipping_notes for the note itself, shipping_carriers for the tenant's shipping operator configuration, and prices (via polymorphic relationships) for the shipping cost per currency.

Everything is tenant-isolated, and SNs support soft delete to maintain traceability and allow recovery after accidental deletion.

The SN does not attach to the UP. It attaches to the shipping payment (a payment with sale_type = 'shipping') and, via order_id, to the order that originated the charge.

What shipping_notes stores

Each row represents a concrete shipment. Beyond the project's standard columns (id, tenant_id, uuid, amount, currency_id, timestamps), the columns carrying the subdomain logic are few.

The SN is tied to three references: payment_id connects it to the shipping payment that originated it (the SN is born only when that charge is approved), order_id resolves the purchase header (from which address, user, and products are drawn), and carrier_id points to the shipping operator chosen during checkout. The carrier model lives in its own doc, see Carriers.

To distinguish between a one-off shipment and the monthly shipments of an annual subscription, there are two additional columns. period_length indicates how many SNs the payment covers in total: 1 for monthlies and one-off purchases, 12 for annuals. shipment_number indicates the position of each SN within that cycle: 1 for the first, 2 through 12 for those the job creates month by month.

The address and carrier data travel as a snapshot. shipping_information is a JSON containing the user's address (or the pickup location), the contact, and the carrier data at the time the SN was created. This prevents a later address change or carrier deletion from breaking already-dispatched SNs.

The type column duplicates the carrier's type (delivery or warehouse) so the dashboard filter can discriminate without joining shipping_carriers.

deleted_at enables soft delete. There is no cleanup job: the flag is meant purely to preserve traceability for manual deletions from tinker or Nova.

There is a composite index on (tenant_id, payment_id, shipment_number) that covers the two frequent lookups: the job querying the last shipment_number per payment, and the retry action looking up SNs by payment.

Statuses

SNs live in one of seven states that the tenant operator manages manually from the dashboard. The constants are grouped in ShippingNote::ALL_STATUS.

An SN is always born in order-generated. From there the operator moves it manually between available states:

StatusMeaning
order-generatedShipment order created and ready for fulfillment.
in-transitThe courier picked up the package and it is on its way to the destination.
pickupFor carriers of type warehouse: the package is ready for pickup.
deliveredClosure for home deliveries.
completedClosure for flows that do not distinguish between transit and final delivery.
cancelledThe shipment is not happening (refund, stock error, subscription cancellation).
pendingNeutral state. The dashboard explicitly filters it out; do not use as a working state.

Transitions are free between any pair of states. The controller validation only requires that the new status belongs to ALL_STATUS and differs from the current one.

There is no state machine or job that rotates states automatically. Operational coherence is the operator's responsibility.

Model signals

The model emits two types of signals on every change: a user notification and an activity log entry.

dispatchNotification() sends ShippingNoteNotification to the order's user. The timing, audience, and localization of the dispatch live in Lifecycle — User notifications.

The model activates Spatie Activitylog with LogOptions::defaults()->logAll()->logOnlyDirty(), so each status update leaves a row in activity_log with the change diff.

Additionally, maybeCreateFromPayment writes a log to the shipping_notes_process channel after creating the first SN, with payment_id, period_length, and shipment_number. The detail of the messages the service writes to this channel lives in the maybeCreateFromPayment validation table.

Associated carrier

The SN points to the carrier via carrier_id. The relevant data (type, title, description, shipping price for the SN currency) are snapshotted in shipping_information at creation time, so a subsequent deletion or change of the carrier does not affect existing SNs.

For recurring subscriptions, the shipping payment amount varies according to the product's interval. The calculation rule (direct price for monthlies, ×12 for annuals) lives in Generation — How the shipping payment enters the order.

The full carrier model — the shipping_carriers table, the two-dimension categorization (type and presence of state_code), the ProvinceShippingService, dashboard settings, and the checkout selection flow — lives in Carriers.

Relationships

From the SN, the useful relationships are order(), payment(), and carrier() (all belongsTo). There is also an orderFromAllTenants() variant that bypasses the global tenant scope: the job uses it when iterating cross-tenant payments before setting the batch's CurrentTenant.

On the Payment side there is an inverse shippingNotes() relationship (with tenant scope) and shippingNotesFromAllTenants() (without scope).

The recurring job uses shippingNotesFromAllTenants() to filter payments whose first SN has already reached the minimum age required to generate the next cycle SN.

Multi-tenant isolation

Both ShippingNote and ShippingCarrier use the BelongsToATenant trait, which adds a global scope filtering by tenant()->id.

The cycle job explicitly removes the scope with fromAllTenants() to iterate payments across multiple tenants in a single pass. When processing each batch grouped by tenant, it calls app('CurrentTenant')->set($tenantId) so that tenant() resolves to the correct tenant during the batch.

SNs created by the job inherit the tenant_id from the order.

Cardinality by purchase type: see overview — Glossary and mental model. Mechanics of how each charge ends up creating SNs: see Generation and cycles.

References

The carrier-side files are listed in Carriers — References.

FileRole
app/Domains/Commerce/Models/ShippingNote.phpModel, statuses, relationships, notification dispatch, activity log
app/Domains/Commerce/Models/Payment.phpInverse relationship shippingNotes() and shippingNotesFromAllTenants()
app/Domains/Shared/Traits/BelongsToATenant.phpGlobal tenant scope applied to ShippingNote and ShippingCarrier
app/Notifications/ShippingNoteNotification.phpNotification triggered by the model's dispatchNotification() helper
X

Graph View