关于 stripe-refund-dispute-lifecycle
面向开发者的技能,用于实现会调整积分或付费状态的 Stripe 退款与争议 Webhook 处理程序。它强调退款差额计算、明确处理争议状态,并区分订阅退款与积分包退款。
主要功能
- 01退款差额来源优先级
- 02争议状态恢复白名单
- 03按比例撤销积分包积分
- 04区分订阅与支付模式
适用场景
- 01实现 Stripe 退款 Webhook 处理程序
- 02处理争议创建与关闭
- 03退款后按比例撤销积分
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ josiahsiegel/ claude-plugin-marketplace --skill stripe-refund-dispute-lifecycle安装后在 Claude Code 中输入/stripe-refund-dispute-lifecycle确认已加载。
详细介绍
快速参考
| 事件 | 操作 | 关键规则 |
|---|---|---|
charge.refunded | 按退款增量比例撤销积分 | G2 — previous_attributes.amount_refunded |
charge.dispute.created | 将用户设为 past_due,并存储检查点 | G1 + G9 |
charge.dispute.closed | won / warning_closed / prevented -> 恢复;lost -> 无操作(由 charge.refunded 处理);其他情况 -> 无操作 | G5 + G7 |
| 退款来源优先级 | 使用时机 |
|---|---|
event.data.previous_attributes.amount_refunded(G2) | 首选 — 始终优先使用 |
charge.refunds.data(按 created 降序排列) | previous_attributes 缺失时的回退方案 |
stripe.refunds.list({charge, limit:1}) | 内嵌数据缺失时的最后手段 |
仅使用 charge.amount_refunded | 绝不使用(这是累计值,并非单次事件的值) |
何时使用此技能
实现任何用于响应 Stripe 退款或争议事件而撤销积分或修改付费状态列的处理程序时,请使用此技能。
相关技能:
- 关于
getRefundDelta(G2 增量辅助函数):stripe-billing-master:stripe-list-pagination-previous-attributes - 关于规范的退款辅助函数和审计行不变量:
stripe-billing-master:stripe-credit-audit-trail - 关于每个争议处理程序都会封装的 G1 检查点模式:
stripe-billing-master:stripe-webhook-idempotency
核心规则
G2:退款增量
使用 stripe-billing-master:stripe-list-pagination-previous-attributes 中的 getRefundDelta() — 该技能负责增量计算辅助函数。关键保证:当没有可用来源时,辅助函数会返回 null,处理程序必须跳过撤销操作,而不是猜测。
G7:穷尽式 shouldRestoreStatus
const shouldRestoreMap = {
won: true,
warning_closed: true,
prevented: true,
lost: false,
needs_response: false,
under_review: false,
warning_needs_response: false,
warning_under_review: false,
charge_refunded: false,
} satisfies Record<Stripe.Dispute.Status, boolean>;
export const shouldRestoreStatus = (s: Stripe.Dispute.Status): boolean => shouldRestoreMap[s];
当 Stripe 在未来的 SDK 版本中新增状态时,在添加对应键之前,此对象会导致编译错误 — 这会强制你审慎决定是否将其加入 G5 允许列表。
积分包与订阅退款
async function resolveCreditsToRevoke(charge: Stripe.Charge, refundAmount: number) {
const sessions = await stripe.checkout.sessions.list({
payment_intent: charge.payment_intent as string,
limit: 1,
expand: ["data.line_items"],
});
const session = sessions.data[0];
if (session?.mode !== "subscription") {
const pack = CREDIT_PACKS.find(p => session?.line_items?.data?.[0]?.price?.id === p.priceId);
if (pack && session.amount_total && session.amount_total > 0) {
// Proportional revocation: if they refunded 50% of the pack, revoke 50% of the credits
return Math.round(pack.credits * (refundAmount / session.amount_total));
}
}
// Subscription: 1 credit = 1 cent at cash-equivalent
return refundAmount;
}
关于积分包计算的说明:由于积分包采用批量定价(例如,1000 积分售价 $9,而不是 $10),因此按固定比例将 refundAmount 转换为积分会导致撤销过多。务必查找发起交易的 Checkout Session,以区分 mode: "subscription"(等值现金)与具有已知积分包价格 ID 的 mode: "payment"(按比例计算)。
评论
支持 Markdown