支付催收与权益宽限:重试证据、限制代际和恢复竞态
一次扣款失败不应立即封号,一封 payment_succeeded 也不能无条件恢复全部权限。本文把发票、重试、宽限和权益限制连接成版本化状态机,处理乱序 Webhook、未知扣款与成功付款和取消的竞争。
本文要点
- 主题聚焦:DevOps、独立开发
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
支付平台报告一张续费发票失败,系统立刻关闭 workspace;客户刚更新卡并付款成功,旧的 failure Webhook 却晚到,又把账号封了一次。另一种做法是“失败后宽限七天”,但重试服务停摆、发票状态未知时计时器仍自动取消,最终既可能误伤已付款客户,也可能让欠款账号长期保持写权限。催收不是一串邮件,而是资金事实和权益代际之间的受控状态机。
AI SaaS 权益快照定义运行时如何消费商业授权,定时自动化运行账本处理日程实例。本篇负责哪一张发票在什么证据下进入宽限、恢复或限制;它不自行发起价格迁移,也不把支付平台的一条通知当成最终资金真相。
发布 dunningPolicy.v1
subscription.dunningPolicy.v1 绑定 provider/account、适用 price/customer cohort、发票状态 mapping、重试 owner(平台或本地二选一)、最大尝试与间隔、grace 起点和截止、允许的 entitlement profile、通知模板 generation、人工例外、取消规则、时钟与恢复前置条件。Smart Retry 的动态安排必须保存 provider next-payment-attempt 证据;本地计划不能同时再创建第二套扣款。
schema: subscription.dunningPolicy.v1
policy: standard-monthly@6
retryAuthority: PAYMENT_PROVIDER
maxAttempts: 4
grace: {duration: P7D, profile: READ_EXPORT_NO_NEW_JOBS}
restrictAfter: GRACE_EXPIRED_AND_INVOICE_OPEN
recoverOn: INVOICE_PAID_RECEIPT
cancelAfter: MANUAL_OR_POLICY_TERMINAL
每个 cycle 绑定 subscription、invoice ID、amount/currency、provider generation、attempt IDs 和 cycleId。订阅另存所有 policy-blocking open/delinquent cycle 的 outstandingCycleRoot;新发票不能覆盖旧债。支付、作废、不可收回、退款和争议是不同事实。Webhook 按 event ID 去重并回查 invoice/payment;到达顺序不决定真相。Webhook 订阅正确性提供验签边界。
资金状态与访问状态在一个接受点相遇
cycle 从 CURRENT -> PAYMENT_FAILED -> GRACE_ACTIVE 开始;重试走 RETRY_SCHEDULED -> RETRY_DISPATCHING -> RECOVERED/RETRY_FAILED/PAYMENT_UNKNOWN,失败回到 GRACE,未知查明后回同一 attempt,不能新建尝试绕过。无尝试且宽限到期才走 GRACE_EXPIRED -> RESTRICTING -> RESTRICTED -> CANCEL_PENDING -> CANCELED。单 cycle 的 paid receipt 可令它 RECOVERED,但不自动恢复 subscription 权益;事件冲突进 EVIDENCE_CONFLICT,人工宽限 EXCEPTION_ACTIVE 必须带 expiresAt。
所有状态边进入一个 transition 接受函数:CAS 匹配 cycle generation、invoice revision、attempt ID、policy、旧 outstandingCycleRoot 和可信时钟,更新 cycle 后重算订阅级 root 与 aggregateEntitlementDecision,原子写 transition receipt 和 outbox。只有阻断债务全部清除,或 policy 明确允许剩余债务下恢复,才签发更高 entitlement generation;否则该 cycle 虽 RECOVERED,订阅仍受限。Webhook 200、邮件或倒计时都不是接受,旧 projection 被 generation fence。
失败后的首次转移只记录平台已经报告的 payment attempt,不主动再扣。确需本地主动 retry 时,先持久化稳定 operation ID、invoice/amount digest 和 PREPARED,dispatch 前 CAS 到 DISPATCHING 并传 provider 幂等键。网络超时进入 PAYMENT_UNKNOWN,只能用 provider payment/invoice receipt 查询;RFC 9110 的方法幂等语义不能替支付平台特定幂等键和保留期。HTTP 幂等键正确性约束未知结果下不换键重发。
宽限 profile 要明确读、写、导出、API token、后台任务和超额资源,不是布尔 active;已有数据仍允许导出,安全和法定义务不因欠款关闭。GRACE_EXPIRED 后限制前必须再取 invoice 未支付的新鲜 receipt。付款与限制 CAS 竞争同一 cycle generation,但胜出的 cycle transition 仍须重算 outstanding root:其他阻断债务存在时只关闭该 cycle,不签发解除限制的 entitlement generation。AI 定价迁移中的旧价格 cohort 必须映射准确 invoice。
注入乱序、停摆和边界付款
测试乱序/重复 Webhook、retry 响应丢失、next attempt 改期、scheduler 停机、截止边界付款、mark-uncollectible、两张 open invoice 只付一张、outbox 重放、缓存落后和限制后 chargeback。预期单 cycle 恢复但另一阻断债务仍在时不恢复订阅,未知不自动取消,旧代更新被拒。
门禁要求 event 可归属率、outstanding root 重算一致率 100%,重复扣款、仍有阻断 cycle 时全量恢复、无新鲜 receipt 自动取消、旧 generation 提交均为 0;确认付款到合规恢复 p99 小于 60 秒,PAYMENT_UNKNOWN 一小时内有 owner 且无盲重试。上线先 shadow 两期,再从通知和 5% 低风险订阅放量。异常时暂停新限制并切回宽松旧 policy,但不伪造付款;恢复必须显式升代,资金事实永不删除。
Related