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, point, point_zipette, amende, created_at) VALUES ($1, $2, $3, $4, $5, 0, 0, 0, 0.0, CURRENT_TIMESTAMP) RETURNING id, created_at` err := d.QueryRow(query, client.Username, client.Password, client.Nom, client.Prenom, client.Telephone).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, point, point_zipette, amende, created_at FROM clients WHERE id = $1` err := d.QueryRow(query, id).Scan( &client.ID, &client.Username, &client.Password, &client.Nom, &client.Prenom, &client.Telephone, &client.Command, &client.Point, &client.PointZipette, &client.Amende, &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) } 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, point, point_zipette, 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.Point, &client.PointZipette, &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, point = $7, point_zipette = $8, amende = $9 WHERE id = $10` result, err := d.Exec(query, client.Username, client.Password, client.Nom, client.Prenom, client.Telephone, client.Command, client.Point, client.PointZipette, 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é") } log.Printf("✅ Client mis à jour: %s (ID: %d)", client.Username, client.ID) return nil } // DeleteClient supprime un client func (d *Database) DeleteClient(id int) error { // ✅ MODIFIÉ : Supprimer tous les tokens du client avec le user_type "client" _ = 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é") } log.Printf("✅ Client supprimé (ID: %d)", id) 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é") } log.Printf("✅ Mot de passe client mis à jour (ID: %d)", clientID) 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é") } log.Printf("✅ Mot de passe client mis à jour + must_change_password=false (ID: %d)", clientID) 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": client.Point, "points_zipette": client.PointZipette, "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) } // Réinitialiser les pénalités 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é") } log.Printf("✅ [PayClientPenalties] Pénalités réglées pour %s", username) // Invalider le cache Redis du client 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 } // ✅ NOUVELLE FONCTION: Ajouter des points selon la catégorie func (d *Database) AddClientPointsByCategory(username string, points int, category string) error { var query string if category == "zipette&co" { query = `UPDATE clients SET point_zipette = point_zipette + $1 WHERE username = $2` log.Printf("🎁 [ADD_POINTS] Ajout de %d points ZIPETTE à %s", points, username) } else { query = `UPDATE clients SET point = point + $1 WHERE username = $2` log.Printf("🎁 [ADD_POINTS] Ajout de %d points WEED/HASH à %s", points, username) } result, err := d.Exec(query, points, username) 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 (%s) ajoutés au client %s (EN DB)", points, category, username) return nil } // ✅ ANCIENNE FONCTION CONSERVÉE POUR COMPATIBILITÉ (utilise weed/hash par défaut) func (d *Database) AddClientPoints(username string, points int) error { return d.AddClientPointsByCategory(username, points, "weed_hash") } func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error) { client := &models.Client{} query := `SELECT id, username, password, nom, prenom, telephone, command, point, point_zipette, 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.Point, &client.PointZipette, &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{} query := `SELECT id, username, password, nom, prenom, telephone, command, point, point_zipette, amende, must_change_password, 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.Point, &client.PointZipette, &client.Amende, &client.MustChangePassword, &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 } 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. // poolIdx=0 → point, poolIdx=1 → point_zipette, poolIdx=-1 → tous // poolIdx>=2 → points_extra[extraPoolKey] func (d *Database) ResetClientPoint(username string, poolIdx int, extraPoolKey string) error { var query string switch { case poolIdx == 0: query = `UPDATE clients SET point = 0, updated_at = CURRENT_TIMESTAMP WHERE username = $1` case poolIdx == 1: query = `UPDATE clients SET point_zipette = 0, updated_at = CURRENT_TIMESTAMP WHERE username = $1` case poolIdx >= 2 && extraPoolKey != "": _, err := d.Exec( `UPDATE clients SET points_extra = points_extra - $2, updated_at = CURRENT_TIMESTAMP WHERE username = $1`, username, extraPoolKey, ) return err default: // -1 → reset total query = `UPDATE clients SET point = 0, point_zipette = 0, 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 } // ✅ FONCTION MODIFIÉE: Calculer les points sans cumuler entre catégories // À remplacer dans db/clients.go à partir de la ligne 498 // À remplacer dans db/clients.go func (d *Database) CalculatePointsForCommand(commandID int) (int, string, error) { query := ` SELECT p.category, ci.prix, ci.quantite FROM command_items ci JOIN products p ON ci.product_id = p.id WHERE ci.command_id = $1 ` rows, err := d.Query(query, commandID) if err != nil { return 0, "", fmt.Errorf("erreur récupération items: %w", err) } defer rows.Close() zipetteTotal := 0.0 weedTotal := 0.0 grosSemiTotal := 0.0 for rows.Next() { var category string var prix float64 var quantite int if err := rows.Scan(&category, &prix, &quantite); err != nil { log.Printf("⚠️ Erreur scan item: %v", err) continue } itemTotal := prix // Prix déjà calculé pour la quantité categoryLower := strings.ToLower(category) if categoryLower == "zipette&co" || categoryLower == "zipette_co" { zipetteTotal += itemTotal } else if categoryLower == "gros&semi" || categoryLower == "gros_semi" { grosSemiTotal += itemTotal } else { weedTotal += itemTotal } } log.Printf("💰 [CALC_POINTS] Cmd %d - Zipette: %.2f€, Weed: %.2f€, GrosSemi: %.2f€", commandID, zipetteTotal, weedTotal, grosSemiTotal) // ✅ CALCUL POINTS WEED&HASH weedPoints := 0 if weedTotal > 0 { switch { case weedTotal >= 30 && weedTotal <= 50: weedPoints = 1 case weedTotal >= 60 && weedTotal <= 150: weedPoints = 2 case weedTotal >= 160 && weedTotal <= 300: weedPoints = 3 case weedTotal >= 310 && weedTotal <= 400: weedPoints = 5 case weedTotal >= 400: weedPoints = 10 } if weedPoints > 0 { log.Printf("🎁 [CALC_POINTS] Weed: %.2f€ → %d points", weedTotal, weedPoints) } } // ✅ CALCUL POINTS ZIPETTE&CO zipettePoints := 0 if zipetteTotal > 0 { switch { case zipetteTotal >= 30 && zipetteTotal <= 100: zipettePoints = 1 case zipetteTotal >= 110 && zipetteTotal <= 200: zipettePoints = 2 case zipetteTotal >= 210: zipettePoints = 3 } if zipettePoints > 0 { log.Printf("🎁 [CALC_POINTS] Zipette: %.2f€ → %d points", zipetteTotal, zipettePoints) } } totalPoints := zipettePoints + weedPoints if totalPoints == 0 { if grosSemiTotal > 0 && zipetteTotal == 0 && weedTotal == 0 { log.Printf("ℹ️ [CALC_POINTS] Cmd %d - Catégorie GROS&SEMI uniquement (%.2f€) → 0 points", commandID, grosSemiTotal) return 0, "gros&semi", nil } log.Printf("ℹ️ [CALC_POINTS] Cmd %d - Aucun montant éligible aux points", commandID) return 0, "unknown", nil } var dominantCategory string if zipetteTotal >= weedTotal { dominantCategory = "zipette&co" } else { dominantCategory = "weed&hash" } log.Printf("✅ [CALC_POINTS] Cmd %d - Total: %d points (Zipette: %d, Weed: %d)", commandID, totalPoints, zipettePoints, weedPoints) return totalPoints, dominantCategory, nil } // ✅ FONCTION CORRIGÉE: Calculer et ajouter les points séparément avec le BON BARÈME func (d *Database) CalculateAndAddPointsForCommand(commandID int, username string) (int, error) { query := ` SELECT p.category, ci.prix, ci.quantite FROM command_items ci JOIN products p ON ci.product_id = p.id WHERE ci.command_id = $1 ` rows, err := d.Query(query, commandID) if err != nil { return 0, fmt.Errorf("erreur récupération items: %w", err) } defer rows.Close() zipetteTotal := 0.0 weedTotal := 0.0 grosSemiTotal := 0.0 for rows.Next() { var category string var prix float64 var quantite int if err := rows.Scan(&category, &prix, &quantite); err != nil { log.Printf("⚠️ Erreur scan item: %v", err) continue } // ✅ prix contient déjà le total pour la quantité itemTotal := prix categoryLower := strings.ToLower(category) if categoryLower == "zipette&co" || categoryLower == "zipette_co" { zipetteTotal += itemTotal } else if categoryLower == "gros&semi" || categoryLower == "gros_semi" { grosSemiTotal += itemTotal } else { weedTotal += itemTotal } } log.Printf("💰 [CALC_POINTS] Cmd %d - Zipette: %.2f€, Weed: %.2f€, GrosSemi: %.2f€", commandID, zipetteTotal, weedTotal, grosSemiTotal) // ✅ CALCUL POINTS WEED&HASH - NOUVEAU BARÈME // De 30 à 50€ -> 1 point // De 60 à 150€ -> 2 points // De 160 à 300€ -> 3 points // De 310 à 400€ -> 5 points // 400€ et + -> 10 points weedPoints := 0 if weedTotal > 0 { switch { case weedTotal >= 30 && weedTotal <= 50: weedPoints = 1 case weedTotal >= 60 && weedTotal <= 150: weedPoints = 2 case weedTotal >= 160 && weedTotal <= 300: weedPoints = 3 case weedTotal >= 310 && weedTotal <= 400: weedPoints = 5 case weedTotal >= 400: weedPoints = 10 } if weedPoints > 0 { log.Printf("🎁 [CALC_POINTS] Weed: %.2f€ → %d points", weedTotal, weedPoints) } } // ✅ CALCUL POINTS ZIPETTE&CO - NOUVEAU BARÈME // De 30 à 100€ -> 1 point // De 110 à 200€ -> 2 points (je suppose que c'est 110 et non 1100) // De 210€ et + -> 3 points zipettePoints := 0 if zipetteTotal > 0 { switch { case zipetteTotal >= 30 && zipetteTotal <= 100: zipettePoints = 1 case zipetteTotal >= 110 && zipetteTotal <= 200: zipettePoints = 2 case zipetteTotal >= 210: zipettePoints = 3 } if zipettePoints > 0 { log.Printf("🎁 [CALC_POINTS] Zipette: %.2f€ → %d points", zipetteTotal, zipettePoints) } } totalPoints := 0 // ✅ AJOUTER LES POINTS SÉPARÉMENT PAR CATÉGORIE if zipettePoints > 0 { if err := d.AddClientPointsByCategory(username, zipettePoints, "zipette&co"); err != nil { log.Printf("❌ Erreur ajout points Zipette: %v", err) } else { totalPoints += zipettePoints log.Printf("✅ %d points ZIPETTE ajoutés à %s", zipettePoints, username) } } if weedPoints > 0 { if err := d.AddClientPointsByCategory(username, weedPoints, "weed&hash"); err != nil { log.Printf("❌ Erreur ajout points Weed: %v", err) } else { totalPoints += weedPoints log.Printf("✅ %d points WEED ajoutés à %s", weedPoints, username) } } if totalPoints == 0 { if grosSemiTotal > 0 && zipetteTotal == 0 && weedTotal == 0 { log.Printf("ℹ️ [CALC_POINTS] Cmd %d - Catégorie GROS&SEMI uniquement → 0 points", commandID) } else { log.Printf("ℹ️ [CALC_POINTS] Cmd %d - Aucun point éligible (Weed: %.2f€, Zipette: %.2f€)", commandID, weedTotal, zipetteTotal) } } log.Printf("✅ [CALC_POINTS] Cmd %d - Total: %d points (Zipette: %d, Weed: %d)", commandID, totalPoints, zipettePoints, weedPoints) return totalPoints, 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 par pool et mettre à jour les colonnes DB // Pool[0] → colonne `point`, Pool[1] → colonne `point_zipette` var totalPoints int var pointCategory string var result sql.Result pts0 := CalcPointsFromTiers(poolTotals[0], pools[0].Tiers) pts1 := 0 if len(pools) >= 2 { pts1 = CalcPointsFromTiers(poolTotals[1], pools[1].Tiers) } totalPoints = pts0 + pts1 // Pools supplémentaires (index 2+) → points_extra JSONB for i := 2; i < len(pools); i++ { ptsExtra := CalcPointsFromTiers(poolTotals[i], pools[i].Tiers) if ptsExtra == 0 { continue } totalPoints += ptsExtra poolKey := pools[i].Key _, 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, poolKey, ptsExtra) if err != nil { log.Printf("❌ [CalcPointsTx] Erreur UPDATE points_extra pool[%d]: %v", i, err) return 0, "", fmt.Errorf("erreur mise à jour points pool[%d]: %w", i, err) } log.Printf("💰 [CalcPointsTx] pool[%d] (%s): +%d pts → points_extra", i, pools[i].Name, ptsExtra) } log.Printf("💰 [CalcPointsTx] pool[0]=%d pts, pool[1]=%d pts, total=%d pts", pts0, pts1, totalPoints) if totalPoints == 0 { return 0, "", nil } var categoryParts []string if pts0 > 0 { categoryParts = append(categoryParts, pools[0].Name) } if pts1 > 0 { categoryParts = append(categoryParts, pools[1].Name) } if len(categoryParts) > 0 { pointCategory = strings.Join(categoryParts, " & ") } else { pointCategory = "points" } if len(pools) == 1 || pts1 == 0 { result, err = tx.Exec(` UPDATE clients SET point = point + $1, updated_at = CURRENT_TIMESTAMP WHERE username = $2 `, pts0, username) } else { result, err = tx.Exec(` UPDATE clients SET point = point + $1, point_zipette = point_zipette + $2, updated_at = CURRENT_TIMESTAMP WHERE username = $3 `, pts0, pts1, username) } if err != nil { log.Printf("❌ [CalcPointsTx] Erreur UPDATE points: %v", err) return 0, "", fmt.Errorf("erreur mise à jour points: %w", err) } affected, _ := result.RowsAffected() if affected == 0 { log.Printf("⚠️ [CalcPointsTx] Client %s non trouvé", username) return 0, "", fmt.Errorf("client non trouvé") } 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 } // SaveClientPushToken enregistre le push token Expo d'un client func (d *Database) SaveClientPushToken(clientID int, pushToken string) error { _, err := d.Exec(`UPDATE clients SET push_token = $1 WHERE id = $2`, pushToken, clientID) return err } // DeleteClientPushToken supprime le push token d'un client func (d *Database) DeleteClientPushToken(clientID int) error { _, err := d.Exec(`UPDATE clients SET push_token = NULL WHERE id = $1`, clientID) return err } // GetClientPushToken retourne le push token d'un client par son username func (d *Database) GetClientPushToken(username string) (string, error) { var token string err := d.QueryRow(`SELECT COALESCE(push_token, '') FROM clients WHERE username = $1`, username).Scan(&token) return token, err }