# Inventory Levels — 在庫

在庫を Last Mile の在庫 SoT (`sot.inventory_levels`) に取り込みます。用途別に、現在庫の**絶対値スナップショット**と、販売可能在庫の**リアルタイム更新**の 2 エンドポイントに分かれます。商品は `barcode`、拠点は `location_id` または外部識別子で解決します。

> **Base URL** `https://api.lmile.io` · **認証** `x-ingest-secret` ヘッダー · `shop` はリクエストボディに含めます

## エンドポイント

| Method | Path                              | 用途                                                   |
| ------ | --------------------------------- | ---------------------------------------------------- |
| `POST` | `/api/ingest/inventory-snapshot`  | 1 拠点の現在庫を絶対値で一括投入（全在庫を上書き）                           |
| `POST` | `/api/ingest/inventory-available` | 販売可能在庫 (`available_qty`) のリアルタイム更新（absolute / delta） |

## 認証

```http
x-ingest-secret: {INGEST_SECRET}
Content-Type: application/json
```

| Header            | Required | 説明                           |
| ----------------- | :------: | ---------------------------- |
| `x-ingest-secret` |     ✓    | ingest API シークレット（サーバーサイドのみ） |

### 拠点（location）の指定

両エンドポイント共通。`location_id`（内部 UUID）か `location`（外部識別子）のいずれかを指定します。

```jsonc
"location_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
// または
"location": { "identifier_type": "smaregi_store_id", "identifier_value": "4" }
```

***

## `POST` /api/ingest/inventory-snapshot

「今この拠点にこの商品が何個あるか」を絶対値で送信します。`sot.inventory_levels` を upsert し、当日の `sot.inventory_daily_snapshots` も更新、差分を `sot.inventory_update_logs` に記録します。

### リクエスト

```http
POST https://api.lmile.io/api/ingest/inventory-snapshot
x-ingest-secret: {INGEST_SECRET}
Content-Type: application/json
```

```json
{
  "shop": "demo-store.myshopify.com",
  "source_system": "smaregi",
  "location": { "identifier_type": "smaregi_store_id", "identifier_value": "4" },
  "inventory_as_of": "2026-06-02T18:00:00+09:00",
  "idempotency_key": "smaregi-2026-06-02-loc4",
  "lines": [
    { "barcode": "1234567890123", "on_hand_qty": 10, "available_qty": 8, "allocated_qty": 2 }
  ]
}
```

### ボディパラメータ

| パラメータ                                                 | 型               |  必須 | 説明                             |
| ----------------------------------------------------- | --------------- | :-: | ------------------------------ |
| `shop`                                                | string          |  ✓  | 店舗ドメイン                         |
| `source_system`                                       | string          |  ✓  | 取込元。ログの `change_source` に記録    |
| `location_id` / `location`                            | string / object |  ✓  | いずれか必須（上記参照）                   |
| `lines`                                               | array           |  ✓  | 在庫行の配列（最大 10,000 件）            |
| `inventory_as_of`                                     | ISO 8601        |     | スナップショット時刻（default: 現在）        |
| `idempotency_key`                                     | string          |     | 監査用キー                          |
| `lines[].barcode`                                     | string          |  ✓  | 商品 barcode                     |
| `lines[].on_hand_qty`                                 | integer (≥0)    |  ✓  | オンハンド在庫                        |
| `lines[].available_qty`                               | integer         |     | 販売可能在庫（**省略時は `on_hand_qty`**） |
| `lines[].allocated_qty` `reserved_qty` `incoming_qty` | integer         |     | 引当 / 予約 / 入荷予定（default: 0）     |

### レスポンス

```json
{
  "ok": true,
  "shop": "demo-store.myshopify.com",
  "location_id": "c1cdb7d4-d435-40a6-9c7f-95108f99b35e",
  "lines_processed": 3,
  "lines_upserted": 2,
  "lines_unresolved": 1,
  "logs_written": 2,
  "unresolved": [ { "barcode": "0000000000000", "reason": "product_not_found" } ]
}
```

### 挙動・冪等性

* `sot.inventory_levels` を `shop + product_id + location_id` で upsert、当日の `sot.inventory_daily_snapshots` も upsert。
* `sot.inventory_update_logs` に `change_type='snapshot'` を**値が変わった行のみ**記録。
* 絶対値 upsert のため**再送に冪等**（在庫・日次は重複せず、無変更行はログも増えない）。
* `barcode` が商品マスター未登録の行は skip し `unresolved[]` に返します。

***

## `POST` /api/ingest/inventory-available

販売可能在庫 (`available_qty`) **だけ**を即時更新します（`on_hand_qty` / `allocated_qty` / `reserved_qty` は据え置き）。1 リクエストで 1〜数件の `(barcode × location)` を更新する用途。

`mode` で更新方式を切り替えます。

| `mode`     | 挙動                                                                                              |
| ---------- | ----------------------------------------------------------------------------------------------- |
| `absolute` | `available_qty = N` で上書き（再送に冪等）                                                                 |
| `delta`    | `available_qty += d` を積算。結果が負なら **0 にクランプ**。`idempotency_key` で best-effort 冪等（同一キーの二重適用を skip） |

### リクエスト

```json
// absolute
{
  "shop": "demo-store.myshopify.com",
  "source_system": "shopify",
  "mode": "absolute",
  "location_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "lines": [ { "barcode": "1234567890123", "available_qty": 8 } ]
}
```

```json
// delta
{
  "shop": "demo-store.myshopify.com",
  "source_system": "pos",
  "mode": "delta",
  "idempotency_key": "sale-1234567890",
  "location": { "identifier_type": "smaregi_store_id", "identifier_value": "4" },
  "lines": [ { "barcode": "1234567890123", "available_delta": -2 } ]
}
```

### ボディパラメータ

| パラメータ                      | 型               |      必須      | 説明                     |
| -------------------------- | --------------- | :----------: | ---------------------- |
| `shop`                     | string          |       ✓      | 店舗ドメイン                 |
| `source_system`            | string          |       ✓      | 取込元                    |
| `mode`                     | string          |       ✓      | `absolute` または `delta` |
| `location_id` / `location` | string / object |       ✓      | いずれか必須                 |
| `lines`                    | array           |       ✓      | 更新行の配列（最大 10,000 件）    |
| `inventory_as_of`          | ISO 8601        |              | 反映時刻（default: 現在）      |
| `idempotency_key`          | string          |              | delta の二重適用防止に推奨       |
| `lines[].barcode`          | string          |       ✓      | 商品 barcode             |
| `lines[].available_qty`    | integer (≥0)    | absolute 時 ✓ | 上書きする販売可能在庫            |
| `lines[].available_delta`  | integer         |   delta 時 ✓  | 変動量（負可）                |

### レスポンス

```json
{
  "ok": true,
  "mode": "delta",
  "shop": "demo-store.myshopify.com",
  "location_id": "c1cdb7d4-d435-40a6-9c7f-95108f99b35e",
  "lines_processed": 2,
  "lines_updated": 1,
  "lines_unresolved": 0,
  "lines_clamped": 1,
  "lines_deduped": 0,
  "unresolved": [],
  "clamped": [ { "barcode": "1234567890123", "computed": -85, "set": 0 } ]
}
```

### 挙動・冪等性

* `sot.inventory_levels.available_qty` のみ更新（`on_hand_qty` 等は不変）。対象行が無ければ作成（`on_hand_qty=0`）。
* `sot.inventory_update_logs` に `change_type='available_set'`（absolute）/ `'available_delta'`（delta）を値が変わった行のみ記録。
* **日次スナップショットは更新しません**（日次履歴は snapshot エンドポイントの責務）。
* delta は順次リトライを吸収する best-effort 冪等のため、イベント単位で一意な `idempotency_key` を付与してください。

***

## エラー

| Status | 条件                          | レスポンス例                                                                                                     |
| :----: | --------------------------- | ---------------------------------------------------------------------------------------------------------- |
|  `401` | `x-ingest-secret` 不一致       | `unauthorized`                                                                                             |
|  `400` | ボディ形式エラー                    | `{ "ok": false, "error": "shop required" }`                                                                |
|  `422` | `ok:false`（`error_kind` 付き） | `{ "ok": false, "error_kind": "location_unresolved", "error": "no location for smaregi_store_id=999999" }` |
|  `500` | サーバー内部エラー                   | `{ "ok": false, "error": "..." }`                                                                          |
|  `503` | `INGEST_SECRET` 未設定         | `{ "ok": false, "error": "INGEST_SECRET not configured" }`                                                 |

`error_kind`: `invalid_payload` / `location_unresolved` / `location_ambiguous`。

## データモデル

* `sot.inventory_levels` — 現在庫マスター（`shop + product_id + location_id` で一意）
* `sot.inventory_daily_snapshots` — 日次履歴（`+ snapshot_date`）
* `sot.inventory_update_logs` — 変更監査ログ（`change_type`: `snapshot` / `available_set` / `available_delta`）
* read は `agent_read.inventory_levels` / `inventory_daily_snapshots` / `inventory_update_logs`（safe view）経由


---

# Agent Instructions: Querying This Documentation

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

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lmile.gitbook.io/lmile-docs/ingestion-apis/inventory-levels.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
