fix(v2): 订阅激活门改按结算结果(unpaid session 不再无付款激活)
HandleCallback 原门 serr==nil && SubscriptionRef!="" 挡不住 Settle() 对非 PaidSucceeded 事件返回 (SettleIgnored, nil) 的情况(如 Stripe checkout.session.completed 异步支付 payment_status=unpaid),导致 onSubscriptionActivated 无确认付款先激活订阅。改为按结算结果开门 (SettleProcessed/SettleDuplicate),并在 onSubscriptionActivated 内加 o.Status.Settled() 纵深防御。
This commit is contained in:
@@ -306,6 +306,49 @@ func TestSettleRenewal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSubscriptionNotActivatedOnUnpaidSession 复现 CRITICAL 发现:Stripe checkout.session.completed
|
||||
// 在异步支付方式下可能 payment_status=unpaid(映射为 PaidPending)先到达,此时 Settle 返回
|
||||
// SettleIgnored——订阅诞生钩子必须跟随结算结果关门,不能只看 serr==nil(nil error 但未结算)。
|
||||
// 断言:无 Subscription 行诞生、无 subscription.created 入队、订单仍 pending。
|
||||
func TestSubscriptionNotActivatedOnUnpaidSession(t *testing.T) {
|
||||
g, fp, spy, orders, subs := newSubGateway(t)
|
||||
ctx := context.Background()
|
||||
res, err := g.CreateSubscription(ctx, gateway.CreateSubscriptionInput{
|
||||
SKU: "pro_monthly", Method: "substripe", BizSystem: "pangolin", BizRef: "u-1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSubscription: %v", err)
|
||||
}
|
||||
|
||||
// checkout.session.completed(异步支付,payment_status=unpaid)归一化为 PaidPending。
|
||||
raw, err := json.Marshal(provider.PaidEvent{
|
||||
Kind: provider.EventPayment, ProviderRef: fp.sessionRef, Status: provider.PaidPending,
|
||||
PaidAmountMinor: 2999, PaidCurrency: "USD", SubscriptionRef: "sub_new",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal event: %v", err)
|
||||
}
|
||||
|
||||
result, err := g.HandleCallback(ctx, "substripe", provider.CallbackInput{Raw: raw})
|
||||
if err != nil {
|
||||
t.Fatalf("HandleCallback: %v", err)
|
||||
}
|
||||
if result != gateway.SettleIgnored {
|
||||
t.Fatalf("result = %v, want ignored", result)
|
||||
}
|
||||
|
||||
o, err := orders.GetOrder(res.OrderNo)
|
||||
if err != nil || o.Status != model.OrderPendingV2 {
|
||||
t.Fatalf("order after unpaid callback = %+v, %v, want still pending", o, err)
|
||||
}
|
||||
if _, err := subs.GetByProviderRef("substripe", "sub_new"); !errors.Is(err, store.ErrSubNotFound) {
|
||||
t.Fatalf("subscription should not exist for unpaid session, err = %v", err)
|
||||
}
|
||||
if len(spy.calls) != 0 {
|
||||
t.Fatalf("webhook calls = %d, want 0(no premature subscription.created): %+v", len(spy.calls), spy.calls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSettleRenewalUnknownSubscription 未知 provider_sub_ref(订阅未诞生/已清理)→ 忽略,不报错。
|
||||
func TestSettleRenewalUnknownSubscription(t *testing.T) {
|
||||
g, _, spy, _, _ := newSubGateway(t)
|
||||
|
||||
Reference in New Issue
Block a user