mcpField
S
stripe-list-pagination-previous-attributes

stripe-list-pagination-previous-attributes

github.com/josiahsiegel開発5969 回インストール19 週間前に更新
リポジトリ

stripe-list-pagination-previous-attributes について

Stripe の Webhook 差分とページ分割された請求書明細を扱う開発者向けスキルです。返金差分の計算や、先頭ページを繰り返し取得しないカーソルページングを説明します。

主な機能

  1. 01Webhook イベントの変更項目の読み取り
  2. 02イベントごとの返金差分の計算
  3. 03カーソルによる請求書明細のページング
  4. 04埋め込みページが空の場合の処理
  5. 05安全な代替手段によるプラン解決

活用シーン

  1. 01Stripe の差分返金処理
  2. 02請求書の全明細のスキャン
  3. 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_moretrue -> 埋め込みの 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)。

コメント