// ============================================ // db/cancel_sanctions_db.go // GESTION DES SANCTIONS ÉVOLUTIVES // ============================================ package db import ( "fmt" "gestion/models" "log" "sort" "gorm.io/gorm" ) // GetClientCancellationsCount récupère le nombre d'annulations tardives d'un client func (d *Database) GetClientCancellationsCount(username string) (int, error) { var result struct { Count int `gorm:"column:count"` } err := d.GDB.Table("clients").Select("COALESCE(cancellations_count, 0) as count").Where("username = ?", username).Scan(&result).Error if err != nil { log.Printf("❌ [GetCancellationsCount] Erreur: %v", err) return 0, fmt.Errorf("erreur récupération compteur: %w", err) } return result.Count, nil } // penaltyForCount retourne le montant du palier applicable pour un nombre d'annulations donné func penaltyForCount(count int, tiers []models.PenaltyTier) int { if len(tiers) == 0 { return 0 } sorted := make([]models.PenaltyTier, len(tiers)) copy(sorted, tiers) sort.Slice(sorted, func(i, j int) bool { return sorted[i].MinCancel > sorted[j].MinCancel }) for _, t := range sorted { if count >= t.MinCancel { return t.Amount } } return sorted[len(sorted)-1].Amount } // penaltyTiers charge le barème de pénalités configuré, avec repli sur le barème par défaut si les settings sont indisponibles func (d *Database) penaltyTiers(logCtx string) []models.PenaltyTier { settings, err := d.GetSettings() if err != nil { log.Printf("⚠️ [%s] Impossible de charger les settings, barème par défaut: %v", logCtx, err) settings = DefaultSettings() } return settings.PenaltyTiers } // CalculateCancellationPenalty calcule la pénalité selon l'historique et le barème configuré func (d *Database) CalculateCancellationPenalty(username string) (int, error) { count, err := d.GetClientCancellationsCount(username) if err != nil { return 0, err } penalty := penaltyForCount(count, d.penaltyTiers("CalculatePenalty")) log.Printf("💰 [CalculatePenalty] Client %s - Annulations: %d → Pénalité: %d points", username, count, penalty) return penalty, nil } // ApplyCancellationPenalty applique une pénalité (cumulative) et incrémente le compteur d'annulations. // Verrouillée via FOR UPDATE pour éviter qu'un appel concurrent (même client, deux livraisons en parallèle) // calcule la pénalité sur un compteur pas encore à jour, et l'amende s'additionne au lieu d'écraser // le solde existant (cohérent avec CancelCommandAtomic pour l'annulation côté client). func (d *Database) ApplyCancellationPenalty(username string) (int, error) { tiers := d.penaltyTiers("ApplyCancellationPenalty") var penalty int err := d.GDB.Transaction(func(tx *gorm.DB) error { var count int if err := tx.Raw(` SELECT COALESCE(cancellations_count, 0) FROM clients WHERE username = ? FOR UPDATE`, username).Scan(&count).Error; err != nil { return fmt.Errorf("erreur récupération compteur: %w", err) } penalty = penaltyForCount(count, tiers) log.Printf("⚠️ [ApplyCancellationPenalty] Client %s - Pénalité calculée: %d points", username, penalty) result := tx.Exec(` UPDATE clients SET cancellations_count = COALESCE(cancellations_count, 0) + 1, amende = amende + ?, updated_at = CURRENT_TIMESTAMP WHERE username = ?`, penalty, username) if result.Error != nil { log.Printf("❌ [ApplyCancellationPenalty] Erreur UPDATE: %v", result.Error) return fmt.Errorf("erreur application pénalité: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } log.Printf("✅ [ApplyCancellationPenalty] Amende %d appliquée à %s", penalty, username) return nil }) if err != nil { return 0, err } cacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, cacheKey) return penalty, nil } // GetClientCancellationHistory récupère l'historique d'annulations d'un client func (d *Database) GetClientCancellationHistory(username string) (map[string]any, error) { nextPenalty, err := d.CalculateCancellationPenalty(username) if err != nil { return nil, err } count, err := d.GetClientCancellationsCount(username) if err != nil { return nil, err } settings, _ := d.GetSettings() client, err := d.GetClientByUsername(username) var currentAmende float64 if err == nil { currentAmende = client.Amende } return map[string]any{ "cancellations_count": count, "current_amende": currentAmende, "next_penalty": nextPenalty, "penalty_tiers": settings.PenaltyTiers, "warning": "Une amende sera appliquée lors de la prochaine annulation tardive", }, nil }