package handlers import ( "encoding/json" "fmt" "gestion/db" "gestion/services" "log" "net/http" "strconv" "github.com/gin-gonic/gin" ) // ============================================ // CONSTANTES DE CONFIGURATION // ============================================ const ( // Distance maximale en mètres pour valider une livraison MAX_DELIVERY_VALIDATION_DISTANCE_METERS = 100 // 100 mètres // Distance maximale en kilomètres MAX_DELIVERY_VALIDATION_DISTANCE_KM = 0.1 // 100 mètres = 0.1 km ) // ============================================ // 1️⃣ VALIDATION LIVRAISON PAR LE LIVREUR (AVEC VÉRIFICATION GPS) // ============================================ func ValidateDeliveryByLivreur(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ SÉCURITÉ: Livreur seulement username, exists := c.Get("username") if !exists || c.GetString("role") != "livreur" { log.Printf("❌ [VALIDATE_LIVREUR] Accès refusé") c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs"}) return } usernameStr := username.(string) commandID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } var req struct { Latitude float64 `json:"latitude" binding:"required"` Longitude float64 `json:"longitude" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { log.Printf("❌ [VALIDATE_LIVREUR] Erreur JSON: %v", err) c.JSON(http.StatusBadRequest, gin.H{ "error": "Coordonnées GPS requises", "details": err.Error(), }) return } log.Printf("📍 [VALIDATE_LIVREUR] Livreur %s valide cmd %d avec GPS: (%.6f, %.6f)", usernameStr, commandID, req.Latitude, req.Longitude) // ✅ ÉTAPE 1: Récupérer la commande command, err := database.GetCommandByID(commandID) if err != nil { log.Printf("❌ [VALIDATE_LIVREUR] Commande non trouvée") c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) return } // ✅ ÉTAPE 2: VÉRIFIER PROPRIÉTÉ livreurAssign, _ := command["livreur_assign"].(string) if livreurAssign != usernameStr { log.Printf("❌ [VALIDATE_LIVREUR] ⚠️ TENTATIVE D'ACCÈS NON AUTORISÉ!") c.JSON(http.StatusForbidden, gin.H{ "error": "Cette commande ne vous est pas assignée", }) return } // ÉTAPE 3: Coordonnées GPS reçues et valides log.Printf("📍 [VALIDATE_LIVREUR] GPS reçu: (%.6f, %.6f)", req.Latitude, req.Longitude) // ÉTAPE 4: Sauvegarder les coordonnées du livreur _, err = database.Exec( "UPDATE commandes SET livreur_latitude = $1, livreur_longitude = $2 WHERE id = $3", req.Latitude, req.Longitude, commandID, ) if err != nil { log.Printf("⚠️ [VALIDATE_LIVREUR] Erreur sauvegarde GPS: %v", err) } // ✅ ÉTAPE 5: Marquer la livraison comme "livre" if err := database.UpdateCommandStatus(commandID, "livre"); err != nil { log.Printf("❌ [VALIDATE_LIVREUR] Erreur update statut: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur validation", "details": err.Error(), }) return } // ✅ ÉTAPE 6: Ajouter un log database.AddCommandLog(commandID, "livre", fmt.Sprintf("Livraison confirmée par livreur - GPS: (%.6f, %.6f)", req.Latitude, req.Longitude), usernameStr) // ✅ ÉTAPE 7: Optimiser la queue log.Printf("📦 [VALIDATE_LIVREUR] Optimisation queue de %s...", usernameStr) err = database.CompleteDeliveryAndProcessNext(usernameStr, commandID) if err != nil { log.Printf("⚠️ [VALIDATE_LIVREUR] Erreur optimisation: %v", err) } log.Printf("✅ [VALIDATE_LIVREUR] Commande %d validée et marquée 'livre'", commandID) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Livraison validée avec succès", "command_id": commandID, "new_status": "livre", "gps_verified": true, }) } // ============================================ // 2️⃣ VÉRIFIER SI LE LIVREUR PEUT VALIDER (SANS VALIDER) // ============================================ // CheckDeliveryValidationEligibility vérifie si le livreur peut valider une livraison // GET /api/v1/deliveries/:id/can-validate func CheckDeliveryValidationEligibility(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, exists := c.Get("username") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"}) return } userRole := c.GetString("role") if userRole != "livreur" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs"}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } command, err := database.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) return } // Vérifier l'assignation livreurAssign, _ := command["livreur_assign"].(string) if livreurAssign != username.(string) { c.JSON(http.StatusOK, gin.H{ "can_validate": false, "reason": "Commande non assignée à vous", }) return } // Récupérer la position du livreur livreurLat, livreurLon, err := database.GetDeliveryPersonLocation(username.(string)) if err != nil { c.JSON(http.StatusOK, gin.H{ "can_validate": false, "reason": "Position GPS non disponible", "action": "Mettez à jour votre position GPS", }) return } // Récupérer les coordonnées de destination (même priorité que ValidateDeliveryByLivreur) var destLat, destLon float64 var coordsSource string // ✅ PRIORITÉ 1: Cache Redis destCacheKey := fmt.Sprintf("command:destination:%d", commandID) destData, redisErr := db.Redis.Get(db.RedisCtx, destCacheKey).Result() if redisErr == 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 coordsSource = "REDIS" log.Printf("📍 [CAN-VALIDATE] Coords depuis Redis: (%.6f, %.6f)", destLat, destLon) } } // ✅ PRIORITÉ 2: DB if coordsSource == "" { if dLat, ok := getFloatFromMap(command, "dest_latitude"); ok && dLat != 0 { destLat = dLat } if dLon, ok := getFloatFromMap(command, "dest_longitude"); ok && dLon != 0 { destLon = dLon } if destLat != 0 && destLon != 0 { coordsSource = "DB" } } // ✅ PRIORITÉ 3: Géocodage if coordsSource == "" { geoService := c.MustGet("geoService").(*services.GeoService) address, _ := command["adresse"].(string) if address != "" && address != "Adresse non spécifiée" { location, err := geoService.GeocodeAddress(address) if err == nil { destLat = location.Latitude destLon = location.Longitude coordsSource = "GEOCODING" } } } if destLat == 0 || destLon == 0 { c.JSON(http.StatusOK, gin.H{ "can_validate": false, "reason": "Coordonnées de destination non disponibles", }) return } // Calculer la distance distance := services.CalculateDistance( services.Coordinates{Latitude: livreurLat, Longitude: livreurLon}, services.Coordinates{Latitude: destLat, Longitude: destLon}, ) distanceMeters := distance * 1000 canValidate := distance <= MAX_DELIVERY_VALIDATION_DISTANCE_KM c.JSON(http.StatusOK, gin.H{ "can_validate": canValidate, "your_position": gin.H{ "latitude": livreurLat, "longitude": livreurLon, }, "destination": gin.H{ "latitude": destLat, "longitude": destLon, "address": command["adresse"], "source": coordsSource, }, "distance_meters": int(distanceMeters), "max_allowed_meters": MAX_DELIVERY_VALIDATION_DISTANCE_METERS, "remaining_meters": maxInt(0, int(distanceMeters)-MAX_DELIVERY_VALIDATION_DISTANCE_METERS), "message": func() string { if canValidate { return "Vous pouvez valider cette livraison" } return fmt.Sprintf("Rapprochez-vous de %.0f mètres pour valider", distanceMeters-float64(MAX_DELIVERY_VALIDATION_DISTANCE_METERS)) }(), }) } // ============================================ // 3️⃣ DÉMARRER UNE LIVRAISON (PASSER EN IN_ROUTE) // ============================================ // StartDelivery permet au livreur de démarrer une livraison (passage en in_transit) // POST /api/v1/deliveries/:id/start func StartDelivery(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, exists := c.Get("username") if !exists || c.GetString("role") != "livreur" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs"}) return } usernameStr := username.(string) commandID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } var req struct { Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Coordonnées GPS requises", "details": err.Error(), }) return } log.Printf("🚗 [START] %s démarre livraison cmd %d", usernameStr, commandID) command, err := database.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) return } // Vérifier propriété livreurAssign, _ := command["livreur_assign"].(string) if livreurAssign != usernameStr { c.JSON(http.StatusForbidden, gin.H{ "error": "Cette commande ne vous est pas assignée", }) return } // Vérifier le statut actuel currentStatus, _ := command["status"].(string) if currentStatus != "assigned" { c.JSON(http.StatusBadRequest, gin.H{ "error": "Impossible de démarrer cette livraison", "current_status": currentStatus, "message": "La commande doit être en statut 'assigned'", }) return } // Mettre à jour le statut en "en_route" if err := database.UpdateCommandStatus(commandID, "en_route"); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur mise à jour statut", "details": err.Error(), }) return } // Mettre à jour la position du livreur database.UpdateLivreurPosition(usernameStr, req.Latitude, req.Longitude, "busy") // Mettre à jour le statut du livreur database.SetDeliveryPersonStatus(usernameStr, "busy", commandID) // Ajouter un log database.AddCommandLog(commandID, "en_route", fmt.Sprintf("Livraison démarrée par %s", usernameStr), usernameStr) // Notifier le client if clientUsername, _ := command["username"].(string); clientUsername != "" { msg := fmt.Sprintf("🛵 Votre commande #%d est en route !", commandID) database.NotifyClient(clientUsername, commandID, "en_route", msg) } log.Printf("✅ [START] Livraison %d démarrée", commandID) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Livraison démarrée", "command_id": commandID, "status": "en_route", }) } // ============================================ // HELPERS // ============================================ func maxInt(a, b int) int { if a > b { return a } return b }