422 lines
11 KiB
Go
422 lines
11 KiB
Go
// ============================================
|
||
// handlers/cancel_command_handler.go
|
||
// ANNULATION DE COMMANDES AVEC SANCTIONS ÉVOLUTIVES
|
||
// VERSION SÉCURISÉE - FIX ETA CHECK
|
||
// ============================================
|
||
|
||
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
|
||
}
|
||
|
||
// ✅ AUTRES ERREURS
|
||
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
|
||
}
|
||
|
||
// ============================================
|
||
// SUCCÈS
|
||
// ============================================
|
||
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)
|
||
}
|
||
|
||
// ============================================
|
||
// HISTORIQUE DES ANNULATIONS - VERSION SÉCURISÉE
|
||
// ============================================
|
||
|
||
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 totalPenalty int
|
||
penaltyQuery := `SELECT COALESCE(amende, 0) FROM clients WHERE username = $1`
|
||
database.QueryRow(penaltyQuery).Scan(&totalPenalty)
|
||
|
||
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
|
||
}
|
||
|
||
// ✅ ENRICHIR les données (sans exposer d'infos sensibles inutiles)
|
||
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,
|
||
},
|
||
})
|
||
}
|