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

はじめに

こんにちは!yamakazu (@yamarkz) です。プロダクトブログへの登場は昨年ぶりになりました。

さて、6月は欧州サッカーのシーズンオフになりますが、対してインターナショナルマッチ(国際Aマッチ)が行われる月なので、代表ファンとしてはワイワイ!な月です。今年は冬のW杯も楽しみですね。

という趣味の小話は最初だけにして、今回はStailerで向き合っているカード決済の難しさと、その難しさに対応するために選択した設計戦略を紹介していこうと思います。

今10Xが賭けているE-Groceryという領域はまだまだニッチで、開発知見がほとんど出回っていないのが現状です。決済に関しては海外含めてGoogle検索でもほとんど確認できませんでした。(ex: instacart, Target)

本記事が、E-Groceryの様な複雑なドメインで決済処理を実装する際の参考になれれば嬉しいです。

なぜ、E-Groceryの決済は難しいのか

早速結論から述べると、E-Groceryの決済が難しい理由は、サービス体験の品質として譲れない機能性を担保するために、不確実性を受け入れなければいけないからです。

これはドメインの特徴として、機能性と不確実性がトレードオフという関係性にあり。片方を取ったら片方を捨てる。機能性を追求するなら、ソフトウェアは不確実で複雑な要件に応えるということです。

このトレードオフは一般的な対比でもあるかもしれませんが、E-Groceryにおいてはその特徴が顕著に現れるものであることを、ここで明言しておきます。

多種多様な商品のまとめ買い体験を提供する

E-Groceryは多種多様な商品を一度にまとめて注文し、注文後の変更余地を残しながら最終的な商品の有無を確定、準備できた商品をお客様に届けて初めて価値提供できるものです。

商品をネットで買って自宅で受け取るという意味ではECと同じですが、その過程が特殊であるため、意図的にE-Grocery (ex: ネットスーパー, ネットドラッグストア) という表現を選択しています。ECという概念の部分集合がE-Groceryというイメージです。Quick Commerceと対比されたりもしますが、これもまた別の部分集合です。

特殊なドメインであることを念頭に置いて、その過程で何が起きるのか、起きた場合に決済周辺ではどういった対応を行うのかをStailerを題材に整理していきます。

前提

  1. Stailerは決済代行業者と提携し、決済処理を委譲しています
  2. Stailerは複数の決済代行業者と連携しています
  3. Stailerは2022/06時点でモノリシックアーキテクチャを選択しています
  4. 一般的なオーソリ(与信)やキャプチャー(売上請求)といった決済手順に則ります

注文過程に存在する不確実性の整理

注文過程と不確実性の整理

Stailerの注文過程とそこで起きうるイベント(= 不確実性)を簡易的な図にまとめると↑になります。各イベントシーンで提供される機能はどういった価値があるのか。その裏側で必要となる決済対応が何なのかを1つずつ取り上げます。

決済確定までの時差

まず最も特徴的なのが時差です。

E-Groceryのサービスは確定までの時間が長く、またその過程で生まれる不確実性も多いです。

注文から決済確定までに時差があること自体は実は一般的で、Amazonや他のECでも一定時間を経て決済確定を行なっていたりします。

この時差を設ける理由の大半が、最終的な物理在庫の確定と配達業務完了を最終的な決済確定タイミングとして、注文後の”もしも”の在庫準備不可や配達不備による例外に対応できる様にするためです。(参考)

Stailerでもそういった側面で時差を設けたりしていますが、それ以上に多くの理由が存在します。それが以下で取り上げる機能性を担保するためです。

機能性を担保するために時差を生み出していますが、これによりシステム観点では都度発生する変化に対応し、決済代行側とのデータ整合性を保つことが求められます。

決済では、

  • 決済ではオーソリを取ってから、キャプチャーを取るまでに時間差を設ける
  • 注文作成の過程ではキャプチャーを取らない
  • オーソリ額は変わる可能性があり、キャプチャーはお客様の手元に商品が届けられたことの確実性が高い確率で証明できる状況で実施する

注文変更による金額変動

締め切りまで注文変更ができる

Stailerには注文変更が機能として存在します。

現代のECはAmazonに代表されるように1タップで注文、即配達という世界から考えると ”注文を変更する” という感覚自体が異質に感じるかもしれません。

注文作成後、指定時間までであれば何度も注文内容を変更でき、「やっぱり追加して買いたい」や「家にあったからやっぱり買うのやめよう」といった「やっぱり」な購買体験を提供します。これは1度の注文で多量の商品を扱うことや、その大半が食料品であるために過不足が調整しやすくあって欲しいという要望からです。

実店舗でも商品をカゴから売り場に戻す行為は推奨されていませんが、禁止されていません。

決済では、

  • 決済では注文変更が起きた場合、オーソリ(与信)を取り直す
  • ただし、内容が変わっても支払い金額が変わらない場合は、取り直さない
  • オーソリが何らかの理由で取れなかった場合、注文変更自体を取り消す

配達/受け取り日時変更による金額変動

配達/受け取り日時変更によって金額が変わる例

Stailerには配達と受け取り日時を選択する機能が存在します。

いつでも自由に届く/受け取れるわけではなく、一定の時間制約が設けられています。これは扱う商品が生鮮食品が中心で時間管理がシビアであることが主な理由です。

注文作成後でも指定時間までであれば何度も希望日時に変えることができ、「やっぱり遅い時間に受け取りたい」や「やっぱり次の日の午前中でいいや」という、これまた「やっぱり」な買い方を提供します。

日時変更によって商品金額が変わるケースがあります。時間で金額が変わるの…??? と思われる方もいるかもしれません。

ネット上の食料雑貨店といえど、実店舗と運営自体は同じであることがほとんどなので、セール価格も店舗と同じサイクルで反映されます

例えば、4/15にセールで148円だった商品が4/16には178円になるといったことが起こります。その際に支払い金額が変わります。

決済では、

  • 決済では配達/受け取り日時変更が起きた場合、オーソリ(与信)を取り直す
  • ただし、日時を変更しても金額が変わらない場合は、取り直さない
  • オーソリが何らかの理由で取れなかった場合、日時変更自体を取り消す

注文のキャンセル

指定時間まで注文のキャンセルが可能

Stailerでは注文後の変更可能な時間までであれば、キャンセルすることができます。

ECでキャンセルした経験がある方は少ないのではないでしょうか。一般的な感覚ではECは注文完了後にキャンセル余地を残していないケースが多いです。

E-Groceryならではの1度に注文する商品の多さから調整しやすくする配慮や、受け取り時間の指定という制約に対する代替としての配慮である意図が強いです。

決済では、

  • 決済では注文キャンセルが起きた場合、オーソリを取り消す
  • オーソリはお客様の決済利用可能枠を一時的に抑えることであり、キャンセルしない場合お客様の決済利用可能枠を圧迫してしまう

品切れによる金額変動

品切れの発生により注文金額が変動する

Stailerには品切れという概念があります。言葉の通り注文自体は受け付けたが、実際には商品を準備できなかったということです。

これはECとしてはかなり特殊な不確実性で、ほとんど発生し得ないと考えられていますが、E-Groceryという領域に絞るとその遭遇頻度は一般ECよりも体感で10xほどになります。

その理由は店舗型に限ってですが、実店舗と物理在庫を共有しているため、時間帯やお客様の購買頻度によっては先に売り場から商品がなくなってしまうからです。

つまりは在庫数管理はしているものの、それ自体が購入を確実に補償するものではないのです。もちろんそうではなく、E-Grocery専用の在庫管理ができれば理想ですが、それでは現状は採算性が合わない、確実に補償するオペレーションを組みきれていないというのが現状です。

品切れになった商品はキャンセルと同等の扱いで注文内容から商品単体で取り消されます。

決済では、

  • 決済では品切れが起きた場合、オーソリを取り直さない
  • 品切れは金額が下振れる現象であり、与信枠が不足する現象ではないから

代替品選択による金額変動

希望する方に代替品を提供する

先の品切れへの補填として、代替品を選択して提供することがあります。

例えば、牛乳Aが売り切れていた場合、牛乳Bを選択して届けるということです。これは実店舗でも行われる行為で、いつも買ってるアレがなかった時に、その代わりを選んで購入すると思います。これをStailerでは代替品選択という機能で実現されています。

品切れ商品に対して代替品が選択されても、お客さまへの支払い金額は下振れることはありますが、上振れることはありません。150円の商品が品切れにになり、170円の商品を代替品として選んだ場合、150円の支払いのまま決済に進みます。これは商品の準備ができなかったことがお客様都合の問題ではなく、店舗の問題だからです。

決済では、

  • 決済では代替品選択が起きた場合、オーソリを取り直さない
  • 代替品選択は金額が下振れる or 同額に収まる現象であり、与信枠が不足する現象ではないから

割引(ポイント利用 / クーポン利用)による金額変動

ポイント利用によって割り引かれる

大半の小売企業ではお客さまへのエンゲージメントを高めるために、一定の利用頻度に対して割引を効かせることがあります。その際たる例がポイント利用とクーポン利用です。

ポイントは中期での割引還元、クーポンは短期での割引還元という位置づけです。どちらも支払い金額に割引を効かせることができます。

決済では、

  • 注文作成でポイントやクーポン利用によって支払い金額が0円になった場合、オーソリを取らない。1円以上である場合はオーソリを取る
  • 注文変更でポイントやクーポン利用によって支払い金額が0円になった場合、オーソリを取っていればオーソリをキャンセル。元々取っていない(0 → 0)場合はオーソリを取らない

支払い金額0円のケースを想定するとかなり複雑になってきました... 🤨

カード種別 (デビットカード) による二重引き落とし対策

カード決済にはクレジットカードの他にデビットカードが使われる可能性があります。

デビットカードとは、口座から即時引き落とされるカードのことです。(参考)

デビッドカードであるかどうかはカード番号から識別できないため、デビッドカードというケースもあり得ることを想定して実装する必要があります。

デビットカードの場合、オーソリを取った時点で口座引き落としがなされます。この仕様は、何度も注文変更を行うとその都度指定額が口座から引き落とされることになり(一時的な二重引き落とし)、お客さまにとって都合が悪くなります。ちなみに、変更のオーソリを取る前の額は後ほど返金されます。

決済では、

  • 決済ではお客様の口座残高を圧迫させないため、金額が上振れた場合にのみオーソリを取り直す
  • 金額が下振れた場合は支払い可能枠が保証されているため、オーソリを取り直さない
  • デビットカードでキャプチャーを取るタイミングに生まれた差額は、後日返金される

ここまでのオーソリ取る/取らない/取り消すがどの場合に選択されるのかを整理したのが以下の図です。

分岐地獄 🤯

以上がStailerにおける、注文過程に存在する不確実性の整理でした。

各機能性が何を狙いに存在するのか、それらを決済の視点で捉えた時にどういった考慮で期待の振る舞いを定義しているのかを理解いただけたのではないでしょうか?

この内容を踏まえて、さらに踏み込んで実装上選択した設計判断を次に紹介します。


Stailerで選択した設計戦略

ここでの設計戦略とは、機能性, 信頼性, 効率性といった、守りたいソフトウェアの品質特性を得るために選択された、論理整合性が担保された設計判断(技術意思決定)の集合のことです。

1. 非同期処理化

Stailerでは決済処理の一部を非同期処理化しています。非同期処理を選択することで、障害やデータの不整合が発生した場合に、同期処理に比べてリカバリーがしやすくなります。

具体的には、キャプチャーを取る処理は定時実行のバッチ処理になっています。

配達完了処理(同期処理)でキャプチャーを取った場合でも機能性は満たせるかもしれませんが、お客さまに商品を届けた後に何らかの問題が発生して返品や返金の対応が発生する可能性があります。そういった不確実なケースへの備えとしても、非同期処理でキャプチャーを取った方が信頼性や効率性を守りやすくなります。

もし処理の同期性が仕様として求められていない (= 犠牲にしても問題ない) のであれば、非同期処理を選択するのが良いでしょう。

2. 支払い方法の再選択可能性を諦める

注文後は支払い方法を変更することができない

Stailerでは機能性を諦める選択もしています。注文後に支払い方法を変更することができません。

純粋に機能性を追求するなら、支払い方法を注文後に変更できると嬉しいと思います。 しかし、その機能性を担保することで対応しなければいけない不確実性の方が大きいと判断し、また支払い方法を変更することで救われるお客様の母数を想定すると、機能価値が低いという結論に至り、注文後は変更不可という仕様にしています。

3. 各社決済代行のAPIに存在する仕様差分を吸収する層を設ける

これはStailerという複数の決済代行業者と連携する仕組みである場合の話なので、かなり特殊なケースです。

Stailerではパートナーの希望に応じた決済代行業者と連携して決済機能を提供しています。決済としての機能は同じなのですが、I/Fが違うことはもちろん、小さな仕様差異が生まれてしまうのは避けられず。そういった差分をなるべく意識しなくて良いように共通のI/Fで体裁を整える考慮がなされています。

具体的にはAPIクライアントは完全に独自の実装、PaymentServiceクラスから共通のI/Fを踏襲、PaymentTransactionServiceクラスで振る舞いの仕様差異吸収と複雑な制御を行う構成になっています。

4. リコンサイルで異常がないか検知する or 検知できる状態をつくる

決済の仕組みは外部の決済代行サービスを利用して実現しています。Stailer自体はモノリスであるものの、外部サービスに依存すると分散システム的な考慮が必要になってきます。

特に気をつけなければいけないのがデータの整合性です。データに差異があることをどのように検知するか、検知した場合どう対処するか (= どちらの何のデータを正と見做して修正するか) まで考慮して設計が必要です。

これに関しては定時実行で期待通りに決済の処理が行われているかを確認するバッチ処理や、リカバリーツールを用意して異常検知とリカバリー対応を行える体裁を整えています。

5. オーソリを取り直さないケースを持つ

Stailerではオーソリを取り直さないケースを持っています。

クレジットカードだけであれば、都度取り直しても良いと思いますが、デビットカードではお客様の口座残高を圧迫してしまいます。これを避けるためにも一部のケースでオーソリを取り直さないという判断をしました。

取り直さないのは支払い予定金額が元の金額よりも下振れた場合のみで、上振れた場合は取り直しています。でなければお客さまに支払い能力がないにも関わらず、注文を受けてしまうという可能性があるからです。これにより、一定の口座金額利用圧迫の負荷を低減しています。

6. ドキュメンテーションで有事の際に共通認識を持てる様にする

いかに複雑で認知負荷が高く、把握が難しいかを理解いただけたかと思います。

複雑な構造でも長くメンテナンスしていく覚悟が必要です。少しでも楽にできるようにドキュメントも拡充を進めてきました。全てを言葉で説明するよりも図を使って、整理した方がわかりやすくなるからです。

ドキュメントではシーケンスフロー、状態遷移、シナリオケースを記載して決済制御を整理し、有事の際や追加変更を加える際にエンジニア、PdM、BizDevが素早く現状理解ができる様に工夫しています。

まとめ

長くなりましたが、ここまで触れてきた内容を端的にまとめると次のような内容でした。

  1. E-Groceryの決済は不確実性が多く存在するドメインである
  2. Stailerでは多様な設計判断を駆使して対応してきた
  3. 総じて不確実な変数とその組み合わせをどう捌くか、開発者としての腕が問われる

最後に

Stailerを例にとってE-Groceryにおける複雑な決済仕様を紹介してきました。

書き始めてみれば思っていた以上に重厚な内容になってしまい、決済がいかに複雑で奥深い領域であるかを改めて実感させられました。 (ps: 書き初めの頃はもっと軽く収まると思っていた…)

これも見方を変えれば、複雑であるが故に技術者としての腕が問われる領域であり、複雑さを受け入れてでも機能性を追求しなければE-Groceryという領域のプロダクトは価値を生み出せないということでしょう。

本記事を読んで、決済機能の技術意思決定がより良いものになっていただけたら幸いです。