package revenue import ( "testing" "github.com/damirmukimov/city_resource_graph/models/customer" "github.com/damirmukimov/city_resource_graph/models/params" "github.com/stretchr/testify/assert" ) func TestCalculateRevenue(t *testing.T) { tests := []struct { name string year int custMetrics customer.CustomerMetrics tierDist customer.TierDistribution expectedTotal float64 expectedSub float64 expectedTrans float64 expectedMun float64 expectedImpl float64 }{ { name: "Year 1 revenue calculation", year: 1, custMetrics: customer.CustomerMetrics{ TotalOrgs: 500, PayingOrgs: 150, }, tierDist: customer.TierDistribution{ Basic: 90, Business: 45, Enterprise: 15, Total: 150, }, expectedSub: 216360.0, // Using actual tier mix from YAML: 0.60/0.30/0.10 expectedTrans: 91500.0, // 200*550*0.35 + 300000*0.15 + 200000*0.04 expectedMun: 60000.0, // 1*60000 + 0 expectedImpl: 312500.0, // 500*0.5*0.25*5000 expectedTotal: 680360.0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := createTestParams(tt.year) result := CalculateRevenue(tt.year, tt.custMetrics, tt.tierDist, p) assert.InDelta(t, tt.expectedSub, result.Subscription.TotalARR, 0.01, "Subscription revenue should match") assert.InDelta(t, tt.expectedTrans, result.Transaction.Total, 0.01, "Transaction revenue should match") assert.InDelta(t, tt.expectedMun, result.Municipal.Total, 0.01, "Municipal revenue should match") assert.InDelta(t, tt.expectedImpl, result.Implementation.Total, 0.01, "Implementation revenue should match") assert.InDelta(t, tt.expectedTotal, result.Total, 0.01, "Total revenue should match") }) } } func TestCalculateSubscriptionRevenue(t *testing.T) { tests := []struct { name string year int tierDist customer.TierDistribution expectedBasic float64 expectedBusiness float64 expectedEnterprise float64 expectedTotal float64 }{ { name: "Year 1 subscription revenue", year: 1, tierDist: customer.TierDistribution{ Basic: 90, Business: 45, Enterprise: 15, }, expectedBasic: 45360.0, // 90 * 35 * 12 * 1.20 expectedBusiness: 81000.0, // 45 * 120 * 12 * 1.25 expectedEnterprise: 90000.0, // 15 * 400 * 12 * 1.25 expectedTotal: 216360.0, }, { name: "Year 3 subscription revenue", year: 3, tierDist: customer.TierDistribution{ Basic: 810, Business: 570, Enterprise: 120, }, expectedBasic: 408240.0, // 810 * 35 * 12 * 1.20 = 408240 expectedBusiness: 1026000.0, // 570 * 120 * 12 * 1.25 = 1026000 expectedEnterprise: 720000.0, // 120 * 400 * 12 * 1.25 = 720000 expectedTotal: 2154240.0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := createTestParams(tt.year) result := calculateSubscriptionRevenue(tt.year, tt.tierDist, p) assert.InDelta(t, tt.expectedBasic, result.BasicARR, 0.01, "Basic ARR should match") assert.InDelta(t, tt.expectedBusiness, result.BusinessARR, 0.01, "Business ARR should match") assert.InDelta(t, tt.expectedEnterprise, result.EnterpriseARR, 0.01, "Enterprise ARR should match") assert.InDelta(t, tt.expectedTotal, result.TotalARR, 0.01, "Total ARR should match") }) } } func TestCalculateTransactionRevenue(t *testing.T) { tests := []struct { name string year int expectedIntro float64 expectedService float64 expectedGroup float64 expectedTotal float64 }{ { name: "Year 1 transaction revenue", year: 1, expectedIntro: 38500.0, // 200 * 550 * 0.35 expectedService: 45000.0, // 300000 * 0.15 expectedGroup: 8000.0, // 200000 * 0.04 expectedTotal: 91500.0, }, { name: "Year 2 transaction revenue", year: 2, expectedIntro: 83600.0, // 400 * 550 * 0.38 expectedService: 120000.0, // 800000 * 0.15 expectedGroup: 16000.0, // 400000 * 0.04 expectedTotal: 219600.0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := createTestParams(tt.year) result := calculateTransactionRevenue(tt.year, p) assert.InDelta(t, tt.expectedIntro, result.IntroRevenue, 0.01, "Intro revenue should match") assert.InDelta(t, tt.expectedService, result.ServiceRevenue, 0.01, "Service revenue should match") assert.InDelta(t, tt.expectedGroup, result.GroupRevenue, 0.01, "Group revenue should match") assert.InDelta(t, tt.expectedTotal, result.Total, 0.01, "Total transaction revenue should match") }) } } func TestCalculateMunicipalRevenue(t *testing.T) { tests := []struct { name string year int expectedLicense float64 expectedData float64 expectedTotal float64 }{ { name: "Year 1 municipal revenue", year: 1, expectedLicense: 60000.0, // 1 * 60000 expectedData: 0.0, // 0 expectedTotal: 60000.0, }, { name: "Year 2 municipal revenue", year: 2, expectedLicense: 180000.0, // 2 * 90000 expectedData: 50000.0, // 50000 expectedTotal: 230000.0, }, { name: "Year 3 municipal revenue", year: 3, expectedLicense: 360000.0, // 4 * 90000 (YAML has 90000 for year 3) expectedData: 100000.0, // 100000 (YAML has 100000 for year 3) expectedTotal: 460000.0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := createTestParams(tt.year) result := calculateMunicipalRevenue(tt.year, p) assert.InDelta(t, tt.expectedLicense, result.LicenseRevenue, 0.01, "License revenue should match") assert.InDelta(t, tt.expectedData, result.DataLicensing, 0.01, "Data licensing revenue should match") assert.InDelta(t, tt.expectedTotal, result.Total, 0.01, "Total municipal revenue should match") }) } } func TestCalculateImplementationRevenue(t *testing.T) { tests := []struct { name string custMetrics customer.CustomerMetrics expectedMatches float64 expectedPaidImpl float64 expectedRevenue float64 }{ { name: "Implementation revenue calculation", custMetrics: customer.CustomerMetrics{ TotalOrgs: 500, }, expectedMatches: 250.0, // 500 * 0.5 expectedPaidImpl: 62.5, // 250 * 0.25 expectedRevenue: 312500.0, // 62.5 * 5000 }, { name: "Different org count", custMetrics: customer.CustomerMetrics{ TotalOrgs: 1000, }, expectedMatches: 500.0, // 1000 * 0.5 expectedPaidImpl: 125.0, // 500 * 0.25 expectedRevenue: 625000.0, // 125 * 5000 }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := createTestParams(1) result := calculateImplementationRevenue(tt.custMetrics, p) assert.InDelta(t, tt.expectedMatches, result.Matches, 0.01, "Matches should match") assert.InDelta(t, tt.expectedPaidImpl, result.PaidImpls, 0.01, "Paid implementations should match") assert.InDelta(t, tt.expectedRevenue, result.Total, 0.01, "Implementation revenue should match") }) } } func TestGetPlatformOnlyRevenue(t *testing.T) { rev := RevenueBreakdown{ Subscription: SubscriptionRevenue{TotalARR: 100000.0}, Transaction: TransactionRevenue{Total: 50000.0}, Municipal: MunicipalRevenue{Total: 25000.0}, Implementation: ImplementationRevenue{Total: 15000.0}, Total: 190000.0, } result := GetPlatformOnlyRevenue(rev) expected := 125000.0 // Subscription + Municipal assert.InDelta(t, expected, result, 0.01, "Platform-only revenue should be subscription + municipal") } func TestGetFullRevenue(t *testing.T) { rev := RevenueBreakdown{ Subscription: SubscriptionRevenue{TotalARR: 100000.0}, Transaction: TransactionRevenue{Total: 50000.0}, Municipal: MunicipalRevenue{Total: 25000.0}, Implementation: ImplementationRevenue{Total: 15000.0}, Total: 190000.0, } result := GetFullRevenue(rev) assert.InDelta(t, 190000.0, result, 0.01, "Full revenue should equal total revenue") } func createTestParams(year int) *params.Params { yearKey := params.YearKey(year) return ¶ms.Params{ Pricing: params.PricingParams{ Basic: 35.0, Business: 120.0, Enterprise: 400.0, BlendedUplift: params.BlendedUplift{ Basic: 0.20, Business: 0.25, Enterprise: 0.25, }, TierMix: params.YearlyTierMix{ yearKey: params.TierMix{ Basic: 0.60, Business: 0.30, Enterprise: 0.10, }, }, }, Transactions: params.TransactionParams{ AvgIntroFee: 550.0, IntrosPerYear: params.YearlyInt{yearKey: 200 + (year-1)*200}, // 200, 400, 600 IntroConversion: params.YearlyFloat{yearKey: 0.35 + float64(year-1)*0.03}, // 0.35, 0.38, 0.40 ServiceGMV: params.YearlyFloat{yearKey: float64(300000 + (year-1)*500000)}, // 300k, 800k, 1.5M ServiceCommission: 0.15, GroupGMV: params.YearlyFloat{yearKey: float64(200000 + (year-1)*200000)}, // 200k, 400k, 800k GroupCommission: 0.04, }, Municipal: params.MunicipalParams{ Cities: params.YearlyInt{yearKey: year}, // 1, 2, 3, 4 AvgLicense: params.YearlyFloat{yearKey: 60000.0 + float64(year-1)*30000.0}, // 60k, 90k, 110k DataLicensing: params.YearlyFloat{yearKey: float64((year - 1) * 50000)}, // 0, 50k, 150k }, ImplServices: params.ImplementationParams{ MatchesPerOrg: 0.5, PaidShare: 0.25, AvgFee: 5000.0, }, } }