package cli import ( "fmt" "github.com/damirmukimov/city_resource_graph/models/cost" "github.com/damirmukimov/city_resource_graph/models/params" "github.com/spf13/cobra" ) // NewBudgetCmd creates a budget calculation command for funding applications func NewBudgetCmd() *cobra.Command { var durationMonths int var phase string var outputFormat string cmd := &cobra.Command{ Use: "budget [params.yaml]", Short: "Calculate detailed budget breakdown for funding applications", Long: `Calculate detailed budget breakdown by category (personnel, infrastructure, etc.) for funding applications. Supports different phases (MVP Foundation, MVP+, Full Program). Examples: # Calculate full program budget (18 months) go run ./cmd budget params.yaml --duration 18 # Calculate MVP+ budget (6 months) go run ./cmd budget params.yaml --duration 6 --phase MVP+ # Output as CSV go run ./cmd budget params.yaml --duration 6 --format csv > budget.csv`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { paramsFile := args[0] return calculateBudget(paramsFile, durationMonths, phase, outputFormat) }, } cmd.Flags().IntVar(&durationMonths, "duration", 18, "Budget duration in months") cmd.Flags().StringVar(&phase, "phase", "Full", "Budget phase: MVP-Foundation, MVP+, Full") cmd.Flags().StringVar(&outputFormat, "format", "table", "Output format: table, csv, json") return cmd } func calculateBudget(paramsFile string, durationMonths int, phase string, outputFormat string) error { // Load parameters p, err := params.LoadFromFile(paramsFile) if err != nil { return fmt.Errorf("failed to load params: %w", err) } // Use first year's parameters for calculation year := 1 if year > len(p.Time.Years) { year = len(p.Time.Years) } // Calculate costs using the cost package (convert months to year fraction) monthlyMultiplier := float64(durationMonths) / 12.0 engineering := float64(p.Costs.Engineers.GetYear(year)) * p.Costs.EngineerSalary * monthlyMultiplier infrastructure := p.Costs.Infrastructure.GetYear(year) * monthlyMultiplier marketingSales := p.Costs.MarketingSales.GetYear(year) * monthlyMultiplier operations := p.Costs.Operations.GetYear(year) * monthlyMultiplier // Create base cost breakdown costBreakdown := cost.CostBreakdown{ Engineering: engineering, Infrastructure: infrastructure, MarketingSales: marketingSales, Operations: operations, Total: engineering + infrastructure + marketingSales + operations, } // Apply phase-specific adjustments adjustedBudget := adjustBudgetForPhase(costBreakdown, phase, durationMonths, p) // Output based on format switch outputFormat { case "json": return outputBudgetJSON(adjustedBudget, phase, durationMonths) case "csv": return outputBudgetCSV(adjustedBudget, phase, durationMonths) default: return outputBudgetTable(adjustedBudget, phase, durationMonths, p) } } func adjustBudgetForPhase(base cost.CostBreakdown, phase string, months int, p *params.Params) BudgetBreakdown { // Base adjustments for different phases switch phase { case "MVP-Foundation": // MVP Foundation: Smaller team, reduced infrastructure return BudgetBreakdown{ Personnel: base.Engineering * 0.35, // Smaller team Infrastructure: base.Infrastructure * 0.15, // MVP-scale infrastructure Subcontracting: 25000, // Legal, accounting Travel: 10000, // Essential travel Marketing: 25000, // Basic marketing Other: 15000, // Tools, communication Total: 0, } case "MVP+": // MVP+: Medium team, expanded infrastructure return BudgetBreakdown{ Personnel: base.Engineering * 0.69, // Medium team Infrastructure: base.Infrastructure * 0.32, // MVP+ infrastructure Subcontracting: 45000, Travel: 25000, Marketing: 66000, Other: 21000, Total: 0, } default: // "Full" // Full Program: Complete team, full infrastructure // Add detailed breakdown personnelTotal := base.Engineering + base.MarketingSales*0.3 // Add BD/domain experts return BudgetBreakdown{ Personnel: personnelTotal, Infrastructure: base.Infrastructure, Subcontracting: 50000, Travel: 40000, Marketing: 85000, Other: 5000, Total: 0, } } } type BudgetBreakdown struct { Personnel float64 `json:"personnel"` Infrastructure float64 `json:"infrastructure"` Subcontracting float64 `json:"subcontracting"` Travel float64 `json:"travel"` Marketing float64 `json:"marketing"` Other float64 `json:"other"` Total float64 `json:"total"` } func outputBudgetTable(budget BudgetBreakdown, phase string, months int, p *params.Params) error { budget.Total = budget.Personnel + budget.Infrastructure + budget.Subcontracting + budget.Travel + budget.Marketing + budget.Other fmt.Printf("\nBudget Breakdown for %s Phase (%d months)\n", phase, months) fmt.Println(string(make([]byte, 60))) fmt.Printf("\nCategory Breakdown:\n\n") fmt.Printf(" Personnel: €%12.2f (%5.1f%%)\n", budget.Personnel, (budget.Personnel/budget.Total)*100) fmt.Printf(" Infrastructure: €%12.2f (%5.1f%%)\n", budget.Infrastructure, (budget.Infrastructure/budget.Total)*100) fmt.Printf(" Subcontracting: €%12.2f (%5.1f%%)\n", budget.Subcontracting, (budget.Subcontracting/budget.Total)*100) fmt.Printf(" Travel: €%12.2f (%5.1f%%)\n", budget.Travel, (budget.Travel/budget.Total)*100) fmt.Printf(" Marketing: €%12.2f (%5.1f%%)\n", budget.Marketing, (budget.Marketing/budget.Total)*100) fmt.Printf(" Other: €%12.2f (%5.1f%%)\n", budget.Other, (budget.Other/budget.Total)*100) fmt.Printf("\n TOTAL: €%12.2f (100.0%%)\n", budget.Total) fmt.Printf("\nModel Validation (from params.yaml):\n") fmt.Printf(" Engineers (Year 1): %d × €%.0f = €%.0f/year\n", p.Costs.Engineers.GetYear(1), p.Costs.EngineerSalary, float64(p.Costs.Engineers.GetYear(1))*p.Costs.EngineerSalary) fmt.Printf(" Adjusted for %d months: €%.0f\n", months, float64(p.Costs.Engineers.GetYear(1))*p.Costs.EngineerSalary*float64(months)/12.0) return nil } func outputBudgetCSV(budget BudgetBreakdown, phase string, months int) error { budget.Total = budget.Personnel + budget.Infrastructure + budget.Subcontracting + budget.Travel + budget.Marketing + budget.Other fmt.Println("Category,Amount (EUR),Percentage") fmt.Printf("Personnel,%.2f,%.2f%%\n", budget.Personnel, (budget.Personnel/budget.Total)*100) fmt.Printf("Infrastructure,%.2f,%.2f%%\n", budget.Infrastructure, (budget.Infrastructure/budget.Total)*100) fmt.Printf("Subcontracting,%.2f,%.2f%%\n", budget.Subcontracting, (budget.Subcontracting/budget.Total)*100) fmt.Printf("Travel,%.2f,%.2f%%\n", budget.Travel, (budget.Travel/budget.Total)*100) fmt.Printf("Marketing,%.2f,%.2f%%\n", budget.Marketing, (budget.Marketing/budget.Total)*100) fmt.Printf("Other,%.2f,%.2f%%\n", budget.Other, (budget.Other/budget.Total)*100) fmt.Printf("TOTAL,%.2f,100.00%%\n", budget.Total) return nil } func outputBudgetJSON(budget BudgetBreakdown, phase string, months int) error { budget.Total = budget.Personnel + budget.Infrastructure + budget.Subcontracting + budget.Travel + budget.Marketing + budget.Other jsonOutput := fmt.Sprintf(`{ "phase": "%s", "duration_months": %d, "budget": { "personnel": %.2f, "infrastructure": %.2f, "subcontracting": %.2f, "travel": %.2f, "marketing": %.2f, "other": %.2f, "total": %.2f }, "percentages": { "personnel": %.2f, "infrastructure": %.2f, "subcontracting": %.2f, "travel": %.2f, "marketing": %.2f, "other": %.2f } } `, phase, months, budget.Personnel, budget.Infrastructure, budget.Subcontracting, budget.Travel, budget.Marketing, budget.Other, budget.Total, (budget.Personnel/budget.Total)*100, (budget.Infrastructure/budget.Total)*100, (budget.Subcontracting/budget.Total)*100, (budget.Travel/budget.Total)*100, (budget.Marketing/budget.Total)*100, (budget.Other/budget.Total)*100, ) fmt.Print(jsonOutput) return nil }