package subscription import ( "context" "errors" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/require" "go.uber.org/zap" "gorm.io/driver/sqlite" "gorm.io/gorm" ) // TestGetUserSubscription_PaymentGate exercises the v1.0.6.2 hotfix: a row in // active/trialing state that lacks an effective payment linkage must not be // returned as a valid subscription. A single test covers the full branch // matrix of hasEffectivePayment so a regression in any clause is caught. func TestGetUserSubscription_PaymentGate(t *testing.T) { ctx := context.Background() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&Plan{}, &UserSubscription{}, &Invoice{})) svc := NewService(db, zap.NewNop()) freePlan := Plan{ID: uuid.New(), Name: PlanFree, DisplayName: "Free", PriceMonthly: 0, IsActive: true} paidPlan := Plan{ID: uuid.New(), Name: PlanCreator, DisplayName: "Creator", PriceMonthly: 999, IsActive: true} require.NoError(t, db.Create(&freePlan).Error) require.NoError(t, db.Create(&paidPlan).Error) now := time.Now() future := now.Add(24 * time.Hour) past := now.Add(-24 * time.Hour) tests := []struct { name string prepare func(t *testing.T, userID uuid.UUID) // sets up the row(s) for this user expectErr error }{ { name: "no subscription row returns ErrNoActiveSubscription", prepare: func(t *testing.T, userID uuid.UUID) { // no-op }, expectErr: ErrNoActiveSubscription, }, { name: "free plan active always passes", prepare: func(t *testing.T, userID uuid.UUID) { sub := UserSubscription{ UserID: userID, PlanID: freePlan.ID, Status: StatusActive, BillingCycle: BillingMonthly, CurrentPeriodStart: now, CurrentPeriodEnd: future, } require.NoError(t, db.Create(&sub).Error) }, expectErr: nil, }, { name: "paid plan active with PSP payment intent on invoice passes", prepare: func(t *testing.T, userID uuid.UUID) { sub := UserSubscription{ UserID: userID, PlanID: paidPlan.ID, Status: StatusActive, BillingCycle: BillingMonthly, CurrentPeriodStart: now, CurrentPeriodEnd: future, } require.NoError(t, db.Create(&sub).Error) inv := Invoice{ SubscriptionID: sub.ID, UserID: userID, AmountCents: 999, Currency: "USD", Status: InvoicePending, BillingPeriodStart: now, BillingPeriodEnd: future, HyperswitchPaymentID: "pay_12345", } require.NoError(t, db.Create(&inv).Error) }, expectErr: nil, }, { name: "paid plan active with invoice but empty hs_payment_id returns ErrSubscriptionNoPayment", prepare: func(t *testing.T, userID uuid.UUID) { sub := UserSubscription{ UserID: userID, PlanID: paidPlan.ID, Status: StatusActive, BillingCycle: BillingMonthly, CurrentPeriodStart: now, CurrentPeriodEnd: future, } require.NoError(t, db.Create(&sub).Error) inv := Invoice{ SubscriptionID: sub.ID, UserID: userID, AmountCents: 999, Currency: "USD", Status: InvoicePending, BillingPeriodStart: now, BillingPeriodEnd: future, HyperswitchPaymentID: "", } require.NoError(t, db.Create(&inv).Error) }, expectErr: ErrSubscriptionNoPayment, }, { name: "paid plan active with no invoice at all returns ErrSubscriptionNoPayment", prepare: func(t *testing.T, userID uuid.UUID) { sub := UserSubscription{ UserID: userID, PlanID: paidPlan.ID, Status: StatusActive, BillingCycle: BillingMonthly, CurrentPeriodStart: now, CurrentPeriodEnd: future, } require.NoError(t, db.Create(&sub).Error) }, expectErr: ErrSubscriptionNoPayment, }, { name: "paid plan trialing with future trial_end passes", prepare: func(t *testing.T, userID uuid.UUID) { sub := UserSubscription{ UserID: userID, PlanID: paidPlan.ID, Status: StatusTrialing, BillingCycle: BillingMonthly, CurrentPeriodStart: now, CurrentPeriodEnd: future, TrialStart: &now, TrialEnd: &future, } require.NoError(t, db.Create(&sub).Error) }, expectErr: nil, }, { name: "paid plan trialing with past trial_end and no payment returns ErrSubscriptionNoPayment", prepare: func(t *testing.T, userID uuid.UUID) { sub := UserSubscription{ UserID: userID, PlanID: paidPlan.ID, Status: StatusTrialing, BillingCycle: BillingMonthly, CurrentPeriodStart: now, CurrentPeriodEnd: future, TrialStart: &past, TrialEnd: &past, } require.NoError(t, db.Create(&sub).Error) }, expectErr: ErrSubscriptionNoPayment, }, { name: "paid plan trialing with nil trial_end and no payment returns ErrSubscriptionNoPayment", prepare: func(t *testing.T, userID uuid.UUID) { sub := UserSubscription{ UserID: userID, PlanID: paidPlan.ID, Status: StatusTrialing, BillingCycle: BillingMonthly, CurrentPeriodStart: now, CurrentPeriodEnd: future, } require.NoError(t, db.Create(&sub).Error) }, expectErr: ErrSubscriptionNoPayment, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { userID := uuid.New() tc.prepare(t, userID) sub, err := svc.GetUserSubscription(ctx, userID) if tc.expectErr == nil { require.NoError(t, err) require.NotNil(t, sub) require.Equal(t, userID, sub.UserID) return } require.Error(t, err) require.True(t, errors.Is(err, tc.expectErr), "expected error %v, got %v", tc.expectErr, err) }) } }