188 lines
5.0 KiB
Go
188 lines
5.0 KiB
Go
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")
|
|
}
|
|
}
|