turash/models/revenue/revenue_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

307 lines
9.4 KiB
Go

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 &params.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,
},
}
}