決済ステータス定義の最適解

ネットスーパーシステムの決済ステータス表現 (状態遷移) は複雑だ。

その理由は要求要件が多いことに起因しているが、多いことが悪いのではなく、それに応えなければシステムとして真の価値を発揮できないからで。逆に問題解決できなければ、著しく利便性を落としてしまうので、必須要件という位置付けにある。

前提文脈を汲み取りづらいモデリングなので、問題解決例を示すのはあまり見かけないが、自分が考えた決済ステータス定義の答えを示す。

この内容は過去にブログや登壇で話した内容の延長でもあるので、過去の内容も参考にすると良いかもしれません。

E-Groceryにおけるカード決済処理の難しさと設計戦略

ネットスーパーの買い物体験を支える工夫と決済機能実現の過程

前提条件

  • 注文から支払い完了まで時間差がある
  • 注文後に注文内容の変更ができる
  • 品切れが発生するケースがある
  • 販売員が注文内容を変更できる
  • 0円での支払いがある
  • 特典 (値引き) の利用可否を途中で変更できる
  • 複数の決済システム間の仕様差分を吸収しなければいけない
  • 与信枠を可能な限り圧迫しない
  • カード決済以外の支払い手段が選択される場合がある
  • 異なる支払い手段で金額を徴収する場合がある

上記前提を置いた上で捻り出したステータス定義は以下。

status

遷移が多く見づらいが、先に挙げた前提条件を漏れなく満たし切っている。

初めからこの形に着地できたのかというとそうではなく、初めはもっと素朴な表現をしていた。

運用上どういった状態遷移が求められるのかが分かりきらなかったため、あえて素朴な表現を選択をしていたところがある。

1年弱運用を続けると、ある程度は必要な要求も見えてくるため、あるべき表現に再定義し直したという流れ。

複雑さを代償として運用容易性や拡張性、状態の完全性を獲得した表現が今の形になる。

この表現に行き着く際に抑えたポイントを列挙する。

  1. ステータスに重複した意味を持たせない
  • 初期状態 (none) とオーソリをとっていない (provisional) を1つにできたりするが分ける
  • 分けることのメリットがなくとも、意味論的に違うのであれば別物として扱う
  • 棲み分けが違うことで誤解するリスクを減らすというメリットを取る
  • 来の拡張性 (変更のしやすさ) を考えれば、可能な限り分けきっている方が良い
  1. 中間ステータスと終点ステータスを分ける
  • 分けることで運用のオペレーションを楽にする
  • ある期日まで中間ステータスが残っていればそれは異常状態であると判断できる
  • 終点が決まっていて、到達する術も確立されていれば運用も容易になる
  1. 完全性を追求する
  • 抽象的だが追求する意識でステータスを整理する (妥協しない)
  • 「追求」とは、運用ケースも想定しきって対応策とステータを定義することを指す
  • “手動で徴収” という、システム上の何らかのイレギュラーに対応するために物理のオペレーションを挟めるようにしている
  • “カード決済以外” というケースも容易されており、親のデータモデルから見て、ステータスに意味がないことが明示されている

システムはSoEとSoRの棲み分けができ、さらにSoRの中でもSoA (System of Activities) と SoM (System of Management) という分け方ができることを最近知った。(「データモデリングでドメインを駆動する」)

今回のステータス定義の最適解は「SoMの側面を拡充するために必要なステータスの完全性を獲得する」という問いに対する回答でもあった。

先に抑えたポイントを列挙したが、ゴールありきで考えないと決めきれないところがある。

その時には 「SoAの要件を満たしながら、追加で必要になるSoMの機能を実現するために必要なモデルは何か?」という問いで考え始めると良いかもしれない。

追加コメント (2024/04/14)

記事を出してからいくつかコメントをいただいたので、それに回答します。

自分の考えは完璧な正解ではなく、また諸事情により歪な構造であったり、あるべきから離れてしまっていることを十分に認めた上で、売上を生むシステムが泥臭い開発運用の末に行き着いた先の最適解であることを理解いただければ。 失敗している点もあるので、類似した表現を取るのではなく、個々の文脈に合わせて最適な道を選択することこそが正解だと言えるのではないかなと。

トピック1

'cancelled'からキャンセル遷移があるのと、カード決済以外が'unsupported'で1つのステータスにしているのが気になった

cancelledからrevokedにキャンセル遷移があるのは、終点ステータスと中間ステータスを分けるためです。

なぜ分けるのか?というと開発運用における注文状態の完全性を管理しやすくするためです。

終点ステータスに至っていなければいけない時点 (店舗の営業終了など) において、中間ステータスで居続けているデータがある場合に即座に気づけます。

至らないケースが生まれる場面とかそうないのでは?という反論が来そうですが、"開発運用とシステムデザインは性善説ではなく、性悪説ベースで考えるべきだ" というのが自分の主張であり、システムデザイン時の根底に持つ哲学です。

システムが複雑化して、あらゆる変更が加わる可能性がある中で、自分たちが見えてない範囲で加わった変更によりデータの不整合が起き得ないことを100%防げる自信がありませんでした。

時間軸におけるあるべき状態定義と遷移を定めて、それに必ず則れているかを最後に確認する、そのために状態を定義するという意図で revoked という状態を設けてます。

カード決済以外は unsupported で対応しています。もしかしたら将来的に分割する可能性が生まれるかもしれませんが、今の所は問題は起きていないため、この選択で十分だという判断。

トピック2

cancelled、なんとなく事情はわかるけどやっぱその名前なの微妙じゃない?という気持ちがある。あとこれオーソリ失敗したときはどうなるんだろう(provisional → authorized)、描かれてない気がする。

名前付けは微妙なので参考にされないほうが良いと思います。

オーソリの取得に失敗した場合はそもそも遷移しません。描かれていないのではなく、遷移してはいけないものだからです。

お金を払う権利を示されていないのに注文を受け付ける場合があるのでしょうか?

あり得るケースでは追加の遷移が必要になりそうですが、自分が面倒を見ているシステムではそのケースの場合、リクエスト処理自体を失敗とみなしてエラーを返しています。

トピック3

Refund関連が見当たらないのでそれはまた別途あるんだろうか。

あります。キャプチャーを取る時点によって扱い方が変わります。

Refundという事実自体を状態に反映する必要性がないので自分たちは状態(ステータス)としては管理していません。

キャプチャー前: オーソリを単純にキャンセルにして終了。(クレジットなら与信枠を解放し、デビットなら返金) キャプチャー時: キャプチャー取得時点で部分売上計上でオーソリ時の金額差分を返金する キャプチャー後: 商品に不備があった場合にオーソリとキャプチャーを取り直して返金をする キャプチャー失敗: マニュアルで対応 (ここは色々な物理対応方法があり、システム外でなんとかする)

ただし、将来的に状態定義が必要な変更が求められたら差し込むかもしれません。

トピック4

ステータス名って事実たがら過去形にした方がいいんだろうか

「過去形にすべき」という主張を通す強い根拠を示すことはできないのですが、無難にそうしておいた方がなんか良いのでは?と思います。

トピック5

状態がcanceledなのにそこからcancelという遷移があったりしてネーミングが気持ち悪い

気持ち悪いと思われるかもしれません。理由はトピック1で示した内容の通りです。

開発運用から逆算して考えた時に、終点ステータスと中間ステータスを分けた方が良いと自分は考えているので分けてます。

トピック6

状態の名前からオーソリのキャンセル等の文脈が消えてるので混乱する

混乱するかもしれません。自分は長年運用したので慣れてしまっているのかも。

オーソリの文脈を守りたいのであれば、別で状態データを持つことを検討されるのが良いと思います。

自分の肌感はオーソリだけの文脈をだとドメインの問題解決は難しい(ユースケースが複雑で耐え切れない)ので、そこにこだわらずに問題解決に集中して、オーソリ文脈を付与するのが良いのではないかと考えます。

トピック7

注文変更をキャンセル&&alt注文作成にすると状態として一気に減らせられる。まあ、集計時に気を遣うけど

そういうデータモデリングもあると思います。自分も今から全く新しくシステムを作り直すとしたら注文データのライフサイクル上分ける気がします。

トピック8

こういうのってそもそも決済と請求と督促が分かれてないのかな?

注文と請求という棲み分けの方が抽象度が揃っている気がします。別物で扱った方が良いと思います。データを分け始めていますが、最初から分けて扱う方が運用的にも良かったなと少し後悔があります。

トピック9

つらみしか感じない。自分ならfinとそれ以外は別カラムに分けるかなぁ。遷移履歴は別テーブルでログとして残すと思う。決済金額変更がフローに、なさそうなのが救いっぽい?

辛いかもしれませんが、運用上は深刻なほど辛さは生まれていないです。

遷移履歴を残すのも良いアプローチだと思います。イベントソーシング的なモデリングになるのでしょうか。

金額変更はめちゃくちゃあります。状態として表現する必要性はないため、またなくても全く問題が起きていないためないです。

トピック10

https://global.alipay.com/docs/ac/cashierpay/payment_status_desc とか https://docs.stripe.com/payments/payment-intents/verifying-status?locale=ja-JP とか https://documentation.paysimple.com/reference/payment-statuses とか先行の決済事業者の仕組みを理解したほうがいい

恥ずかしながらそういったドキュメントがあり、定義が示されていることを知りませんでした。ありがとうございます。

今から新たに定義し直す場合は大いに参考にすると思います。

ただやはり、ドメイン特有の課題に素直に向き合って解決するのが一番なのではないかなというのは自分の根底に残っています。ので、業界のサンプルを鵜呑みにするのではなく、ドメインごとの最適解を泥臭く探索するのが一番良い答えを出せる道ではないかなと。

トピック11

世のモデリング例って、「きれいなモデル」のことが多いので、複雑だったり泥臭かったりする定義は参考になる。/stripeなどのAPIももちろん参考になるけど、API応答は内部状態を生でさらすとは限らない気がする

自分が示したかったのはまさにこのコメントのところでした。色々な事情を汲むと示しづらいものではありますが、もっと発信されてほしいなと思っています。

SHARES