決済システム概要
注目すべき点
本章は v4.0.0 以降の決済システムについて説明します。v4 は書き直しであり、料金はデータベースからコードへ移され、3 プロバイダーのフルフィルメントロジックは 1 つの層に集約されました。3.x からアップグレードする場合は、まず バージョン 4.x チェンジログ をお読みください。
NEXTY.DEV の決済システムは Stripe・Creem・PayPal の 3 チャネルに同時対応し、サブスクリプション(月額/年額)と買い切り購入(クレジットパック)をカバーします。クレジット台帳、チーム共有プール、返金、照合、不正検知への対応を内蔵しています。
全体像を 1 本の流れで
config/pricing.ts 料金の唯一の情報源(1 プラン = 1 オブジェクト)
│ planId
▼
actions/billing/checkout.ts 統一チェックアウト入口。plan.provider で振り分け
│
▼
プロバイダーのホスト型決済画面 Stripe Checkout / Creem Checkout / PayPal
│ ユーザーが支払う
▼
app/api/{stripe,creem,paypal}/webhook ルートは署名検証と振り分けのみ。業務ロジックを持たない
│
▼
lib/billing/{stripe,creem,paypal}.ts アダプター:Webhook ペイロードをコアの入力へ変換
│
▼
lib/billing/fulfillment.ts プロバイダー非依存のフルフィルメントコア。1 トランザクションで完結
│
├──▶ orders 注文行(冪等キー = provider + provider_order_id)
├──▶ subscriptions サブスクリプション行(年額のドリップ計画を含む)
└──▶ lib/credits/ クレジット台帳(残高 + 追記専用ログ)
app/api/cron/credits 定期実行:失われた Webhook の補償、年額ドリップの決済5 つの設計原則
この 5 つを理解すれば、以降のドキュメントは自明になります。
1. 料金はデータではなくコード。 pricing_plans テーブルも管理画面の CRUD も存在しません。config/pricing.ts が唯一の情報源であり、価格変更はコードレビューとデプロイを経ます。詳細は料金設定を参照してください。
2. 金額は常に整数のセント。 データベースの金額カラムはすべて integer で、単位はプロバイダーネイティブのセントです。numeric も浮動小数点も使いません。
3. 冪等性は防御的ではなく構造的。 orders には (provider, provider_order_id) の一意インデックスがあり、書き込みは onConflictDoNothing、返金は行を追加せず累計額を更新します。Webhook の再送は構造上無害で、業務コードのあちこちに重複判定を書く必要がありません。
4. 残高は台帳経由でしか変わらない。 残高テーブルを直接 UPDATE することはありません。すべての変更は lib/credits/ledger.ts を通り、同一トランザクション内で credit_transactions にスナップショット付きの 1 行を追記します。
5. プロバイダー SDK の型はアダプターの外に出ない。 フルフィルメントコアが知っているのは lib/billing/types.ts の統一入力だけで、各プロバイダーのネイティブペイロードはそれぞれのアダプター内で変換されます。プロバイダーの追加はアダプターの追加であり、コアには手を入れません。
データベーステーブル
| テーブル | 役割 | ポイント |
|---|---|---|
orders | お金の動き 1 件につき 1 行 | 冪等キーは (provider, provider_order_id)。返金は行を追加せず元の行を書き換える。アカウント削除時は user_id が NULL になり、注文自体は残る |
subscriptions | サブスクリプションの状態 | 正規化された単一のステータス列(プロバイダーのステータスはアダプターが正規化)。年額のドリップ計画は一級の状態として保持:monthly_credits / remaining_grants / next_grant_at |
credit_balances | 個人のクレジット残高 | 2 つのバケット:subscription_credits(付与のたびにリセット)と purchased_credits(無期限) |
organization_credit_balances | チームの共有プール | 個人テーブルと同形で、主キーが organization_id |
credit_transactions | クレジット台帳 | 追記専用。各行が符号付きの amount、バケット別の増減、変更後のスナップショット、単調増加の seq を持つ |
注文種別 order_type:
| 値 | 意味 |
|---|---|
one_time | クレジットパックの購入 |
subscription_initial | サブスクリプションの初回請求 |
subscription_renewal | 継続請求 |
subscription_upgrade | 期間途中のプラン変更による日割り請求 |
注文ステータス order_status:pending(PayPal eCheck などの保留中キャプチャのみが到達。クレジットは 0 で、定期実行が前進させる)、completed、partially_refunded、refunded、failed。
コードマップ
config/
pricing.ts プラン定義 + 参照ヘルパー(唯一の情報源)
credits.ts 決済以外のクレジット規則(登録ボーナス)
actions/
billing/checkout.ts 統一チェックアウト入口
billing/portal.ts サブスクリプションのセルフサービス入口(プロバイダー別に振り分け)
credits/ クレジットの読み取りと消費(ユーザー側)、管理側の調整と一括付与
orders/ 注文照会(ユーザー/管理)、返金の入口
coupons/admin.ts クーポン管理(Stripe が唯一のストア)
lib/billing/
types.ts 型の契約
fulfillment.ts プロバイダー非依存のフルフィルメントコア
stripe.ts creem.ts paypal.ts 3 つのアダプター
cancel.ts 統一されたプロバイダー側キャンセル
duplicate.ts 重複サブスクリプションの決着
reconcile.ts 定期照合(失われた Webhook の補償)
notify.ts Webhook 起点の通知(Redis で重複排除)
customer.ts stripeCustomerId の唯一の読み書き経路
lib/credits/
ledger.ts 書き込み層:残高変更の唯一の通路
index.ts 読み取り層 + 年額ドリップの決済
app/api/
stripe/webhook creem/webhook paypal/webhook 3 つの Webhook ルート
paypal/create-order paypal/capture-order PayPal の買い切り購入
cron/credits 定期実行の入口
components/pricing/ PricingSection / PricingCard / CheckoutButton課金主体:個人とチーム
同じコードが 2 種類の課金主体を扱います。
- 個人:
organization_idが NULL。クレジットはcredit_balancesに入ります - チーム:
organization_idに値が入り、クレジットはorganization_credit_balancesの共有プールに入ります。user_idは「支払者/操作者」に降格します
両者はバケット構造と冪等メカニズムを共有し、異なるのは残高テーブルだけです。いくつかの約束事:
- チームプランは
audience: 'team'で示し、seatsがメンバー数の上限を決めます。サブスクリプションがない場合の上限は 1(オーナーのみ)です - クレジットパックは個人の資産であり、チームワークスペースでは購入できません(チェックアウトが
TEAM_WORKSPACE_CREDIT_PACKを返し、クライアントが個人ワークスペースへの切り替えを促します) - 「有効なサブスクリプションを持つか」の判定は個人とチームで互いに独立しており、一方が他方の枠を占めることはありません
3 プロバイダーの機能差
| 機能 | Stripe | Creem | PayPal |
|---|---|---|---|
| サブスクリプション | ✅ Checkout Session | ✅ Checkout | ✅ Billing Plan の事前作成が必要 |
| 買い切り購入 | ✅ Checkout Session | ✅ Checkout | ✅ price から動的に注文を作成 |
| ホスト型セルフサービスポータル | ✅ Billing Portal | ✅ Customer Portal | ❌ PayPal の自動支払いページへ遷移 |
| プロモーションコードの自動適用 | ✅ | ✅ discountCode | ❌ |
| クーポン管理画面 | ✅ | ❌ | ❌ |
| 返金 API | ✅ | ✅ | ✅ |
| 保留中の決済(eCheck) | — | — | ✅ pending 注文として記録し、定期実行が前進させる |
| 不正検知シグナル | ✅ Radar | ✅ dispute | ✅ reversal |
各プランが自身の provider を宣言し、3 つすべてを同時に有効化できます。
次に読む
- 料金設定 —— プランの定義方法とプロバイダーへの接続
- 決済フロー —— 購入クリックからクレジット付与まで
- Webhook 処理 —— 3 プロバイダーのイベントとフルフィルメント
- クレジットシステム —— 2 バケット、台帳、年額ドリップ
- 注文とサブスクリプション管理 —— 照会、返金、セルフサービスポータル
- サブスクリプション変更 —— 期間途中のアップグレードの計算方法