package store_test import ( "testing" "time" "github.com/wangjia/pay/internal/model" "github.com/wangjia/pay/internal/store" ) func newSubStore(t *testing.T) *store.SubscriptionStore { return store.NewSubscriptionStore(model.OpenTestDB(t)) } func TestSubscriptionCreateIdempotent(t *testing.T) { s := newSubStore(t) sub := &model.Subscription{SubID: "SUB-1", Channel: "stripe", ProviderSubRef: "sub_x", OutTradeNo: "PAY-1", BizSystem: "pangolin", BizRef: "u-1", BizCode: "pro_month", AmountMinor: 2999, Currency: "USD", Status: model.SubActive} created, err := s.Create(sub) if err != nil || !created { t.Fatalf("first create: created=%v err=%v", created, err) } // 同 provider_sub_ref 再建 → 幂等 no-op(Stripe 重投 checkout.completed) again, err := s.Create(&model.Subscription{SubID: "SUB-2", Channel: "stripe", ProviderSubRef: "sub_x", OutTradeNo: "PAY-1", AmountMinor: 2999, Currency: "USD", Status: model.SubActive}) if err != nil || again { t.Fatalf("dup create: again=%v err=%v", again, err) } } func TestSubscriptionStateMachine(t *testing.T) { s := newSubStore(t) end := time.Now().Add(30 * 24 * time.Hour) if _, err := s.Create(&model.Subscription{SubID: "SUB-9", Channel: "stripe", ProviderSubRef: "sub_9", OutTradeNo: "PAY-9", AmountMinor: 2999, Currency: "USD", Status: model.SubActive}); err != nil { t.Fatal(err) } // active → past_due ok, _ := s.MarkPastDue("stripe", "sub_9") if !ok { t.Fatal("MarkPastDue should flip active→past_due") } // past_due → active(续费恢复) ok, _ = s.Activate("SUB-9", &end) if !ok { t.Fatal("Activate should recover past_due→active") } // → canceled(终态) ok, _ = s.MarkCanceled("SUB-9") if !ok { t.Fatal("MarkCanceled should flip →canceled") } // canceled 后不可复活 if ok, _ := s.Activate("SUB-9", &end); ok { t.Fatal("canceled sub must not be re-activated") } if ok, _ := s.MarkPastDue("stripe", "sub_9"); ok { t.Fatal("canceled sub must not go past_due") } got, err := s.GetBySubID("SUB-9") if err != nil || got.Status != model.SubCanceled { t.Fatalf("final status = %v err=%v", got.Status, err) } }