Add cost service for per-VIN cost estimation
This commit is contained in:
187
services/cost/handlers/handlers.go
Normal file
187
services/cost/handlers/handlers.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user