Shipping Notes — Lifecycle and Operations
This page describes how the tenant logistics operator works with already-created SNs: dashboard, transitions, notifications, refunds, cancellations, and the two Nova actions reserved for super admins. Statuses and creation mechanics live in Data model and Generation.
Overview
Once created, the SN lives in the tenant dashboard and advances through states that the operator manages manually. There is no state machine or job that rotates states automatically: transitions always happen from the UI or from a Nova action.
Status transitions
The SN is always born in order-generated. The valid values, the validation rules, and the meaning of each status live in Data model — Statuses.
Operationally, transitions are manual and free between any pair of valid states. The tenant operator triggers them from the dashboard, and each change is persisted in activity_log with the diff by the LogsActivity trait configured in the model.
Tenant dashboard
The dashboard is accessed from two different routes depending on role: the operator page and the data endpoints that feed it. The naming difference between them is a routing concern, not a behavioral one.
The operator page lives at /dashboard/shipping-notes (kebab-case, frontend) and is served by an endpoint of DashboardController. The data endpoints (index, update, and export) live under the prefix api/v1/dashboard/shipping_notes (snake-case) and are handled by App\Http\Controllers\Dashboard\ShippingNotes\ShippingNoteController.
Both the page and the endpoints are gated by the userIsAdmin middleware: only users with is_admin = true see the dashboard and operate on SNs. A plan_admin is redirected to library by UserIsAdmin::handlePlanAdmin, which does not currently whitelist the shipping notes routes.
Index
GET /api/v1/dashboard/shipping_notes lists SNs paginated in groups of 15 using fastPaginate.
The base query applies the global tenant scope (BelongsToATenant), excludes SNs in pending with the notPending() scope, and sorts by created_at with a direction configurable via the date_sort query param (desc by default). A free-text search over id, order_id, uuid, and shipping_information JSON substring is activated with the query query param.
On top of that the filter pipeline App\Filters\ShippingNote\Type and App\Filters\ShippingNote\Status runs.
Relation hydration (order.user, order.userPlan.issue, order.userPlan.plan with id, name, order.userPlan.coupon) is done with explicit select calls to minimize traffic. When transforming the collection, OrdersService::getIssues($note->order) is invoked to append the list of products in the shipment.
Before returning, the controller clears appends and unsets the order relationship to avoid lazy-loading errors and description queries on Issue.
Update
PUT /api/v1/dashboard/shipping_notes/{shippingNote} receives { status }, validates that it is a ShippingNote::ALL_STATUS value different from the current one, and calls ShippingNoteService::updateShippingNote($shippingNote, ['status' => ...], notifiable: true).
The service default is notifiable = false, but the dashboard controller passes true hardcoded on every update. This means any status change from the UI always dispatches ShippingNoteNotification to the user, with no option to silence it from the client.
If the new status equals the current one, it responds 422 with No changes were made. Status is the only editable field through this endpoint.
Export
GET /api/v1/dashboard/shipping_notes/export applies the same filtering as the index (without pagination) and delegates to ShippingNoteService::exportShippingNotes.
The service generates ShippingNotesExcelExport, persists it on the EXPORTS disk with private visibility, and dispatches GenerateShippingNotesExcelExport to send the admin an email with the download link.
The filename combines the current date and trans('shipment.export.export-title') so the admin can distinguish multiple exports on the same day.
User notifications
ShippingNoteNotification is dispatched at two moments: when the SN is created and when its status changes from the dashboard.
The creation dispatch goes via ShippingNote::dispatchNotification() invoked inside ShippingNoteService::createShippingNote, both for the first SN of the cycle (created by maybeCreateFromPayment) and for those the job generates month by month in annual subscriptions. The update dispatch happens when the dashboard endpoint passes notifiable=true and updateShippingNote resends the same notification.
The notification travels with locale(tenant()->lang ?? 'en') and respects the user's preference via notifyIfEnabled. If the user disabled notifications for the tenant, neither creation nor update sends an email: the SN is still created or updated and recorded in activity log.
Nova actions (super admin)
On the Order resource in Nova there are two actions exposed only to Publica super admins (auth()->user()->is_pla_super_admin).
Retry missing SNs
RetryShippingNotes iterates each selected order, filters its payments in approved status, and for each payment with sale_type = shipping calls ShippingNoteService::maybeCreateFromPayment. It is safe to re-execute because the service is idempotent (see Generation — Idempotency and retries): if the first cycle SN is already there it returns null and does not duplicate.
If for some reason it was never created, this action is the way to recover it without reprocessing the webhook.
The action reports how many orders it processed and how many SNs it created, and writes a log to shipping_notes_process with the message Shipping notes retry from nova action and the payments_ids property for auditing.
The list logged in payments_ids includes all approved payments for the order, not just those of type shipping. The sale_type = shipping filter is applied in the creation loop, not in the set being logged.
Force the job for a specific order
CreateShippingNotesForOrder dispatches CreateShippingNotesForSubscriptionCycles with an explicit set of orderIds. It is used to force the job to process a specific annual even if it falls outside the automatic 13-month lookback, or to retrigger it after manually correcting shipping_information.
The action aborts with Action::danger if the order has no shipping_information, if the first UP from the userPlan relationship (HasMany) does not exist or is not physical, or if the interval is not annual. The "is physical" check is $userPlan->plan->physical directly on the physical column of plans, cast to boolean by $casts, not a PHP accessor.
If all conditions pass, it dispatches the job and returns a confirmation message with the processed IDs. Activity in shipping_notes_process is logged as Creating shipping notes for orders from Nova with the list of IDs.
Post-purchase address change
App\Http\Livewire\MyAccount\EditOrderShippingInformation allows the user to update the order's shipping_information from My Account. The operation modifies the order's JSON.
Since shipping_information travels as a snapshot inside each SN at creation time, already-created SNs are not updated automatically and continue showing the old address. Future SNs for the same order — those the job generates month by month in an annual — do read the new address, because the service reads order.shipping_information each time it creates an SN.
The operational consequence: a mid-year change updates the destination of subsequent SNs but not those already dispatched. This is usually the desired behavior because the previous ones are already in transit or delivered.
Subscription cancellation
When an annual subscription is cancelled in month N, the order moves to a status other than approved and the job excludes it in the next run. The already-created SNs (1..N) remain intact with their current status: no logic marks them as cancelled automatically. The reason is that those SNs may correspond to shipments already in transit or already delivered, and forcing a status change would erase operational information.
The operator decides what to do with unshipped SNs. If the SN for the current month has not yet been shipped, mark it manually as cancelled from the dashboard; if previous SNs are already in delivered or completed, leave them as-is. Any proportional refund for unshipped months is handled from the gateway refund flow and does not affect SNs.
Refunds
Product refunds do not automatically update the status of associated SNs. The SN remains in its state, and the operator is responsible for marking it cancelled if appropriate. The service UserProductService::handleRefundCancellation cancels the UP, pauses the subscription on the gateway side, and synchronizes the order status, but does not touch SNs.
This is deliberate: a refund may not imply the shipment is cancelled (for example, the customer received the product and requested a refund due to dissatisfaction). The decision about what to do with the SN is left to the logistics operator.
Chargebacks
Yuno notifies chargebacks via the payment.chargeback webhook (other gateways have no equivalent webhook implemented). The current flow does not call handleRefundCancellation or cancel the UP automatically: the SN remains in its status.
The transaction_fee_in_usd_in_cents calculation (which excludes refunded) does not exclude chargebacks, so a confirmed chargeback can leave prorated fees unadjusted.
If the chargeback is confirmed, the operator must mark the SN as cancelled manually and handle the refund separately.
Relevant operational edge cases
pending as a working state. An SN manually moved to pending disappears from the dashboard because the index applies the notPending() scope. Avoid using pending as a working state.
The behavior with disabled notifications is described in the User notifications section.
Other cross-cutting cases (soft delete, tenant context) are described in Data model and Generation. Carrier-related cases (deactivate, delete, multi-currency) live in Carriers — Edge cases.
References
| File | Role |
|---|---|
app/Domains/Commerce/Models/ShippingNote.php | Statuses, notification dispatch, activity log opts |
app/Domains/Commerce/Services/ShippingNoteService.php | updateShippingNote, exportShippingNotes |
app/Http/Controllers/Dashboard/ShippingNotes/ShippingNoteController.php | Index, update, and export endpoints |
app/Http/Controllers/Dashboard/Exports/ShippingNotesExcelExport.php | Excel export class (file structure and row mapping) |
app/Domains/BulkOperations/Jobs/Exports/Dashboard/GenerateShippingNotesExcelExport.php | Job that persists the file to EXPORTS and sends the email |
app/Filters/ShippingNote/Status.php and Type.php | Dashboard filters |
app/Notifications/ShippingNoteNotification.php | Email to the user |
app/Nova/Order.php | Integration of the actions into the Order resource |
app/Nova/Commerce/Actions/RetryShippingNotes.php | Manual reprocessing of missing SNs |
app/Nova/Commerce/Actions/CreateShippingNotesForOrder.php | Force the job to process specific orders |
app/Http/Livewire/MyAccount/EditOrderShippingInformation.php | Post-purchase address change |
app/Domains/Commerce/Services/UserProductService.php | handleRefundCancellation — cancellation after refund (does not touch SNs) |