// ============================================ // db/cancel_commands_db.go // FONCTIONS DB ATOMIQUES POUR L'ANNULATION // ============================================ package db import ( "fmt" "log" "slices" "gorm.io/gorm" ) func (d *Database) CancelCommandAtomic(commandID int, username, reason string, force bool) (int, error) { log.Printf("🔒 [CancelAtomic] START - cmd=%d, user=%s, force=%v", commandID, username, force) var penalty int err := d.GDB.Transaction(func(tx *gorm.DB) error { var cmdResult struct { Status string `gorm:"column:status"` Username string `gorm:"column:username"` LivreurAssign string `gorm:"column:livreur_assign"` } err := tx.Raw(` SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error if err != nil { return err } if cmdResult.Username == "" { return fmt.Errorf("commande non trouvĂ©e") } log.Printf("📋 [CancelAtomic] TrouvĂ©e - status=%s, owner=%s, livreur=%s", cmdResult.Status, cmdResult.Username, cmdResult.LivreurAssign) if cmdResult.Username != username { return fmt.Errorf("commande ne vous appartient pas") } nonCancellableStatuses := []string{"livre", "approved", "cancelled", "disabled"} if slices.Contains(nonCancellableStatuses, cmdResult.Status) { return fmt.Errorf("impossible d'annuler") } isLateCancel := false if cmdResult.LivreurAssign != "" { if cmdResult.Status == "en_route" || cmdResult.Status == "arrived" { isLateCancel = true log.Printf("⚠ [CancelAtomic] Annulation TARDIVE dĂ©tectĂ©e - Statut: %s", cmdResult.Status) } else if d.CheckCommandETAExistsAndValid(commandID) { isLateCancel = true log.Printf("⚠ [CancelAtomic] Annulation TARDIVE dĂ©tectĂ©e - ETA dĂ©finie") } else { log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Statut: %s, Pas d'ETA valide", cmdResult.Status) } } else { log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Aucun livreur assignĂ©") } if isLateCancel && !force { return fmt.Errorf("confirmation requise") } cancelMsg := reason if cancelMsg == "Annulation par le client" { cancelMsg = "" } result := tx.Exec(` UPDATE commandes SET status = 'cancelled', cancel_reason = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND status = ? AND username = ?`, cancelMsg, commandID, cmdResult.Status, username) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return fmt.Errorf("commande dĂ©jĂ  modifiĂ©e") } log.Printf("✅ [CancelAtomic] Statut mis Ă  jour: %s → cancelled", cmdResult.Status) if err := tx.Exec(` UPDATE products p SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP FROM command_items ci WHERE ci.command_id = ? AND ci.product_id = p.id`, commandID).Error; err != nil { log.Printf("⚠ [CancelAtomic] Erreur remboursement stock: %v", err) } else { log.Printf("✅ [CancelAtomic] Stock remboursĂ©") } if err := tx.Exec(` UPDATE clients SET cancellations_count = COALESCE(cancellations_count, 0) + 1, updated_at = CURRENT_TIMESTAMP WHERE username = ?`, username).Error; err != nil { log.Printf("⚠ [CancelAtomic] Erreur incrĂ©mentation count: %v", err) } if isLateCancel { log.Printf("⚠ [CancelAtomic] Annulation tardive confirmĂ©e - Application pĂ©nalitĂ©") penalty, _ = d.CalculateCancellationPenalty(username) if err := tx.Exec(` UPDATE clients SET amende = amende + ?, updated_at = CURRENT_TIMESTAMP WHERE username = ?`, penalty, username).Error; err != nil { log.Printf("❌ [CancelAtomic] Erreur pĂ©nalitĂ©: %v", err) } else { log.Printf("⚠ [CancelAtomic] PĂ©nalitĂ©: %d appliquĂ©e Ă  %s", penalty, username) } } if err := tx.Exec(` INSERT INTO command_logs (command_id, status, message, author, created_at) VALUES (?, 'cancelled', ?, ?, CURRENT_TIMESTAMP)`, commandID, fmt.Sprintf("AnnulĂ©e par %s - Raison: %s", username, reason), username, ).Error; err != nil { log.Printf("⚠ [CancelAtomic] Erreur log: %v", err) } // Nettoyage async aprĂšs commit livreur := cmdResult.LivreurAssign if livreur != "" { go func() { if err := d.CleanupCompletedCommandFromQueue(commandID, livreur); err != nil { log.Printf("⚠ [CancelAtomic] Erreur cleanup queue: %v", err) } }() } go func() { Redis.Del(RedisCtx, fmt.Sprintf("command:%d", commandID), fmt.Sprintf("client:%s", username), fmt.Sprintf("client:%s:commands", username), ) }() return nil }) if err != nil { return 0, err } log.Printf("🎉 [CancelAtomic] SUCCÈS - Commande %d annulĂ©e", commandID) return penalty, nil } // CheckCommandETAExistsAndValid vĂ©rifie si une ETA RÉELLE existe (> 0 minutes, non expirĂ©e) func (d *Database) CheckCommandETAExistsAndValid(commandID int) bool { etaKey := fmt.Sprintf("command:eta:%d", commandID) etaMinutesStr, err := Redis.Get(RedisCtx, etaKey).Result() if err != nil { log.Printf("⚠ [CheckETA] Pas d'ETA trouvĂ©e pour cmd %d", commandID) return false } var etaMinutes int _, err = fmt.Sscanf(etaMinutesStr, "%d", &etaMinutes) if err != nil || etaMinutes <= 0 { log.Printf("⚠ [CheckETA] ETA invalide pour cmd %d: %s", commandID, etaMinutesStr) return false } ttl, err := Redis.TTL(RedisCtx, etaKey).Result() if err != nil || ttl <= 0 { log.Printf("⚠ [CheckETA] ETA expirĂ©e pour cmd %d", commandID) return false } log.Printf("✅ [CheckETA] ETA valide trouvĂ©e pour cmd %d: %d min (TTL: %v)", commandID, etaMinutes, ttl) return true } func (d *Database) DeleteCommandAtomic(commandID int, deletedBy, role string) error { log.Printf("🔒 [DeleteAtomic] START - cmd=%d, by=%s (%s)", commandID, deletedBy, role) return d.GDB.Transaction(func(tx *gorm.DB) error { var cmdResult struct { Status string `gorm:"column:status"` Username string `gorm:"column:username"` LivreurAssign string `gorm:"column:livreur_assign"` } err := tx.Raw(` SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmdResult).Error if err != nil { return err } if cmdResult.Username == "" { return fmt.Errorf("commande non trouvĂ©e") } log.Printf("📋 [DeleteAtomic] TrouvĂ©e - status=%s, client=%s", cmdResult.Status, cmdResult.Username) if err := tx.Exec(` UPDATE products p SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP FROM command_items ci WHERE ci.command_id = ? AND ci.product_id = p.id`, commandID).Error; err != nil { log.Printf("⚠ [DeleteAtomic] Erreur remboursement: %v", err) } else { log.Printf("✅ [DeleteAtomic] Stock remboursĂ©") } tx.Exec(` INSERT INTO command_logs (command_id, status, message, author, created_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`, commandID, "deleted", fmt.Sprintf("SupprimĂ©e par %s (%s) - Ancien statut: %s", deletedBy, role, cmdResult.Status), deletedBy) if err := tx.Exec(`DELETE FROM command_items WHERE command_id = ?`, commandID).Error; err != nil { return err } result := tx.Exec(`DELETE FROM commandes WHERE id = ?`, commandID) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return fmt.Errorf("commande non trouvĂ©e") } log.Printf("✅ [DeleteAtomic] SupprimĂ©e de la DB") livreur := cmdResult.LivreurAssign cmdUsername := cmdResult.Username if livreur != "" { go d.RemoveCommandFromAllQueues(commandID, livreur) } go func() { Redis.Del(RedisCtx, fmt.Sprintf("command:%d", commandID), fmt.Sprintf("client:%s", cmdUsername), fmt.Sprintf("client:%s:commands", cmdUsername), ) }() return nil }) } func (d *Database) GetCommandPositionInQueue(livreurUsername string, commandID int) (int, error) { queueKey := fmt.Sprintf("queue:deliveryman:%s", livreurUsername) commandIDStr := fmt.Sprintf("%d", commandID) rank, err := Redis.ZRank(RedisCtx, queueKey, commandIDStr).Result() if err != nil { return 0, fmt.Errorf("commande non trouvĂ©e dans la queue") } return int(rank) + 1, nil } func (d *Database) GetCancelledCommands(username string, limit int) ([]map[string]any, error) { query := ` SELECT id, client_order_id AS client_order_number, username, status, adresse, total_prix::float8 as total_prix, created_at, updated_at, COALESCE(cancel_reason, '') AS cancel_reason FROM commandes WHERE status = 'cancelled'` args := []any{} if username != "" { query += " AND username = ?" args = append(args, username) } query += " ORDER BY updated_at DESC" if limit > 0 { query += " LIMIT ?" args = append(args, limit) } var commands []map[string]any if err := d.GDB.Raw(query, args...).Scan(&commands).Error; err != nil { return nil, fmt.Errorf("erreur rĂ©cupĂ©ration: %w", err) } return commands, nil } // AddClientPenalty ajoute une pĂ©nalitĂ© Ă  un client func (d *Database) AddClientPenalty(username string, points int) error { log.Printf("⚠ [AddPenalty] Ajout pĂ©nalitĂ©: %d points pour client %s", points, username) if points <= 0 { return fmt.Errorf("points invalides: %d", points) } if username == "" { return fmt.Errorf("username vide") } result := d.GDB.Exec(` UPDATE clients SET amende = amende + ?, updated_at = CURRENT_TIMESTAMP WHERE username = ?`, points, username) if result.Error != nil { log.Printf("❌ [AddPenalty] Erreur UPDATE: %v", result.Error) return fmt.Errorf("erreur ajout pĂ©nalitĂ©: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("client non trouvĂ©: %s", username) } log.Printf("✅ [AddPenalty] PĂ©nalitĂ© ajoutĂ©e: +%d points pour %s", points, username) cacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, cacheKey) return nil }