mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- 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
602 lines
21 KiB
Go
602 lines
21 KiB
Go
package params
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestLoadFromFile(t *testing.T) {
|
|
p, err := LoadFromFile("../params.yaml")
|
|
require.NoError(t, err, "Failed to load params.yaml")
|
|
|
|
// Test time configuration
|
|
assert.Equal(t, []int{1, 2, 3}, p.Time.Years, "Years should be [1,2,3]")
|
|
|
|
// Test adoption parameters
|
|
assert.Equal(t, 500, p.Adoption.TotalOrgs.GetYear(1), "Year 1 total orgs should be 500")
|
|
assert.Equal(t, 2000, p.Adoption.TotalOrgs.GetYear(2), "Year 2 total orgs should be 2000")
|
|
assert.Equal(t, 5000, p.Adoption.TotalOrgs.GetYear(3), "Year 3 total orgs should be 5000")
|
|
|
|
assert.Equal(t, 0.30, p.Adoption.PayingShare.GetYear(1), "Year 1 paying share should be 0.30")
|
|
|
|
// Test pricing
|
|
assert.Equal(t, 35.0, p.Pricing.Basic, "Basic tier price should be 35")
|
|
assert.Equal(t, 120.0, p.Pricing.Business, "Business tier price should be 120")
|
|
assert.Equal(t, 400.0, p.Pricing.Enterprise, "Enterprise tier price should be 400")
|
|
}
|
|
|
|
// Test LoadFromFile with non-existent file
|
|
func TestLoadFromFile_FileNotFound(t *testing.T) {
|
|
_, err := LoadFromFile("non_existent_file.yaml")
|
|
assert.Error(t, err, "Should return error for non-existent file")
|
|
assert.Contains(t, err.Error(), "non_existent_file.yaml", "Error should mention the filename")
|
|
}
|
|
|
|
func TestValidate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
params *Params
|
|
wantError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "valid params",
|
|
params: createValidParams(),
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "no years",
|
|
params: &Params{
|
|
Time: TimeParams{Years: []int{}},
|
|
},
|
|
wantError: true,
|
|
errorMsg: "must have at least one year",
|
|
},
|
|
{
|
|
name: "missing total orgs",
|
|
params: &Params{
|
|
Time: TimeParams{Years: []int{1}},
|
|
Adoption: AdoptionParams{
|
|
TotalOrgs: YearlyInt{}, // empty
|
|
},
|
|
},
|
|
wantError: true,
|
|
errorMsg: "missing value for year 1",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.params.Validate()
|
|
|
|
if tt.wantError {
|
|
assert.Error(t, err, "Expected validation error")
|
|
assert.Contains(t, err.Error(), tt.errorMsg, "Error message should contain expected text")
|
|
} else {
|
|
assert.NoError(t, err, "Expected no validation error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestYearlyInt_GetYear(t *testing.T) {
|
|
yi := YearlyInt{
|
|
"1": 100,
|
|
"2": 200,
|
|
"3": 300,
|
|
}
|
|
|
|
assert.Equal(t, 100, yi.GetYear(1), "Year 1 should return 100")
|
|
assert.Equal(t, 200, yi.GetYear(2), "Year 2 should return 200")
|
|
assert.Equal(t, 300, yi.GetYear(3), "Year 3 should return 300")
|
|
assert.Equal(t, 0, yi.GetYear(4), "Non-existent year should return 0")
|
|
}
|
|
|
|
func TestYearlyFloat_GetYear(t *testing.T) {
|
|
yf := YearlyFloat{
|
|
"1": 1.5,
|
|
"2": 2.5,
|
|
"3": 3.5,
|
|
}
|
|
|
|
assert.Equal(t, 1.5, yf.GetYear(1), "Year 1 should return 1.5")
|
|
assert.Equal(t, 2.5, yf.GetYear(2), "Year 2 should return 2.5")
|
|
assert.Equal(t, 3.5, yf.GetYear(3), "Year 3 should return 3.5")
|
|
assert.Equal(t, 0.0, yf.GetYear(4), "Non-existent year should return 0.0")
|
|
}
|
|
|
|
func TestYearlyTierMix_GetYear(t *testing.T) {
|
|
ytm := YearlyTierMix{
|
|
"1": TierMix{Basic: 0.6, Business: 0.3, Enterprise: 0.1},
|
|
"2": TierMix{Basic: 0.5, Business: 0.35, Enterprise: 0.15},
|
|
}
|
|
|
|
tier1 := ytm.GetYear(1)
|
|
assert.Equal(t, 0.6, tier1.Basic, "Year 1 Basic should be 0.6")
|
|
assert.Equal(t, 0.3, tier1.Business, "Year 1 Business should be 0.3")
|
|
assert.Equal(t, 0.1, tier1.Enterprise, "Year 1 Enterprise should be 0.1")
|
|
|
|
tier2 := ytm.GetYear(2)
|
|
assert.Equal(t, 0.5, tier2.Basic, "Year 2 Basic should be 0.5")
|
|
assert.Equal(t, 0.35, tier2.Business, "Year 2 Business should be 0.35")
|
|
|
|
// Test empty case
|
|
emptyTier := ytm.GetYear(3)
|
|
assert.Equal(t, TierMix{}, emptyTier, "Non-existent year should return empty TierMix")
|
|
}
|
|
|
|
func createValidParams() *Params {
|
|
return &Params{
|
|
Time: TimeParams{Years: []int{1, 2, 3}},
|
|
Adoption: AdoptionParams{
|
|
TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000},
|
|
PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3},
|
|
},
|
|
Pricing: PricingParams{
|
|
Basic: 35.0,
|
|
Business: 120.0,
|
|
Enterprise: 400.0,
|
|
TierMix: YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}},
|
|
},
|
|
Transactions: TransactionParams{
|
|
AvgIntroFee: 550.0,
|
|
IntrosPerYear: YearlyInt{"1": 200, "2": 400, "3": 600},
|
|
IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40},
|
|
ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000},
|
|
ServiceCommission: 0.15,
|
|
GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000},
|
|
GroupCommission: 0.04,
|
|
},
|
|
Municipal: MunicipalParams{
|
|
Cities: YearlyInt{"1": 1, "2": 2, "3": 4},
|
|
AvgLicense: YearlyFloat{"1": 60000, "2": 90000, "3": 110000},
|
|
DataLicensing: YearlyFloat{"1": 0, "2": 50000, "3": 150000},
|
|
},
|
|
ImplServices: ImplementationParams{
|
|
MatchesPerOrg: 0.5,
|
|
PaidShare: 0.25,
|
|
AvgFee: 5000.0,
|
|
},
|
|
Impact: ImpactParams{
|
|
HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000},
|
|
GridFactor: 0.3,
|
|
HXEff: 0.9,
|
|
Utilization: 0.7,
|
|
WaterPerOrg: 25000.0,
|
|
WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30},
|
|
WastePerOrg: 100.0,
|
|
WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35},
|
|
},
|
|
Costs: CostParams{
|
|
Engineers: YearlyInt{"1": 8, "2": 12, "3": 15},
|
|
EngineerSalary: 100000.0,
|
|
Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000},
|
|
MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000},
|
|
Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000},
|
|
},
|
|
Market: 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: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Test LoadFromJSON function
|
|
func TestLoadFromJSON(t *testing.T) {
|
|
jsonData := `{
|
|
"time": {"years": [1, 2]},
|
|
"adoption": {
|
|
"total_orgs": {"1": 100, "2": 200},
|
|
"paying_share": {"1": 0.5, "2": 0.6}
|
|
},
|
|
"pricing": {
|
|
"basic": 30.0,
|
|
"business": 100.0,
|
|
"enterprise": 300.0
|
|
}
|
|
}`
|
|
|
|
p, err := LoadFromJSON([]byte(jsonData))
|
|
require.NoError(t, err, "Should load JSON successfully")
|
|
|
|
assert.Equal(t, []int{1, 2}, p.Time.Years, "Years should be loaded from JSON")
|
|
assert.Equal(t, 100, p.Adoption.TotalOrgs.GetYear(1), "Total orgs should be loaded from JSON")
|
|
assert.Equal(t, 0.5, p.Adoption.PayingShare.GetYear(1), "Paying share should be loaded from JSON")
|
|
assert.Equal(t, 30.0, p.Pricing.Basic, "Basic price should be loaded from JSON")
|
|
}
|
|
|
|
// Test LoadFromYAML function
|
|
func TestLoadFromYAML(t *testing.T) {
|
|
yamlData := `
|
|
time:
|
|
years: [1, 2]
|
|
adoption:
|
|
total_orgs:
|
|
"1": 150
|
|
"2": 250
|
|
paying_share:
|
|
"1": 0.4
|
|
"2": 0.5
|
|
pricing:
|
|
basic: 40.0
|
|
business: 130.0
|
|
enterprise: 350.0
|
|
`
|
|
|
|
p, err := LoadFromYAML([]byte(yamlData))
|
|
require.NoError(t, err, "Should load YAML successfully")
|
|
|
|
assert.Equal(t, []int{1, 2}, p.Time.Years, "Years should be loaded from YAML")
|
|
assert.Equal(t, 150, p.Adoption.TotalOrgs.GetYear(1), "Total orgs should be loaded from YAML")
|
|
assert.Equal(t, 0.4, p.Adoption.PayingShare.GetYear(1), "Paying share should be loaded from YAML")
|
|
assert.Equal(t, 40.0, p.Pricing.Basic, "Basic price should be loaded from YAML")
|
|
}
|
|
|
|
// Test ErrInvalidParams error formatting
|
|
func TestErrInvalidParams_Error(t *testing.T) {
|
|
err := ErrInvalidParams{
|
|
Field: "test.field",
|
|
Message: "test message",
|
|
}
|
|
|
|
errorMsg := err.Error()
|
|
assert.Contains(t, errorMsg, "invalid params", "Should contain error prefix")
|
|
assert.Contains(t, errorMsg, "test.field", "Should contain field name")
|
|
assert.Contains(t, errorMsg, "test message", "Should contain message")
|
|
}
|
|
|
|
// Test yearKey function
|
|
func TestYearKey(t *testing.T) {
|
|
assert.Equal(t, "1", yearKey(1), "Year 1 should convert to string '1'")
|
|
assert.Equal(t, "42", yearKey(42), "Year 42 should convert to string '42'")
|
|
assert.Equal(t, "0", yearKey(0), "Year 0 should convert to string '0'")
|
|
}
|
|
|
|
// Test GetYear methods with missing keys to ensure 100% coverage
|
|
func TestGetYear_Methods_MissingKeys(t *testing.T) {
|
|
// Test YearlyInt.GetYear with missing key
|
|
yi := YearlyInt{"1": 100, "2": 200}
|
|
assert.Equal(t, 100, yi.GetYear(1), "Should return value for existing key")
|
|
assert.Equal(t, 200, yi.GetYear(2), "Should return value for existing key")
|
|
assert.Equal(t, 0, yi.GetYear(3), "Should return 0 for missing key")
|
|
|
|
// Test YearlyFloat.GetYear with missing key
|
|
yf := YearlyFloat{"1": 100.5, "2": 200.5}
|
|
assert.Equal(t, 100.5, yf.GetYear(1), "Should return value for existing key")
|
|
assert.Equal(t, 200.5, yf.GetYear(2), "Should return value for existing key")
|
|
assert.Equal(t, 0.0, yf.GetYear(3), "Should return 0.0 for missing key")
|
|
|
|
// Test YearlyTierMix.GetYear with missing key
|
|
ytm := YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.5, 0.3, 0.2}}
|
|
tier1 := ytm.GetYear(1)
|
|
assert.Equal(t, 0.6, tier1.Basic, "Should return value for existing key")
|
|
assert.Equal(t, 0.3, tier1.Business, "Should return value for existing key")
|
|
assert.Equal(t, 0.1, tier1.Enterprise, "Should return value for existing key")
|
|
|
|
tier2 := ytm.GetYear(2)
|
|
assert.Equal(t, 0.5, tier2.Basic, "Should return value for existing key")
|
|
assert.Equal(t, 0.3, tier2.Business, "Should return value for existing key")
|
|
assert.Equal(t, 0.2, tier2.Enterprise, "Should return value for existing key")
|
|
|
|
tier3 := ytm.GetYear(3)
|
|
assert.Equal(t, TierMix{}, tier3, "Should return empty TierMix for missing key")
|
|
}
|
|
|
|
// Test Params.Validate method thoroughly
|
|
func TestParams_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
params *Params
|
|
expectError bool
|
|
errorField string
|
|
}{
|
|
{
|
|
name: "valid params",
|
|
params: createValidParams(),
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "no years",
|
|
params: &Params{
|
|
Time: TimeParams{Years: []int{}},
|
|
},
|
|
expectError: true,
|
|
errorField: "time.years",
|
|
},
|
|
{
|
|
name: "missing total orgs for year 1",
|
|
params: &Params{
|
|
Time: TimeParams{Years: []int{1, 2, 3}},
|
|
Adoption: AdoptionParams{
|
|
TotalOrgs: YearlyInt{"2": 2000, "3": 5000}, // Missing year 1
|
|
PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3},
|
|
},
|
|
},
|
|
expectError: true,
|
|
errorField: "adoption.total_orgs",
|
|
},
|
|
{
|
|
name: "missing paying share for year 2",
|
|
params: &Params{
|
|
Time: TimeParams{Years: []int{1, 2, 3}},
|
|
Adoption: AdoptionParams{
|
|
TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000},
|
|
PayingShare: YearlyFloat{"1": 0.3, "3": 0.3}, // Missing year 2
|
|
},
|
|
Pricing: PricingParams{
|
|
Basic: 35.0,
|
|
Business: 120.0,
|
|
Enterprise: 400.0,
|
|
TierMix: YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}},
|
|
},
|
|
Transactions: TransactionParams{
|
|
AvgIntroFee: 550.0,
|
|
IntrosPerYear: YearlyInt{"1": 200, "2": 400, "3": 600},
|
|
IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40},
|
|
ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000},
|
|
ServiceCommission: 0.15,
|
|
GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000},
|
|
GroupCommission: 0.04,
|
|
},
|
|
Municipal: MunicipalParams{
|
|
Cities: YearlyInt{"1": 1, "2": 2, "3": 4},
|
|
AvgLicense: YearlyFloat{"1": 60000, "2": 90000, "3": 110000},
|
|
DataLicensing: YearlyFloat{"1": 0, "2": 50000, "3": 150000},
|
|
},
|
|
ImplServices: ImplementationParams{
|
|
MatchesPerOrg: 0.5,
|
|
PaidShare: 0.25,
|
|
AvgFee: 5000.0,
|
|
},
|
|
Impact: ImpactParams{
|
|
HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000},
|
|
GridFactor: 0.3,
|
|
HXEff: 0.9,
|
|
Utilization: 0.7,
|
|
WaterPerOrg: 25000.0,
|
|
WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30},
|
|
WastePerOrg: 100.0,
|
|
WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35},
|
|
},
|
|
Costs: CostParams{
|
|
Engineers: YearlyInt{"1": 8, "2": 12, "3": 15},
|
|
EngineerSalary: 100000.0,
|
|
Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000},
|
|
MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000},
|
|
Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000},
|
|
},
|
|
Market: 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: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000},
|
|
},
|
|
},
|
|
expectError: true,
|
|
errorField: "adoption.paying_share",
|
|
},
|
|
{
|
|
name: "missing pricing tier mix for year 1",
|
|
params: &Params{
|
|
Time: TimeParams{Years: []int{1, 2, 3}},
|
|
Adoption: AdoptionParams{
|
|
TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000},
|
|
PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3},
|
|
},
|
|
Pricing: PricingParams{
|
|
Basic: 35.0,
|
|
Business: 120.0,
|
|
Enterprise: 400.0,
|
|
TierMix: YearlyTierMix{"2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}}, // Missing year 1
|
|
},
|
|
Transactions: TransactionParams{
|
|
AvgIntroFee: 550.0,
|
|
IntrosPerYear: YearlyInt{"1": 200, "2": 400, "3": 600},
|
|
IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40},
|
|
ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000},
|
|
ServiceCommission: 0.15,
|
|
GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000},
|
|
GroupCommission: 0.04,
|
|
},
|
|
Municipal: MunicipalParams{
|
|
Cities: YearlyInt{"1": 1, "2": 2, "3": 4},
|
|
AvgLicense: YearlyFloat{"1": 60000, "2": 90000, "3": 110000},
|
|
DataLicensing: YearlyFloat{"1": 0, "2": 50000, "3": 150000},
|
|
},
|
|
ImplServices: ImplementationParams{
|
|
MatchesPerOrg: 0.5,
|
|
PaidShare: 0.25,
|
|
AvgFee: 5000.0,
|
|
},
|
|
Impact: ImpactParams{
|
|
HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000},
|
|
GridFactor: 0.3,
|
|
HXEff: 0.9,
|
|
Utilization: 0.7,
|
|
WaterPerOrg: 25000.0,
|
|
WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30},
|
|
WastePerOrg: 100.0,
|
|
WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35},
|
|
},
|
|
Costs: CostParams{
|
|
Engineers: YearlyInt{"1": 8, "2": 12, "3": 15},
|
|
EngineerSalary: 100000.0,
|
|
Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000},
|
|
MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000},
|
|
Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000},
|
|
},
|
|
Market: 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: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000},
|
|
},
|
|
},
|
|
expectError: true,
|
|
errorField: "pricing.tier_mix",
|
|
},
|
|
{
|
|
name: "missing transaction intros for year 1",
|
|
params: &Params{
|
|
Time: TimeParams{Years: []int{1, 2, 3}},
|
|
Adoption: AdoptionParams{
|
|
TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000},
|
|
PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3},
|
|
},
|
|
Pricing: PricingParams{
|
|
Basic: 35.0,
|
|
Business: 120.0,
|
|
Enterprise: 400.0,
|
|
TierMix: YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}},
|
|
},
|
|
Transactions: TransactionParams{
|
|
AvgIntroFee: 550.0,
|
|
IntrosPerYear: YearlyInt{"2": 400, "3": 600}, // Missing year 1
|
|
IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40},
|
|
ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000},
|
|
ServiceCommission: 0.15,
|
|
GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000},
|
|
GroupCommission: 0.04,
|
|
},
|
|
Impact: ImpactParams{
|
|
HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000},
|
|
GridFactor: 0.3,
|
|
HXEff: 0.9,
|
|
Utilization: 0.7,
|
|
WaterPerOrg: 25000.0,
|
|
WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30},
|
|
WastePerOrg: 100.0,
|
|
WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35},
|
|
},
|
|
Costs: CostParams{
|
|
Engineers: YearlyInt{"1": 8, "2": 12, "3": 15},
|
|
EngineerSalary: 100000.0,
|
|
Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000},
|
|
MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000},
|
|
Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000},
|
|
},
|
|
Market: 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: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000},
|
|
},
|
|
},
|
|
expectError: true,
|
|
errorField: "transactions.intros_per_year",
|
|
},
|
|
{
|
|
name: "missing municipal cities for year 2",
|
|
params: &Params{
|
|
Time: TimeParams{Years: []int{1, 2, 3}},
|
|
Adoption: AdoptionParams{
|
|
TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000},
|
|
PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3},
|
|
},
|
|
Pricing: PricingParams{
|
|
Basic: 35.0,
|
|
Business: 120.0,
|
|
Enterprise: 400.0,
|
|
TierMix: YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}},
|
|
},
|
|
Transactions: TransactionParams{
|
|
AvgIntroFee: 550.0,
|
|
IntrosPerYear: YearlyInt{"1": 200, "2": 400, "3": 600},
|
|
IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40},
|
|
ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000},
|
|
ServiceCommission: 0.15,
|
|
GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000},
|
|
GroupCommission: 0.04,
|
|
},
|
|
Municipal: MunicipalParams{
|
|
Cities: YearlyInt{"1": 1, "3": 4}, // Missing year 2
|
|
AvgLicense: YearlyFloat{"1": 60000, "2": 90000, "3": 110000},
|
|
DataLicensing: YearlyFloat{"1": 0, "2": 50000, "3": 150000},
|
|
},
|
|
ImplServices: ImplementationParams{
|
|
MatchesPerOrg: 0.5,
|
|
PaidShare: 0.25,
|
|
AvgFee: 5000.0,
|
|
},
|
|
Impact: ImpactParams{
|
|
HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000},
|
|
GridFactor: 0.3,
|
|
HXEff: 0.9,
|
|
Utilization: 0.7,
|
|
WaterPerOrg: 25000.0,
|
|
WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30},
|
|
WastePerOrg: 100.0,
|
|
WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35},
|
|
},
|
|
Costs: CostParams{
|
|
Engineers: YearlyInt{"1": 8, "2": 12, "3": 15},
|
|
EngineerSalary: 100000.0,
|
|
Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000},
|
|
MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000},
|
|
Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000},
|
|
},
|
|
Market: 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: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000},
|
|
},
|
|
},
|
|
expectError: true,
|
|
errorField: "municipal.cities",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.params.Validate()
|
|
|
|
if tt.expectError {
|
|
assert.Error(t, err, "Should return validation error")
|
|
assert.IsType(t, ErrInvalidParams{}, err, "Should be ErrInvalidParams")
|
|
errInvalid, ok := err.(ErrInvalidParams)
|
|
if !ok {
|
|
t.Fatalf("expected ErrInvalidParams, got %T", err)
|
|
}
|
|
assert.Contains(t, errInvalid.Field, tt.errorField, "Error field should match")
|
|
} else {
|
|
assert.NoError(t, err, "Should not return validation error")
|
|
}
|
|
})
|
|
}
|
|
}
|