stripe-list-pagination-previous-attributes について
Stripe の Webhook 差分とページ分割された請求書明細を扱う開発者向けスキルです。返金差分の計算や、先頭ページを繰り返し取得しないカーソルページングを説明します。
主な機能
- 01Webhook イベントの変更項目の読み取り
- 02イベントごとの返金差分の計算
- 03カーソルによる請求書明細のページング
- 04埋め込みページが空の場合の処理
- 05安全な代替手段によるプラン解決
活用シーン
- 01Stripe の差分返金処理
- 02請求書の全明細のスキャン
- 03請求書データからのサブスクリプションプラン解決
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ josiahsiegel/ claude-plugin-marketplace --skill stripe-list-pagination-previous-attributesインストール後、Claude Code で/stripe-list-pagination-previous-attributesを入力して読み込まれたか確認します。
詳細
クイックリファレンス
| フィールド | 意味 |
|---|---|
event.data.previous_attributes | 変更されたフィールドのみです。現在のオブジェクトとの差分を確認する際に使用します |
event.data.previous_attributes.amount_refunded | 以前の累計返金額です。イベントごとの差分を求めるには、charge.amount_refunded から差し引きます |
charge.amount_refunded | すべての返金の累計額です。イベントごとの金額ではありません |
invoice.lines.has_more | true -> 埋め込みの data は1ページ目なので、starting_after を使ってページネーションします |
invoice.lines.data.at(-1).id | 次のページで starting_after に指定するカーソルです |
stripe.invoices.listLineItems(invoice.id, { starting_after }) | カーソルの次から再開します。1ページ目を再スキャンしません |
このスキルを使う場面
次の場合に使用します。
- Stripe オブジェクトの埋め込み
.data[]配列を読み取り、ページネーションが必要になる可能性があるとき - Webhook イベントから「何が変わったか」の差分を計算するとき
- 請求書の明細項目またはサブスクリプション項目から、プラン、SKU、利用権を特定するとき
関連スキル:
- 返金ハンドラーで
getRefundDeltaが使われる箇所:stripe-billing-master:stripe-refund-dispute-lifecycle resolvedViaがメールのレンダリングと監査ログを制御する箇所:stripe-billing-master:stripe-credit-audit-trail
ページネーションのスキャンパターン
async function scanAllLineItems(invoice: Stripe.Invoice): Promise<Stripe.InvoiceLineItem[]> {
const items: Stripe.InvoiceLineItem[] = [...invoice.lines.data];
let cursor = invoice.lines.data.at(-1)?.id;
let hasMore = invoice.lines.has_more;
while (hasMore && cursor) {
const page = await stripe.invoices.listLineItems(invoice.id, {
limit: 100,
starting_after: cursor,
});
items.push(...page.data);
hasMore = page.has_more;
cursor = page.data.at(-1)?.id;
}
return items;
}
starting_after を指定せずに stripe.invoices.listLineItems(invoice.id) を呼び出すと、すでに invoice.lines.data に埋め込まれている1ページ目を再取得します。スキャンは同じ内容を繰り返し処理し、has_more が切り替わらないまま、実際にはスキャンされていない明細項目について「一致なし」と結論づけます。必ずカーソルを引き継いでください。
エッジケース:
invoice.lines.dataが空でもinvoice.lines.has_moreがtrueの場合(まれですが、埋め込みのlimitが0の場合にStripeがこの形式を返すことがあります)、cursorがundefinedのため上記のループは実行されません。この場合は、starting_afterを指定せずにstripe.invoices.listLineItems(invoice.id, { limit: 100 })を呼び出して、新たにスキャンを開始してください。
previous_attributes の差分ヘルパー
type AmountRefundedChanged = { amount_refunded?: number };
export function getRefundDelta(event: Stripe.Event, charge: Stripe.Charge): number | null {
const prev = (event.data.previous_attributes as AmountRefundedChanged | undefined)?.amount_refunded;
if (typeof prev === "number") return charge.amount_refunded - prev;
return null; // caller falls back to embedded / list / skip-revocation
}
ページネーションと G6 の安全フォールバックを備えたプランリゾルバー
export async function resolvePlanFromInvoice(invoice: Stripe.Invoice): Promise<{
plan: Plan;
credits: number;
resolvedVia: "priceMap" | "safetyFallback";
}> {
try {
const items = await scanAllLineItems(invoice); // G3 -- paginate first
for (const item of items) {
const mapped = PRICE_TO_PLAN[item.price?.id ?? ""];
if (mapped) return { ...mapped, resolvedVia: "priceMap" };
}
logEvent("credit_price_resolve_unknown", { invoiceId: invoice.id, lineCount: items.length });
return { plan: "free", credits: 0, resolvedVia: "safetyFallback" }; // G6
} catch (err) {
logEvent("credit_price_resolve_error", { invoiceId: invoice.id, err: String(err) });
return { plan: "free", credits: 0, resolvedVia: "safetyFallback" }; // G6
}
}
メールレンダラーは resolvedVia を使用して、priceMap にあるプラン名の表示を制御します。安全フォールバック経路では「Welcome to Free」を表示しません(G-bonus)。