package db import ( "database/sql" "encoding/json" "fmt" "gestion/models" "log" "strings" ) func (d *Database) CreateClient(client *models.Client) error { query := `INSERT INTO clients (username, password, nom, prenom, telephone, command, amende, must_change_password, created_at) VALUES ($1, $2, $3, $4, $5, 0, 0.0, $6, CURRENT_TIMESTAMP) RETURNING id, created_at` err := d.QueryRow(query, client.Username, client.Password, client.Nom, client.Prenom, client.Telephone, client.MustChangePassword).Scan( &client.ID, &client.CreatedAt, ) if err != nil { return fmt.Errorf("erreur lors de la création du client: %w", err) } 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 client models.Client query := `SELECT id, username, password, nom, prenom, telephone, command, amende, COALESCE(points_extra, '{}'::jsonb), created_at FROM clients WHERE id = $1` var pointsExtraJSON []byte err := d.QueryRow(query, id).Scan( &client.ID, &client.Username, &client.Password, &client.Nom, &client.Prenom, &client.Telephone, &client.Command, &client.Amende, &pointsExtraJSON, &client.CreatedAt, ) if err == sql.ErrNoRows { return nil, fmt.Errorf("client non trouvé") } if err != nil { return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err) } if len(pointsExtraJSON) > 0 { json.Unmarshal(pointsExtraJSON, &client.PointsExtra) } return &client, nil } // GetAllClients récupère tous les clients func (d *Database) GetAllClients() ([]*models.Client, error) { query := `SELECT id, username, password, nom, prenom, telephone, command, amende, referral_balance, COALESCE(points_extra, '{}'::jsonb), created_at FROM clients ORDER BY created_at DESC` rows, err := d.Query(query) if err != nil { return nil, fmt.Errorf("erreur lors de la récupération des clients: %w", err) } defer rows.Close() var clients []*models.Client for rows.Next() { client := &models.Client{} var pointsExtraJSON []byte err := rows.Scan( &client.ID, &client.Username, &client.Password, &client.Nom, &client.Prenom, &client.Telephone, &client.Command, &client.Amende, &client.ReferralBalance, &pointsExtraJSON, &client.CreatedAt, ) if err != nil { return nil, fmt.Errorf("erreur lors du scan du client: %w", err) } client.PointsExtra = map[string]int{} if len(pointsExtraJSON) > 0 { json.Unmarshal(pointsExtraJSON, &client.PointsExtra) } clients = append(clients, client) } if err = rows.Err(); err != nil { return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err) } return clients, nil } // UpdateClient met à jour un client existant func (d *Database) UpdateClient(client *models.Client) error { query := `UPDATE clients SET username = $1, password = $2, nom = $3, prenom = $4, telephone = $5, command = $6, amende = $7 WHERE id = $8` result, err := d.Exec(query, client.Username, client.Password, client.Nom, client.Prenom, client.Telephone, client.Command, client.Amende, client.ID, ) if err != nil { return fmt.Errorf("erreur lors de la mise à jour du client: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur lors de la vérification des lignes affectées: %w", err) } if 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") query := `DELETE FROM clients WHERE id = $1` result, err := d.Exec(query, id) if err != nil { return fmt.Errorf("erreur lors de la suppression du client: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur lors de la vérification des lignes affectées: %w", err) } if 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 { query := `UPDATE clients SET password = $1 WHERE id = $2` result, err := d.Exec(query, hashedPassword, clientID) if err != nil { return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur lors de la vérification: %w", err) } if 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 { query := `UPDATE clients SET password = $1, must_change_password = FALSE, updated_at = CURRENT_TIMESTAMP WHERE id = $2` result, err := d.Exec(query, hashedPassword, clientID) if err != nil { return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur lors de la vérification: %w", err) } if 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 } // Compter les commandes du client var totalCommands, pendingCommands, completedCommands int countQuery := `SELECT COUNT(*) as total, SUM(CASE WHEN status = 'pending' OR status = 'livre' THEN 1 ELSE 0 END) as pending, SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as completed FROM commandes WHERE username = $1` err = d.QueryRow(countQuery, client.Username).Scan(&totalCommands, &pendingCommands, &completedCommands) if err != nil { log.Printf("⚠️ Erreur calcul stats: %v", err) totalCommands, pendingCommands, completedCommands = 0, 0, 0 } stats := map[string]interface{}{ "id": clientID, "username": client.Username, "nom": client.Nom, "prenom": client.Prenom, "telephone": client.Telephone, "total_commands": totalCommands, "pending_commands": pendingCommands, "completed_commands": completedCommands, "points_extra": client.PointsExtra, "amende": client.Amende, "member_since": client.CreatedAt, } return stats, nil } func (d *Database) GetClientAmende(username string) (float64, error) { var amende float64 query := `SELECT COALESCE(amende, 0) FROM clients WHERE username = $1` err := d.QueryRow(query, username).Scan(&amende) 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, amende) return amende, nil } func (d *Database) PayClientPenalties(username string, amountPaid float64) error { log.Printf("💳 [PayClientPenalties] Paiement de %.2f points pour %s", amountPaid, username) // Vérifier le montant actuel 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) } query := `UPDATE clients SET amende = 0, updated_at = CURRENT_TIMESTAMP WHERE username = $1` result, err := d.Exec(query, username) if err != nil { log.Printf("❌ [PayClientPenalties] Erreur UPDATE: %v", err) return fmt.Errorf("erreur paiement pénalités: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur vérification: %w", err) } if 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 { query := `UPDATE clients SET command = command + 1 WHERE username = $1` result, err := d.Exec(query, username) if err != nil { return fmt.Errorf("erreur lors de l'incrémentation du compteur: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur lors de la vérification: %w", err) } if 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, err := d.Exec(` UPDATE clients SET points_extra = jsonb_set( COALESCE(points_extra, '{}'::jsonb), ARRAY[$2], to_jsonb(COALESCE((points_extra->>$2)::int, 0) + $3) ), updated_at = CURRENT_TIMESTAMP WHERE username = $1 `, username, poolKey, points) if err != nil { return fmt.Errorf("erreur lors de l'ajout de points: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur lors de la vérification: %w", err) } if 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) { tx, err := d.Begin() if err != nil { return 0, fmt.Errorf("erreur transaction: %w", err) } defer tx.Rollback() points, _, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, username) if err != nil { return 0, err } if err := tx.Commit(); err != nil { return 0, fmt.Errorf("erreur commit: %w", err) } return points, nil } func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error) { client := &models.Client{} query := `SELECT id, username, password, nom, prenom, telephone, command, amende, created_at FROM clients WHERE telephone = $1` err := d.QueryRow(query, telephone).Scan( &client.ID, &client.Username, &client.Password, &client.Nom, &client.Prenom, &client.Telephone, &client.Command, &client.Amende, &client.CreatedAt, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err) } return client, nil } // GetClientByUsername récupère un client par son username func (d *Database) GetClientByUsername(username string) (*models.Client, error) { client := &models.Client{} var pointsExtraJSON []byte query := `SELECT id, username, password, nom, prenom, telephone, command, amende, must_change_password, COALESCE(points_extra, '{}'::jsonb), created_at FROM clients WHERE username = $1` err := d.QueryRow(query, username).Scan( &client.ID, &client.Username, &client.Password, &client.Nom, &client.Prenom, &client.Telephone, &client.Command, &client.Amende, &client.MustChangePassword, &pointsExtraJSON, &client.CreatedAt, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err) } client.PointsExtra = map[string]int{} if len(pointsExtraJSON) > 0 { json.Unmarshal(pointsExtraJSON, &client.PointsExtra) } return client, nil } func (d *Database) GetClientPenaltiesInfo(username string) (map[string]interface{}, error) { // Récupérer le montant des pénalités amende, err := d.GetClientAmende(username) if err != nil { return nil, err } // Récupérer le nombre d'annulations cancellationsCount, err := d.GetClientCancellationsCount(username) if err != nil { log.Printf("⚠️ [GetClientPenaltiesInfo] Erreur récup annulations: %v", err) cancellationsCount = 0 } // Récupérer l'historique d'annulations 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 { var query string switch { case extraPoolKey != "": _, err := d.Exec( `UPDATE clients SET points_extra = points_extra - $2, updated_at = CURRENT_TIMESTAMP WHERE username = $1`, username, extraPoolKey, ) if err != nil { log.Printf("❌ [ResetClientPointAdmin] Erreur UPDATE extra: %v", err) } else { cacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, cacheKey) } return err default: // -1 ou poolIdx sans clé → reset total query = `UPDATE clients SET points_extra = '{}'::jsonb, updated_at = CURRENT_TIMESTAMP WHERE username = $1` } result, err := d.Exec(query, username) if err != nil { log.Printf("❌ [ResetClientPointAdmin] Erreur UPDATE: %v", err) return fmt.Errorf("erreur reset points: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur vérification: %w", err) } if rowsAffected == 0 { return fmt.Errorf("client non trouvé") } // Invalider le cache Redis du client 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 = $1` } else { query = `UPDATE clients SET amende = 0, updated_at = CURRENT_TIMESTAMP WHERE username = $1` } result, err := d.Exec(query, username) if err != nil { log.Printf("❌ [ResetClientPenalties] Erreur UPDATE: %v", err) return fmt.Errorf("erreur reset pénalités: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("erreur vérification: %w", err) } if rowsAffected == 0 { return fmt.Errorf("client non trouvé") } // Invalider le cache Redis du client cacheKey := fmt.Sprintf("client:%s", username) Redis.Del(RedisCtx, cacheKey) return nil } func (d *Database) GetAllClientsWithPenalties() ([]map[string]interface{}, error) { query := ` SELECT username, amende, COALESCE(cancellations_count, 0) as cancellations_count, updated_at FROM clients WHERE amende > 0 ORDER BY amende DESC ` rows, err := d.Query(query) if err != nil { log.Printf("❌ [GetAllClientsWithPenalties] Erreur query: %v", err) return nil, fmt.Errorf("erreur récupération clients: %w", err) } defer rows.Close() var clients []map[string]interface{} for rows.Next() { var username string var amende float64 var cancellationsCount int var updatedAt interface{} err := rows.Scan(&username, &amende, &cancellationsCount, &updatedAt) if err != nil { log.Printf("⚠️ [GetAllClientsWithPenalties] Erreur scan: %v", err) continue } clients = append(clients, map[string]interface{}{ "username": username, "total_penalty": amende, "cancellations_count": cancellationsCount, "last_updated": updatedAt, }) } log.Printf("📊 [GetAllClientsWithPenalties] %d clients avec pénalités", len(clients)) return clients, nil } func (d *Database) GetClientPenaltiesStats() (map[string]interface{}, error) { query := ` 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 ` var stats struct { ClientsWithPenalties int TotalPenalties float64 AvgPenalty float64 MaxPenalty float64 TotalClients int } err := d.QueryRow(query).Scan( &stats.ClientsWithPenalties, &stats.TotalPenalties, &stats.AvgPenalty, &stats.MaxPenalty, &stats.TotalClients, ) if err != nil { log.Printf("❌ [GetClientPenaltiesStats] Erreur: %v", err) return nil, fmt.Errorf("erreur récupération stats: %w", err) } result := map[string]interface{}{ "clients_with_penalties": stats.ClientsWithPenalties, "total_penalties": stats.TotalPenalties, "average_penalty": stats.AvgPenalty, "max_penalty": stats.MaxPenalty, "total_clients": stats.TotalClients, } log.Printf("📊 [GetClientPenaltiesStats] Stats: %d/%d clients avec pénalités", stats.ClientsWithPenalties, stats.TotalClients) return result, nil } func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, 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 rows, err := tx.Query(` 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 = $1 `, commandID) if err != nil { log.Printf("❌ [CalcPointsTx] Erreur query items: %v", err) return 0, "", fmt.Errorf("erreur récupération items: %w", err) } defer rows.Close() var itemCount int poolTotals := make([]float64, len(pools)) for rows.Next() { var quantite, prix float64 var category string if err := rows.Scan(&quantite, &prix, &category); err != nil { log.Printf("❌ [CalcPointsTx] Erreur scan: %v", err) return 0, "", fmt.Errorf("erreur lecture item: %w", err) } itemCount++ catLower := strings.ToLower(category) if poolIdx, ok := catToPool[catLower]; ok { poolTotals[poolIdx] += prix } } if err = rows.Err(); err != nil { log.Printf("❌ [CalcPointsTx] Erreur rows: %v", err) return 0, "", fmt.Errorf("erreur itération items: %w", err) } if itemCount == 0 { log.Printf("⚠️ [CalcPointsTx] Aucun item trouvé pour cmd %d", commandID) return 0, "", nil } 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 } _, err = tx.Exec(` UPDATE clients SET points_extra = jsonb_set( COALESCE(points_extra, '{}'::jsonb), ARRAY[$2], to_jsonb(COALESCE((points_extra->>$2)::int, 0) + $3) ), updated_at = CURRENT_TIMESTAMP WHERE username = $1 `, username, pool.Key, poolPts[i]) if 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.QueryRow(` SELECT EXISTS( SELECT 1 FROM commandes WHERE id = $1 AND livreur_assign = $2 ) `, commandID, username).Scan(&exists) return exists, err } // 👤 User : seulement SES commandes err := d.QueryRow(` SELECT EXISTS( SELECT 1 FROM commandes WHERE id = $1 AND username = $2 ) `, commandID, username).Scan(&exists) return exists, err }