feat(pay-v2): P8 Task5 订阅取消(API+入站)+ past_due 状态

This commit is contained in:
wangjia
2026-07-10 18:18:55 +08:00
parent b8bdb2c193
commit 62abdc1d65
8 changed files with 669 additions and 13 deletions
+16
View File
@@ -214,6 +214,22 @@ func (p *Provider) VerifyCallback(_ context.Context, in provider.CallbackInput)
ev.PaidAt = &t
}
return ev, nil
case "customer.subscription.deleted":
var sub gostripe.Subscription
if err := json.Unmarshal(event.Data.Raw, &sub); err != nil {
return nil, fmt.Errorf("stripe: 解析 subscription 失败: %w", err)
}
return &provider.PaidEvent{Kind: provider.EventSubscriptionCanceled, SubscriptionRef: sub.ID, Raw: string(in.Raw)}, nil
case "invoice.payment_failed":
var inv gostripe.Invoice
if err := json.Unmarshal(event.Data.Raw, &inv); err != nil {
return nil, fmt.Errorf("stripe: 解析 invoice 失败: %w", err)
}
ev := &provider.PaidEvent{Kind: provider.EventSubscriptionPastDue, InvoiceRef: inv.ID, Raw: string(in.Raw)}
if inv.Subscription != nil {
ev.SubscriptionRef = inv.Subscription.ID
}
return ev, nil
default:
// 其它事件此阶段不处理:归一化 pending(管线 Settle 视为 ignored)。
return &provider.PaidEvent{Status: provider.PaidPending, Raw: string(in.Raw)}, nil
+50
View File
@@ -217,6 +217,56 @@ func TestVerifyInvoicePaidFirstPeriodSkipped(t *testing.T) {
}
}
// customer.subscription.deleted(Stripe 侧取消,主动取消/欠费催收耗尽后网关删除)→
// 归一化为 EventSubscriptionCanceled,携带渠道订阅号供 gateway 反查 Subscription。
func TestVerifyCustomerSubscriptionDeleted(t *testing.T) {
ts := fakeStripeAPI(t)
defer ts.Close()
p := newStripe(t, ts)
payload := `{"id":"evt_del","object":"event","type":"customer.subscription.deleted","data":{"object":{"id":"sub_new","object":"subscription","status":"canceled"}}}`
sig := signStripe(payload, whSecret, time.Now().Unix())
ev, err := p.VerifyCallback(context.Background(), provider.CallbackInput{
Raw: []byte(payload),
Headers: map[string]string{"Stripe-Signature": sig},
})
if err != nil {
t.Fatalf("verify: %v", err)
}
if ev.Kind != provider.EventSubscriptionCanceled {
t.Fatalf("kind = %v, want EventSubscriptionCanceled", ev.Kind)
}
if ev.SubscriptionRef != "sub_new" {
t.Fatalf("subscription_ref = %s, want sub_new", ev.SubscriptionRef)
}
}
// invoice.payment_failed(某期扣款失败)→ 归一化为 EventSubscriptionPastDue,携带失败
// 发票号(供 gateway 铸 outbox 幂等键)+ 渠道订阅号。
func TestVerifyInvoicePaymentFailed(t *testing.T) {
ts := fakeStripeAPI(t)
defer ts.Close()
p := newStripe(t, ts)
payload := `{"id":"evt_fail","object":"event","type":"invoice.payment_failed","data":{"object":{"id":"in_failed","object":"invoice","subscription":{"id":"sub_new","object":"subscription"}}}}`
sig := signStripe(payload, whSecret, time.Now().Unix())
ev, err := p.VerifyCallback(context.Background(), provider.CallbackInput{
Raw: []byte(payload),
Headers: map[string]string{"Stripe-Signature": sig},
})
if err != nil {
t.Fatalf("verify: %v", err)
}
if ev.Kind != provider.EventSubscriptionPastDue {
t.Fatalf("kind = %v, want EventSubscriptionPastDue", ev.Kind)
}
if ev.InvoiceRef != "in_failed" || ev.SubscriptionRef != "sub_new" {
t.Fatalf("event = %+v", ev)
}
}
// signStripe 复刻 Stripe webhook 签名头: t=<ts>,v1=hex(HMAC-SHA256(secret, "<ts>.<payload>"))
func signStripe(payload, secret string, ts int64) string {
mac := hmac.New(sha256.New, []byte(secret))