注文確定 設計書
版数
2.1
プロダクト
Example
ステータス
レビュー中
更新日
2026-10-01
その他のメタ情報(2)
ownercommerce-team
reviewerspayments-team, inventory-team

注文確定 設計書

Note

この設計書では、カート画面の「注文を確定」ボタンから注文完了までの流れを説明する。配送と請求は別の設計書で説明する。

注文確定のシーケンス

決済ゲートウェイ在庫サービス注文 APIWeb アプリ決済ゲートウェイ在庫サービス注文 APIWeb アプリalt[オーソリ成功][決済拒否]顧客注文を確定1POST /orders2在庫を引き当て3引き当て ID4オーソリ5承認番号6注文を確定済みで保存7201 Created8注文完了画面を表示9拒否10在庫を解放11402 Payment Required12顧客

1. 注文を確定

顧客がカート画面で「注文を確定」を押す。

  • 配送先と支払い方法を選ぶまで、ボタンは押せない。
  • カートは冪等キーを付けて送るので、ボタンを 2 回押しても注文は 1 件しかできない。

2. POST /orders

POST /orders HTTP/1.1
Content-Type: application/json
Idempotency-Key: 6f1c2a9e-0b7d-4f3a-9c55-1d2e8a7b4c10
{
  "cartId": "c_8241",
  "shipping": { "addressId": "a_17", "timeSlot": "am" },
  "payment": { "methodId": "pm_card_visa" },
  "items": [{ "sku": "SKU-1042", "quantity": 2 }]
}
ステータス 条件
201 注文確定
402 決済拒否
409 在庫切れの商品がある

3. 在庫を引き当て

カートのすべての商品を 1 つのトランザクションで引き当てる。1 つでも足りなければ何も引き当てず、409 Conflict を返す。

UPDATE stock
SET reserved = reserved + $2
WHERE sku = $1 AND quantity - reserved >= $2;

4引き当て ID

注文が確定しなければ、引き当ては 15 分で失効する[1]。

5. オーソリ

注文金額をオーソリする。売上の確定は出荷時に行う。

Important

カード番号は注文 API に送らない。Web アプリが決済ゲートウェイに直接送り、返された payment.methodId だけを渡す。

6承認番号

注文と一緒に保存し、出荷時の売上確定に使う。

7注文を確定済みにする

注文をステータス confirmed で保存し、引き当てを注文に紐付ける。

export async function confirmOrder(tx: Tx, order: NewOrder, auth: Authorization): Promise<Order> {
  const saved = await tx.orders.insert({ ...order, status: 'confirmed', authCode: auth.code });
  await tx.reservations.attach(order.reservationId, saved.id);
  return saved;
}

8201 Created

注文 ID とお届け予定日を返す。

9. 注文完了画面を表示

注文番号を表示し、注文確認メールを送る。

  • 注文番号と商品
  • お届け予定日
  • 30 分以内のキャンセルへのリンク

10拒否

ゲートウェイが拒否コードを返す。リトライするコードは決済リトライのフローを参照。

11在庫を解放

他の顧客が買えるよう、引き当てをすぐに解放する。

12402 Payment Required

Warning

拒否コードは顧客に見せない。「お支払いを完了できませんでした。」と表示し、別の支払い方法を選べるようにする。

決済リトライのフロー

いいえ

はい

はい

いいえ、3 回未満

いいえ、3 回

決済拒否

リトライ可能?

注文を取り消す

時間をおく

再オーソリ

承認された?

支払済みにする

完了

顧客に通知

決済拒否

定期購入の更新など、バッチ処理が始めたオーソリをゲートウェイが拒否したときに始まる。

リトライ可能?

拒否コード リトライ
processing_error する
try_again_later する
insufficient_funds しない
card_declined しない

時間をおく

1 分、5 分、30 分と待つ。

再オーソリ

最初と同じ冪等キーを使う。

承認された?

再試行したオーソリの結果を確かめる。

決済を完了

注文のステータスを paid にする。

注文を取り消す

在庫を解放し、注文を取り消す。

完了

注文は出荷に進む。

顧客に通知

支払い方法の更新をお願いするメールを送る。

注文のステータス

作成済み

支払済み

出荷済み

配達済み

キャンセル済み

作成済み

注文を保存し、支払いを待っている。

支払済み

オーソリが承認された。

出荷済み

配送業者に荷物を渡した。この時点で売上を確定する。

配達済み

配送業者から配達完了の連絡があった。

キャンセル済み

出荷前に注文がキャンセルされた。オーソリは取り消す。

エラーコード

注文 API のエラー。エラーのレスポンスの本文は { "code": "ORD-011", "message": "..." }。

コード HTTP ステータス 意味 クライアントの対応
ORD-001 400 リクエストの本文が JSON として正しくない。 リクエストを直す。
ORD-002 400 必須の項目がない。 リクエストを直す。
ORD-003 400 Idempotency-Key ヘッダがない。 ヘッダを付ける。
ORD-004 400 quantity が正の整数でない。 リクエストを直す。
ORD-005 400 1 回の注文の商品が多すぎる(最大 100)。 注文を分ける。
ORD-006 401 アクセストークンがないか、期限切れ。 ログインし直す。
ORD-007 402 決済が拒否された。 別の支払い方法を選んでもらう。
ORD-008 402 カードの有効期限が切れている。 別のカードを選んでもらう。
ORD-009 402 3-D セキュアの認証に失敗した。 認証をやり直す。
ORD-010 403 アカウントが停止されている。 問い合わせ先を表示する。
ORD-011 404 カートが存在しない。 カートを読み直す。
ORD-012 404 配送先が存在しない。 配送先を選び直してもらう。
ORD-013 404 支払い方法が存在しない。 支払い方法を選び直してもらう。
ORD-014 409 在庫切れの商品がある。 在庫数を表示して選び直してもらう。
ORD-015 409 注文確定の途中でカートが変わった。 新しいカートを表示する。
ORD-016 409 Idempotency-Key が別のカートで使われている。 新しいキーを作る。
ORD-017 422 配送先が配送エリアの外にある。 別の配送先を選んでもらう。
ORD-018 422 合計金額が最低注文金額に満たない。 最低注文金額を表示する。
ORD-019 429 リクエストが多すぎる。 Retry-After の後にリトライする。
ORD-020 502 決済ゲートウェイが応答しなかった。 注文一覧を確かめてからリトライする。
ORD-021 503 在庫サービスが利用できない。 時間をおいてリトライする。

POST /orders のリクエスト本文

JSON の本文の項目。子の項目の「必須」は、親を送るときに必須かを表す。

No 階層 項目名物理名 物理名 型 必須 説明
1 1 カート IDcartId cartId string ○ 注文するカート。ログイン中の顧客のもので、まだ注文していないこと
2 1 配送shipping shipping object ○
3 2 配送先 IDaddressId addressId string ○ 顧客の登録済みの住所。配送エリア外の住所は受け付けないので、注文画面では配送できない住所をグレーアウトして選べなくする
4 2 配達日deliveryDate deliveryDate string YYYY-MM-DD。省略時は最短の日。予約が埋まっている日と 14 日より先の日は受け付けない
5 2 時間帯timeSlot timeSlot string am・pm・evening。省略時は指定なし。日曜日は evening を選べない
6 1 支払いpayment payment object ○
7 2 支払い方法 IDmethodId methodId string ○ 登録済みのカードやウォレット。有効期限切れや前回のオーソリで拒否されたカードは、顧客が更新するまで使えない
8 2 クーポンコードcouponCode couponCode string 1 回の注文に 1 枚まで。期限切れのクーポンは適用せず、理由をレスポンスで返す
9 1 商品items items array ○ 1〜100 件。同じ SKU は 1 件にまとめ、合計を quantity で送る
10 2 SKUsku sku string ○
11 2 数量quantity quantity integer ○ 1〜99。在庫と商品ごとの購入数の上限のうち、少ない方までに制限する
12 2 ギフト包装giftWrap giftWrap boolean 省略時は false。包装は商品ごとに有料で、家具など一部の商品では選べない

注文完了画面

注文確定のシーケンスの 9 で表示する画面。ワイヤーフレームの番号は下の項目の No と対応する。

<html lang="ja"><head>

<title>注文完了</title>
<style>/* Shared styles of the wireframes: a plain, gray look, so that they read as mock-ups rather than finished screens. */
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", Meiryo, "Noto Sans JP", sans-serif;
  font-size: 14px;
  color: #333;
  background: #fafafa;
}
header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px;
  background: #e0e0e0;
  font-weight: bold;
}
header img { width: 24px; height: 24px; }
main { padding: 20px 16px; }
h1 { margin: 0 0 16px; font-size: 20px; }
label { display: block; margin-bottom: 12px; }
label span { display: block; margin-bottom: 4px; color: #666; font-size: 12px; }
input[type="text"],
input[type="password"] {
  box-sizing: border-box;
  width: 100%;
  padding: 8px;
  border: 1px solid #999;
  border-radius: 4px;
  background: #fff;
}
.check { display: flex; align-items: center; gap: 6px; }
.check span { display: inline; margin: 0; font-size: 14px; color: #333; }
button {
  width: 100%;
  padding: 10px;
  border: none;
  border-radius: 4px;
  background: #666;
  color: #fff;
  font-size: 15px;
}
.error {
  margin-bottom: 12px;
  padding: 8px;
  border: 1px dashed #c00;
  color: #c00;
  font-size: 12px;
}
.links { margin-top: 16px; text-align: center; }
a { color: #333; }
.done { margin-bottom: 16px; padding: 12px; border-radius: 4px; background: #e8e8e8; text-align: center; }
.done strong { display: block; margin-top: 4px; font-size: 18px; letter-spacing: 1px; }
dl { display: grid; grid-template-columns: auto 1fr; gap: 6px 12px; margin: 0 0 16px; }
dt { color: #666; }
dd { margin: 0; text-align: right; }
.items { margin: 0 0 12px; padding: 0; list-style: none; border-top: 1px solid #ccc; }
.items li { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #ccc; }
.total { font-weight: bold; }
</style>
</head>
<body>
<header><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHJ4PSI1IiBmaWxsPSIjODg4Ii8+PHBhdGggZD0iTTcgMTJsMyAzIDctNyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIuNSIvPjwvc3ZnPgo=" alt=""> Example Shop</header>
<main>
  <div class="done">ご注文ありがとうございました。<strong data-item="1">ORD-2026-000123</strong></div>
  <ul class="items">
    <li data-item="2"><span>デスクライト × 1</span><span>¥5,800</span></li>
    <li data-item="2"><span>LED 電球 × 2</span><span>¥1,760</span></li>
  </ul>
  <dl>
    <dt>合計</dt><dd class="total" data-item="3">¥7,560</dd>
    <dt>お届け予定</dt><dd data-item="4">10月28日 午前</dd>
  </dl>
  <button data-item="5">買い物を続ける</button>
  <p class="links"><a href="#" data-item="6">注文履歴を見る</a></p>
</main>


</body></html>
1
2
3
4
5
6
No 項目名物理名 物理名 種別 説明
1 注文番号orderNo orderNo テキスト POST /orders のレスポンスの注文番号。
2 注文商品items items リスト SKU ごとに 1 行。数量と小計を表示する。
3 合計total total テキスト 税込み・送料込み。
4 お届け予定delivery delivery テキスト 選んだ日付と時間帯。
5 買い物を続けるcontinue continue ボタン
6 注文履歴を見るorders orders リンク

5. 買い物を続ける

トップページを開く。カートはこの時点で空になっている。

6. 注文履歴を見る

注文履歴を開く。この注文が先頭に表示される。

返金(草案)

決済ゲートウェイ注文 API決済ゲートウェイ注文 API顧客返金を申請1返金を実行2返金 ID3返金メールを送信4顧客

1返金を申請

返金はお届けから 30 日以内に申請できる。

2返金を実行

確定した売上を元の支払い方法に返金する。

4返金メールを送信

返金が反映されるまで最大 10 日かかることを顧客に伝える。

在庫に戻す

返品された商品を検品し、在庫に戻す。

未決事項

Tip

ローカルで注文確定を試すには、docker compose up -d payment-mock でゲートウェイのモックを起動し、カード 4242 4242 4242 4242 を使う。

なぜオーソリより先に在庫を引き当てるのか

先にオーソリしてから在庫が切れると、オーソリを取り消すことになり、銀行によっては数日間それが顧客に見えてしまう。

Caution

承認番号やカード番号は、一部であってもログに出さない。


  1. 数分かかることのある 3-D セキュアの認証に足りる長さにしている。失効した引き当ては、バッチが 1 分ごとに解放する。 ↩︎

  2. ゲートウェイはキーを 24 時間保持する。それより後にリトライすると、二重に決済されてしまう。 ↩︎