Complete Flows
Every major CargoPRO lifecycle as a diagram β order statuses, prealerts, on-hold invoices, pickups, notifications, payments, consolidation and the user app session.
Order status lifecycle
Both shipments and customer packages move through the same eleven system statuses. An admin changes the status from the panel; the app reacts to the new lifecycle flags that come with it.
stateDiagram-v2
[*] --> pending : order or package created
pending --> approved : admin accepts
pending --> rejected : admin declines
pending --> cancelled : customer cancels
approved --> in_transit : handed to courier
approved --> out_for_delivery : same day delivery
approved --> ready_for_pickup : at warehouse counter
approved --> on_hold : issue found
in_transit --> out_for_delivery : loaded on van
in_transit --> on_hold : hold reason set
out_for_delivery --> delivered : customer signs
out_for_delivery --> on_hold : issue found
on_hold --> in_transit : admin releases hold
on_hold --> ready_for_pickup : admin releases hold
ready_for_pickup --> self_pickup : customer books slot
ready_for_pickup --> delivered : collected at counter
self_pickup --> delivered : collected at counter
consolidate --> in_transit : CON order shipped
consolidate --> delivered : handed to customer
delivered --> [*]
rejected --> [*]
cancelled --> [*]
note right of ready_for_pickup
Pickups module row
end note
note right of self_pickup
Pickups module row
end note
note left of consolidate
Set on child rows added to a CON order
end note
| Code | Meaning | Notes |
|---|---|---|
pending | Created, waiting for review | Default on creation |
approved | Accepted by staff | Driver can be assigned |
in_transit | Moving between facilities | β |
out_for_delivery | On the delivery van | β |
on_hold | Blocked β needs action | Requires hold_reason: invoice or other |
consolidate | Part of a CON order | Set by the consolidation screen |
delivered | Handed to the customer | Terminal state |
rejected | Declined by staff | Terminal state |
cancelled | Cancelled | Terminal state |
ready_for_pickup | Waiting at the warehouse | Pickups module |
self_pickup | Customer booked a counter slot | Pickups module |
styles table. The code references the
stable styles.code key while admins can rename the display
label (mod_style) and pick colours freely β ids differ per
install, so the code never hard-codes them. Source:
app/Support/ShipmentStatus.php.
ready_for_pickup and self_pickup set
is_pickup = 1 on the row, which is what makes it appear in the
Pickups module instead of the courier lists.
Prealert to package conversion
A prealert is the customer's heads-up that a parcel is on its way. The app records it, staff match it when the parcel physically arrives, and the prealert is converted into a real customer package.
flowchart TD
A[Customer adds prealert in app] --> B[POST /api/prealerts]
B --> C[Row in pre_alerts - is_package 0]
C --> D[Staff notified - prealert_created]
D --> E[Parcel arrives at warehouse]
E --> F[Admin opens Convert in Customer Packages]
F --> G[Convert form prefilled from prealert]
G --> H[Row in customers_packages - is_prealert 1]
H --> I[Prealert marked is_package 1]
H --> J[Invoice file reused from prealert]
H --> K[Customer notified - package_registered]
H --> L[Normal status lifecycle begins]
Customer creates the prealert in the app
Prealerts β add: store tracking number, shop, courier, purchase price, description and estimated arrival date. Files: tracking number, provider shop, courier, purchase price, package description, prealert date, estimated date.
/api/prealerts Β· stored in
pre_alerts with is_package = 0Staff get notified
OrderNotify::staff sends the
prealert_created event to every active admin and manager
through the database, email and push channels.
The parcel arrives
Warehouse staff match the physical parcel to the prealert by its tracking number.
Admin converts it
In the panel: Customer Packages β find the prealert β
Convert. The form (createFromPrealert) is
prefilled with the prealert's customer, and the prealert's invoice
file can be reused instead of uploading a new one.
A customer package is born
Saving writes the customers_packages row with
is_prealert = 1 and status_invoice = 2,
marks the prealert is_package = 1 so it cannot be
converted twice, and notifies the customer
(package_registered).
On-hold invoice flow
Customs sometimes needs the purchase invoice. Staff put the package on
hold with reason invoice, the customer uploads the document
from the app, and staff release the hold.
flowchart TD
A[Admin sets status on_hold] --> B{hold_reason}
B -->|invoice| C[App shows can_upload_invoice true]
B -->|other| D[hold_note required - shown to customer]
C --> E[Customer uploads invoice in app]
E --> F[POST /api/packages/customer/id/invoice]
F --> G[Staff notified - invoice_uploaded]
G --> H[Admin reviews the file]
H --> I[Admin changes the status]
I --> J[Hold fields cleared automatically]
J --> K[Continues to ready_for_pickup or in_transit]
Admin puts the package on hold
Setting on_hold requires a hold reason:
invoice or other. Reason
other also requires a note explaining the problem β
enforced by OrderLifecycle::validateHoldForStatus.
Customer uploads the invoice
The package detail screen shows the upload action only while
can_upload_invoice is true. Accepted files: jpg, jpeg,
png or pdf, maximum 10 MB.
/api/packages/customer/{id}/invoiceStaff review and release
Staff receive the invoice_uploaded notice, open the
file from the package screen, then set the next status. Leaving
On Hold clears hold_reason and hold_note
automatically, and the package continues to
ready_for_pickup or in_transit.
Home pickup booking flow
A home pickup is a request for a driver to collect a parcel from the
customer's address. It is stored as a shipment row flagged
is_pickup = 1 and follows the pickup part of the status
lifecycle.
stateDiagram-v2
[*] --> pending : booked in app with date and address
pending --> approved : admin accepts and assigns driver
pending --> rejected : admin declines
pending --> cancelled : customer cancels
approved --> ready_for_pickup : parcel collected to warehouse
ready_for_pickup --> in_transit : handed to the line haul
in_transit --> out_for_delivery : loaded on van
in_transit --> delivered : directly handed over
out_for_delivery --> delivered : customer signs
delivered --> [*]
rejected --> [*]
cancelled --> [*]
PickupController::store inserts the add_order
row with is_pickup = 1, order_incomplete = 1,
status_invoice = 2 and the requested
pickup_date, plus one detail_add_order item
line, and fires the pickup_created notification to the
customer and staff. Driver assignment is a separate call to
POST /api/pickups/{id}/assign-driver.
Warehouse self-pickup flow
When a package is already at the warehouse and marked
ready_for_pickup, the customer can book a counter slot and
collect it in person β no driver involved.
flowchart TD
A[Package status ready_for_pickup] --> B[lifecycle flag can_book_pickup true]
B --> C[Customer picks date and window in app]
C --> D[POST /api/packages/customer/id/self-pickup]
D --> E[Status self_pickup - is_pickup 1 - date and window saved]
E --> F[Track entry - Warehouse self pickup booked]
E --> G[Staff notified - self_pickup_booked]
E --> H[Customer collects at warehouse counter]
H --> I[Admin marks the package delivered]
Package becomes collectable
Only status ready_for_pickup allows booking β checked
by OrderLifecycle::canBookSelfPickup before anything is
written.
Customer books the slot
The app sends a pickup_date and a
pickup_window string (max 120 characters). The warehouse
name and address come back in the response so the customer knows
where to go.
/api/packages/customer/{id}/self-pickupStatus flips to self_pickup
OrderLifecycle::selfPickupPayload sets the status,
is_pickup = 1 and the slot fields; a tracking entry
reading "Warehouse self pickup booked: date window" is written and
staff are notified with self_pickup_booked.
Collection at the counter
The customer shows the tracking number; staff verify, hand over the
parcel and set the status to delivered.
Push notification flow
Every status change fans out through one service,
app/Services/OrderNotify.php, on three channels:
database (in-app inbox row), email and
push (FCM).
sequenceDiagram
autonumber
participant A as Admin panel
participant O as OrderNotify
participant N as NotificationService
participant F as FcmService
participant G as FCM
participant P as Customer phone
A->>O: package or shipment updated
O->>O: fanout - customer plus active staff minus the actor
O->>N: createNotification with channels
N->>N: write the in-app inbox row
N->>N: pick email template by event type and user role
N-->>P: queued SMTP email
N->>F: push channel
F->>F: checks - fcm enabled - template exists - role allowed - token present
alt any check fails
F-->>N: skip push and log the reason
else all checks pass
F->>G: HTTP v1 messages:send with service account OAuth
G->>P: notification on the phone
F->>F: deactivate dead tokens
end
Payment flow
Orders and packages carry a separate status_invoice
column β 2 pending, 3 verify, 1 paid β independent of the shipping
status. The app shows the Pay Online button from the lifecycle flag
can_pay_online.
flowchart TD
A[Order with status_invoice 2 pending] --> B{can_pay_online}
B -->|blocked| X[Pay button hidden]
B -->|allowed| C[Customer taps Pay Online in app]
C --> D[Gateway checkout - Stripe PayPal Paystack]
D --> E[Row in payments_gateway]
E --> F{gateway status}
F -->|approved| G[Charge row in charges_order]
F -->|pending| H[Waits for gateway callback]
F -->|failed| I[Customer can retry]
H --> G
G --> J[status_invoice 3 verify]
J --> K[Admin confirms - charges must cover total]
K --> L[status_invoice 1 paid]
L --> M[Customer notified - payment_confirmed]
OrderLifecycle::canPayOnline returns false when the shipping
status is pending, rejected or
cancelled, or when status_invoice is already 1
β an order never gets charged twice through the button. Confirmation
(confirmPayment) re-checks that recorded charges cover the
order total before flipping to paid, then sends the
payment_confirmed notice on the database and email
channels.
Consolidation flow
Consolidation merges several shipments or customer packages into one CON order so they travel and clear customs together.
flowchart TD
A[Admin opens Consolidations - Create] --> B{Source type}
B -->|shipments| C[Pick add_order tracking numbers]
B -->|packages| D[Pick customers_packages tracking numbers]
C --> E[Minimum two items required]
D --> E
E --> F[CON tracking number generated]
F --> G[Row in consolidate or consolidate_packages]
G --> H[Rows in consolidation_items link each child]
H --> I[Children flagged is_consolidate 1 - status consolidate]
H --> J[Customer notified - package_consolidated]
I --> K[CON order lifecycle - pending to delivered]
K --> L[delivered as one unit]
consolidate, package consolidations in
consolidate_packages; both keep child links in
consolidation_items. The CON order gets its own tracking
number from generate_tracking('consolidate') and a
consolidation fee, and each child row is stamped
is_consolidate = 1 plus status consolidate so
it disappears from the open lists.
User app session flow
On every launch the user app runs the same gate sequence before showing
content β connectivity first, then auth, then onboarding
(cargoproapp/lib/main.dart).
flowchart TD
S[App start] --> T[Splash while services init]
T --> U[ApiService init - status catalog warm up - language load]
U --> C{Connectivity}
C -->|offline| N[NoInternetScreen with retry]
C -->|online| A{Auth state}
A -->|logged in| AD{Address on file}
AD -->|missing| R[RequiredAddressScreen]
AD -->|present| M[MainNavigation]
A -->|guest| OB{Onboarding seen}
OB -->|no| O[OnboardingScreen]
OB -->|yes| L[LoginScreen]
SharedPreferences before the first paint, so a first launch
goes straight to onboarding instead of login. A logged-in user missing a
required address is sent to RequiredAddressScreen before
MainNavigation.