Files
projet_gestion_commande/backend/gestion/handlers/cancel_command.go
T
2026-06-14 17:50:35 +02:00

412 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
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
}
// ✅ 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,
},
})
}