409 lines
12 KiB
Go
409 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",
|
|
})
|
|
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",
|
|
})
|
|
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",
|
|
})
|
|
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",
|
|
})
|
|
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 != "" {
|
|
var msg string
|
|
etaMinutes := 0
|
|
if etaData, err := database.GetCommandETA(commandID); err == nil {
|
|
if v, ok := etaData["total_eta_minutes"]; ok {
|
|
if n, err2 := strconv.Atoi(v); err2 == nil && n > 0 {
|
|
etaMinutes = n
|
|
}
|
|
}
|
|
}
|
|
if etaMinutes == 0 && req.Latitude != 0 && req.Longitude != 0 {
|
|
destLat, _ := command["dest_latitude"].(float64)
|
|
destLon, _ := command["dest_longitude"].(float64)
|
|
if destLat != 0 && destLon != 0 {
|
|
etaMinutes = database.CalculateETAForDeliveryman(usernameStr, destLat, destLon)
|
|
}
|
|
}
|
|
if etaMinutes > 0 {
|
|
var etaStr string
|
|
if etaMinutes >= 60 {
|
|
h := etaMinutes / 60
|
|
m := etaMinutes % 60
|
|
if m > 0 {
|
|
etaStr = fmt.Sprintf("%dh%02d", h, m)
|
|
} else {
|
|
etaStr = fmt.Sprintf("%dh", h)
|
|
}
|
|
} else {
|
|
etaStr = fmt.Sprintf("%d min", etaMinutes)
|
|
}
|
|
msg = fmt.Sprintf("Un livreur vient de prendre en charge ta commande #%d, il est en route (~%s)", database.GetClientOrderID(commandID), etaStr)
|
|
} else {
|
|
msg = fmt.Sprintf("Un livreur vient de prendre en charge ta commande #%d, il est en route.", database.GetClientOrderID(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
|
|
}
|