カタログ・在庫・引当(Square Catalog / Inventory)
1. 方針
| 項目 |
決定 |
| マスター |
Square Catalog |
| 在庫 |
Square Inventory(on_hand)+当方の引当 |
| 鮮度 |
webhookで秒、フォールバックで15分 |
| 表示(顧客) |
在庫あり / 残りわずか / 売切れ(数は出さない) |
| 表示(店員) |
手元・引当・カート内・販売可能(数) |
| 引当 |
注文作成時にhold。期限切れ/取消で解放、決済完了で確定 |
| 引当TTL |
注文10分 / 決済待ち15分 |
| 安全在庫 |
店員アプリで設定(admin)、既定0、店舗単位 |
| カート下書き |
heartbeat 120秒・TTL 5分・変更時2秒デバウンス・匿名 |
| ロケーション |
今は1。キーには locationId を含める |
| バーコード |
今はUI非対応。索引は設計に含める |
可用数の定義
available = on_hand - orderHolds - safetyStock
cartHolds は表示専用(下書きカート合計)。可用数からは差し引かない(二重計上を避ける)
- 注文が期限切れ/取消になると
orderHolds は解放され、カート下書きが残っていれば店員画面の「カート内」に再び現れる
2. データモデル
| キー |
内容 |
SHOP#{shopId}/META |
catalogVersion / lastCatalogSync / sync{auto,interval,lastRunBy} / safetyStock |
SHOP#{shopId}/CATALOG |
カタログスナップショット(配信用) |
CATALOG#{itemId}/META |
商品名・カテゴリ・税 |
CATALOG#{itemId}/VAR#{variationId} |
SKU・barcode・価格・trackInventory・locationOverrides.soldOut |
BARCODE#{barcode}/META |
→ variationId(スキャン用) |
INV#{locationId}/VAR#{variationId} |
onHand / reserved / soldOut / calculatedAt |
RESERVE#{orderId}/META |
引当明細・TTL |
CART#{cartId}/META |
下書きカート(shopId・items・TTL・GSI1PK=SHOP#{shopId}#CART) |
WEBHOOK#{eventId}/META |
冪等(TTL 30日) |
3. 同期
| 対象 |
方式 |
| Catalog 全件 |
SearchCatalogObjects(ITEM/ITEM_VARIATION/CATEGORY/TAX)→ スナップショット+個別項目を更新 |
| Catalog 増分 |
catalog.version.updated → begin_time=lastCatalogSync で差分(latest_time 保存) |
| Inventory |
inventory.count.updated(秒)→ INV# を更新。BatchRetrieveInventoryCounts(updated_after) で再取得 |
| フォールバック |
15分間隔(自動同期ON時) |
| 売切れ |
sold_out を最優先(在庫数より優先) |
| 失敗時 |
前回キャッシュ保持+staleSeconds を返す |
| 参照元 |
/api/bootstrap・/api/catalog・注文価格解決は snapshot を参照(未作成時のみmock) |
店員アプリからの操作
- 表示: 最終同期、カタログ版、鮮度、商品数
- 操作(admin): 今すぐ同期 / 自動ON-OFF / 頻度(1/5/15分、既定5)
- 手動同期は30秒のクールダウン。監査ログに記録
API
| メソッド |
パス |
内容 |
| GET |
/api/catalog |
顧客向け(availability) |
| GET |
/api/staff/catalog |
店員向け(数・cartQty・staleSeconds) |
| GET/PUT |
/api/staff/sync |
同期状態・設定(adminで更新) |
| POST |
/api/staff/sync/run |
手動同期(admin) |
| POST |
/api/carts/{cartId} |
カート下書き(匿名・レート制限) |
| POST |
/webhooks/square |
署名検証・冪等 |
4. 引当
注文作成時
- 明細の価格を**マスター(キャッシュ)**から解決
RESERVE#{orderId} に明細+TTLを記録
INV# を条件付き更新(楽観ロック)reserved = if_not_exists(reserved, 0) + n
条件: soldOut = false かつ reserved = 読み取り時の値
事前に可用数(onHand - reserved - safetyStock >= n)を確認し、競合時は失敗扱い。
- 失敗時は
409 OUT_OF_STOCK(該当商品の詳細付き)
解放・確定
| 事象 |
処理 |
| 取消 / 期限切れ / FAILED |
reserved を戻す+RESERVE削除 |
| 決済完了(PAID) |
reserved を戻す(Squareがon_handを減算)+RESERVE削除 |
| カート下書きが有効 |
表示上「カート内」に戻る(可用数は解放済み) |
整合性
- TTL切れRESERVEは定期同期(
syncHandler、既定15分)で掃除し reserved を戻す
- 注文作成の再送(同じ
clientToken)は既存注文を返し、二重引当しない
- 同時引当は条件付き更新で片方のみ成功
5. カート下書き(B)
- 顧客PWA: カート変更を2秒デバウンスして
POST /api/carts/{cartId}
- 保存:
CART#{cartId}(TTL 5分)、heartbeat 120秒、画面非表示/空カートは送信停止
- 店員:
GSI1(SHOP#{shopId}#CART) で有効カートを取得し、SKU別に合計
- プライバシー: 匿名・TTLで消滅・レート制限(1端末 1req/2秒)
6. 安全在庫
- 店舗単位(
SHOP#{shopId}.safetyStock、既定0)。将来SKU単位
- 店員アプリ(admin)で変更。変更は即時反映
available = on_hand - reserved - safetyStock
7. フェーズ
| Phase |
内容 |
状態 |
| C-1 |
CatalogProvider+キャッシュ+同期+店員の同期設定 |
実装済み |
| C-2 |
Inventory+可用数+顧客/店員表示 |
実装済み |
| C-3 |
引当(hold/release/commit)+OUT_OF_STOCK |
実装済み |
| C-4 |
カート下書き+店員のカート内合計 |
実装済み |
| C-5 |
安全在庫の店員設定 |
実装済み |
| C-6 |
複数ロケーション |
後続 |
| C-7 |
バーコード索引+スキャンUI |
M8 |
8. テスト
- Contract: mock / cache で list・get・barcode・version
- 同期: 全件・増分・
sold_out・順不同/重複webhook・失敗時のフォールバック
- 引当: 最後の1点を2注文同時 → 片方のみ成功
- E2E: 売切れ商品の注文で
OUT_OF_STOCK、店員画面に在庫数
- カート下書き: TTL後に合計から消える
9. コスト・セキュリティ