turash/models/customer/customer_test.go
Damir Mukimov 4a2fda96cd
Initial commit: Repository setup with .gitignore, golangci-lint v2.6.0, and code quality checks
- Initialize git repository
- Add comprehensive .gitignore for Go projects
- Install golangci-lint v2.6.0 (latest v2) globally
- Configure .golangci.yml with appropriate linters and formatters
- Fix all formatting issues (gofmt)
- Fix all errcheck issues (unchecked errors)
- Adjust complexity threshold for validation functions
- All checks passing: build, test, vet, lint
2025-11-01 07:36:22 +01:00

286 lines
8.6 KiB
Go

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 := &params.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 := &params.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 := &params.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 := &params.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")
})
}
}