# AMOPS 外部連携API v1

在庫、入荷予定、日次締め済みの売上を、連携先のサーバーから取得します。
現在はローカル開発用です。外部公開はしていません。本番ではHTTPS、APIゲートウェイ、アクセス監視の構成が必要です。

## キーの管理

企業本部でログインし、「設定・ヘルプ → 外部サービス連携」から発行します。
連携先名、対象店舗、閲覧項目、原価の許可、有効日数（1〜365日）を指定し、パスワードで本人確認します。
サンプル利用者・店舗スタッフ・店長・提供会社は発行できません。

キー本体は発行成功時に一度だけ表示し、サーバーにはSHA-256ハッシュを保存します。
取得に失敗した場合も同じ発行IDで重複発行を防ぎますが、キー本体の再表示はできません。管理画面からそのキーを失効して再発行してください。
連携先のシークレットストアに保管し、ブラウザーのJavaScript、URL、共有するソースコードに記載しないでください。
更新時は新しいキーへ切り替え、旧キーを失効します。有効なキーは企業あたり50件、管理一覧は有効なキーを優先して最新200件です。

発行者のユーザー・所属が停止中、本部権限がない場合もリクエストを拒否します。発行者の権限が戻ると再び使えるため、永久に止める場合はキーを失効してください。

## 認証・制限

`Authorization: Bearer <APIキー>` が必須です。`Bearer` の大文字・小文字は区別しませんが、キー本体は発行時の表記のまま送ってください。
店舗向け・提供会社向けのログインCookie、URLのクエリ、Basic認証ではキーを受け付けません。
GETのみ対応です。ほかのメソッドは405と `Allow: GET` を返します。書き込み、任意SQL、書類原本、利用者情報の取得はできません。
URLは4096文字までです。応答には `Cache-Control: no-store` を付けます。
1キーあたり1分間120リクエスト、1ページ最大200行。制限時は429と `Retry-After: 60` を返します。
利用回数は認証できたリクエストを数えます。権限・入力条件による拒否も含みます。

```sh
# PLAYBASE_API_KEY は連携先の秘密情報ストアから設定します。
curl --fail-with-body \
  -H "Authorization: Bearer ${PLAYBASE_API_KEY}" \
  'http://127.0.0.1:4317/api/v1/inventory?store=yokohama&limit=100'
```

## 取得先

| GET                                           | 権限             | 内容                                                                               |
| --------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------- |
| `/api/v1/inventory`                           | `inventory:read` | 帳簿在庫、期限内で使える数量、移動引当、未転記払出。原価許可時のみ在庫評価額       |
| `/api/v1/receipts`                            | `receipts:read`  | 予定数量・受入済み・不良隔離・返品・未着数量・予定日・伝票番号。原価許可時のみ単価 |
| `/api/v1/sales?from=2026-09-01&to=2026-09-30` | `sales:read`     | 各営業日の最新の締め版。原価許可時のみ払出原価                                     |

共通クエリ：`store`（省略するとキーの対象店舗すべて）、`limit`（1〜200、既定100）、`cursor`。
売上は `from` と `to` が必須です。日付はYYYY-MM-DD、両端を含み、差は最大366日です。
許可していない店舗・項目は403、未知・重複したクエリは400です。

応答例：

```json
{
  "data": [
    {
      "store": "yokohama",
      "sku": "P001",
      "name": "サンプル商品",
      "category": "ぬいぐるみ",
      "active": true,
      "expiry_kind": null,
      "quantity": 10,
      "version": 1,
      "reserved": 2,
      "pending_payouts": 0,
      "usable": null
    }
  ],
  "next_cursor": null,
  "as_of": "2026-09-13T00:00:00.000Z",
  "scope": { "stores": ["yokohama"], "cost_included": false }
}
```

金額は小数点付きの文字列で返します。JavaScriptの浮動小数点で直接加算せず、小数演算ライブラリ等を利用してください。
原価の許可がないキーには原価のプロパティ自体を返しません。

## ページングとデータの意味

`next_cursor` がある間は、同じキー・取得先・店舗・日付条件で `cursor` に渡してください。独自に加工しないでください。
店舗→商品コード、店舗→伝票ID、店舗→営業日の順で取得します。
ページをまたいだ取得は一括のスナップショットではありません。取得中の追加・訂正があり得ます。
在庫は定期的に全ページを再取得し、`store + sku` で更新してください。売上は `store + business_date` で最新 `revision` を反映し、過去日訂正に備えて対象期間を再取得してください。

- `quantity` は帳簿在庫です。期限管理する商品では、期限切れ・期限不明・出荷停止ロットも含みます。
- `usable` は期限管理する商品（`expiry_kind` が `best_before`・`use_by`・`internal`）だけに返します。期限日が分かり、店舗の営業日時点で期限内で、出荷停止されていないロットの合計です。払出と店舗間移動はこの数量までしか使えません。期限管理しない商品は `null` です。
- `in_booths` は筐体内（ブース）に入っていると記録された数量で、`quantity` に含まれます。商品合計の払出や店舗間移動はこの分を使えません（ブース別の払出で減ります）。
- `quantity - reserved - pending_payouts` や `usable` を、そのまま店頭で提供できる数と扱わないでください。移動の引当や未転記払出の差し引き方は連携先の用途で決めてください。
- `active` が `false` の商品も、在庫が残っていれば返します。
- 入荷予定は在庫を増やす記録ではありません。`received` は在庫へ受け入れた数量、`defective` は受入時に不良として隔離した数量（在庫・評価額に含まない）、`defective_returned` はそのうち仕入先へ返品した数量、`returned` は受入済み在庫から返品した数量です。
- `closed_quantity` は打ち切った数量です（欠品・発注取消・二重登録などで届かないことが確定した分）。
- `open_quantity` は未着数量で、`expected - received - defective - closed_quantity` です。受入数を訂正して減らした分は、再び未着数量に戻ります。返品した分の代替納入は別の入荷予定になり、`replaces` に元の入荷予定IDが入ります。
- 売上は最新の日次締めだけを返し、営業中の日は含めません。検針未入力や未転記払出がある状態の締めもあり、`meters_entered / meters_total` と `pending_payouts` を確認してください。休業日として締めた日は `holiday: true`（検針・払出なし、売上0）です。会計売上や営業利益ではありません。

## エラー

`{"error":"説明","code":"識別子"}` を返します。
|状態|code|内容|
|---|---|---|
|400|なし|未知・重複したクエリ、`limit` が範囲外、`cursor` が別条件、日付範囲の誤り、URLが長すぎる|
|401|`INVALID_API_KEY`|ヘッダーがない・キー不正・期限切れ・失効・発行者の停止や本部権限の解除。`WWW-Authenticate: Bearer` を付けます|
|403|`SCOPE_REQUIRED`|キーに許可していない閲覧項目|
|403|`STORE_REQUIRED`|キーに許可していない店舗|
|404|なし|取得先がない|
|405|`METHOD_NOT_ALLOWED`|GET以外。`Allow: GET` を付けます|
|429|`RATE_LIMITED`|1分間の上限超過。`Retry-After: 60` 秒後に再試行してください|

400・401・403・404は同じ内容で再送しても結果は変わりません。条件やキーを見直してください。
GETは何度再送してもデータを変更しません。ネットワーク切断時は同じURLで再送して構いません（利用回数には数えます）。
500はサーバー側の障害です。間隔を空けて再試行してください。
失効・権限変更は以後の認証から適用します。すでに認証された処理の応答を取り消すものではありません。

構造定義は `integration-openapi.json` を参照してください。
