package db import ( "encoding/json" "fmt" "gestion/models" "log" "strings" "time" "gorm.io/gorm" ) func (d *Database) CreateClient(client *models.Client) error { var result struct { ID int `gorm:"column:id"` CreatedAt time.Time `gorm:"column:created_at"` } err := d.GDB.Raw(` INSERT INTO clients (username, password, nom, prenom, telephone, command, amende, must_change_password, created_at) VALUES (?, ?, ?, ?, ?, 0, 0.0, ?, CURRENT_TIMESTAMP) RETURNING id, created_at`, client.Username, client.Password, client.Nom, client.Prenom, client.Telephone, client.MustChangePassword, ).Scan(&result).Error if err != nil { return fmt.Errorf("erreur lors de la création du client: %w", err) } client.ID = result.ID client.CreatedAt = result.CreatedAt log.Printf("✅ Client créé avec succès: %s %s (ID: %d)", client.Prenom, client.Nom, client.ID) return nil } // GetClientByID récupère un client par son ID func (d *Database) GetClientByID(id int) (*models.Client, error) { var row struct { ID int `gorm:"column:id"` Username string `gorm:"column:username"` Password string `gorm:"column:password"` Nom string `gorm:"column:nom"` Prenom string `gorm:"column:prenom"` Telephone string `gorm:"column:telephone"` Command int `gorm:"column:command"` Amende float64 `gorm:"column:amende"` PointsExtraJSON []byte `gorm:"column:points_extra"` CreatedAt time.Time `gorm:"column:created_at"` } err := d.GDB.Raw(` SELECT id, username, password, nom, prenom, telephone, command, amende, COALESCE(points_extra, '{}'::jsonb) as points_extra, created_at FROM clients WHERE id = ?`, id).Scan(&row).Error if err != nil { return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err) } if row.ID == 0 { return nil, fmt.Errorf("client non trouvé") } client := &models.Client{ ID: row.ID, Username: row.Username, Password: row.Password, Nom: row.Nom, Prenom: row.Prenom, Telephone: row.Telephone, Command: row.Command, Amende: row.Amende, CreatedAt: row.CreatedAt, } client.PointsExtra = map[string]int{} if len(row.PointsExtraJSON) > 0 { json.Unmarshal(row.PointsExtraJSON, &client.PointsExtra) } return client, nil } // GetAllClients récupère tous les clients func (d *Database) GetAllClients() ([]*models.Client, error) { var rows []struct { ID int `gorm:"column:id"` Username string `gorm:"column:username"` Password string `gorm:"column:password"` Nom string `gorm:"column:nom"` Prenom string `gorm:"column:prenom"` Telephone string `gorm:"column:telephone"` Command int `gorm:"column:command"` Amende float64 `gorm:"column:amende"` ReferralBalance float64 `gorm:"column:referral_balance"` CancellationsCount int `gorm:"column:cancellations_count"` PointsExtraJSON []byte `gorm:"column:points_extra"` CreatedAt time.Time `gorm:"column:created_at"` } err := d.GDB.Raw(` SELECT id, username, password, nom, prenom, telephone, command, amende, referral_balance, COALESCE(cancellations_count, 0) as cancellations_count, COALESCE(points_extra, '{}'::jsonb) as points_extra, created_at FROM clients ORDER BY created_at DESC`).Scan(&rows).Error if err != nil { return nil, fmt.Errorf("erreur lors de la récupération des clients: %w", err) } clients := make([]*models.Client, 0, len(rows)) for _, row := range rows { client := &models.Client{ ID: row.ID, Username: row.Username, Password: row.Password, Nom: row.Nom, Prenom: row.Prenom, Telephone: row.Telephone, Command: row.Command, Amende: row.Amende, ReferralBalance: row.ReferralBalance, CancellationsCount: row.CancellationsCount, CreatedAt: row.CreatedAt, } client.PointsExtra = map[string]int{} if len(row.PointsExtraJSON) > 0 { json.Unmarshal(row.PointsExtraJSON, &client.PointsExtra) } clients = append(clients, client) } return clients, nil } // UpdateClient met à jour un client existant func (d *Database) UpdateClient(client *models.Client) error { result := d.GDB.Model(&models.Client{}).Where("id = ?", client.ID).Updates(map[string]any{ "username": client.Username, "password": client.Password, "nom": client.Nom, "prenom": client.Prenom, "telephone": client.Telephone, "command": client.Command, "amende": client.Amende, }) if result.Error != nil { return fmt.Errorf("erreur lors de la mise à jour du client: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } return nil } // DeleteClient supprime un client func (d *Database) DeleteClient(id int) error { _ = d.RevokeAllUserTokens(id, "client") result := d.GDB.Delete(&models.Client{}, id) if result.Error != nil { return fmt.Errorf("erreur lors de la suppression du client: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } return nil } // UpdateClientPassword met à jour le mot de passe d'un client func (d *Database) UpdateClientPassword(clientID int, hashedPassword string) error { result := d.GDB.Model(&models.Client{}).Where("id = ?", clientID).Update("password", hashedPassword) if result.Error != nil { return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } return nil } // UpdateClientPasswordAndClearFlag met à jour le mot de passe et remet must_change_password à false func (d *Database) UpdateClientPasswordAndClearFlag(clientID int, hashedPassword string) error { result := d.GDB.Model(&models.Client{}).Where("id = ?", clientID).Updates(map[string]any{ "password": hashedPassword, "must_change_password": false, }) if result.Error != nil { return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } return nil } // GetClientStats récupère les statistiques d'un client func (d *Database) GetClientStats(clientID int) (map[string]interface{}, error) { client, err := d.GetClientByID(clientID) if err != nil { return nil, err } var statsResult struct { Total int `gorm:"column:total"` Pending int `gorm:"column:pending"` Completed int `gorm:"column:completed"` } if err := d.GDB.Raw(` SELECT COUNT(*) as total, COALESCE(SUM(CASE WHEN status = 'pending' OR status = 'livre' THEN 1 ELSE 0 END), 0) as pending, COALESCE(SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END), 0) as completed FROM commandes WHERE username = ?`, client.Username).Scan(&statsResult).Error; err != nil { log.Printf("⚠️ Erreur calcul stats: %v", err) } stats := map[string]interface{}{ "id": clientID, "username": client.Username, "nom": client.Nom, "prenom": client.Prenom, "telephone": client.Telephone, "total_commands": statsResult.Total, "pending_commands": statsResult.Pending, "completed_commands": statsResult.Completed, "points_extra": client.PointsExtra, "amende": client.Amende, "member_since": client.CreatedAt, } return stats, nil } func (d *Database) GetClientAmende(username string) (float64, error) { var result struct { Amende float64 `gorm:"column:amende"` } err := d.GDB.Raw(`SELECT COALESCE(amende, 0) as amende FROM clients WHERE username = ?`, username).Scan(&result).Error if err != nil { log.Printf("❌ [GetClientAmende] Erreur pour %s: %v", username, err) return 0, fmt.Errorf("erreur récupération pénalités: %w", err) } log.Printf("💰 [GetClientAmende] Client %s: %.2f points", username, result.Amende) return result.Amende, nil } func (d *Database) PayClientPenalties(username string, amountPaid float64) error { log.Printf("💳 [PayClientPenalties] Paiement de %.2f points pour %s", amountPaid, username) currentAmount, err := d.GetClientAmende(username) if err != nil { return err } if currentAmount <= 0 { return fmt.Errorf("aucune pénalité à payer") } if amountPaid < currentAmount { return fmt.Errorf("montant insuffisant: %.2f payé, %.2f requis", amountPaid, currentAmount) } result := d.GDB.Model(&models.Client{}).Where("username = ?", username).Update("amende", 0.0) if result.Error != nil { log.Printf("❌ [PayClientPenalties] Erreur UPDATE: %v", result.Error) return fmt.Errorf("erreur paiement pénalités: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } cacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, cacheKey) return nil } // IncrementClientCommandCount incrémente le compteur de commandes du client func (d *Database) IncrementClientCommandCount(username string) error { result := d.GDB.Model(&models.Client{}).Where("username = ?", username).UpdateColumn("command", gorm.Expr("command + 1")) if result.Error != nil { return fmt.Errorf("erreur lors de l'incrémentation du compteur: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } return nil } func (d *Database) AddClientPointsByCategory(username string, points int, poolKey string) error { if poolKey == "" { poolKey = "pool_0" } result := d.GDB.Exec(` UPDATE clients SET points_extra = jsonb_set( COALESCE(points_extra, '{}'::jsonb), ARRAY[?], to_jsonb(COALESCE((points_extra->>?)::int, 0) + ?) ), updated_at = CURRENT_TIMESTAMP WHERE username = ?`, poolKey, poolKey, points, username) if result.Error != nil { return fmt.Errorf("erreur lors de l'ajout de points: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } log.Printf("✅ %d points (key=%s) ajoutés au client %s", points, poolKey, username) return nil } func (d *Database) CalculateAndAddPointsForCommand(commandID int, username string) (int, error) { var totalPoints int err := d.GDB.Transaction(func(tx *gorm.DB) error { points, _, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, username) if err != nil { return err } totalPoints = points return nil }) if err != nil { return 0, err } return totalPoints, nil } func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error) { var row struct { ID int `gorm:"column:id"` Username string `gorm:"column:username"` Password string `gorm:"column:password"` Nom string `gorm:"column:nom"` Prenom string `gorm:"column:prenom"` Telephone string `gorm:"column:telephone"` Command int `gorm:"column:command"` Amende float64 `gorm:"column:amende"` CreatedAt time.Time `gorm:"column:created_at"` } err := d.GDB.Raw(` SELECT id, username, password, nom, prenom, telephone, command, amende, created_at FROM clients WHERE telephone = ?`, telephone).Scan(&row).Error if err != nil { return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err) } if row.ID == 0 { return nil, nil } return &models.Client{ ID: row.ID, Username: row.Username, Password: row.Password, Nom: row.Nom, Prenom: row.Prenom, Telephone: row.Telephone, Command: row.Command, Amende: row.Amende, CreatedAt: row.CreatedAt, }, nil } // GetClientByUsername récupère un client par son username func (d *Database) GetClientByUsername(username string) (*models.Client, error) { var row struct { ID int `gorm:"column:id"` Username string `gorm:"column:username"` Password string `gorm:"column:password"` Nom string `gorm:"column:nom"` Prenom string `gorm:"column:prenom"` Telephone string `gorm:"column:telephone"` Command int `gorm:"column:command"` Amende float64 `gorm:"column:amende"` MustChangePassword bool `gorm:"column:must_change_password"` PointsExtraJSON []byte `gorm:"column:points_extra"` CreatedAt time.Time `gorm:"column:created_at"` } err := d.GDB.Raw(` SELECT id, username, password, nom, prenom, telephone, command, amende, must_change_password, COALESCE(points_extra, '{}'::jsonb) as points_extra, created_at FROM clients WHERE username = ?`, username).Scan(&row).Error if err != nil { return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err) } if row.ID == 0 { return nil, nil } client := &models.Client{ ID: row.ID, Username: row.Username, Password: row.Password, Nom: row.Nom, Prenom: row.Prenom, Telephone: row.Telephone, Command: row.Command, Amende: row.Amende, MustChangePassword: row.MustChangePassword, CreatedAt: row.CreatedAt, } client.PointsExtra = map[string]int{} if len(row.PointsExtraJSON) > 0 { json.Unmarshal(row.PointsExtraJSON, &client.PointsExtra) } return client, nil } func (d *Database) GetClientPenaltiesInfo(username string) (map[string]interface{}, error) { amende, err := d.GetClientAmende(username) if err != nil { return nil, err } cancellationsCount, err := d.GetClientCancellationsCount(username) if err != nil { log.Printf("⚠️ [GetClientPenaltiesInfo] Erreur récup annulations: %v", err) cancellationsCount = 0 } cancellationHistory, err := d.GetClientCancellationHistory(username) if err != nil { log.Printf("⚠️ [GetClientPenaltiesInfo] Erreur récup historique: %v", err) cancellationHistory = map[string]interface{}{ "cancellations_count": cancellationsCount, "next_penalty": 20, } } info := map[string]interface{}{ "username": username, "total_penalty": amende, "cancellations_count": cancellationsCount, "cancellation_history": cancellationHistory, "has_penalties": amende > 0, } return info, nil } // CheckClientCanOrder vérifie si un client peut passer commande (pas de pénalités impayées) func (d *Database) CheckClientCanOrder(username string) (bool, float64, error) { amende, err := d.GetClientAmende(username) if err != nil { return false, 0, err } if amende > 0 { log.Printf("⚠️ [CheckClientCanOrder] Client %s bloqué: %.2f points de pénalités", username, amende) return false, amende, fmt.Errorf("pénalités impayées: %.2f points", amende) } return true, 0, nil } // ResetClientPoint réinitialise les points d'un client. // extraPoolKey != "" → reset points_extra[extraPoolKey] uniquement // extraPoolKey == "" (poolIdx=-1) → reset total points_extra func (d *Database) ResetClientPoint(username string, poolIdx int, extraPoolKey string) error { if extraPoolKey != "" { err := d.GDB.Exec(` UPDATE clients SET points_extra = points_extra - ?, updated_at = CURRENT_TIMESTAMP WHERE username = ?`, extraPoolKey, username).Error if err != nil { log.Printf("❌ [ResetClientPointAdmin] Erreur UPDATE extra: %v", err) } else { cacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, cacheKey) } return err } // -1 ou poolIdx sans clé → reset total result := d.GDB.Exec(` UPDATE clients SET points_extra = '{}'::jsonb, updated_at = CURRENT_TIMESTAMP WHERE username = ?`, username) if result.Error != nil { log.Printf("❌ [ResetClientPointAdmin] Erreur UPDATE: %v", result.Error) return fmt.Errorf("erreur reset points: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } cacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, cacheKey) return nil } func (d *Database) ResetClientPenalties(username string, resetCancellationsCount bool) error { log.Printf("🔄 [ResetClientPenalties] Reset pour %s (reset_count=%v)", username, resetCancellationsCount) var query string if resetCancellationsCount { query = `UPDATE clients SET amende = 0, cancellations_count = 0, updated_at = CURRENT_TIMESTAMP WHERE username = ?` } else { query = `UPDATE clients SET amende = 0, updated_at = CURRENT_TIMESTAMP WHERE username = ?` } result := d.GDB.Exec(query, username) if result.Error != nil { log.Printf("❌ [ResetClientPenalties] Erreur UPDATE: %v", result.Error) return fmt.Errorf("erreur reset pénalités: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("client non trouvé") } cacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, cacheKey) return nil } func (d *Database) GetAllClientsWithPenalties() ([]map[string]interface{}, error) { var rows []struct { Username string `gorm:"column:username"` Amende float64 `gorm:"column:amende"` CancellationsCount int `gorm:"column:cancellations_count"` UpdatedAt interface{} `gorm:"column:updated_at"` } err := d.GDB.Raw(` SELECT username, amende, COALESCE(cancellations_count, 0) as cancellations_count, updated_at FROM clients WHERE amende > 0 ORDER BY amende DESC`).Scan(&rows).Error if err != nil { log.Printf("❌ [GetAllClientsWithPenalties] Erreur query: %v", err) return nil, fmt.Errorf("erreur récupération clients: %w", err) } clients := make([]map[string]interface{}, 0, len(rows)) for _, row := range rows { clients = append(clients, map[string]interface{}{ "username": row.Username, "total_penalty": row.Amende, "cancellations_count": row.CancellationsCount, "last_updated": row.UpdatedAt, }) } log.Printf("📊 [GetAllClientsWithPenalties] %d clients avec pénalités", len(clients)) return clients, nil } func (d *Database) GetClientPenaltiesStats() (map[string]interface{}, error) { var result struct { ClientsWithPenalties int `gorm:"column:clients_with_penalties"` TotalPenalties float64 `gorm:"column:total_penalties"` AvgPenalty float64 `gorm:"column:avg_penalty"` MaxPenalty float64 `gorm:"column:max_penalty"` TotalClients int `gorm:"column:total_clients"` } err := d.GDB.Raw(` SELECT COUNT(CASE WHEN amende > 0 THEN 1 END) as clients_with_penalties, COALESCE(SUM(amende), 0) as total_penalties, COALESCE(AVG(amende), 0) as avg_penalty, COALESCE(MAX(amende), 0) as max_penalty, COUNT(*) as total_clients FROM clients`).Scan(&result).Error if err != nil { log.Printf("❌ [GetClientPenaltiesStats] Erreur: %v", err) return nil, fmt.Errorf("erreur récupération stats: %w", err) } stats := map[string]interface{}{ "clients_with_penalties": result.ClientsWithPenalties, "total_penalties": result.TotalPenalties, "average_penalty": result.AvgPenalty, "max_penalty": result.MaxPenalty, "total_clients": result.TotalClients, } log.Printf("📊 [GetClientPenaltiesStats] Stats: %d/%d clients avec pénalités", result.ClientsWithPenalties, result.TotalClients) return stats, nil } func (d *Database) CalculateAndAddPointsForCommandTx(tx *gorm.DB, commandID int, username string) (int, string, error) { log.Printf("💰 [CalcPointsTx] START - cmd=%d, user=%s", commandID, username) // Charger les paramètres globaux settings, err := d.GetSettings() if err != nil { log.Printf("⚠️ [CalcPointsTx] Erreur lecture settings, utilisation des défauts: %v", err) settings = DefaultSettings() } pools := settings.PointsPools if len(pools) == 0 { log.Printf("ℹ️ [CalcPointsTx] Aucun pool configuré → 0 points") return 0, "", nil } // Construire la map catégorie → index de pool catToPool := make(map[string]int) for i, pool := range pools { for _, cat := range pool.Categories { catToPool[strings.ToLower(cat)] = i } } if len(catToPool) == 0 { log.Printf("ℹ️ [CalcPointsTx] Aucune catégorie assignée aux pools → 0 points") return 0, "", nil } // ✅ ÉTAPE 1: Récupérer tous les items de la commande avec leurs catégories var items []struct { Quantite float64 `gorm:"column:quantite"` Prix float64 `gorm:"column:prix"` Category string `gorm:"column:category"` } if err := tx.Raw(` SELECT ci.quantite, ci.prix, COALESCE(p.category, '') as category FROM command_items ci LEFT JOIN products p ON ci.product_id = p.id WHERE ci.command_id = ? `, commandID).Scan(&items).Error; err != nil { log.Printf("❌ [CalcPointsTx] Erreur query items: %v", err) return 0, "", fmt.Errorf("erreur récupération items: %w", err) } if len(items) == 0 { log.Printf("⚠️ [CalcPointsTx] Aucun item trouvé pour cmd %d", commandID) return 0, "", nil } poolTotals := make([]float64, len(pools)) for _, item := range items { catLower := strings.ToLower(item.Category) if poolIdx, ok := catToPool[catLower]; ok { poolTotals[poolIdx] += item.Prix } } for i, t := range poolTotals { log.Printf("📊 [CalcPointsTx] Pool[%d] (%s): %.2f€", i, pools[i].Name, t) } // ✅ ÉTAPE 2: Calculer les points pour tous les pools var totalPoints int var pointCategory string poolPts := make([]int, len(pools)) var categoryParts []string for i, pool := range pools { poolPts[i] = CalcPointsFromTiers(poolTotals[i], pool.Tiers) totalPoints += poolPts[i] if poolPts[i] > 0 { categoryParts = append(categoryParts, pool.Name) } } log.Printf("💰 [CalcPointsTx] points par pool: %v, total=%d", poolPts, totalPoints) if totalPoints == 0 { return 0, "", nil } if len(categoryParts) > 0 { pointCategory = strings.Join(categoryParts, " & ") } else { pointCategory = "points" } for i, pool := range pools { if poolPts[i] == 0 { continue } if err := tx.Exec(` UPDATE clients SET points_extra = jsonb_set( COALESCE(points_extra, '{}'::jsonb), ARRAY[?], to_jsonb(COALESCE((points_extra->>?)::int, 0) + ?) ), updated_at = CURRENT_TIMESTAMP WHERE username = ? `, pool.Key, pool.Key, poolPts[i], username).Error; err != nil { log.Printf("❌ [CalcPointsTx] Erreur UPDATE points_extra pool[%d] (%s): %v", i, pool.Key, err) return 0, "", fmt.Errorf("erreur mise à jour points pool[%d]: %w", i, err) } log.Printf("💰 [CalcPointsTx] pool[%d] (%s / key=%s): +%d pts", i, pool.Name, pool.Key, poolPts[i]) } log.Printf("🎉 [CalcPointsTx] SUCCÈS - %d points [%s] → %s", totalPoints, pointCategory, username) return totalPoints, pointCategory, nil } func (d *Database) CanUserAccessCommand( commandID int, username string, role string, ) (bool, error) { // 👑 Admin : accès total if role == "admin" { return true, nil } var exists bool // 🚚 Livreur : seulement commandes assignées if role == "livreur" { err := d.GDB.Raw(` SELECT EXISTS( SELECT 1 FROM commandes WHERE id = ? AND livreur_assign = ? ) `, commandID, username).Scan(&exists).Error return exists, err } // 👤 User : seulement SES commandes err := d.GDB.Raw(` SELECT EXISTS( SELECT 1 FROM commandes WHERE id = ? AND username = ? ) `, commandID, username).Scan(&exists).Error return exists, err }