fix(v2): 取消失配收敛——渠道已取消(哨兵)→本地补齐+事件,不再500

CancelSubscription 本地 active/past_due 但渠道已先行取消(dashboard 手工 /
竞态未消费的 deleted webhook)时,stripe adapter 原样透传渠道拒绝,handler 映
成 500 cancel_failed——渠道取消这一事实明明已成立。新增 provider.ErrSubAlready
Canceled 哨兵,stripe adapter 识别 resource_missing / "already been canceled"
两种真实 Stripe 错误形态并 wrap;gateway.CancelSubscription 命中哨兵后走与入
站 webhook 相同的 finalizeCanceled 本地收敛,两路对同一终态天然幂等。

同 re-review 顺手核掉同型缺口:onSubscriptionActivated 的
`!created→return nil` 在入队 subscription.created 之前短路,首次入队失败后
Stripe 重投会因 created=false 永久跳过入队——通知永久丢失。改为无论 created
与否都无条件入队,outbox 唯一键 ON CONFLICT DO NOTHING 天然幂等自愈,与
finalizeCanceled/markSubscriptionPastDue/settleRenewal 同一写法。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
wangjia
2026-07-10 18:42:20 +08:00
parent ff65a06bf2
commit 02b2fcfa41
6 changed files with 270 additions and 6 deletions
+7
View File
@@ -122,6 +122,13 @@ type QueryRequest struct {
var (
ErrUnknownMethod = errors.New("provider: unknown method")
ErrNotSupported = errors.New("provider: capability not supported")
// ErrSubAlreadyCanceled — SubscriptionProvider.CancelSubscription 的哨兵:渠道侧订阅
// 已处于取消终态(dashboard 手工取消 / 竞态下未消费的 deleted webhook 抢先落地),本地
// 发起的主动取消打到渠道时渠道拒绝(如 Stripe "already been canceled" / resource_missing)。
// 调用方(gateway.CancelSubscription)须将其视为"取消事实已成立",走本地收敛而非报错——
// 具体渠道 adapter 负责把渠道原生错误 wrap 成本哨兵(参见 stripe.isAlreadyCanceledErr)。
ErrSubAlreadyCanceled = errors.New("provider: subscription already canceled at channel")
)
// Provider — 每个支付渠道实现的统一接口(设计 §4.1 PaymentProvider)。