package models import ( "testing" "github.com/damirmukimov/city_resource_graph/models/cost" "github.com/damirmukimov/city_resource_graph/models/customer" "github.com/damirmukimov/city_resource_graph/models/impact" "github.com/damirmukimov/city_resource_graph/models/params" "github.com/damirmukimov/city_resource_graph/models/revenue" "github.com/damirmukimov/city_resource_graph/models/validator" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCalculate(t *testing.T) { p := createTestParamsForCalc() result, err := Calculate(p) require.NoError(t, err, "Calculate should not return an error") assert.Equal(t, 3, len(result.Years), "Should calculate 3 years") // Test Year 1 year1 := result.Years[0] assert.Equal(t, 1, year1.Year, "Year 1 should have year=1") assert.Equal(t, 500, year1.Customer.TotalOrgs, "Year 1 should have 500 total orgs") assert.Equal(t, 150, year1.Customer.PayingOrgs, "Year 1 should have 150 paying orgs") // Test that types are correct assert.IsType(t, revenue.RevenueBreakdown{}, year1.Revenue, "Revenue should be RevenueBreakdown") assert.IsType(t, cost.CostBreakdown{}, year1.Costs, "Costs should be CostBreakdown") assert.IsType(t, impact.ImpactMetrics{}, year1.Impact, "Impact should be ImpactMetrics") assert.IsType(t, customer.CustomerMetrics{}, year1.Customer, "Customer should be CustomerMetrics") assert.IsType(t, customer.TierDistribution{}, year1.TierDist, "TierDist should be TierDistribution") assert.IsType(t, validator.ValidationResult{}, year1.Validation, "Validation should be ValidationResult") // Test ARPU range (B2B SaaS typical) assert.True(t, year1.ARPU > 300, "Year 1 ARPU should be above 300") assert.True(t, year1.ARPU < 6000, "Year 1 ARPU should be below 6000") // Test summary assert.True(t, result.Summary.TotalRevenue > 0, "Total revenue should be positive") assert.True(t, result.Summary.TotalProfit > 0, "Total profit should be positive") assert.True(t, result.Summary.TotalCO2Avoided > 0, "Total CO2 avoided should be positive") } func TestCalculateYear(t *testing.T) { p := createTestParamsForCalc() year1, err := CalculateYear(1, p) require.NoError(t, err, "CalculateYear should not return an error") assert.Equal(t, 1, year1.Year, "Should return year 1") assert.Equal(t, 500, year1.Customer.TotalOrgs, "Should calculate customer metrics") assert.True(t, year1.Revenue.Total > 0, "Should calculate revenue") assert.True(t, year1.Costs.Total > 0, "Should calculate costs") assert.True(t, year1.Impact.CO2Avoided > 0, "Should calculate impact") } func TestCalculateSummary(t *testing.T) { // Create test data years := []YearResult{ { Revenue: revenue.RevenueBreakdown{Total: 100000.0}, Costs: cost.CostBreakdown{Total: 80000.0}, Profit: 20000.0, Impact: impact.ImpactMetrics{ CO2Avoided: 1000.0, WaterReused: 50000.0, WasteDiverted: 2000.0, }, }, { Revenue: revenue.RevenueBreakdown{Total: 150000.0}, Costs: cost.CostBreakdown{Total: 100000.0}, Profit: 50000.0, Impact: impact.ImpactMetrics{ CO2Avoided: 1500.0, WaterReused: 75000.0, WasteDiverted: 3000.0, }, }, } // Test the summary calculation directly summary := calculateSummary(years) assert.InDelta(t, 250000.0, summary.TotalRevenue, 0.01, "Total revenue should be sum of all years") assert.InDelta(t, 180000.0, summary.TotalCosts, 0.01, "Total costs should be sum of all years") assert.InDelta(t, 70000.0, summary.TotalProfit, 0.01, "Total profit should be sum of all years") assert.InDelta(t, 2500.0, summary.TotalCO2Avoided, 0.01, "Total CO2 avoided should be sum of all years") assert.InDelta(t, 125000.0, summary.TotalWaterReused, 0.01, "Total water reused should be sum of all years") assert.InDelta(t, 5000.0, summary.TotalWasteDiverted, 0.01, "Total waste diverted should be sum of all years") } func createTestParamsForCalc() *params.Params { return ¶ms.Params{ Time: params.TimeParams{Years: []int{1, 2, 3}}, Adoption: params.AdoptionParams{ TotalOrgs: params.YearlyInt{"1": 500, "2": 2000, "3": 5000}, PayingShare: params.YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3}, }, 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{ "1": params.TierMix{Basic: 0.60, Business: 0.30, Enterprise: 0.10}, "2": params.TierMix{Basic: 0.60, Business: 0.30, Enterprise: 0.10}, "3": params.TierMix{Basic: 0.54, Business: 0.38, Enterprise: 0.08}, }, }, Transactions: params.TransactionParams{ AvgIntroFee: 550.0, IntrosPerYear: params.YearlyInt{"1": 200, "2": 400, "3": 600}, IntroConversion: params.YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40}, ServiceGMV: params.YearlyFloat{"1": 300000, "2": 800000, "3": 1500000}, ServiceCommission: 0.15, GroupGMV: params.YearlyFloat{"1": 200000, "2": 400000, "3": 800000}, GroupCommission: 0.04, }, Municipal: params.MunicipalParams{ Cities: params.YearlyInt{"1": 1, "2": 2, "3": 4}, AvgLicense: params.YearlyFloat{"1": 60000, "2": 90000, "3": 110000}, DataLicensing: params.YearlyFloat{"1": 0, "2": 50000, "3": 150000}, }, ImplServices: params.ImplementationParams{ MatchesPerOrg: 0.5, PaidShare: 0.25, AvgFee: 5000.0, }, Impact: params.ImpactParams{ HeatMWh: params.YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000}, GridFactor: 0.3, HXEff: 0.9, Utilization: 0.7, WaterPerOrg: 25000.0, WaterReuseRate: params.YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30}, WastePerOrg: 100.0, WasteDiversionRate: params.YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35}, }, Costs: params.CostParams{ Engineers: params.YearlyInt{"1": 8, "2": 12, "3": 15}, EngineerSalary: 100000.0, Infrastructure: params.YearlyFloat{"1": 200000, "2": 250000, "3": 400000}, MarketingSales: params.YearlyFloat{"1": 300000, "2": 600000, "3": 900000}, Operations: params.YearlyFloat{"1": 100000, "2": 150000, "3": 200000}, }, Market: params.MarketParams{ TAM: 500000000000.0, AddressableDigital: 3000000000.0, PilotCityEconomicBenefit: 4000000.0, ScalabilityPotential: 400000000.0, EUIndustrialFacilities: 2100000, EnergyWastePotential: 0.45, ResourceCostReduction: 0.25, ViableExchangeRate: 0.15, PlatformCaptureRate: 0.50, SOM: params.YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000}, }, } }