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
138 lines
4.4 KiB
Go
138 lines
4.4 KiB
Go
package validator
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"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/revenue"
|
|
)
|
|
|
|
// ValidationError represents a validation failure.
|
|
type ValidationError struct {
|
|
Year int
|
|
Rule string
|
|
Message string
|
|
Value interface{}
|
|
}
|
|
|
|
func (ve ValidationError) Error() string {
|
|
return fmt.Sprintf("validation failed [Year %d, Rule: %s]: %s (value: %v)", ve.Year, ve.Rule, ve.Message, ve.Value)
|
|
}
|
|
|
|
// ValidationResult contains all validation errors found.
|
|
type ValidationResult struct {
|
|
Errors []ValidationError
|
|
}
|
|
|
|
// IsValid returns true if no validation errors were found.
|
|
func (vr ValidationResult) IsValid() bool {
|
|
return len(vr.Errors) == 0
|
|
}
|
|
|
|
// Validate performs all sanity checks on the model outputs for a given year.
|
|
func Validate(
|
|
year int,
|
|
custMetrics customer.CustomerMetrics,
|
|
revBreakdown revenue.RevenueBreakdown,
|
|
costBreakdown cost.CostBreakdown,
|
|
impactMetrics impact.ImpactMetrics,
|
|
) ValidationResult {
|
|
var errors []ValidationError
|
|
|
|
// 1. ARPU sanity check
|
|
// ARPU should be within typical B2B SaaS range: €300-€6,000/year
|
|
if custMetrics.PayingOrgs > 0 {
|
|
arpu := revBreakdown.Total / float64(custMetrics.PayingOrgs)
|
|
if arpu < 300 || arpu > 6000 {
|
|
errors = append(errors, ValidationError{
|
|
Year: year,
|
|
Rule: "ARPU_SANITY",
|
|
Message: fmt.Sprintf("ARPU (€%.2f) outside typical B2B SaaS range (€300-€6,000)", arpu),
|
|
Value: arpu,
|
|
})
|
|
}
|
|
}
|
|
|
|
// 2. Implementation density check
|
|
// Matches per organization should not exceed 1.5 (unrealistic for 3-year horizon)
|
|
if impactMetrics.CO2Avoided > 0 || custMetrics.TotalOrgs > 0 {
|
|
// Estimate matches from implementation revenue
|
|
// This is approximate - ideally should come from model directly
|
|
implRev := revBreakdown.Implementation.Total
|
|
if implRev > 0 && revBreakdown.Implementation.PaidImpls > 0 {
|
|
matchesPerOrg := revBreakdown.Implementation.Matches / float64(custMetrics.TotalOrgs)
|
|
if matchesPerOrg > 1.5 {
|
|
errors = append(errors, ValidationError{
|
|
Year: year,
|
|
Rule: "IMPLEMENTATION_DENSITY",
|
|
Message: fmt.Sprintf("Matches per org (%.2f) exceeds realistic limit (1.5)", matchesPerOrg),
|
|
Value: matchesPerOrg,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. CO₂ / revenue ratio check
|
|
// Catches unit mistakes (e.g., if CO₂ is in wrong units)
|
|
if revBreakdown.Total > 0 {
|
|
co2PerEur := impactMetrics.CO2Avoided / revBreakdown.Total
|
|
if co2PerEur > 10 {
|
|
errors = append(errors, ValidationError{
|
|
Year: year,
|
|
Rule: "CO2_REVENUE_RATIO",
|
|
Message: fmt.Sprintf("CO₂ per EUR (%.2f t CO₂/€) exceeds sanity limit (10)", co2PerEur),
|
|
Value: co2PerEur,
|
|
})
|
|
}
|
|
}
|
|
|
|
// 4. Heat MWh sanity check
|
|
// If heat exceeds 5,000,000 MWh/year, likely multiplied by 12 (monthly mistake)
|
|
if impactMetrics.CO2Avoided > 0 {
|
|
// Reconstruct heat from CO₂ to check
|
|
// This is approximate - ideally should validate input directly
|
|
estimatedHeat := impactMetrics.CO2Avoided / (0.3 * 0.9 * 0.7) // Reverse calculation
|
|
if estimatedHeat > 5_000_000 {
|
|
errors = append(errors, ValidationError{
|
|
Year: year,
|
|
Rule: "HEAT_MWH_SANITY",
|
|
Message: fmt.Sprintf("Estimated heat (%.0f MWh) exceeds sanity limit (5M MWh/year) - possible monthly multiplier error", estimatedHeat),
|
|
Value: estimatedHeat,
|
|
})
|
|
}
|
|
}
|
|
|
|
// 5. Profitability check (warning only)
|
|
if revBreakdown.Total > 0 && costBreakdown.Total > 0 {
|
|
profit := revBreakdown.Total - costBreakdown.Total
|
|
margin := (profit / revBreakdown.Total) * 100
|
|
if margin < -100 {
|
|
// Losses exceed revenue - suspicious
|
|
errors = append(errors, ValidationError{
|
|
Year: year,
|
|
Rule: "PROFITABILITY_WARNING",
|
|
Message: fmt.Sprintf("Margin (%.1f%%) indicates losses exceeding revenue - verify cost inputs", margin),
|
|
Value: margin,
|
|
})
|
|
}
|
|
}
|
|
|
|
return ValidationResult{Errors: errors}
|
|
}
|
|
|
|
// ValidateMunicipalPenetration checks if municipal city count is reasonable.
|
|
// This requires external context (total cities in target region).
|
|
func ValidateMunicipalPenetration(year int, cities int, maxCities int) ValidationError {
|
|
if cities > maxCities {
|
|
return ValidationError{
|
|
Year: year,
|
|
Rule: "MUNICIPAL_PENETRATION",
|
|
Message: fmt.Sprintf("Municipal cities (%d) exceeds maximum expected (%d)", cities, maxCities),
|
|
Value: cities,
|
|
}
|
|
}
|
|
return ValidationError{} // No error
|
|
}
|