> For the complete documentation index, see [llms.txt](https://lmile.gitbook.io/lmile-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lmile.gitbook.io/lmile-docs/shelf-api/lookup.md).

# Shelf Lookup — 棚照会・ピック

倉庫スタッフのピッキングアプリ（picking-ios）向けの照会・ピック API です。ユニットラベルの**ユニットQR** または**棚コード**をスキャンし、対象ユニット・棚番号・出荷/注文状態・picked 作業状態を引き、ピッキング完了を per-unit で記録します。

> **Base URL** `https://api.lmile.io` · **認証** operator JWT（`Authorization: Bearer`）。`/api/operator/shelf/pick` は `x-ingest-secret` でも可（[詳細](/lmile-docs/getting-started/authentication.md)） · read API は `shop` を query、pick は `shop` をボディに含めます（pick を API キーで呼ぶ場合は省略できます）

> operator 認証は倉庫スタッフ（picking-ios）専用の Bearer トークンで、スタッフに束縛された shop だけを操作できます（詳細は [認証](/lmile-docs/getting-started/authentication.md)）。read 系（`lookup` / `by-code`）は Bearer のみ、pick は機械間連携も想定し `x-ingest-secret` でも呼べます。フィールドは camelCase（routing 系と同じ）。

> **ユニットQR 規格**: `<barcode>|<shipmentId>`（`shipmentId` = `sot.lm_fulfillment_shipments.id`。出荷指示ラベルの本番印字形式）。2 値は同一 barcode の複数ユニットを区別しないため、shipment 配下の barcode 一致ユニットから **未 picked の `lineInstanceNo` 昇順先頭 1 件**をサーバ側で解決します。旧規格（`JAN|FulfillmentOrderId`）は受理せず `422 legacy_qr`（ラベル再印刷を案内）。

## エンドポイント

| Method | Path                          | 用途                                    | テーブル                                                                                                      |
| ------ | ----------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `GET`  | `/api/operator/shelf/lookup`  | ユニットQR → ユニット + 棚 + 出荷/注文 + picked 状態 | `lm_fulfillment_units` / `lm_shelf_links` / `lm_fulfillment_shipments` / `lm_routing_orders` / `products` |
| `GET`  | `/api/operator/shelf/by-code` | 棚コード → アクティブ紐付け shipment + 棚在庫        | `lm_shelf_links` / `lm_shelf_inventory` / `lm_fulfillment_shipments` / `lm_routing_orders`                |
| `POST` | `/api/operator/shelf/pick`    | ユニットQR スキャンで unit を picked / unpick   | `lm_fulfillment_units`                                                                                    |

***

## `GET` /api/operator/shelf/lookup

ユニットQR から対象ユニットを解決し、紐付いている棚番号・出荷/注文の取消状態・picked 作業状態・商品情報を返します。`unit` は「次に pick 対象となるユニット」（未 picked の `lineInstanceNo` 昇順先頭。全て picked なら直近の対象 = 降順先頭）です。

### リクエスト

```http
GET https://api.lmile.io/api/operator/shelf/lookup?shop=demo-store.myshopify.com&qr=4912345678901%7Caaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
Authorization: Bearer {OPERATOR_JWT}
```

| クエリ    | 型      |  必須 | 説明                                          |
| ------ | ------ | :-: | ------------------------------------------- |
| `shop` | string |  ✓  | 店舗ドメイン（スタッフの束縛 shop 内）                      |
| `qr`   | string |  ✓  | ユニットQR（`<barcode>\|<shipmentId>`。最大 200 文字） |

### レスポンス

```json
{
  "ok": true,
  "unit": {
    "id": "uuuuuuuu-1111-2222-3333-444444444444",
    "barcode": "4912345678901",
    "sku": "SKU-001",
    "lineInstanceNo": 2,
    "status": "secured",
    "secured": true,
    "picked": false,
    "pickedAt": null,
    "pickedBy": null,
    "cancelledAt": null,
    "allocatedLocationId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
    "allocatedLocationLabel": "大阪MFC"
  },
  "units": {
    "total": 2,
    "picked": 1
  },
  "shelf": {
    "shelfCode": "A-7-WQ",
    "locationId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
    "locationLabel": "大阪MFC",
    "linkedAt": "2026-06-07T09:00:00.000Z",
    "linkedBy": null
  },
  "shipment": {
    "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "status": "secured",
    "trackingNumber": "4912345678901",
    "carrierCode": "yamato",
    "fromLocationId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
    "cancelledAt": null
  },
  "order": {
    "id": "7b1f08a3-1234-4abc-9def-0123456789ab",
    "orderNumber": "1001",
    "sourceSystem": "shopify",
    "status": "routed"
  },
  "cancelled": false,
  "product": {
    "sku": "SKU-001",
    "productName": "サンプル商品",
    "variantName": "M",
    "imageUrl": "https://.../image.jpg"
  }
}
```

| フィールド                                   | 説明                                                                                                            |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `unit`                                  | 次に pick 対象となるユニット（未 picked の `lineInstanceNo` 昇順先頭。全て picked なら降順先頭）                                          |
| `unit.picked` / `pickedAt` / `pickedBy` | picked 作業状態（`picked_at` の有無）/ 完了日時 / 操作者                                                                      |
| `units.total` / `units.picked`          | shipment 配下の barcode 一致ユニット総数 / うち picked 済（数量 >1 の進捗表示用）                                                     |
| `shelf`                                 | アクティブな棚紐付け（未紐付けは `null`）                                                                                      |
| `order.orderNumber`                     | 注文番号（`sot.lm_routing_orders.external_order_ref`）                                                              |
| `cancelled`                             | unit / shipment / 注文のいずれかが `cancelled` なら `true`（出荷済は [Cancel](/lmile-docs/routing-api/cancel-update.md) で保護） |
| `product`                               | barcode で解決した商品情報（未登録は `null`）                                                                                |

### 挙動

* QR 不正形式は `422 invalid_qr`、旧規格は `422 legacy_qr`。`shipmentId` の shipment が無ければ `404 shipment_not_found`、shipment 配下に barcode 一致 unit が無ければ `404 unit_not_found`。
* shipment が指す注文が引けない不整合は隠さず `500 data_inconsistency` で表面化します（fallback しません）。

***

## `GET` /api/operator/shelf/by-code

棚コードをスキャンし、その棚にアクティブに紐付いている shipment（注文 / 出荷状態）と、棚在庫（barcode × qty）を返します。

### リクエスト

```http
GET https://api.lmile.io/api/operator/shelf/by-code?shop=demo-store.myshopify.com&shelfCode=A-7-WQ
Authorization: Bearer {OPERATOR_JWT}
```

| クエリ         | 型      |  必須 | 説明                                 |
| ----------- | ------ | :-: | ---------------------------------- |
| `shop`      | string |  ✓  | 店舗ドメイン（スタッフの束縛 shop 内）             |
| `shelfCode` | string |  ✓  | 棚コード（最大 64 文字で受理し、大文字へ正規化してから規格照合） |

### レスポンス

```json
{
  "ok": true,
  "shelfCode": "A-7-WQ",
  "link": {
    "shelfCode": "A-7-WQ",
    "locationId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
    "locationLabel": "大阪MFC",
    "linkedAt": "2026-06-07T09:00:00.000Z",
    "linkedBy": null,
    "shipment": {
      "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
      "status": "secured",
      "trackingNumber": "4912345678901",
      "carrierCode": "yamato",
      "fromLocationId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
      "cancelledAt": null
    },
    "order": {
      "id": "7b1f08a3-1234-4abc-9def-0123456789ab",
      "orderNumber": "1001",
      "sourceSystem": "shopify",
      "status": "routed"
    }
  },
  "inventory": [
    {
      "locationId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
      "barcode": "4912345678901",
      "qty": 3,
      "updatedAt": "2026-06-07T09:05:00.000Z"
    }
  ]
}
```

| フィールド         | 説明                                                |
| ------------- | ------------------------------------------------- |
| `link`        | アクティブ紐付け + enrich した shipment / 注文（紐付け無しは `null`） |
| `inventory[]` | 棚在庫の qty 集約行（同一棚に複数 barcode がありうるので配列。空でも `200`）  |

### 挙動

* 棚コード不正形式は `422 invalid_shelf_code`。
* 紐付けも在庫も無い棚は `404` ではなく `200` で `link: null` / `inventory: []`（「空」を正常応答で表現）。
* 紐付けが指す shipment / 注文が引けない不整合は `500 data_inconsistency` で表面化します。

***

## `POST` /api/operator/shelf/pick

ユニットQR から対象ユニットを解決し、picked 状態を更新します。picked は配送ライフサイクル status とは直交の作業状態で、`sot.lm_fulfillment_units.picked_at` / `picked_by` で表現します。

### リクエスト

```http
POST https://api.lmile.io/api/operator/shelf/pick
Authorization: Bearer {OPERATOR_JWT}
Content-Type: application/json
```

```json
{
  "shop": "demo-store.myshopify.com",
  "qr": "4912345678901|aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "action": "pick"
}
```

### ボディパラメータ

| パラメータ    | 型      |  必須 | 説明                                          |
| -------- | ------ | :-: | ------------------------------------------- |
| `shop`   | string |  ✓  | 店舗ドメイン（API キーで呼ぶ場合は省略できます）                  |
| `qr`     | string |  ✓  | ユニットQR（`<barcode>\|<shipmentId>`。最大 200 文字） |
| `action` | string |     | `pick`（既定）/ `unpick`（picked 取り消し）           |

### レスポンス

```json
{
  "ok": true,
  "action": "pick",
  "unitId": "uuuuuuuu-1111-2222-3333-444444444444",
  "picked": true,
  "alreadyPicked": false,
  "pickedAt": "2026-06-07T09:10:00.000Z",
  "pickedBy": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "units": {
    "total": 2,
    "picked": 2
  }
}
```

| フィールド                               | 説明                                                    |
| ----------------------------------- | ----------------------------------------------------- |
| `action`                            | `pick` / `unpick`                                     |
| `unitId`                            | 今回対象になったユニット                                          |
| `picked`                            | 操作後の picked 状態                                        |
| `alreadyPicked` / `alreadyUnpicked` | 既にその状態だった（冪等な再スキャン）か                                  |
| `pickedAt` / `pickedBy`             | 完了日時 / 操作者（operator JWT 経由のみ非 null = `auth.users.id`） |
| `units.total` / `units.picked`      | shipment 配下の barcode 一致ユニット総数 / うち picked 済（操作後）      |

### 挙動・冪等性

* **順次 pick**: 2 値QR は同一 barcode の複数ユニットを区別しないため、`pick` はスキャンごとに「未 picked の `lineInstanceNo` 昇順先頭 1 件」を picked にします（数量 2 なら 2 回のスキャンで全て picked）。
* **冪等**: barcode 一致ユニットが全て picked の状態での再 `pick` は `200` + `alreadyPicked: true`（上書きしません）。全て未 picked での `unpick` も同様（`alreadyUnpicked: true`）。
* **取り消し**: `action=unpick` は「picked の `lineInstanceNo` 降順先頭 1 件」（= 直近の pick から LIFO）の `picked_at` / `picked_by` を `NULL` に戻します。
* **自己在庫の棚 qty 減算（LM-769）**: 対象ユニットが自己在庫 MFC（`sot.lm_shelf_inventory` に棚在庫を持つ出荷元）に引き当てられている場合、`pick`（未 picked→picked の遷移）でそのユニットが置かれていた棚の `qty` を **−1（`reason=pick`）** し、`unpick`（picked→未 picked の遷移）で **+1（`reason=receive`）** 復元します。減算する棚は star ラベルが作業者に提示する棚（同一 `(location, barcode)` で qty>0 優先・同点は `shelf_code` 昇順の 1 棚）です。続けて自己在庫の `available_qty` を再計算し（[MFC 入庫](/lmile-docs/ingestion-api/mfc-inventory.md) LM-761 と同式）、変化分を源在庫 webhook（[Webhooks](https://github.com/falcon0119/inventory-os/tree/main/docs/webhooks/README.md)）で販売面へ反映します。棚を持たない店舗 / 同期拠点のユニットは該当棚が無いため no-op です。減算/復元は `picked_at` の遷移時のみ行うため、冪等な再 `pick` / 再 `unpick` では二重に増減しません。ベストエフォートで、棚操作が失敗しても `pick` 自体は成功し、`available_qty` は後続の再計算が冪等に整合させます。
* QR 不正形式は `422 invalid_qr`、旧規格は `422 legacy_qr`。shipment 配下に barcode 一致 unit が無ければ `404 unit_not_found`。

## エラー

エンドポイント固有（3 エンドポイント共通の `error_kind`）:

| Status | 条件                                                                                                               | レスポンス例                                                                                                     |
| :----: | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|  `422` | ユニットQR 形式不正                                                                                                      | \`{ "ok": false, "error\_kind": "invalid\_qr", "error": "unit QR must be                                   |
|  `422` | 旧 mfc QR（受理せず再印刷を案内）                                                                                             | \`{ "ok": false, "error\_kind": "legacy\_qr", "error": "legacy unit QR (JAN                                |
|  `422` | 棚コード形式不正（`by-code`）                                                                                              | `{ "ok": false, "error_kind": "invalid_shelf_code", "error": "invalid shelf code format" }`                |
|  `404` | shipment が存在しない（`lookup`）                                                                                        | `{ "ok": false, "error_kind": "shipment_not_found", "error": "fulfillment shipment not found" }`           |
|  `404` | barcode 一致 unit が存在しない（`lookup` / `pick`）                                                                        | `{ "ok": false, "error_kind": "unit_not_found", "error": "fulfillment unit not found" }`                   |
|  `409` | ユニットが[倉庫間移動](/lmile-docs/transfer-api/batches.md)（出荷元変更）フロー中（`pick`。pick/unpick の対象外・行き先 / バルク所属を `units[]` に同梱） | `{ "ok": false, "error_kind": "transfer_required", "units": [...] }`                                       |
|  `500` | shipment / 紐付けが指す注文の欠落（データ異常）                                                                                    | `{ "ok": false, "error_kind": "data_inconsistency", "error": "routing_order not found for shipment ..." }` |

共通エラー（`401` / `400` / `403` / `405` / `500` / `503`）は [Error Codes](/lmile-docs/reference/error-codes.md) を参照。

## データモデル

* `sot.lm_fulfillment_units` — [Units](/lmile-docs/fulfillment-api/units.md) の単位明細。`picked_at` / `picked_by`（MFC 内ピッキング完了 = 配送 status と直交の作業状態。旧 mfc `ready_for_pickup.status=picked` 相当）を本 API が更新します。
* `sot.lm_shelf_links` / `sot.lm_shelf_inventory` — [Shelf Link](/lmile-docs/shelf-api/link.md) / [Shelf Inventory](/lmile-docs/shelf-api/inventory.md) を参照。read 用 view: `public.lm_v_shelf_links` / `public.lm_v_shelf_inventory`（内部 service / 管理用途）。本 read API は service\_role で `sot.*` を直接参照し、operator JWT の束縛 shop で絞り込みます。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://lmile.gitbook.io/lmile-docs/shelf-api/lookup.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
