CONTENT
ここから
第12章では、SquareからWebhookを受け取る基本を学びました。
この章では、その次のステップとして、決済が完了したあとにECシステム側で何をするべきかを整理します。
ECでは、購入ボタンを押して決済画面に進んだだけでは注文完了ではありません。
大切なのは、Square側で決済が完了したことを確認し、その結果を自分たちのECシステムに正しく反映することです。
つまり、この章で作るのは次のような処理です。
決済完了
↓
Webhook受信
↓
注文をpaidに更新
↓
在庫を減らす
↓
メールを送る
↓
配送準備へ進める
攻略本風に言えば、ここは「購入イベントを正式な注文に変えるステージ」です。
13.1 決済完了フローの全体像
まず、決済完了後の流れを全体で見ておきます。
ユーザーが商品を購入すると、画面上では次のように見えます。
商品ページ
↓
購入ボタン
↓
Squareの決済画面
↓
決済完了ページ
しかし、システム側ではこれだけでは不十分です。
なぜなら、ユーザーが決済完了ページに戻ってきたとしても、そこで本当に支払いが完了しているとは限らないからです。
たとえば、次のようなことが起きます。
・決済完了ページを開く前にブラウザを閉じた
・決済は完了したが、戻り先ページの表示に失敗した
・ユーザーが戻るボタンを押した
・通信が遅れて画面表示とWebhookの順番が前後した
・Webhookが同じ内容で複数回届いた
そのため、ECシステムではフロントエンドの表示ではなく、Webhookを基準に注文を確定するのが基本です。
決済完了後の安全な流れは、次のようになります。
1. 注文をpendingで作成する
2. Squareの決済リンクを作る
3. ユーザーがSquareで支払う
4. SquareからWebhookが届く
5. Webhookの署名を検証する
6. payment.statusがCOMPLETEDか確認する
7. 自前DBの注文をpaidに更新する
8. 在庫を減らす
9. メール通知を送る
10. 配送準備へ進める
ここで重要なのは、決済前の注文と決済後の注文を分けることです。
注文データは、最初から完了扱いにしてはいけません。
最初は仮注文として保存し、Squareから決済完了の通知を受け取った時点で正式な注文にします。
pending = 決済待ち
paid = 決済完了
preparing = 配送準備中
shipped = 発送済み
cancelled = キャンセル
failed = 決済失敗または処理失敗
この流れを作っておくと、決済エラー、通信エラー、Webhookの遅延にも対応しやすくなります。
13.2 pendingからpaidへ変更する
ECシステムでは、注文作成直後のステータスをpendingにしておきます。
pendingは、まだ支払いが確定していない状態です。
注文は作られている
でも、まだ支払いは完了していない
たとえば、購入ボタンを押した時点で次のような注文データを作ります。
type OrderStatus =
| 'pending'
| 'paid'
| 'preparing'
| 'shipped'
| 'cancelled'
| 'failed'
| 'manual_review';
type Order = {
id: string;
squarePaymentId: string | null;
squareOrderId: string | null;
status: OrderStatus;
totalAmount: number;
paidAt: string | null;
createdAt: string;
updatedAt: string;
};
この時点では、まだpaidにしてはいけません。
購入者がSquareの決済画面を開いただけで、途中で離脱する可能性があるからです。
注文ステータスをpaidに変更してよいのは、Squareから届いたWebhookを確認し、支払いステータスが完了していると判断できたときです。
イメージは次の通りです。
Webhook受信
↓
署名検証OK
↓
payment.updated
↓
payment.status === COMPLETED
↓
注文をpendingからpaidへ変更
処理としては、次のような考え方になります。
type PaymentStatus =
| 'APPROVED'
| 'PENDING'
| 'COMPLETED'
| 'CANCELED'
| 'FAILED';
type UpdateOrderToPaidInput = {
orderId: string;
squarePaymentId: string;
paymentStatus: PaymentStatus;
paidAt: string;
};
/**
* 役割: Squareの決済完了を受けて、自前DBの注文をpaidへ変更する
* 入力: 注文ID、Square決済ID、決済ステータス、決済完了日時
* 出力: 更新後の注文ステータス
*/
async function updateOrderToPaid(
input: UpdateOrderToPaidInput
): Promise<OrderStatus> {
if (input.paymentStatus !== 'COMPLETED') {
return 'pending';
}
// 実際の実装ではここでDBを更新する
// update orders set status = 'paid', paid_at = input.paidAt where id = input.orderId
return 'paid';
}
ここでのポイントは、COMPLETED以外を簡単に成功扱いしないことです。
PENDINGは処理中、FAILEDは失敗、CANCELEDはキャンセルとして扱います。
13.3 注文ステータスを更新する
注文ステータスは、EC運用の中心です。
ステータスが曖昧だと、管理画面で何をすればよいのか分からなくなります。
まずは、実務で使いやすいステータスを用意します。
pending 決済待ち
paid 決済完了
preparing 配送準備中
shipped 発送済み
cancelled キャンセル
failed 決済失敗
manual_review 手動確認
最初から細かくしすぎる必要はありません。
小規模ECであれば、最初はこれくらいで十分です。
注文ステータスの更新は、イベントごとに決めておきます。
購入ボタン押下
→ pending
Square決済完了
→ paid
店舗が発送準備を開始
→ preparing
発送完了
→ shipped
決済失敗
→ failed
金額不一致・在庫不足・Webhook不整合
→ manual_review
特に重要なのは、決済完了後すぐにshippedにしないことです。
決済が完了しただけでは、まだ発送は終わっていません。
そのため、次のように段階を分けます。
paid = お金を受け取った
preparing = 店舗が発送作業に入った
shipped = 実際に発送した
この分け方をしておくと、管理画面が作りやすくなります。
type OrderStatusLabelMap = Record<OrderStatus, string>;
const ORDER_STATUS_LABELS: OrderStatusLabelMap = {
pending: '決済待ち',
paid: '決済完了',
preparing: '配送準備中',
shipped: '発送済み',
cancelled: 'キャンセル',
failed: '決済失敗',
manual_review: '手動確認',
};
管理画面では、英語のステータスをそのまま表示するより、日本語のラベルに変換した方が分かりやすくなります。
13.4 在庫を減らす
決済完了後の処理で、特に注意が必要なのが在庫です。
在庫を減らすタイミングには、大きく2つあります。
A. 購入ボタンを押した時点で在庫を確保する
B. 決済が完了した時点で在庫を減らす
小規模ECでは、まずは決済完了後に在庫を減らす方式から始めるのが分かりやすいです。
理由はシンプルです。
支払いが完了した注文だけを在庫反映すればよい
ただし、この方式には注意点もあります。
同じ商品を複数人が同時に購入した場合、在庫数を超えて売れてしまう可能性があります。
たとえば、在庫が1個の商品を2人が同時に購入した場合です。
在庫:1個
Aさんが購入
Bさんも同時に購入
Webhookが順番に届く
↓
両方とも在庫ありとして処理してしまう危険がある
この問題を防ぐには、在庫更新を安全に行う必要があります。
考え方は次の通りです。
在庫数を確認する
↓
在庫が足りる場合だけ減らす
↓
在庫が足りない場合はmanual_reviewにする
SQLで考えると、次のような条件付き更新が必要です。
UPDATE products
SET stock = stock - 1
WHERE id = 'product_id'
AND stock >= 1;
この更新結果が0件だった場合は、在庫不足です。
その場合は、注文を自動で完了させず、手動確認に回します。
在庫更新成功
→ paid または preparingへ進める
在庫更新失敗
→ manual_review
TypeScriptでは、次のような処理イメージになります。
type DecreaseStockResult =
| {
success: true;
}
| {
success: false;
reason: 'not_enough_stock';
};
type DecreaseStockInput = {
productId: string;
quantity: number;
};
/**
* 役割: 商品在庫を注文数量分だけ減らす
* 入力: 商品ID、購入数量
* 出力: 在庫更新の成功可否
*/
async function decreaseStock(
input: DecreaseStockInput
): Promise<DecreaseStockResult> {
if (input.quantity <= 0) {
return {
success: false,
reason: 'not_enough_stock',
};
}
// 実際の実装では、DB側で条件付き更新を行う
// stock >= input.quantity の場合だけ stock を減らす
return {
success: true,
};
}
在庫処理は、画面表示よりもサーバー側の処理を信用します。
ユーザーの画面に「在庫あり」と表示されていても、その後に別の人が購入している可能性があるからです。
13.5 サンクスメールを送る
決済が完了したら、購入者へ注文完了メールを送ります。
サンクスメールの役割は、単なるお礼ではありません。
購入者に安心してもらうための確認通知です。
最低限、次の内容を入れます。
・購入のお礼
・注文番号
・購入商品
・合計金額
・配送先情報
・今後の流れ
・問い合わせ先
文面は、商材によって変えます。
食品や日用品であれば、分かりやすく簡潔にします。
高級商材やギフト商品であれば、少し丁寧に余白のある文章にします。
例文です。
このたびはご注文いただき、誠にありがとうございます。
ご注文を受け付けました。
商品の準備が整い次第、順次発送いたします。
ご注文内容は以下の通りです。
注文番号:ORDER-0001
商品名:クラシック節ギフトセット
合計金額:5,400円
発送が完了しましたら、改めてご連絡いたします。
メール送信のタイミングは、基本的にpaidへ変更したあとです。
決済完了確認
↓
注文をpaidへ更新
↓
在庫を減らす
↓
サンクスメール送信
ただし、在庫更新に失敗した場合は、通常の注文完了メールを送らない方が安全です。
その場合は、管理者に通知して、手動確認後に個別対応します。
メール送信にも失敗することがあります。
そのため、メール送信結果も記録しておくと安心です。
email_status:
pending
sent
failed
メール送信が失敗しても、決済そのものを失敗扱いにしてはいけません。
支払いは完了しているため、注文はpaidのままです。
メールだけ再送できるようにします。
13.6 管理者へ通知する
購入者へのメールとは別に、店舗側にも通知を送ります。
管理者通知の目的は、すぐに注文に気づけるようにすることです。
小規模ECでは、管理画面を毎日見に行く運用より、メールやLINE通知で気づける設計の方が現実的です。
管理者通知に入れる内容は、次の通りです。
・新規注文が入ったこと
・注文番号
・購入者名
・商品名
・数量
・合計金額
・配送先
・管理画面URL
・注意事項
通知文面の例です。
新しい注文が入りました。
注文番号:ORDER-0001
購入者:山田 太郎 様
商品:クラシック節ギフトセット
数量:1
合計金額:5,400円
管理画面から配送準備を進めてください。
通知先は、最初はメールで十分です。
運用が増えてきたら、次のような通知も検討できます。
・Slack通知
・LINE通知
・Discord通知
・Google Chat通知
ただし、個人情報を外部チャットに流す場合は注意が必要です。
チャット通知には、配送先住所や電話番号などを入れず、管理画面URLだけにする設計もあります。
安全な通知例:
新しい注文が入りました。
注文番号:ORDER-0001
詳細は管理画面で確認してください。
個人情報は、必要な場所にだけ表示する。
これがEC運用の基本です。
13.7 配送準備ステータスへ移す
決済が完了した注文は、すぐに発送済みにするのではなく、まずpaidにします。
その後、店舗側が内容を確認して、問題がなければpreparingに変更します。
paid
↓
preparing
↓
shipped
preparingは、配送準備中という意味です。
このステータスを用意しておくと、管理画面で作業状況が見えやすくなります。
paid:
決済は完了しているが、まだ店舗が確認していない
preparing:
店舗が確認し、梱包や発送作業に入っている
shipped:
発送が完了している
小規模ECでは、paidとpreparingを分けない運用もできます。
しかし、注文数が増えたときに混乱しやすくなります。
特に、次のような商品ではpreparingを分けた方が安全です。
・ギフト商品
・数量限定商品
・手作業で梱包する商品
・配送温度帯がある商品
・催事や店頭受け取りがある商品
・一点ものの商品
管理画面では、paidの注文だけを一覧表示し、店舗スタッフが確認後にpreparingへ変更できるようにします。
新規注文一覧
↓
内容確認
↓
配送準備へ進めるボタン
↓
ステータスをpreparingへ変更
この流れにしておくと、注文の見落としを防ぎやすくなります。
13.8 失敗時のリカバリー処理
ECでは、すべての処理が必ず成功するとは限りません。
決済完了後にも、次のような失敗が起きます。
・Webhookの受信に失敗する
・注文データが見つからない
・SquareのPayment IDと自前注文が紐づかない
・在庫更新に失敗する
・メール送信に失敗する
・DB更新中にエラーが起きる
・同じWebhookが複数回届く
ここで重要なのは、失敗をなかったことにしないことです。
失敗した場合は、ログに残し、管理者が確認できる状態にします。
おすすめは、処理ごとに状態を分けることです。
payment_confirmed:
決済確認済み
stock_updated:
在庫更新済み
customer_email_sent:
顧客メール送信済み
admin_notified:
管理者通知済み
たとえば、注文テーブルに次のようなカラムを持たせます。
type OrderProcessingFlags = {
paymentConfirmed: boolean;
stockUpdated: boolean;
customerEmailSent: boolean;
adminNotified: boolean;
};
これにより、途中で失敗しても、どこまで処理できたのかが分かります。
決済確認済み
在庫更新済み
メール送信失敗
管理者通知済み
この場合、やるべきことはメールの再送です。
決済や在庫処理をもう一度やってはいけません。
失敗時の基本方針は次の通りです。
決済確認に失敗
→ pendingのまま、再確認
注文が見つからない
→ manual_review
在庫更新に失敗
→ manual_review
メール送信に失敗
→ paidのまま、メール再送待ち
管理者通知に失敗
→ paidのまま、通知再送待ち
失敗したからといって、すぐに注文をキャンセル扱いにしないことが大切です。
お金を受け取っている可能性があるからです。
13.9 手動確認が必要な注文を抽出する
自動処理だけで完璧に運用しようとすると、かえって危険です。
ECでは、必ず手動確認用の逃げ道を用意します。
手動確認が必要な注文は、manual_reviewにします。
manual_review = 自動では判断できないため、人が確認する注文
手動確認に回すべきケースは、次のようなものです。
・決済は完了しているが、自前注文が見つからない
・注文金額とSquareの決済金額が一致しない
・在庫が足りない
・同じ注文に対して複数の決済通知がある
・配送先情報が不足している
・メール送信に何度も失敗している
・Webhookの処理中にDBエラーが起きた
管理画面では、manual_reviewだけを抽出できるようにします。
管理画面
↓
手動確認が必要な注文
↓
内容を確認
↓
対応済みにする
表示項目は、次のようにします。
・注文番号
・発生日時
・エラー理由
・Square Payment ID
・Square Order ID
・自前注文ID
・購入者情報
・対応メモ
手動確認では、対応メモを残せるようにすると実務で便利です。
対応メモ例:
Square上では決済完了を確認。
在庫不足のため、購入者へ代替商品を案内済み。
2026-07-06 渥美対応。
このように記録を残しておくと、あとから状況を追いやすくなります。
13.10 決済完了ページとWebhookのズレを吸収する
最後に、初心者がつまずきやすいポイントを整理します。
それは、決済完了ページの表示とWebhookの到着は、必ずしも同じタイミングではないということです。
ユーザーの画面では、決済後に完了ページが表示されます。
Square決済画面
↓
決済完了
↓
完了ページへ戻る
しかし、サーバー側ではWebhookが少し遅れて届くことがあります。
逆に、完了ページより先にWebhookが届くこともあります。
パターンA:
決済完了ページが先
Webhookが後
パターンB:
Webhookが先
決済完了ページが後
パターンC:
決済完了ページは開かれない
Webhookだけ届く
そのため、決済完了ページでは、注文を確定させる処理を書かない方が安全です。
完了ページの役割は、あくまでユーザーに案内を表示することです。
決済完了ページの役割:
・ご注文ありがとうございますと表示する
・注文確認中の場合はその旨を表示する
・メールが届くことを案内する
・問い合わせ先を表示する
たとえば、完了ページでは次のように表示します。
ご注文ありがとうございます。
現在、決済情報を確認しています。
確認が完了しましたら、ご注文完了メールをお送りします。
通常は数分以内に反映されます。
しばらく経ってもメールが届かない場合は、お問い合わせください。
すでにWebhook処理が完了している場合は、次のように表示できます。
ご注文が完了しました。
ご登録のメールアドレス宛に注文完了メールをお送りしました。
商品発送まで今しばらくお待ちください。
つまり、完了ページでは注文ステータスを見て表示を切り替えます。
pending:
決済確認中です
paid:
ご注文が完了しました
manual_review:
ご注文内容を確認しています
failed:
決済を確認できませんでした
この設計にしておくと、画面表示とWebhook処理のズレを吸収できます。
第13章のまとめ
決済完了後の注文処理では、Webhookを基準に注文を確定することが重要です。
購入者が決済完了ページに戻ってきたかどうかで、注文を完了扱いにしてはいけません。
基本の流れは次の通りです。
pendingで注文作成
↓
Squareで決済
↓
Webhookで決済完了を確認
↓
paidへ更新
↓
在庫を減らす
↓
購入者へメール
↓
管理者へ通知
↓
配送準備へ進める
この章で覚えておきたいポイントは、次の5つです。
1. 決済完了ページでは注文確定しない
2. Webhookを基準にpaidへ変更する
3. 在庫更新はサーバー側で安全に行う
4. メール送信失敗と決済失敗を混同しない
5. 自動処理できない注文はmanual_reviewへ回す
ECシステムは、売れた瞬間よりも、売れたあとの処理が重要です。
決済、在庫、メール、配送準備がきれいにつながると、店舗側の運用が一気に楽になります。
第14章では、この続きとして、購入者と店舗に送るメール通知を詳しく作っていきます。