package handlers import ( "encoding/json" "fmt" "net/http" "strconv" "strings" "time" "github.com/fiskerinc/cloud-services/pkg/logger" "github.com/fiskerinc/cloud-services/services/cost/services" ) // GetVinCost returns cost data for a specific VIN // GET /cost/vin/{vin}?from=2024-01-01&to=2024-01-31 func GetVinCost(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Extract VIN from path path := strings.TrimPrefix(r.URL.Path, "/cost/vin/") vin := strings.TrimSuffix(path, "/") if vin == "" { http.Error(w, "VIN required", http.StatusBadRequest) return } from, to := parseTimeRange(r) summary, err := services.GetVinCostSummary(vin, from, to) if err != nil { logger.Error().Err(err).Str("vin", vin).Msg("Failed to get VIN cost") http.Error(w, "Failed to get cost data", http.StatusInternalServerError) return } respondJSON(w, summary) } // GetFleetCost returns fleet-wide cost summary // GET /cost/fleet?from=2024-01-01&to=2024-01-31&limit=10 func GetFleetCost(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } from, to := parseTimeRange(r) limit := 10 if l := r.URL.Query().Get("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil { limit = parsed } } summary, err := services.GetFleetCostSummary(from, to, limit) if err != nil { logger.Error().Err(err).Msg("Failed to get fleet cost") http.Error(w, "Failed to get cost data", http.StatusInternalServerError) return } respondJSON(w, summary) } // GetCostSummary returns a high-level cost summary // GET /cost/summary?period=day|week|month func GetCostSummary(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } period := r.URL.Query().Get("period") if period == "" { period = "day" } to := time.Now() var from time.Time switch period { case "week": from = to.AddDate(0, 0, -7) case "month": from = to.AddDate(0, -1, 0) default: from = to.AddDate(0, 0, -1) } summary, err := services.GetFleetCostSummary(from, to, 5) if err != nil { logger.Error().Err(err).Msg("Failed to get cost summary") http.Error(w, "Failed to get cost data", http.StatusInternalServerError) return } respondJSON(w, map[string]interface{}{ "period": period, "summary": summary, "cost_rates": map[string]interface{}{ "cloud": map[string]float64{ "cpu_per_core_hour": services.CloudCPUPerCoreHour, "memory_per_gb_hour": services.CloudMemoryPerGBHour, }, "onprem": map[string]float64{ "cpu_per_core_hour": services.OnpremCPUPerCoreHour, "memory_per_gb_hour": services.OnpremMemoryPerGBHour, }, }, }) } // GetCostComparison returns cloud vs on-prem cost comparison // GET /cost/comparison?from=2024-01-01&to=2024-01-31 func GetCostComparison(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } from, to := parseTimeRange(r) summary, err := services.GetFleetCostSummary(from, to, 0) if err != nil { logger.Error().Err(err).Msg("Failed to get cost comparison") http.Error(w, "Failed to get cost data", http.StatusInternalServerError) return } comparison := map[string]interface{}{ "period": map[string]interface{}{ "from": from, "to": to, }, "cloud": map[string]interface{}{ "total_cost_usd": summary.TotalCloudCost, "cost_per_vehicle": 0.0, "description": "Azure cloud hosting with managed services", }, "onprem": map[string]interface{}{ "total_cost_usd": summary.TotalOnpremCost, "cost_per_vehicle": 0.0, "description": "Self-hosted on owned hardware (amortized)", }, "savings": map[string]interface{}{ "total_usd": summary.TotalSavings, "percent": summary.SavingsPercent, "annual_projected": summary.TotalSavings * 12, }, "vehicle_count": summary.VehicleCount, } if summary.VehicleCount > 0 { comparison["cloud"].(map[string]interface{})["cost_per_vehicle"] = summary.TotalCloudCost / float64(summary.VehicleCount) comparison["onprem"].(map[string]interface{})["cost_per_vehicle"] = summary.TotalOnpremCost / float64(summary.VehicleCount) } respondJSON(w, comparison) } func parseTimeRange(r *http.Request) (from, to time.Time) { to = time.Now() from = to.AddDate(0, 0, -30) // Default to last 30 days if fromStr := r.URL.Query().Get("from"); fromStr != "" { if parsed, err := time.Parse("2006-01-02", fromStr); err == nil { from = parsed } } if toStr := r.URL.Query().Get("to"); toStr != "" { if parsed, err := time.Parse("2006-01-02", toStr); err == nil { to = parsed } } return } func respondJSON(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(data); err != nil { logger.Error().Err(err).Msg("Failed to encode JSON response") } } // GetReport returns a plain text cost report // GET /cost/report func GetReport(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } to := time.Now() from := to.AddDate(0, -1, 0) // Last month summary, err := services.GetFleetCostSummary(from, to, 10) if err != nil { logger.Error().Err(err).Msg("Failed to get report data") http.Error(w, "Failed to get report data", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/plain") report := ` ╔══════════════════════════════════════════════════════════════════╗ ║ COST SERVICE REPORT ║ ╠══════════════════════════════════════════════════════════════════╣ ║ Period: %s to %s ╠══════════════════════════════════════════════════════════════════╣ ║ FLEET OVERVIEW ║ ║ ─────────────────────────────────────────────────────────────── ║ ║ Active Vehicles: %d ║ Cloud Cost: $%.2f ║ On-Prem Cost: $%.2f ║ Savings: $%.2f (%.1f%%) ╠══════════════════════════════════════════════════════════════════╣ ║ RESOURCE USAGE MODEL ║ ║ ─────────────────────────────────────────────────────────────── ║ ║ Platform Base: %.0f cores / %.0f GB RAM (fixed) ║ Per-VIN Marginal: %.0f millicores / %.0f MB RAM ║ Total Fleet: %.1f cores / %.1f GB RAM ╠══════════════════════════════════════════════════════════════════╣ ║ COST FORMULA ║ ║ ─────────────────────────────────────────────────────────────── ║ ║ (Platform Base) + (Per-VIN × %d VINs) + Managed Services ╠══════════════════════════════════════════════════════════════════╣ ║ COST RATES ║ ║ ─────────────────────────────────────────────────────────────── ║ ║ Cloud: CPU $%.2f/core-hr Memory $%.3f/GB-hr ║ On-Prem: CPU $%.2f/core-hr Memory $%.3f/GB-hr ║ Base Infra: Cloud $%.2f/15min On-Prem $%.2f/15min ╠══════════════════════════════════════════════════════════════════╣ ║ ANNUAL PROJECTION (based on current usage) ║ ║ ─────────────────────────────────────────────────────────────── ║ ║ Cloud Annual: $%.2f ║ On-Prem Annual: $%.2f ║ Annual Savings: $%.2f ╚══════════════════════════════════════════════════════════════════╝ ` annualCloud := summary.TotalCloudCost * 12 annualOnprem := summary.TotalOnpremCost * 12 annualSavings := annualCloud - annualOnprem // Calculate total fleet resources totalCPU := services.PlatformBaseCPUCores + (services.PerVinCPUCores * float64(summary.VehicleCount)) totalRAM := services.PlatformBaseMemoryGB + (services.PerVinMemoryGB * float64(summary.VehicleCount)) fmt.Fprintf(w, report, from.Format("2006-01-02"), to.Format("2006-01-02"), summary.VehicleCount, summary.TotalCloudCost, summary.TotalOnpremCost, summary.TotalSavings, summary.SavingsPercent, services.PlatformBaseCPUCores, services.PlatformBaseMemoryGB, services.PerVinCPUCores*1000, services.PerVinMemoryGB*1024, totalCPU, totalRAM, summary.VehicleCount, services.CloudCPUPerCoreHour, services.CloudMemoryPerGBHour, services.OnpremCPUPerCoreHour, services.OnpremMemoryPerGBHour, services.BaseInfraCloudCost, services.BaseInfraOnpremCost, annualCloud, annualOnprem, annualSavings, ) // Add top cost VINs if any if len(summary.TopCostVins) > 0 { fmt.Fprintf(w, "\nTOP COST VEHICLES:\n") fmt.Fprintf(w, "%-20s %10s %10s %12s %12s %10s\n", "VIN", "CPU (mc)", "RAM (MB)", "Cloud $", "On-Prem $", "Savings %") fmt.Fprintf(w, "%-20s %10s %10s %12s %12s %10s\n", "───────────────────", "────────", "────────", "──────────", "──────────", "────────") for _, v := range summary.TopCostVins { fmt.Fprintf(w, "%-20s %10.0f %10.0f %12.2f %12.2f %9.1f%%\n", truncateVIN(v.VIN), v.AvgCPUCores*1000, v.AvgMemoryGB*1024, v.TotalCloudCost, v.TotalOnpremCost, v.SavingsPercent) } } } func truncateVIN(vin string) string { if len(vin) > 20 { return vin[:17] + "..." } return vin }