判断の出発点
33,333ウォンの商品を取り消したのに、返金額が20,333ウォンになります。クーポンを適用したまとめ注文で、別の商品を先に取り消すと、同じ商品の返金額がまた変わります。商品価格だけでは誤りかどうか判断できません。残す商品の割引条件と送料がどう変わったかまで確認します。
部分取消額は商品の表示価格ではなく、合意したポリシーに従って取消後に残すべき金額と、現在の確定残高との差で計算します。 当初の販売金額、商品別の割引配賦、送料、税・調整項目を保存し、成功した取消だけを累計します。同じ条件で同じ商品が残るなら、取消順序が異なっても累計返金額と残高は一致する必要があります。
以下は韓国ウォン建ての通常のカード決済一件を対象とした技術設計例です。発送前で、カード会社の即時割引、ポイント、複合決済、両替、追加請求、紛争・チャージバックはありません。割引回収や送料負担の法的な正解を示すものではありません。実際のサービスでは約款と適用法令を検討し、先にポリシーを確定します。
商品価格・割引・送料を一つの数字に隠さない
返金の基準となる商品金額は仕入原価ではなく、顧客への販売金額です。注文全体のクーポンも総額だけを保存すると、後で商品と数量ごとの配賦を再現しにくくなります。注文確定時に、以下を分けて保存する設計を推奨します。
| 項目 | 保存する内容 | 部分取消で確認する問い |
|---|---|---|
| 販売金額 | 商品・オプション・数量ごとの販売単価と通貨 | どの販売単位を取り消すか |
| 割引 | 提供主体、規則の版、対象、商品・数量別の配賦額 | 残った商品の割引も変わるか |
| 送料 | 配送単位ごとの当初請求額と無料条件 | 条件を失う場合や全取消でどう変わるか |
| 税・調整 | 税込価格かどうか、課税区分、調整理由と金額 | 税の二重加算や別項目への隠蔽がないか |
| 決済 | 承認通貨・金額、決済識別子、成功した取消履歴 | 実際の承認金額のうち、いくら取り消したか |
販売者のクーポン、カード会社の割引、自社ポイントは別項目にする方が適切です。特典の提供者と返却先の決済手段を保持するためです。税込価格なら、税の内訳を保存しても請求総額に再加算しません。課税・免税が混在する場合は返金総額と免税部分も区別します。Toss Paymentsは該当店舗の部分取消要求でtaxFreeAmountを別に扱います。[5]
既にコマースプラットフォームを使っている場合は、独自の推定より先に、プラットフォームが確定した配賦結果を確認します。Shopifyでは、DiscountApplicationが割引規則、DiscountAllocationが商品または配送項目に実際に適用された割引額を表します。これらのオブジェクトが自社の返金ポリシーまで自動決定するわけではありません。[6]
配送も商品取消とは別に判断します。Shopify管理画面では送料返金を別途選択し、請求済み送料の返金可能額を超えることはできません。後述の送料無料条件の再判定は、この機能の説明ではなく、架空店舗が別途定めたポリシーです。[7]
注文から承認・返金まで状態を接続 — 決済手段を注文フローにつなぎ、応答遅延、再試行、取消し、返金の例外を検証します。
設計例:66,666ウォンの注文に10,000ウォンのクーポンを適用
商品A・B・Cを一個ずつ注文したとします。金額はすべてウォンで、販売価格には該当する税が含まれると仮定します。税の別途加算やその他の調整はありません。
| 商品 | 販売金額 | 当初のクーポン配賦 | 割引後の商品金額 |
|---|---|---|---|
| A | 33,333 | 5,000 | 28,333 |
| B | 22,222 | 3,333 | 18,889 |
| C | 11,111 | 1,667 | 9,444 |
| 合計 | 66,666 | 10,000 | 56,666 |
クーポンと配送には以下のポリシーを適用します。これは計算を説明するための選択であり、特定のPGやコマースプラットフォームの標準ポリシーではありません。
| ポリシー項目 | この例で選ぶ規則 |
|---|---|
| クーポン提供者と配賦 | 販売者負担の10,000ウォンを当初販売金額の比率で配賦し、保存します。 |
| 部分取消後のクーポン | 残った商品の割引前合計が50,000ウォン以上なら、その商品の当初配賦だけを維持します。取消商品の分は再配賦しません。50,000ウォン未満なら残った商品の割引はゼロです。 |
| 送料 | 残った商品の割引前合計が50,000ウォン以上ならゼロ、商品が残り合計がそれ未満なら注文全体で3,000ウォンです。 |
| 最後の商品も取消 | 発送前で他の費用もないため、送料もゼロにします。 |
最初の承認額は66,666 - 10,000 + 0 = 56,666ウォンです。その後送料が3,000ウォンになっても、新たにカード承認を行うわけではありません。この例では既存の決済残高のうち、保持する金額の内訳が変わります。運用画面にも商品返金、割引変更、送料変更を分けて表示します。
1ウォンの配賦も最初に決める
正確な比例配賦はAが5,000ウォン、Bが3,333と1/3ウォン、Cが1,666と2/3ウォンです。まずウォン単位で切り捨てると合計9,999ウォンになります。残り1ウォンは小数の余りが最大のCに割り当てます。余りが等しい場合は固定の商品・販売単位識別子順を使い、取消要求の到着順は使いません。
これは最大剰余方式を採用したこの例の配賦規則です。別の規則も選べますが、配賦合計は元のクーポン額と等しく、再計算でも同じ結果になる必要があります。同じ商品を複数販売した場合は、一部数量の取消でも再現できるよう、販売単位別の配賦か同等の決定規則が必要です。
このウォン建ての例では内部計算を整数で処理します。小数価格や比率を扱うなら、正確な十進数・分数演算の後、定めた時点で単位を変換します。APIへのシリアライズ規則は別です。Toss Payments取消APIのcancelAmountはnumberなので、9,444ウォンは数値9444で送信します。[1]
この表現を他の通貨やAPIにそのまま移してはいけません。例えばStripeは通貨の最小単位で金額を受け取り、10 USDは1000、10 JPYは10になります。一部通貨には例外もあります。内部金額に通貨・単位を付け、PGごとの変換を一つの境界で行う方が安全です。[8]
取消商品の価格より先に「保持金額」を計算する
この例で残る商品の集合をSとすると、次のように計算できます。
保持金額 V(S)
= 残る商品の販売金額合計
- ポリシー上維持する商品別割引の合計
+ ポリシー上の送料
今回の取消要求額
= 直前までに確認した決済残高 - V(取消後に残る商品)直前残高は、以前の取消が確定し、内部記録とPGが一致している状態の値です。処理中の取消が残る場合や、PG管理画面で別の取消が行われていた場合は、その差を先に解消します。
Cを先に取り消す場合
AとBの販売金額合計は55,555ウォンです。クーポン条件を満たすため当初配賦の8,333ウォンを維持し、送料はゼロです。
保持金額 = 55,555 - 8,333 = 47,222ウォン
C取消額 = 56,666 - 47,222 = 9,444ウォン続けてBを取り消すとAだけが残ります。割引前合計が50,000ウォン未満になり、Aの割引5,000ウォンがなくなり、送料3,000ウォンが発生します。
保持金額 = 33,333 + 3,000 = 36,333ウォン
B取消額 = 47,222 - 36,333 = 10,889ウォン
累計取消 = 9,444 + 10,889 = 20,333ウォンBの当初の割引後金額18,889ウォンをそのまま返すと、残るAの割引変更5,000ウォンと送料変更3,000ウォンを見落とします。
Bを先に取り消す場合
AとCの販売金額合計は44,444ウォンです。最初の部分取消時点でクーポン条件を失い、送料が発生します。
保持金額 = 44,444 + 3,000 = 47,444ウォン
B取消額 = 56,666 - 47,444 = 9,222ウォン
続けてCを取り消した後の保持金額 = 33,333 + 3,000 = 36,333ウォン
C取消額 = 47,444 - 36,333 = 11,111ウォン
累計取消 = 9,222 + 11,111 = 20,333ウォン二つの順序でBとCの個別返金額は異なります。しかしAだけが残った時点では、どちらも累計取消額20,333ウォン、残高36,333ウォンです。比較すべきなのは商品の固定返金額ではなく、最終条件が同じときの累計結果です。 配送状況やポリシーの版が変わっていれば同条件の比較ではないため、その変化も記録します。
この計算は、どのポリシーにも無条件で使える返金公式ではありません。例えば49,900ウォンと100ウォンの商品に10,000ウォンのクーポンを適用し、40,000ウォンを受領したとします。100ウォンの商品取消で割引を撤回して送料も加えると、保持金額は52,900ウォン、計算結果はマイナス12,900ウォンです。ゼロで上書きしたり負の取消額を送ったりしてはいけません。自動返金を止め、割引維持などのポリシー調整や別途の追加決済が必要か検討します。この数値も法的な請求権を意味しません。PG送信額は正で、確認済み残高以下という前提条件も検査します。計算結果そのものがゼロになる正当な業務取消は、金額誤りとは分けて別経路で処理します。
注文状態とPG残高を結ぶ不変条件
承認済みの元決済一件に通常の取消だけが発生する範囲を、次のように定義します。
P = 当初承認額
C = 重複除去した成功取消額の累計
B = PGで確認した取消可能残高
P = C + B
0 ≤ C ≤ P
0 ≤ B ≤ PToss PaymentsではtotalAmountが当初金額、balanceAmountが取消可能残高です。個々の取消のcancelStatusがDONEか確認します。本稿の通常カード決済の範囲では、一部成功ならPARTIAL_CANCELED、全額ならCANCELEDという状態を金額と併せて検査します。[1]
内部の検査規則はさらに厳密にします。成功記録の合計とPG残高が一致しても、別の商品を取り消したり送料を二重控除したりした可能性があるためです。処理完了後、内部とPGが同期した時点ではB = V(残る商品)も満たす必要があります。処理中は確定残高と取消後の目標額を別フィールドにし、その差を解消済みと表示しません。
REQUESTED、PROCESSING、UNKNOWN、FAILEDなどの内部要求状態の金額は、成功累計に加えません。これらは本稿が提案する内部状態であり、Toss Paymentsの取消状態一覧ではありません。要求回数やHTTP再試行回数も取消回数ではありません。
取消取引は取引ごとの識別子で保存します。Toss PaymentsのlastTransactionKeyは最後の取引を指すため、その値だけを上書きしても以前の部分取消は保存できません。個別のtransactionKeyと金額を記録し、同じ取引を再受信しても一度だけ反映します。[4]
最後の商品がなくなっても、常に残高をゼロにするわけではありません。この例は発送前で残す費用がないためV(空の注文)=0です。既に発生した費用を残すポリシーなら商品状態と決済状態を分けます。また、PGで取消が成功する時点と顧客のカード・口座に返金が表示される時点は異なる場合があるため、案内でも区別します。[2]
取引異常から対応・決済事業者との協議へ — 症状別の確認手順で取引状態を調べ、障害、返金、繰り返す問題を共通の運用手順で処理します。
再試行時に金額を再決定しない
取消実行前に、処理識別子、対象販売単位、ポリシー版、計算前残高、計算後の目標額、確定要求額を保存する設計を推奨します。同じ処理が再度届いたら、新しい計算や取消を作らず既存結果を返します。同じ識別子なのに金額や対象が異なる場合は、不正な再利用として拒否します。
外部要求には処理単位の冪等キーを使います。Toss PaymentsのIdempotency-Keyの有効期間は初回使用日から15日で、処理中なら409 IDEMPOTENT_REQUEST_PROCESSINGが返る場合があります。エラーを受け取ったからといってキーを変え、同じ取消を送らないよう注意も示されています。[3]
タイムアウトは「取消失敗確定」ではなく「結果未確定」と扱います。有効期間内での同一要求の再試行と決済の再照会で結果を確認します。キーと要求本文に加え、APIキー・アドレス・メソッドも同一要求の範囲に含まれる点を考慮します。有効期限経過や認証情報変更後は、新しいキーで実行せず以前の実行有無を先に確認します。[3]
部分取消と全取消も同じ決済残高を共有します。この例では決済ごとに未確定の取消処理を一つだけ許可し、顧客画面・運用者画面・再試行が同じ実行経路を使います。短いDBトランザクションで実行権と版を確保し、ネットワーク呼び出し中に長時間ロックしません。ワーカー再起動後も実行権と未確定処理が失われないようにします。
「全取消」要求の意味も保存します。先の部分取消確定後に残りすべてを取り消すなら、確認済みの最新残高で実行額を確定します。Toss PaymentsはcancelAmount省略時に全額を取り消すため、金額を固定した処理では項目の欠落を防ぎます。isPartialCancelable=falseの決済は、この部分取消経路で実行しません。[2][1]
PGでは成功したのに内部保存が失敗した場合
同じ金額を新しい処理として再取消しません。未確定記録を維持し、同一処理の応答を回復するか決済を再照会して取消取引と残高を確認します。確認済みPG取引を重複防止制約の下で一度だけ記録し、内部の成功累計と注文の返金表示を併せて反映します。後続通知は確定記録に結び、再送可能にします。
冪等な再試行で返るのは最初の要求の応答かもしれません。それは該当処理の結果確認に使い、その後別の取消が反映された最新残高の上書きには使いません。内部処理の版と確認時点を比較し、必要なら現在の決済状態を再照会する方法を推奨します。[3]
運用者がPG管理画面で別途行った取消が混ざる場合もあります。同額というだけで特定商品の要求と結び付けてはいけません。保存済みの取引識別子、処理記録、前後の取引集合などで対応を証明できなければ運用者確認に回します。PG残高だけを合わせるために商品状態を勝手に変更しません。
運用者には、注文・決済・取消処理の識別子、ポリシー版、期待金額、確認済みPG取引と残高、エラー分類、最終確認時刻を渡します。シークレットキーや完全なカード・口座情報は含めません。再試行上限や確認期限を超えた処理、金額不一致、関連付けできない外部取消は自動実行を保留します。
取消順序を変えて検算し、障害は別途試験する
前の注文を六つの可能な順序で最後まで取り消すと、以下になります。ローカル計算コードで検算した設計例であり、実際のPG要求結果ではありません。1・2・3回目は、その順序に対応する取消要求額です。
| 取消順序 | 1回目 | 2回目 | 3回目 | 成功累計 | 最終残高 |
|---|---|---|---|---|---|
| A → B → C | 20,333 | 22,222 | 14,111 | 56,666 | 0 |
| A → C → B | 20,333 | 11,111 | 25,222 | 56,666 | 0 |
| B → A → C | 9,222 | 33,333 | 14,111 | 56,666 | 0 |
| B → C → A | 9,222 | 11,111 | 36,333 | 56,666 | 0 |
| C → A → B | 9,444 | 22,000 | 25,222 | 56,666 | 0 |
| C → B → A | 9,444 | 10,889 | 36,333 | 56,666 | 0 |
すべての段階で、当初承認額は成功取消累計と残高の合計に一致しました。ただし計算が合うだけで決済連携を検証したことにはなりません。実装後、以下の期待結果を許可されたテスト環境で別途確認します。
| 検証状況 | 期待結果 |
|---|---|
| Cだけを一度取消 | 取消額9,444ウォン、残高47,222ウォンでA・Bが残ります。 |
| 同じ取消要求を重複送信 | 同一処理として認識し、PGでの効果と内部成功反映は一度だけ生じます。 |
| 成功取引の再照会・再受信 | 記録済みの取引識別子を成功累計に再加算しません。 |
| 古い取消応答が遅れて到着 | 処理成功は重複を除いて記録し、古い残高で最新の注文・決済状態を戻しません。 |
| 部分取消と全取消が競合 | 一件ずつ確定します。C取消後の残額全取消なら9,444ウォンと47,222ウォンです。全取消が先なら後続の部分取消を実行しません。 |
| タイムアウトまたは処理中応答 | 成功累計を増やさず未確定金額を保持します。同じ残高を使う新規処理は待機します。 |
| PG成功後に内部反映失敗 | 新規取消なしで成功取引を回復し、一度だけ反映します。 |
| 別商品を同額で取消 | 金額一致だけで要求を関連付けず、対象と処理・取引の対応を確認します。 |
| 丸めの余りまたは負の返金 | 保存した配賦規則で再現します。説明できない差や追加請求が必要な状態を最後の商品に押し付けません。 |
最後の取消で確認済み残高を全額返すのは、この例の全額返還ポリシーを完了することです。以前の計算誤りを消す補正手段ではありません。当初配賦の最小単位の余りは配賦規則で処理し、その後に生じた説明できない残高差は例外として残します。
ポリシー変更時は正常取消だけを再実行せず、条件境界、取消順序、再試行、未確定状態も回帰範囲に含めます。採用したテストと除外理由の記録には、変更別の回帰テスト範囲決定ワークブックを活用できます。
部分取消を安定させる出発点は新しいツールではなく、説明できる金額ポリシーと保存済みの取引記録です。単一PGと単純な注文なら、既存注文システム内にポリシー版、配賦値、処理別の冪等性、成功取引記録から整備できます。複合決済や外部運用者の取消が混在する場合は、決済手段別の返還と例外処理の範囲を追加設計します。
ポリシー合意後の実装と検証の役割分担が難しい場合は、IXCのPG連携・決済システム構築サービスで支援範囲を確認できます。



