package handlers import ( "fmt" "gestion/db" "log" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" ) var ( cancelRateLimitMap = make(map[string][]time.Time) cancelMaxRequests = 5 // Max 5 annulations cancelTimeWindow = time.Hour // Par heure ) func checkCancelRateLimit(key string) bool { now := time.Now() if timestamps, exists := cancelRateLimitMap[key]; exists { var validTimestamps []time.Time for _, ts := range timestamps { if now.Sub(ts) < cancelTimeWindow { validTimestamps = append(validTimestamps, ts) } } cancelRateLimitMap[key] = validTimestamps if len(validTimestamps) >= cancelMaxRequests { return false } } cancelRateLimitMap[key] = append(cancelRateLimitMap[key], now) return true } func validateReason(reason string) string { if len(reason) > 500 { reason = reason[:500] } reason = strings.Map(func(r rune) rune { if r < 32 || r == 127 { return -1 } return r }, reason) if strings.TrimSpace(reason) == "" { return "Annulation par le client" } return reason } func CancelCommandByClient(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, err := safeGetUsername(c) if err != nil || c.GetString("role") != "client" { log.Printf("❌ [CANCEL_CLIENT] Accès refusé") c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux clients"}) return } rateLimitKey := fmt.Sprintf("cancel:%s", username) if !checkCancelRateLimit(rateLimitKey) { log.Printf("⚠️ [CANCEL_CLIENT] Rate limit dépassé pour %s", username) c.JSON(http.StatusTooManyRequests, gin.H{ "error": "Trop d'annulations récentes", "message": "Veuillez attendre avant de réessayer", }) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil || commandID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } var req struct { Reason string `json:"reason"` Force bool `json:"force"` } if err := c.ShouldBindJSON(&req); err != nil { req.Reason = "Annulation par le client" req.Force = false } req.Reason = validateReason(req.Reason) penalty, err := database.CancelCommandAtomic(commandID, username, req.Reason, req.Force) if err != nil { log.Printf("❌ [CANCEL_CLIENT] Erreur: %v", err) if err.Error() == "confirmation requise" { command, errCmd := database.GetCommandByID(commandID) if errCmd != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) return } livreurAssign, _ := command["livreur_assign"].(string) currentStatus, _ := command["status"].(string) hasETA := false if livreurAssign != "" { hasETA = database.CheckCommandETAExistsAndValid(commandID) } nextPenalty, _ := database.CalculateCancellationPenalty(username) cancelCount, _ := database.GetClientCancellationsCount(username) response := gin.H{ "success": false, "warning": true, "command_info": gin.H{ "command_id": commandID, "status": currentStatus, "livreur": livreurAssign, }, } if hasETA { log.Printf("⚠️ [CANCEL_CLIENT] Annulation tardive avec ETA - Status: %s, Livreur: %s", currentStatus, livreurAssign) response["message"] = "⚠️ Un livreur est en route vers votre adresse (ETA définie)" response["details"] = gin.H{ "livreur": livreurAssign, "status": currentStatus, "has_eta": true, } response["penalty_warning"] = gin.H{ "will_apply": true, "penalty_amount": nextPenalty, "current_violations": cancelCount, "message": fmt.Sprintf( "⚠️ ATTENTION: Une amende de %d sera appliquée pour annulation tardive", nextPenalty, ), "scale": gin.H{ "1st_cancel": 20, "2nd_cancel": 50, "3rd_cancel": 100, "4th+_cancel": 150, }, } } else { log.Printf("ℹ️ [CANCEL_CLIENT] Livreur assigné mais pas d'ETA - Annulation sans pénalité") response["message"] = "ℹ️ Un livreur est assigné mais n'est pas encore en route" response["details"] = gin.H{ "livreur": livreurAssign, "status": currentStatus, "has_eta": false, } response["penalty_warning"] = gin.H{ "will_apply": false, "message": "Aucune pénalité ne sera appliquée car le livreur n'est pas encore en route", "points_safe": true, } } // ✅ AJOUTER LES INSTRUCTIONS D'ACTION response["action_required"] = "Pour confirmer l'annulation, renvoyez la même requête avec 'force': true" response["example"] = gin.H{ "reason": req.Reason, "force": true, } // ✅ AJOUTER POSITION DANS LA QUEUE (si disponible) if livreurAssign != "" { position, posErr := database.GetCommandPositionInQueue(livreurAssign, commandID) if posErr == nil && position > 0 { response["details"].(gin.H)["position_in_queue"] = position queueInfo, _ := database.GetDeliverymanQueueInfo(livreurAssign) if queueInfo != nil { response["details"].(gin.H)["queue_info"] = queueInfo } } } log.Printf("⚠️ [CANCEL_CLIENT] Confirmation requise pour cmd %d - hasETA=%v, penalty=%d", commandID, hasETA, nextPenalty) c.JSON(http.StatusConflict, response) return } switch err.Error() { case "commande non trouvée": c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) case "commande ne vous appartient pas": c.JSON(http.StatusForbidden, gin.H{"error": "Cette commande ne vous appartient pas"}) case "impossible d'annuler": c.JSON(http.StatusBadRequest, gin.H{"error": "Cette commande ne peut plus être annulée"}) default: c.JSON(http.StatusBadRequest, gin.H{"error": "Impossible d'annuler la commande"}) } return } log.Printf("✅ [CANCEL_CLIENT] Commande %d annulée", commandID) response := gin.H{ "success": true, "message": "Commande annulée avec succès", "command_id": commandID, "new_status": "cancelled", } if penalty > 0 { response["penalty"] = gin.H{ "penalty_amount": penalty, "warning": "Une amende a été appliquée pour annulation tardive", } } else { response["info"] = "Aucune pénalité appliquée" } c.JSON(http.StatusOK, response) } func GetMyCancellationHistory(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, err := safeGetUsername(c) if err != nil || c.GetString("role") != "client" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux clients"}) return } log.Printf("📊 [CANCEL_HISTORY] Client %s - Consultation historique", username) history, err := database.GetClientCancellationHistory(username) if err != nil { log.Printf("❌ [CANCEL_HISTORY] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur récupération historique", }) return } var penaltyResult struct { Amende int `gorm:"column:amende"` } database.GDB.Raw(`SELECT COALESCE(amende, 0) as amende FROM clients WHERE username = ?`, username).Scan(&penaltyResult) totalPenalty := penaltyResult.Amende c.JSON(http.StatusOK, gin.H{ "success": true, "data": gin.H{ "username": username, "history": history, "total_penalties": totalPenalty, }, }) } func GetAllCancelledOrders(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) return } userRole := c.GetString("role") if userRole != "admin" && userRole != "cabine" { log.Printf("❌ [GET_ALL_CANCELLED] Accès refusé - role=%s", userRole) c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux administrateurs"}) return } filterUsername := c.Query("username") if len(filterUsername) > 100 { c.JSON(http.StatusBadRequest, gin.H{"error": "Username trop long"}) return } limitStr := c.DefaultQuery("limit", "50") limit, err := strconv.Atoi(limitStr) if err != nil || limit < 1 { limit = 50 } if limit > 500 { limit = 500 } log.Printf("📋 [GET_ALL_CANCELLED] %s (%s) récupère %d commandes", username, userRole, limit) cancelledOrders, err := database.GetCancelledCommands(filterUsername, limit) if err != nil { log.Printf("❌ [GET_ALL_CANCELLED] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la récupération", }) return } var enrichedOrders []map[string]any for _, order := range cancelledOrders { orderID, _ := strconv.Atoi(fmt.Sprintf("%v", order["id"])) items, _ := database.GetCommandItems(orderID) logs, _ := database.GetCommandLogs(orderID) var cancellationLog map[string]any for _, logEntry := range logs { status, _ := logEntry["status"].(string) if status == "cancelled" { cancellationLog = logEntry break } } cancelReason, _ := order["cancel_reason"].(string) enrichedOrder := map[string]any{ "id": order["id"], "username": order["username"], "total_prix": order["total_prix"], "created_at": order["created_at"], "updated_at": order["updated_at"], "items_count": len(items), "cancel_reason": cancelReason, } if cancellationLog != nil { enrichedOrder["cancellation"] = gin.H{ "cancelled_at": cancellationLog["created_at"], "cancelled_by": cancellationLog["author"], "reason": cancelReason, } } enrichedOrders = append(enrichedOrders, enrichedOrder) } log.Printf("✅ [GET_ALL_CANCELLED] %d commandes récupérées", len(enrichedOrders)) c.JSON(http.StatusOK, gin.H{ "success": true, "data": gin.H{ "cancelled_orders": enrichedOrders, "count": len(enrichedOrders), }, }) } func DeleteCommandByCabine(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) return } userRole := c.GetString("role") if userRole != "cabine" && userRole != "admin" { log.Printf("❌ [DELETE_COMMAND] Accès refusé - role=%s", userRole) c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux cabines"}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil || commandID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } log.Printf("🗑️ [DELETE_COMMAND] %s (%s) supprime cmd %d", username, userRole, commandID) err = database.DeleteCommandAtomic(commandID, username, userRole) if err != nil { log.Printf("❌ [DELETE_COMMAND] Erreur: %v", err) if err.Error() == "commande non trouvée" { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la suppression"}) } return } log.Printf("✅ [DELETE_COMMAND] Commande %d supprimée", commandID) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Commande supprimée définitivement", "deleted_by": gin.H{ "username": username, "role": userRole, }, }) }