クラウドファンディングの基盤をフルスクラッチで作りました。
機能一覧を並べる前に、先に決めておかないと後で作り直しになる論点が
いくつもあったので、実際に判断した順にまとめます。
1. 「支援」を何のレコードとして持つか
最初に迷うのがここです。支援を「注文」として持つか、「約束」として持つか。
クラウドファンディングは その場で確定しない のが普通の EC と決定的に違います。
募集期間中は「支援を表明した」だけで、成立するのは締切後です。
そこで支援レコードには最低限これらの状態を持たせました。
pending … 支援表明(オーソリのみ)
captured … 目標達成 → 実売上
refunded … 不成立 or キャンセル → 返金
failed … 決済失敗
支援 = 状態機械として設計しておくと、後から
「All-in / All-or-Nothing の両方に対応したい」と言われても耐えられます。
2. 金額を整数で持つ
これは早めに決めるほど傷が浅い論点です。Decimal か int かで悩みますが、
日本円だけを扱うなら 円単位の整数が結局いちばん事故りません。
amount = models.PositiveIntegerField("支援額(円)")
Float は論外です。手数料の計算で 0.1 円のズレが出て、集計が合わなくなります。
3. 決済代行に「何を任せるか」を線引きする
カード番号は絶対に自前で持たない、というのは前提として、
その先の線引きを最初に決めておきます。
| 項目 | 自前 | 決済代行 |
|---|---|---|
| カード情報 | 持たない | トークン化して保持 |
| オーソリ / キャプチャ | 呼ぶだけ | 実処理 |
| 支援履歴・集計 | 自前DBが正 | 参照用 |
| 返金 | 申請を作る | 実処理 |
「自前DBを正とし、決済代行は実行装置として扱う」と決めておくと、
将来決済代行を乗り換えるときの影響範囲が読めます。
4. 冪等性キーを最初から入れる
支援ボタンの二度押し、通信断による再送、Webhook の重複配信。
お金が動く処理では 必ず起きます。後付けは本当に大変なので最初に入れます。
class Pledge(models.Model):
idempotency_key = models.CharField(max_length=64, unique=True)
リクエスト側でキーを生成し、同じキーの処理は結果を返すだけにします。
5. 締切処理を「バッチ」と割り切る
締切ちょうどに全プロジェクトの集計と決済確定を走らせようとすると、
必ずどこかで失敗します。失敗を前提に、何度流しても同じ結果になる
バッチとして書きました。
# 未処理のものだけを拾い、1件ずつ確定していく
for pledge in Pledge.objects.filter(project=p, status="pending"):
with transaction.atomic():
capture(pledge) # 失敗したらこの1件だけロールバック
プロジェクト単位ではなく 支援1件ずつトランザクションを切るのが要点です。
1件の失敗で全体が巻き戻ると、リトライのたびに状況が悪化します。
まとめ
お金が動くサービスで後から効いてくるのは、機能の数ではなく
状態の持ち方と、失敗したときの戻り方でした。
この5つを先に決めておくと、機能追加のたびに設計を壊さずに済みます。