关于 stripe-list-pagination-previous-attributes
面向开发者的技能,讲解如何处理 Stripe Webhook 变更和分页的发票明细,涵盖退款增量计算及避免反复读取首页的游标分页方式。
主要功能
- 01读取 Webhook 事件变更字段
- 02计算单次退款增量
- 03使用游标分页读取发票明细
- 04处理嵌入式空页面
- 05通过安全回退解析套餐
适用场景
- 01处理 Stripe 增量退款
- 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) 会重新获取第 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