package services import ( "encoding/json" "testing" "github.com/breakpilot/billing-service/internal/models" ) func TestSubscriptionStatus_Transitions(t *testing.T) { // Test valid subscription status values validStatuses := []models.SubscriptionStatus{ models.StatusTrialing, models.StatusActive, models.StatusPastDue, models.StatusCanceled, models.StatusExpired, } for _, status := range validStatuses { if status == "" { t.Errorf("Status should not be empty") } } } func TestPlanID_ValidValues(t *testing.T) { validPlanIDs := []models.PlanID{ models.PlanBasic, models.PlanStandard, models.PlanPremium, } expected := []string{"basic", "standard", "premium"} for i, planID := range validPlanIDs { if string(planID) != expected[i] { t.Errorf("PlanID should be '%s', got '%s'", expected[i], planID) } } } func TestPlanFeatures_JSONSerialization(t *testing.T) { features := models.PlanFeatures{ MonthlyTaskAllowance: 100, MaxTaskBalance: 500, FeatureFlags: []string{"basic_ai", "templates"}, MaxTeamMembers: 3, PrioritySupport: false, CustomBranding: false, BatchProcessing: true, CustomTemplates: true, FairUseMode: false, } // Test JSON serialization data, err := json.Marshal(features) if err != nil { t.Fatalf("Failed to marshal PlanFeatures: %v", err) } // Test JSON deserialization var decoded models.PlanFeatures err = json.Unmarshal(data, &decoded) if err != nil { t.Fatalf("Failed to unmarshal PlanFeatures: %v", err) } // Verify fields if decoded.MonthlyTaskAllowance != features.MonthlyTaskAllowance { t.Errorf("MonthlyTaskAllowance mismatch: got %d, expected %d", decoded.MonthlyTaskAllowance, features.MonthlyTaskAllowance) } if decoded.MaxTaskBalance != features.MaxTaskBalance { t.Errorf("MaxTaskBalance mismatch: got %d, expected %d", decoded.MaxTaskBalance, features.MaxTaskBalance) } if decoded.BatchProcessing != features.BatchProcessing { t.Errorf("BatchProcessing mismatch: got %v, expected %v", decoded.BatchProcessing, features.BatchProcessing) } } func TestBillingPlan_DefaultPlansAreValid(t *testing.T) { plans := models.GetDefaultPlans() if len(plans) != 3 { t.Fatalf("Expected 3 default plans, got %d", len(plans)) } // Verify all plans have required fields for _, plan := range plans { if plan.ID == "" { t.Errorf("Plan ID should not be empty") } if plan.Name == "" { t.Errorf("Plan '%s' should have a name", plan.ID) } if plan.Description == "" { t.Errorf("Plan '%s' should have a description", plan.ID) } if plan.PriceCents <= 0 { t.Errorf("Plan '%s' should have a positive price, got %d", plan.ID, plan.PriceCents) } if plan.Currency != "eur" { t.Errorf("Plan '%s' currency should be 'eur', got '%s'", plan.ID, plan.Currency) } if plan.Interval != "month" { t.Errorf("Plan '%s' interval should be 'month', got '%s'", plan.ID, plan.Interval) } if !plan.IsActive { t.Errorf("Plan '%s' should be active", plan.ID) } if plan.SortOrder <= 0 { t.Errorf("Plan '%s' should have a positive sort order, got %d", plan.ID, plan.SortOrder) } } } func TestBillingPlan_TaskAllowanceProgression(t *testing.T) { plans := models.GetDefaultPlans() // Basic should have lowest allowance basic := plans[0] standard := plans[1] premium := plans[2] if basic.Features.MonthlyTaskAllowance >= standard.Features.MonthlyTaskAllowance { t.Error("Standard plan should have more tasks than Basic") } if standard.Features.MonthlyTaskAllowance >= premium.Features.MonthlyTaskAllowance { t.Error("Premium plan should have more tasks than Standard") } } func TestBillingPlan_PriceProgression(t *testing.T) { plans := models.GetDefaultPlans() // Prices should increase with each tier if plans[0].PriceCents >= plans[1].PriceCents { t.Error("Standard should cost more than Basic") } if plans[1].PriceCents >= plans[2].PriceCents { t.Error("Premium should cost more than Standard") } } func TestBillingPlan_FairUseModeOnlyForPremium(t *testing.T) { plans := models.GetDefaultPlans() for _, plan := range plans { if plan.ID == models.PlanPremium { if !plan.Features.FairUseMode { t.Error("Premium plan should have FairUseMode enabled") } } else { if plan.Features.FairUseMode { t.Errorf("Plan '%s' should not have FairUseMode enabled", plan.ID) } } } } func TestBillingPlan_MaxTaskBalanceCalculation(t *testing.T) { plans := models.GetDefaultPlans() for _, plan := range plans { expected := plan.Features.MonthlyTaskAllowance * models.CarryoverMonthsCap if plan.Features.MaxTaskBalance != expected { t.Errorf("Plan '%s' MaxTaskBalance should be %d (allowance * 5), got %d", plan.ID, expected, plan.Features.MaxTaskBalance) } } } func TestAuditLogJSON_Marshaling(t *testing.T) { // Test that audit log values can be properly serialized oldValue := map[string]interface{}{ "plan_id": "basic", "status": "active", } newValue := map[string]interface{}{ "plan_id": "standard", "status": "active", } metadata := map[string]interface{}{ "reason": "upgrade", } // Marshal all values oldJSON, err := json.Marshal(oldValue) if err != nil { t.Fatalf("Failed to marshal oldValue: %v", err) } newJSON, err := json.Marshal(newValue) if err != nil { t.Fatalf("Failed to marshal newValue: %v", err) } metaJSON, err := json.Marshal(metadata) if err != nil { t.Fatalf("Failed to marshal metadata: %v", err) } // Verify non-empty if len(oldJSON) == 0 || len(newJSON) == 0 || len(metaJSON) == 0 { t.Error("JSON outputs should not be empty") } } func TestSubscriptionTrialCalculation(t *testing.T) { // Test trial days calculation logic trialDays := 7 if trialDays <= 0 { t.Error("Trial days should be positive") } if trialDays > 30 { t.Error("Trial days should not exceed 30") } } func TestSubscriptionInfo_TrialingStatus(t *testing.T) { info := models.SubscriptionInfo{ PlanID: models.PlanBasic, PlanName: "Basic", Status: models.StatusTrialing, IsTrialing: true, TrialDaysLeft: 5, CancelAtPeriodEnd: false, PriceCents: 990, Currency: "eur", } if !info.IsTrialing { t.Error("Should be trialing") } if info.Status != models.StatusTrialing { t.Errorf("Status should be 'trialing', got '%s'", info.Status) } if info.TrialDaysLeft <= 0 { t.Error("TrialDaysLeft should be positive during trial") } } func TestSubscriptionInfo_ActiveStatus(t *testing.T) { info := models.SubscriptionInfo{ PlanID: models.PlanStandard, PlanName: "Standard", Status: models.StatusActive, IsTrialing: false, TrialDaysLeft: 0, CancelAtPeriodEnd: false, PriceCents: 1990, Currency: "eur", } if info.IsTrialing { t.Error("Should not be trialing") } if info.Status != models.StatusActive { t.Errorf("Status should be 'active', got '%s'", info.Status) } } func TestSubscriptionInfo_CanceledStatus(t *testing.T) { info := models.SubscriptionInfo{ PlanID: models.PlanStandard, PlanName: "Standard", Status: models.StatusActive, IsTrialing: false, CancelAtPeriodEnd: true, // Scheduled for cancellation PriceCents: 1990, Currency: "eur", } if !info.CancelAtPeriodEnd { t.Error("CancelAtPeriodEnd should be true") } // Status remains active until period end if info.Status != models.StatusActive { t.Errorf("Status should still be 'active', got '%s'", info.Status) } } func TestWebhookEventTypes(t *testing.T) { // Test common Stripe webhook event types we handle eventTypes := []string{ "checkout.session.completed", "customer.subscription.created", "customer.subscription.updated", "customer.subscription.deleted", "invoice.paid", "invoice.payment_failed", } for _, eventType := range eventTypes { if eventType == "" { t.Error("Event type should not be empty") } } } func TestIdempotencyKey_Format(t *testing.T) { // Test that we can handle Stripe event IDs sampleEventIDs := []string{ "evt_1234567890abcdef", "evt_test_abc123xyz789", "evt_live_real_event_id", } for _, eventID := range sampleEventIDs { if len(eventID) < 10 { t.Errorf("Event ID '%s' seems too short", eventID) } // Stripe event IDs typically start with "evt_" if eventID[:4] != "evt_" { t.Errorf("Event ID '%s' should start with 'evt_'", eventID) } } }