Claude CodeでStripeの日割り請求を検算する — previewとのずれ要因
プラン変更の日割り額を、実装前に自前の関数で計算してStripeの請求書プレビューと突き合わせる手順です。ずれる条件を割引・billing_mode・未払いなど原因別に切り分けます。
プラン変更の日割りは、実装してから「請求額が思っていたのと違う」と気づくと痛い領域です。顧客に見せる差額と実際の請求がずれれば、そのまま問い合わせになります。
対策は単純で、実装の前に期待額を別の計算で出しておくことです。Claude Codeに期待額の関数とテストを書かせ、Stripeの請求書プレビューと突き合わせます。ずれたときは、ずれ方から原因を絞れるようにしておきます。この記事は、その検算の組み方と、公式ドキュメントが示すずれの条件を扱います。
日割りの計算は2行で書ける。ずれるのは前提のほう
Stripeの日割りは、旧プランの未使用分をクレジット、新プランの残り期間をデビットとして足し合わせるだけです。公式の例では、請求期間の中途で月額10USDから20USDへ上げると、5USDの追加請求になります。
| 項目 | 金額 | 合計 |
|---|---|---|
| 旧10USDプランの未使用期間(クレジット) | 金額-5USD | 合計-5USD |
| 新20USDプランの残り期間(デビット) | 金額+10USD | 合計+5USD |
期間の半分で切り替えた例です。式にすると、クレジットは -旧価格 × 残り秒数 ÷ 期間の秒数、デビットは 新価格 × 残り秒数 ÷ 期間の秒数 になります。比例配分は秒単位で計算されるので、日数ではなく秒数で割ります。
式そのものは難しくありません。検算が必要になるのは、式に入れる値が実装の外で動くからです。割引、請求期間の違い、課金方式、未払いの請求書。これらが入ると、素直な式の答えとプレビューの答えが食い違います。
検算の関数とテストをClaude Codeに書かせる
期待額の関数は、Stripeを呼ばない純粋関数にします。入力は旧価格・新価格・請求期間の開始と終了・比例配分の日時だけです。ネットワークに触れないので、テストが速く、失敗の原因がStripe側か自前側かを混ぜずに済みます。
最初の依頼は次の形です。
src/billing/expected-proration.ts に、プラン変更の期待額を返す純粋関数を書いて。
入力は旧価格・新価格・請求期間の開始/終了・比例配分の日時(いずれも整数)。
返すのは credit / debit / net。金額は最小通貨単位の整数。
Stripeのネットワーク呼び出しは使わない。まずテストを先に書いて。関数の中身は、次のような形になります。公式の式をそのまま写したもので、丸め方は別途確かめる前提です。
export function expectedProration(p: {
oldAmount: number; newAmount: number;
periodStart: number; periodEnd: number; prorationDate: number;
}) {
const total = p.periodEnd - p.periodStart;
const remain = p.periodEnd - p.prorationDate;
const credit = -Math.round((p.oldAmount * remain) / total);
const debit = Math.round((p.newAmount * remain) / total);
return { credit, debit, net: credit + debit };
}テストの期待値は、Claudeに計算させず手で置きます。30日の期間で10日目に月額10USDから20USDへ上げる場合は、残りが20日です。クレジットは1000×20÷30=666.67、デビットは2000×20÷30=1333.33になり、整数に丸めて-667と+1333、差し引き+666です。期待値を関数と同じ式でClaudeに出させると、式の間違いまで一緒に写ります。
Claudeが書くテストが、先に置いた手計算と合っているかは、自分で読んで確かめます。
Stripeのプレビューと突き合わせる
実際の請求書プレビューは、請求書プレビューのAPI(invoices.createPreview)で取ります。このAPI呼び出しでサブスクリプションは変更されません。渡したパラメーターだけで、次回の請求書の見え方が返ります。
突き合わせで押さえる点は2つあります。
- 比例配分日を固定する。比例配分は秒単位なので、プレビューを取った時刻と実際に更新する時刻でずれます。プレビューに
subscription_details.proration_dateを渡し、更新時にも同じ値をproration_dateで渡します - 比較するのは
totalではなく、比例配分の明細行だけ。プレビューには次の請求期間の通常の引き落とし(prorationがfalseの行)が同居します。公式のサンプルでも、比例配分の行と通常の行が並んで返ります
const at = Math.floor(Date.now() / 1000);
const preview = await stripe.invoices.createPreview({
customer: customerId,
subscription: subscriptionId,
subscription_details: {
items: [{ id: subscriptionItemId, price: newPriceId }],
proration_date: at,
},
});
const prorationLines = preview.lines.data.filter(
(l) => l.parent?.subscription_item_details?.proration,
);
const actualNet = prorationLines.reduce((sum, l) => sum + l.amount, 0);比例配分の判定は、請求書明細の parent.subscription_item_details.proration を使います。期待額の net と actualNet を比べ、差が最小通貨単位の1以内なら一致とみなします。丸めの規則は公式ページに書かれていないので、許容幅は観測した差で決め、決めた値をテストに残します。
比較スクリプトはテストモードのキーでだけ動かします。課金コードを触らせる前の権限の絞り方はStripeサブスク同期の記事にあり、拒否ルールの評価順は権限モデルの変遷で追えます。
期待額とプレビューがずれる条件
ずれたときは、まず差額の大きさと符号を見ます。公式ドキュメントから、ずれる条件を原因別に並べます。
| 条件 | 何が起きるか | 期待額側の直し方 |
|---|---|---|
| 割引がある | 何が起きるか比例配分は元の価格でなく割引後の価格で計算される | 期待額側の直し方入力に割引後の価格を使う |
| 割引を同時に変更 | 何が起きるか同じ呼び出しで割引も変えると、変更後の割引で計算される | 期待額側の直し方変更後の割引を入力にする |
billing_mode がclassic | 何が起きるかクレジットは現在の価格で作られる | 期待額側の直し方旧価格の扱いを分岐させる |
billing_mode がflexible | 何が起きるかクレジットは最後に請求された価格で作られる | 期待額側の直し方最後の請求価格を入力に持つ |
amount_off クーポンと複数項目 | 何が起きるかクラシックとフレキシブルで割引の按分が違う | 期待額側の直し方項目ごとの按分式を持つ |
| 請求期間が違う価格へ変更 | 何が起きるか変更日に新しい期間が始まる | 期待額側の直し方期間の開始・終了を新しい側で取る |
| 数量や価格以外の変更 | 何が起きるかメタデータや支払い方法だけでは比例配分の行が出ない | 期待額側の直し方期待額を0にする |
| 従量課金 | 何が起きるか比例配分の対象外 | 期待額側の直し方検算の対象から外す |
| 直近の請求書が未払い | 何が起きるか未払いでも支払い済みとみなしてクレジットが出る | 期待額側の直し方未払いかどうかを入力に持つ |
順に、数字が出るものを見ます。
割引は「割引後の価格」で計算される
割引があるサブスクでは、比例配分のクレジットとデビットは元の価格でなく割引後の価格で計算されます。比例配分の行自体は discountable=false なので、そこへさらに割引は重なりません。自前の関数に定価を渡すと、割引の割合分だけプレビューより大きくなります。
同じ呼び出しで数量と割引を一緒に変えるときは、変更後の割引で計算されます。割引だけを更新しても、比例配分の行は作られません。
billing_modeでクレジットの根拠が変わる
公式は、10USDの月額サブスクを4月1日に作り、4月11日に日割りなしで20USDへ上げ、4月21日に10USDへ戻して日割りを作る例を挙げています。残りは月の3分の1です。
| billing_mode | クレジットの根拠 | 最新の請求書の合計 |
|---|---|---|
| classic | クレジットの根拠現在の価格(20USD)の3分の1 | 最新の請求書の合計-334(最小通貨単位) |
| flexible | クレジットの根拠最後に請求された価格(10USD)の3分の1 | 最新の請求書の合計0 |
20USDを一度も支払っていないのに、classicでは20USD側でクレジットが立ちます。クレジット約667とデビット約333の差額が-334になる計算です。flexibleは、実際に請求した10USDでクレジットを立てるので、デビットと打ち消し合って0になります。
期待額の関数にはこの分岐を入れておきます。サブスクの billing_mode を入力に含め、旧価格として「現在の価格」と「最後に請求された価格」のどちらを使うかを切り替えます。
複数項目と amount_off クーポンの組み合わせでも、同じ分岐が出ます。公式の例では、10USDと20USDの項目に5USDのクーポンを付け、10USDの項目を月の半ばで外します。
- classic: クーポンが各項目に2.5USDずつ配られ、
-0.5 × (10 - 5) = -2.50USD - flexible: 項目に按分された割引額1.66USDを使い、
-0.5 × (10 - 1.66) = -4.17USD
同じ操作でも、クレジットが2.50USDから4.17USDまで動きます。複数項目のサブスクを検算するなら、この分岐は外せません。
proration_behaviorで「いつ請求されるか」が変わる
プレビューの金額が合っていても、顧客の請求がその時点で発生するとは限りません。proration_behavior の既定値は create_prorations で、比例配分の項目は作られますが、請求されるのは特定の条件を満たすときに限られます。変更と同時に請求書を出すなら always_invoice、比例配分を作らないなら none です。
期待額の検算とは別に、「この変更でいつ、いくら引き落とされるか」を画面に出す実装では、この値を入力に含めます。none を選ぶと比例配分は作られず、次の請求書で新料金が全額請求されます。また、always_invoice で請求が即時に走って支払いが失敗した場合、変更自体は通り、サブスクリプションは past_due になります。期待額が合っていても、この状態遷移はテストで別に押さえます。
未払いの請求書があると、払っていない分のクレジットが出る
Stripeは日割りを、それ以前の請求書がすべて支払い済みという前提で計算します。現在の期間の請求書が未払いのままプランを上げ下げすると、未払いの期間に対してクレジットが出ます。期待額の関数は、この前提を満たしているかを入力として受け取れるようにします。
回避策として、公式は proration_behavior を none にする方法を挙げています。ただし、二重決済につながる可能性があるので、古い請求書は無効化する手当てが要ります。
ずれの原因を順に切り分ける
差額が出たら、上から順に確かめます。
日割りの差額が出たときの切り分け
- Qプレビューと更新で同じ proration_date を渡している?答えがいいえのとき答えがはい
秒単位で時刻がずれています。プレビューに使った日時を、更新にも渡します。
- Q比較しているのは proration の行だけで、total ではない?答えがいいえのとき答えがはい
次期間の通常の引き落とし行が混ざっています。比例配分の行だけを足し直します。
- Q割引後の価格を入力にしている?答えがいいえのとき答えがはい
定価で計算しています。割引後の価格に差し替えます。
- Qbilling_mode ごとに旧価格の取り方を切り替えている?答えがいいえのとき答えがはい
classicは現在の価格、flexibleは最後に請求された価格で計算します。分岐を入れます。
それでも残る差
差が最小通貨単位の1程度なら丸めの差です。それ以上なら、未払いの請求書か、請求期間が違う価格への変更を確かめます。
検算の手順をCLAUDE.mdに残す
毎回の依頼で前提を言い直さないよう、規約をCLAUDE.mdに置きます。
## 日割りの検算
- 期待額は src/billing/expected-proration.ts の純粋関数で出す。Stripeの呼び出しは含めない
- 金額は最小通貨単位の整数。許容差は1単位
- プレビューは proration_date を固定し、proration の明細行だけを足して比較する
- 入力に billing_mode と割引後の価格を含める。未払いの請求書があるかも入力にする
- 差が出たら、原因を上の切り分けの順に報告してから直す作業のあとで、Claudeにテストを実行させて出力を読ませる反復が、この手順の核です。
npm test -- expected-prorationテストが落ちたら、落ちた入力と差額を貼って原因の仮説を挙げさせます。公式のコードレシピにも、テストを走らせて失敗を直させる反復があります。ここでは、仮説を上の表のどの条件に当たるかで言わせると、検討が散らばりません。
Claudeにこの検算を非対話で回させる方法もあります。標準入力にテスト結果を流し、claude -p に原因の分類だけを頼む使い方です。標準入力と標準出力は通常のUnixツールと同じように扱えます。
まとめ
日割りの検算は、式の正しさを確かめる作業ではなく、式に入れる前提を洗い出す作業です。期待額の関数は純粋関数にして、手計算の期待値を先に置きます。プレビューとは比例配分日を固定し、比例配分の行だけを比べます。ずれが出たら、割引後の価格、billing_mode、未払いの請求書の順に疑います。
Claude自身のプラン変更の日割りを知りたい場合は、Claudeのプラン変更とダウングレードに別立てで整理しています。決済の同期側はStripeサブスクの同期と、重複配信への備えを扱うwebhookの冪等受信が続きの実装です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。