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. 01读取 Webhook 事件变更字段
  2. 02计算单次退款增量
  3. 03使用游标分页读取发票明细
  4. 04处理嵌入式空页面
  5. 05通过安全回退解析套餐

适用场景

  1. 01处理 Stripe 增量退款
  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) 会重新获取第 1 页 — 即 invoice.lines.data 中已经嵌入的同一页。扫描会循环处理相同内容,直到 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 中套餐名称的显示 — 在安全回退路径中绝不会渲染“欢迎使用 Free”(G-bonus)。

评论

支持 Markdown