308 lines
9.4 KiB
Go
308 lines
9.4 KiB
Go
package handlers
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"gestion/db"
|
||
"gestion/services"
|
||
"log"
|
||
"net/http"
|
||
"strconv"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// returnStaleOrUnavailable retourne le cache périmé avec le temps restant recalculé,
|
||
// ou {eta_available: false, message: "Aucune heure disponible"} si le cache est absent ou expiré.
|
||
func returnStaleOrUnavailable(commandID int, status string, etaData map[string]string) gin.H {
|
||
if len(etaData) > 0 {
|
||
if updatedAtStr, ok := etaData["updated_at"]; ok {
|
||
var updatedAt int64
|
||
fmt.Sscanf(updatedAtStr, "%d", &updatedAt)
|
||
var etaMin int64
|
||
if etaStr, ok2 := etaData["eta_minutes"]; ok2 {
|
||
fmt.Sscanf(etaStr, "%d", &etaMin)
|
||
}
|
||
elapsed := int64(time.Since(time.Unix(updatedAt, 0)).Minutes())
|
||
remaining := etaMin - elapsed
|
||
if remaining > 0 {
|
||
arrival := time.Now().Add(time.Duration(remaining) * time.Minute)
|
||
log.Printf("📦 [ETA] Cache périmé utilisé - %d min restantes", remaining)
|
||
return gin.H{
|
||
"success": true,
|
||
"command_id": commandID,
|
||
"status": status,
|
||
"eta_minutes": remaining,
|
||
"estimated_arrival": arrival.Format("15:04"),
|
||
"eta_available": true,
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return gin.H{
|
||
"success": true,
|
||
"command_id": commandID,
|
||
"status": status,
|
||
"eta_available": false,
|
||
"message": "Aucune heure disponible",
|
||
}
|
||
}
|
||
|
||
func GetOrderETA(c *gin.Context) {
|
||
database := c.MustGet("database").(*db.Database)
|
||
geoService := c.MustGet("geoService").(*services.GeoService)
|
||
|
||
// 1️⃣ AUTHENTIFICATION
|
||
username, exists := c.Get("username")
|
||
if !exists {
|
||
log.Printf("❌ [ETA] Non authentifié")
|
||
c.JSON(http.StatusUnauthorized, gin.H{
|
||
"success": false,
|
||
"error": "Utilisateur non authentifié",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 2️⃣ RÉCUPÉRER L'ID DE LA COMMANDE
|
||
commandID, err := strconv.Atoi(c.Param("id"))
|
||
if err != nil {
|
||
log.Printf("❌ [ETA] ID invalide: %v", err)
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"success": false,
|
||
"error": "ID de commande invalide",
|
||
})
|
||
return
|
||
}
|
||
|
||
command, err := database.GetCommandByID(commandID)
|
||
if err != nil {
|
||
log.Printf("❌ [ETA] Commande %d non trouvée", commandID)
|
||
c.JSON(http.StatusNotFound, gin.H{
|
||
"success": false,
|
||
"error": "Commande non trouvée",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 4️⃣ VÉRIFIER LES DROITS D'ACCÈS
|
||
cmdUsername, _ := command["username"].(string)
|
||
userRole := c.GetString("role")
|
||
|
||
if userRole != "admin" {
|
||
if userRole == "client" && cmdUsername != username.(string) {
|
||
log.Printf("❌ [ETA] Accès refusé - commande appartient à %s", cmdUsername)
|
||
c.JSON(http.StatusForbidden, gin.H{
|
||
"success": false,
|
||
"error": "Vous n'avez pas accès à cette commande",
|
||
})
|
||
return
|
||
}
|
||
|
||
if userRole == "livreur" {
|
||
livreurAssign, _ := command["livreur_assign"].(string)
|
||
if livreurAssign != username.(string) {
|
||
log.Printf("❌ [ETA] Accès refusé - livreur non assigné")
|
||
c.JSON(http.StatusForbidden, gin.H{
|
||
"success": false,
|
||
"error": "Vous n'avez pas accès à cette commande",
|
||
})
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5️⃣ VÉRIFIER LE STATUT DE LA COMMANDE
|
||
cmdStatus, _ := command["status"].(string)
|
||
|
||
// ✅ CORRECTION: Vérifier si commande terminée
|
||
if cmdStatus == "livre" || cmdStatus == "delivered" || cmdStatus == "approved" {
|
||
log.Printf("ℹ️ [ETA] Commande déjà %s - pas d'ETA applicable", cmdStatus)
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"command_id": commandID,
|
||
"status": cmdStatus,
|
||
"message": "La commande a déjà été livrée",
|
||
"eta_available": false,
|
||
"estimated_arrival": "Livraison complétée",
|
||
})
|
||
return
|
||
}
|
||
|
||
// Pour pending/assigned: pas encore de position livreur disponible
|
||
if cmdStatus == "pending" || cmdStatus == "assigned" {
|
||
log.Printf("⏳ [ETA] Commande %s - pas d'ETA disponible", cmdStatus)
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"command_id": commandID,
|
||
"status": cmdStatus,
|
||
"eta_available": false,
|
||
"message": "En attente de démarrage de la livraison",
|
||
})
|
||
return
|
||
}
|
||
|
||
// Pour arrived: livreur sur place, ETA non pertinent
|
||
if cmdStatus == "arrived" {
|
||
log.Printf("ℹ️ [ETA] Commande arrived - livreur déjà sur place")
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"command_id": commandID,
|
||
"status": cmdStatus,
|
||
"eta_available": false,
|
||
"message": "Le livreur est arrivé à destination",
|
||
})
|
||
return
|
||
}
|
||
// Pour en_route: calcul ETA réel via position du livreur
|
||
|
||
// 6️⃣ VÉRIFIER LE CACHE REDIS POUR ETA
|
||
etaKey := fmt.Sprintf("command:eta:%d", commandID)
|
||
etaData, err := db.Redis.HGetAll(db.RedisCtx, etaKey).Result()
|
||
|
||
if err == nil && len(etaData) > 0 {
|
||
// ETA existe, vérifier s'il est récent
|
||
if updatedAtStr, ok := etaData["updated_at"]; ok {
|
||
var updatedAt int64
|
||
fmt.Sscanf(updatedAtStr, "%d", &updatedAt)
|
||
|
||
timeSinceUpdate := time.Since(time.Unix(updatedAt, 0))
|
||
if timeSinceUpdate < 30*time.Second {
|
||
// Cache valide
|
||
var etaMinutes int64
|
||
if etaStr, ok := etaData["eta_minutes"]; ok {
|
||
fmt.Sscanf(etaStr, "%d", &etaMinutes)
|
||
}
|
||
|
||
var arrivalTime int64
|
||
if arrivalStr, ok := etaData["arrival_time"]; ok {
|
||
fmt.Sscanf(arrivalStr, "%d", &arrivalTime)
|
||
}
|
||
|
||
log.Printf("✅ [ETA] Cache hit - ETA: %d min", etaMinutes)
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"command_id": commandID,
|
||
"status": cmdStatus,
|
||
"eta_minutes": etaMinutes,
|
||
"estimated_arrival": time.Unix(arrivalTime, 0).Format("15:04"),
|
||
"eta_available": true,
|
||
"with_traffic": true,
|
||
"livreur_assign": command["livreur_assign"],
|
||
"delivery_address": command["adresse"],
|
||
})
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// 7️⃣ Pas de cache valide - Recalculer l'ETA
|
||
log.Printf("🔄 [ETA] Cache miss ou expiré - Recalcul de l'ETA...")
|
||
|
||
// Récupérer coordonnées destination
|
||
var destLat, destLon float64
|
||
|
||
destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
|
||
destData, err := db.Redis.Get(db.RedisCtx, destCacheKey).Result()
|
||
if err == nil && destData != "" {
|
||
var coords struct {
|
||
Lat float64 `json:"lat"`
|
||
Lon float64 `json:"lon"`
|
||
}
|
||
if err := json.Unmarshal([]byte(destData), &coords); err == nil && coords.Lat != 0 && coords.Lon != 0 {
|
||
destLat = coords.Lat
|
||
destLon = coords.Lon
|
||
log.Printf("📍 Destination depuis cache Redis: (%.6f, %.6f)", destLat, destLon)
|
||
}
|
||
}
|
||
|
||
if destLat == 0 || destLon == 0 {
|
||
if dLat, okLat := command["dest_latitude"].(float64); okLat && dLat != 0 {
|
||
destLat = dLat
|
||
}
|
||
if dLon, okLon := command["dest_longitude"].(float64); okLon && dLon != 0 {
|
||
destLon = dLon
|
||
}
|
||
}
|
||
|
||
if destLat == 0 || destLon == 0 {
|
||
log.Printf("⚠️ [ETA] Coordonnées destination manquantes - retour cache périmé ou message")
|
||
c.JSON(http.StatusOK, returnStaleOrUnavailable(commandID, cmdStatus, etaData))
|
||
return
|
||
}
|
||
|
||
// Récupérer position du livreur
|
||
livreurAssign, _ := command["livreur_assign"].(string)
|
||
if livreurAssign == "" {
|
||
log.Printf("⚠️ [ETA] Aucun livreur assigné")
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"command_id": commandID,
|
||
"status": cmdStatus,
|
||
"eta_available": false,
|
||
"message": "Aucune heure disponible",
|
||
})
|
||
return
|
||
}
|
||
|
||
toCoords := services.Coordinates{Latitude: destLat, Longitude: destLon}
|
||
|
||
// Cas 1 : GPS livreur disponible
|
||
livreurLocation, gpsErr := geoService.GetDeliveryPersonLocation(livreurAssign)
|
||
if gpsErr != nil {
|
||
// Cas 2 : GPS absent → dernière adresse de livraison
|
||
lastLat, lastLon, lastErr := database.GetLastDeliveryCoords(livreurAssign)
|
||
if lastErr != nil || lastLat == 0 {
|
||
// Cas 3 : Aucune position → cache périmé ou message
|
||
log.Printf("⚠️ [ETA] Aucune position disponible pour %s", livreurAssign)
|
||
c.JSON(http.StatusOK, returnStaleOrUnavailable(commandID, cmdStatus, etaData))
|
||
return
|
||
}
|
||
livreurLocation = &services.Coordinates{Latitude: lastLat, Longitude: lastLon}
|
||
log.Printf("📍 [ETA] Position depuis dernière livraison: (%.6f, %.6f)", lastLat, lastLon)
|
||
}
|
||
|
||
// Calculer ETA avec TomTom
|
||
log.Printf("🛣️ [ETA] Calcul TomTom: (%.6f, %.6f) -> (%.6f, %.6f)",
|
||
livreurLocation.Latitude, livreurLocation.Longitude, toCoords.Latitude, toCoords.Longitude)
|
||
|
||
etaMinutes, distanceKm, err := services.GetETAWithTraffic(*livreurLocation, toCoords)
|
||
if err != nil {
|
||
log.Printf("⚠️ [ETA] TomTom failed, fallback local: %v", err)
|
||
distanceKm = services.CalculateDistance(*livreurLocation, toCoords)
|
||
etaMinutes = services.CalculateETA(distanceKm)
|
||
}
|
||
|
||
// Sauvegarder en cache
|
||
now := time.Now()
|
||
arrivalTime := now.Add(time.Duration(etaMinutes) * time.Minute)
|
||
|
||
etaCache := map[string]interface{}{
|
||
"command_id": commandID,
|
||
"eta_minutes": etaMinutes,
|
||
"updated_at": now.Unix(),
|
||
"arrival_time": arrivalTime.Unix(),
|
||
"distance_km": distanceKm,
|
||
"with_traffic": err == nil,
|
||
}
|
||
|
||
db.Redis.HSet(db.RedisCtx, etaKey, etaCache)
|
||
db.Redis.Expire(db.RedisCtx, etaKey, 4*time.Hour)
|
||
|
||
log.Printf("✅ [ETA] SUCCESS - ETA: %d min, arrivée: %s", etaMinutes, arrivalTime.Format("15:04"))
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"command_id": commandID,
|
||
"status": cmdStatus,
|
||
"eta_minutes": etaMinutes,
|
||
"distance_km": fmt.Sprintf("%.2f", distanceKm),
|
||
"estimated_arrival": arrivalTime.Format("15:04"),
|
||
"eta_available": true,
|
||
"with_traffic": err == nil,
|
||
"livreur_assign": livreurAssign,
|
||
"delivery_address": command["adresse"],
|
||
})
|
||
}
|