Shipping Notes
A Shipping Note (SN) is the internal record that materializes a physical shipment within Farfalla. Each SN represents a concrete delivery cycle: the tenant operator sees it in the dashboard, ships the package through their courier of choice, and manually updates the status as it progresses.
There is no courier API integration. The SN is an internal document, not a real shipment triggered by code.
The number of SNs a purchase generates depends on the plan type and is always tied to the shipping payment, not the product. The concrete cardinality per sale type lives in the Glossary and mental model table; the mechanics of how each charge becomes one or more SNs lives in Generation and cycles.
This page is the integration index. Each subtopic lives in its own document.
Overview
The subdomain relies on two models: ShippingNote over the shipping_notes table and ShippingCarrier for the shipping operator configured by the tenant, described in its own doc.
The logic lives in two services. ShippingNoteService centralizes SN creation, updates, and exports. ProvinceShippingService orchestrates province rates and pickup points for checkout and settings, gated by the shipping_by_province feature.
The recurring generation for annual subscriptions runs in the CreateShippingNotesForSubscriptionCycles job. Flow details and thresholds in Generation and cycles.
The entire subdomain is gated by the tenant feature physicals_goods. Without it, physical plans and products are hidden from the catalog and checkout, and no SNs are generated.
activity_log records operational activity under the shipping_notes_process channel, where creation, discard, and reprocessing messages are stored.
Each SN triggers a ShippingNoteNotification to the user at the moment of its creation.
The tenant UI lives at /dashboard/shipping-notes (read, filters, export, status change). There are also two Nova actions reserved for Publica super admins for manual retrigger.
Glossary and mental model
Three internal terms appear throughout the docs with names that are not literal in the code.
- Order: the purchase as a single header. Has a
shipping_informationJSON (with carrier, address, and amount) when there are physical products. - UserPlan (UP): the item within the order. The name does not reflect its current function: it simply represents the purchased product, and an order can have one or more UPs.
- Payment: each actual charge against the gateway. An order has N payments (one per product sold and, when applicable, one extra for shipping). The
sale_typedistinguishes betweenretail,subscription,external, andshipping.
Current cardinalities:
| Purchase type | Order : UP : Payments |
|---|---|
| One-off purchase (cart) | 1 order : N UP : N payments + 1 shipping payment (if physical) |
| Physical monthly subscription | 1 order : 1 UP : 1 payment + 1 shipping payment per cycle |
| Physical annual subscription | 1 order : 1 UP : 1 payment + 1 shipping payment (covers 12 SNs) |
The SN is always tied to the shipping payment, not to the product payment. The order_id column connects it to the original order.
The detail of how those charges become SNs lives in Generation and cycles.
Data model
The shipping_notes table stores each shipment with its identification columns (payment_id, order_id, carrier_id), cycle columns (shipment_number, period_length), status, and shipping snapshot. The SN is tenant-isolated and supports soft delete to preserve traceability.
The full schema, the seven statuses, the relationships, and the composite index that covers the job lookups live in Data model.
Generation and cycles
Each SN is born from an approved shipping payment. In one-off and monthly subscriptions the relationship is 1:1 with the charge; in annuals the first charge produces the first SN and a scheduled job completes the remaining eleven over the course of the year.
The detail of how the cart mounts the shipping payment, how maybeCreateFromPayment builds the first SN, how the recurring job filters and creates subsequent ones, and what idempotency guarantees exist at each step lives in Generation and cycles.
Lifecycle and operations
Once created, the SN lives in the tenant dashboard. The operator moves it manually between statuses, triggers exports, and receives email notifications every time something changes.
The detail of the dashboard, transitions, user notifications, refund handling, chargebacks, and the two Nova actions reserved for super admins lives in Lifecycle.
Carriers
A carrier is the shipping operator configuration the tenant offers in checkout. There are manual carriers (without state) and province rates (with state), and two configuration modes depending on the category.
The model, the shipping_by_province feature, multi-currency pricing logic, free shipping, settings, and the checkout selection flow live in Carriers.
Code structure
Tree of the files that make up the subdomain, grouped by layer.
app/
├── Domains/Commerce/
│ ├── Models/
│ │ ├── ShippingNote.php # shipping_notes table, statuses, helpers
│ │ └── ShippingCarrier.php # shipping_carriers table (delivery / warehouse)
│ ├── Services/
│ │ ├── ShippingNoteService.php # createShippingNote, maybeCreateFromPayment, export, prepare
│ │ └── ProvinceShippingService.php # Province rates + pickup points (feature shipping_by_province)
│ ├── Jobs/
│ │ └── CreateShippingNotesForSubscriptionCycles.php # Generates SNs 2-12 for annuals
│ └── Traits/
│ └── ShippingHasPrices.php # Polymorphic priceMorph for multi-currency carrier pricing
├── Http/
│ ├── Controllers/Dashboard/
│ │ ├── ShippingNotes/ShippingNoteController.php # Index, export, update status
│ │ └── Settings/ShippingCarrierSettingsController.php # Legacy manual carrier CRUD
│ └── Livewire/
│ ├── CheckoutV2/CheckoutV2.php # Current checkout, integrates province feature
│ ├── CheckoutV2/ShippingForm.php # Sub-component with groupedCarriers
│ ├── Cart/ShippingChoice.php # Legacy checkout (no province support)
│ ├── Dashboard/Settings/ProvinceShippingSettings.php # Settings UI for province rates + pickup points
│ └── MyAccount/EditOrderShippingInformation.php # Post-purchase address change
├── Nova/Commerce/Actions/
│ ├── CreateShippingNotesForOrder.php # Manual retrigger for an annual (super admin)
│ └── RetryShippingNotes.php # Reprocessing of payments with missing SN (super admin)
└── Notifications/
└── ShippingNoteNotification.php # Email to the user when an SN is created
routes/console.php # Daily job schedule (02:22)
Runbook
When an SN does not appear as expected, the first place to look is activity_log filtered by log_name = 'shipping_notes_process' and subject_id = order_id. There you will find messages like No shipping note created: ... explaining why the service discarded the charge.
If nothing appears in that channel, there are two possible explanations.
One is that the shipping payment never reached approved status or was not typed as shipping, and maybeCreateFromPayment cut short before reaching the log.
The other is that the order's shipping_information has no valid carrier_id or type: in that case the service returns null silently (see maybeCreateFromPayment validation table).
To verify idempotency when duplicate SNs are suspected, run a direct query:
SELECT payment_id, COUNT(*)
FROM shipping_notes
WHERE shipment_number = 1
GROUP BY payment_id HAVING COUNT(*) > 1;
The result must be zero.
To monitor the recurring job, its last run leaves a log with total_payments_evaluated, shipping_notes_created, failed, skipped, and skipped_invalid_carrier under the message CreateShippingNotesForSubscriptionCycles job completed. Sustained drops in shipping_notes_created or spikes in failed are a signal to investigate.
References
| Resource | Role |
|---|---|
| Shipping Notes (product) | Tenant admin-oriented view. |
| Payments Overview | How the shipping payment mixes with the other order charges. |
| Shipping summary | Livewire component that shows the user the carrier selected during checkout. |