Idempotency
全公開 API の冪等性の一覧です。リトライ・二重送信時の挙動はここを正とします。
Ingestion API
Products — /api/ingest/products
shop + barcode で UPSERT。同じ barcode を何回送っても安全です。
値に変更がなければ
unchanged値に変更があれば
updated+product_update_logsにchanged_fieldsを記録GET /api/ingest/accessory-services(read)は書き込みが無いため冪等性の対象外です。何回呼んでも安全です。
Inventory Snapshot — /api/ingest/inventory-snapshot
店舗 × 商品 × 拠点ごとの 1 行を絶対値で丸ごと置き換えます。同じ値の再送は在庫・日次とも重複せず、変更ログも値が変わった行のみ追記されます(再送に冪等)。読み値が反映済みより古い行は適用されず stale_skipped[] で返ります(同時刻は適用 = 再送冪等を維持。→ 古い読み値の上書き防止)。
Inventory Available — /api/ingest/inventory-available
mode=absolute:available_qty = Nの上書きのため再送に冪等。mode=delta: 積算のため本質的に非冪等。idempotency_keyを付与すると、同一キー × 同一(product, location)のavailable_deltaログが既存なら適用を skip(lines_deduped)。順次リトライを吸収する best-effort であり、完全な並行重複保証ではないため、イベント単位で一意なidempotency_keyを付与してください。committed_qty/on_hand_qty(任意): 送られた行だけ absolute で上書きするため再送に冪等。省略した行は据え置き(modeに依らず常に absolute)。ルーティングはavailable_qty + committed_qtyで引き当てを判定します。as_of(任意): 行の読み値を在庫システム側で読み取った時刻。反映済みより古い行は適用されずstale_skipped[]で返ります(同時刻は適用 = 再送冪等を維持。→ 古い読み値の上書き防止)。
Channel Available — /api/ingest/channel-available
shop + location_id + barcodeの行を、送ったchannel_available_qtyで上書きするため再送に冪等です。行の
as_of(省略時はinventory_as_of→ 受信時刻)が、反映済みの値の時刻より古い行は適用されずstale_skipped[]で返ります。順序が入れ替わって届いても新しい値が勝ちます。倉庫から届いた在庫数(
on_hand_qty)は変わりません。販売可能在庫は、チャネルの値が 0 以下なら 0、それ以外は通常の計算で決まります。
MFC Inventory — /api/ingest/mfc-inventory
shop + fulfillment_source_location_id + shelf_code + barcode で棚別 qty を反映し、同一トランザクションで barcode を全棚合算し sot.inventory_levels.available_qty を更新します。
mode=absolute(既定 / 省略時): 絶対値 set のため同一ボディの再送に冪等です(棚在庫・合算とも重複しません)。mode=adjust: 符号付き差分の加減算のため本質的に非冪等です。冪等キーは持たないため、リトライ時は適用済みかを呼び出し側で確認してください(既存 routing write の secure / oversell と同方針)。
barcode 未登録(product_not_found)の行や adjust で在庫が負になる行は、silent skip せずバッチ全体を reject します(部分適用なし)。
Stocktake — /api/operator/stocktake/*
open: 拠点ごとに進行中のセッションは 1 つ(部分 UNIQUE)。進行中があれば409 session_already_openでその id を返す(新しく作らない)。shelf-start: 数えている途中の棚への再 start は何もしない。締めた棚への start は再計数(前の計数はsupersededとして残す)。shelf-submit: 棚単位の置き換え。同じ内容の再送は同じ結果(二重にならない)。帳簿数(expected)は締めた時点で固定。transition: 状態機械。許されない遷移は409 invalid_state。location(店舗)のsubmitは商品ごとの帳簿数の行を作り直す(取り下げ → 再提出で置き換わる)。commit(location): 商品単位でapplied_atを付け、外部システムへの調整はUNIQUE (session_id, product_id)で 1 行だけ作る。再 commit で二重に作らない。commit(shelf): 商品(barcode)単位の部分反映で冪等。反映した行にはapplied_atが付き、再 commit は未反映の行だけを処理する。差分(counted − expected)を当てるので、締めたあとの出荷・入庫と二重にならない。保留(counted_below_active/negative_after_delta)は再 commit で再評価する。committedへの再 commit はalreadyCommitted: trueの no-op。商品ごとに(shop, location, barcode)の advisory lock を取り、引き当て・再計算・数量修正と直列化する。
Inventory Holds — /api/inventory/hold / /api/inventory/release
hold は shop + reference_id + location + barcode の取り置き数を絶対値で設定します(quantity: 0 は削除)。同じ内容の再送は同じ結果に収束し、履歴(inventory_ledger)も数が変わったときだけ追記します。reference_id を省略すると毎回新しい参照 ID が生成されるため、省略した再送は二重の取り置きになります。1 リクエスト内の全行は 1 トランザクションで、1 行でも失敗すれば全行を取り消します。release は shop + reference_id の取り置きを削除し、該当なしの再送も ok: true(空配列)です。
Order Line Master — /api/order-line-master/bulk-upsert
shop + source_system + transaction_id + transaction_line_id + line_instance_no で UPSERT。同一行の再送は差分があれば更新(無ければ実質 no-op)、order_line_master_update_logs に記録します。
Share Analytics — /api/ingest/share-analytics
source + source_share_id(shares)/ source + source_item_id(items)/ source + source_event_id(events)の UNIQUE への upsert。同一行の再送・backfill 重複に冪等です。行単位の不備(必須欠落・許可外 event_type・不正/未来 occurred_at)は skipped[](reason 付き)で返し、他行の処理は継続します。
Routing API
Orders — /api/routing/orders
入口冪等:
shop + sourceSystem + externalOrderRefで routing order 1 行に収束。status = dispatchedの注文を再 POST すると再ルーティングせず、既存の出荷指示をidempotent: trueで返します(外部への再送なし)。出口冪等: 出荷指示は
shop + idempotencyKey(省略時{sourceSystem}:{externalOrderRef})で UPSERT し、外部 fulfillment への二重送信を防ぎます。まだ dispatch されていない注文の再 POST は、前回の decision を破棄して再計算します。
Orders (shipped) — /api/routing/orders/shipped
shop + sourceSystem + externalOrderRefの注文に、同じ拠点の出荷が既にあれば何も変えずにidempotent: true。そのまま再送してよいです。同じ注文で別の拠点を送ると、その拠点の出荷が追加されます。取消済みの注文は
409 order_cancelled。
Cancel / Update — /api/routing/cancel / /api/routing/update
cancel: 既に
cancelledの注文は再 dispatch せずidempotent: true。取消は SoT 側で常に確定(WMS への cancel 通知は best-effort)。update: 全量再ルーティング。出荷指示は元注文の
idempotencyKeyを維持するため、shop + idempotencyKeyの同一行を更新(create→update で二重作成しない)。
Inventory Adjustments — /api/routing/inventory-adjustments/*
claim: 取った行を同じ呼び出しの中でclaimedにする(FOR UPDATE SKIP LOCKED)。同時に呼んでも同じ行は 1 つにしか渡らない。claimedのままreclaimAfterMinutes(既定 30 分)を過ぎた行は取り直す(attemptsが増える)。result:claimed/unknownの行だけ確定する。applied/failedへの再報告はrecorded: falseの no-op(結果を上書きしない)。差分は相対値なので、外部システムへ 2 回当てると二重になる。結果が分からないときは
unknownで返し、自動では再送しない。
WMS Shipping Request — /api/routing/wms-shipping-request
冪等キーはありません。1 回の呼び出し = 1 件の送信記録 +(live 時)1 回の送信です。
★同じ伝票番号での送り直しは内容が反映されません。 WMS は伝票番号ごとに 1 伝票を持ち、2 回目以降を無視します(上書きではありません)。重複送信は無害ですが、初回に送った内容がそのまま出荷されるため、送る前に内容を確定させてください。あとから変えたい場合は取り下げてから別の伝票として作り直します。1 注文を複数の荷物に分けるときは荷物ごとに別の伝票番号(2 本目以降は連番)で送ります。
送信が失敗したときは WMS に届いていないので、そのまま再送して構いません。shadow は記録のみで送信しません。リトライはこの API 自身は行わず、送信側のループが担います。
Fulfillment API
Secure — /api/routing/secure
単位は
shop + routing_order_id + line_instance_no + barcodeで一意。確保対象はallocatedの単位のみで、既にsecuredの単位は再確保せずalreadySecuredに計上(再 POST は no-op)。出荷元ヘッダの全単位が確保済になった遷移時のみラベル生成。既に確保済ヘッダの再 secure はラベルを再生成しません。
Picking List — /api/routing/picking-list
非冪等(再発行前提): 生成のたびに新しい PDF をタイムスタンプ付きファイル名で Storage 保存し、
sot.lm_picking_listsに 1 行追記します(再印刷履歴の append-only ログ)。出力は常に最新の routing データ(
sot.lm_routing_decisionsのdecision_status = applied)から組み立てるため、冪等なのは入力側の routing です。売越し・再引き当て後の再生成は最新の引き当て先を反映します。バッチ(
/api/routing/picking-list/batch) も同様に非冪等(生成のたびに Storage 上書き保存)。ただし複数注文にまたがるためsot.lm_picking_listsへの追記は行いません(生成物は公開 URL が成果物)。
Address Recovery — /api/routing/shipment/address / /api/routing/shipment/retry-label
住所修正: 注文 1 件(
shop + sourceSystem + externalOrderRefまたはroutingOrderId)のsot.lm_routing_orders.ship_to_*を部分上書き。同じ住所での再 POST は同値で収束(冪等)。住所はラベル生成の単一ソースのため修正が即反映されます。変更履歴: 住所の更新と変更履歴(
sot.lm_routing_order_ship_to_changes)の記録は同一トランザクション。実際に値が変わったときだけ 1 行 append するため、同値の再 POST(冪等)では履歴を増やしません(before/after を残す append-only ログ)。dry-run:
validateOnly: trueは読み取りのみ(保存・履歴・webhook なし)。何度呼んでも状態を変えません。ラベル再発行: 出荷元(shipment)単位。既に
tracking_numberがある出荷元はalready_issuedで skip し、force: trueのときだけ再発行します(二重発行ガード)。確保前(unallocated/allocated)はnot_securedで skip。再発行成功で
last_label_errorをクリア、失敗で更新。action=label設定が無い出荷元はno_label_configで skip(silent skip しません)。自動再発行(opt-in): 住所修正の
autoRetryLabel: trueは、ラベルエラー中の出荷元に加え、ラベル面に影響する項目が実際に変わった(changedFields)ときだけ発行済み出荷元(secured/oversell_secured・未破棄)を再発行します。同値の再 POST では発行済み出荷元を再発行しません(冪等)。再発行成功時の自動印刷: 印刷イベント(
fulfillment_shipment_status_changed)を印刷アプリへ自動配信します。イベントの冪等キーはroutingOrderId:shipmentId:status:transitionedAt(transitionedAt= 再発行時刻)のため、初回確保イベントと重複起票しません。label_printed_at/last_print_errorは再発行成功時にリセットされます(新ラベルは未印刷)。
Print Label — /api/routing/shipment/print-label
非冪等(再印刷前提): 呼ぶたびに保存済みラベル PDF を出荷元 print server へ送信します(複数回 POST = 複数枚出力)。意図的な再印刷操作としてのみ使用してください。
ラベル生成時の自動印刷(Fulfillment)はラベル生成と同じトリガーに従うため、確保済への遷移時のみ 1 回実行されます(リトライなし。失敗は
last_print_errorに記録し本 API で回収)。
Shipment Memo — /api/routing/shipment/memo
追記のみ(非冪等): 呼ぶたびに
sot.lm_shipment_memosへ 1 行 append します(同じbodyの再 POST も別行)。編集・削除の API はありません(完全な記録 / 監査ログ)。タイムラインはcreated_at降順で表示します。
Shipment Delivery — /api/routing/shipment/delivery
冪等(部分更新): 解決キーは
routingOrderId。sot.lm_routing_ordersのdesired_delivery_date/desired_delivery_timeの 2 列だけを最新値で上書きします(同じ値の再 POST は副作用なし・updated_atのみ前進)。他のヘッダ列は触らず、再ルーティング・再 dispatch も行いません。nullで指定解除。値はラベル生成時に Ship&Cosetup.date/setup.timeへ載るため、既発行の送り状へ反映するにはretry-label(force)で再発行します。遅延のお知らせ:
notifyDelay: trueで遅延になった変更 1 回につき 1 通です。同じ値の再 POST は遅延ではないので送りません。遅延の変更を繰り返すと、そのたびに 1 通送ります。
External Fulfillment — /api/routing/shipment/fulfill-job / /fulfill-result / /fulfill-pending / /api/ingest/sku-mappings
fulfill-job / fulfill-pending / sku-mappings(read): SoT への書き込みが無いため冪等性の対象外です。何回呼んでも安全(同じ入力には同じ時点のスナップショットを返します)。
fulfill-result: 解決キーは
shipmentId。acceptedはexternal_fulfillment_id/ 受理時刻の列上書きのため再送に冪等です(externalFulfillmentIdは同一 shipment で常に同じ決定的な値を推奨。例LM-{shipmentId})。failedはエラー記録の上書きのみ(受理マーカーは既存値を保持)。二重発注ガード: 受理後の
fulfill-jobはalreadyAccepted: trueを返します。イベント再配信で同じ shipment のジョブを再取得しても、呼び出し側はこれを見て外部への再発注を skip できます。
Shipment Ship — /api/routing/shipment/ship
冪等: 既に出荷済みの shipment への再 POST は
200+noop: true(更新も webhook の再発火もしない = 二重 fulfill 防止)。出荷済みへの遷移は 1 回だけ副作用(配下単位への cascade / 棚紐付け解除 / 出荷ステータス webhook)を実行します。
trackingNumber/carrierCode指定時は shipped と同一の書き込みで保存されます(未指定は保存済みの値を使用)。MFC 集約グループの一員を指定した場合はグループ全体を一括で shipped にします(既 shipped グループは
noop: true)。
Shipment Collect — /api/routing/shipment/collect
冪等(残りだけ前進): 解決キーは
shipmentId。集荷済みの単位はそのまま維持し、未集荷の単位だけを集荷済みにします。全単位が集荷済みならalreadyCollected: trueを返します(集荷の更新なし)。issueLabel: trueは発行済みラベル(追跡番号あり)を再発行しません(already_issued)。ラベル発行だけが失敗した出荷は、同じリクエストの再送がそのまま再試行になります。棚の減算処理が途中で失敗した場合、処理済みの単位は集荷済みのまま残ります。再送すれば残りの単位だけが前進します。
Shipment Unsecure — /api/routing/shipment/unsecure
冪等: 解決キーは
shipmentId(+unitId)。既に確保前の単位はそのまま維持し、unsecured: 0を返します。そのまま再送して構いません。対象に解除できない単位が 1 つでもあれば、何も変えずに
409を返します(一部だけ解除されることはありません)。
Carrier Pickup — /api/routing/pickup / /api/routing/pickup/cancel
冪等ではありません(重複依頼は拒否): 解決キーは出荷の
pickup_id列。既に生きた集荷依頼に入っている出荷を含むリクエストは409 pickup_already_requestedで拒否し、既存の受付番号を返します。同じ荷物に受付番号が 2 つ立つことを構造的に防ぐためで、再送しても新しい依頼にはなりません。キャリアへ問い合わせる前に対象の出荷を予約します。同時に 2 か所から依頼しても、あとから来た方はキャリアに触れずに
409で止まります。キャリアへの依頼が失敗した場合は予約が自動で解除され、対象の出荷は未依頼に戻ります(そのまま再送できます)。取消はキャリア側が成功してからこちらを解除します。キャリアの取消が失敗すると
502を返し、出荷は依頼済みのまま残ります(未依頼に戻して二重に呼ばないため)。取消済みの依頼の再取消は409。時間帯や対象の変更は「取消 → 依頼し直し」で行います(依頼の書き換えはできません)。
Oversell API
Reallocate — /api/routing/reallocate
売越し(
oversold)decision を同一出荷元の別メンバーへ再引き当て。旧 decision はsupersededにし後継へリンク(重複適用なし)。出荷指示は
shop + idempotencyKeyで UPSERT されるため、再 dispatch は同一行を更新(外部への二重送信を防止)。
Oversell Cases — /api/routing/oversell-case
1 単位につきケース 1 件(
shop + fulfillment_unit_idで一意)。初回呼び出しで起票、以降は前進。同一 status への再呼び出しは
changed: falseの no-op(timestamp 再設定なし)。不正な遷移は409 invalid_transition。
Source Change — /api/routing/change-source
1 単位につき変更 1 件(
shop + fulfillment_unit_idで一意)。初回呼び出しで起票(change_requested、対象 unit はsecuredのみ=LM-913)、以降は前進。前回がcancelled(終端で前進不可)の単位は同一行を再オープンして再起票します(reopened: true。LM-956。前回の進行・バルク・チェック・返送ラベル列は初期化)。securedからの起票は確保を解除(secured/secured_atクリア)し、旧出荷元ヘッダが空になった場合のみヘッダcancelled・ラベル運用破棄(label_voided_at)・棚紐付け解放(released_reason="source_changed")を行います(残アクティブ unit があるヘッダは触りません = 誤破棄なし)。起票時に旧 decision を
supersededにし後継(出荷元=B)へリンク(重複適用なし)。出荷指示の dispatch(A への cancel / B への create)は WMS 設定が無ければskipped。同一 status への再呼び出しは
changed: falseの no-op。不正な遷移は409 invalid_transition。cancelledは SoT を起票前の出荷元へ巻き戻します(WMS 再連絡は best-effort)。在庫は状態のみでinventory_levelsは不変。mode=switch(即時切替): ケースを起票せず同一レグの出荷元だけを更新します。切替完了後に同じボディを再送すると新旧出荷元が同一のため422 same_sourceで弾かれ、重複適用されません(自己在庫 available の再計算は派生量のため冪等)。
Transfer API
Transfer Batches — /api/operator/transfer/batches (+ add-unit / remove-unit)
バルク作成は呼び出しごとに新規(同一経路の複数
openバルク = 複数箱は許容)。一覧は read 専用。add-unit: 同一バルクへの再スキャンは200の no-op(added: false, alreadyInBatch: true)。別バルク所属は409 already_in_batch。同 barcode の複数単位は未所属の先頭(lineInstanceNo昇順)から 1 件ずつ。remove-unit: 既に外れていれば200の no-op(alreadyRemoved: true)。いずれもopen中のみ(それ以外は409 batch_not_open)。
Transfer Dispatch — /api/operator/transfer/batches/dispatch
dispatched/received済みへの再実行は200+ 既存pdfUrlの no-op(alreadyDispatched: true)。処理順は PDF → 単位遷移 → バルク確定。途中失敗時はバルクが
openのまま残り、再実行がchange_requestedの残りだけ遷移して回復(遷移済みは skip・PDF は同一パスへ上書き)。旧拠点の棚 −1 はピック済み(
picked_atあり)単位を skip(ピック時に減算済み = 二重減算なし)。
Transfer Receive — /api/operator/transfer/batches/receive
1 スキャン = 1 単位。受領済み単位の再スキャンは
200の no-op(alreadyReceived: true)。受領遷移は Source Change の
return_receivedと同一実装(重複適用なし)。全所属単位の終端でバルクがreceivedに締まる(再判定は冪等)。
Transfer Inbound — /api/routing/transfer/inbound
解決キーは
shipmentId。その出荷の有効な単位に生きた入荷転送がすでにあれば作り直さず、created: falseと既存のbatchIdを返す(イベント再配信で移動が二重に増えない)。そのまま再送してよい。中止済みの移動しか残っていない場合は、新しく登録し直せる。
受け取りは配送状況の配達完了検知で自動的に確定する(同じ配達完了を再検知しても遷移は 1 回だけ)。倉庫アプリのスキャン受領はこの移動には使えない(
409 inbound_batch_not_manually_receivable)。
Stock Transfer API
拠点間在庫移動(Stock Transfer)。返送バルク(上記 Transfer API)とは別機能です。
Create / Create Request — /api/routing/stock-transfer/create / /create-request
作成は冪等:
idempotencyKeyを付けて同じ値で再送すると、新しい移動を作らず既存の移動をidempotent: trueで返します(shop + idempotency_keyで一意)。イベント単位で一意なキーを付与してください。idempotencyKeyを省略すると毎回新しい移動になります。再送で既存の移動が返ったとき、最初に決めた送り状の出し方(
labelMethod)は変わりません。
Set Lines / Remove Lines / Update — /api/routing/stock-transfer/set-lines / /remove-lines / /update
set-lines: 同一
barcodeは数量を置換(行を増やしません)。同じボディの再送は同値で収束(冪等)。出庫後はnot_editable。remove-lines: 既に無い
barcodeの削除は数に含まれないだけで安全(removedに計上しない)。update: 指定したキー(
identificationNo/note)だけを上書き。同値の再送は副作用なし。
Ship — /api/routing/stock-transfer/ship
冪等: 既に出庫済み(
shipped以降)への再shipはnoop: true(棚を二重に減らしません)。自社在庫の棚 − は明細ごとの適用済みマーカーで一度きりに保たれます。移動可能量(棚合計 − 引き当て済み)を超える
shippedQtyはinsufficient_transferable_qtyで拒否(売越防止)。承認前はapproval_required。print-label は何回でも呼べます: 発行済みのラベルを印刷し直すだけで、発行し直しません。
配送ラベルは 1 回だけ発行: ラベルを発行する移動で、発行後に出庫が失敗して呼び直しても、前回のラベルを使います(二重に発行しません)。
Receive — /api/routing/stock-transfer/receive
絶対値で冪等:
acceptedQtyは「受け入れた総数」として上書きします。同じ値の再送は棚を二重に増やしません(適用済みを超えた差分だけ棚を増やします)。既にreceivedへの再送はnoop: true。自社在庫の受領は
destinationShelfCode必須(destination_shelf_required)。acceptedQty + rejectedQtyは出庫数を超えられません(invalid_quantity)。修正依頼中はmodification_requested_locked。
Approve / Reject / Cancel / Delete — /api/routing/stock-transfer/approve / /reject / /cancel / /delete
approve: 既に
approvedはnoop: true。既定(autoShip: true)は承認後にshipまで実行し、出庫が失敗しても承認は確定済み(shipを呼び直して回復)。reject: 既に
rejectedはnoop: true。承認待ち以外はillegal_transition。cancel: 既に
canceledはnoop: true。出庫済みは出庫元の棚を復元してから取消(復元失敗はrestore_failedで取消を確定しない)。receiving/receivedはnot_cancelable。delete: 既に存在しない移動の削除は
noop: true(200)。削除できるのは出庫前のみ(not_deletable)。
Modification Request — /api/routing/stock-transfer/modification-request(+ /complete / /delete)
1 つの移動に開ける依頼は 1 件(active は partial unique)。既に開いていれば
modification_request_already_open。開いた依頼が無い状態でのcomplete/deleteはno_open_modification_request。起票は入庫検品中(
receiving)のみ(not_receiving)。棚・在庫は動かしません(実数量は receive の受領数で確定)。
List / Get / Lines / Modification Requests(read)— /api/routing/stock-transfer/list / /get / /lines / /modification-request(s)
read 専用: SoT への書き込みが無いため冪等性の対象外です。何回呼んでも安全(同じ入力には同じ時点のスナップショットを返します)。
Customer Orders API
Customer Orders — /api/customer-orders/list / /api/customer-orders/detail
read 専用: SoT への書き込みが無いため冪等性の対象外です。何回呼んでも安全(同じ入力には同じ時点のスナップショットを返します)。
Operator API
Operator Login — /api/operator/login / /api/operator/login/google / /api/operator/refresh
非冪等(トークン発行)だが安全: 呼ぶたびに新しい JWT / refresh token を発行します(毎回同じトークンは返しません)。SoT の業務状態は変えないため、再試行に害はありません(古いトークンも失効まで有効です)。
/api/operator/login/otp/requestは呼ぶたびに確認コードのメールを送ります。再送は短い間隔だと429 rate_limitedになるので、少し待ってから送り直してください。試行回数の上限超過は
429 rate_limited、認証基盤に到達できない場合は503 auth_unavailable(フォールバックしません)。
Operator Me / Lookup / Queues / Locations(read)— /api/operator/me / /api/operator/shipments / /api/operator/shipments/by-tracking / /api/operator/products/by-barcode / /api/operator/oversell-cases / /api/operator/unallocated-orders / /api/operator/locations
read 専用: SoT への書き込みが無いため冪等性の対象外です。何回呼んでも安全(同じ入力には同じ時点のスナップショットを返します)。
Account Delete — /api/operator/account/delete
解決キー: JWT の
sub(本人のauth.users.id)。削除対象は常に本人で、外部から指定するキーはありません。冪等: 既に削除済みのアカウント(有効だが失効済みの JWT を保持したケース等)への再実行は
200+deleted: true/alreadyDeleted: trueを返します(エラーにしません)。監査: 実行のたびに
sot.lm_operator_account_deletionsに監査行を 1 件記録します(削除完了時刻を含む)。削除自体は冪等ですが、監査行は試行ごとに残します(トレイル目的)。デモ保護: デモアカウントは状態を変えず
403 demo_account_protectedを返します。
Secure Scan — /api/operator/secure-scan
順次確保: ユニットQR(
<barcode>|<shipmentId>)は同一 barcode の複数 unit を区別しないため、スキャンごとに「allocatedかつ 未 picked のline_instance_no昇順先頭 1 件」を確保します(数量 2 なら 2 回のスキャンで全確保)。冪等: 確保対象が無く既確保 unit が居る再スキャンは
200+secured: 0/alreadySecured: N(二重確保しません)。ヘッダ遷移・ラベル生成は/api/routing/secureと同一で、既に確保済ヘッダはラベルを再生成しません。確保可能でも既確保でもない unit(取消済 / 売越し / picked 済のみ等)は
409 unit_not_securableで表面化します(silent skip しません)。
Secure by Tracking — /api/operator/secure-by-tracking
解決キー: 配送ラベルの
trackingNumber(sot.lm_fulfillment_shipments.tracking_number・shipment 単位)。同一追跡番号が複数 shipment に当たりうるため該当 shipment を全件処理し、各 shipment 配下のstatus=allocatedユニットを一括確保します。案 A(pick→secure 順)のため picked 済も含めて確保します(Secure Scan の「未 picked のみ」と異なります)。冪等: 確保対象(
allocated)が無く既確保ユニットが居る再スキャンは200+ 当該 shipment のsecured: 0/alreadySecured: N(二重確保しません)。全ユニット確保でヘッダsecuredに遷移しますが、先行生成済みラベルは再生成しません。追跡番号に該当 shipment が無ければ
404 shipment_not_found。確保可能でも既確保でもないユニットしか無い shipment は409 unit_not_securableで表面化します(silent skip しません)。operator 記録: operator JWT 経由の確保は
secured_by_staff_user_idに操作者(auth.users.id)を記録します。
Shelf API
Shelf Link — /api/shelf/link
再送冪等: shipment(
shop + trackingNumberで 1 件に解決)に既にアクティブ紐付けがあり、棚コード・ロケーションが同一なら200+idempotent: true(同値で収束)。別棚への紐付けは自動で付け替えず409 shipment_already_linked。付け替え(relink):
relink: true指定時のみ、別棚への既存紐付けをmanual_releaseで解除して新棚へ付け替える(200+relinked: true+previousShelfCode)。解除行は履歴として残る。アクティブ一意性: 同一棚に 1 shipment / 同一 shipment に 1 棚(
released_at IS NULLの partial unique index)。並行 INSERT 競合は409。自動解除: 出荷完了(
shipped遷移 →released_reason="shipped")/ 取消(注文 cancel →released_reason="cancelled")/ 出荷元変更(securedからの起票で旧ヘッダが空 →released_reason="source_changed")で解除。解除は DELETE せず行を履歴として残します。
Shelf Inventory — /api/shelf/inventory/receive / pick / move / adjust
非冪等(積算): 入庫・ピックは qty を増減する操作のため自然冪等ではありません。冪等キーは持たないため、リトライ時はクライアント側で適用済みかを確認してください(既存 routing write の secure / oversell と同方針)。
qty 更新と増減ログ追記は write RPC が同一トランザクションで実行(部分適用なし)。
pickの在庫不足は409 insufficient_shelf_qty、同一棚の別 barcode 占有は409 shelf_occupied_by_different_barcode。moveは非冪等(差分): 再送すると二重に動く。移動元の不足は409 insufficient_shelf_qty。adjustは冪等(絶対値): 同じ値の再送はdelta: 0で何もしない(ログも書かない)。adjustとmove(barcode の付け替え)で、減らした結果その商品の棚の合計が注文に引き当て済みの数を下回る操作は409 shelf_qty_below_activeで何も変えない。
Shelf Condition Move — /api/shelf/inventory/condition-move
非冪等(差分): 商品状態の間で数を動かす操作のため、再送すると二重に動く。元の数の不足は
409 insufficient_shelf_qty/409 insufficient_condition_qty。良品を減らした結果が注文に引き当て済みの数を下回る移動は
409 shelf_qty_below_activeで何も変えない。2 表の更新・ログ 2 行・販売可能数の再計算は write RPC が同一トランザクションで実行(部分適用なし)。
Shelves — /api/shelf/master/upsert
冪等(絶対値):
(shop, locationId, shelfCode)で upsert。同じ内容の再送は同じ結果。
Shelf Pick — /api/operator/shelf/pick
順次 pick: ユニットQR(
<barcode>|<shipmentId>)は同一 barcode の複数 unit を区別しないため、pickはスキャンごとに「未 picked のline_instance_no昇順先頭 1 件」を picked にします。unpickは「picked の降順先頭 1 件」(直近の pick から LIFO)を戻します。冪等: barcode 一致 unit が全て picked の状態での再
pickは200+alreadyPicked: true(上書きしません)。全て未 picked でのunpickも同様(alreadyUnpicked: true)。状態の記録はpicked_at/picked_byのみ。棚 qty 減算の冪等(LM-769): 自己在庫 MFC ユニットの
pick(−1reason=pick)/unpick(+1reason=receive)と続くavailable_qty再計算はpicked_atの遷移時のみ 実行します。既に picked の再pick/ 既に未 picked の再unpickは遷移しないため棚 qty を増減しません(二重減算なし)。再計算は派生量(max(0, 棚合計 − 引き当て済み未出荷数))なので冪等です。棚操作はベストエフォートで、失敗してもpick本体は成功し後続の再計算が整合させます。
Shelf Lookup(read)— /api/operator/shelf/lookup / /api/operator/shelf/by-code
read 専用: SoT への書き込みが無いため冪等性の対象外です。何回呼んでも安全(同じ入力には同じ時点のスナップショットを返します)。
Configuration
Star Printer — /api/routing/star-printer
設定 upsert:
(shop, locationId)で 1 件に収束(1 ロケーション 1 プリンター)。同じdeviceIdの再送は同値で収束(冪等)。detach: 物理削除。該当無しの再 POST は
deleted: 0の no-op(冪等)。
Webhooks
Source Inventory Webhook — /api/routing/source-webhook-config
設定 upsert:
(shop, fulfillment_source_location_id)で1件に収束。authSecret省略時は既存の暗号化値を維持。変動の冪等: 出荷元の
barcode(product)単位の合算available_qtyをsot.lm_fulfillment_source_inventory_stateに「前回送信値」として保持し、異なるときだけ送信します。同値の在庫再取込では送信しません。送信ペイロードは合算の絶対値(差分ではない)のため、受け手は冪等に上書きできます。再送: 送信前にプロセスが落ちても event は outbox(
sot.lm_fulfillment_source_inventory_events)にpending/failedで残り、回収・再送できます(合算の絶対値を再送するため二重適用の害なし)。
Routing Allocation Webhook — /api/routing/allocation-webhook-config
設定 upsert:
shopで1件に収束。authSecret省略時は既存の暗号化値を維持。発火の冪等:
/api/routing/ordersの入口冪等(dispatched済み注文の同一キー再 POST)では発火しません。未 dispatch の同一注文を再 POST した場合は再ルーティングされ、最新の引き当て結果を改めて送信します(引き当て結果の絶対状態を送るため、受け手は最新イベントで冪等に上書きできます)。受け手の冪等キーはshipmentId(unallocatedイベントはroutingOrderId+allocationStatus)。再送: 送信前にプロセスが落ちても event は outbox(
sot.lm_routing_allocation_events)にpending/failedで残り、回収・再送できます(明細は起票時 snapshot を再送するため、後続の再引き当てに影響されません)。
Fulfillment Status Webhook — /api/routing/status-webhook-config
設定 upsert:
shop×取込元(sourceSystem。省略時はその shop の既定宛先)で1件に収束。authSecret省略時は既存の暗号化値を維持。遷移の冪等:
/api/routing/fulfillmentが各 shipment(shop + routing_order_id + from_location_id)のstatusを更新する際、直前の値と異なる shipment だけ event を起票・送信します。同じ status の再報告では送信しません。ペイロードには遷移後の絶対状態(status/previousStatus/trackingNumber)と shipment の出荷明細(shipmentId/lines: barcode 単位集約)を含むため、受け手はshipmentIdを冪等キーに同一 shipment の再送を弾きつつ、該当明細だけを冪等に反映できます。再送: 送信前にプロセスが落ちても event は outbox(
sot.lm_fulfillment_status_events)にpending/failedで残り、回収・再送できます。
Oversell Status Webhook — /api/routing/oversell-status-webhook-config
設定 upsert:
shopで1件に収束。authSecret省略時は既存の暗号化値を維持。発行の冪等:
/api/routing/oversell-caseでケースがwarehouse_receivedに遷移した時に 1 通発行します。outbox はUNIQUE (oversell_case_id)のため、同一ケースの event は 1 件しか起票されません(二重起票は無視して no-op)。warehouse_receivedは終端ステップで再遷移できないため、通常 1 ケース = 1 通です。受け手の冪等キーはoversellCaseId。再送: 送信前にプロセスが落ちても event は outbox(
sot.lm_oversell_status_events)にpending/failedで残り、回収・再送できます(明細は起票時 snapshot を再送します)。
Order Update Webhook — /api/routing/order-update-webhook-config
設定 upsert:
shopで1件に収束。authSecret省略時は既存の暗号化値を維持。発行の冪等:
/api/routing/shipment/addressで配送先住所の手修正が成功するたびに 1 通発行します。1 注文に複数回の修正が起こり得るため outbox はUNIQUE制約を持たず 1 修正 = 1 event を起票します。ペイロードは修正後のshipTo全体(差分ではなく絶対状態)を含むため、受け手はroutingOrderIdを冪等キーに、correctedAtで順序判断して最新の配送先で冪等に上書きできます。再送: 送信前にプロセスが落ちても event は outbox(
sot.lm_order_update_events)にpending/failedで残り、回収・再送できます(修正後shipToは起票時 snapshot を再送します)。
Apps
イベント配信 — LMile ホスト型ミニアプリ
起票の冪等: 同一ルーティング実行内の二重発火は配信キー(
routingOrderId× 出荷先 ×decidedAt)で 1 配信に収束します。再ルーティング(decidedAtが変わる)は新しいイベントとして配信されます。handler の冪等契約: 配信は at-least-once / 順序保証なし。同じイベントが複数回届くことがあるため、handler は冪等・順序非依存で書きます。
ctx.idempotencyKeyは同一イベントの再配信で同じ値のため、action 呼び出しの重複実行抑止に流用できます。再送: 配信失敗(実行環境の未起動 / handler 例外 / timeout)は outbox(
sot.lm_app_deliveries)にpending/failedで残り、回収・再送できます。
Last updated
Was this helpful?