# 2026-10-14 — Warehouse scan: damaged flag + scan list API

## Why
The transporter scans every unit (QR `reference_id`) when collecting a delivery. The warehouse then scans the same units again to verify them. Before this change, the warehouse scan only recorded *that* a unit was verified, not its condition. The "Verification Progress" app screen also needs a per-variant list of units showing which are not scanned yet, OK or damaged.

## What changed

### Database
New migration `database/migrations/2026_10_14_000000_add_is_damaged_to_order_delivery_item_products_table.php`:

| Table | Column | Type |
|---|---|---|
| `order_delivery_item_products` | `is_damaged` | boolean, default `false`, after `warehouse_verified_at` |

The scan state of a collected unit is derived from the row:

| `is_warehouse_verified` | `is_damaged` | `scan_status` | UI |
|---|---|---|---|
| 0 | any | `0` not scanned | grey |
| 1 | 0 | `1` OK | green |
| 1 | 1 | `2` damaged | red |

These codes are `DeliveryController::SCAN_PENDING / SCAN_OK / SCAN_DAMAGED`.

### `POST /api/v1/warehouse/deliveries/scan_product` (changed, backward compatible)
New optional body field `is_damaged`: `0` = OK (the default when omitted) or `1` = damaged.

Behaviour:
- **First scan of a unit:** sets `is_warehouse_verified = 1`, `warehouse_verified_at = now()` and `is_damaged`. The existing notifications (`verification_in_progress`, `all_items_verified`) are unchanged.
- **Rescan with the same condition:** error `product_already_verified`, as before.
- **Rescan with a different condition:** only `is_damaged` is updated, so a mistake can be corrected. No notifications are sent, and the message is `product_condition_updated`.
- **Messages:**
  - OK scan: `item_verified_successfully` (unchanged)
  - damaged scan: `item_verified_as_damaged`

New response fields (all existing fields are kept):
- `is_damaged`, `scan_status`
- `order_delivery_item_id`, `variant_id`
- `variant_verified`, `variant_expected`
- `ok_count`, `damaged_count` (delivery-wide)

### `POST /api/v1/warehouse/deliveries/get_scan_list` (new)
Lists the units the transporter collected for one variant of a delivery, in scan order, with their warehouse scan state.

Access: role 7 (Cargo Company Warehouse). The delivery must belong to the warehouse's parent cargo company.

| Body field | Required | Notes |
|---|---|---|
| `delivery_id` | yes | |
| `order_item_variant_id` | yes | variant shown on the screen |
| `page` | no | |
| `limit` | no | defaults to `config('global.default_pagination_limit')` (20) |

Response `oData`:
```json
{
  "delivery_id": "27", "tracking_number": "DLV-00000027", "status": "4", "status_info": {...},
  "item":    { "order_item_id": "5", "item_name": "Rice", "variant_id": "9", "variant_name": "2 kg", "image_url": "..." },
  "summary": { "expected_quantity": "200", "collected_quantity": "200", "verified_quantity": "80",
               "ok_count": "78", "damaged_count": "2", "pending_count": "120", "percentage": "40" },
  "list": {
    "current_page": 1,
    "data": [
      { "sequence": "1", "product_id": "301", "reference_id": "OIVP-1A2B3C-00009-00001", "image_url": "...",
        "scan_status": "1", "scan_status_text": "OK", "is_verified": "1", "is_damaged": "0",
        "verified_at": "2026-10-14 10:20:00" }
    ],
    "per_page": 20, "total": 200, ...
  }
}
```
`list` is a standard Laravel paginator. `percentage` = verified / expected × 100.

**Progress blocks:** added after the first version, for the completion screen. All three use the same keys:
- `summary`: the requested variant
- `delivery_progress`: the whole delivery
- `variants_progress[]`: one entry per variant in the delivery, with item and variant name and image, plus `is_current`

The keys are `expected_quantity`, `collected_quantity`, `verified_quantity`, `ok_count`, `damaged_count`, `pending_count`, `missing_count` (expected but never collected), `percentage` and `is_completed`. The counts for all variants come from one grouped query. See `docs/api/warehouse-verification-api.md` for the screen mapping.

### `POST /api/v1/warehouse/deliveries/get_scanned_product` (new, read-only)
- Body: `delivery_id` and `code`. Returns the same `oData` as `scan_product` for that unit, describing its current state. It writes nothing: no DB update and no notifications.
- Purpose: let the app show the unit's details card before the user picks OK or Damaged.

To keep the two endpoints identical, `scanProduct` and `getScannedProduct` now share two protected helpers:
- `findDeliveryUnit(delivery, code)` returns the unit and its scan-log row.
- `buildScannedUnitData(delivery, product, pivot)` builds the response payload.

`scanProduct` re-reads the row after saving and builds its response from it. Its response gained these fields: `order_item_id`, `variant_image_url`, `is_verified`, `scan_status_text`, `verified_at`, `collected_at`, `is_scannable`, `delivery_id`, `delivery_status` and `delivery_status_info`. Every existing field kept its value and meaning.

### Other files
- `routes/api/v1/warehouse_routes.php`: adds the `get_scan_list` route.
- `lang/en/api.php`, `lang/ar/api.php`: add `item_verified_as_damaged`, `product_condition_updated`, `scan_status_pending`, `scan_status_ok` and `scan_status_damaged`.
- Bruno:
  - `Protected/Warehouse/Deliveries/Scan Product.yml` gets the `is_damaged` field.
  - The new request is `Get Scan List.yml`.

### Backups
Originals of the edited files were copied before editing to `backups/2026-10-14-warehouse-damage-scan/`, keeping their paths.

## Decisions
- The damaged flag lives on the existing scan-log row (`order_delivery_item_products`). This avoids a new table, because each collected unit has exactly one row per delivery.
- The scan-status constants are class constants on the warehouse `DeliveryController`. Only that controller uses them.
- Leaving out `is_damaged` means OK, so older app builds keep working.

## Testing performed
- `php -l` on every changed PHP file: no syntax errors.
- `php artisan route:list --path=v1/warehouse`: `get_scan_list` is registered.
- The live database is **PostgreSQL**. The first version compared booleans with `= 1`, which PostgreSQL rejects (`boolean = integer`). The raw SQL now uses `= true`, matching the existing `getStatistics()` and working on both MySQL and PostgreSQL.
- With the live DB connected locally (the `is_damaged` column was already migrated there), `getScanList` was called in-process as warehouse user 77, for delivery 72:
  - `order_item_variant_id=204` returned `status "1"`: 2 units, both `scan_status "1"` (OK), percentage 100.
  - `order_item_variant_id=126` (an order item ID, not a variant ID) returned `Record not found.`, as expected.
- After the progress blocks were added, `getScanList` was called again for delivery 72 / variant 204 with `limit=1`:
  - `summary` was 2/2 at 100%.
  - `delivery_progress` was 4/4 at 100%, with `is_completed "1"`.
  - `variants_progress` had 2 entries (204 with `is_current "1"`, 205 with `"0"`), each 2/2.
  - `list.total` was 2.
- The user tested `scan_product` from Bruno on delivery 39 with `is_damaged=1`. It returned "Item verified and marked as damaged." with `scan_status "2"`, and `ok_count` / `damaged_count` both at 1.
- `getScannedProduct` was called in-process (read-only) on delivery 39:
  - A unit marked damaged returned `scan_status "2"`.
  - An unscanned unit returned `scan_status "0"`.
  - A unit from another delivery returned "does not belong to the selected delivery".
  - An unknown code returned "Product not found".
  - The damaged unit's DB row was identical before and after the calls.
- `scan_product` has not been re-run since the helper refactor (it writes data). Please run one Bruno scan to confirm it.
- No automated tests were run. `phpunit.xml` doesn't use a separate test DB, and the existing feature test uses `RefreshDatabase`, which would wipe the DB set in `.env`.

### Manual test steps
Use a warehouse token and a delivery in status 3 or 4 that has transporter-scanned units:
1. Call `get_scan_list`: units show `scan_status "0"`.
2. Call `scan_product` with `is_damaged=0`, then `scan_product` with `is_damaged=1` on a different unit: the list shows `"1"` and `"2"`, and the summary counts change.
3. Rescan with the same value: "already verified". Rescan with the flipped value: "Product condition updated."
4. Call `scan_product` without `is_damaged`: the unit is saved as OK.

## Known limitations / follow-ups
- Units the transporter never scanned have no row, so they can't be listed. The gap is visible only as `expected_quantity - collected_quantity`.
- `complete_item_verification` bulk-verifies units without a scan, which leaves them `is_damaged = 0` (OK).
- `get_details`, `get_item_variants` and `complete_delivery` don't report damaged counts yet.
- No damage note or photo is stored.
