Docs / Complete Flows
🏠 Home

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
      
CodeMeaningNotes
pendingCreated, waiting for reviewDefault on creation
approvedAccepted by staffDriver can be assigned
in_transitMoving between facilitiesβ€”
out_for_deliveryOn the delivery vanβ€”
on_holdBlocked β€” needs actionRequires hold_reason: invoice or other
consolidatePart of a CON orderSet by the consolidation screen
deliveredHanded to the customerTerminal state
rejectedDeclined by staffTerminal state
cancelledCancelledTerminal state
ready_for_pickupWaiting at the warehousePickups module
self_pickupCustomer booked a counter slotPickups module
Where statuses live Statuses are rows in the 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.
Pickups module membership 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.

POST /api/prealerts Β· stored in pre_alerts with is_package = 0

Staff 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.

POST /api/packages/customer/{id}/invoice

Staff 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 --> [*]
      
What the booking creates 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.

POST /api/packages/customer/{id}/self-pickup

Status 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
      
Push skip conditions Push is silently skipped β€” logged, never an error β€” when: FCM is disabled in Tools β†’ FCM, no push template exists for the event type, the template does not allow the recipient's role, or the user has no active device token. Database and email still go out. See Firebase & Push to enable it.
Fanout rules The customer always gets a row on the notifications table so the inbox works. Admins and managers get copies of customer-initiated actions β€” always excluding the acting user. Shipping types carried in each notice: shipment 1, package 2, pickup 3, prealert 5.

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]
      
When pay online is blocked 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]
      
Key details At least two tracking numbers are required. Shipment consolidations land in 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]
      
Notes While connectivity or auth is still loading the app stays on the splash screen. The onboarding flag is read once from 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.