# Feature 07 — Order Status Updates (Captain App)

## What this feature does

It builds on Feature 06 (the captain's orders) so that two requirements hold:

| Requirement | How it is met |
| --- | --- |
| **One-tap confirmation of pickup from the store (Picked Up), with the option to confirm the number of items collected** | `PATCH /api/driver/orders/{uuid}/picked-up` works with an **empty body** (one tap). It can optionally send `items_collected`, which is compared with the order's expected piece count. |
| **Clear, sequential status updates: Picked Up → On the Way → Delivered (or "Delivery Failed" with a reason)** | The state machine allows only this order: skipped or repeated steps get **422**, and `failed` requires `reason`. Every captain order response now includes `steps`, the whole sequence with each step marked `done` / `next` / `upcoming` / `failed` and its time, plus `next_statuses` for the buttons. |

The step endpoints, their order and the failure reason already existed from Feature 06.
This feature adds the item count confirmation and the `steps` progress list.

## Pickup item confirmation

```
PATCH /api/driver/orders/{uuid}/picked-up
{}                          ← one tap
{ "items_collected": 3 }    ← with a count
```

- **Expected count** (`items_expected`) = the sum of the order's product-line quantities
  (2 perfumes + 1 gift wrap = 3).
- **Count matches:** `items_collected = 3`, `items_mismatch = false`. Message: *"The order
  has been picked up."*
- **Count differs:** the pickup **still succeeds** (the captain is never stuck at the store).
  `items_collected = 2`, `items_mismatch = true`. Message: *"The order has been picked up.
  The number of items collected does not match the order, so it has been flagged for the
  operations team."*
- **No count sent (one tap):** `items_collected = null`, `items_mismatch = false`.
- **Order without product lines:** there is nothing to compare, so it is never flagged.
- **Invalid count** (0, not a whole number, text, > 9999): **422**, and the order is not
  picked up.

The operations team finds flagged pickups with `GET /api/dashboard/orders?items_mismatch=1`.
Every order detail shows `items_expected`, `items_collected` and `items_mismatch`.

New columns (migration `2026_09_13_120000_add_pickup_item_confirmation_to_orders_table`):

| Column | Type |
| --- | --- |
| `items_collected` | unsigned small integer, nullable |
| `items_mismatch` | boolean, default `false`, indexed |

## The delivery sequence

```
assigned → picked_up → on_the_way → delivered
                                  └→ delivery_failed (reason required)
```

Each captain order response carries three `steps`:

| Order status | Picked up | On the way | Outcome |
| --- | --- | --- | --- |
| assigned | **next** | upcoming | upcoming (delivered) |
| picked_up | done | **next** | upcoming (delivered) |
| on_the_way | done | done | **next** (delivered) |
| delivered | done | done | done (delivered) |
| delivery_failed | done | done | **failed** (delivery_failed) |

Each step has `status`, `label`, `state`, `state_label` and `at` (when it happened, or
`null`). The logic lives in `OrderStatus::deliverySteps()`, next to the allowed transitions
the state machine enforces, so the progress bar and the rules can't disagree.

## Code

- `app/Enums/Order/OrderStepState.php` (new): done / next / upcoming / failed.
- `app/Enums/Order/OrderStatus.php`: `deliverySteps()`.
- `app/Http/Requests/Mobile/Order/PickUpOrderRequest.php` (new): optional `items_collected`,
  whole number 1–9999.
- `app/Services/Order/OrderService.php`:
  - `markPickedUp()` takes an optional count; `itemCountConfirmation()` decides the flag.
  - `advanceForCaptain()` passes the count through.
- `app/Repositories/Order/OrderRepository.php`:
  - `expectedItemCount()`
  - the `items_mismatch` list filter
- `app/Http/Resources/Order/CaptainOrderResource.php`:
  - `steps`
  - `items_expected` / `items_collected` / `items_mismatch`
- `app/Http/Resources/Order/OrderResource.php`: the item check fields.
- `app/Http/Controllers/Mobile/Order/OrderController.php`: `pickedUp()` uses the new request
  and picks the message.
- `OrderFilterData` + `PaginateOrdersRequest`: the `items_mismatch` filter.
- Translations (en/ar), OpenAPI (`OrderDeliveryStep` schema, the pickup body, the list
  filter), factory defaults.

## Tests

- `CaptainOrderApiTest`:
  - one-tap pickup with no count
  - matching count recorded
  - different count goes through and is flagged
  - order without lines never flagged
  - invalid counts get 422
  - `steps` correct for all five captain statuses, including times
- `OrderAssignmentApiTest`: operations list only mismatched pickups and see expected vs
  collected.

## Not included

- Confirming counts per product line (a single total was chosen).
- Pushing an alert to the operations team on a mismatch; it is a dashboard flag and filter.
- A reason for the mismatch (the pickup doesn't ask for one).
