- Add subscription module (models, service, tests) - Plans: Free, Creator ($9.99/mo), Premium ($19.99/mo) - Features: subscribe, cancel, reactivate, change billing cycle - 14-day trial for Premium plan - Upgrade immediate, downgrade at period end - Invoice tracking and history - Handler tests for auth and validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
226 lines
5.6 KiB
Go
226 lines
5.6 KiB
Go
package subscription
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func TestPlanTableName(t *testing.T) {
|
|
p := Plan{}
|
|
if p.TableName() != "subscription_plans" {
|
|
t.Errorf("expected subscription_plans, got %s", p.TableName())
|
|
}
|
|
}
|
|
|
|
func TestUserSubscriptionTableName(t *testing.T) {
|
|
s := UserSubscription{}
|
|
if s.TableName() != "user_subscriptions" {
|
|
t.Errorf("expected user_subscriptions, got %s", s.TableName())
|
|
}
|
|
}
|
|
|
|
func TestInvoiceTableName(t *testing.T) {
|
|
i := Invoice{}
|
|
if i.TableName() != "subscription_invoices" {
|
|
t.Errorf("expected subscription_invoices, got %s", i.TableName())
|
|
}
|
|
}
|
|
|
|
func TestPlanConstants(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
plan PlanName
|
|
expected string
|
|
}{
|
|
{"free plan", PlanFree, "free"},
|
|
{"creator plan", PlanCreator, "creator"},
|
|
{"premium plan", PlanPremium, "premium"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if string(tt.plan) != tt.expected {
|
|
t.Errorf("expected %s, got %s", tt.expected, string(tt.plan))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSubscriptionStatusConstants(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
status SubscriptionStatus
|
|
expected string
|
|
}{
|
|
{"active", StatusActive, "active"},
|
|
{"trialing", StatusTrialing, "trialing"},
|
|
{"canceled", StatusCanceled, "canceled"},
|
|
{"past_due", StatusPastDue, "past_due"},
|
|
{"expired", StatusExpired, "expired"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if string(tt.status) != tt.expected {
|
|
t.Errorf("expected %s, got %s", tt.expected, string(tt.status))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBillingCycleConstants(t *testing.T) {
|
|
if string(BillingMonthly) != "monthly" {
|
|
t.Errorf("expected monthly, got %s", string(BillingMonthly))
|
|
}
|
|
if string(BillingYearly) != "yearly" {
|
|
t.Errorf("expected yearly, got %s", string(BillingYearly))
|
|
}
|
|
}
|
|
|
|
func TestInvoiceStatusConstants(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
status InvoiceStatus
|
|
expected string
|
|
}{
|
|
{"pending", InvoicePending, "pending"},
|
|
{"paid", InvoicePaid, "paid"},
|
|
{"failed", InvoiceFailed, "failed"},
|
|
{"refunded", InvoiceRefunded, "refunded"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if string(tt.status) != tt.expected {
|
|
t.Errorf("expected %s, got %s", tt.expected, string(tt.status))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserSubscription_IsTrialing(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
sub UserSubscription
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "trialing with future trial end",
|
|
sub: UserSubscription{
|
|
Status: StatusTrialing,
|
|
TrialEnd: timePtr(time.Now().Add(24 * time.Hour)),
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "trialing with past trial end",
|
|
sub: UserSubscription{
|
|
Status: StatusTrialing,
|
|
TrialEnd: timePtr(time.Now().Add(-24 * time.Hour)),
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "active status",
|
|
sub: UserSubscription{
|
|
Status: StatusActive,
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "trialing without trial end",
|
|
sub: UserSubscription{
|
|
Status: StatusTrialing,
|
|
},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := tt.sub.IsTrialing(); got != tt.expected {
|
|
t.Errorf("IsTrialing() = %v, want %v", got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserSubscription_IsActiveOrTrialing(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
status SubscriptionStatus
|
|
expected bool
|
|
}{
|
|
{"active", StatusActive, true},
|
|
{"trialing", StatusTrialing, true},
|
|
{"canceled", StatusCanceled, false},
|
|
{"expired", StatusExpired, false},
|
|
{"past_due", StatusPastDue, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
sub := UserSubscription{Status: tt.status}
|
|
if got := sub.IsActiveOrTrialing(); got != tt.expected {
|
|
t.Errorf("IsActiveOrTrialing() = %v, want %v", got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPlanFields(t *testing.T) {
|
|
plan := Plan{
|
|
ID: uuid.New(),
|
|
Name: PlanPremium,
|
|
DisplayName: "Premium",
|
|
PriceMonthly: 1999,
|
|
PriceYearly: 19999,
|
|
Currency: "USD",
|
|
StorageLimitBytes: 214748364800,
|
|
MarketplaceCommission: 0.10,
|
|
CanSellOnMarketplace: true,
|
|
HasPrioritySupport: true,
|
|
HasCollaborationTools: true,
|
|
HasDistribution: true,
|
|
TrialDays: 14,
|
|
IsActive: true,
|
|
SortOrder: 2,
|
|
}
|
|
|
|
if plan.PriceMonthly != 1999 {
|
|
t.Errorf("expected price_monthly_cents 1999, got %d", plan.PriceMonthly)
|
|
}
|
|
if plan.PriceYearly != 19999 {
|
|
t.Errorf("expected price_yearly_cents 19999, got %d", plan.PriceYearly)
|
|
}
|
|
if plan.TrialDays != 14 {
|
|
t.Errorf("expected trial_days 14, got %d", plan.TrialDays)
|
|
}
|
|
if plan.MarketplaceCommission != 0.10 {
|
|
t.Errorf("expected commission 0.10, got %f", plan.MarketplaceCommission)
|
|
}
|
|
}
|
|
|
|
func TestServiceErrors(t *testing.T) {
|
|
if ErrPlanNotFound.Error() != "subscription plan not found" {
|
|
t.Errorf("unexpected error message: %s", ErrPlanNotFound.Error())
|
|
}
|
|
if ErrAlreadySubscribed.Error() != "user already has an active subscription to this plan" {
|
|
t.Errorf("unexpected error message: %s", ErrAlreadySubscribed.Error())
|
|
}
|
|
if ErrNoActiveSubscription.Error() != "no active subscription found" {
|
|
t.Errorf("unexpected error message: %s", ErrNoActiveSubscription.Error())
|
|
}
|
|
if ErrInvalidBillingCycle.Error() != "invalid billing cycle: must be 'monthly' or 'yearly'" {
|
|
t.Errorf("unexpected error message: %s", ErrInvalidBillingCycle.Error())
|
|
}
|
|
if ErrFreePlanNoBilling.Error() != "free plan does not require billing" {
|
|
t.Errorf("unexpected error message: %s", ErrFreePlanNoBilling.Error())
|
|
}
|
|
}
|
|
|
|
func timePtr(t time.Time) *time.Time {
|
|
return &t
|
|
}
|