Files
projet_gestion_commande/backend/gestion/handlers/validation_deleviry.go
T
2026-03-11 19:57:34 +01:00

402 lines
12 KiB
Go

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: VALIDATION GPS (CRITIQUE)
destLat, _ := command["dest_latitude"].(float64)
destLon, _ := command["dest_longitude"].(float64)
distance := calculateDistance(req.Latitude, req.Longitude, destLat, destLon)
log.Printf("📍 [VALIDATE_LIVREUR] Distance: %.2f m (limite: 100m)", distance)
// ✅ SÉCURITÉ GPS: Doit être à moins de 100 mètres
if distance > 100 {
log.Printf("❌ [VALIDATE_LIVREUR] Trop loin! Distance: %.2f m", distance)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Vous êtes trop loin de la destination",
"required_distance": 100,
"current_distance": fmt.Sprintf("%.2f", distance),
"unit": "meters",
"destination_coords": gin.H{
"latitude": destLat,
"longitude": destLon,
},
"your_coords": gin.H{
"latitude": req.Latitude,
"longitude": req.Longitude,
},
})
return
}
log.Printf("✅ [VALIDATE_LIVREUR] GPS VALIDÉ - Distance: %.2f m < 100m", distance)
// ✅ É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 - Distance: %.2f m", distance),
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",
"distance": fmt.Sprintf("%.2f", distance),
"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)
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
}