package customer import ( "testing" "github.com/damirmukimov/city_resource_graph/models/params" "github.com/stretchr/testify/assert" ) func TestCalculateCustomerMetrics(t *testing.T) { tests := []struct { name string year int totalOrgs int payingShare float64 expectedTotal int expectedPaying int expectedFree int expectedShare float64 }{ { name: "Year 1 - 500 orgs, 30% paying", year: 1, totalOrgs: 500, payingShare: 0.30, expectedTotal: 500, expectedPaying: 150, expectedFree: 350, expectedShare: 0.30, }, { name: "Year 2 - 2000 orgs, 30% paying", year: 2, totalOrgs: 2000, payingShare: 0.30, expectedTotal: 2000, expectedPaying: 600, expectedFree: 1400, expectedShare: 0.30, }, { name: "Year 3 - 5000 orgs, 30% paying", year: 3, totalOrgs: 5000, payingShare: 0.30, expectedTotal: 5000, expectedPaying: 1500, expectedFree: 3500, expectedShare: 0.30, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := ¶ms.Params{ Adoption: params.AdoptionParams{ TotalOrgs: params.YearlyInt{params.YearKey(tt.year): tt.totalOrgs}, PayingShare: params.YearlyFloat{params.YearKey(tt.year): tt.payingShare}, }, } result := CalculateCustomerMetrics(tt.year, p) assert.Equal(t, tt.expectedTotal, result.TotalOrgs, "Total orgs should match") assert.Equal(t, tt.expectedPaying, result.PayingOrgs, "Paying orgs should match") assert.Equal(t, tt.expectedFree, result.FreeOrgs, "Free orgs should match") assert.Equal(t, tt.expectedShare, result.PayingShare, "Paying share should match") }) } } func TestCalculateTierDistribution(t *testing.T) { tests := []struct { name string payingOrgs int tierMix params.TierMix expectedBasic int expectedBusiness int expectedEnterprise int expectedTotal int }{ { name: "Year 1 tier distribution - 150 paying orgs", payingOrgs: 150, tierMix: params.TierMix{Basic: 0.60, Business: 0.30, Enterprise: 0.10}, expectedBasic: 90, // 150 * 0.60 expectedBusiness: 45, // 150 * 0.30 expectedEnterprise: 15, // 150 * 0.10 expectedTotal: 150, }, { name: "Year 3 tier distribution - 1500 paying orgs", payingOrgs: 1500, tierMix: params.TierMix{Basic: 0.54, Business: 0.38, Enterprise: 0.08}, expectedBasic: 810, // 1500 * 0.54 expectedBusiness: 570, // 1500 * 0.38 expectedEnterprise: 120, // 1500 * 0.08 expectedTotal: 1500, }, { name: "Rounding adjustment needed", payingOrgs: 10, tierMix: params.TierMix{Basic: 0.60, Business: 0.30, Enterprise: 0.10}, expectedBasic: 6, // 10 * 0.60 = 6 expectedBusiness: 3, // 10 * 0.30 = 3 expectedEnterprise: 1, // 10 * 0.10 = 1 expectedTotal: 10, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := ¶ms.Params{ Pricing: params.PricingParams{ TierMix: params.YearlyTierMix{"1": tt.tierMix}, }, } result := CalculateTierDistribution(1, tt.payingOrgs, p) assert.Equal(t, tt.expectedBasic, result.Basic, "Basic tier should match") assert.Equal(t, tt.expectedBusiness, result.Business, "Business tier should match") assert.Equal(t, tt.expectedEnterprise, result.Enterprise, "Enterprise tier should match") assert.Equal(t, tt.expectedTotal, result.Total, "Total should match") // Verify rounding adjustment works total := result.Basic + result.Business + result.Enterprise assert.Equal(t, tt.payingOrgs, total, "Distribution should sum to total paying orgs") }) } } func TestDefaultChurnMetrics(t *testing.T) { churn := DefaultChurnMetrics() // Basic tier assert.Equal(t, 0.15, churn.Basic.AnnualChurn, "Basic churn rate should be 0.15") assert.Equal(t, 0.85, churn.Basic.Retention, "Basic retention should be 0.85") assert.Equal(t, 48, churn.Basic.AvgLifetimeMonths, "Basic lifetime should be 48 months") // Business tier assert.Equal(t, 0.10, churn.Business.AnnualChurn, "Business churn rate should be 0.10") assert.Equal(t, 0.90, churn.Business.Retention, "Business retention should be 0.90") assert.Equal(t, 64, churn.Business.AvgLifetimeMonths, "Business lifetime should be 64 months") // Enterprise tier assert.Equal(t, 0.05, churn.Enterprise.AnnualChurn, "Enterprise churn rate should be 0.05") assert.Equal(t, 0.95, churn.Enterprise.Retention, "Enterprise retention should be 0.95") assert.Equal(t, 80, churn.Enterprise.AvgLifetimeMonths, "Enterprise lifetime should be 80 months") } // Test customer metrics with edge cases for 100% coverage func TestCalculateCustomerMetrics_EdgeCases(t *testing.T) { tests := []struct { name string year int totalOrgs int payingShare float64 expectedTotal int expectedPaying int expectedFree int expectedShare float64 }{ { name: "Zero total orgs", year: 1, totalOrgs: 0, payingShare: 0.3, expectedTotal: 0, expectedPaying: 0, expectedFree: 0, expectedShare: 0.3, }, { name: "100% paying share", year: 1, totalOrgs: 100, payingShare: 1.0, expectedTotal: 100, expectedPaying: 100, expectedFree: 0, expectedShare: 1.0, }, { name: "0% paying share", year: 1, totalOrgs: 100, payingShare: 0.0, expectedTotal: 100, expectedPaying: 0, expectedFree: 100, expectedShare: 0.0, }, { name: "Non-integer paying orgs (rounding)", year: 1, totalOrgs: 10, payingShare: 0.33, // 10 * 0.33 = 3.3, should round to 3 expectedTotal: 10, expectedPaying: 3, expectedFree: 7, expectedShare: 0.33, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := ¶ms.Params{ Adoption: params.AdoptionParams{ TotalOrgs: params.YearlyInt{params.YearKey(tt.year): tt.totalOrgs}, PayingShare: params.YearlyFloat{params.YearKey(tt.year): tt.payingShare}, }, } result := CalculateCustomerMetrics(tt.year, p) assert.Equal(t, tt.expectedTotal, result.TotalOrgs, "Total orgs should match") assert.Equal(t, tt.expectedPaying, result.PayingOrgs, "Paying orgs should match") assert.Equal(t, tt.expectedFree, result.FreeOrgs, "Free orgs should match") assert.Equal(t, tt.expectedShare, result.PayingShare, "Paying share should match") }) } } // Test tier distribution with edge cases func TestCalculateTierDistribution_EdgeCases(t *testing.T) { tests := []struct { name string payingOrgs int tierMix params.TierMix expectedBasic int expectedBusiness int expectedEnterprise int expectedTotal int }{ { name: "Zero paying orgs", payingOrgs: 0, tierMix: params.TierMix{Basic: 0.6, Business: 0.3, Enterprise: 0.1}, expectedBasic: 0, expectedBusiness: 0, expectedEnterprise: 0, expectedTotal: 0, }, { name: "Single paying org - basic tier", payingOrgs: 1, tierMix: params.TierMix{Basic: 0.6, Business: 0.3, Enterprise: 0.1}, expectedBasic: 1, // Gets the rounding adjustment expectedBusiness: 0, expectedEnterprise: 0, expectedTotal: 1, }, { name: "Uneven distribution requiring rounding", payingOrgs: 7, tierMix: params.TierMix{Basic: 0.5, Business: 0.3, Enterprise: 0.2}, expectedBasic: 4, // 7 * 0.5 = 3.5, rounded to 4 with adjustment expectedBusiness: 2, // 7 * 0.3 = 2.1, floored to 2 expectedEnterprise: 1, // 7 * 0.2 = 1.4, floored to 1 expectedTotal: 7, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := ¶ms.Params{ Pricing: params.PricingParams{ TierMix: params.YearlyTierMix{"1": tt.tierMix}, }, } result := CalculateTierDistribution(1, tt.payingOrgs, p) assert.Equal(t, tt.expectedBasic, result.Basic, "Basic tier should match") assert.Equal(t, tt.expectedBusiness, result.Business, "Business tier should match") assert.Equal(t, tt.expectedEnterprise, result.Enterprise, "Enterprise tier should match") assert.Equal(t, tt.expectedTotal, result.Total, "Total should match") // Verify distribution sums correctly total := result.Basic + result.Business + result.Enterprise assert.Equal(t, tt.payingOrgs, total, "Distribution should sum to total paying orgs") }) } }