# Warehouse Verification API — Scan with damage flag & Scan list

This guide is for mobile app developers building the warehouse app's **Verification Progress** flow.

When a transporter brings a delivery to the warehouse, the warehouse user scans the QR code of every unit again. With each scan they record whether the unit arrived **OK** or **Damaged**.

---

## 1. Basics

| | |
|---|---|
| Base URL | `{{domain}}`. On the dev server this is `https://dxtserver.com/kabamex_web/public/api` |
| Method | `POST` for every endpoint |
| Body | `multipart/form-data` |
| Who can call | Warehouse users only (role `7`, Cargo Company Warehouse). Any other role gets HTTP 403. |

### Required headers (every request)

| Header | Value |
|---|---|
| `X-App-Token` | the app token (the `app_token` value in the Bruno environment) |
| `Accept-Language` | `en` or `ar`. Messages come back in this language. |
| `Authorization` | `Bearer <token>`, using the token from login |

### Getting the token
A warehouse user logs in through the cargo company login:

```
POST {{domain}}/v1/public/auth/login_with_email
  email     = warehouse@example.com
  password  = ********
  user_type = 4          // cargo company login; it also lets warehouse accounts in
```

The response returns `oData.token`, and `oData.user.user_type` is `7` for a warehouse user. Send the token as `Authorization: Bearer <token>`.

### Response envelope (all endpoints)
```json
{
  "status": "1",            // "1" = success, "0" = failure
  "message": "…",           // ready to show to the user (localised)
  "oData": { … },           // payload
  "errors": { … },
  "meta": { … }
}
```
- Business errors such as "Product not found" come back as **HTTP 200** with `status: "0"`, so always check `status`.
- Auth errors come back as HTTP **401** (bad or missing token or app token) or **403** (wrong role, or the account is inactive).
- Every value in `oData` is a **string**, including numbers. The paginated `list` in `get_scan_list` is the exception.

---

## 2. Where each call fits in the app

```
Warehouse home / delivery list
        │  tap a delivery
        ▼
get_details                      → items in the delivery + overall progress
        │  tap an item (e.g. "Rice")
        ▼
get_item_variants                → variants of that item (e.g. "2 kg")
        │  tap a variant
        ▼
┌─ get_scan_list ◄────────────────────────────┐   "Verification Progress" screen
│       │  user scans a QR and picks OK / Damaged
│       ▼                                     │
│  scan_product (code, is_damaged) ───────────┘   update the row / reload the list
└───────│
        ▼  all units checked
complete_item_verification (optional)  /  complete_delivery
```

| Screen / action | Endpoint |
|---|---|
| Open the Verification Progress screen for a variant | `get_scan_list` |
| Scroll to the bottom of the list (next page) | `get_scan_list` with `page + 1` |
| The camera reads a QR code and the app shows the unit before saving (details card with OK / Damaged buttons) | `get_scanned_product` (read-only) |
| User confirms OK or Damaged (or the app saves straight away on scan) | `scan_product` |
| User corrects a unit's condition (OK ↔ Damaged) | `scan_product` again with the same `code` and the other `is_damaged` |
| After a successful scan | Update that row locally using `product_id` / `scan_status` from the scan response, or call `get_scan_list` again |

Scanning only works while the delivery `status` is **3 In Transit, 4 Arrived or 5 Completed**. Any other status returns `"Delivery is not in a valid status for verification."`

---

## 3. `get_scan_list` (new)

Lists every unit the transporter collected for **one variant** of a delivery, in the order the transporter scanned them. Each unit carries its warehouse scan state.

```
POST {{domain}}/v1/warehouse/deliveries/get_scan_list
```

### Body
| Field | Required | Type | Description |
|---|---|---|---|
| `delivery_id` | ✅ | int | Delivery ID (from `get_list` / `get_details`) |
| `order_item_variant_id` | ✅ | int | Variant ID: `variant_id` from `get_details` → `item_breakdown[].variants[]`, or from `get_item_variants` → `variants[]`. **Not** the `order_item_id`; sending that returns "Record not found." |
| `page` | – | int | Page number, default `1` |
| `limit` | – | int | Items per page, default `20` |

### Example (cURL)
```bash
curl -X POST "{{domain}}/v1/warehouse/deliveries/get_scan_list" \
  -H "X-App-Token: <app_token>" \
  -H "Accept-Language: en" \
  -H "Authorization: Bearer <token>" \
  -F delivery_id=27 \
  -F order_item_variant_id=9 \
  -F page=1 \
  -F limit=20
```

### Success response
```json
{
  "status": "1",
  "message": "Data fetched successfully.",
  "oData": {
    "delivery_id": "27",
    "tracking_number": "DLV-00000027",
    "status": "4",
    "status_info": { "text": "Arrived at Warehouse", "class": "danger" },
    "item": {
      "order_item_id": "5",
      "item_name": "Rice",
      "variant_id": "9",
      "variant_name": "2 kg",
      "image_url": "https://…/rice.jpg"
    },
    "summary": {                       // progress of THIS variant
      "expected_quantity": "200",
      "collected_quantity": "200",
      "verified_quantity": "80",
      "ok_count": "78",
      "damaged_count": "2",
      "pending_count": "120",
      "missing_count": "0",
      "percentage": "40",
      "is_completed": "0"
    },
    "delivery_progress": {             // progress of the WHOLE delivery (same keys)
      "expected_quantity": "400",
      "collected_quantity": "400",
      "verified_quantity": "280",
      "ok_count": "277",
      "damaged_count": "3",
      "pending_count": "120",
      "missing_count": "0",
      "percentage": "70",
      "is_completed": "0"
    },
    "variants_progress": [             // one entry per variant in the delivery (same keys + identity)
      {
        "order_item_id": "5", "item_name": "Rice", "variant_id": "9", "variant_name": "2 kg",
        "image_url": "https://…/rice.jpg", "is_current": "1",
        "expected_quantity": "200", "collected_quantity": "200", "verified_quantity": "80",
        "ok_count": "78", "damaged_count": "2", "pending_count": "120", "missing_count": "0",
        "percentage": "40", "is_completed": "0"
      },
      {
        "order_item_id": "6", "item_name": "Machinery", "variant_id": "12", "variant_name": "Standard",
        "image_url": "https://…/machine.jpg", "is_current": "0",
        "expected_quantity": "200", "collected_quantity": "200", "verified_quantity": "200",
        "ok_count": "199", "damaged_count": "1", "pending_count": "0", "missing_count": "0",
        "percentage": "100", "is_completed": "1"
      }
    ],
    "list": {
      "current_page": 1,
      "data": [
        {
          "sequence": "1",
          "product_id": "301",
          "reference_id": "OIVP-1A2B3C-00009-00001",
          "image_url": "https://…/rice.jpg",
          "scan_status": "1",
          "scan_status_text": "OK",
          "is_verified": "1",
          "is_damaged": "0",
          "verified_at": "2026-10-14 10:20:00"
        },
        {
          "sequence": "2",
          "product_id": "302",
          "reference_id": "OIVP-1A2B3C-00009-00002",
          "image_url": "https://…/rice.jpg",
          "scan_status": "0",
          "scan_status_text": "Not scanned",
          "is_verified": "0",
          "is_damaged": "0",
          "verified_at": null
        }
      ],
      "last_page": 10,
      "per_page": 20,
      "total": 200,
      "next_page_url": "…",
      "prev_page_url": null
    }
  },
  "errors": {},
  "meta": {}
}
```

### Progress keys
`summary`, `delivery_progress` and every entry in `variants_progress` use the same keys:

| Key | Meaning |
|---|---|
| `expected_quantity` | Units that should be in the delivery (or variant) |
| `collected_quantity` | Units the transporter scanned at pickup |
| `verified_quantity` | Units the warehouse has scanned (`ok_count + damaged_count`) |
| `ok_count` | Scanned as OK (green) |
| `damaged_count` | Scanned as damaged (red) |
| `pending_count` | Collected but not scanned by the warehouse yet (grey) |
| `missing_count` | Expected but never collected by the transporter. These units have no QR row and never appear in `list`. |
| `percentage` | `verified / expected × 100` |
| `is_completed` | `"1"` once `verified_quantity >= expected_quantity` |

`variants_progress[]` also carries `order_item_id`, `item_name`, `variant_id`, `variant_name` and `image_url`, plus `is_current` (`"1"` for the variant whose units are in `list`).

### Mapping the response to the screen
| UI element | Field |
|---|---|
| Header "Rice (2 kg)" | `item.item_name` + `item.variant_name` |
| "80 / 200" | `summary.verified_quantity` / `summary.expected_quantity` |
| Progress bar and "40%" | `summary.percentage` |
| OK / damaged / pending counters | `summary.ok_count` / `summary.damaged_count` / `summary.pending_count` |
| "Continue scanning all 200 items for Rice (2 kg)" banner | show while `summary.is_completed == "0"` |
| Row number | `list.data[].sequence` |
| Row reference | `list.data[].reference_id` |
| Row icon | `list.data[].scan_status`: **`0` grey (not scanned)**, **`1` green ✓ (OK)**, **`2` red (damaged)** |
| Load more | while `list.current_page < list.last_page`, request `page + 1` |
| **Completed screen:** item rows "Rice 50 / 50 ✓" | `variants_progress[]`: `item_name` (+ `variant_name`), `verified_quantity` / `expected_quantity`, tick when `is_completed == "1"`, damaged badge when `damaged_count > 0` |
| **Completed screen:** "All items verified! 200/200 items" | `delivery_progress.verified_quantity` / `delivery_progress.expected_quantity`, shown when `delivery_progress.is_completed == "1"` |
| "Missing" note | `summary.missing_count > 0` (or `delivery_progress.missing_count` for the whole delivery) |

After a `scan_product` call, call `get_scan_list` again (page 1) to refresh all progress blocks, or update the row and header locally from the scan response.

### Errors (`status: "0"`)
| Message | Cause |
|---|---|
| `The delivery id field is required.` / other validation text | Missing or invalid field |
| `Delivery order not found.` | The delivery doesn't belong to this warehouse's cargo company |
| `Record not found.` | The variant is not part of this delivery |

---

## 4. `scan_product` (updated: new `is_damaged` field)

Verifies one unit by its QR code and records its condition.

```
POST {{domain}}/v1/warehouse/deliveries/scan_product
```

### Body
| Field | Required | Type | Description |
|---|---|---|---|
| `delivery_id` | ✅ | int | Delivery being verified |
| `code` | ✅ | string | The scanned QR value (the unit's `reference_id`) |
| `is_damaged` | – | `0` / `1` | **New.** `0` = OK (default when omitted), `1` = Damaged |

### Example
```bash
curl -X POST "{{domain}}/v1/warehouse/deliveries/scan_product" \
  -H "X-App-Token: <app_token>" \
  -H "Accept-Language: en" \
  -H "Authorization: Bearer <token>" \
  -F delivery_id=27 \
  -F code=OIVP-1A2B3C-00009-00002 \
  -F is_damaged=1
```

### Success response
```json
{
  "status": "1",
  "message": "Item verified and marked as damaged.",
  "oData": {
    "product_id": "302",
    "reference_id": "OIVP-1A2B3C-00009-00002",
    "item_name": "Rice",
    "variant_name": "2 kg",
    "order_item_id": "5",
    "image_url": "https://…",
    "variant_image_url": "https://…",
    "is_verified": "1",
    "is_damaged": "1",
    "scan_status": "2",
    "scan_status_text": "Damaged",
    "verified_at": "2026-10-14 13:05:19",
    "collected_at": "2026-10-10 14:59:00",
    "is_scannable": "1",
    "order_delivery_item_id": "41",
    "variant_id": "9",
    "variant_verified": "81",
    "variant_expected": "200",
    "ok_count": "78",
    "damaged_count": "3",
    "verified": "81",
    "expected": "350",
    "delivery_id": "27",
    "delivery_status": "4",
    "delivery_status_info": { "text": "Arrived at Warehouse", "class": "danger" },
    "shipment_ref": "KBX-…",
    "delivery_tracking": "DLV-00000027",
    "transporter_id": "TRANS-01",
    "transporter_code": "TRANS-01",
    "transporter_name": "…"
  },
  "errors": {},
  "meta": {}
}
```
- `variant_verified` / `variant_expected` are the counts for the variant that was just scanned. Use them to update the "x / y" header.
- `verified` / `expected` / `ok_count` / `damaged_count` are the counts for the **whole delivery**.
- If the scanned `variant_id` isn't the variant on screen, the unit was still verified (it belongs to another variant of the same delivery). Show a toast and leave the current list as it is.

### Results by case
| Case | `status` | `message` (en) |
|---|---|---|
| First scan, OK | `1` | Item verified successfully. |
| First scan, damaged | `1` | Item verified and marked as damaged. |
| Already verified, sent with the **other** `is_damaged` | `1` | Product condition updated. |
| Already verified, sent with the **same** `is_damaged` | `0` | Product already verified by warehouse. |
| QR value not found | `0` | Product not found for the provided code. |
| QR belongs to a different delivery | `0` | This product does not belong to the selected delivery. |
| Delivery not in status 3 / 4 / 5 | `0` | Delivery is not in a valid status for verification. |
| Delivery not owned by this warehouse | `0` | Delivery order not found. |

### Suggested UX
1. The camera reads a QR code.
2. Ask "OK / Damaged". Pick OK by default, so a quick scan sends `is_damaged=0`.
3. Call `scan_product`.
4. On `status: "1"`, change the matching row (by `product_id`) to green or red, and update the header from `variant_verified` / `variant_expected`.
5. To fix a mistake, the user taps the row or scans again and picks the other condition. Send the same `code` with the flipped `is_damaged`.

Older app builds that never send `is_damaged` keep working; their scans are saved as OK.

---

## 5. `get_scanned_product` (new, read-only)

Looks up a scanned QR code inside a delivery and returns **the same data as `scan_product`**. It shows the unit's current state and **saves nothing**: no verification, no condition change, no notifications.

Use it to show the unit's details card (item, variant, image, reference, shipment, delivery, transporter) **before** the user confirms OK or Damaged. It also works for checking a unit that was already scanned.

```
POST {{domain}}/v1/warehouse/deliveries/get_scanned_product
```

### Body
| Field | Required | Type | Description |
|---|---|---|---|
| `delivery_id` | ✅ | int | Delivery being verified |
| `code` | ✅ | string | Scanned QR value (`reference_id`) |

### Example
```bash
curl -X POST "{{domain}}/v1/warehouse/deliveries/get_scanned_product" \
  -H "X-App-Token: <app_token>" \
  -H "Accept-Language: en" \
  -H "Authorization: Bearer <token>" \
  -F delivery_id=39 \
  -F code=OIVP-7F1DE2-00135-00002
```

### Success response
The `oData` has exactly the same keys as the `scan_product` response (section 4), with values describing the unit **as it is now**. Example for a unit the warehouse has not scanned yet:
```json
{
  "status": "1",
  "message": "Data fetched successfully.",
  "oData": {
    "product_id": "8443",
    "reference_id": "OIVP-7F1DE2-00135-00002",
    "order_item_id": "87",
    "item_name": "Rise",
    "variant_name": "2",
    "image_url": "https://…/items/….jpg",
    "variant_image_url": "https://…/items/variants/….jpg",
    "is_verified": "0",
    "is_damaged": "0",
    "scan_status": "0",
    "scan_status_text": "Not scanned",
    "verified_at": null,
    "collected_at": "2026-10-10 14:59:35",
    "is_scannable": "1",
    "order_delivery_item_id": "71",
    "variant_id": "135",
    "variant_verified": "2",
    "variant_expected": "5",
    "ok_count": "1",
    "damaged_count": "1",
    "verified": "2",
    "expected": "39",
    "delivery_id": "39",
    "delivery_status": "4",
    "delivery_status_info": { "text": "Arrived at Warehouse", "class": "danger" },
    "shipment_ref": "KBX-00000086",
    "delivery_tracking": "DLV-00000039",
    "transporter_id": "TRANS-0002",
    "transporter_code": "TRANS-0002",
    "transporter_name": "Asad Nazir"
  },
  "errors": {},
  "meta": {}
}
```

### Key fields
| Field | Meaning |
|---|---|
| `scan_status` | `0` not scanned yet · `1` scanned OK · `2` scanned damaged |
| `is_verified` / `is_damaged` | The same state as two flags |
| `verified_at` | When the warehouse scanned it (`null` if not yet) |
| `collected_at` | When the transporter scanned it at pickup |
| `is_scannable` | `"1"` if the delivery's status allows `scan_product` now (3 In Transit, 4 Arrived, 5 Completed). If `"0"`, disable the OK / Damaged buttons. |
| `image_url` / `variant_image_url` | Item image / variant image. Prefer the variant image when it isn't null. |

### Errors (`status: "0"`)
| Message | Cause |
|---|---|
| Product not found for the provided code. | No unit has this QR value |
| This product does not belong to the selected delivery. | The unit exists, but wasn't collected in this delivery |
| Delivery order not found. | The delivery doesn't belong to this warehouse's cargo company |

Unlike `scan_product`, this endpoint does **not** send the `invalid_qr` notification and does **not** check the delivery status. Check `is_scannable` instead.

### Suggested flow with the details card
1. The camera reads a QR code. Call `get_scanned_product`.
2. Show the card. If `scan_status` is `1` or `2`, show "Already verified as OK / Damaged" and offer to change it.
3. The user taps **OK** or **Damaged**. Call `scan_product` with the same `code` and `is_damaged=0` or `1`.

---

## 5b. `get_item_scan_list` (new): item-level list, like the transporter's `get_item_variants`

This is the warehouse version of `transporter_company/deliveries/get_item_variants`. For one **order item** of a delivery, it returns every variant with its warehouse progress. Each variant lists its collected units (`scanned_products`) with their scan state.

Units are **paginated across the whole item**: they're ordered by variant, then by the transporter's scan order. `variants[]` always contains **every** variant with its full counts. Only `scanned_products` is limited to the current page, so a variant can have an empty `scanned_products` on some pages.

Use `get_scan_list` (section 3) when the screen shows a single variant. Use this endpoint when it shows the whole item grouped by variant, like the transporter screen.

```
POST {{domain}}/v1/warehouse/deliveries/get_item_scan_list
```

### Body
| Field | Required | Type | Description |
|---|---|---|---|
| `delivery_id` | ✅ | int | Delivery ID |
| `order_item_id` | ✅ | int | **Order item** ID: `order_item_id` from `get_details` → `item_breakdown[]`. Not a variant ID. |
| `page` | – | int | Default `1` |
| `limit` | – | int | Units per page across all variants. Default `20`. |

### Success response (delivery 39, item 87, `limit=3`, `page=4`)
```json
{
  "status": "1",
  "message": "Data fetched successfully.",
  "oData": {
    "delivery_id": "39",
    "tracking_number": "DLV-00000039",
    "status": "4",
    "status_info": { "text": "Arrived at Warehouse", "class": "danger" },
    "order_item_id": "87",
    "item_name": "Rise",
    "item_image_url": "https://…",
    "total_variants": "3",
    "total_items": "18",
    "is_all_varients_verified": "0",
    "summary": {
      "expected_quantity": "18", "collected_quantity": "18", "verified_quantity": "2",
      "ok_count": "1", "damaged_count": "1", "pending_count": "16", "missing_count": "0",
      "percentage": "11.11", "is_completed": "0"
    },
    "pagination": {
      "current_page": "4", "last_page": "6", "per_page": "3", "total": "18",
      "from": "10", "to": "12", "has_more_pages": "1"
    },
    "variants": [
      {
        "variant_id": "134", "name": "3", "variant_name": "3 (10)", "display_name": "3 (10)",
        "image_url": "https://…",
        "scanned_quantity": "0", "is_scanned": "0",
        "expected_quantity": "10", "collected_quantity": "10", "verified_quantity": "0",
        "ok_count": "0", "damaged_count": "0", "pending_count": "10", "missing_count": "0",
        "percentage": "0", "is_completed": "0",
        "scanned_products": [
          { "id": "8432", "sequence": "10", "reference_id": "OIVP-02522A-00134-00001",
            "variant_name": "3", "image_url": "https://…",
            "qr_code_url": "https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=OIVP-02522A-00134-00001",
            "scan_status": "0", "scan_status_text": "Not scanned",
            "is_verified": "0", "is_damaged": "0", "verified_at": null }
        ]
      },
      {
        "variant_id": "135", "name": "2", "variant_name": "2 (5)", "display_name": "2 (5)",
        "scanned_quantity": "2", "is_scanned": "0",
        "expected_quantity": "5", "collected_quantity": "5", "verified_quantity": "2",
        "ok_count": "1", "damaged_count": "1", "pending_count": "3", "missing_count": "0",
        "percentage": "40", "is_completed": "0",
        "scanned_products": [
          { "id": "8442", "sequence": "1", "reference_id": "OIVP-7F1DE2-00135-00001",
            "scan_status": "1", "scan_status_text": "OK", "is_verified": "1", "is_damaged": "0",
            "verified_at": "2026-10-14 13:04:29", "...": "…" },
          { "id": "8444", "sequence": "2", "reference_id": "OIVP-7F1DE2-00135-00003",
            "scan_status": "2", "scan_status_text": "Damaged", "is_verified": "1", "is_damaged": "1",
            "verified_at": "2026-10-14 13:05:19", "...": "…" }
        ]
      },
      {
        "variant_id": "136", "name": "1", "variant_name": "1 (3)", "display_name": "1 (3)",
        "expected_quantity": "3", "verified_quantity": "0", "...": "…",
        "scanned_products": []
      }
    ]
  },
  "errors": {},
  "meta": {}
}
```

### Comparison with the transporter API
| Transporter `get_item_variants` | Warehouse `get_item_scan_list` |
|---|---|
| `item_name`, `item_image_url`, `total_variants`, `total_items` | same |
| `is_all_varients_scanned` | `is_all_varients_verified` (every variant fully verified by the warehouse) |
| `variants[].variant_id / name / variant_name / display_name / image_url / expected_quantity` | same |
| `variants[].scanned_quantity` (units the transporter scanned) | `scanned_quantity` = units the **warehouse** verified |
| `variants[].is_scanned` | `"1"` when the warehouse has verified every expected unit |
| `variants[].is_collected` | n/a. Use `collected_quantity` / `missing_count` instead. |
| `scanned_products[]`: `id, reference_id, variant_name, image_url, qr_code_url` | same, plus `sequence, scan_status, scan_status_text, is_verified, is_damaged, verified_at` |
| no pagination | `pagination` block, plus `summary` for the whole item |

### Notes
- `sequence` restarts at 1 for each variant and stays correct across pages. The first unit of variant 135 is `1` even when it appears on page 4.
- `scanned_products` includes **every collected unit**, including unscanned ones (`scan_status "0"`, grey). The existing warehouse `get_item_variants` returns only verified units, and it is unchanged.
- **Infinite scroll:** request `page + 1` while `pagination.has_more_pages == "1"`. Append each variant's `scanned_products` under the matching `variant_id`. Variant counts don't change between pages, so you can take them from any page.
- Errors: `Delivery order not found.` (not this warehouse's delivery), `Record not found.` (the item has no variants in this delivery), or a validation message.

---

## 6. Related warehouse endpoints (unchanged)

| Endpoint | Body | Use |
|---|---|---|
| `/v1/warehouse/deliveries/home` | – | Dashboard counts and recent deliveries |
| `/v1/warehouse/deliveries/get_list` | `tab`, `search`, `limit`, `page` | Delivery list (tabs: `arriving`, `in_transit`, `arrived`, `verification_pending`, `completed`) |
| `/v1/warehouse/deliveries/get_details` | `delivery_id` | Items and variants in the delivery (`variant_id` for `get_scan_list` comes from here) |
| `/v1/warehouse/deliveries/get_item_variants` | `delivery_id`, `order_item_id` | Variants of an item, with **verified** units only |
| `/v1/warehouse/deliveries/complete_item_verification` | `delivery_id`, `order_item_id` | Marks every collected unit of the item as verified **without scanning**. Those units count as OK. |
| `/v1/warehouse/deliveries/complete_delivery` | `delivery_id` | Final step: marks the delivery as completed |

---

## 7. Testing with Bruno
Collection folder: `bruno/Protected/Warehouse/Deliveries/`
- **Scan Product**: now has the `is_damaged` field.
- **Get Scan List**: new request.
- **Get Scanned Product**: new request (read-only lookup).
- **Get Item Scan List**: new request (item-level list with pagination).

Before testing, log in as a warehouse user and set `warehouse_access_token` in the Kabamex environment.
