Menu

サブスクリプション変更

注目すべき点

v4.0.0 以降、期間途中のプラン変更はボイラープレートが内蔵で処理するため、自分で実装する必要はありません。3.x の「コメントを見ながら自分で開発する」ガイドは廃止されました。本ページでは入口の設定方法と、内蔵ロジックの具体的な計算方法を説明します。

製品と料金の関係

まず重要な前提が 1 つあります。Stripe のサブスクリプション変更は異なる製品間の切り替えです。

  • 対応:Product A → Product B
  • 非対応:Product A の Price 1 → Product A の Price 2

そのため、すべての価格を 1 つの製品に詰め込むのではなく、機能の階層で製品を分けることを推奨します。

Product1:
├── Price: $10/month
└── Price: $100/year
 
Product2:
├── Price: $20/month
└── Price: $200/year
 
Product3:
├── Price: $50/month
└── Price: $500/year

config/pricing.ts では、これが starter-monthly / starter-yearly / pro-monthly / pro-yearly のようなエントリに対応し、それぞれが自分の Price ID を指します。

プラン変更の入口を有効にする

Stripe Customer Portal の設定を開きます。

Stripe Customer Portal

Subscriptions で変更の入口を有効にし、切り替えオプションを順に設定します。

Stripe Customer portal subscription
Stripe Customer portal subscription
Stripe Customer portal subscription

設定が完了すると、/dashboard/billing で「サブスクリプションを管理」をクリックしてポータルへ入ったユーザーに、プラン変更の機能が表示されます。

Stripe Customer portal subscription
Stripe Customer portal subscription
Stripe Customer portal subscription

ユーザーがプランを変更すると、Stripe が差額を計算し、日割りの請求書を発行します。

変更後に起きること

ユーザーが Billing Portal でプランを変更
        │
        ▼
Stripe が日割りの請求書を発行
        │
        ▼
invoice.paid(billing_reason = subscription_update)
        │
        ▼
lib/billing/stripe.ts が期間途中の変更と判定
        │
        ▼
fulfillSubscriptionUpgrade()          ← リセットではなく増分

3 種類の請求の振り分けを比べてみます。

billing_reason経路クレジットの扱い
subscription_createfulfillSubscriptionPayment新プランの枠へ完全リセット
subscription_cyclefulfillSubscriptionPayment完全リセット(新しい周期の開始)
subscription_updatefulfillSubscriptionUpgrade増分の上乗せ。リセットしない

クレジット差分の計算方法

式は 1 行です。

delta = round( max(0, 新プランの月次枠 − この窓で既に付与した累計) × 窓の残り時間の割合 )

サブスクリプションバケットは増えるだけで、ここではリセットされません。2 つの因子がそれぞれ 1 つの役割を担います。

因子 1:窓の高水位(既に付与した累計)

旧プランの設定値を読むのではなく、台帳からこの窓の subscription_grant 行のサブスクリプションバケット増分を合算して求めます。

これには 2 つの利点があります。

  1. Webhook の順序乱れに構造的に強い —— 「変更前はどのプランだったか」という、順序乱れで壊れうる状態に依存しません
  2. 同時に不正防止の上限として機能する —— 1 つの付与窓における累計付与量は、その窓で到達した最高プランの月次枠を決して超えません

因子 2:残り時間の割合

付与量を、実際に支払われた日割り差額と厳密に対応させます。

2 つの因子が組み合わさって防ぐもの

行ったり来たりでクレジットを稼ぐ:先にダウングレードするとクレジット残高が生まれ、次のアップグレード請求を相殺します(実支払いはほぼ 0)。しかしダウングレードで高水位は下がらないため、2 回目のアップグレードの delta は常に 0 になり、クレジットを生み出せません。

周期末に安くひと月分をかすめ取る:残り時間の割合分しか付与されないため、丸ひと月分の差額は得られません。

支払額のしきい値はない

クレジット残高で完全に相殺され、実支払いが 0 円になったアップグレード請求でも、同じ式で上乗せされます。高水位の仕組みが既に安全性を担保しているため、「いくら払ったら付与する」という判定を追加する必要はありません。

ダウングレード

付与も回収も行いません。

高水位は下がらないため、ダウングレード時点では何も付与されません。次の周期の請求(subscription_cycle)が完全リセットの経路を通り、枠は自然に新しいプランの水準へ下がります。

この設計により、「ダウングレードした瞬間に、既に手元にあるクレジットが没収される」という体験上の惨事を避けられます。

周期をまたぐ変更(月 ↔ 年)

支払いによって課金周期が引き直されるため、年額ドリップの計画も追随させる必要があります。

変更の方向ドリップの扱い
月額 → 年額再アンカー:この注文が初月分の差額となり、残り 11 期がスケジュールに入ります
年額 → 月額ドリップ計画をクリア
年額 → 年額(同一周期)ドリップは触りません。以降の決済はサブスクリプション行の monthly_credits を読み、syncSubscription が既に更新済みのため、自動的に新しい枠が適用されます

2 つの境界ケース

既に重複と判定されたサブスクリプション:ローカルの状態が終端のため、お金の痕跡(クレジット 0 の注文行)だけを残し、それ以上は何も付与しません。

金額 0 かつ差分 0 の日割り請求(純粋なダウングレードや同格の切り替え):お金もクレジットもないため、注文行を書きません。

ほかのプロバイダー

Creem と PayPal は現時点でこの期間途中の経路を通りません。両者のプラン変更は各ポータルの機能に依存し、支払いイベントは通常のサブスクリプション決済として処理されます。これらが主力プロバイダーの場合、プラン変更は解約してから再契約するようユーザーを誘導してください。

テスト

Stripe の Test Clock で時間を進めれば、サブスクリプションのライフサイクル全体をシミュレートできます。

カバーすべきシナリオ:

同一周期での変更

  • 月額のアップグレード:starter-monthly → pro-monthly
  • 月額のダウングレード:pro-monthly → starter-monthly
  • 年額のアップグレード / ダウングレード:同様

周期をまたぐ変更

  • 月額から年額:任意の月額プラン → 対応する年額プラン
  • 年額から月額:任意の年額プラン → 対応する月額プラン

不正防止の検証

  • ダウングレード直後にアップグレードで戻し、2 回目のアップグレードの delta が 0 であることを確認
  • 周期の終わり際にアップグレードし、残り時間の割合分しか付与されないことを確認

変更のたびに次の 4 か所を確認してください。

場所見るもの
/dashboard/billing残高と現在のプランが正しいか
/dashboard/my-orderssubscription_upgrade 種別の注文があるか
credit_transactions種別、増分、スナップショットが整合しているか
subscriptions テーブル周期をまたぐ変更のあと、monthly_credits / remaining_grants / next_grant_at が期待通りか

開発モードでは /dashboard/credit-usage-example を使って消費経路を素早く検証することもできます。