chore: refacto

This commit is contained in:
2026-03-27 22:23:46 +01:00
parent 75bf4caaa1
commit 5380abe8ed
67 changed files with 1751 additions and 2573 deletions
+26 -90
View File
@@ -10,6 +10,7 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"log" "log"
"slices"
"time" "time"
) )
@@ -17,17 +18,15 @@ import (
// ANNULATION ATOMIQUE // ANNULATION ATOMIQUE
// ============================================ // ============================================
func (d *Database) CancelCommandAtomic(commandID int, username, reason string, force bool) (int, map[string]int, error) { 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) log.Printf("🔒 [CancelAtomic] START - cmd=%d, user=%s, force=%v", commandID, username, force)
// ✅ TRANSACTION
tx, err := d.Begin() tx, err := d.Begin()
if err != nil { if err != nil {
return 0, nil, fmt.Errorf("erreur transaction: %w", err) return 0, fmt.Errorf("erreur transaction: %w", err)
} }
defer tx.Rollback() defer tx.Rollback()
// ✅ SELECT FOR UPDATE - Verrouiller la ligne
var currentStatus, cmdUsername, livreurAssign string var currentStatus, cmdUsername, livreurAssign string
err = tx.QueryRow(` err = tx.QueryRow(`
SELECT status, username, COALESCE(livreur_assign, '') SELECT status, username, COALESCE(livreur_assign, '')
@@ -37,163 +36,101 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f
`, commandID).Scan(&currentStatus, &cmdUsername, &livreurAssign) `, commandID).Scan(&currentStatus, &cmdUsername, &livreurAssign)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return 0, nil, fmt.Errorf("commande non trouvée") return 0, fmt.Errorf("commande non trouvée")
} }
if err != nil { if err != nil {
return 0, nil, err return 0, err
} }
log.Printf("📋 [CancelAtomic] Trouvée - status=%s, owner=%s, livreur=%s", currentStatus, cmdUsername, livreurAssign) log.Printf("📋 [CancelAtomic] Trouvée - status=%s, owner=%s, livreur=%s", currentStatus, cmdUsername, livreurAssign)
// ✅ VÉRIFIER PROPRIÉTÉ
if cmdUsername != username { if cmdUsername != username {
return 0, nil, fmt.Errorf("commande ne vous appartient pas") return 0, fmt.Errorf("commande ne vous appartient pas")
} }
// ✅ VÉRIFIER STATUT
nonCancellableStatuses := []string{"livre", "approved", "cancelled", "disabled"} nonCancellableStatuses := []string{"livre", "approved", "cancelled", "disabled"}
for _, s := range nonCancellableStatuses { if slices.Contains(nonCancellableStatuses, currentStatus) {
if currentStatus == s { return 0, fmt.Errorf("impossible d'annuler")
return 0, nil, fmt.Errorf("impossible d'annuler")
} }
}
// ============================================
// ✅ FIX: DÉTECTION CORRECTE DE L'ANNULATION TARDIVE
// ============================================
// Une annulation est tardive UNIQUEMENT si :
// 1. Un livreur est assigné
// 2. Le statut est "en_route" (livreur parti) OU "arrived" (livreur arrivé)
// 3. OU une ETA a été définie (ce qui signifie que le livreur est en route)
isLateCancel := false isLateCancel := false
if livreurAssign != "" { if livreurAssign != "" {
// Cas 1: Statut en_route ou arrived = toujours tardif
if currentStatus == "en_route" || currentStatus == "arrived" { if currentStatus == "en_route" || currentStatus == "arrived" {
isLateCancel = true isLateCancel = true
log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - Statut: %s", currentStatus) log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - Statut: %s", currentStatus)
} else { } else if d.CheckCommandETAExistsAndValid(commandID) {
// Cas 2: Pour les autres statuts, vérifier si une ETA existe
hasRealETA := d.CheckCommandETAExistsAndValid(commandID)
if hasRealETA {
isLateCancel = true isLateCancel = true
log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - ETA définie") log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - ETA définie")
} else { } else {
log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Statut: %s, Pas d'ETA valide", currentStatus) log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Statut: %s, Pas d'ETA valide", currentStatus)
} }
}
} else { } else {
log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Aucun livreur assigné") log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Aucun livreur assigné")
} }
// ✅ SI ANNULATION TARDIVE SANS CONFIRMATION
if isLateCancel && !force { if isLateCancel && !force {
return 0, nil, fmt.Errorf("confirmation requise") return 0, fmt.Errorf("confirmation requise")
} }
// ✅ UPDATE STATUT (avec vérification pour éviter race condition)
result, err := tx.Exec(` result, err := tx.Exec(`
UPDATE commandes UPDATE commandes
SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP
WHERE id = $1 AND status = $2 AND username = $3 WHERE id = $1 AND status = $2 AND username = $3
`, commandID, currentStatus, username) `, commandID, currentStatus, username)
if err != nil { if err != nil {
return 0, nil, err return 0, err
} }
if rows, _ := result.RowsAffected(); rows == 0 {
rows, _ := result.RowsAffected() return 0, fmt.Errorf("commande déjà modifiée")
if rows == 0 {
return 0, nil, fmt.Errorf("commande déjà modifiée")
} }
log.Printf("✅ [CancelAtomic] Statut mis à jour: %s → cancelled", currentStatus) log.Printf("✅ [CancelAtomic] Statut mis à jour: %s → cancelled", currentStatus)
// ✅ REMBOURSER LE STOCK ATOMIQUEMENT if _, err = tx.Exec(`
_, err = tx.Exec(`
UPDATE products p UPDATE products p
SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP
FROM command_items ci FROM command_items ci
WHERE ci.command_id = $1 AND ci.product_id = p.id WHERE ci.command_id = $1 AND ci.product_id = p.id
`, commandID) `, commandID); err != nil {
if err != nil {
log.Printf("⚠️ [CancelAtomic] Erreur remboursement stock: %v", err) log.Printf("⚠️ [CancelAtomic] Erreur remboursement stock: %v", err)
} else { } else {
log.Printf("✅ [CancelAtomic] Stock remboursé") log.Printf("✅ [CancelAtomic] Stock remboursé")
} }
// ============================================
// APPLIQUER PÉNALITÉ SI ANNULATION TARDIVE
// ============================================
penalty := 0 penalty := 0
pointsLost := map[string]int{"weed": 0, "zipette": 0} if isLateCancel {
if isLateCancel && force {
log.Printf("⚠️ [CancelAtomic] Annulation tardive confirmée - Application pénalité") log.Printf("⚠️ [CancelAtomic] Annulation tardive confirmée - Application pénalité")
// ✅ RÉCUPÉRER LES POINTS ACTUELS
var currentPointsWeed, currentPointsZipette int
err := tx.QueryRow(`
SELECT point, point_zipette FROM clients WHERE username = $1
`, username).Scan(&currentPointsWeed, &currentPointsZipette)
if err == nil {
pointsLost["weed"] = currentPointsWeed
pointsLost["zipette"] = currentPointsZipette
// ✅ CALCULER PÉNALITÉ
penalty, _ = d.CalculateCancellationPenalty(username) penalty, _ = d.CalculateCancellationPenalty(username)
if _, err = tx.Exec(`
// ✅ APPLIQUER: Remettre points à 0 + Ajouter pénalité + Incrémenter compteur
_, err = tx.Exec(`
UPDATE clients UPDATE clients
SET point = 0, SET amende = amende + $1,
point_zipette = 0,
amende = amende + $1,
cancellations_count = COALESCE(cancellations_count, 0) + 1, cancellations_count = COALESCE(cancellations_count, 0) + 1,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE username = $2 WHERE username = $2
`, penalty, username) `, penalty, username); err != nil {
if err != nil {
log.Printf("❌ [CancelAtomic] Erreur pénalité: %v", err) log.Printf("❌ [CancelAtomic] Erreur pénalité: %v", err)
} else { } else {
log.Printf("⚠️ [CancelAtomic] Pénalité: %d pts, Points perdus: weed=%d, zipette=%d", log.Printf("⚠️ [CancelAtomic] Pénalité: %d appliquée à %s", penalty, username)
penalty, pointsLost["weed"], pointsLost["zipette"])
}
} }
} }
// ✅ LOG if _, err = tx.Exec(`
_, err = tx.Exec(`
INSERT INTO command_logs (command_id, status, message, author, created_at) INSERT INTO command_logs (command_id, status, message, author, created_at)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) VALUES ($1, 'cancelled', $2, $3, CURRENT_TIMESTAMP)
`, commandID, "cancelled", `, commandID, fmt.Sprintf("Annulée par %s - Raison: %s", username, reason), username); err != nil {
fmt.Sprintf("Annulée par %s - Raison: %s", username, reason),
username)
if err != nil {
log.Printf("⚠️ [CancelAtomic] Erreur log: %v", err) log.Printf("⚠️ [CancelAtomic] Erreur log: %v", err)
} }
// ✅ COMMIT
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
return 0, nil, fmt.Errorf("erreur commit: %w", err) return 0, fmt.Errorf("erreur commit: %w", err)
} }
// ✅ NETTOYER QUEUE (async, après commit)
if livreurAssign != "" { if livreurAssign != "" {
go func() { go func() {
err := d.CleanupCompletedCommandFromQueue(commandID, livreurAssign) if err := d.CleanupCompletedCommandFromQueue(commandID, livreurAssign); err != nil {
if err != nil {
log.Printf("⚠️ [CancelAtomic] Erreur cleanup queue: %v", err) log.Printf("⚠️ [CancelAtomic] Erreur cleanup queue: %v", err)
} }
}() }()
} }
// ✅ INVALIDER CACHES (async)
go func() { go func() {
Redis.Del(RedisCtx, Redis.Del(RedisCtx,
fmt.Sprintf("command:%d", commandID), fmt.Sprintf("command:%d", commandID),
@@ -203,8 +140,7 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f
}() }()
log.Printf("🎉 [CancelAtomic] SUCCÈS - Commande %d annulée", commandID) log.Printf("🎉 [CancelAtomic] SUCCÈS - Commande %d annulée", commandID)
return penalty, nil
return penalty, pointsLost, nil
} }
// ============================================ // ============================================
+55 -338
View File
@@ -10,8 +10,8 @@ import (
) )
func (d *Database) CreateClient(client *models.Client) error { func (d *Database) CreateClient(client *models.Client) error {
query := `INSERT INTO clients (username, password, nom, prenom, telephone, command, point, point_zipette, amende, must_change_password, created_at) 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, 0.0, $6, CURRENT_TIMESTAMP) VALUES ($1, $2, $3, $4, $5, 0, 0.0, $6, CURRENT_TIMESTAMP)
RETURNING id, created_at` RETURNING id, created_at`
err := d.QueryRow(query, client.Username, client.Password, client.Nom, client.Prenom, client.Telephone, client.MustChangePassword).Scan( err := d.QueryRow(query, client.Username, client.Password, client.Nom, client.Prenom, client.Telephone, client.MustChangePassword).Scan(
@@ -29,9 +29,10 @@ func (d *Database) CreateClient(client *models.Client) error {
// GetClientByID récupère un client par son ID // GetClientByID récupère un client par son ID
func (d *Database) GetClientByID(id int) (*models.Client, error) { func (d *Database) GetClientByID(id int) (*models.Client, error) {
var client models.Client var client models.Client
query := `SELECT id, username, password, nom, prenom, telephone, command, point, point_zipette, amende, created_at query := `SELECT id, username, password, nom, prenom, telephone, command, amende, COALESCE(points_extra, '{}'::jsonb), created_at
FROM clients WHERE id = $1` FROM clients WHERE id = $1`
var pointsExtraJSON []byte
err := d.QueryRow(query, id).Scan( err := d.QueryRow(query, id).Scan(
&client.ID, &client.ID,
&client.Username, &client.Username,
@@ -40,9 +41,8 @@ func (d *Database) GetClientByID(id int) (*models.Client, error) {
&client.Prenom, &client.Prenom,
&client.Telephone, &client.Telephone,
&client.Command, &client.Command,
&client.Point,
&client.PointZipette,
&client.Amende, &client.Amende,
&pointsExtraJSON,
&client.CreatedAt, &client.CreatedAt,
) )
@@ -53,12 +53,16 @@ func (d *Database) GetClientByID(id int) (*models.Client, error) {
return nil, fmt.Errorf("erreur lors de la récupération du client: %w", err) 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 return &client, nil
} }
// GetAllClients récupère tous les clients // GetAllClients récupère tous les clients
func (d *Database) GetAllClients() ([]*models.Client, error) { 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 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` FROM clients ORDER BY created_at DESC`
rows, err := d.Query(query) rows, err := d.Query(query)
@@ -79,8 +83,6 @@ func (d *Database) GetAllClients() ([]*models.Client, error) {
&client.Prenom, &client.Prenom,
&client.Telephone, &client.Telephone,
&client.Command, &client.Command,
&client.Point,
&client.PointZipette,
&client.Amende, &client.Amende,
&client.ReferralBalance, &client.ReferralBalance,
&pointsExtraJSON, &pointsExtraJSON,
@@ -107,8 +109,8 @@ func (d *Database) GetAllClients() ([]*models.Client, error) {
func (d *Database) UpdateClient(client *models.Client) error { func (d *Database) UpdateClient(client *models.Client) error {
query := `UPDATE clients query := `UPDATE clients
SET username = $1, password = $2, nom = $3, prenom = $4, telephone = $5, SET username = $1, password = $2, nom = $3, prenom = $4, telephone = $5,
command = $6, point = $7, point_zipette = $8, amende = $9 command = $6, amende = $7
WHERE id = $10` WHERE id = $8`
result, err := d.Exec(query, result, err := d.Exec(query,
client.Username, client.Username,
@@ -117,8 +119,6 @@ func (d *Database) UpdateClient(client *models.Client) error {
client.Prenom, client.Prenom,
client.Telephone, client.Telephone,
client.Command, client.Command,
client.Point,
client.PointZipette,
client.Amende, client.Amende,
client.ID, client.ID,
) )
@@ -135,13 +135,11 @@ func (d *Database) UpdateClient(client *models.Client) error {
return fmt.Errorf("client non trouvé") return fmt.Errorf("client non trouvé")
} }
log.Printf("✅ Client mis à jour: %s (ID: %d)", client.Username, client.ID)
return nil return nil
} }
// DeleteClient supprime un client // DeleteClient supprime un client
func (d *Database) DeleteClient(id int) error { func (d *Database) DeleteClient(id int) error {
// ✅ MODIFIÉ : Supprimer tous les tokens du client avec le user_type "client"
_ = d.RevokeAllUserTokens(id, "client") _ = d.RevokeAllUserTokens(id, "client")
query := `DELETE FROM clients WHERE id = $1` query := `DELETE FROM clients WHERE id = $1`
@@ -160,7 +158,6 @@ func (d *Database) DeleteClient(id int) error {
return fmt.Errorf("client non trouvé") return fmt.Errorf("client non trouvé")
} }
log.Printf("✅ Client supprimé (ID: %d)", id)
return nil return nil
} }
@@ -182,7 +179,6 @@ func (d *Database) UpdateClientPassword(clientID int, hashedPassword string) err
return fmt.Errorf("client non trouvé") return fmt.Errorf("client non trouvé")
} }
log.Printf("✅ Mot de passe client mis à jour (ID: %d)", clientID)
return nil return nil
} }
@@ -204,7 +200,6 @@ func (d *Database) UpdateClientPasswordAndClearFlag(clientID int, hashedPassword
return fmt.Errorf("client non trouvé") return fmt.Errorf("client non trouvé")
} }
log.Printf("✅ Mot de passe client mis à jour + must_change_password=false (ID: %d)", clientID)
return nil return nil
} }
@@ -239,8 +234,7 @@ func (d *Database) GetClientStats(clientID int) (map[string]interface{}, error)
"total_commands": totalCommands, "total_commands": totalCommands,
"pending_commands": pendingCommands, "pending_commands": pendingCommands,
"completed_commands": completedCommands, "completed_commands": completedCommands,
"points": client.Point, "points_extra": client.PointsExtra,
"points_zipette": client.PointZipette,
"amende": client.Amende, "amende": client.Amende,
"member_since": client.CreatedAt, "member_since": client.CreatedAt,
} }
@@ -279,7 +273,6 @@ func (d *Database) PayClientPenalties(username string, amountPaid float64) error
return fmt.Errorf("montant insuffisant: %.2f payé, %.2f requis", amountPaid, currentAmount) return fmt.Errorf("montant insuffisant: %.2f payé, %.2f requis", amountPaid, currentAmount)
} }
// Réinitialiser les pénalités
query := `UPDATE clients query := `UPDATE clients
SET amende = 0, updated_at = CURRENT_TIMESTAMP SET amende = 0, updated_at = CURRENT_TIMESTAMP
WHERE username = $1` WHERE username = $1`
@@ -298,9 +291,6 @@ func (d *Database) PayClientPenalties(username string, amountPaid float64) error
return fmt.Errorf("client non trouvé") 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) cacheKey := fmt.Sprintf("client:%s", username)
Redis.Del(RedisCtx, cacheKey) Redis.Del(RedisCtx, cacheKey)
@@ -329,27 +319,22 @@ func (d *Database) IncrementClientCommandCount(username string) error {
return nil return nil
} }
// ✅ NOUVELLE FONCTION: Ajouter des points selon la catégorie func (d *Database) AddClientPointsByCategory(username string, points int, poolKey string) error {
func (d *Database) AddClientPointsByCategory(username string, points int, category string) error { if poolKey == "" {
var query string poolKey = "pool_0"
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(`
result, err := d.Exec(query, points, username) 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 { if err != nil {
return fmt.Errorf("erreur lors de l'ajout de points: %w", err) return fmt.Errorf("erreur lors de l'ajout de points: %w", err)
} }
rowsAffected, err := result.RowsAffected() rowsAffected, err := result.RowsAffected()
if err != nil { if err != nil {
return fmt.Errorf("erreur lors de la vérification: %w", err) return fmt.Errorf("erreur lors de la vérification: %w", err)
@@ -357,19 +342,31 @@ func (d *Database) AddClientPointsByCategory(username string, points int, catego
if rowsAffected == 0 { if rowsAffected == 0 {
return fmt.Errorf("client non trouvé") return fmt.Errorf("client non trouvé")
} }
log.Printf("✅ %d points (key=%s) ajoutés au client %s", points, poolKey, username)
log.Printf("✅ %d points (%s) ajoutés au client %s (EN DB)", points, category, username)
return nil return nil
} }
// ✅ ANCIENNE FONCTION CONSERVÉE POUR COMPATIBILITÉ (utilise weed/hash par défaut) func (d *Database) CalculateAndAddPointsForCommand(commandID int, username string) (int, error) {
func (d *Database) AddClientPoints(username string, points int) error { tx, err := d.Begin()
return d.AddClientPointsByCategory(username, points, "weed_hash") 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) { func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error) {
client := &models.Client{} client := &models.Client{}
query := `SELECT id, username, password, nom, prenom, telephone, command, point, point_zipette, amende, created_at query := `SELECT id, username, password, nom, prenom, telephone, command, amende, created_at
FROM clients WHERE telephone = $1` FROM clients WHERE telephone = $1`
err := d.QueryRow(query, telephone).Scan( err := d.QueryRow(query, telephone).Scan(
@@ -380,8 +377,6 @@ func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error
&client.Prenom, &client.Prenom,
&client.Telephone, &client.Telephone,
&client.Command, &client.Command,
&client.Point,
&client.PointZipette,
&client.Amende, &client.Amende,
&client.CreatedAt, &client.CreatedAt,
) )
@@ -400,7 +395,7 @@ func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error
func (d *Database) GetClientByUsername(username string) (*models.Client, error) { func (d *Database) GetClientByUsername(username string) (*models.Client, error) {
client := &models.Client{} client := &models.Client{}
var pointsExtraJSON []byte var pointsExtraJSON []byte
query := `SELECT id, username, password, nom, prenom, telephone, command, point, point_zipette, amende, must_change_password, COALESCE(points_extra, '{}'::jsonb), created_at query := `SELECT id, username, password, nom, prenom, telephone, command, amende, must_change_password, COALESCE(points_extra, '{}'::jsonb), created_at
FROM clients WHERE username = $1` FROM clients WHERE username = $1`
err := d.QueryRow(query, username).Scan( err := d.QueryRow(query, username).Scan(
@@ -411,8 +406,6 @@ func (d *Database) GetClientByUsername(username string) (*models.Client, error)
&client.Prenom, &client.Prenom,
&client.Telephone, &client.Telephone,
&client.Command, &client.Command,
&client.Point,
&client.PointZipette,
&client.Amende, &client.Amende,
&client.MustChangePassword, &client.MustChangePassword,
&pointsExtraJSON, &pointsExtraJSON,
@@ -485,23 +478,25 @@ func (d *Database) CheckClientCanOrder(username string) (bool, float64, error) {
} }
// ResetClientPoint réinitialise les points d'un client. // ResetClientPoint réinitialise les points d'un client.
// poolIdx=0 → point, poolIdx=1 → point_zipette, poolIdx=-1 → tous // extraPoolKey != "" → reset points_extra[extraPoolKey] uniquement
// poolIdx>=2 → points_extra[extraPoolKey] // extraPoolKey == "" (poolIdx=-1) → reset total points_extra
func (d *Database) ResetClientPoint(username string, poolIdx int, extraPoolKey string) error { func (d *Database) ResetClientPoint(username string, poolIdx int, extraPoolKey string) error {
var query string var query string
switch { switch {
case poolIdx == 0: case extraPoolKey != "":
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( _, err := d.Exec(
`UPDATE clients SET points_extra = points_extra - $2, updated_at = CURRENT_TIMESTAMP WHERE username = $1`, `UPDATE clients SET points_extra = points_extra - $2, updated_at = CURRENT_TIMESTAMP WHERE username = $1`,
username, extraPoolKey, 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 return err
default: // -1 → reset total default: // -1 ou poolIdx sans clé → reset total
query = `UPDATE clients SET point = 0, point_zipette = 0, points_extra = '{}'::jsonb, updated_at = CURRENT_TIMESTAMP WHERE username = $1` query = `UPDATE clients SET points_extra = '{}'::jsonb, updated_at = CURRENT_TIMESTAMP WHERE username = $1`
} }
result, err := d.Exec(query, username) result, err := d.Exec(query, username)
@@ -649,240 +644,6 @@ func (d *Database) GetClientPenaltiesStats() (map[string]interface{}, error) {
return result, nil 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) { func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int, username string) (int, string, error) {
log.Printf("💰 [CalcPointsTx] START - cmd=%d, user=%s", commandID, username) log.Printf("💰 [CalcPointsTx] START - cmd=%d, user=%s", commandID, username)
@@ -982,8 +743,6 @@ func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int,
pointCategory = "points" pointCategory = "points"
} }
// Écrire tous les pools dans points_extra[pool.Key] (stockage dynamique)
// + maintenir les colonnes legacy point/point_zipette pour la compatibilité admin
for i, pool := range pools { for i, pool := range pools {
if poolPts[i] == 0 { if poolPts[i] == 0 {
continue continue
@@ -1004,29 +763,6 @@ func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int,
log.Printf("💰 [CalcPointsTx] pool[%d] (%s / key=%s): +%d pts", i, pool.Name, pool.Key, poolPts[i]) log.Printf("💰 [CalcPointsTx] pool[%d] (%s / key=%s): +%d pts", i, pool.Name, pool.Key, poolPts[i])
} }
// Maintenir colonnes legacy pour affichage admin
pts0 := poolPts[0]
pts1 := 0
if len(pools) >= 2 {
pts1 = poolPts[1]
}
var legacyErr error
var result sql.Result
if pts1 == 0 {
result, legacyErr = tx.Exec(`UPDATE clients SET point = point + $1, updated_at = CURRENT_TIMESTAMP WHERE username = $2`, pts0, username)
} else {
result, legacyErr = tx.Exec(`UPDATE clients SET point = point + $1, point_zipette = point_zipette + $2, updated_at = CURRENT_TIMESTAMP WHERE username = $3`, pts0, pts1, username)
}
if legacyErr != nil {
log.Printf("❌ [CalcPointsTx] Erreur UPDATE colonnes legacy: %v", legacyErr)
return 0, "", fmt.Errorf("erreur mise à jour points: %w", legacyErr)
}
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) log.Printf("🎉 [CalcPointsTx] SUCCÈS - %d points [%s] → %s", totalPoints, pointCategory, username)
return totalPoints, pointCategory, nil return totalPoints, pointCategory, nil
@@ -1066,22 +802,3 @@ func (d *Database) CanUserAccessCommand(
return exists, err 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
}
+3 -27
View File
@@ -216,7 +216,7 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err
c.referral_used, c.referral_used,
c.livreur_assign, c.livreur_assign,
c.created_at as command_created_at, c.created_at as command_created_at,
COALESCE(p.category, 'weed_hash') as category p.category
FROM command_items ci FROM command_items ci
LEFT JOIN commandes c ON ci.command_id = c.id LEFT JOIN commandes c ON ci.command_id = c.id
LEFT JOIN products p ON ci.product_id = p.id LEFT JOIN products p ON ci.product_id = p.id
@@ -301,14 +301,7 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err
return items, nil return items, nil
} }
// ============================================
// GET COMMAND ITEMS BY USERNAME - VERSION SÉCURISÉE + FIX NULL
// ============================================
func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]interface{}, error) { func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]interface{}, error) {
log.Printf("📦 [GetCommandItemsByUsername] START - username=%s", username)
// ✅ VALIDATION
if err := validateUsername(username); err != nil { if err := validateUsername(username); err != nil {
log.Printf("❌ [GetCommandItemsByUsername] %v", err) log.Printf("❌ [GetCommandItemsByUsername] %v", err)
return nil, err return nil, err
@@ -352,7 +345,7 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte
for rows.Next() { for rows.Next() {
var id, commandID int var id, commandID int
var quantite float64 var quantite float64
var productID sql.NullInt64 // ✅ FIX: NullInt64 var productID sql.NullInt64
var produit, clientUsername, clientNom, clientPrenom, clientTelephone string var produit, clientUsername, clientNom, clientPrenom, clientTelephone string
var deliveryAddress, status sql.NullString var deliveryAddress, status sql.NullString
var prix, totalPrix float64 var prix, totalPrix float64
@@ -370,7 +363,6 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte
return nil, fmt.Errorf("erreur scan: %w", err) return nil, fmt.Errorf("erreur scan: %w", err)
} }
// ✅ CONVERTIR NullInt64
productIDValue := 0 productIDValue := 0
if productID.Valid { if productID.Valid {
productIDValue = int(productID.Int64) productIDValue = int(productID.Int64)
@@ -380,7 +372,7 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte
"id": id, "id": id,
"command_id": commandID, "command_id": commandID,
"produit": produit, "produit": produit,
"product_id": productIDValue, // ✅ FIX "product_id": productIDValue,
"quantite": quantite, "quantite": quantite,
"prix": prix, "prix": prix,
"client_username": clientUsername, "client_username": clientUsername,
@@ -391,7 +383,6 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte
"status": status.String, "status": status.String,
"created_at": createdAt, "created_at": createdAt,
"updated_at": updatedAt, "updated_at": updatedAt,
// Infos commande
"command_status": commandStatus.String, "command_status": commandStatus.String,
"command_address": commandAddress.String, "command_address": commandAddress.String,
"total_prix": totalPrix, "total_prix": totalPrix,
@@ -410,10 +401,6 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte
return items, nil return items, nil
} }
// ============================================
// DELETE COMMAND ITEM - ADMIN ONLY
// ============================================
func (d *Database) DeleteCommandItem(commandID, itemID int) error { func (d *Database) DeleteCommandItem(commandID, itemID int) error {
log.Printf("🗑️ [DeleteCommandItem] START - commandID=%d, itemID=%d", commandID, itemID) log.Printf("🗑️ [DeleteCommandItem] START - commandID=%d, itemID=%d", commandID, itemID)
@@ -451,18 +438,10 @@ func (d *Database) DeleteCommandItem(commandID, itemID int) error {
log.Printf("⚠️ [DeleteCommandItem] Erreur maj total commande: %v", err) log.Printf("⚠️ [DeleteCommandItem] Erreur maj total commande: %v", err)
} }
log.Printf("✅ [DeleteCommandItem] Item %d supprimé de la commande %d", itemID, commandID)
return nil return nil
} }
// ============================================
// UPDATE COMMAND ITEM STATUS - VERSION SÉCURISÉE
// ============================================
func (d *Database) UpdateCommandItemStatus(itemID int, status string) error { func (d *Database) UpdateCommandItemStatus(itemID int, status string) error {
log.Printf("📝 [UpdateCommandItemStatus] START - itemID=%d, status=%s", itemID, status)
// ✅ VALIDATION
if err := validateItemID(itemID); err != nil { if err := validateItemID(itemID); err != nil {
log.Printf("❌ [UpdateCommandItemStatus] %v", err) log.Printf("❌ [UpdateCommandItemStatus] %v", err)
return err return err
@@ -473,7 +452,6 @@ func (d *Database) UpdateCommandItemStatus(itemID int, status string) error {
return err return err
} }
// ✅ VÉRIFIER QUE L'ITEM EXISTE
var exists bool var exists bool
checkQuery := `SELECT EXISTS(SELECT 1 FROM command_items WHERE id = $1)` checkQuery := `SELECT EXISTS(SELECT 1 FROM command_items WHERE id = $1)`
err := d.QueryRow(checkQuery, itemID).Scan(&exists) err := d.QueryRow(checkQuery, itemID).Scan(&exists)
@@ -486,7 +464,6 @@ func (d *Database) UpdateCommandItemStatus(itemID int, status string) error {
return fmt.Errorf("item %d non trouvé", itemID) return fmt.Errorf("item %d non trouvé", itemID)
} }
// ✅ UPDATE
query := `UPDATE command_items query := `UPDATE command_items
SET status = $1, updated_at = CURRENT_TIMESTAMP SET status = $1, updated_at = CURRENT_TIMESTAMP
WHERE id = $2` WHERE id = $2`
@@ -507,6 +484,5 @@ func (d *Database) UpdateCommandItemStatus(itemID int, status string) error {
return fmt.Errorf("item non trouvé") return fmt.Errorf("item non trouvé")
} }
log.Printf("✅ Statut item %d mis à jour: %s", itemID, status)
return nil return nil
} }
+3 -18
View File
@@ -10,11 +10,11 @@ import (
"fmt" "fmt"
"gestion/models" "gestion/models"
"log" "log"
"slices"
"time" "time"
) )
// GetAllCommandsOldestFirst récupère les commandes triées par ancienneté (plus anciennes en premier) // GetAllCommandsOldestFirst récupère les commandes triées par ancienneté (plus anciennes en premier)
// Utilisé pour le système de priorisation automatique
func (d *Database) GetAllCommandsOldestFirst(status, username string) ([]map[string]interface{}, error) { func (d *Database) GetAllCommandsOldestFirst(status, username string) ([]map[string]interface{}, error) {
query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix,
c.livreur_assign, c.created_at, c.updated_at c.livreur_assign, c.created_at, c.updated_at
@@ -27,13 +27,8 @@ func (d *Database) GetAllCommandsOldestFirst(status, username string) ([]map[str
// Filtrer par status // Filtrer par status
if status != "" { if status != "" {
validStatuses := []string{"pending", "assigned", "en_route", "livre", "approved", "cancelled", "disabled"} validStatuses := []string{"pending", "assigned", "en_route", "livre", "approved", "cancelled", "disabled"}
isValid := false
for _, vs := range validStatuses { isValid := slices.Contains(validStatuses, status)
if status == vs {
isValid = true
break
}
}
if !isValid { if !isValid {
return nil, fmt.Errorf("statut invalide: %s", status) return nil, fmt.Errorf("statut invalide: %s", status)
} }
@@ -50,11 +45,8 @@ func (d *Database) GetAllCommandsOldestFirst(status, username string) ([]map[str
argPosition++ argPosition++
} }
// ✅ TRI PAR ANCIENNETÉ: Les plus anciennes d'abord (ASC)
query += " ORDER BY c.created_at ASC" query += " ORDER BY c.created_at ASC"
log.Printf("🔍 [PRIORITY] Query: %s | Args: %v", query, args)
rows, err := d.Query(query, args...) rows, err := d.Query(query, args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur récupération commandes prioritaires: %w", err) return nil, fmt.Errorf("erreur récupération commandes prioritaires: %w", err)
@@ -97,8 +89,6 @@ func (d *Database) GetAllCommandsOldestFirst(status, username string) ([]map[str
return nil, fmt.Errorf("erreur itération résultats: %w", err) return nil, fmt.Errorf("erreur itération résultats: %w", err)
} }
log.Printf("✅ [PRIORITY] %d commandes récupérées (ordre: plus anciennes → plus récentes)", len(commands))
return commands, nil return commands, nil
} }
@@ -145,9 +135,6 @@ func (d *Database) GetOldestPendingCommand() (map[string]interface{}, error) {
command["livreur_assign"] = nil command["livreur_assign"] = nil
} }
log.Printf("📌 [PRIORITY] Commande la plus ancienne: ID=%d, créée le %s",
id, createdAt.Format("2006-01-02 15:04:05"))
return command, nil return command, nil
} }
@@ -199,8 +186,6 @@ func (d *Database) GetPendingCommandsWithPriority() ([]*models.CommandPriority,
return nil, fmt.Errorf("erreur itération résultats priorité: %w", err) return nil, fmt.Errorf("erreur itération résultats priorité: %w", err)
} }
log.Printf("✅ [PRIORITY] %d commandes avec score de priorité calculé", len(commands))
return commands, nil return commands, nil
} }
+67 -244
View File
@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"gestion/models" "gestion/models"
"log" "log"
"slices"
"strings" "strings"
"time" "time"
) )
@@ -42,6 +43,35 @@ func validateAddress(address string) error {
return nil return nil
} }
type basketItem struct {
ProductID int
Quantity float64
Price float64
}
func (d *Database) fetchBasketItems(username string) ([]basketItem, float64, error) {
rows, err := d.Query(`SELECT product_id, quantity, price FROM baskets WHERE username = $1`, username)
if err != nil {
return nil, 0, fmt.Errorf("erreur récupération panier: %w", err)
}
defer rows.Close()
var items []basketItem
total := 0.0
for rows.Next() {
var item basketItem
if err := rows.Scan(&item.ProductID, &item.Quantity, &item.Price); err != nil {
return nil, 0, fmt.Errorf("erreur scan panier: %w", err)
}
items = append(items, item)
total += item.Price
}
if err := rows.Err(); err != nil {
return nil, 0, err
}
return items, total, nil
}
// validateCommandStatus vérifie si le statut est valide // validateCommandStatus vérifie si le statut est valide
func validateCommandStatus(status string) error { func validateCommandStatus(status string) error {
validStatuses := map[string]bool{ validStatuses := map[string]bool{
@@ -63,48 +93,22 @@ func validateCommandStatus(status string) error {
} }
func (d *Database) CreateCommand(username string) (*models.Command, error) { func (d *Database) CreateCommand(username string) (*models.Command, error) {
// Récupérer le client pour obtenir son adresse
var adresse string var adresse string
clientQuery := `SELECT username FROM clients WHERE username = $1` clientQuery := `SELECT username FROM clients WHERE username = $1`
err := d.QueryRow(clientQuery, username).Scan(&adresse) err := d.QueryRow(clientQuery, username).Scan(&adresse)
if err != nil { if err != nil {
// Si le client n'existe pas dans la table clients, utiliser une adresse par défaut
adresse = "Adresse non spécifiée" adresse = "Adresse non spécifiée"
} }
// Récupérer les items du panier basketItems, totalPrix, err := d.fetchBasketItems(username)
basketQuery := `SELECT product_id, quantity, price FROM baskets WHERE username = $1`
rows, err := d.Query(basketQuery, username)
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur lors de la récupération du panier: %w", err) return nil, err
}
defer rows.Close()
type BasketItem struct {
ProductID int
Quantity float64
Price float64
}
var basketItems []BasketItem
totalPrix := 0.0
for rows.Next() {
var item BasketItem
if err := rows.Scan(&item.ProductID, &item.Quantity, &item.Price); err != nil {
return nil, fmt.Errorf("erreur lors du scan du panier: %w", err)
}
basketItems = append(basketItems, item)
// ✅ FIX: Ne PAS multiplier par quantity
totalPrix += item.Price // Prix déjà calculé pour les grammes
} }
if len(basketItems) == 0 { if len(basketItems) == 0 {
return nil, fmt.Errorf("le panier est vide") return nil, fmt.Errorf("le panier est vide")
} }
// Créer la commande
commandQuery := `INSERT INTO commandes (username, status, adresse, total_prix, created_at, updated_at) commandQuery := `INSERT INTO commandes (username, status, adresse, total_prix, created_at, updated_at)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, created_at, updated_at` RETURNING id, created_at, updated_at`
@@ -120,9 +124,7 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) {
return nil, fmt.Errorf("erreur lors de la création de la commande: %w", err) return nil, fmt.Errorf("erreur lors de la création de la commande: %w", err)
} }
// Insérer les items de la commande
for _, item := range basketItems { for _, item := range basketItems {
// Récupérer le nom du produit
productName, err := d.GetProductNameByID(item.ProductID) productName, err := d.GetProductNameByID(item.ProductID)
if err != nil { if err != nil {
productName = "Produit inconnu" productName = "Produit inconnu"
@@ -136,7 +138,6 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) {
} }
} }
// Vider le panier
clearBasketQuery := `DELETE FROM baskets WHERE username = $1` clearBasketQuery := `DELETE FROM baskets WHERE username = $1`
_, err = d.Exec(clearBasketQuery, username) _, err = d.Exec(clearBasketQuery, username)
if err != nil { if err != nil {
@@ -153,7 +154,6 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) {
} }
func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*models.Command, error) { func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*models.Command, error) {
// ✅ SÉCURITÉ: Validation des entrées
if err := validateUsername(username); err != nil { if err := validateUsername(username); err != nil {
return nil, err return nil, err
} }
@@ -162,9 +162,6 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
return nil, err return nil, err
} }
log.Printf("📝 [CreateCommandWithAddress] START - user=%s", username)
// Récupérer les infos du client
client, err := d.GetClientByUsername(username) client, err := d.GetClientByUsername(username)
if err != nil { if err != nil {
log.Printf("⚠️ Client non trouvé: %v", err) log.Printf("⚠️ Client non trouvé: %v", err)
@@ -179,51 +176,26 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
clientTelephone = sanitizeString(client.Telephone) clientTelephone = sanitizeString(client.Telephone)
} }
// Récupérer les items du panier basketItems, totalPrix, err := d.fetchBasketItems(username)
basketQuery := `SELECT product_id, quantity, price FROM baskets WHERE username = $1`
rows, err := d.Query(basketQuery, username)
if err != nil { if err != nil {
log.Printf("❌ Erreur query basket: %v", err) log.Printf("❌ Erreur query basket: %v", err)
return nil, fmt.Errorf("erreur récupération panier: %w", err) return nil, err
}
defer rows.Close()
type BasketItem struct {
ProductID int
Quantity float64
Price float64
}
var basketItems []BasketItem
totalPrix := 0.0
for rows.Next() {
var item BasketItem
if err := rows.Scan(&item.ProductID, &item.Quantity, &item.Price); err != nil {
return nil, fmt.Errorf("erreur scan panier: %w", err)
}
// ✅ SÉCURITÉ: Validation des valeurs
if item.ProductID <= 0 || item.Quantity <= 0 || item.Price < 0 {
return nil, fmt.Errorf("données panier invalides")
}
basketItems = append(basketItems, item)
totalPrix += item.Price
} }
if len(basketItems) == 0 { if len(basketItems) == 0 {
return nil, fmt.Errorf("le panier est vide") return nil, fmt.Errorf("le panier est vide")
} }
// ✅ SÉCURITÉ: Vérifier que le total est cohérent for _, item := range basketItems {
if item.ProductID <= 0 || item.Quantity <= 0 || item.Price < 0 {
return nil, fmt.Errorf("données panier invalides")
}
}
if totalPrix <= 0 || totalPrix > 100000 { if totalPrix <= 0 || totalPrix > 100000 {
return nil, fmt.Errorf("montant de commande invalide: %.2f€", totalPrix) return nil, fmt.Errorf("montant de commande invalide: %.2f€", totalPrix)
} }
log.Printf("✅ Panier validé: %d items, total=%.2f€", len(basketItems), totalPrix)
// Créer la commande en base
commandQuery := `INSERT INTO commandes (username, status, adresse, total_prix, created_at, updated_at) commandQuery := `INSERT INTO commandes (username, status, adresse, total_prix, created_at, updated_at)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, created_at, updated_at` RETURNING id, created_at, updated_at`
@@ -235,13 +207,9 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
&commandID, &createdAt, &updatedAt, &commandID, &createdAt, &updatedAt,
) )
if err != nil { if err != nil {
log.Printf("❌ Erreur INSERT commande: %v", err)
return nil, fmt.Errorf("erreur création commande: %w", err) return nil, fmt.Errorf("erreur création commande: %w", err)
} }
log.Printf("✅ Commande %d créée en base", commandID)
// Insérer les items de la commande AVEC les infos client
for _, item := range basketItems { for _, item := range basketItems {
productName, err := d.GetProductNameByID(item.ProductID) productName, err := d.GetProductNameByID(item.ProductID)
if err != nil || productName == "" { if err != nil || productName == "" {
@@ -265,7 +233,6 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
return nil, fmt.Errorf("erreur insertion items: %w", err) return nil, fmt.Errorf("erreur insertion items: %w", err)
} }
// Décrémenter le stock du produit
stockQuery := `UPDATE products SET stock = stock - $1 WHERE id = $2 AND stock >= $1` stockQuery := `UPDATE products SET stock = stock - $1 WHERE id = $2 AND stock >= $1`
result, err := d.Exec(stockQuery, item.Quantity, item.ProductID) result, err := d.Exec(stockQuery, item.Quantity, item.ProductID)
if err != nil { if err != nil {
@@ -275,28 +242,22 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
rowsAffected, _ := result.RowsAffected() rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 { if rowsAffected == 0 {
// Le stock était déjà réservé lors de l'ajout au panier (DecrementProductStockByID).
// On ne bloque pas la commande : tous les articles doivent être insérés.
log.Printf("⚠️ [CHECKOUT] Stock déjà réservé pour produit %d (double réservation panier/checkout)", item.ProductID) log.Printf("⚠️ [CHECKOUT] Stock déjà réservé pour produit %d (double réservation panier/checkout)", item.ProductID)
} }
} }
// Vider le panier
clearBasketQuery := `DELETE FROM baskets WHERE username = $1` clearBasketQuery := `DELETE FROM baskets WHERE username = $1`
_, err = d.Exec(clearBasketQuery, username) _, err = d.Exec(clearBasketQuery, username)
if err != nil { if err != nil {
log.Printf("⚠️ Erreur vidage panier: %v", err) log.Printf("⚠️ Erreur vidage panier: %v", err)
} }
// Ajouter un log de commande
sanitizedAddress := sanitizeLogMessage(deliveryAddress) sanitizedAddress := sanitizeLogMessage(deliveryAddress)
d.AddCommandLog(commandID, "created", d.AddCommandLog(commandID, "created",
fmt.Sprintf("Commande créée - Adresse: %s - Total: %.2f€ - Client: %s %s", fmt.Sprintf("Commande créée - Adresse: %s - Total: %.2f€ - Client: %s %s",
sanitizedAddress, totalPrix, sanitizeLogMessage(clientNom), sanitizeLogMessage(clientPrenom)), sanitizedAddress, totalPrix, sanitizeLogMessage(clientNom), sanitizeLogMessage(clientPrenom)),
username) username)
log.Printf("🎉 SUCCÈS - Commande %d | User: %s | Total: %.2f€", commandID, username, totalPrix)
command := &models.Command{ command := &models.Command{
ID: commandID, ID: commandID,
Username: username, Username: username,
@@ -310,9 +271,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
return command, nil return command, nil
} }
// ✅ CORRECTION MAJEURE: Utiliser des paramètres préparés au lieu de fmt.Sprintf func (d *Database) GetAllCommands(status, username string) ([]map[string]any, error) {
func (d *Database) GetAllCommands(status, username string) ([]map[string]interface{}, error) {
// ✅ SÉCURITÉ: Validation des paramètres
if username != "" { if username != "" {
if err := validateUsername(username); err != nil { if err := validateUsername(username); err != nil {
return nil, err return nil, err
@@ -327,14 +286,14 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix,
c.livreur_assign, c.created_at, c.updated_at, c.livreur_assign, c.created_at, c.updated_at,
c.proposed_address, c.address_proposal_status c.proposed_address, c.address_proposal_status,
ROW_NUMBER() OVER (PARTITION BY c.username ORDER BY c.id) AS client_order_number
FROM commandes c FROM commandes c
WHERE 1=1` WHERE 1=1`
args := []interface{}{} args := []interface{}{}
argPosition := 1 argPosition := 1
// Filtrage du statut
if status == "" { if status == "" {
query += ` AND c.status IN ('pending', 'assigned', 'en_route', 'arrived', 'livre')` query += ` AND c.status IN ('pending', 'assigned', 'en_route', 'arrived', 'livre')`
} else { } else {
@@ -343,16 +302,13 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
argPosition++ argPosition++
} }
// Filtrage du username
if username != "" { if username != "" {
query += fmt.Sprintf(" AND c.username = $%d", argPosition) query += fmt.Sprintf(" AND c.username = $%d", argPosition)
args = append(args, username) args = append(args, username)
argPosition++ argPosition++
} }
query += " ORDER BY c.created_at DESC LIMIT 1000" // ✅ Protection DoS query += " ORDER BY c.created_at DESC LIMIT 1000"
log.Printf("🔍 [GetAllCommands] Query avec %d args", len(args))
rows, err := d.Query(query, args...) rows, err := d.Query(query, args...)
if err != nil { if err != nil {
@@ -360,7 +316,7 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
} }
defer rows.Close() defer rows.Close()
var commands []map[string]interface{} var commands []map[string]any
for rows.Next() { for rows.Next() {
var id int var id int
var username, status, adresse string var username, status, adresse string
@@ -369,16 +325,16 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
var addressProposalStatus string var addressProposalStatus string
var totalPrix float64 var totalPrix float64
var createdAt, updatedAt time.Time var createdAt, updatedAt time.Time
var clientOrderNumber int
err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, &livreurAssign, &createdAt, &updatedAt, &proposedAddress, &addressProposalStatus) err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, &livreurAssign, &createdAt, &updatedAt, &proposedAddress, &addressProposalStatus, &clientOrderNumber)
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur lors du scan de la commande: %w", err) return nil, fmt.Errorf("erreur lors du scan de la commande: %w", err)
} }
// ✅ SÉCURITÉ: Sanitization de l'adresse
adresse = sanitizeString(adresse) adresse = sanitizeString(adresse)
command := map[string]interface{}{ command := map[string]any{
"id": id, "id": id,
"username": username, "username": username,
"status": status, "status": status,
@@ -387,6 +343,7 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
"created_at": createdAt, "created_at": createdAt,
"updated_at": updatedAt, "updated_at": updatedAt,
"address_proposal_status": addressProposalStatus, "address_proposal_status": addressProposalStatus,
"client_order_number": clientOrderNumber,
} }
if livreurAssign.Valid { if livreurAssign.Valid {
@@ -408,8 +365,6 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err) return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err)
} }
log.Printf("✅ [GetAllCommands] %d commandes récupérées", len(commands))
return commands, nil return commands, nil
} }
@@ -427,11 +382,11 @@ func (d *Database) SetCommandReferralUsed(commandID int, amount float64) error {
return err return err
} }
func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) { func (d *Database) GetCommandByID(id int) (map[string]any, error) {
// ✅ Déjà sécurisé avec paramètre $1 query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign, c.created_at, c.updated_at,
query := `SELECT id, username, status, adresse, total_prix, livreur_assign, created_at, updated_at, c.proposed_address, c.address_proposal_status, c.referral_used,
proposed_address, address_proposal_status, referral_used (SELECT COUNT(*) FROM commandes c2 WHERE c2.username = c.username AND c2.id <= c.id) AS client_order_number
FROM commandes WHERE id = $1` FROM commandes c WHERE c.id = $1`
var commandID int var commandID int
var username, status, adresse string var username, status, adresse string
@@ -440,6 +395,7 @@ func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) {
var addressProposalStatus string var addressProposalStatus string
var totalPrix, referralUsed float64 var totalPrix, referralUsed float64
var createdAt, updatedAt time.Time var createdAt, updatedAt time.Time
var clientOrderNumber int
err := d.QueryRow(query, id).Scan( err := d.QueryRow(query, id).Scan(
&commandID, &commandID,
@@ -453,6 +409,7 @@ func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) {
&proposedAddress, &proposedAddress,
&addressProposalStatus, &addressProposalStatus,
&referralUsed, &referralUsed,
&clientOrderNumber,
) )
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@@ -462,7 +419,7 @@ func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) {
return nil, fmt.Errorf("erreur lors de la récupération de la commande: %w", err) return nil, fmt.Errorf("erreur lors de la récupération de la commande: %w", err)
} }
command := map[string]interface{}{ command := map[string]any{
"id": commandID, "id": commandID,
"username": username, "username": username,
"status": status, "status": status,
@@ -472,6 +429,7 @@ func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) {
"updated_at": updatedAt, "updated_at": updatedAt,
"address_proposal_status": addressProposalStatus, "address_proposal_status": addressProposalStatus,
"referral_used": referralUsed, "referral_used": referralUsed,
"client_order_number": clientOrderNumber,
} }
if livreurAssign.Valid { if livreurAssign.Valid {
@@ -505,7 +463,6 @@ func (d *Database) GetCommandAddress(commandID int) (string, error) {
} }
func (d *Database) UpdateCommandAddress(commandID int, deliveryAddress string) error { func (d *Database) UpdateCommandAddress(commandID int, deliveryAddress string) error {
// ✅ SÉCURITÉ: Validation de l'adresse
if len(deliveryAddress) > 500 { if len(deliveryAddress) > 500 {
return fmt.Errorf("adresse trop longue (max 500 caractères)") return fmt.Errorf("adresse trop longue (max 500 caractères)")
} }
@@ -565,7 +522,6 @@ func (d *Database) ProposeAddressChange(commandID int, proposedAddress, proposed
func (d *Database) RespondToAddressProposal(commandID int, clientUsername string, accepted bool) error { func (d *Database) RespondToAddressProposal(commandID int, clientUsername string, accepted bool) error {
var query string var query string
if accepted { if accepted {
// Remplace l'adresse par la proposition
query = `UPDATE commandes query = `UPDATE commandes
SET adresse = proposed_address, proposed_address = NULL, SET adresse = proposed_address, proposed_address = NULL,
address_proposal_status = 'accepted', updated_at = CURRENT_TIMESTAMP address_proposal_status = 'accepted', updated_at = CURRENT_TIMESTAMP
@@ -600,17 +556,8 @@ func (d *Database) RespondToAddressProposal(commandID int, clientUsername string
// UpdateCommandStatus met à jour le statut d'une commande // UpdateCommandStatus met à jour le statut d'une commande
func (d *Database) UpdateCommandStatus(commandID int, status string) error { func (d *Database) UpdateCommandStatus(commandID int, status string) error {
// ✅ SÉCURITÉ: Validation du statut if err := validateCommandStatus(status); err != nil {
validStatuses := []string{"pending", "assigned", "en_route", "arrived", "livre", "approved", "cancelled", "disabled"} return err
isValid := false
for _, vs := range validStatuses {
if status == vs {
isValid = true
break
}
}
if !isValid {
return fmt.Errorf("statut invalide: %s", status)
} }
query := `UPDATE commandes SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2` query := `UPDATE commandes SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2`
@@ -632,9 +579,7 @@ func (d *Database) UpdateCommandStatus(commandID int, status string) error {
return nil return nil
} }
// AddCommandLog ajoute un log pour une commande
func (d *Database) AddCommandLog(commandID int, status, message, author string) error { func (d *Database) AddCommandLog(commandID int, status, message, author string) error {
// ✅ SÉCURITÉ: Sanitize le message pour éviter l'injection dans les logs
sanitizedMessage := sanitizeLogMessage(message) sanitizedMessage := sanitizeLogMessage(message)
sanitizedAuthor := sanitizeLogMessage(author) sanitizedAuthor := sanitizeLogMessage(author)
@@ -643,7 +588,6 @@ func (d *Database) AddCommandLog(commandID int, status, message, author string)
_, err := d.Exec(query, commandID, status, sanitizedMessage, sanitizedAuthor) _, err := d.Exec(query, commandID, status, sanitizedMessage, sanitizedAuthor)
if err != nil { if err != nil {
// Si la table n'existe pas, on ne retourne pas d'erreur pour ne pas bloquer
log.Printf("⚠️ Avertissement: impossible d'ajouter le log (table command_logs peut-être manquante): %v", err) log.Printf("⚠️ Avertissement: impossible d'ajouter le log (table command_logs peut-être manquante): %v", err)
return nil return nil
} }
@@ -652,7 +596,7 @@ func (d *Database) AddCommandLog(commandID int, status, message, author string)
} }
// GetCommandLogs récupère tous les logs d'une commande // GetCommandLogs récupère tous les logs d'une commande
func (d *Database) GetCommandLogs(commandID int) ([]map[string]interface{}, error) { func (d *Database) GetCommandLogs(commandID int) ([]map[string]any, error) {
query := `SELECT id, command_id, status, message, author, created_at query := `SELECT id, command_id, status, message, author, created_at
FROM command_logs FROM command_logs
WHERE command_id = $1 WHERE command_id = $1
@@ -662,11 +606,11 @@ func (d *Database) GetCommandLogs(commandID int) ([]map[string]interface{}, erro
if err != nil { if err != nil {
// Si la table n'existe pas, retourner un tableau vide au lieu d'une erreur // Si la table n'existe pas, retourner un tableau vide au lieu d'une erreur
log.Printf("⚠️ Avertissement: impossible de récupérer les logs: %v", err) log.Printf("⚠️ Avertissement: impossible de récupérer les logs: %v", err)
return []map[string]interface{}{}, nil return []map[string]any{}, nil
} }
defer rows.Close() defer rows.Close()
var logs []map[string]interface{} var logs []map[string]any
for rows.Next() { for rows.Next() {
var id, commandID int var id, commandID int
var status, message, author string var status, message, author string
@@ -677,7 +621,7 @@ func (d *Database) GetCommandLogs(commandID int) ([]map[string]interface{}, erro
return nil, fmt.Errorf("erreur lors du scan du log: %w", err) return nil, fmt.Errorf("erreur lors du scan du log: %w", err)
} }
logEntry := map[string]interface{}{ logEntry := map[string]any{
"id": id, "id": id,
"command_id": commandID, "command_id": commandID,
"status": status, "status": status,
@@ -695,108 +639,14 @@ func (d *Database) GetCommandLogs(commandID int) ([]map[string]interface{}, erro
return logs, nil return logs, nil
} }
// ✅ CORRECTION MAJEURE: Utiliser des paramètres préparés
func (d *Database) GetCommandsWithFilter(status, username string, excludeApproved bool) ([]map[string]interface{}, error) {
query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign, c.created_at, c.updated_at
FROM commandes c WHERE 1=1`
args := []interface{}{}
argPosition := 1
// ✅ Filtrer par username si fourni
if username != "" {
query += fmt.Sprintf(" AND c.username = $%d", argPosition)
args = append(args, username)
argPosition++
}
// ✅ Filtrer par status si fourni avec validation
if status != "" {
validStatuses := []string{"pending", "assigned", "en_route", "arrived", "livre", "approved", "cancelled", "disabled"}
isValid := false
for _, vs := range validStatuses {
if status == vs {
isValid = true
break
}
}
if !isValid {
return nil, fmt.Errorf("statut invalide")
}
query += fmt.Sprintf(" AND c.status = $%d", argPosition)
args = append(args, status)
argPosition++
}
// ✅ Exclure les commandes approved si demandé
if excludeApproved {
query += " AND c.status != 'approved'"
log.Printf("🔍 [FILTER] Exclusion des commandes approved activée")
}
query += " ORDER BY c.created_at DESC"
log.Printf("🔍 [FILTER] Query: %s | Args: %v", query, args)
rows, err := d.Query(query, args...)
if err != nil {
return nil, fmt.Errorf("erreur lors de la récupération des commandes: %w", err)
}
defer rows.Close()
var commands []map[string]interface{}
for rows.Next() {
var id int
var username, status, adresse string
var livreurAssign sql.NullString
var totalPrix float64
var createdAt, updatedAt time.Time
err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, &livreurAssign, &createdAt, &updatedAt)
if err != nil {
return nil, fmt.Errorf("erreur lors du scan de la commande: %w", err)
}
command := map[string]interface{}{
"id": id,
"username": username,
"status": status,
"adresse": adresse,
"total_prix": totalPrix,
"created_at": createdAt,
"updated_at": updatedAt,
}
if livreurAssign.Valid {
command["livreur_assign"] = livreurAssign.String
} else {
command["livreur_assign"] = nil
}
commands = append(commands, command)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err)
}
log.Printf("✅ [FILTER] %d commandes trouvées (excludeApproved=%v)", len(commands), excludeApproved)
return commands, nil
}
// ✅ NOUVELLE FONCTION: Sanitize les messages de log
func sanitizeLogMessage(message string) string { func sanitizeLogMessage(message string) string {
// Supprimer les caractères de contrôle et limiter la longueur
sanitized := strings.Map(func(r rune) rune { sanitized := strings.Map(func(r rune) rune {
if r < 32 || r == 127 { if r < 32 || r == 127 {
return -1 // Supprimer les caractères de contrôle return -1
} }
return r return r
}, message) }, message)
// Limiter à 1000 caractères
if len(sanitized) > 1000 { if len(sanitized) > 1000 {
sanitized = sanitized[:1000] sanitized = sanitized[:1000]
} }
@@ -805,17 +655,13 @@ func sanitizeLogMessage(message string) string {
} }
func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (int, error) { func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (int, error) {
log.Printf("🔒 [ValidateAtomic] START - cmd=%d, admin=%s", commandID, adminUsername)
// ✅ ÉTAPE 1: Démarrer une transaction
tx, err := d.Begin() tx, err := d.Begin()
if err != nil { if err != nil {
log.Printf("❌ [ValidateAtomic] Erreur début transaction: %v", err) log.Printf("❌ [ValidateAtomic] Erreur début transaction: %v", err)
return 0, fmt.Errorf("erreur transaction: %w", err) return 0, fmt.Errorf("erreur transaction: %w", err)
} }
defer tx.Rollback() // Rollback automatique si pas de commit defer tx.Rollback()
// ✅ ÉTAPE 2: SELECT FOR UPDATE pour verrouiller la commande
var currentStatus, cmdUsername, livreurAssign string var currentStatus, cmdUsername, livreurAssign string
var totalPrix float64 var totalPrix float64
err = tx.QueryRow(` err = tx.QueryRow(`
@@ -837,28 +683,19 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (
log.Printf("📋 [ValidateAtomic] Commande trouvée - status=%s, client=%s, livreur=%s", log.Printf("📋 [ValidateAtomic] Commande trouvée - status=%s, client=%s, livreur=%s",
currentStatus, cmdUsername, livreurAssign) currentStatus, cmdUsername, livreurAssign)
// ✅ ÉTAPE 3: Vérifier que le statut permet la validation
validStatuses := []string{"assigned", "en_route", "pending", "livre"} validStatuses := []string{"assigned", "en_route", "pending", "livre"}
isValid := false isValid := slices.Contains(validStatuses, currentStatus)
for _, s := range validStatuses {
if currentStatus == s {
isValid = true
break
}
}
if !isValid { if !isValid {
log.Printf("❌ [ValidateAtomic] Statut invalide pour validation: %s", currentStatus) log.Printf("❌ [ValidateAtomic] Statut invalide pour validation: %s", currentStatus)
return 0, fmt.Errorf("statut invalide pour validation: %s", currentStatus) return 0, fmt.Errorf("statut invalide pour validation: %s", currentStatus)
} }
// ✅ ÉTAPE 4: Vérifier si déjà approuvée (double protection)
if currentStatus == "approved" { if currentStatus == "approved" {
log.Printf("⚠️ [ValidateAtomic] Commande %d déjà approuvée", commandID) log.Printf("⚠️ [ValidateAtomic] Commande %d déjà approuvée", commandID)
return 0, fmt.Errorf("commande déjà approuvée") return 0, fmt.Errorf("commande déjà approuvée")
} }
// ✅ ÉTAPE 5: UPDATE avec vérification du statut (protection race condition)
result, err := tx.Exec(` result, err := tx.Exec(`
UPDATE commandes UPDATE commandes
SET status = 'approved', updated_at = CURRENT_TIMESTAMP SET status = 'approved', updated_at = CURRENT_TIMESTAMP
@@ -876,14 +713,10 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (
return 0, fmt.Errorf("commande déjà modifiée par une autre requête") return 0, fmt.Errorf("commande déjà modifiée par une autre requête")
} }
log.Printf("✅ [ValidateAtomic] Statut mis à jour: %s → approved", currentStatus)
// ✅ ÉTAPE 6: Calculer et ajouter les points au client
totalPoints := 0 totalPoints := 0
if cmdUsername != "" { if cmdUsername != "" {
log.Printf("🔍 [ValidateAtomic] Calcul points pour client: %s", cmdUsername) log.Printf("🔍 [ValidateAtomic] Calcul points pour client: %s", cmdUsername)
// Utiliser la version transactionnelle du calcul de points
points, _, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, cmdUsername) points, _, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, cmdUsername)
if err != nil { if err != nil {
log.Printf("❌ [ValidateAtomic] Erreur calcul/ajout points: %v", err) log.Printf("❌ [ValidateAtomic] Erreur calcul/ajout points: %v", err)
@@ -893,7 +726,6 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (
log.Printf("✅ [ValidateAtomic] %d points attribués à %s", totalPoints, cmdUsername) log.Printf("✅ [ValidateAtomic] %d points attribués à %s", totalPoints, cmdUsername)
// ✅ ÉTAPE 7: Incrémenter le compteur de commandes du client
_, err = tx.Exec(` _, err = tx.Exec(`
UPDATE clients UPDATE clients
SET command = command + 1, updated_at = CURRENT_TIMESTAMP SET command = command + 1, updated_at = CURRENT_TIMESTAMP
@@ -901,13 +733,11 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (
`, cmdUsername) `, cmdUsername)
if err != nil { if err != nil {
log.Printf("⚠️ [ValidateAtomic] Erreur incrémentation compteur: %v", err) log.Printf("⚠️ [ValidateAtomic] Erreur incrémentation compteur: %v", err)
// On ne bloque pas pour ça
} else { } else {
log.Printf("✅ [ValidateAtomic] Compteur commandes incrémenté pour %s", cmdUsername) log.Printf("✅ [ValidateAtomic] Compteur commandes incrémenté pour %s", cmdUsername)
} }
} }
// ✅ ÉTAPE 8: Ajouter un log dans command_logs
_, err = tx.Exec(` _, err = tx.Exec(`
INSERT INTO command_logs (command_id, status, message, author, created_at) INSERT INTO command_logs (command_id, status, message, author, created_at)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)
@@ -917,10 +747,8 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (
if err != nil { if err != nil {
log.Printf("⚠️ [ValidateAtomic] Erreur ajout log: %v", err) log.Printf("⚠️ [ValidateAtomic] Erreur ajout log: %v", err)
// On ne bloque pas pour ça
} }
// ✅ ÉTAPE 9: Commit de la transaction
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
log.Printf("❌ [ValidateAtomic] Erreur COMMIT: %v", err) log.Printf("❌ [ValidateAtomic] Erreur COMMIT: %v", err)
return 0, fmt.Errorf("erreur commit transaction: %w", err) return 0, fmt.Errorf("erreur commit transaction: %w", err)
@@ -929,12 +757,9 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (
log.Printf("🎉 [ValidateAtomic] SUCCÈS - Commande %d validée, %d points attribués", log.Printf("🎉 [ValidateAtomic] SUCCÈS - Commande %d validée, %d points attribués",
commandID, totalPoints) commandID, totalPoints)
// ✅ ÉTAPE 10: Optimiser la queue du livreur (APRÈS le commit)
// Cette opération est faite en dehors de la transaction car elle touche Redis
if livreurAssign != "" { if livreurAssign != "" {
log.Printf("📦 [ValidateAtomic] Optimisation queue pour livreur: %s", livreurAssign) log.Printf("📦 [ValidateAtomic] Optimisation queue pour livreur: %s", livreurAssign)
go func() { go func() {
// Async pour ne pas bloquer la réponse
err := d.CompleteDeliveryAndProcessNext(livreurAssign, commandID) err := d.CompleteDeliveryAndProcessNext(livreurAssign, commandID)
if err != nil { if err != nil {
log.Printf("⚠️ [ValidateAtomic] Erreur optimisation queue: %v", err) log.Printf("⚠️ [ValidateAtomic] Erreur optimisation queue: %v", err)
@@ -942,9 +767,7 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (
}() }()
} }
// ✅ ÉTAPE 11: Invalider les caches Redis (APRÈS le commit)
go func() { go func() {
// Async pour ne pas bloquer
commandCacheKey := fmt.Sprintf("command:%d", commandID) commandCacheKey := fmt.Sprintf("command:%d", commandID)
Redis.Del(RedisCtx, commandCacheKey) Redis.Del(RedisCtx, commandCacheKey)
+12 -86
View File
@@ -4,11 +4,12 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"log" "log"
"slices"
"time" "time"
) )
// GetAvailableDeliveryPersons récupère tous les livreurs disponibles // GetAvailableDeliveryPersons récupère tous les livreurs disponibles
func (d *Database) GetAvailableDeliveryPersons() ([]map[string]interface{}, error) { func (d *Database) GetAvailableDeliveryPersons() ([]map[string]any, error) {
query := `SELECT id, username, total, livraison query := `SELECT id, username, total, livraison
FROM users FROM users
WHERE role = 'livreur' WHERE role = 'livreur'
@@ -20,7 +21,7 @@ func (d *Database) GetAvailableDeliveryPersons() ([]map[string]interface{}, erro
} }
defer rows.Close() defer rows.Close()
var livreurs []map[string]interface{} var livreurs []map[string]any
for rows.Next() { for rows.Next() {
var id int var id int
var username string var username string
@@ -31,7 +32,7 @@ func (d *Database) GetAvailableDeliveryPersons() ([]map[string]interface{}, erro
return nil, fmt.Errorf("erreur lors du scan du livreur: %w", err) return nil, fmt.Errorf("erreur lors du scan du livreur: %w", err)
} }
livreur := map[string]interface{}{ livreur := map[string]any{
"id": id, "id": id,
"username": username, "username": username,
"total": total, "total": total,
@@ -47,17 +48,13 @@ func (d *Database) GetAvailableDeliveryPersons() ([]map[string]interface{}, erro
func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) error { func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) error {
log.Printf("📦 [AssignDeliveryPerson] START - commandID=%d, livreur=%s", commandID, livreurUsername) log.Printf("📦 [AssignDeliveryPerson] START - commandID=%d, livreur=%s", commandID, livreurUsername)
// ✅ DÉMARRER UNE TRANSACTION
tx, err := d.Begin() tx, err := d.Begin()
if err != nil { if err != nil {
log.Printf("❌ Erreur démarrage transaction: %v", err) log.Printf("❌ Erreur démarrage transaction: %v", err)
return fmt.Errorf("erreur démarrage transaction: %w", err) return fmt.Errorf("erreur démarrage transaction: %w", err)
} }
defer tx.Rollback() // Rollback automatique si non commité defer tx.Rollback()
// ============================================
// ÉTAPE 1: Vérifier le livreur (DANS la transaction)
// ============================================
var role string var role string
checkQuery := `SELECT role FROM users WHERE username = $1 FOR UPDATE` checkQuery := `SELECT role FROM users WHERE username = $1 FOR UPDATE`
err = tx.QueryRow(checkQuery, livreurUsername).Scan(&role) err = tx.QueryRow(checkQuery, livreurUsername).Scan(&role)
@@ -74,18 +71,12 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e
return fmt.Errorf("l'utilisateur n'est pas un livreur") return fmt.Errorf("l'utilisateur n'est pas un livreur")
} }
log.Printf(" ✅ Livreur valide: %s", livreurUsername)
// ============================================
// ÉTAPE 2: Vérifier et VERROUILLER la commande
// ✅ FOR UPDATE empêche les modifications concurrentes
// ============================================
var currentStatus string var currentStatus string
var currentLivreur sql.NullString var currentLivreur sql.NullString
statusQuery := `SELECT status, livreur_assign statusQuery := `SELECT status, livreur_assign
FROM commandes FROM commandes
WHERE id = $1 WHERE id = $1
FOR UPDATE` // ⚠️ VERROUILLAGE CRITIQUE FOR UPDATE`
err = tx.QueryRow(statusQuery, commandID).Scan(&currentStatus, &currentLivreur) err = tx.QueryRow(statusQuery, commandID).Scan(&currentStatus, &currentLivreur)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@@ -97,33 +88,14 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e
return fmt.Errorf("erreur lors de la vérification de la commande: %w", err) return fmt.Errorf("erreur lors de la vérification de la commande: %w", err)
} }
log.Printf(" ✅ Commande trouvée: status=%s, livreur_assign=%s",
currentStatus, currentLivreur.String)
// ============================================
// ÉTAPE 3: Vérifier que la commande est assignable
// ============================================
// ✅ Vérifier le statut (pending ou assigned pour permettre la réassignation)
validStatusesForAssignment := []string{"pending", "assigned"} validStatusesForAssignment := []string{"pending", "assigned"}
isValidStatus := false isValidStatus := slices.Contains(validStatusesForAssignment, currentStatus)
for _, vs := range validStatusesForAssignment {
if currentStatus == vs {
isValidStatus = true
break
}
}
if !isValidStatus { if !isValidStatus {
log.Printf("❌ Statut invalide pour assignation: %s", currentStatus) log.Printf("❌ Statut invalide pour assignation: %s", currentStatus)
return fmt.Errorf("commande en statut '%s', impossible d'assigner un livreur", currentStatus) return fmt.Errorf("commande en statut '%s', impossible d'assigner un livreur", currentStatus)
} }
log.Printf(" ✅ Statut valide pour assignation: %s", currentStatus)
// ============================================
// ÉTAPE 4: Assigner le livreur (ATOMIQUE)
// ============================================
updateQuery := `UPDATE commandes updateQuery := `UPDATE commandes
SET livreur_assign = $1, SET livreur_assign = $1,
status = 'assigned', status = 'assigned',
@@ -147,11 +119,6 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e
return fmt.Errorf("impossible d'assigner la commande (déjà assignée ou statut changé)") return fmt.Errorf("impossible d'assigner la commande (déjà assignée ou statut changé)")
} }
log.Printf(" ✅ Commande assignée au livreur: %s", livreurUsername)
// ============================================
// ÉTAPE 5: Ajouter un log (DANS la transaction)
// ============================================
logQuery := `INSERT INTO command_logs (command_id, status, message, author, created_at) logQuery := `INSERT INTO command_logs (command_id, status, message, author, created_at)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)` VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)`
@@ -163,28 +130,22 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e
// Non bloquant // Non bloquant
} }
// ============================================
// ✅ COMMIT de la transaction
// ============================================
err = tx.Commit() err = tx.Commit()
if err != nil { if err != nil {
log.Printf("❌ Erreur COMMIT: %v", err) log.Printf("❌ Erreur COMMIT: %v", err)
return fmt.Errorf("erreur commit transaction: %w", err) return fmt.Errorf("erreur commit transaction: %w", err)
} }
log.Printf("🎉 [AssignDeliveryPerson] SUCCÈS - Commande %d assignée à %s", commandID, livreurUsername)
log.Printf(" Workflow: pending → ✅ assigned (TRANSACTION COMMITTED)")
return nil return nil
} }
// GetDeliveryPersonCommands récupère les commandes assignées à un livreur // GetDeliveryPersonCommands récupère les commandes assignées à un livreur
func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status string) ([]map[string]interface{}, error) { func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status string) ([]map[string]any, error) {
query := `SELECT id, username, status, adresse, total_prix, livreur_assign, created_at, updated_at query := `SELECT id, username, status, adresse, total_prix, livreur_assign, created_at, updated_at
FROM commandes FROM commandes
WHERE livreur_assign = $1` WHERE livreur_assign = $1`
args := []interface{}{livreurUsername} args := []any{livreurUsername}
if status != "" { if status != "" {
query += " AND status = $2" query += " AND status = $2"
@@ -199,7 +160,7 @@ func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status stri
} }
defer rows.Close() defer rows.Close()
var commands []map[string]interface{} var commands []map[string]any
for rows.Next() { for rows.Next() {
var id int var id int
var username, status, adresse string var username, status, adresse string
@@ -212,7 +173,7 @@ func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status stri
return nil, fmt.Errorf("erreur lors du scan: %w", err) return nil, fmt.Errorf("erreur lors du scan: %w", err)
} }
command := map[string]interface{}{ command := map[string]any{
"id": id, "id": id,
"username": username, "username": username,
"status": status, "status": status,
@@ -228,7 +189,6 @@ func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status stri
return commands, nil return commands, nil
} }
// ✅ NOUVELLE MÉTHODE: IncrementLivreurDeliveryCount incrémente le compteur de livraisons d'un livreur
func (d *Database) IncrementLivreurDeliveryCount(livreurUsername string) error { func (d *Database) IncrementLivreurDeliveryCount(livreurUsername string) error {
query := `UPDATE users query := `UPDATE users
SET livraison = livraison + 1, SET livraison = livraison + 1,
@@ -249,14 +209,10 @@ func (d *Database) IncrementLivreurDeliveryCount(livreurUsername string) error {
return fmt.Errorf("livreur non trouvé") return fmt.Errorf("livreur non trouvé")
} }
log.Printf("✅ Livraison incrémentée pour le livreur: %s", livreurUsername)
return nil return nil
} }
func (d *Database) ApproveDelivery(commandID int, clientUsername string) error { func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
log.Printf("📝 [ApproveDelivery] START - commandID=%d, client=%s", commandID, clientUsername)
// ✅ DÉMARRER UNE TRANSACTION
tx, err := d.Begin() tx, err := d.Begin()
if err != nil { if err != nil {
log.Printf("❌ Erreur démarrage transaction: %v", err) log.Printf("❌ Erreur démarrage transaction: %v", err)
@@ -264,16 +220,13 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
} }
defer tx.Rollback() defer tx.Rollback()
// ============================================
// ÉTAPE 1: Vérifier et VERROUILLER la commande
// ============================================
var commandUsername, currentStatus string var commandUsername, currentStatus string
var livreurAssign sql.NullString var livreurAssign sql.NullString
checkQuery := `SELECT username, status, livreur_assign checkQuery := `SELECT username, status, livreur_assign
FROM commandes FROM commandes
WHERE id = $1 WHERE id = $1
FOR UPDATE` // ⚠️ VERROUILLAGE CRITIQUE FOR UPDATE`
err = tx.QueryRow(checkQuery, commandID).Scan(&commandUsername, &currentStatus, &livreurAssign) err = tx.QueryRow(checkQuery, commandID).Scan(&commandUsername, &currentStatus, &livreurAssign)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@@ -283,25 +236,14 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
return fmt.Errorf("erreur lors de la vérification de la commande: %w", err) return fmt.Errorf("erreur lors de la vérification de la commande: %w", err)
} }
// ============================================
// ÉTAPE 2: Validations métier
// ============================================
// ✅ Vérifier que c'est bien la commande du client
if commandUsername != clientUsername { if commandUsername != clientUsername {
return fmt.Errorf("cette commande ne vous appartient pas") return fmt.Errorf("cette commande ne vous appartient pas")
} }
// ✅ Vérifier le statut
if currentStatus != "livre" { if currentStatus != "livre" {
return fmt.Errorf("cette commande n'est pas encore livrée (statut actuel: %s)", currentStatus) return fmt.Errorf("cette commande n'est pas encore livrée (statut actuel: %s)", currentStatus)
} }
log.Printf(" ✅ Validations OK: client=%s, status=%s", commandUsername, currentStatus)
// ============================================
// ÉTAPE 3: Mettre à jour le statut (ATOMIQUE)
// ============================================
updateQuery := `UPDATE commandes updateQuery := `UPDATE commandes
SET status = 'approved', SET status = 'approved',
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
@@ -322,11 +264,6 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
return fmt.Errorf("impossible d'approuver: statut changé ou commande introuvable") return fmt.Errorf("impossible d'approuver: statut changé ou commande introuvable")
} }
log.Printf(" ✅ Statut mis à jour: livre → approved")
// ============================================
// ÉTAPE 4: Incrémenter le compteur du livreur (ATOMIQUE)
// ============================================
if livreurAssign.Valid && livreurAssign.String != "" { if livreurAssign.Valid && livreurAssign.String != "" {
incrementQuery := `UPDATE users incrementQuery := `UPDATE users
SET livraison = livraison + 1, SET livraison = livraison + 1,
@@ -337,7 +274,6 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
result, err := tx.Exec(incrementQuery, livreurAssign.String) result, err := tx.Exec(incrementQuery, livreurAssign.String)
if err != nil { if err != nil {
log.Printf("⚠️ Erreur incrémentation livreur: %v", err) log.Printf("⚠️ Erreur incrémentation livreur: %v", err)
// Non bloquant mais on continue dans la transaction
} else { } else {
rows, _ := result.RowsAffected() rows, _ := result.RowsAffected()
if rows > 0 { if rows > 0 {
@@ -346,9 +282,6 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
} }
} }
// ============================================
// ÉTAPE 5: Ajouter un log (DANS la transaction)
// ============================================
logQuery := `INSERT INTO command_logs (command_id, status, message, author, created_at) logQuery := `INSERT INTO command_logs (command_id, status, message, author, created_at)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)` VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)`
@@ -357,20 +290,13 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
clientUsername) clientUsername)
if err != nil { if err != nil {
log.Printf("⚠️ Erreur ajout log: %v", err) log.Printf("⚠️ Erreur ajout log: %v", err)
// Non bloquant
} }
// ============================================
// ✅ COMMIT de la transaction
// ============================================
err = tx.Commit() err = tx.Commit()
if err != nil { if err != nil {
log.Printf("❌ Erreur COMMIT: %v", err) log.Printf("❌ Erreur COMMIT: %v", err)
return fmt.Errorf("erreur commit transaction: %w", err) return fmt.Errorf("erreur commit transaction: %w", err)
} }
log.Printf("🎉 [ApproveDelivery] SUCCÈS - Commande %d approuvée par %s", commandID, clientUsername)
log.Printf(" Workflow: livre → ✅ approved (TRANSACTION COMMITTED)")
return nil return nil
} }
+1 -2
View File
@@ -14,7 +14,7 @@ func (d *Database) GetDeliveryIssues(status string) ([]models.DeliveryIssue, err
FROM delivery_issues FROM delivery_issues
` `
var args []interface{} var args []any
if status != "" { if status != "" {
query += " WHERE status = $1" query += " WHERE status = $1"
args = append(args, status) args = append(args, status)
@@ -76,7 +76,6 @@ func (d *Database) CreateDeliveryIssue(commandID int, issueType, description, re
return nil, fmt.Errorf("erreur création problème: %w", err) return nil, fmt.Errorf("erreur création problème: %w", err)
} }
log.Printf("✅ Problème créé: ID=%d, Type=%s, Commande=%d", issue.ID, issueType, commandID)
return &issue, nil return &issue, nil
} }
+3 -38
View File
@@ -19,10 +19,6 @@ var allowedStatuses = map[string]bool{
"busy": true, "busy": true,
} }
// ============================================
// 📊 STATISTIQUES LIVREUR
// ============================================
// CountDeliveriesByStatus compte les livraisons d'un livreur par statut // CountDeliveriesByStatus compte les livraisons d'un livreur par statut
func (d *Database) CountDeliveriesByStatus(livreurUsername string, statuses string) (int, error) { func (d *Database) CountDeliveriesByStatus(livreurUsername string, statuses string) (int, error) {
if statuses == "" { if statuses == "" {
@@ -231,17 +227,6 @@ func (d *Database) GetDeliveryPersonHistory(livreurUsername string, limit, offse
return history, nil return history, nil
} }
// ============================================
// 📍 GESTION POSITION GPS
// ============================================
// ⚠️ REMARQUE: Les fonctions UpdateDeliveryPersonLocation et GetDeliveryPersonLocation
// sont déjà définies dans db/delivery_db.go
// Nous réutilisons ces fonctions existantes au lieu de les redéfinir ici
// ============================================
// 🔄 GESTION STATUT LIVREUR
// ============================================
// GetDeliveryPersonStatus récupère le statut d'un livreur // GetDeliveryPersonStatus récupère le statut d'un livreur
func (d *Database) GetDeliveryPersonStatus(livreurUsername string) (string, error) { func (d *Database) GetDeliveryPersonStatus(livreurUsername string) (string, error) {
statusKey := fmt.Sprintf("delivery:status:%s", livreurUsername) statusKey := fmt.Sprintf("delivery:status:%s", livreurUsername)
@@ -277,10 +262,6 @@ func (d *Database) UpdateDeliveryPersonStatus(livreurUsername string, status str
return nil return nil
} }
// ============================================
// 📦 GESTION QUEUE LIVREUR
// ============================================
// GetDeliverymanQueueSize récupère la taille de la queue d'un livreur // GetDeliverymanQueueSize récupère la taille de la queue d'un livreur
func (d *Database) GetDeliverymanQueueSize(livreurUsername string) (int, error) { func (d *Database) GetDeliverymanQueueSize(livreurUsername string) (int, error) {
queueKey := fmt.Sprintf("delivery:queue:%s", livreurUsername) queueKey := fmt.Sprintf("delivery:queue:%s", livreurUsername)
@@ -312,10 +293,6 @@ func (d *Database) GetDeliverymanQueue(livreurUsername string) ([]int, error) {
return queue, nil return queue, nil
} }
// ============================================
// 🔧 FONCTIONS UTILITAIRES
// ============================================
// UpdateCommandLivreur met à jour le livreur assigné à une commande // UpdateCommandLivreur met à jour le livreur assigné à une commande
func (d *Database) UpdateCommandLivreur(commandID int, livreurUsername string) error { func (d *Database) UpdateCommandLivreur(commandID int, livreurUsername string) error {
query := `UPDATE commandes query := `UPDATE commandes
@@ -338,19 +315,15 @@ func (d *Database) UpdateCommandLivreur(commandID int, livreurUsername string) e
return nil return nil
} }
// ============================================
// 📊 STATISTIQUES SYSTÈME
// ============================================
// GetAllDeliveryPersonsStats récupère les stats de tous les livreurs // GetAllDeliveryPersonsStats récupère les stats de tous les livreurs
func (d *Database) GetAllDeliveryPersonsStats() ([]map[string]interface{}, error) { func (d *Database) GetAllDeliveryPersonsStats() ([]map[string]any, error) {
// Récupérer tous les livreurs // Récupérer tous les livreurs
livreurs, err := d.GetAvailableDeliveryPersons() livreurs, err := d.GetAvailableDeliveryPersons()
if err != nil { if err != nil {
return nil, fmt.Errorf("erreur récupération livreurs: %w", err) return nil, fmt.Errorf("erreur récupération livreurs: %w", err)
} }
var stats []map[string]interface{} var stats []map[string]any
for _, livreur := range livreurs { for _, livreur := range livreurs {
username := livreur["username"].(string) username := livreur["username"].(string)
@@ -361,7 +334,7 @@ func (d *Database) GetAllDeliveryPersonsStats() ([]map[string]interface{}, error
queueSize, _ := d.GetDeliverymanQueueSize(username) queueSize, _ := d.GetDeliverymanQueueSize(username)
status, _ := d.GetDeliveryPersonStatus(username) status, _ := d.GetDeliveryPersonStatus(username)
statEntry := map[string]interface{}{ statEntry := map[string]any{
"username": username, "username": username,
"total_deliveries": totalDeliveries, "total_deliveries": totalDeliveries,
"completed_deliveries": completedDeliveries, "completed_deliveries": completedDeliveries,
@@ -375,10 +348,6 @@ func (d *Database) GetAllDeliveryPersonsStats() ([]map[string]interface{}, error
return stats, nil return stats, nil
} }
// ============================================
// 🔍 RECHERCHE & FILTRAGE
// ============================================
// GetDeliveryPersonsByStatus récupère les livreurs par statut // GetDeliveryPersonsByStatus récupère les livreurs par statut
func (d *Database) GetDeliveryPersonsByStatus(status string) ([]string, error) { func (d *Database) GetDeliveryPersonsByStatus(status string) ([]string, error) {
// Récupérer tous les livreurs // Récupérer tous les livreurs
@@ -411,10 +380,6 @@ func (d *Database) GetAvailableDeliveryPersonsCount() (int, error) {
return len(availableLivreurs), nil return len(availableLivreurs), nil
} }
// ============================================
// 🗑️ SUPPRESSION & NETTOYAGE
// ============================================
// ClearDeliveryPersonData supprime toutes les données d'un livreur (admin uniquement) // ClearDeliveryPersonData supprime toutes les données d'un livreur (admin uniquement)
func (d *Database) ClearDeliveryPersonData(livreurUsername string) error { func (d *Database) ClearDeliveryPersonData(livreurUsername string) error {
log.Printf("🗑️ [ClearDeliveryData] Nettoyage données pour: %s", livreurUsername) log.Printf("🗑️ [ClearDeliveryData] Nettoyage données pour: %s", livreurUsername)
+94
View File
@@ -0,0 +1,94 @@
package db
import (
"fmt"
"log"
)
// GetCommandCategories retourne les catégories distinctes des produits d'une commande
func (d *Database) GetCommandCategories(commandID int) ([]string, error) {
query := `
SELECT DISTINCT p.category
FROM command_items ci
JOIN products p ON p.id = ci.product_id
WHERE ci.command_id = $1 AND p.category IS NOT NULL AND p.category != ''`
rows, err := d.Query(query, commandID)
if err != nil {
return nil, fmt.Errorf("erreur lecture catégories commande %d: %w", commandID, err)
}
defer rows.Close()
var categories []string
for rows.Next() {
var cat string
if err := rows.Scan(&cat); err == nil {
categories = append(categories, cat)
}
}
return categories, rows.Err()
}
// GetEligibleDeliverymenForCommand retourne les usernames des livreurs éligibles pour une commande.
// En mode "single" → tous les livreurs actifs.
// En mode "category_based" → le(s) livreur(s) assigné(s) aux catégories de la commande.
// Si aucune correspondance → fallback sur tous les livreurs actifs.
func (d *Database) GetEligibleDeliverymenForCommand(commandID int) ([]string, error) {
settings, err := d.GetSettings()
if err != nil {
log.Printf("⚠️ [DELIVERY_MODE] Erreur lecture settings: %v — fallback single", err)
}
allActive := func() ([]string, error) {
livreurs, err := d.GetAllActiveDeliveryPersons()
if err != nil {
return nil, err
}
names := make([]string, len(livreurs))
for i, l := range livreurs {
names[i] = l.Username
}
return names, nil
}
if settings.DeliveryMode.Mode != "category_based" || len(settings.DeliveryMode.CategoryRoutes) == 0 {
return allActive()
}
// Mode category_based : récupérer les catégories de la commande
categories, err := d.GetCommandCategories(commandID)
if err != nil || len(categories) == 0 {
log.Printf("⚠️ [DELIVERY_MODE] Cmd %d: catégories non trouvées — fallback single", commandID)
return allActive()
}
// Construire un set des catégories de la commande
catSet := make(map[string]bool, len(categories))
for _, c := range categories {
catSet[c] = true
}
// Trouver les livreurs dont la route intersecte les catégories
eligible := make(map[string]bool)
for _, route := range settings.DeliveryMode.CategoryRoutes {
for _, routeCat := range route.Categories {
if catSet[routeCat] {
eligible[route.DeliverymanUsername] = true
break
}
}
}
if len(eligible) == 0 {
log.Printf("⚠️ [DELIVERY_MODE] Cmd %d: aucun livreur pour catégories %v — fallback single", commandID, categories)
return allActive()
}
result := make([]string, 0, len(eligible))
for u := range eligible {
result = append(result, u)
}
log.Printf("🎯 [DELIVERY_MODE] Cmd %d: livreurs éligibles %v (catégories: %v)", commandID, result, categories)
return result, nil
}
+14 -93
View File
@@ -7,7 +7,6 @@ package db
import ( import (
"fmt" "fmt"
"net/url"
) )
// MapLinks contient les liens vers différentes plateformes de cartographie // MapLinks contient les liens vers différentes plateformes de cartographie
@@ -22,126 +21,48 @@ type MapLinks struct {
HereMaps string `json:"here_maps"` HereMaps string `json:"here_maps"`
} }
func wazeAppLink(lat, lon float64) string {
return fmt.Sprintf("waze://?ll=%.6f,%.6f&navigate=yes", lat, lon)
}
// GenerateMapLinks génère tous les liens de cartes pour une position GPS // GenerateMapLinks génère tous les liens de cartes pour une position GPS
func (d *Database) GenerateMapLinks(lat, lon float64, label string) MapLinks { func (d *Database) GenerateMapLinks(lat, lon float64, label string) MapLinks {
// Encoder le label pour l'URL
encodedLabel := url.QueryEscape(label)
return MapLinks{ return MapLinks{
// Google Maps (Web) WazeApp: wazeAppLink(lat, lon),
GoogleMaps: fmt.Sprintf(
"https://www.google.com/maps?q=%.6f,%.6f&label=%s",
lat, lon, encodedLabel,
),
// Google Maps (App - Deep link)
GoogleMapsApp: fmt.Sprintf(
"https://maps.google.com/?q=%.6f,%.6f",
lat, lon,
),
// Waze (Web)
Waze: fmt.Sprintf(
"https://www.waze.com/ul?ll=%.6f,%.6f&navigate=yes",
lat, lon,
),
// Waze (App - Deep link)
WazeApp: fmt.Sprintf(
"waze://?ll=%.6f,%.6f&navigate=yes",
lat, lon,
),
// Apple Maps (iOS/macOS)
AppleMaps: fmt.Sprintf(
"http://maps.apple.com/?ll=%.6f,%.6f&q=%s",
lat, lon, encodedLabel,
),
// OpenStreetMap
OpenStreetMap: fmt.Sprintf(
"https://www.openstreetmap.org/?mlat=%.6f&mlon=%.6f#map=16/%.6f/%.6f",
lat, lon, lat, lon,
),
// Bing Maps
BingMaps: fmt.Sprintf(
"https://www.bing.com/maps?cp=%.6f~%.6f&lvl=16",
lat, lon,
),
// HERE Maps
HereMaps: fmt.Sprintf(
"https://wego.here.com/?map=%.6f,%.6f,16,normal",
lat, lon,
),
} }
} }
// GenerateNavigationLink génère un lien de navigation depuis une origine vers une destination // GenerateNavigationLink génère un lien de navigation vers une destination
// fromLat/fromLon sont ignorés : Waze part toujours de la position GPS courante
func (d *Database) GenerateNavigationLink(fromLat, fromLon, toLat, toLon float64, platform string) string { func (d *Database) GenerateNavigationLink(fromLat, fromLon, toLat, toLon float64, platform string) string {
switch platform { return wazeAppLink(toLat, toLon)
case "google":
return fmt.Sprintf(
"https://www.google.com/maps/dir/?api=1&origin=%.6f,%.6f&destination=%.6f,%.6f&travelmode=driving",
fromLat, fromLon, toLat, toLon,
)
case "waze":
return fmt.Sprintf(
"https://www.waze.com/ul?ll=%.6f,%.6f&navigate=yes&from=%.6f,%.6f",
toLat, toLon, fromLat, fromLon,
)
case "apple":
return fmt.Sprintf(
"http://maps.apple.com/?saddr=%.6f,%.6f&daddr=%.6f,%.6f",
fromLat, fromLon, toLat, toLon,
)
default:
return fmt.Sprintf(
"https://www.google.com/maps/dir/?api=1&origin=%.6f,%.6f&destination=%.6f,%.6f",
fromLat, fromLon, toLat, toLon,
)
}
} }
// GenerateMapLinksForCommand génère les liens de navigation pour une commande // GenerateMapLinksForCommand génère les liens de navigation pour une commande
// (depuis la position du livreur vers la destination de la commande)
func (d *Database) GenerateMapLinksForCommand(commandID int, deliverymanUsername string) (map[string]string, error) { func (d *Database) GenerateMapLinksForCommand(commandID int, deliverymanUsername string) (map[string]string, error) {
// 1. Récupérer la position du livreur _, _, err := d.GetDeliveryPersonLocation(deliverymanUsername)
livreurLat, livreurLon, err := d.GetDeliveryPersonLocation(deliverymanUsername)
if err != nil { if err != nil {
return nil, fmt.Errorf("position livreur non disponible: %w", err) return nil, fmt.Errorf("position livreur non disponible: %w", err)
} }
// 2. Récupérer la destination de la commande
command, err := d.GetCommandByID(commandID) command, err := d.GetCommandByID(commandID)
if err != nil { if err != nil {
return nil, fmt.Errorf("commande non trouvée: %w", err) return nil, fmt.Errorf("commande non trouvée: %w", err)
} }
var destLat, destLon float64 var destLat, destLon float64
if command["dest_latitude"] != nil { if v, ok := command["dest_latitude"].(float64); ok {
if latVal, ok := command["dest_latitude"].(float64); ok { destLat = v
destLat = latVal
}
}
if command["dest_longitude"] != nil {
if lonVal, ok := command["dest_longitude"].(float64); ok {
destLon = lonVal
} }
if v, ok := command["dest_longitude"].(float64); ok {
destLon = v
} }
if destLat == 0 && destLon == 0 { if destLat == 0 && destLon == 0 {
return nil, fmt.Errorf("coordonnées destination invalides") return nil, fmt.Errorf("coordonnées destination invalides")
} }
// 3. Générer les liens de navigation
return map[string]string{ return map[string]string{
"google_maps": d.GenerateNavigationLink(livreurLat, livreurLon, destLat, destLon, "google"), "waze_app": wazeAppLink(destLat, destLon),
"waze": d.GenerateNavigationLink(livreurLat, livreurLon, destLat, destLon, "waze"),
"apple_maps": d.GenerateNavigationLink(livreurLat, livreurLon, destLat, destLon, "apple"),
}, nil }, nil
} }
+17 -46
View File
@@ -9,15 +9,12 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"log" "log"
"maps"
"time" "time"
) )
// GetCompletedCommandsByUsername récupère toutes les commandes terminées (approved) d'un utilisateur // GetCompletedCommandsByUsername récupère toutes les commandes terminées (approved) d'un utilisateur
// ✅ Retourne uniquement les commandes avec status = "approved" func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string]any, error) {
// ✅ Ordonnées par date de création décroissante (plus récentes en premier)
func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string]interface{}, error) {
log.Printf("📚 [GetCompletedCommands] START - username=%s", username)
query := ` query := `
SELECT SELECT
id, id,
@@ -40,7 +37,7 @@ func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string
} }
defer rows.Close() defer rows.Close()
var commands []map[string]interface{} var commands []map[string]any
for rows.Next() { for rows.Next() {
var id int var id int
@@ -64,7 +61,7 @@ func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string
return nil, fmt.Errorf("erreur lors du scan de la commande: %w", err) return nil, fmt.Errorf("erreur lors du scan de la commande: %w", err)
} }
command := map[string]interface{}{ command := map[string]any{
"id": id, "id": id,
"username": username, "username": username,
"status": status, "status": status,
@@ -74,7 +71,6 @@ func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string
"updated_at": updatedAt, "updated_at": updatedAt,
} }
// Ajouter livreur_assign seulement s'il n'est pas NULL
if livreurAssign.Valid { if livreurAssign.Valid {
command["livreur_assign"] = livreurAssign.String command["livreur_assign"] = livreurAssign.String
} else { } else {
@@ -89,23 +85,17 @@ func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string
return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err) return nil, fmt.Errorf("erreur lors de l'itération des résultats: %w", err)
} }
log.Printf("✅ [GetCompletedCommands] %d commandes terminées trouvées pour %s", len(commands), username)
return commands, nil return commands, nil
} }
// GetCompletedCommandsWithItems récupère les commandes terminées avec leurs items // GetCompletedCommandsWithItems récupère les commandes terminées avec leurs items
// ✅ Retourne les commandes approved avec tous les détails func (d *Database) GetCompletedCommandsWithItems(username string) ([]map[string]any, error) {
func (d *Database) GetCompletedCommandsWithItems(username string) ([]map[string]interface{}, error) {
log.Printf("📚 [GetCompletedWithItems] START - username=%s", username)
// 1. Récupérer les commandes terminées
commands, err := d.GetCompletedCommandsByUsername(username) commands, err := d.GetCompletedCommandsByUsername(username)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 2. Pour chaque commande, récupérer les items var enrichedCommands []map[string]any
var enrichedCommands []map[string]interface{}
for _, command := range commands { for _, command := range commands {
commandID, ok := command["id"].(int) commandID, ok := command["id"].(int)
@@ -117,29 +107,22 @@ func (d *Database) GetCompletedCommandsWithItems(username string) ([]map[string]
items, err := d.GetCommandItems(commandID) items, err := d.GetCommandItems(commandID)
if err != nil { if err != nil {
log.Printf("⚠️ [GetCompletedWithItems] Erreur items pour cmd %d: %v", commandID, err) log.Printf("⚠️ [GetCompletedWithItems] Erreur items pour cmd %d: %v", commandID, err)
items = []map[string]interface{}{} items = []map[string]any{}
} }
// Enrichir la commande enrichedCommand := make(map[string]any)
enrichedCommand := make(map[string]interface{}) maps.Copy(enrichedCommand, command)
for k, v := range command {
enrichedCommand[k] = v
}
enrichedCommand["items"] = items enrichedCommand["items"] = items
enrichedCommand["items_count"] = len(items) enrichedCommand["items_count"] = len(items)
enrichedCommands = append(enrichedCommands, enrichedCommand) enrichedCommands = append(enrichedCommands, enrichedCommand)
} }
log.Printf("✅ [GetCompletedWithItems] %d commandes enrichies", len(enrichedCommands))
return enrichedCommands, nil return enrichedCommands, nil
} }
// GetCommandsStatsByUsername récupère les statistiques des commandes d'un utilisateur // GetCommandsStatsByUsername récupère les statistiques des commandes d'un utilisateur
// ✅ Compte total, approved, pending, cancelled, etc. func (d *Database) GetCommandsStatsByUsername(username string) (map[string]any, error) {
func (d *Database) GetCommandsStatsByUsername(username string) (map[string]interface{}, error) {
log.Printf("📊 [GetCommandsStats] START - username=%s", username)
query := ` query := `
SELECT SELECT
COUNT(*) FILTER (WHERE status = 'approved') as approved_count, COUNT(*) FILTER (WHERE status = 'approved') as approved_count,
@@ -172,7 +155,7 @@ func (d *Database) GetCommandsStatsByUsername(username string) (map[string]inter
return nil, fmt.Errorf("erreur lors de la récupération des statistiques: %w", err) return nil, fmt.Errorf("erreur lors de la récupération des statistiques: %w", err)
} }
stats := map[string]interface{}{ stats := map[string]any{
"approved_count": approvedCount, "approved_count": approvedCount,
"pending_count": pendingCount, "pending_count": pendingCount,
"assigned_count": assignedCount, "assigned_count": assignedCount,
@@ -183,15 +166,11 @@ func (d *Database) GetCommandsStatsByUsername(username string) (map[string]inter
"total_spent": totalSpent, "total_spent": totalSpent,
} }
log.Printf("✅ [GetCommandsStats] Stats calculées: total=%d, approved=%d, spent=%.2f€",
totalCount, approvedCount, totalSpent)
return stats, nil return stats, nil
} }
// GetCommandsByStatus récupère les commandes d'un utilisateur par statut // GetCommandsByStatus récupère les commandes d'un utilisateur par statut
// ✅ Permet de filtrer par un statut spécifique func (d *Database) GetCommandsByStatus(username, status string) ([]map[string]any, error) {
func (d *Database) GetCommandsByStatus(username, status string) ([]map[string]interface{}, error) {
log.Printf("📋 [GetCommandsByStatus] START - username=%s, status=%s", username, status) log.Printf("📋 [GetCommandsByStatus] START - username=%s, status=%s", username, status)
query := ` query := `
@@ -211,12 +190,11 @@ func (d *Database) GetCommandsByStatus(username, status string) ([]map[string]in
rows, err := d.Query(query, username, status) rows, err := d.Query(query, username, status)
if err != nil { if err != nil {
log.Printf("❌ [GetCommandsByStatus] Erreur query: %v", err)
return nil, fmt.Errorf("erreur lors de la récupération des commandes: %w", err) return nil, fmt.Errorf("erreur lors de la récupération des commandes: %w", err)
} }
defer rows.Close() defer rows.Close()
var commands []map[string]interface{} var commands []map[string]any
for rows.Next() { for rows.Next() {
var id int var id int
@@ -240,7 +218,7 @@ func (d *Database) GetCommandsByStatus(username, status string) ([]map[string]in
return nil, fmt.Errorf("erreur lors du scan: %w", err) return nil, fmt.Errorf("erreur lors du scan: %w", err)
} }
command := map[string]interface{}{ command := map[string]any{
"id": id, "id": id,
"username": username, "username": username,
"status": status, "status": status,
@@ -260,18 +238,13 @@ func (d *Database) GetCommandsByStatus(username, status string) ([]map[string]in
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
log.Printf("❌ [GetCommandsByStatus] Erreur itération: %v", err)
return nil, fmt.Errorf("erreur lors de l'itération: %w", err) return nil, fmt.Errorf("erreur lors de l'itération: %w", err)
} }
log.Printf("✅ [GetCommandsByStatus] %d commandes trouvées", len(commands))
return commands, nil return commands, nil
} }
// GetRecentCompletedOrders récupère les N dernières commandes terminées d'un utilisateur // GetRecentCompletedOrders récupère les N dernières commandes terminées d'un utilisateur
// ✅ Utile pour afficher les dernières commandes dans le dashboard func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[string]any, error) {
func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[string]interface{}, error) {
log.Printf("📚 [GetRecentCompleted] START - username=%s, limit=%d", username, limit)
query := ` query := `
SELECT SELECT
@@ -296,7 +269,7 @@ func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[s
} }
defer rows.Close() defer rows.Close()
var commands []map[string]interface{} var commands []map[string]any
for rows.Next() { for rows.Next() {
var id int var id int
@@ -320,7 +293,7 @@ func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[s
return nil, fmt.Errorf("erreur lors du scan: %w", err) return nil, fmt.Errorf("erreur lors du scan: %w", err)
} }
command := map[string]interface{}{ command := map[string]any{
"id": id, "id": id,
"username": username, "username": username,
"status": status, "status": status,
@@ -340,10 +313,8 @@ func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[s
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
log.Printf("❌ [GetRecentCompleted] Erreur itération: %v", err)
return nil, fmt.Errorf("erreur lors de l'itération: %w", err) return nil, fmt.Errorf("erreur lors de l'itération: %w", err)
} }
log.Printf("✅ [GetRecentCompleted] %d commandes récentes trouvées", len(commands))
return commands, nil return commands, nil
} }
+1 -36
View File
@@ -2,7 +2,6 @@ package db
import ( import (
"database/sql" "database/sql"
"encoding/json"
"fmt" "fmt"
"log" "log"
"os" "os"
@@ -197,37 +196,6 @@ func InitDB() *Database {
log.Fatalf("❌ Erreur migration clients.points_extra: %v", err) log.Fatalf("❌ Erreur migration clients.points_extra: %v", err)
} }
// Migration: copier point/point_zipette → points_extra[pool.Key] selon les clés admin
{
var poolsJSON string
_ = database.QueryRow(`SELECT value FROM app_settings WHERE key = 'points_pools'`).Scan(&poolsJSON)
if poolsJSON != "" {
var pools []struct {
Key string `json:"key"`
}
if jsonErr := json.Unmarshal([]byte(poolsJSON), &pools); jsonErr == nil {
if len(pools) > 0 && pools[0].Key != "" {
k0 := pools[0].Key
if _, mErr := database.Exec(`UPDATE clients SET points_extra = points_extra || jsonb_build_object($1::text, point) WHERE point > 0 AND NOT (points_extra ? $1)`, k0); mErr != nil {
log.Printf("⚠️ Migration points_extra pool[0] (%s): %v", k0, mErr)
} else {
log.Printf("✅ Migration points_extra pool[0] key=%s", k0)
}
}
if len(pools) > 1 && pools[1].Key != "" {
k1 := pools[1].Key
if _, mErr := database.Exec(`UPDATE clients SET points_extra = points_extra || jsonb_build_object($1::text, point_zipette) WHERE point_zipette > 0 AND NOT (points_extra ? $1)`, k1); mErr != nil {
log.Printf("⚠️ Migration points_extra pool[1] (%s): %v", k1, mErr)
} else {
log.Printf("✅ Migration points_extra pool[1] key=%s", k1)
}
}
}
} else {
log.Printf("️ Migration points_extra: aucun pool configuré (app_settings vide)")
}
}
// Lancer le nettoyage périodique des tokens expirés // Lancer le nettoyage périodique des tokens expirés
go database.cleanExpiredTokensPeriodically() go database.cleanExpiredTokensPeriodically()
@@ -267,8 +235,6 @@ func (db *Database) createTables() error {
prenom VARCHAR(100) NOT NULL, prenom VARCHAR(100) NOT NULL,
telephone VARCHAR(20) NOT NULL UNIQUE, telephone VARCHAR(20) NOT NULL UNIQUE,
command INTEGER DEFAULT 0, command INTEGER DEFAULT 0,
point INTEGER DEFAULT 0,
point_zipette INTEGER DEFAULT 0,
amende NUMERIC(10,2) DEFAULT 0.0, amende NUMERIC(10,2) DEFAULT 0.0,
cancel_commande INTEGER DEFAULT 0, cancel_commande INTEGER DEFAULT 0,
cancellations_count INTEGER DEFAULT 0 NOT NULL, cancellations_count INTEGER DEFAULT 0 NOT NULL,
@@ -436,8 +402,7 @@ func (db *Database) createTables() error {
`CREATE INDEX IF NOT EXISTS idx_command_items_client_username ON command_items(client_username);`, `CREATE INDEX IF NOT EXISTS idx_command_items_client_username ON command_items(client_username);`,
`CREATE INDEX IF NOT EXISTS idx_command_items_status ON command_items(status);`, `CREATE INDEX IF NOT EXISTS idx_command_items_status ON command_items(status);`,
`CREATE INDEX IF NOT EXISTS idx_command_items_produit ON command_items(produit);`, `CREATE INDEX IF NOT EXISTS idx_command_items_produit ON command_items(produit);`,
`CREATE INDEX IF NOT EXISTS idx_clients_point_zipette ON clients(point_zipette) WHERE point_zipette > 0;`, `CREATE INDEX IF NOT EXISTS idx_jwt_user_id ON jwt_tokens(user_id);`,
`CREATE INDEX IF NOT EXISTS idx_jwt_user_id ON jwt_tokens(user_id);`,
`CREATE INDEX IF NOT EXISTS idx_jwt_user_type ON jwt_tokens(user_type);`, `CREATE INDEX IF NOT EXISTS idx_jwt_user_type ON jwt_tokens(user_type);`,
`CREATE INDEX IF NOT EXISTS idx_jwt_token ON jwt_tokens(token);`, `CREATE INDEX IF NOT EXISTS idx_jwt_token ON jwt_tokens(token);`,
`CREATE INDEX IF NOT EXISTS idx_jwt_date_fin ON jwt_tokens(date_fin);`, `CREATE INDEX IF NOT EXISTS idx_jwt_date_fin ON jwt_tokens(date_fin);`,
+5 -5
View File
@@ -79,7 +79,7 @@ func (d *Database) RevokeAllUserTokens(userID int, userType string) error {
} }
// GetUserActiveTokens récupère tous les tokens actifs d'un utilisateur // GetUserActiveTokens récupère tous les tokens actifs d'un utilisateur
func (d *Database) GetUserActiveTokens(userID int, userType string) ([]map[string]interface{}, error) { func (d *Database) GetUserActiveTokens(userID int, userType string) ([]map[string]any, error) {
query := `SELECT id, token, date_save, date_fin query := `SELECT id, token, date_save, date_fin
FROM jwt_tokens FROM jwt_tokens
WHERE user_id = $1 AND user_type = $2 AND date_fin > $3 WHERE user_id = $1 AND user_type = $2 AND date_fin > $3
@@ -91,7 +91,7 @@ func (d *Database) GetUserActiveTokens(userID int, userType string) ([]map[strin
} }
defer rows.Close() defer rows.Close()
var tokens []map[string]interface{} var tokens []map[string]any
for rows.Next() { for rows.Next() {
var id int var id int
var token string var token string
@@ -102,7 +102,7 @@ func (d *Database) GetUserActiveTokens(userID int, userType string) ([]map[strin
return nil, fmt.Errorf("erreur lors du scan: %w", err) return nil, fmt.Errorf("erreur lors du scan: %w", err)
} }
tokens = append(tokens, map[string]interface{}{ tokens = append(tokens, map[string]any{
"id": id, "id": id,
"token": token[:20] + "...", // Tronquer pour la sécurité "token": token[:20] + "...", // Tronquer pour la sécurité
"date_save": dateSave, "date_save": dateSave,
@@ -114,7 +114,7 @@ func (d *Database) GetUserActiveTokens(userID int, userType string) ([]map[strin
return tokens, nil return tokens, nil
} }
func (d *Database) GetTokenInfo(token string) (map[string]interface{}, error) { func (d *Database) GetTokenInfo(token string) (map[string]any, error) {
query := `SELECT user_id, user_type, date_save, date_fin query := `SELECT user_id, user_type, date_save, date_fin
FROM jwt_tokens FROM jwt_tokens
WHERE token = $1 AND date_fin > $2` WHERE token = $1 AND date_fin > $2`
@@ -131,7 +131,7 @@ func (d *Database) GetTokenInfo(token string) (map[string]interface{}, error) {
return nil, fmt.Errorf("erreur lors de la récupération des infos du token: %w", err) return nil, fmt.Errorf("erreur lors de la récupération des infos du token: %w", err)
} }
tokenInfo := map[string]interface{}{ tokenInfo := map[string]any{
"user_id": userID, "user_id": userID,
"user_type": userType, "user_type": userType,
"date_save": dateSave, "date_save": dateSave,
+42 -66
View File
@@ -5,13 +5,17 @@ import (
"fmt" "fmt"
"gestion/models" "gestion/models"
"log" "log"
"slices"
"strings" "strings"
"time" "time"
) )
// ============================================ type MediaInterface interface {
// VALIDATION HELPERS GetProductID() int
// ============================================ GetType() string
GetURL() string
SetID(int)
}
// validateMediaID vérifie la validité d'un ID média // validateMediaID vérifie la validité d'un ID média
func validateMediaID(mediaID int) error { func validateMediaID(mediaID int) error {
@@ -40,13 +44,9 @@ func validateMediaType(mediaType string) error {
validTypes := []string{"image", "video"} validTypes := []string{"image", "video"}
mediaType = strings.ToLower(strings.TrimSpace(mediaType)) mediaType = strings.ToLower(strings.TrimSpace(mediaType))
if slices.Contains(validTypes, mediaType) {
for _, valid := range validTypes {
if mediaType == valid {
return nil return nil
} }
}
return fmt.Errorf("type de média invalide: %s (autorisé: image, video)", mediaType) return fmt.Errorf("type de média invalide: %s (autorisé: image, video)", mediaType)
} }
@@ -61,7 +61,7 @@ func validateMediaURL(url string) error {
} }
// ✅ PROTECTION PATH TRAVERSAL // ✅ PROTECTION PATH TRAVERSAL
if strings.Contains(url, "..") { if strings.Contains(url, "..") || strings.Contains(url, "...") || strings.Contains(url, "..//") {
return fmt.Errorf("path traversal détecté dans l'URL") return fmt.Errorf("path traversal détecté dans l'URL")
} }
@@ -88,16 +88,9 @@ func validateMediaURL(url string) error {
// CREATE MEDIA - VERSION SÉCURISÉE // CREATE MEDIA - VERSION SÉCURISÉE
// ============================================ // ============================================
func (db *Database) CreateMedia(media interface{}) error { func (db *Database) CreateMedia(media any) error {
log.Printf("🔒 [CreateMedia] START - Type: %T", media) log.Printf("🔒 [CreateMedia] START - Type: %T", media)
type MediaInterface interface {
GetProductID() int
GetType() string
GetURL() string
SetID(int)
}
// ✅ TYPE ASSERTION SÉCURISÉE // ✅ TYPE ASSERTION SÉCURISÉE
m, ok := media.(MediaInterface) m, ok := media.(MediaInterface)
if !ok { if !ok {
@@ -115,32 +108,33 @@ func (db *Database) CreateMedia(media interface{}) error {
return fmt.Errorf("type de média invalide") return fmt.Errorf("type de média invalide")
} }
// ✅ VALIDATION COMPLÈTE
productID := m.GetProductID() productID := m.GetProductID()
mediaType := m.GetType() mediaType := m.GetType()
mediaURL := m.GetURL() mediaURL := m.GetURL()
log.Printf("📋 [CreateMedia] ProductID=%d, Type=%s, URL=%s", productID, mediaType, mediaURL)
// Valider le product ID
if err := validateProductID(productID); err != nil { if err := validateProductID(productID); err != nil {
log.Printf("❌ [CreateMedia] %v", err) log.Printf("❌ [CreateMedia] %v", err)
return err return err
} }
// Valider le type
if err := validateMediaType(mediaType); err != nil { if err := validateMediaType(mediaType); err != nil {
log.Printf("❌ [CreateMedia] %v", err) log.Printf("❌ [CreateMedia] %v", err)
return err return err
} }
// Valider l'URL
if err := validateMediaURL(mediaURL); err != nil { if err := validateMediaURL(mediaURL); err != nil {
log.Printf("❌ [CreateMedia] %v", err) log.Printf("❌ [CreateMedia] %v", err)
return err return err
} }
// ✅ VÉRIFIER QUE LE PRODUIT EXISTE if err := db.InsertMedia(m, productID, mediaURL, mediaType); err != nil {
log.Printf("❌ [InsertMedia] %v", err)
return err
}
return nil
}
func (db *Database) InsertMedia(m any, productID int, mediaURL any, mediaType string) error {
var exists bool var exists bool
checkQuery := `SELECT EXISTS(SELECT 1 FROM products WHERE id = $1)` checkQuery := `SELECT EXISTS(SELECT 1 FROM products WHERE id = $1)`
err := db.QueryRow(checkQuery, productID).Scan(&exists) err := db.QueryRow(checkQuery, productID).Scan(&exists)
@@ -153,7 +147,6 @@ func (db *Database) CreateMedia(media interface{}) error {
return fmt.Errorf("produit %d n'existe pas", productID) return fmt.Errorf("produit %d n'existe pas", productID)
} }
// ✅ INSÉRER LE MÉDIA
query := `INSERT INTO media (product_id, url, type, created_at) query := `INSERT INTO media (product_id, url, type, created_at)
VALUES ($1, $2, $3, $4) RETURNING id` VALUES ($1, $2, $3, $4) RETURNING id`
@@ -165,10 +158,10 @@ func (db *Database) CreateMedia(media interface{}) error {
log.Printf("❌ [CreateMedia] Erreur INSERT: %v", err) log.Printf("❌ [CreateMedia] Erreur INSERT: %v", err)
return fmt.Errorf("erreur création média: %w", err) return fmt.Errorf("erreur création média: %w", err)
} }
if mi, ok := m.(MediaInterface); ok {
m.SetID(mediaID) mi.SetID(mediaID)
}
log.Printf("✅ [CreateMedia] Média créé: ID=%d, Type=%s", mediaID, mediaType) log.Printf("✅ [CreateMedia] Média créé: ID=%d, Type=%s", mediaID, mediaType)
return nil return nil
} }
@@ -274,9 +267,6 @@ func (db *Database) GetMediaByProductID(productID int) ([]models.Media, error) {
// ============================================ // ============================================
func (db *Database) UpdateMedia(media *models.Media) error { func (db *Database) UpdateMedia(media *models.Media) error {
log.Printf("🔄 [UpdateMedia] START - ID=%d", media.ID)
// ✅ VALIDATION
if media == nil { if media == nil {
return fmt.Errorf("média nil") return fmt.Errorf("média nil")
} }
@@ -297,16 +287,9 @@ func (db *Database) UpdateMedia(media *models.Media) error {
} }
// ✅ VÉRIFIER QUE LE MÉDIA EXISTE // ✅ VÉRIFIER QUE LE MÉDIA EXISTE
var exists bool if err := db.CheckMediaExists(media); err != nil {
checkQuery := `SELECT EXISTS(SELECT 1 FROM media WHERE id = $1)` log.Printf("❌ [UpdateMedia] %v", err)
err := db.QueryRow(checkQuery, media.ID).Scan(&exists) return err
if err != nil {
log.Printf("❌ [UpdateMedia] Erreur vérification: %v", err)
return fmt.Errorf("erreur vérification média: %w", err)
}
if !exists {
log.Printf("❌ [UpdateMedia] Média %d n'existe pas", media.ID)
return fmt.Errorf("média %d non trouvé", media.ID)
} }
// ✅ UPDATE // ✅ UPDATE
@@ -331,33 +314,33 @@ func (db *Database) UpdateMedia(media *models.Media) error {
return nil return nil
} }
// ============================================ func (db *Database) CheckMediaExists(media *models.Media) error {
// DELETE MEDIA - VERSION SÉCURISÉE var exists bool
// ============================================ checkQuery := `SELECT EXISTS(SELECT 1 FROM media WHERE id = $1)`
err := db.QueryRow(checkQuery, media.ID).Scan(&exists)
if err != nil {
log.Printf("❌ [UpdateMedia] Erreur vérification: %v", err)
return fmt.Errorf("erreur vérification média: %w", err)
}
if !exists {
log.Printf("❌ [UpdateMedia] Média %d n'existe pas", media.ID)
return fmt.Errorf("média %d non trouvé", media.ID)
}
return nil
}
func (db *Database) DeleteMedia(mediaID int) error { func (db *Database) DeleteMedia(mediaID int) error {
log.Printf("🗑️ [DeleteMedia] START - ID=%d", mediaID)
// ✅ VALIDATION
if err := validateMediaID(mediaID); err != nil { if err := validateMediaID(mediaID); err != nil {
log.Printf("❌ [DeleteMedia] %v", err) log.Printf("❌ [DeleteMedia] %v", err)
return err return err
} }
// ✅ VÉRIFIER QUE LE MÉDIA EXISTE media := models.Media{ID: mediaID}
var exists bool if err := db.CheckMediaExists(&media); err != nil {
checkQuery := `SELECT EXISTS(SELECT 1 FROM media WHERE id = $1)` log.Printf("❌ [DeleteMedia] %v", err)
err := db.QueryRow(checkQuery, mediaID).Scan(&exists) return err
if err != nil {
log.Printf("❌ [DeleteMedia] Erreur vérification: %v", err)
return fmt.Errorf("erreur vérification média: %w", err)
}
if !exists {
log.Printf("❌ [DeleteMedia] Média %d n'existe pas", mediaID)
return fmt.Errorf("média %d non trouvé", mediaID)
} }
// ✅ DELETE
query := `DELETE FROM media WHERE id = $1` query := `DELETE FROM media WHERE id = $1`
result, err := db.Exec(query, mediaID) result, err := db.Exec(query, mediaID)
@@ -377,20 +360,14 @@ func (db *Database) DeleteMedia(mediaID int) error {
return nil return nil
} }
// ============================================
// DELETE MEDIA BY PRODUCT ID - VERSION SÉCURISÉE
// ============================================
func (db *Database) DeleteMediaByProductID(productID int) error { func (db *Database) DeleteMediaByProductID(productID int) error {
log.Printf("🗑️ [DeleteMediaByProductID] START - ProductID=%d", productID) log.Printf("🗑️ [DeleteMediaByProductID] START - ProductID=%d", productID)
// ✅ VALIDATION
if err := validateProductID(productID); err != nil { if err := validateProductID(productID); err != nil {
log.Printf("❌ [DeleteMediaByProductID] %v", err) log.Printf("❌ [DeleteMediaByProductID] %v", err)
return err return err
} }
// ✅ VÉRIFIER QUE LE PRODUIT EXISTE
var exists bool var exists bool
checkQuery := `SELECT EXISTS(SELECT 1 FROM products WHERE id = $1)` checkQuery := `SELECT EXISTS(SELECT 1 FROM products WHERE id = $1)`
err := db.QueryRow(checkQuery, productID).Scan(&exists) err := db.QueryRow(checkQuery, productID).Scan(&exists)
@@ -403,7 +380,6 @@ func (db *Database) DeleteMediaByProductID(productID int) error {
return fmt.Errorf("produit %d non trouvé", productID) return fmt.Errorf("produit %d non trouvé", productID)
} }
// ✅ DELETE
query := `DELETE FROM media WHERE product_id = $1` query := `DELETE FROM media WHERE product_id = $1`
result, err := db.Exec(query, productID) result, err := db.Exec(query, productID)
+42 -102
View File
@@ -1,12 +1,10 @@
package db package db
import ( import (
"bytes"
"database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"gestion/services"
"log" "log"
"net/http"
"time" "time"
) )
@@ -14,7 +12,7 @@ func (d *Database) NotifyClient(username string, commandID int, notifType, messa
// Sauvegarder la notification dans Redis // Sauvegarder la notification dans Redis
notifKey := fmt.Sprintf("notifications:%s", username) notifKey := fmt.Sprintf("notifications:%s", username)
notification := map[string]interface{}{ notification := map[string]any{
"command_id": commandID, "command_id": commandID,
"type": notifType, "type": notifType,
"message": message, "message": message,
@@ -26,90 +24,22 @@ func (d *Database) NotifyClient(username string, commandID int, notifType, messa
Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.LPush(RedisCtx, notifKey, notifJSON)
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
// Envoyer push notification si le client a un token enregistré // Diffusion Telegram si le client a lié son compte
pushToken, err := d.GetClientPushToken(username) if services.TelegramBot != nil && services.TelegramBot.IsConfigured() {
if err == nil && pushToken != "" { if chatID, ok, err := d.GetClientTelegramChatID(username); err == nil && ok {
go sendExpoPush(pushToken, "Uber Stup", message, commandID, notifType) go services.TelegramBot.SendMessage(chatID, fmt.Sprintf("🔔 <b>Notification</b>\n\n%s", message))
}
} }
log.Printf("📬 Notification envoyée à %s: %s", username, message) log.Printf("📬 Notification envoyée à %s: %s", username, message)
return nil return nil
} }
func sendExpoPush(token, title, body string, commandID int, notifType string) { // NotifyLivreur envoie une notification in-app (Redis) à un livreur
sendExpoPushWithChannel(token, title, body, commandID, notifType, "orders")
}
func sendExpoPushWithChannel(token, title, body string, commandID int, notifType, channelID string) {
payload := map[string]interface{}{
"to": token,
"title": title,
"body": body,
"channelId": channelID,
"data": map[string]interface{}{
"command_id": commandID,
"type": notifType,
},
"sound": "default",
}
jsonBody, err := json.Marshal(payload)
if err != nil {
log.Printf("❌ [EXPO_PUSH] Erreur marshal: %v", err)
return
}
req, err := http.NewRequest("POST", "https://exp.host/--/api/v2/push/send", bytes.NewBuffer(jsonBody))
if err != nil {
log.Printf("❌ [EXPO_PUSH] Erreur création requête: %v", err)
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Accept-Encoding", "gzip, deflate")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
log.Printf("❌ [EXPO_PUSH] Erreur envoi: %v", err)
return
}
defer resp.Body.Close()
log.Printf("✅ [EXPO_PUSH] Push envoyé à %s (channel: %s, status: %d)", token, channelID, resp.StatusCode)
}
// ============================================
// PUSH TOKEN - LIVREURS (table users)
// ============================================
// SaveUserPushToken sauvegarde le token push d'un livreur dans la table users
func (d *Database) SaveUserPushToken(username, token string) error {
_, err := d.Exec(`UPDATE users SET push_token = $1 WHERE username = $2`, token, username)
return err
}
// GetUserPushToken récupère le token push d'un livreur depuis la table users
func (d *Database) GetUserPushToken(username string) (string, error) {
var token sql.NullString
err := d.QueryRow(`SELECT push_token FROM users WHERE username = $1`, username).Scan(&token)
if err != nil || !token.Valid {
return "", err
}
return token.String, nil
}
// DeleteUserPushToken supprime le token push d'un livreur
func (d *Database) DeleteUserPushToken(username string) error {
_, err := d.Exec(`UPDATE users SET push_token = NULL WHERE username = $1`, username)
return err
}
// NotifyLivreur envoie une notification in-app (Redis) + push Expo à un livreur
func (d *Database) NotifyLivreur(username string, commandID int, notifType, message string) error { func (d *Database) NotifyLivreur(username string, commandID int, notifType, message string) error {
notifKey := fmt.Sprintf("notifications:%s", username) notifKey := fmt.Sprintf("notifications:%s", username)
notification := map[string]interface{}{ notification := map[string]any{
"command_id": commandID, "command_id": commandID,
"type": notifType, "type": notifType,
"message": message, "message": message,
@@ -121,10 +51,11 @@ func (d *Database) NotifyLivreur(username string, commandID int, notifType, mess
Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.LPush(RedisCtx, notifKey, notifJSON)
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
// Push notification si le livreur a un token enregistré // Diffusion Telegram si le livreur a lié son compte
pushToken, err := d.GetUserPushToken(username) if services.TelegramBot != nil && services.TelegramBot.IsConfigured() {
if err == nil && pushToken != "" { if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok {
go sendExpoPushWithChannel(pushToken, "Nouvelle commande assignée", message, commandID, notifType, "deliveries") go services.TelegramBot.SendMessage(chatID, fmt.Sprintf("🔔 <b>Notification</b>\n\n%s", message))
}
} }
log.Printf("📬 [LIVREUR_NOTIF] Notification envoyée à %s: %s", username, message) log.Printf("📬 [LIVREUR_NOTIF] Notification envoyée à %s: %s", username, message)
@@ -135,7 +66,7 @@ func (d *Database) NotifyLivreur(username string, commandID int, notifType, mess
// et envoie un push aux ceux qui ont un token. Appelé dès qu'une nouvelle commande est créée. // et envoie un push aux ceux qui ont un token. Appelé dès qu'une nouvelle commande est créée.
func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryAddr string) { func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryAddr string) {
rows, err := d.Query( rows, err := d.Query(
`SELECT username, COALESCE(push_token, '') FROM users WHERE role IN ('admin','cabine')`, `SELECT username FROM users WHERE role IN ('admin','cabine')`,
) )
if err != nil { if err != nil {
log.Printf("❌ [ADMIN_NOTIF] Erreur lecture users admin/cabine: %v", err) log.Printf("❌ [ADMIN_NOTIF] Erreur lecture users admin/cabine: %v", err)
@@ -145,7 +76,7 @@ func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryA
msg := fmt.Sprintf("Nouvelle commande #%d de %s — %s", commandID, clientUsername, deliveryAddr) msg := fmt.Sprintf("Nouvelle commande #%d de %s — %s", commandID, clientUsername, deliveryAddr)
notification := map[string]interface{}{ notification := map[string]any{
"command_id": commandID, "command_id": commandID,
"type": "new_order", "type": "new_order",
"message": msg, "message": msg,
@@ -154,29 +85,34 @@ func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryA
} }
notifJSON, _ := json.Marshal(notification) notifJSON, _ := json.Marshal(notification)
sent := 0 count := 0
for rows.Next() { for rows.Next() {
var username, token string var username string
if err := rows.Scan(&username, &token); err != nil { if err := rows.Scan(&username); err != nil {
continue continue
} }
notifKey := fmt.Sprintf("notifications:%s", username) notifKey := fmt.Sprintf("notifications:%s", username)
Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.LPush(RedisCtx, notifKey, notifJSON)
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
if token != "" { // Diffusion Telegram individuelle
go sendExpoPushWithChannel(token, "Nouvelle commande", msg, commandID, "new_order", "orders") if services.TelegramBot != nil && services.TelegramBot.IsConfigured() {
sent++ if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok {
capturedChatID := chatID
capturedMsg := msg
go services.TelegramBot.SendMessage(capturedChatID, fmt.Sprintf("🔔 <b>Nouvelle commande</b>\n\n%s", capturedMsg))
} }
} }
log.Printf("📬 [ADMIN_NOTIF] Notif Redis + push (%d tokens) pour commande #%d", sent, commandID) count++
}
log.Printf("📬 [ADMIN_NOTIF] Notif Redis (%d users) pour commande #%d", count, commandID)
} }
// NotifyAllAdminCabineAlert envoie une notification Redis + push à tous les admins/cabines // NotifyAllAdminCabineAlert envoie une notification Redis + push à tous les admins/cabines
// lors du déclenchement d'une alerte par un livreur. // lors du déclenchement d'une alerte par un livreur.
func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alertMessage string) { func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alertMessage string) {
rows, err := d.Query( rows, err := d.Query(
`SELECT username, COALESCE(push_token, '') FROM users WHERE role IN ('admin','cabine')`, `SELECT username FROM users WHERE role IN ('admin','cabine')`,
) )
if err != nil { if err != nil {
log.Printf("❌ [ALERT_NOTIF] Erreur lecture users admin/cabine: %v", err) log.Printf("❌ [ALERT_NOTIF] Erreur lecture users admin/cabine: %v", err)
@@ -184,10 +120,9 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert
} }
defer rows.Close() defer rows.Close()
title := "🚨 Alerte livreur"
body := fmt.Sprintf("%s — livreur : %s", alertMessage, livreurUsername) body := fmt.Sprintf("%s — livreur : %s", alertMessage, livreurUsername)
notification := map[string]interface{}{ notification := map[string]any{
"alert_id": alertID, "alert_id": alertID,
"type": "alert", "type": "alert",
"message": body, "message": body,
@@ -196,22 +131,27 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert
} }
notifJSON, _ := json.Marshal(notification) notifJSON, _ := json.Marshal(notification)
sent := 0 count := 0
for rows.Next() { for rows.Next() {
var username, token string var username string
if err := rows.Scan(&username, &token); err != nil { if err := rows.Scan(&username); err != nil {
continue continue
} }
notifKey := fmt.Sprintf("notifications:%s", username) notifKey := fmt.Sprintf("notifications:%s", username)
Redis.LPush(RedisCtx, notifKey, notifJSON) Redis.LPush(RedisCtx, notifKey, notifJSON)
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
if token != "" { // Diffusion Telegram individuelle
go sendExpoPushWithChannel(token, title, body, alertID, "alert", "orders") if services.TelegramBot != nil && services.TelegramBot.IsConfigured() {
sent++ if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok {
capturedChatID := chatID
capturedBody := body
go services.TelegramBot.SendMessage(capturedChatID, fmt.Sprintf("🚨 <b>Alerte livreur</b>\n\n%s", capturedBody))
} }
} }
log.Printf("🚨 [ALERT_NOTIF] Notif Redis + push (%d tokens) pour alerte #%d de %s", sent, alertID, livreurUsername) count++
}
log.Printf("🚨 [ALERT_NOTIF] Notif Redis (%d users) pour alerte #%d de %s", count, alertID, livreurUsername)
} }
// AddDeliveryRating ajoute une note pour un livreur // AddDeliveryRating ajoute une note pour un livreur
+23 -4
View File
@@ -9,7 +9,7 @@ import (
) )
// CreateProduct crée un nouveau produit avec ses prix // CreateProduct crée un nouveau produit avec ses prix
func (db *Database) CreateProduct(product interface{}) error { func (db *Database) CreateProduct(product any) error {
log.Printf("🔍 [DB CreateProduct] Type reçu: %T", product) log.Printf("🔍 [DB CreateProduct] Type reçu: %T", product)
log.Printf("🔍 [DB CreateProduct] Valeur: %+v", product) log.Printf("🔍 [DB CreateProduct] Valeur: %+v", product)
@@ -27,7 +27,6 @@ func (db *Database) CreateProduct(product interface{}) error {
p, ok := product.(ProductInterface) p, ok := product.(ProductInterface)
if !ok { if !ok {
// Vérifier si c'est un pointeur vers models.Product
if prodPtr, isPtr := product.(*models.Product); isPtr { if prodPtr, isPtr := product.(*models.Product); isPtr {
log.Printf("✅ [DB CreateProduct] C'est un *models.Product, utilisons-le directement") log.Printf("✅ [DB CreateProduct] C'est un *models.Product, utilisons-le directement")
p = prodPtr // ça fonctionne maintenant car *models.Product implémente ProductInterface p = prodPtr // ça fonctionne maintenant car *models.Product implémente ProductInterface
@@ -221,8 +220,28 @@ func (db *Database) GetProductsByCategory(category string) ([]models.Product, er
return products, nil return products, nil
} }
func (db *Database) UpdateProduct(productID int, product interface{}) error { func (db *Database) UpdateProduct(productID int, name, category, description, unit string, stock float64, prices []models.ProductPrice) error {
// À implémenter selon vos besoins _, err := db.Exec(`
UPDATE products
SET name = $1, category = $2, description = $3, stock = $4, unit = $5, updated_at = $6
WHERE id = $7
`, name, category, description, stock, unit, time.Now(), productID)
if err != nil {
return fmt.Errorf("erreur mise à jour produit: %w", err)
}
db.Exec(`DELETE FROM product_prices WHERE product_id = $1`, productID)
for _, price := range prices {
_, err := db.Exec(`
INSERT INTO product_prices (product_id, quantity, price)
VALUES ($1, $2, $3)
`, productID, price.Quantity, price.Price)
if err != nil {
log.Printf("❌ [UpdateProduct] Erreur prix: %v", err)
}
}
return nil return nil
} }
+3 -28
View File
@@ -10,10 +10,10 @@ import (
"fmt" "fmt"
"gestion/models" "gestion/models"
"log" "log"
"slices"
) )
// ProcessNextCommandForDeliveryman traite automatiquement la prochaine commande // ProcessNextCommandForDeliveryman traite automatiquement la prochaine commande
// Appelé après qu'une commande soit livrée ou annulée
func (d *Database) ProcessNextCommandForDeliveryman(deliveryman string) error { func (d *Database) ProcessNextCommandForDeliveryman(deliveryman string) error {
log.Printf("🔄 [NEXT_COMMAND] Traitement prochaine commande pour %s", deliveryman) log.Printf("🔄 [NEXT_COMMAND] Traitement prochaine commande pour %s", deliveryman)
@@ -34,9 +34,6 @@ func (d *Database) ProcessNextCommandForDeliveryman(deliveryman string) error {
return nil return nil
} }
log.Printf("📋 [NEXT_COMMAND] %d commande(s) dans la queue de %s", len(commandIDs), deliveryman)
// ✅ BOUCLE: Essayer toutes les commandes jusqu'à trouver une valide
for i, cmdIDStr := range commandIDs { for i, cmdIDStr := range commandIDs {
commandID := extractCommandID(cmdIDStr) commandID := extractCommandID(cmdIDStr)
if commandID <= 0 { if commandID <= 0 {
@@ -74,32 +71,16 @@ func (d *Database) ProcessNextCommandForDeliveryman(deliveryman string) error {
currentStatus, _ := command["status"].(string) currentStatus, _ := command["status"].(string)
log.Printf("📊 [NEXT_COMMAND] Commande %d: statut = '%s'", commandID, currentStatus) log.Printf("📊 [NEXT_COMMAND] Commande %d: statut = '%s'", commandID, currentStatus)
// ✅ Si la commande n'est plus assignable, la retirer et passer à la suivante
nonAssignableStatuses := []string{"livre", "approved", "cancelled", "disabled"} nonAssignableStatuses := []string{"livre", "approved", "cancelled", "disabled"}
isNonAssignable := false if slices.Contains(nonAssignableStatuses, currentStatus) {
for _, s := range nonAssignableStatuses {
if currentStatus == s {
isNonAssignable = true
break
}
}
if isNonAssignable {
log.Printf("⚠️ [NEXT_COMMAND] Commande %d en statut '%s' - Retrait de la queue", commandID, currentStatus)
d.RemoveCommandFromAllQueues(commandID, deliveryman) d.RemoveCommandFromAllQueues(commandID, deliveryman)
continue // Essayer la suivante continue
} }
// ✅ Commande valide trouvée !
log.Printf("✅ [NEXT_COMMAND] Commande %d prête pour %s (statut: %s)", commandID, deliveryman, currentStatus)
// Optimiser la queue par proximité si possible
go d.OptimizeDeliverymanQueueByProximity(deliveryman) go d.OptimizeDeliverymanQueueByProximity(deliveryman)
// ✅ Mettre à jour le statut basé sur la queue
d.UpdateDeliverymanStatusBasedOnQueue(deliveryman) d.UpdateDeliverymanStatusBasedOnQueue(deliveryman)
// Ajouter un log
d.AddCommandLog(commandID, "next_in_queue", d.AddCommandLog(commandID, "next_in_queue",
fmt.Sprintf("Commande suivante dans la queue de %s", deliveryman), fmt.Sprintf("Commande suivante dans la queue de %s", deliveryman),
"system") "system")
@@ -107,9 +88,6 @@ func (d *Database) ProcessNextCommandForDeliveryman(deliveryman string) error {
return nil return nil
} }
// ✅ Si on arrive ici, toutes les commandes étaient invalides
log.Printf("⚠️ [NEXT_COMMAND] Toutes les commandes de %s étaient invalides - Queue vidée", deliveryman)
// Mettre le livreur en available // Mettre le livreur en available
d.SetDeliveryPersonStatus(deliveryman, "available", 0) d.SetDeliveryPersonStatus(deliveryman, "available", 0)
d.UpdateDeliverymanStatusBasedOnQueue(deliveryman) d.UpdateDeliverymanStatusBasedOnQueue(deliveryman)
@@ -138,10 +116,8 @@ func (d *Database) RemoveCommandFromDeliverymanQueue(deliveryman string, command
log.Printf("✅ [RemoveFromDeliverymanQueue] Commande %d retirée", commandID) log.Printf("✅ [RemoveFromDeliverymanQueue] Commande %d retirée", commandID)
} }
// Supprimer les données
Redis.Del(RedisCtx, commandKey) Redis.Del(RedisCtx, commandKey)
// Décrémenter le compteur
counterKey := fmt.Sprintf("queue:deliveryman:%s:count", deliveryman) counterKey := fmt.Sprintf("queue:deliveryman:%s:count", deliveryman)
Redis.Decr(RedisCtx, counterKey) Redis.Decr(RedisCtx, counterKey)
@@ -149,7 +125,6 @@ func (d *Database) RemoveCommandFromDeliverymanQueue(deliveryman string, command
} }
// CleanupCompletedCommandFromQueue nettoie une commande terminée et prépare la suivante // CleanupCompletedCommandFromQueue nettoie une commande terminée et prépare la suivante
// À appeler depuis les handlers d'annulation et de livraison
func (d *Database) CleanupCompletedCommandFromQueue(commandID int, deliveryman string) error { func (d *Database) CleanupCompletedCommandFromQueue(commandID int, deliveryman string) error {
log.Printf("🧹 [CLEANUP] Nettoyage commande %d pour %s", commandID, deliveryman) log.Printf("🧹 [CLEANUP] Nettoyage commande %d pour %s", commandID, deliveryman)
-6
View File
@@ -70,13 +70,7 @@ func (d *Database) DebitReferralBalance(username string, amount float64) error {
return tx.Commit() return tx.Commit()
} }
// RestoreReferralBalance restaure le solde parrainage si la commande échoue après le débit.
func (d *Database) RestoreReferralBalance(username string, amount float64) error {
return d.CreditClientReferral(username, amount)
}
// UseClientReferralBalance déduit un montant du solde parrainage dans une transaction. // UseClientReferralBalance déduit un montant du solde parrainage dans une transaction.
// Retourne une erreur si le solde est insuffisant.
func (d *Database) UseClientReferralBalance(tx *sql.Tx, username string, amount float64) error { func (d *Database) UseClientReferralBalance(tx *sql.Tx, username string, amount float64) error {
if amount <= 0 { if amount <= 0 {
return nil return nil
+39 -52
View File
@@ -7,7 +7,9 @@ package db
import ( import (
"fmt" "fmt"
"gestion/models"
"log" "log"
"sort"
) )
// GetClientCancellationsCount récupère le nombre d'annulations tardives d'un client // GetClientCancellationsCount récupère le nombre d'annulations tardives d'un client
@@ -45,51 +47,53 @@ func (d *Database) IncrementClientCancellationsCount(username string) error {
return fmt.Errorf("client non trouvé") return fmt.Errorf("client non trouvé")
} }
log.Printf("✅ [IncrementCancellations] Compteur incrémenté pour %s", username)
// Invalider le cache Redis
cacheKey := fmt.Sprintf("client:%s", username) cacheKey := fmt.Sprintf("client:%s", username)
Redis.Del(RedisCtx, cacheKey) Redis.Del(RedisCtx, cacheKey)
return nil return nil
} }
// CalculateCancellationPenalty calcule la pénalité selon l'historique // penaltyForCount retourne le montant du palier applicable pour un nombre d'annulations donné
// 1ère fois: 20 points func penaltyForCount(count int, tiers []models.PenaltyTier) int {
// 2ème fois: 50 points if len(tiers) == 0 {
// 3ème fois: 100 points return 0
// 4ème+ fois: 150 points }
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
}
// CalculateCancellationPenalty calcule la pénalité selon l'historique et le barème configuré
func (d *Database) CalculateCancellationPenalty(username string) (int, error) { func (d *Database) CalculateCancellationPenalty(username string) (int, error) {
count, err := d.GetClientCancellationsCount(username) count, err := d.GetClientCancellationsCount(username)
if err != nil { if err != nil {
return 0, err return 0, err
} }
var penalty int settings, err := d.GetSettings()
switch { if err != nil {
case count == 0: log.Printf("⚠️ [CalculatePenalty] Impossible de charger les settings, barème par défaut: %v", err)
penalty = 20 // Première annulation tardive settings = DefaultSettings()
case count == 1:
penalty = 50 // Deuxième annulation tardive
case count == 2:
penalty = 100 // Troisième annulation tardive
default:
penalty = 150 // À partir de la 4ème annulation
} }
penalty := penaltyForCount(count, settings.PenaltyTiers)
log.Printf("💰 [CalculatePenalty] Client %s - Annulations: %d → Pénalité: %d points", log.Printf("💰 [CalculatePenalty] Client %s - Annulations: %d → Pénalité: %d points",
username, count, penalty) username, count, penalty)
return penalty, nil return penalty, nil
} }
// ApplyCancellationPenalty applique une pénalité, incrémente le compteur et REMET TOUS LES POINTS À ZÉRO // ApplyCancellationPenalty applique une pénalité et incrémente le compteur d'annulations
// ✅ MODIFIÉ: Récupère les points AVANT de les remettre à zéro pour le log dans le handler
func (d *Database) ApplyCancellationPenalty(username string) (int, error) { func (d *Database) ApplyCancellationPenalty(username string) (int, error) {
// ✅ ÉTAPE 0: Récupérer les points AVANT modification (pour le handler)
// Note: Le handler récupère aussi les points, mais on garde cette fonction autonome
// Calculer la pénalité AVANT d'incrémenter
penalty, err := d.CalculateCancellationPenalty(username) penalty, err := d.CalculateCancellationPenalty(username)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -97,16 +101,12 @@ func (d *Database) ApplyCancellationPenalty(username string) (int, error) {
log.Printf("⚠️ [ApplyCancellationPenalty] Client %s - Pénalité calculée: %d points", username, penalty) log.Printf("⚠️ [ApplyCancellationPenalty] Client %s - Pénalité calculée: %d points", username, penalty)
// ✅ ÉTAPE 1: Incrémenter le compteur d'annulations
if err := d.IncrementClientCancellationsCount(username); err != nil { if err := d.IncrementClientCancellationsCount(username); err != nil {
return 0, err return 0, err
} }
// ✅ ÉTAPE 2: Mettre l'amende au montant de la pénalité ET remettre TOUS les points à zéro
query := `UPDATE clients query := `UPDATE clients
SET amende = $1, SET amende = $1,
point = 0,
point_zipette = 0,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE username = $2` WHERE username = $2`
@@ -124,9 +124,8 @@ func (d *Database) ApplyCancellationPenalty(username string) (int, error) {
return 0, fmt.Errorf("client non trouvé") return 0, fmt.Errorf("client non trouvé")
} }
log.Printf("✅ [ApplyCancellationPenalty] %d points d'amende appliqués à %s + TOUS points remis à 0 (weed + zipette)", penalty, username) log.Printf("✅ [ApplyCancellationPenalty] Amende %d appliquée à %s", penalty, username)
// Invalider le cache Redis
cacheKey := fmt.Sprintf("client:%s", username) cacheKey := fmt.Sprintf("client:%s", username)
Redis.Del(RedisCtx, cacheKey) Redis.Del(RedisCtx, cacheKey)
@@ -134,42 +133,30 @@ func (d *Database) ApplyCancellationPenalty(username string) (int, error) {
} }
// GetClientCancellationHistory récupère l'historique d'annulations d'un client // GetClientCancellationHistory récupère l'historique d'annulations d'un client
func (d *Database) GetClientCancellationHistory(username string) (map[string]interface{}, error) { 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) count, err := d.GetClientCancellationsCount(username)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Calculer la prochaine pénalité settings, _ := d.GetSettings()
var nextPenalty int
switch count {
case 0:
nextPenalty = 20
case 1:
nextPenalty = 50
case 2:
nextPenalty = 100
default:
nextPenalty = 150
}
// Récupérer l'amende actuelle
client, err := d.GetClientByUsername(username) client, err := d.GetClientByUsername(username)
var currentAmende float64 var currentAmende float64
if err == nil { if err == nil {
currentAmende = client.Amende currentAmende = client.Amende
} }
return map[string]interface{}{ return map[string]any{
"cancellations_count": count, "cancellations_count": count,
"current_amende": currentAmende, "current_amende": currentAmende,
"next_penalty": nextPenalty, "next_penalty": nextPenalty,
"penalty_scale": map[string]int{ "penalty_tiers": settings.PenaltyTiers,
"1st": 20, "warning": "Une amende sera appliquée lors de la prochaine annulation tardive",
"2nd": 50,
"3rd": 100,
"4th+": 150,
},
"warning": "TOUS les points de fidélité (weed/hash ET zipette) seront remis à zéro lors de la prochaine annulation tardive",
}, nil }, nil
} }
+49 -76
View File
@@ -3,52 +3,20 @@ package db
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"gestion/models"
) )
// DaySchedule représente les horaires de livraison pour un jour de la semaine
type DaySchedule struct {
Enabled bool `json:"enabled"`
OpenTime string `json:"open_time"` // ex: "09:00"
CloseTime string `json:"close_time"` // ex: "20:00"
}
// DeliverySchedule représente les horaires de livraison pour chaque jour
type DeliverySchedule struct {
Monday DaySchedule `json:"monday"`
Tuesday DaySchedule `json:"tuesday"`
Wednesday DaySchedule `json:"wednesday"`
Thursday DaySchedule `json:"thursday"`
Friday DaySchedule `json:"friday"`
Saturday DaySchedule `json:"saturday"`
Sunday DaySchedule `json:"sunday"`
}
// DefaultDeliverySchedule retourne un planning de livraison par défaut (tous les jours, 9h-20h) // DefaultDeliverySchedule retourne un planning de livraison par défaut (tous les jours, 9h-20h)
func DefaultDeliverySchedule() DeliverySchedule { func DefaultDeliverySchedule() models.DeliverySchedule {
day := DaySchedule{Enabled: true, OpenTime: "09:00", CloseTime: "20:00"} day := models.DaySchedule{Enabled: true, OpenTime: "09:00", CloseTime: "20:00"}
return DeliverySchedule{ return models.DeliverySchedule{
Monday: day, Tuesday: day, Wednesday: day, Thursday: day, Monday: day, Tuesday: day, Wednesday: day, Thursday: day,
Friday: day, Saturday: day, Sunday: day, Friday: day, Saturday: day, Sunday: day,
} }
} }
// PostalZone représente une zone de livraison avec un minimum de commande
type PostalZone struct {
Name string `json:"name"`
MinAmount float64 `json:"min_amount"`
Codes []string `json:"codes"`
}
// PointsTier représente un palier du barème de points
// Si Max == 0, il n'y a pas de borne supérieure (illimité)
type PointsTier struct {
Min float64 `json:"min"`
Max float64 `json:"max"` // 0 = illimité
Points int `json:"points"`
}
// CalcPointsFromTiers retourne le nombre de points correspondant au total selon les paliers // CalcPointsFromTiers retourne le nombre de points correspondant au total selon les paliers
func CalcPointsFromTiers(total float64, tiers []PointsTier) int { func CalcPointsFromTiers(total float64, tiers []models.PointsTier) int {
for _, t := range tiers { for _, t := range tiers {
if total >= t.Min && (t.Max == 0 || total <= t.Max) { if total >= t.Min && (t.Max == 0 || total <= t.Max) {
return t.Points return t.Points
@@ -57,44 +25,25 @@ func CalcPointsFromTiers(total float64, tiers []PointsTier) int {
return 0 return 0
} }
// PointsPool représente un type de point personnalisable par l'admin
// Pool[0] → colonne `point`, Pool[1] → colonne `point_zipette`
type PointsPool struct {
Key string `json:"key"` // identifiant interne (ex: "pool_0")
Name string `json:"name"` // nom affiché (ex: "Cannabis", "Accessoires")
Categories []string `json:"categories"` // catégories de produits assignées à ce pool
Tiers []PointsTier `json:"tiers"` // barème de points
}
// AppSettings contient les paramètres globaux de l'application
type AppSettings struct {
PenaltiesEnabled bool `json:"penalties_enabled"`
ShowAmendeScore bool `json:"show_amende_score"` // afficher le score d'amendes aux clients/cabine
PointsEnabled bool `json:"points_enabled"` // afficher/activer le système de points
PointsPools []PointsPool `json:"points_pools"` // types de points personnalisés
ReferralEnabled bool `json:"referral_enabled"` // activer/désactiver le système de parrainage
CryptoPaymentEnabled bool `json:"crypto_payment_enabled"` // activer/désactiver le paiement crypto
CryptoOnly bool `json:"crypto_only"` // forcer le paiement crypto uniquement (pas d'espèces)
NowPaymentsAPIKey string `json:"nowpayments_api_key"` // clé API NowPayments
NowPaymentsIPNSecret string `json:"nowpayments_ipn_secret"` // secret IPN NowPayments
NowPaymentsCurrencies []string `json:"nowpayments_currencies"` // cryptos acceptées (ex: ["btc","eth","ltc"])
DeliverySchedule DeliverySchedule `json:"delivery_schedule"` // horaires de livraison par jour
PostalZones []PostalZone `json:"postal_zones"` // zones de livraison avec minimum de commande
}
// DefaultSettings retourne les paramètres par défaut // DefaultSettings retourne les paramètres par défaut
func DefaultSettings() AppSettings { func DefaultSettings() models.AppSettings {
return AppSettings{ return models.AppSettings{
PenaltiesEnabled: true, PenaltiesEnabled: true,
ShowAmendeScore: true, ShowAmendeScore: true,
PenaltyTiers: []models.PenaltyTier{
{MinCancel: 0, Amount: 20},
{MinCancel: 1, Amount: 50},
{MinCancel: 2, Amount: 100},
{MinCancel: 3, Amount: 150},
},
PointsEnabled: true, PointsEnabled: true,
ReferralEnabled: true, ReferralEnabled: true,
PointsPools: []PointsPool{ PointsPools: []models.PointsPool{
{ {
Key: "pool_0", Key: "pool_0",
Name: "Pool 1", Name: "Pool 1",
Categories: []string{}, Categories: []string{},
Tiers: []PointsTier{ Tiers: []models.PointsTier{
{Min: 30, Max: 50, Points: 1}, {Min: 30, Max: 50, Points: 1},
{Min: 60, Max: 150, Points: 2}, {Min: 60, Max: 150, Points: 2},
{Min: 160, Max: 300, Points: 3}, {Min: 160, Max: 300, Points: 3},
@@ -106,15 +55,19 @@ func DefaultSettings() AppSettings {
Key: "pool_1", Key: "pool_1",
Name: "Pool 2", Name: "Pool 2",
Categories: []string{}, Categories: []string{},
Tiers: []PointsTier{ Tiers: []models.PointsTier{
{Min: 30, Max: 100, Points: 1}, {Min: 30, Max: 100, Points: 1},
{Min: 110, Max: 200, Points: 2}, {Min: 110, Max: 200, Points: 2},
{Min: 210, Max: 0, Points: 3}, {Min: 210, Max: 0, Points: 3},
}, },
}, },
}, },
DeliveryMode: models.DeliveryModeConfig{
Mode: "single",
CategoryRoutes: []models.CategoryRoute{},
},
DeliverySchedule: DefaultDeliverySchedule(), DeliverySchedule: DefaultDeliverySchedule(),
PostalZones: []PostalZone{ PostalZones: []models.PostalZone{
{Name: "Zone 30€", MinAmount: 30, Codes: []string{"44000", "44100", "44200", "44300"}}, {Name: "Zone 30€", MinAmount: 30, Codes: []string{"44000", "44100", "44200", "44300"}},
{Name: "Zone 50€", MinAmount: 50, Codes: []string{ {Name: "Zone 50€", MinAmount: 50, Codes: []string{
"44400", "44880", "44120", "44230", "44115", "44400", "44880", "44120", "44230", "44115",
@@ -128,7 +81,7 @@ func DefaultSettings() AppSettings {
} }
// GetSettings récupère les paramètres depuis la DB // GetSettings récupère les paramètres depuis la DB
func (d *Database) GetSettings() (AppSettings, error) { func (d *Database) GetSettings() (models.AppSettings, error) {
settings := DefaultSettings() settings := DefaultSettings()
rows, err := d.Query(`SELECT key, value FROM app_settings`) rows, err := d.Query(`SELECT key, value FROM app_settings`)
@@ -150,7 +103,7 @@ func (d *Database) GetSettings() (AppSettings, error) {
case "points_enabled": case "points_enabled":
settings.PointsEnabled = value == "true" settings.PointsEnabled = value == "true"
case "points_pools": case "points_pools":
var pools []PointsPool var pools []models.PointsPool
if err := json.Unmarshal([]byte(value), &pools); err == nil { if err := json.Unmarshal([]byte(value), &pools); err == nil {
settings.PointsPools = pools settings.PointsPools = pools
} }
@@ -170,22 +123,31 @@ func (d *Database) GetSettings() (AppSettings, error) {
settings.NowPaymentsCurrencies = currencies settings.NowPaymentsCurrencies = currencies
} }
case "delivery_schedule": case "delivery_schedule":
var sched DeliverySchedule var sched models.DeliverySchedule
if err := json.Unmarshal([]byte(value), &sched); err == nil { if err := json.Unmarshal([]byte(value), &sched); err == nil {
settings.DeliverySchedule = sched settings.DeliverySchedule = sched
} }
case "postal_zones": case "postal_zones":
var zones []PostalZone var zones []models.PostalZone
if err := json.Unmarshal([]byte(value), &zones); err == nil { if err := json.Unmarshal([]byte(value), &zones); err == nil {
settings.PostalZones = zones settings.PostalZones = zones
} }
case "telegram_bot_token":
settings.TelegramBotToken = value
case "telegram_bot_username":
settings.TelegramBotUsername = value
case "delivery_mode":
var mode models.DeliveryModeConfig
if err := json.Unmarshal([]byte(value), &mode); err == nil {
settings.DeliveryMode = mode
}
} }
} }
return settings, rows.Err() return settings, rows.Err()
} }
// UpdateSettings sauvegarde les paramètres dans la DB // UpdateSettings sauvegarde les paramètres dans la DB
func (d *Database) UpdateSettings(s AppSettings) error { func (d *Database) UpdateSettings(s models.AppSettings) error {
boolStr := func(b bool) string { boolStr := func(b bool) string {
if b { if b {
return "true" return "true"
@@ -194,7 +156,7 @@ func (d *Database) UpdateSettings(s AppSettings) error {
} }
if s.PointsPools == nil { if s.PointsPools == nil {
s.PointsPools = []PointsPool{} s.PointsPools = []models.PointsPool{}
} }
// S'assurer que chaque pool a des slices non-nil // S'assurer que chaque pool a des slices non-nil
for i := range s.PointsPools { for i := range s.PointsPools {
@@ -202,7 +164,7 @@ func (d *Database) UpdateSettings(s AppSettings) error {
s.PointsPools[i].Categories = []string{} s.PointsPools[i].Categories = []string{}
} }
if s.PointsPools[i].Tiers == nil { if s.PointsPools[i].Tiers == nil {
s.PointsPools[i].Tiers = []PointsTier{} s.PointsPools[i].Tiers = []models.PointsTier{}
} }
} }
@@ -248,13 +210,24 @@ func (d *Database) UpdateSettings(s AppSettings) error {
pairs = append(pairs, [2]string{"delivery_schedule", string(schedJSON)}) pairs = append(pairs, [2]string{"delivery_schedule", string(schedJSON)})
if s.PostalZones == nil { if s.PostalZones == nil {
s.PostalZones = []PostalZone{} s.PostalZones = []models.PostalZone{}
} }
zonesJSON, err := json.Marshal(s.PostalZones) zonesJSON, err := json.Marshal(s.PostalZones)
if err != nil { if err != nil {
return fmt.Errorf("erreur sérialisation postal_zones: %w", err) return fmt.Errorf("erreur sérialisation postal_zones: %w", err)
} }
pairs = append(pairs, [2]string{"postal_zones", string(zonesJSON)}) pairs = append(pairs, [2]string{"postal_zones", string(zonesJSON)})
pairs = append(pairs, [2]string{"telegram_bot_token", s.TelegramBotToken})
pairs = append(pairs, [2]string{"telegram_bot_username", s.TelegramBotUsername})
if s.DeliveryMode.CategoryRoutes == nil {
s.DeliveryMode.CategoryRoutes = []models.CategoryRoute{}
}
deliveryModeJSON, err := json.Marshal(s.DeliveryMode)
if err != nil {
return fmt.Errorf("erreur sérialisation delivery_mode: %w", err)
}
pairs = append(pairs, [2]string{"delivery_mode", string(deliveryModeJSON)})
for _, p := range pairs { for _, p := range pairs {
if _, err = tx.Exec(upsert, p[0], p[1]); err != nil { if _, err = tx.Exec(upsert, p[0], p[1]); err != nil {
+121
View File
@@ -0,0 +1,121 @@
package db
import (
"crypto/rand"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"gestion/models"
"log"
"time"
)
// MigrateAddTelegramColumns ajoute les colonnes telegram_chat_id si elles n'existent pas
func (d *Database) MigrateAddTelegramColumns() {
migrations := []string{
`ALTER TABLE clients ADD COLUMN IF NOT EXISTS telegram_chat_id BIGINT`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS telegram_chat_id BIGINT`,
}
for _, q := range migrations {
if _, err := d.Exec(q); err != nil {
log.Printf("⚠️ [TELEGRAM_MIGRATION] %v", err)
}
}
log.Println("✅ [TELEGRAM] Colonnes telegram_chat_id vérifiées")
}
const linkTokenTTL = 10 * time.Minute
// GenerateLinkToken crée un token aléatoire sécurisé et le stocke dans Redis (10 min)
func GenerateLinkToken(username, role string) (string, error) {
raw := make([]byte, 16)
if _, err := rand.Read(raw); err != nil {
return "", fmt.Errorf("génération token: %w", err)
}
token := hex.EncodeToString(raw)
data := models.TelegramLinkData{Username: username, Role: role}
val, err := json.Marshal(data)
if err != nil {
return "", err
}
key := fmt.Sprintf("telegram:link:%s", token)
if err := Redis.Set(RedisCtx, key, val, linkTokenTTL).Err(); err != nil {
return "", fmt.Errorf("Redis SET: %w", err)
}
return token, nil
}
// ValidateAndConsumeLinkToken valide le token, retourne les données, puis le supprime
func ValidateAndConsumeLinkToken(token string) (username, role string, err error) {
key := fmt.Sprintf("telegram:link:%s", token)
val, err := Redis.Get(RedisCtx, key).Bytes()
if err != nil {
return "", "", fmt.Errorf("token invalide ou expiré")
}
var data models.TelegramLinkData
if err := json.Unmarshal(val, &data); err != nil {
return "", "", fmt.Errorf("données corrompues")
}
Redis.Del(RedisCtx, key)
return data.Username, data.Role, nil
}
func (d *Database) SaveClientTelegramChatID(username string, chatID int64) error {
_, err := d.Exec(`UPDATE clients SET telegram_chat_id = $1 WHERE username = $2`, chatID, username)
return err
}
func (d *Database) GetClientTelegramChatID(username string) (int64, bool, error) {
var chatID sql.NullInt64
err := d.QueryRow(`SELECT telegram_chat_id FROM clients WHERE username = $1`, username).Scan(&chatID)
if err != nil {
return 0, false, err
}
return chatID.Int64, chatID.Valid, nil
}
func (d *Database) DeleteClientTelegramChatID(username string) error {
_, err := d.Exec(`UPDATE clients SET telegram_chat_id = NULL WHERE username = $1`, username)
return err
}
func (d *Database) SaveUserTelegramChatID(username string, chatID int64) error {
_, err := d.Exec(`UPDATE users SET telegram_chat_id = $1 WHERE username = $2`, chatID, username)
return err
}
func (d *Database) GetUserTelegramChatID(username string) (int64, bool, error) {
var chatID sql.NullInt64
err := d.QueryRow(`SELECT telegram_chat_id FROM users WHERE username = $1`, username).Scan(&chatID)
if err != nil {
return 0, false, err
}
return chatID.Int64, chatID.Valid, nil
}
func (d *Database) DeleteUserTelegramChatID(username string) error {
_, err := d.Exec(`UPDATE users SET telegram_chat_id = NULL WHERE username = $1`, username)
return err
}
// GetUserByTelegramChatID retrouve un utilisateur (clients + users) par chat_id
func (d *Database) GetUserByTelegramChatID(chatID int64) (username, role string, err error) {
err = d.QueryRow(`SELECT username FROM clients WHERE telegram_chat_id = $1`, chatID).Scan(&username)
if err == nil {
return username, "client", nil
}
err = d.QueryRow(`SELECT username, role FROM users WHERE telegram_chat_id = $1`, chatID).Scan(&username, &role)
if err == nil {
return username, role, nil
}
return "", "", fmt.Errorf("aucun compte lié à ce chat_id")
}
-3
View File
@@ -123,7 +123,6 @@ func (d *Database) UpdateUser(user *models.User) error {
// DeleteUser supprime un utilisateur // DeleteUser supprime un utilisateur
func (d *Database) DeleteUser(id int) error { func (d *Database) DeleteUser(id int) error {
// Récupérer le rôle de l'utilisateur avant de le supprimer
var role string var role string
err := d.QueryRow(`SELECT role FROM users WHERE id = $1`, id).Scan(&role) err := d.QueryRow(`SELECT role FROM users WHERE id = $1`, id).Scan(&role)
if err != nil { if err != nil {
@@ -133,7 +132,6 @@ func (d *Database) DeleteUser(id int) error {
return fmt.Errorf("erreur lors de la récupération du rôle: %w", err) return fmt.Errorf("erreur lors de la récupération du rôle: %w", err)
} }
// ✅ MODIFIÉ : Supprimer tous les tokens de l'utilisateur
_ = d.RevokeAllUserTokens(id, role) _ = d.RevokeAllUserTokens(id, role)
query := `DELETE FROM users WHERE id = $1` query := `DELETE FROM users WHERE id = $1`
@@ -194,7 +192,6 @@ func (d *Database) GetUserByUsername(username string) (*models.User, error) {
&user.Role, &user.Role,
) )
// ✅ Vérifier sql.ErrNoRows et retourner une erreur
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("utilisateur non trouvé") return nil, fmt.Errorf("utilisateur non trouvé")
} }
@@ -9,7 +9,6 @@ import (
) )
// FindLeastLoadedDeliveryman trouve le livreur avec le moins de commandes ET qui peut accepter // FindLeastLoadedDeliveryman trouve le livreur avec le moins de commandes ET qui peut accepter
// ✅ Respecte le statut BUSY (queue >= 10)
func (d *Database) FindLeastLoadedDeliveryman() (string, error) { func (d *Database) FindLeastLoadedDeliveryman() (string, error) {
keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result() keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result()
if err != nil || len(keys) == 0 { if err != nil || len(keys) == 0 {
@@ -30,7 +29,6 @@ func (d *Database) FindLeastLoadedDeliveryman() (string, error) {
var status models.DeliveryPersonStatus var status models.DeliveryPersonStatus
json.Unmarshal([]byte(data), &status) json.Unmarshal([]byte(data), &status)
// ✅ Ignorer les livreurs offline
if status.Status == "offline" { if status.Status == "offline" {
continue continue
} }
@@ -70,7 +68,6 @@ func (d *Database) FindLeastLoadedDeliveryman() (string, error) {
} }
// FindAvailableOrLeastLoadedDeliveryman trouve un livreur disponible ou le moins chargé // FindAvailableOrLeastLoadedDeliveryman trouve un livreur disponible ou le moins chargé
// ✅ Respecte le statut BUSY
func (d *Database) FindAvailableOrLeastLoadedDeliveryman() (string, string, int, error) { func (d *Database) FindAvailableOrLeastLoadedDeliveryman() (string, string, int, error) {
keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result() keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result()
if err != nil || len(keys) == 0 { if err != nil || len(keys) == 0 {
@@ -96,7 +93,6 @@ func (d *Database) FindAvailableOrLeastLoadedDeliveryman() (string, string, int,
continue continue
} }
// ✅ NOUVEAU: Vérifier si le livreur peut accepter
if !d.CanDeliverymanAcceptCommands(username) { if !d.CanDeliverymanAcceptCommands(username) {
continue continue
} }
@@ -104,7 +100,6 @@ func (d *Database) FindAvailableOrLeastLoadedDeliveryman() (string, string, int,
queueKey := fmt.Sprintf("queue:deliveryman:%s", username) queueKey := fmt.Sprintf("queue:deliveryman:%s", username)
queueSize, _ := Redis.ZCard(RedisCtx, queueKey).Result() queueSize, _ := Redis.ZCard(RedisCtx, queueKey).Result()
// Priorité: available > busy avec moins de commandes
if status.Status == "available" && queueSize == 0 { if status.Status == "available" && queueSize == 0 {
return username, "available", 0, nil return username, "available", 0, nil
} }
@@ -124,7 +119,6 @@ func (d *Database) FindAvailableOrLeastLoadedDeliveryman() (string, string, int,
} }
// GetLeastLoadedDeliverymanForced retourne le livreur avec le moins de commandes (SANS limite) // GetLeastLoadedDeliverymanForced retourne le livreur avec le moins de commandes (SANS limite)
// ⚠️ À utiliser uniquement pour assignation forcée par admin
func (d *Database) GetLeastLoadedDeliverymanForced() (string, int64, error) { func (d *Database) GetLeastLoadedDeliverymanForced() (string, int64, error) {
activeUsernames, err := d.GetAllActiveDeliverymenUsernames() activeUsernames, err := d.GetAllActiveDeliverymenUsernames()
if err != nil || len(activeUsernames) == 0 { if err != nil || len(activeUsernames) == 0 {
@@ -144,9 +138,6 @@ func (d *Database) GetLeastLoadedDeliverymanForced() (string, int64, error) {
} }
} }
log.Printf("⚠️ [FORCED] %s sélectionné (%d commandes - SANS LIMITE)",
leastLoaded, minQueueSize)
return leastLoaded, minQueueSize, nil return leastLoaded, minQueueSize, nil
} }
@@ -157,7 +148,6 @@ func (d *Database) AreAllDeliverymenAtCapacity() (bool, int, error) {
return false, 0, fmt.Errorf("aucun livreur actif") return false, 0, fmt.Errorf("aucun livreur actif")
} }
// Si un seul livreur, jamais à capacité max
if len(activeUsernames) == 1 { if len(activeUsernames) == 1 {
return false, 1, nil return false, 1, nil
} }
@@ -10,9 +10,8 @@ import (
) )
func (d *Database) UpdateDeliveryPersonLocation(username string, lat, lon float64) error { func (d *Database) UpdateDeliveryPersonLocation(username string, lat, lon float64) error {
// 1️⃣ Mettre à jour la position GPS dans Redis
key := fmt.Sprintf("delivery:location:%s", username) key := fmt.Sprintf("delivery:location:%s", username)
location := map[string]interface{}{ location := map[string]any{
"latitude": lat, "latitude": lat,
"longitude": lon, "longitude": lon,
"last_update": time.Now().Unix(), "last_update": time.Now().Unix(),
@@ -25,25 +24,20 @@ func (d *Database) UpdateDeliveryPersonLocation(username string, lat, lon float6
log.Printf("📍 Position GPS mise à jour pour %s: (%.6f, %.6f)", username, lat, lon) log.Printf("📍 Position GPS mise à jour pour %s: (%.6f, %.6f)", username, lat, lon)
// 2️⃣ ✅ NOUVEAU: Auto-initialiser/synchroniser le statut
statusKey := fmt.Sprintf("delivery:status:%s", username) statusKey := fmt.Sprintf("delivery:status:%s", username)
statusData, err := Redis.Get(RedisCtx, statusKey).Result() statusData, err := Redis.Get(RedisCtx, statusKey).Result()
if err != nil || statusData == "" { if err != nil || statusData == "" {
// ✅ Pas de statut → Créer "available" par défaut
log.Printf("🆕 [INIT_STATUS] Création statut 'available' pour %s (première position GPS)", username) log.Printf("🆕 [INIT_STATUS] Création statut 'available' pour %s (première position GPS)", username)
d.SetDeliveryPersonStatus(username, "available", 0) d.SetDeliveryPersonStatus(username, "available", 0)
} else { } else {
// ✅ Statut existe → Vérifier s'il faut le réactiver
var status models.DeliveryPersonStatus var status models.DeliveryPersonStatus
json.Unmarshal([]byte(statusData), &status) json.Unmarshal([]byte(statusData), &status)
if status.Status == "offline" { if status.Status == "offline" {
// Si le livreur était offline et envoie sa position → Le remettre available
log.Printf("🔄 [REACTIVATE] %s passe de 'offline' à 'available' (position GPS reçue)", username) log.Printf("🔄 [REACTIVATE] %s passe de 'offline' à 'available' (position GPS reçue)", username)
d.SetDeliveryPersonStatus(username, "available", 0) d.SetDeliveryPersonStatus(username, "available", 0)
} else { } else {
// ✅ Statut actif → Synchroniser basé sur la queue
go d.UpdateDeliverymanStatusBasedOnQueue(username) go d.UpdateDeliverymanStatusBasedOnQueue(username)
} }
} }
@@ -66,12 +60,11 @@ func (d *Database) GetDeliveryPersonLocation(username string) (float64, float64,
return 0, 0, fmt.Errorf("aucune donnée de position pour %s", username) return 0, 0, fmt.Errorf("aucune donnée de position pour %s", username)
} }
var location map[string]interface{} var location map[string]any
if err := json.Unmarshal([]byte(data), &location); err != nil { if err := json.Unmarshal([]byte(data), &location); err != nil {
return 0, 0, fmt.Errorf("erreur parsing JSON Redis: %w", err) return 0, 0, fmt.Errorf("erreur parsing JSON Redis: %w", err)
} }
// Extraire latitude avec gestion de type robuste
var lat, lon float64 var lat, lon float64
if v, ok := location["latitude"]; ok && v != nil { if v, ok := location["latitude"]; ok && v != nil {
switch val := v.(type) { switch val := v.(type) {
@@ -88,7 +81,6 @@ func (d *Database) GetDeliveryPersonLocation(username string) (float64, float64,
} }
} }
// Extraire longitude avec gestion de type robuste
if v, ok := location["longitude"]; ok && v != nil { if v, ok := location["longitude"]; ok && v != nil {
switch val := v.(type) { switch val := v.(type) {
case float64: case float64:
+3 -16
View File
@@ -3,7 +3,6 @@ package db
import ( import (
"fmt" "fmt"
"gestion/services" "gestion/services"
"log"
"strconv" "strconv"
"time" "time"
@@ -16,7 +15,7 @@ func (d *Database) SetCommandETA(commandID, minutes int) error {
now := time.Now() now := time.Now()
arrivalTime := now.Add(time.Duration(minutes) * time.Minute) arrivalTime := now.Add(time.Duration(minutes) * time.Minute)
eta := map[string]interface{}{ eta := map[string]any{
"command_id": commandID, "command_id": commandID,
"total_eta_minutes": minutes, "total_eta_minutes": minutes,
"eta_minutes": minutes, "eta_minutes": minutes,
@@ -37,9 +36,6 @@ func (d *Database) SetCommandETA(commandID, minutes int) error {
d.ScheduleETANotifications(commandID, minutes) d.ScheduleETANotifications(commandID, minutes)
log.Printf("✅ ETA défini pour commande %d: %d minutes (arrivée: %s)",
commandID, minutes, arrivalTime.Format("15:04"))
return nil return nil
} }
@@ -86,7 +82,6 @@ func (d *Database) ScheduleETANotifications(commandID, etaMinutes int) error {
}) })
} }
log.Printf("✅ Notifications programmées pour commande %d (ETA: %d min)", commandID, etaMinutes)
return nil return nil
} }
@@ -121,15 +116,12 @@ func (d *Database) SendETANotification(commandID int, notifType string) {
channel := fmt.Sprintf("notifications:command:%d", commandID) channel := fmt.Sprintf("notifications:command:%d", commandID)
Redis.Publish(RedisCtx, channel, message) Redis.Publish(RedisCtx, channel, message)
log.Printf("📢 Notification envoyée: %s", message)
} }
// CalculateETAForDeliveryman calcule l'ETA entre un livreur et une destination // CalculateETAForDeliveryman calcule l'ETA entre un livreur et une destination
func (d *Database) CalculateETAForDeliveryman(deliveryman string, destLat, destLng float64) int { func (d *Database) CalculateETAForDeliveryman(deliveryman string, destLat, destLng float64) int {
livreurLat, livreurLng, err := d.GetDeliveryPersonLocation(deliveryman) livreurLat, livreurLng, err := d.GetDeliveryPersonLocation(deliveryman)
if err != nil { if err != nil {
log.Printf("⚠️ Position livreur %s non trouvée, utilisation ETA par défaut", deliveryman)
return services.MinETA return services.MinETA
} }
@@ -145,8 +137,6 @@ func (d *Database) CalculateETAForDeliveryman(deliveryman string, destLat, destL
distance := services.CalculateDistance(from, to) distance := services.CalculateDistance(from, to)
eta := services.CalculateETA(distance) eta := services.CalculateETA(distance)
log.Printf("📍 ETA calculé pour %s: %.2f km -> %d min", deliveryman, distance, eta)
return eta return eta
} }
@@ -163,7 +153,7 @@ func (d *Database) CalculateETABetweenPoints(lat1, lng1, lat2, lng2 float64) int
return services.CalculateETA(distance) return services.CalculateETA(distance)
} }
func (d *Database) GetDeliverymanQueueStats(deliveryman string) (map[string]interface{}, error) { func (d *Database) GetDeliverymanQueueStats(deliveryman string) (map[string]any, error) {
return d.GetDeliverymanQueueInfo(deliveryman) return d.GetDeliverymanQueueInfo(deliveryman)
} }
@@ -173,7 +163,7 @@ func (d *Database) SetCommandETAWithDetails(commandID, totalETA, queuePosition i
now := time.Now() now := time.Now()
arrivalTime := now.Add(time.Duration(totalETA) * time.Minute) arrivalTime := now.Add(time.Duration(totalETA) * time.Minute)
eta := map[string]interface{}{ eta := map[string]any{
"command_id": commandID, "command_id": commandID,
"total_eta_minutes": totalETA, "total_eta_minutes": totalETA,
"queue_position": queuePosition, "queue_position": queuePosition,
@@ -189,8 +179,5 @@ func (d *Database) SetCommandETAWithDetails(commandID, totalETA, queuePosition i
Redis.Expire(RedisCtx, key, 4*time.Hour) Redis.Expire(RedisCtx, key, 4*time.Hour)
log.Printf("✅ ETA détaillé pour commande %d: Total=%dmin Position=%d",
commandID, totalETA, queuePosition)
return nil return nil
} }
-10
View File
@@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"gestion/models" "gestion/models"
"log"
"math" "math"
"time" "time"
) )
@@ -74,10 +73,6 @@ func (d *Database) UpdateLivreurPosition(authUsername string, lat, lon float64,
return fmt.Errorf("erreur mise à jour statut: %w", err) return fmt.Errorf("erreur mise à jour statut: %w", err)
} }
// 🔹 5️⃣ Logs anonymisés (troncature)
log.Printf("📍 Position GPS mise à jour pour %s: (lat: %.4f, lon: %.4f)", authUsername, truncate(lat), truncate(lon))
log.Printf("✅ Statut mis à jour pour %s: %s", authUsername, status)
// 🔹 6️⃣ Publication événement sécurisée (à sécuriser côté subscriber) // 🔹 6️⃣ Publication événement sécurisée (à sécuriser côté subscriber)
d.PublishDeliveryPersonLocationUpdate(authUsername, lat, lon) d.PublishDeliveryPersonLocationUpdate(authUsername, lat, lon)
@@ -110,8 +105,3 @@ func isValidCoordinates(lat, lon float64) bool {
lat >= -90 && lat <= 90 && lat >= -90 && lat <= 90 &&
lon >= -180 && lon <= 180 lon >= -180 && lon <= 180
} }
// Tronque les coordonnées pour logs (4 décimales suffisent pour anonymiser)
func truncate(f float64) float64 {
return math.Round(f*10000) / 10000
}
+2 -2
View File
@@ -11,7 +11,7 @@ import (
func (d *Database) PublishCommandEvent(commandID int, eventType, message string) { func (d *Database) PublishCommandEvent(commandID int, eventType, message string) {
channel := fmt.Sprintf("events:command:%d", commandID) channel := fmt.Sprintf("events:command:%d", commandID)
event := map[string]interface{}{ event := map[string]any{
"command_id": commandID, "command_id": commandID,
"type": eventType, "type": eventType,
"message": message, "message": message,
@@ -24,7 +24,7 @@ func (d *Database) PublishCommandEvent(commandID int, eventType, message string)
// PublishDeliveryPersonLocationUpdate publie un événement de mise à jour de position // PublishDeliveryPersonLocationUpdate publie un événement de mise à jour de position
func (d *Database) PublishDeliveryPersonLocationUpdate(username string, lat, lon float64) { func (d *Database) PublishDeliveryPersonLocationUpdate(username string, lat, lon float64) {
message := map[string]interface{}{ message := map[string]any{
"type": "location_update", "type": "location_update",
"username": username, "username": username,
"latitude": lat, "latitude": lat,
@@ -26,7 +26,6 @@ func (d *Database) AssignCommandToDeliverymanQueue(commandID int, deliveryman st
return fmt.Errorf("livreur %s a atteint le maximum de commandes (%d)", deliveryman, MAX_COMMANDS_PER_DELIVERYMAN) return fmt.Errorf("livreur %s a atteint le maximum de commandes (%d)", deliveryman, MAX_COMMANDS_PER_DELIVERYMAN)
} }
// ✅ MODIFIÉ: ETA = temps de trajet direct uniquement
totalETA := estimatedTravelTime totalETA := estimatedTravelTime
var lat, lng float64 var lat, lng float64
@@ -86,9 +85,6 @@ func (d *Database) AssignCommandToDeliverymanQueue(commandID int, deliveryman st
deliveryman, currentQueueSize+1, limitInfo, totalETA), deliveryman, currentQueueSize+1, limitInfo, totalETA),
"system") "system")
log.Printf("✅ Commande %d -> Queue %s (pos: %d%s, ETA trajet: %d min)",
commandID, deliveryman, currentQueueSize+1, limitInfo, totalETA)
return nil return nil
} }
@@ -168,9 +164,6 @@ func (d *Database) AssignCommandToDeliverymanQueueWithCoords(commandID int, deli
return fmt.Errorf("erreur ajout à la queue: %w", err) return fmt.Errorf("erreur ajout à la queue: %w", err)
} }
// ============================================
// ✅ CORRECTION: Mettre à jour livreur_assign dans la DB
// ============================================
updateQuery := `UPDATE commandes updateQuery := `UPDATE commandes
SET livreur_assign = $1, SET livreur_assign = $1,
status = 'assigned', status = 'assigned',
@@ -213,7 +206,6 @@ func (d *Database) AssignCommandToDeliverymanQueueWithCoords(commandID int, deli
return nil return nil
} }
// ForceAssignCommandToDeliverymanWithCoords assigne une commande de force avec coordonnées
// ForceAssignCommandToDeliverymanWithCoords assigne une commande de force avec coordonnées // ForceAssignCommandToDeliverymanWithCoords assigne une commande de force avec coordonnées
func (d *Database) ForceAssignCommandToDeliverymanWithCoords(commandID int, deliveryman string, estimatedTravelTime int, lat, lng float64, address string) error { func (d *Database) ForceAssignCommandToDeliverymanWithCoords(commandID int, deliveryman string, estimatedTravelTime int, lat, lng float64, address string) error {
command, err := d.GetCommandByID(commandID) command, err := d.GetCommandByID(commandID)
+10 -24
View File
@@ -8,27 +8,22 @@ import (
) )
// GetDeliverymanQueueInfo récupère les infos de queue d'un livreur // GetDeliverymanQueueInfo récupère les infos de queue d'un livreur
func (d *Database) GetDeliverymanQueueInfo(deliveryman string) (map[string]interface{}, error) { func (d *Database) GetDeliverymanQueueInfo(deliveryman string) (map[string]any, error) {
queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman) queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman)
// Nombre de commandes dans la queue
queueSize, _ := Redis.ZCard(RedisCtx, queueKey).Result() queueSize, _ := Redis.ZCard(RedisCtx, queueKey).Result()
// Récupérer toutes les commandes
commandIDs, _ := Redis.ZRange(RedisCtx, queueKey, 0, -1).Result() commandIDs, _ := Redis.ZRange(RedisCtx, queueKey, 0, -1).Result()
// Vérifier le nombre de livreurs actifs pour déterminer la limite
activeCount, _ := d.CountActiveDeliverymen() activeCount, _ := d.CountActiveDeliverymen()
// Récupérer les détails de chaque commande var commands []map[string]any
var commands []map[string]interface{}
for i, cmdIDStr := range commandIDs { for i, cmdIDStr := range commandIDs {
commandID := extractCommandID(cmdIDStr) commandID := extractCommandID(cmdIDStr)
if commandID <= 0 { if commandID <= 0 {
continue continue
} }
// Récupérer les données de la commande
commandKey := fmt.Sprintf("queue:pending:%d", commandID) commandKey := fmt.Sprintf("queue:pending:%d", commandID)
data, err := Redis.Get(RedisCtx, commandKey).Result() data, err := Redis.Get(RedisCtx, commandKey).Result()
if err != nil { if err != nil {
@@ -40,7 +35,7 @@ func (d *Database) GetDeliverymanQueueInfo(deliveryman string) (map[string]inter
continue continue
} }
commands = append(commands, map[string]interface{}{ commands = append(commands, map[string]any{
"position": i + 1, "position": i + 1,
"command_id": commandID, "command_id": commandID,
"address": queueItem.Address, "address": queueItem.Address,
@@ -51,15 +46,12 @@ func (d *Database) GetDeliverymanQueueInfo(deliveryman string) (map[string]inter
}) })
} }
// Déterminer si le livreur peut accepter plus de commandes
canAcceptMore := true canAcceptMore := true
if activeCount > 1 { if activeCount > 1 {
// Plusieurs livreurs: limite de 10
canAcceptMore = queueSize < MAX_COMMANDS_PER_DELIVERYMAN canAcceptMore = queueSize < MAX_COMMANDS_PER_DELIVERYMAN
} }
// Si un seul livreur: pas de limite (canAcceptMore reste true)
return map[string]interface{}{ return map[string]any{
"deliveryman": deliveryman, "deliveryman": deliveryman,
"queue_size": queueSize, "queue_size": queueSize,
"commands": commands, "commands": commands,
@@ -69,15 +61,13 @@ func (d *Database) GetDeliverymanQueueInfo(deliveryman string) (map[string]inter
}, nil }, nil
} }
func (d *Database) GetAllQueuesOverview() (map[string]interface{}, error) { func (d *Database) GetAllQueuesOverview() (map[string]any, error) {
overview := make(map[string]interface{}) overview := make(map[string]any)
// Queue générale
generalQueueSize, _ := Redis.ZCard(RedisCtx, "queue:pending:sorted").Result() generalQueueSize, _ := Redis.ZCard(RedisCtx, "queue:pending:sorted").Result()
overview["general_queue"] = generalQueueSize overview["general_queue"] = generalQueueSize
// Queues par livreur avec détails deliverymanQueues := make(map[string]any)
deliverymanQueues := make(map[string]interface{})
keys, _ := Redis.Keys(RedisCtx, "queue:deliveryman:*").Result() keys, _ := Redis.Keys(RedisCtx, "queue:deliveryman:*").Result()
for _, key := range keys { for _, key := range keys {
@@ -88,7 +78,7 @@ func (d *Database) GetAllQueuesOverview() (map[string]interface{}, error) {
username := key[len("queue:deliveryman:"):] username := key[len("queue:deliveryman:"):]
queueSize, _ := Redis.ZCard(RedisCtx, key).Result() queueSize, _ := Redis.ZCard(RedisCtx, key).Result()
deliverymanQueues[username] = map[string]interface{}{ deliverymanQueues[username] = map[string]any{
"queue_size": queueSize, "queue_size": queueSize,
"can_accept_more": queueSize < MAX_COMMANDS_PER_DELIVERYMAN, "can_accept_more": queueSize < MAX_COMMANDS_PER_DELIVERYMAN,
"capacity": fmt.Sprintf("%d/%d", queueSize, MAX_COMMANDS_PER_DELIVERYMAN), "capacity": fmt.Sprintf("%d/%d", queueSize, MAX_COMMANDS_PER_DELIVERYMAN),
@@ -96,8 +86,6 @@ func (d *Database) GetAllQueuesOverview() (map[string]interface{}, error) {
} }
overview["deliveryman_queues"] = deliverymanQueues overview["deliveryman_queues"] = deliverymanQueues
// Total
var totalPending int64 = generalQueueSize var totalPending int64 = generalQueueSize
for _, key := range keys { for _, key := range keys {
if len(key) > 6 && key[len(key)-6:] == ":count" { if len(key) > 6 && key[len(key)-6:] == ":count" {
@@ -112,11 +100,10 @@ func (d *Database) GetAllQueuesOverview() (map[string]interface{}, error) {
} }
// GetQueueStats - Statistiques détaillées // GetQueueStats - Statistiques détaillées
func (d *Database) GetQueueStats() (map[string]interface{}, error) { func (d *Database) GetQueueStats() (map[string]any, error) {
normalCount, _ := Redis.ZCard(RedisCtx, "queue:pending:sorted").Result() normalCount, _ := Redis.ZCard(RedisCtx, "queue:pending:sorted").Result()
priorityCount, _ := Redis.ZCard(RedisCtx, "queue:priority:sorted").Result() priorityCount, _ := Redis.ZCard(RedisCtx, "queue:priority:sorted").Result()
// Compter les commandes dans les queues des livreurs
var deliverymanQueueCount int64 var deliverymanQueueCount int64
keys, _ := Redis.Keys(RedisCtx, "queue:deliveryman:*").Result() keys, _ := Redis.Keys(RedisCtx, "queue:deliveryman:*").Result()
for _, key := range keys { for _, key := range keys {
@@ -127,7 +114,6 @@ func (d *Database) GetQueueStats() (map[string]interface{}, error) {
deliverymanQueueCount += count deliverymanQueueCount += count
} }
// Calculer temps d'attente moyen
var totalWaitTime int64 var totalWaitTime int64
var commandCount int64 var commandCount int64
@@ -154,7 +140,7 @@ func (d *Database) GetQueueStats() (map[string]interface{}, error) {
avgWaitTime = int(totalWaitTime / commandCount) avgWaitTime = int(totalWaitTime / commandCount)
} }
stats := map[string]interface{}{ stats := map[string]any{
"total_pending": normalCount + priorityCount, "total_pending": normalCount + priorityCount,
"general_queue": normalCount, "general_queue": normalCount,
"priority_queue": priorityCount, "priority_queue": priorityCount,
+79 -218
View File
@@ -16,8 +16,6 @@ import (
// ============================================ // ============================================
// UpdateDeliverymanStatusBasedOnQueue met à jour automatiquement le statut // UpdateDeliverymanStatusBasedOnQueue met à jour automatiquement le statut
// ✅ Status = "busy" si queue >= 10
// ✅ Status = "available" si queue < 10
func (d *Database) UpdateDeliverymanStatusBasedOnQueue(deliveryman string) error { func (d *Database) UpdateDeliverymanStatusBasedOnQueue(deliveryman string) error {
queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman) queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman)
queueSize, err := Redis.ZCard(RedisCtx, queueKey).Result() queueSize, err := Redis.ZCard(RedisCtx, queueKey).Result()
@@ -25,7 +23,6 @@ func (d *Database) UpdateDeliverymanStatusBasedOnQueue(deliveryman string) error
return fmt.Errorf("erreur récupération taille queue: %w", err) return fmt.Errorf("erreur récupération taille queue: %w", err)
} }
// Récupérer le statut actuel
statusKey := fmt.Sprintf("delivery:status:%s", deliveryman) statusKey := fmt.Sprintf("delivery:status:%s", deliveryman)
data, err := Redis.Get(RedisCtx, statusKey).Result() data, err := Redis.Get(RedisCtx, statusKey).Result()
if err != nil { if err != nil {
@@ -38,18 +35,14 @@ func (d *Database) UpdateDeliverymanStatusBasedOnQueue(deliveryman string) error
return err return err
} }
// Déterminer le nouveau statut
var newStatus string var newStatus string
var currentCommand int var currentCommand int
if queueSize >= MAX_COMMANDS_PER_DELIVERYMAN { if queueSize >= MAX_COMMANDS_PER_DELIVERYMAN {
// 🔴 BUSY car queue pleine (10 commandes ou plus)
newStatus = "busy" newStatus = "busy"
currentCommand = 0 currentCommand = 0
log.Printf("🔴 [STATUS] %s -> BUSY (queue pleine: %d/10)", deliveryman, queueSize) log.Printf("🔴 [STATUS] %s -> BUSY (queue pleine: %d/10)", deliveryman, queueSize)
} else { } else {
// 🟢 AVAILABLE tant que queue < 10
// Exception: si le livreur est en train de livrer (delivering), on garde ce statut
if status.Status == "delivering" && status.CurrentCommand > 0 { if status.Status == "delivering" && status.CurrentCommand > 0 {
newStatus = "delivering" newStatus = "delivering"
currentCommand = status.CurrentCommand currentCommand = status.CurrentCommand
@@ -62,19 +55,16 @@ func (d *Database) UpdateDeliverymanStatusBasedOnQueue(deliveryman string) error
} }
} }
// Mettre à jour le statut
return d.SetDeliveryPersonStatus(deliveryman, newStatus, currentCommand) return d.SetDeliveryPersonStatus(deliveryman, newStatus, currentCommand)
} }
// CanDeliverymanAcceptCommands vérifie si un livreur peut accepter de nouvelles commandes // CanDeliverymanAcceptCommands vérifie si un livreur peut accepter de nouvelles commandes
// ✅ Retourne false si: status=busy ET queue>=10, ou status=offline
func (d *Database) CanDeliverymanAcceptCommands(deliveryman string) bool { func (d *Database) CanDeliverymanAcceptCommands(deliveryman string) bool {
// 1. Vérifier le statut Redis
statusKey := fmt.Sprintf("delivery:status:%s", deliveryman) statusKey := fmt.Sprintf("delivery:status:%s", deliveryman)
data, err := Redis.Get(RedisCtx, statusKey).Result() data, err := Redis.Get(RedisCtx, statusKey).Result()
if err != nil { if err != nil {
log.Printf("⚠️ [CHECK] Livreur %s sans statut Redis", deliveryman) log.Printf("⚠️ [CHECK] Livreur %s sans statut Redis", deliveryman)
return true // Fallback: autoriser si pas de statut return true
} }
var status models.DeliveryPersonStatus var status models.DeliveryPersonStatus
@@ -82,9 +72,7 @@ func (d *Database) CanDeliverymanAcceptCommands(deliveryman string) bool {
return true return true
} }
// 2. Si offline, refuser
if status.Status == "offline" { if status.Status == "offline" {
log.Printf("⚫ [CHECK] %s REFUSÉ: offline", deliveryman)
return false return false
} }
@@ -122,7 +110,6 @@ func (d *Database) GetAvailableDeliveryPersonsForAssignment() ([]models.Delivery
continue continue
} }
// ✅ Vérifier si le livreur peut accepter des commandes
if d.CanDeliverymanAcceptCommands(status.Username) { if d.CanDeliverymanAcceptCommands(status.Username) {
available = append(available, status) available = append(available, status)
} }
@@ -144,11 +131,9 @@ func (d *Database) AddToDeliverymanQueue(deliveryman string, queueItem models.Co
return fmt.Errorf("erreur serialisation: %w", err) return fmt.Errorf("erreur serialisation: %w", err)
} }
// Sauvegarder les données de la commande
commandKey := fmt.Sprintf("queue:pending:%d", queueItem.CommandID) commandKey := fmt.Sprintf("queue:pending:%d", queueItem.CommandID)
Redis.Set(RedisCtx, commandKey, data, 24*time.Hour) Redis.Set(RedisCtx, commandKey, data, 24*time.Hour)
// Ajouter à la queue sorted set du livreur (score = timestamp)
queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman) queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman)
score := float64(time.Now().Unix()) score := float64(time.Now().Unix())
@@ -161,13 +146,9 @@ func (d *Database) AddToDeliverymanQueue(deliveryman string, queueItem models.Co
return fmt.Errorf("erreur ajout queue Redis: %w", err) return fmt.Errorf("erreur ajout queue Redis: %w", err)
} }
// Incrémenter le compteur de commandes en attente pour ce livreur
counterKey := fmt.Sprintf("queue:deliveryman:%s:count", deliveryman) counterKey := fmt.Sprintf("queue:deliveryman:%s:count", deliveryman)
Redis.Incr(RedisCtx, counterKey) Redis.Incr(RedisCtx, counterKey)
log.Printf("✅ Commande %d ajoutée à la queue de %s", queueItem.CommandID, deliveryman)
// ✅ NOUVEAU: Mettre à jour automatiquement le statut
go d.UpdateDeliverymanStatusBasedOnQueue(deliveryman) go d.UpdateDeliverymanStatusBasedOnQueue(deliveryman)
return nil return nil
@@ -184,8 +165,6 @@ func (d *Database) AddCommandToQueue(commandID int) error {
if err != nil { if err != nil {
return fmt.Errorf("commande introuvable: %w", err) return fmt.Errorf("commande introuvable: %w", err)
} }
// Gestion des coordonnées (NULL safe)
var lat, lng float64 var lat, lng float64
if command["dest_latitude"] != nil { if command["dest_latitude"] != nil {
if latVal, ok := command["dest_latitude"].(float64); ok { if latVal, ok := command["dest_latitude"].(float64); ok {
@@ -198,13 +177,11 @@ func (d *Database) AddCommandToQueue(commandID int) error {
} }
} }
// Récupérer le total_prix avec gestion de type
var totalPrice float64 var totalPrice float64
if tp, ok := command["total_prix"].(float64); ok { if tp, ok := command["total_prix"].(float64); ok {
totalPrice = tp totalPrice = tp
} }
// Récupérer l'adresse
var address string var address string
if addr, ok := command["delivery_address"].(string); ok { if addr, ok := command["delivery_address"].(string); ok {
address = addr address = addr
@@ -221,7 +198,6 @@ func (d *Database) AddCommandToQueue(commandID int) error {
EstimatedETA: 0, EstimatedETA: 0,
} }
// Ajouter à la queue générale
return d.AddToGeneralQueue(queueItem) return d.AddToGeneralQueue(queueItem)
} }
@@ -235,8 +211,6 @@ func (d *Database) AddCommandToSmartQueue(commandID int, address string) error {
if err != nil { if err != nil {
return fmt.Errorf("commande introuvable: %w", err) return fmt.Errorf("commande introuvable: %w", err)
} }
// Gestion des coordonnées (NULL safe)
var lat, lng float64 var lat, lng float64
if command["dest_latitude"] != nil { if command["dest_latitude"] != nil {
if latVal, ok := command["dest_latitude"].(float64); ok { if latVal, ok := command["dest_latitude"].(float64); ok {
@@ -249,7 +223,6 @@ func (d *Database) AddCommandToSmartQueue(commandID int, address string) error {
} }
} }
// Récupérer le total_prix avec gestion de type
var totalPrice float64 var totalPrice float64
if tp, ok := command["total_prix"].(float64); ok { if tp, ok := command["total_prix"].(float64); ok {
totalPrice = tp totalPrice = tp
@@ -269,12 +242,10 @@ func (d *Database) AddCommandToSmartQueue(commandID int, address string) error {
// ✅ MODIFIÉ: Utiliser FindLeastLoadedDeliveryman qui respecte maintenant le statut // ✅ MODIFIÉ: Utiliser FindLeastLoadedDeliveryman qui respecte maintenant le statut
assignedDeliveryman, err := d.FindLeastLoadedDeliveryman() assignedDeliveryman, err := d.FindLeastLoadedDeliveryman()
if err != nil { if err != nil {
// Aucun livreur trouvé, ajouter à la queue générale
log.Printf("⚠️ Aucun livreur trouvé, ajout à la queue générale") log.Printf("⚠️ Aucun livreur trouvé, ajout à la queue générale")
return d.AddToGeneralQueue(queueItem) return d.AddToGeneralQueue(queueItem)
} }
// Ajouter la commande à la queue spécifique du livreur (auto-update du statut)
err = d.AddToDeliverymanQueue(assignedDeliveryman, queueItem) err = d.AddToDeliverymanQueue(assignedDeliveryman, queueItem)
if err != nil { if err != nil {
return fmt.Errorf("erreur ajout à la queue du livreur: %w", err) return fmt.Errorf("erreur ajout à la queue du livreur: %w", err)
@@ -282,7 +253,6 @@ func (d *Database) AddCommandToSmartQueue(commandID int, address string) error {
log.Printf("📋 Commande %d assignée à la queue de %s", commandID, assignedDeliveryman) log.Printf("📋 Commande %d assignée à la queue de %s", commandID, assignedDeliveryman)
// Publier l'événement
d.PublishCommandEvent(commandID, "queued", d.PublishCommandEvent(commandID, "queued",
fmt.Sprintf("En attente dans la queue de %s", assignedDeliveryman)) fmt.Sprintf("En attente dans la queue de %s", assignedDeliveryman))
@@ -459,7 +429,6 @@ func (d *Database) ClearDeliverymanQueue(deliveryman string) error {
continue continue
} }
// Récupérer les données de la commande
commandKey := fmt.Sprintf("queue:pending:%d", commandID) commandKey := fmt.Sprintf("queue:pending:%d", commandID)
data, err := Redis.Get(RedisCtx, commandKey).Result() data, err := Redis.Get(RedisCtx, commandKey).Result()
if err != nil { if err != nil {
@@ -471,15 +440,12 @@ func (d *Database) ClearDeliverymanQueue(deliveryman string) error {
continue continue
} }
// Trouver un nouveau livreur
newDeliveryman, err := d.FindLeastLoadedDeliveryman() newDeliveryman, err := d.FindLeastLoadedDeliveryman()
if err != nil { if err != nil {
// Fallback: queue générale
d.AddToGeneralQueue(queueItem) d.AddToGeneralQueue(queueItem)
continue continue
} }
// Réassigner à un autre livreur
if newDeliveryman != deliveryman { if newDeliveryman != deliveryman {
d.AddToDeliverymanQueue(newDeliveryman, queueItem) d.AddToDeliverymanQueue(newDeliveryman, queueItem)
log.Printf("🔄 Commande %d réassignée de %s à %s", log.Printf("🔄 Commande %d réassignée de %s à %s",
@@ -491,9 +457,6 @@ func (d *Database) ClearDeliverymanQueue(deliveryman string) error {
Redis.Del(RedisCtx, queueKey) Redis.Del(RedisCtx, queueKey)
Redis.Del(RedisCtx, fmt.Sprintf("queue:deliveryman:%s:count", deliveryman)) Redis.Del(RedisCtx, fmt.Sprintf("queue:deliveryman:%s:count", deliveryman))
log.Printf("🗑️ Queue de %s vidée et redistribuée", deliveryman)
// ✅ NOUVEAU: Mettre à jour le statut
go d.UpdateDeliverymanStatusBasedOnQueue(deliveryman) go d.UpdateDeliverymanStatusBasedOnQueue(deliveryman)
return nil return nil
@@ -521,227 +484,125 @@ func (d *Database) SetDeliveryPersonStatus(username, status string, commandID in
// GetAvailableDeliveryPersonsRedis récupère les livreurs disponibles (LEGACY) // GetAvailableDeliveryPersonsRedis récupère les livreurs disponibles (LEGACY)
func (d *Database) GetAvailableDeliveryPersonsRedis() ([]models.DeliveryPersonStatus, error) { func (d *Database) GetAvailableDeliveryPersonsRedis() ([]models.DeliveryPersonStatus, error) {
keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result()
if err != nil {
return nil, err
}
var available []models.DeliveryPersonStatus var available []models.DeliveryPersonStatus
err := d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) {
for _, key := range keys { if s.Status == "available" {
data, err := Redis.Get(RedisCtx, key).Result() available = append(available, s)
if err != nil {
continue
} }
})
var status models.DeliveryPersonStatus return available, err
json.Unmarshal([]byte(data), &status)
if status.Status == "available" {
available = append(available, status)
}
}
return available, nil
} }
// GetAllActiveDeliveryPersons - VERSION MISE À JOUR avec vérification capacité // GetAllActiveDeliveryPersons retourne les livreurs actifs pouvant accepter des commandes
func (d *Database) GetAllActiveDeliveryPersons() ([]models.DeliveryPersonStatus, error) { func (d *Database) GetAllActiveDeliveryPersons() ([]models.DeliveryPersonStatus, error) {
keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result()
if err != nil {
return nil, err
}
var active []models.DeliveryPersonStatus var active []models.DeliveryPersonStatus
err := d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) {
for _, key := range keys { if s.Status != "offline" && d.CanDeliverymanAcceptCommands(s.Username) {
data, err := Redis.Get(RedisCtx, key).Result() active = append(active, s)
if err != nil {
continue
} }
})
var status models.DeliveryPersonStatus return active, err
json.Unmarshal([]byte(data), &status)
if status.Status != "offline" {
// ✅ Vérifier si le livreur peut accepter des commandes
if d.CanDeliverymanAcceptCommands(status.Username) {
active = append(active, status)
}
}
}
return active, nil
} }
// CountActiveDeliverymen compte le nombre de livreurs actifs (non offline) // CountActiveDeliverymen compte le nombre de livreurs actifs (non offline)
func (d *Database) CountActiveDeliverymen() (int, error) { func (d *Database) CountActiveDeliverymen() (int, error) {
keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result()
if err != nil {
return 0, err
}
count := 0 count := 0
for _, key := range keys { err := d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) {
data, err := Redis.Get(RedisCtx, key).Result() if s.Status != "offline" {
if err != nil {
continue
}
var status models.DeliveryPersonStatus
json.Unmarshal([]byte(data), &status)
if status.Status != "offline" {
count++ count++
} }
} })
return count, err
return count, nil
} }
func (d *Database) GetSingleActiveDeliveryman() (string, error) { func (d *Database) GetSingleActiveDeliveryman() (string, error) {
keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result() found := ""
if err != nil { d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) { //nolint
return "", err if found == "" && s.Status != "offline" {
found = s.Username
} }
})
for _, key := range keys { if found == "" {
data, err := Redis.Get(RedisCtx, key).Result()
if err != nil {
continue
}
var status models.DeliveryPersonStatus
json.Unmarshal([]byte(data), &status)
if status.Status != "offline" {
return status.Username, nil
}
}
return "", fmt.Errorf("aucun livreur actif trouvé") return "", fmt.Errorf("aucun livreur actif trouvé")
}
return found, nil
} }
// GetAllActiveDeliverymenUsernames retourne les usernames de tous les livreurs actifs // GetAllActiveDeliverymenUsernames retourne les usernames de tous les livreurs actifs
func (d *Database) GetAllActiveDeliverymenUsernames() ([]string, error) { func (d *Database) GetAllActiveDeliverymenUsernames() ([]string, error) {
keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result() var usernames []string
if err != nil { err := d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) {
return nil, err if s.Status != "offline" {
usernames = append(usernames, s.Username)
} }
})
var activeUsernames []string return usernames, err
for _, key := range keys {
data, err := Redis.Get(RedisCtx, key).Result()
if err != nil {
continue
}
var status models.DeliveryPersonStatus
json.Unmarshal([]byte(data), &status)
if status.Status != "offline" {
activeUsernames = append(activeUsernames, status.Username)
}
}
return activeUsernames, nil
} }
// ============================================
// 🔄 SYNCHRONISATION DES STATUTS
// ============================================
// SyncAllDeliverymanStatuses synchronise tous les statuts (à appeler au démarrage) // SyncAllDeliverymanStatuses synchronise tous les statuts (à appeler au démarrage)
func (d *Database) SyncAllDeliverymanStatuses() error { func (d *Database) SyncAllDeliverymanStatuses() error {
log.Println("🔄 [SYNC] Synchronisation des statuts livreurs...")
return d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) {
d.UpdateDeliverymanStatusBasedOnQueue(s.Username)
})
}
// GetDeliverymanCapacityReport génère un rapport détaillé
func (d *Database) GetDeliverymanCapacityReport() (map[string]any, error) {
report := map[string]any{
"total_deliverymen": 0,
"available": 0,
"busy_full": 0,
"busy_delivering": 0,
"offline": 0,
"details": []map[string]any{},
}
err := d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) {
queueKey := fmt.Sprintf("queue:deliveryman:%s", s.Username)
queueSize, _ := Redis.ZCard(RedisCtx, queueKey).Result()
canAccept := d.CanDeliverymanAcceptCommands(s.Username)
report["total_deliverymen"] = report["total_deliverymen"].(int) + 1
switch {
case s.Status == "offline":
report["offline"] = report["offline"].(int) + 1
case s.Status == "busy" && queueSize >= MAX_COMMANDS_PER_DELIVERYMAN:
report["busy_full"] = report["busy_full"].(int) + 1
case s.Status == "busy":
report["busy_delivering"] = report["busy_delivering"].(int) + 1
case canAccept:
report["available"] = report["available"].(int) + 1
}
report["details"] = append(report["details"].([]map[string]any), map[string]any{
"username": s.Username,
"status": s.Status,
"queue_size": queueSize,
"capacity": fmt.Sprintf("%d/10", queueSize),
"can_accept": canAccept,
"current_order": s.CurrentCommand,
})
})
return report, err
}
// iterDeliveryStatuses itère sur tous les statuts Redis des livreurs et appelle fn pour chacun.
func (d *Database) iterDeliveryStatuses(fn func(models.DeliveryPersonStatus)) error {
keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result() keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result()
if err != nil { if err != nil {
return err return err
} }
log.Println("🔄 [SYNC] Synchronisation des statuts livreurs...")
for _, key := range keys { for _, key := range keys {
data, err := Redis.Get(RedisCtx, key).Result() data, err := Redis.Get(RedisCtx, key).Result()
if err != nil { if err != nil {
continue continue
} }
var status models.DeliveryPersonStatus var status models.DeliveryPersonStatus
if err := json.Unmarshal([]byte(data), &status); err != nil { if err := json.Unmarshal([]byte(data), &status); err != nil {
continue continue
} }
fn(status)
// Mettre à jour le statut basé sur la queue
d.UpdateDeliverymanStatusBasedOnQueue(status.Username)
} }
log.Println("✅ [SYNC] Synchronisation terminée")
return nil return nil
} }
// ============================================
// 📊 RAPPORT DE CAPACITÉ
// ============================================
// GetDeliverymanCapacityReport génère un rapport détaillé
func (d *Database) GetDeliverymanCapacityReport() (map[string]interface{}, error) {
keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result()
if err != nil {
return nil, err
}
report := map[string]interface{}{
"total_deliverymen": 0,
"available": 0,
"busy_full": 0, // BUSY car queue pleine
"busy_delivering": 0, // BUSY car en livraison
"offline": 0,
"details": []map[string]interface{}{},
}
for _, key := range keys {
data, err := Redis.Get(RedisCtx, key).Result()
if err != nil {
continue
}
var status models.DeliveryPersonStatus
if err := json.Unmarshal([]byte(data), &status); err != nil {
continue
}
queueKey := fmt.Sprintf("queue:deliveryman:%s", status.Username)
queueSize, _ := Redis.ZCard(RedisCtx, queueKey).Result()
canAccept := d.CanDeliverymanAcceptCommands(status.Username)
detail := map[string]interface{}{
"username": status.Username,
"status": status.Status,
"queue_size": queueSize,
"capacity": fmt.Sprintf("%d/10", queueSize),
"can_accept": canAccept,
"current_order": status.CurrentCommand,
}
report["total_deliverymen"] = report["total_deliverymen"].(int) + 1
if status.Status == "offline" {
report["offline"] = report["offline"].(int) + 1
} else if status.Status == "busy" {
if queueSize >= MAX_COMMANDS_PER_DELIVERYMAN {
report["busy_full"] = report["busy_full"].(int) + 1
} else {
report["busy_delivering"] = report["busy_delivering"].(int) + 1
}
} else if canAccept {
report["available"] = report["available"].(int) + 1
}
report["details"] = append(report["details"].([]map[string]interface{}), detail)
}
return report, nil
}
-1
View File
@@ -78,7 +78,6 @@ func (d *Database) CreateClientSession(clientID int, username string, sessionID
// Charger les infos du client (points, penalty) dans le cache // Charger les infos du client (points, penalty) dans le cache
if client, err := d.GetClientByUsername(username); err == nil { if client, err := d.GetClientByUsername(username); err == nil {
sessionData.PointsCache = int(client.Point)
sessionData.PenaltyCache = float64(client.Amende) sessionData.PenaltyCache = float64(client.Amende)
sessionJSON, _ := json.Marshal(sessionData) sessionJSON, _ := json.Marshal(sessionData)
Redis.Set(RedisCtx, sessionKey, sessionJSON, ttl) Redis.Set(RedisCtx, sessionKey, sessionJSON, ttl)
+25 -166
View File
@@ -1,14 +1,12 @@
package handlers package handlers
import ( import (
"crypto/rand"
"encoding/hex"
"gestion/db" "gestion/db"
"gestion/models" "gestion/models"
"gestion/utils"
"log" "log"
"net/http" "net/http"
"os" "os"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -19,109 +17,16 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
// ============================================
// TYPES JWT CLAIMS (Duplicés dans middleware aussi)
// ============================================
type ClientClaims struct {
ClientID int `json:"client_id"`
Username string `json:"username"`
Role string `json:"role"`
SessionID string `json:"session_id"`
jwt.RegisteredClaims
}
type AdminClaims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
SessionID string `json:"session_id"`
jwt.RegisteredClaims
}
// ============================================
// STRUCTURES REQUÊTE / RÉPONSE
// ============================================
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type RegisterClientRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Password string `json:"password" binding:"required,min=8"`
Nom string `json:"nom" binding:"required,min=2,max=100"`
Prenom string `json:"prenom" binding:"required,min=2,max=100"`
Telephone string `json:"telephone" binding:"required"`
}
type RegisterAdminRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Password string `json:"password" binding:"required,min=8"`
Role string `json:"role" binding:"required,oneof=admin cabine livreur"`
}
type LoginResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
User interface{} `json:"user"`
}
type ProfileResponse struct {
Username string `json:"username"`
Role string `json:"role"`
}
// ============================================
// VARIABLES & CONSTANTS
// ============================================
var ( var (
clientTokenDuration = 5 * time.Hour clientTokenDuration = 5 * time.Hour
adminTokenDuration = 10 * time.Hour adminTokenDuration = 10 * time.Hour
userJWTSecret = []byte(os.Getenv("USER_JWT_SECRET")) // ✅ Pour clients userJWTSecret = []byte(os.Getenv("USER_JWT_SECRET"))
adminJWTSecret = []byte(os.Getenv("ADMIN_JWT_SECRET")) // ✅ Pour admin/cabine/livreur adminJWTSecret = []byte(os.Getenv("ADMIN_JWT_SECRET"))
) )
// ============================================
// UTILITAIRES
// ============================================
func generateSessionID() string {
bytes := make([]byte, 16)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
// validatePhoneNumber valide le format du numéro de téléphone
func validatePhoneNumber(phone string) bool {
// Supprimer espaces/tirets
clean := regexp.MustCompile(`[\s\-\(\)]`).ReplaceAllString(phone, "")
// Format international ou national
validFormat := regexp.MustCompile(`^(\+33|0)[1-9]\d{8}$`)
return validFormat.MatchString(clean)
}
func normalizePhoneNumber(phone string) string {
clean := regexp.MustCompile(`[\s\-\(\)]`).ReplaceAllString(phone, "")
// Convertir 06... en +336...
if strings.HasPrefix(clean, "0") {
return "+33" + clean[1:]
}
return clean
}
// ============================================
// GENERATION TOKENS
// ============================================
func generateClientToken(client *models.Client) (string, error) { func generateClientToken(client *models.Client) (string, error) {
sessionID := generateSessionID() sessionID := utils.GenerateSessionID()
claims := ClientClaims{ claims := models.ClientClaims{
ClientID: client.ID, ClientID: client.ID,
Username: client.Username, Username: client.Username,
Role: "client", Role: "client",
@@ -135,7 +40,7 @@ func generateClientToken(client *models.Client) (string, error) {
}, },
} }
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(userJWTSecret) // ✅ UTILISER userJWTSecret (CLIENT) tokenString, err := token.SignedString(userJWTSecret)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -143,8 +48,8 @@ func generateClientToken(client *models.Client) (string, error) {
} }
func generateAdminToken(user *models.User) (string, error) { func generateAdminToken(user *models.User) (string, error) {
sessionID := generateSessionID() sessionID := utils.GenerateSessionID()
claims := AdminClaims{ claims := models.AdminClaims{
UserID: user.ID, UserID: user.ID,
Username: user.Username, Username: user.Username,
Role: user.Role, // ← "admin" ou "cabine" ou "livreur" Role: user.Role, // ← "admin" ou "cabine" ou "livreur"
@@ -158,21 +63,16 @@ func generateAdminToken(user *models.User) (string, error) {
}, },
} }
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(adminJWTSecret) // ✅ UTILISER adminJWTSecret (ADMIN/CABINE/LIVREUR) tokenString, err := token.SignedString(adminJWTSecret)
if err != nil { if err != nil {
return "", err return "", err
} }
return tokenString, nil return tokenString, nil
} }
// ============================================
// HANDLERS AUTHENTIFICATION CLIENT
// ============================================
// RegisterClient crée un nouveau compte client // RegisterClient crée un nouveau compte client
// POST /api/v1/auth/register
func RegisterClient(c *gin.Context) { func RegisterClient(c *gin.Context) {
var req RegisterClientRequest var req models.RegisterClientRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("❌ [REGISTER_CLIENT] Erreur binding: %v", err) log.Printf("❌ [REGISTER_CLIENT] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
@@ -183,7 +83,7 @@ func RegisterClient(c *gin.Context) {
} }
// Validation téléphone // Validation téléphone
if !validatePhoneNumber(req.Telephone) { if !utils.ValidatePhoneNumber(req.Telephone) {
log.Printf("❌ [REGISTER_CLIENT] Téléphone invalide: %s", req.Telephone) log.Printf("❌ [REGISTER_CLIENT] Téléphone invalide: %s", req.Telephone)
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": "Numéro de téléphone invalide", "error": "Numéro de téléphone invalide",
@@ -191,7 +91,7 @@ func RegisterClient(c *gin.Context) {
return return
} }
normalizedPhone := normalizePhoneNumber(req.Telephone) normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
// Vérifier username unique // Vérifier username unique
@@ -256,9 +156,7 @@ func RegisterClient(c *gin.Context) {
client.Password = "" client.Password = ""
log.Printf("✅ [REGISTER_CLIENT] Client créé: %s (ID=%d)", client.Username, client.ID) c.JSON(http.StatusCreated, models.LoginResponse{
c.JSON(http.StatusCreated, LoginResponse{
AccessToken: token, AccessToken: token,
TokenType: "Bearer", TokenType: "Bearer",
ExpiresIn: int(clientTokenDuration.Seconds()), ExpiresIn: int(clientTokenDuration.Seconds()),
@@ -275,20 +173,19 @@ func RegisterClient(c *gin.Context) {
} }
// AdminCreateClient crée un client depuis l'interface admin (sans session ni token) // AdminCreateClient crée un client depuis l'interface admin (sans session ni token)
// POST /api/v2/admin/protected/clients
func AdminCreateClient(c *gin.Context) { func AdminCreateClient(c *gin.Context) {
var req RegisterClientRequest var req models.RegisterClientRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
return return
} }
if !validatePhoneNumber(req.Telephone) { if !utils.ValidatePhoneNumber(req.Telephone) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
return return
} }
normalizedPhone := normalizePhoneNumber(req.Telephone) normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
if existing, _ := database.GetClientByUsername(req.Username); existing != nil { if existing, _ := database.GetClientByUsername(req.Username); existing != nil {
@@ -322,8 +219,6 @@ func AdminCreateClient(c *gin.Context) {
return return
} }
log.Printf("✅ [ADMIN_CREATE_CLIENT] Client créé par admin: %s (ID=%d)", client.Username, client.ID)
c.JSON(http.StatusCreated, gin.H{ c.JSON(http.StatusCreated, gin.H{
"message": "Client créé avec succès", "message": "Client créé avec succès",
"client": gin.H{ "client": gin.H{
@@ -337,9 +232,8 @@ func AdminCreateClient(c *gin.Context) {
} }
// LoginClient authentifie un client // LoginClient authentifie un client
// POST /api/v1/auth/login
func LoginClient(c *gin.Context) { func LoginClient(c *gin.Context) {
var req LoginRequest var req models.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("❌ [LOGIN_CLIENT] Erreur binding: %v", err) log.Printf("❌ [LOGIN_CLIENT] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
@@ -361,7 +255,6 @@ func LoginClient(c *gin.Context) {
return return
} }
// ✅ LIGNE 302 CORRIGÉE - Gérer l'erreur !
token, err := generateClientToken(client) token, err := generateClientToken(client)
if err != nil { if err != nil {
log.Printf("❌ [LOGIN_CLIENT] Erreur génération token: %v", err) log.Printf("❌ [LOGIN_CLIENT] Erreur génération token: %v", err)
@@ -382,9 +275,7 @@ func LoginClient(c *gin.Context) {
log.Printf("⚠️ [LOGIN_CLIENT] Erreur session Redis: %v", err) log.Printf("⚠️ [LOGIN_CLIENT] Erreur session Redis: %v", err)
} }
log.Printf("✅ [LOGIN_CLIENT] Client authentifié: %s", req.Username) c.JSON(http.StatusOK, models.LoginResponse{
c.JSON(http.StatusOK, LoginResponse{
AccessToken: token, AccessToken: token,
TokenType: "Bearer", TokenType: "Bearer",
ExpiresIn: int(clientTokenDuration.Seconds()), ExpiresIn: int(clientTokenDuration.Seconds()),
@@ -402,7 +293,6 @@ func LoginClient(c *gin.Context) {
} }
// ChangePassword permet à un client de changer son mot de passe // ChangePassword permet à un client de changer son mot de passe
// PUT /api/v1/auth/change-password
func ChangePassword(c *gin.Context) { func ChangePassword(c *gin.Context) {
var req struct { var req struct {
CurrentPassword string `json:"current_password" binding:"required"` CurrentPassword string `json:"current_password" binding:"required"`
@@ -442,16 +332,13 @@ func ChangePassword(c *gin.Context) {
return return
} }
log.Printf("✅ [CHANGE_PASSWORD] Mot de passe changé: ID=%d", clientID)
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Mot de passe mis à jour avec succès"}) c.JSON(http.StatusOK, gin.H{"success": true, "message": "Mot de passe mis à jour avec succès"})
} }
// LogoutClient déconnecte un client // LogoutClient déconnecte un client
// POST /api/v1/auth/logout
func LogoutClient(c *gin.Context) { func LogoutClient(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
// Invalider la session Redis
clientID, hasClientID := c.Get("client_id") clientID, hasClientID := c.Get("client_id")
if hasClientID && clientID != nil { if hasClientID && clientID != nil {
if err := database.InvalidateSession(clientID.(int)); err != nil { if err := database.InvalidateSession(clientID.(int)); err != nil {
@@ -459,26 +346,18 @@ func LogoutClient(c *gin.Context) {
} }
} }
// Révoquer le JWT token
authHeader := c.GetHeader("Authorization") authHeader := c.GetHeader("Authorization")
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") { if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
tokenStr := strings.TrimPrefix(authHeader, "Bearer ") tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
database.RevokeToken(tokenStr) database.RevokeToken(tokenStr)
} }
log.Printf("✅ [LOGOUT_CLIENT] Client déconnecté")
c.JSON(http.StatusOK, gin.H{"message": "Déconnexion réussie"}) c.JSON(http.StatusOK, gin.H{"message": "Déconnexion réussie"})
} }
// ============================================
// HANDLERS AUTHENTIFICATION ADMIN
// ============================================
// RegisterAdmin crée un nouvel utilisateur admin/cabine/livreur // RegisterAdmin crée un nouvel utilisateur admin/cabine/livreur
// POST /api/v1/auth/admin/register
func RegisterAdmin(c *gin.Context) { func RegisterAdmin(c *gin.Context) {
var req RegisterAdminRequest var req models.RegisterAdminRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("❌ [REGISTER_ADMIN] Erreur binding: %v", err) log.Printf("❌ [REGISTER_ADMIN] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
@@ -487,7 +366,6 @@ func RegisterAdmin(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
// Vérifier que l'user n'existe pas
if existingUser, _ := database.GetUserByUsername(req.Username); existingUser != nil { if existingUser, _ := database.GetUserByUsername(req.Username); existingUser != nil {
log.Printf("❌ [REGISTER_ADMIN] Username déjà utilisé: %s", req.Username) log.Printf("❌ [REGISTER_ADMIN] Username déjà utilisé: %s", req.Username)
c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"}) c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"})
@@ -507,9 +385,6 @@ func RegisterAdmin(c *gin.Context) {
return return
} }
log.Printf("✅ [REGISTER_ADMIN] User créé: %s (role=%s, ID=%d)", user.Username, user.Role, user.ID)
// Générer le token
token, err := generateAdminToken(user) token, err := generateAdminToken(user)
if err != nil { if err != nil {
log.Printf("❌ [REGISTER_ADMIN] Erreur génération token: %v", err) log.Printf("❌ [REGISTER_ADMIN] Erreur génération token: %v", err)
@@ -517,11 +392,6 @@ func RegisterAdmin(c *gin.Context) {
return return
} }
log.Printf("✅ [REGISTER_ADMIN] Token généré: %s...", token[:50])
// ============================================
// ✅ CRITICAL FIX: ENREGISTRER LE TOKEN EN DB
// ============================================
expiresAt := time.Now().Add(adminTokenDuration) expiresAt := time.Now().Add(adminTokenDuration)
if err := database.SaveToken(user.ID, user.Role, token, expiresAt); err != nil { if err := database.SaveToken(user.ID, user.Role, token, expiresAt); err != nil {
log.Printf("❌ [REGISTER_ADMIN] Erreur SaveToken: %v", err) log.Printf("❌ [REGISTER_ADMIN] Erreur SaveToken: %v", err)
@@ -529,11 +399,9 @@ func RegisterAdmin(c *gin.Context) {
return return
} }
log.Printf("✅ [REGISTER_ADMIN] Token enregistré en DB pour user ID: %d", user.ID)
user.Password = "" user.Password = ""
c.JSON(http.StatusCreated, LoginResponse{ c.JSON(http.StatusCreated, models.LoginResponse{
AccessToken: token, AccessToken: token,
TokenType: "Bearer", TokenType: "Bearer",
ExpiresIn: int(adminTokenDuration.Seconds()), ExpiresIn: int(adminTokenDuration.Seconds()),
@@ -542,10 +410,8 @@ func RegisterAdmin(c *gin.Context) {
} }
// LoginAdmin authentifie un admin/cabine/livreur // LoginAdmin authentifie un admin/cabine/livreur
// POST /api/v1/auth/admin/login
// ✅ AMÉLIORÉ: Meilleure gestion d'erreurs
func LoginAdmin(c *gin.Context) { func LoginAdmin(c *gin.Context) {
var req LoginRequest var req models.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("❌ [LOGIN_ADMIN] Erreur binding: %v", err) log.Printf("❌ [LOGIN_ADMIN] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
@@ -561,14 +427,12 @@ func LoginAdmin(c *gin.Context) {
return return
} }
// Vérifier que c'est un admin/cabine/livreur
if user.Role != "admin" && user.Role != "cabine" && user.Role != "livreur" { if user.Role != "admin" && user.Role != "cabine" && user.Role != "livreur" {
log.Printf("❌ [LOGIN_ADMIN] Rôle invalide: %s", user.Role) log.Printf("❌ [LOGIN_ADMIN] Rôle invalide: %s", user.Role)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Accès non autorisé"}) c.JSON(http.StatusUnauthorized, gin.H{"error": "Accès non autorisé"})
return return
} }
// Vérifier le mot de passe
if user.Password == "" { if user.Password == "" {
log.Printf("❌ [LOGIN_ADMIN] Mot de passe vide en base") log.Printf("❌ [LOGIN_ADMIN] Mot de passe vide en base")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Identifiants invalides"}) c.JSON(http.StatusUnauthorized, gin.H{"error": "Identifiants invalides"})
@@ -581,7 +445,6 @@ func LoginAdmin(c *gin.Context) {
return return
} }
// Générer le token
token, _ := generateAdminToken(user) token, _ := generateAdminToken(user)
expiresAt := time.Now().Add(adminTokenDuration) expiresAt := time.Now().Add(adminTokenDuration)
@@ -591,9 +454,7 @@ func LoginAdmin(c *gin.Context) {
return return
} }
log.Printf("✅ [LOGIN_ADMIN] User authentifié: %s (role=%s)", user.Username, user.Role) c.JSON(http.StatusOK, models.LoginResponse{
c.JSON(http.StatusOK, LoginResponse{
AccessToken: token, AccessToken: token,
TokenType: "Bearer", TokenType: "Bearer",
ExpiresIn: int(adminTokenDuration.Seconds()), ExpiresIn: int(adminTokenDuration.Seconds()),
@@ -606,7 +467,6 @@ func LoginAdmin(c *gin.Context) {
} }
// LogoutAdmin déconnecte un admin/cabine/livreur // LogoutAdmin déconnecte un admin/cabine/livreur
// POST /api/v1/auth/admin/logout
func LogoutAdmin(c *gin.Context) { func LogoutAdmin(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -651,8 +511,8 @@ func GetCurrentClient(c *gin.Context) {
"prenom": client.Prenom, "prenom": client.Prenom,
"telephone": client.Telephone, "telephone": client.Telephone,
"command": client.Command, "command": client.Command,
"point": client.Point,
"amende": client.Amende, "amende": client.Amende,
"points_extra": client.PointsExtra,
}, },
}) })
} }
@@ -675,7 +535,7 @@ func GetCurrentAdmin(c *gin.Context) {
log.Printf("✅ [GET_CURRENT_ADMIN] User récupéré: %s", user.Username) log.Printf("✅ [GET_CURRENT_ADMIN] User récupéré: %s", user.Username)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"user": ProfileResponse{ "user": models.ProfileResponse{
Username: user.Username, Username: user.Username,
Role: user.Role, Role: user.Role,
}, },
@@ -795,8 +655,7 @@ func GetAllClients(c *gin.Context) {
"prenom": cl.Prenom, "prenom": cl.Prenom,
"telephone": cl.Telephone, "telephone": cl.Telephone,
"command": cl.Command, "command": cl.Command,
"point": cl.Point, "points_extra": cl.PointsExtra,
"points_zipette": cl.PointZipette,
"amende": cl.Amende, "amende": cl.Amende,
"cancellations_count": cl.CancellationsCount, "cancellations_count": cl.CancellationsCount,
"last_penalty_reason": cl.LastPenaltyReason, "last_penalty_reason": cl.LastPenaltyReason,
+17 -47
View File
@@ -9,8 +9,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"gestion/db" "gestion/db"
"gestion/utils"
"log" "log"
"net/http" "net/http"
"slices"
"strconv" "strconv"
"time" "time"
@@ -22,7 +24,6 @@ import (
// ============================================ // ============================================
// SetCommandDestinationCoordinates stocke les coordonnées destination en Redis // SetCommandDestinationCoordinates stocke les coordonnées destination en Redis
// POST /api/v2/admin/protected/orders/:id/set-destination
func SetCommandDestinationCoordinates(c *gin.Context) { func SetCommandDestinationCoordinates(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -69,15 +70,10 @@ func SetCommandDestinationCoordinates(c *gin.Context) {
return return
} }
// Vérifier que la commande existe if !utils.CheckCommand(commandID, database) {
command, err := database.GetCommandByID(commandID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
return
} }
log.Printf("Command: %v", command)
// Stocker en Redis avec format JSON
destCacheKey := fmt.Sprintf("command:destination:%d", commandID) destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
coordsJSON, _ := json.Marshal(map[string]float64{ coordsJSON, _ := json.Marshal(map[string]float64{
"lat": req.Latitude, "lat": req.Latitude,
@@ -137,8 +133,8 @@ func GetClientProfile(c *gin.Context) {
"id": client.ID, "id": client.ID,
"username": client.Username, "username": client.Username,
"command": client.Command, "command": client.Command,
"point": client.Point,
"amende": client.Amende, "amende": client.Amende,
"points_extra": client.PointsExtra,
"created_at": client.CreatedAt, "created_at": client.CreatedAt,
}, },
}) })
@@ -170,8 +166,8 @@ func GetClientFullHistory(c *gin.Context) {
"client": gin.H{ "client": gin.H{
"username": client.Username, "username": client.Username,
"total_commands": client.Command, "total_commands": client.Command,
"points": client.Point,
"amende": client.Amende, "amende": client.Amende,
"points_extra": client.PointsExtra,
}, },
"commands": commands, "commands": commands,
"count": len(commands), "count": len(commands),
@@ -209,15 +205,7 @@ func UpdateCommandAddressCabine(c *gin.Context) {
status, _ := command["status"].(string) status, _ := command["status"].(string)
allowedStatuses := []string{"pending", "", "assigned"} allowedStatuses := []string{"pending", "", "assigned"}
isAllowed := false if !slices.Contains(allowedStatuses, status) {
for _, s := range allowedStatuses {
if status == s {
isAllowed = true
break
}
}
if !isAllowed {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": "Impossible de modifier l'adresse d'une commande en cours ou terminée", "error": "Impossible de modifier l'adresse d'une commande en cours ou terminée",
"current_status": status, "current_status": status,
@@ -288,10 +276,6 @@ func GetLivreurPosition(c *gin.Context) {
}) })
} }
// ============================================
// 4. DELIVERY TRACKING CLIENT (SANS GPS)
// ============================================
func GetDeliveryTrackingClient(c *gin.Context) { func GetDeliveryTrackingClient(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -394,10 +378,6 @@ func GetDeliveryTracking(c *gin.Context) {
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
} }
// ============================================
// 6. DELIVERY ISSUES
// ============================================
func GetDeliveryIssues(c *gin.Context) { func GetDeliveryIssues(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -630,24 +610,6 @@ func ForceValidateDelivery(c *gin.Context) {
return return
} }
validStatuses := []string{"assigned", "in_transit", "en_route", "en_cours", "pending", "priority"}
isValidStatus := false
for _, vs := range validStatuses {
if status == vs {
isValidStatus = true
break
}
}
if !isValidStatus && status != "livre" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Commande ne peut pas être validée de force dans ce statut",
"current_status": status,
"valid_statuses": validStatuses,
})
return
}
if status == "livre" { if status == "livre" {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": "Cette commande a déjà été validée", "error": "Cette commande a déjà été validée",
@@ -656,6 +618,16 @@ func ForceValidateDelivery(c *gin.Context) {
return return
} }
validStatuses := []string{"assigned", "en_route", "pending", "priority"}
if !slices.Contains(validStatuses, status) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Commande ne peut pas être validée de force dans ce statut",
"current_status": status,
"valid_statuses": validStatuses,
})
return
}
err = database.UpdateCommandStatus(commandID, "livre") err = database.UpdateCommandStatus(commandID, "livre")
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
@@ -668,7 +640,6 @@ func ForceValidateDelivery(c *gin.Context) {
clientUsername, _ := command["username"].(string) clientUsername, _ := command["username"].(string)
livreurAssign, _ := command["livreur_assign"].(string) livreurAssign, _ := command["livreur_assign"].(string)
// Notifier le client
if clientUsername != "" { if clientUsername != "" {
clientMsg := fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID) clientMsg := fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID)
database.NotifyClient(clientUsername, commandID, "livre", clientMsg) database.NotifyClient(clientUsername, commandID, "livre", clientMsg)
@@ -678,12 +649,11 @@ func ForceValidateDelivery(c *gin.Context) {
log.Printf("⚠️ Erreur compteur commandes: %v", err) log.Printf("⚠️ Erreur compteur commandes: %v", err)
} }
if err := database.AddClientPoints(clientUsername, 10); err != nil { if err := database.AddClientPointsByCategory(clientUsername, 10, ""); err != nil {
log.Printf("⚠️ Erreur ajout points: %v", err) log.Printf("⚠️ Erreur ajout points: %v", err)
} }
if livreurAssign != "" { if livreurAssign != "" {
log.Printf("📦 Commande %d validée de force par admin - Optimisation queue de %s...", commandID, livreurAssign)
err := database.CompleteDeliveryAndProcessNext(livreurAssign, commandID) err := database.CompleteDeliveryAndProcessNext(livreurAssign, commandID)
if err != nil { if err != nil {
log.Printf("⚠️ Erreur optimisation: %v", err) log.Printf("⚠️ Erreur optimisation: %v", err)
+12 -68
View File
@@ -18,10 +18,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// ============================================
// RATE LIMITING
// ============================================
var ( var (
cancelRateLimitMap = make(map[string][]time.Time) cancelRateLimitMap = make(map[string][]time.Time)
cancelMaxRequests = 5 // Max 5 annulations cancelMaxRequests = 5 // Max 5 annulations
@@ -49,16 +45,10 @@ func checkCancelRateLimit(key string) bool {
return true return true
} }
// ============================================
// HELPERS DE SÉCURITÉ
// ============================================
func validateReason(reason string) string { func validateReason(reason string) string {
// Limiter la longueur
if len(reason) > 500 { if len(reason) > 500 {
reason = reason[:500] reason = reason[:500]
} }
// Sanitizer
reason = strings.Map(func(r rune) rune { reason = strings.Map(func(r rune) rune {
if r < 32 || r == 127 { if r < 32 || r == 127 {
return -1 return -1
@@ -73,10 +63,6 @@ func validateReason(reason string) string {
return reason return reason
} }
// ============================================
// 1️⃣ ANNULATION PAR LE CLIENT - VERSION SÉCURISÉE
// ============================================
func CancelCommandByClient(c *gin.Context) { func CancelCommandByClient(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -115,19 +101,12 @@ func CancelCommandByClient(c *gin.Context) {
req.Reason = validateReason(req.Reason) req.Reason = validateReason(req.Reason)
log.Printf("🚫 [CANCEL_CLIENT] Client %s annule cmd %d (force=%v)", username, commandID, req.Force) penalty, err := database.CancelCommandAtomic(commandID, username, req.Reason, req.Force)
// ============================================
// UTILISER LA FONCTION ATOMIQUE
// ============================================
penalty, pointsLost, err := database.CancelCommandAtomic(commandID, username, req.Reason, req.Force)
if err != nil { if err != nil {
log.Printf("❌ [CANCEL_CLIENT] Erreur: %v", err) log.Printf("❌ [CANCEL_CLIENT] Erreur: %v", err)
// ✅ GESTION SPÉCIALE POUR "confirmation requise"
if err.Error() == "confirmation requise" { if err.Error() == "confirmation requise" {
// ✅ RÉCUPÉRER LES INFORMATIONS DE LA COMMANDE
command, errCmd := database.GetCommandByID(commandID) command, errCmd := database.GetCommandByID(commandID)
if errCmd != nil { if errCmd != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
@@ -137,28 +116,14 @@ func CancelCommandByClient(c *gin.Context) {
livreurAssign, _ := command["livreur_assign"].(string) livreurAssign, _ := command["livreur_assign"].(string)
currentStatus, _ := command["status"].(string) currentStatus, _ := command["status"].(string)
// ✅ VÉRIFIER SI ETA EXISTE (VERSION CORRIGÉE)
hasETA := false hasETA := false
if livreurAssign != "" { if livreurAssign != "" {
// ✅ FIX: Utiliser la nouvelle fonction qui vérifie VRAIMENT l'ETA
hasETA = database.CheckCommandETAExistsAndValid(commandID) hasETA = database.CheckCommandETAExistsAndValid(commandID)
} }
// ✅ CALCULER LA PÉNALITÉ QUI SERA APPLIQUÉE
nextPenalty, _ := database.CalculateCancellationPenalty(username) nextPenalty, _ := database.CalculateCancellationPenalty(username)
cancelCount, _ := database.GetClientCancellationsCount(username) cancelCount, _ := database.GetClientCancellationsCount(username)
// ✅ RÉCUPÉRER LES POINTS ACTUELS
client, _ := database.GetClientByUsername(username)
currentPointsWeed := 0
currentPointsZipette := 0
if client != nil {
currentPointsWeed = client.Point
currentPointsZipette = client.PointZipette
}
totalPoints := currentPointsWeed + currentPointsZipette
// ✅ CONSTRUIRE LA RÉPONSE EN FONCTION DE hasETA
response := gin.H{ response := gin.H{
"success": false, "success": false,
"warning": true, "warning": true,
@@ -170,7 +135,6 @@ func CancelCommandByClient(c *gin.Context) {
} }
if hasETA { if hasETA {
// ⚠️ CAS 1: LIVREUR EN ROUTE (ETA définie) = PÉNALITÉ TOTALE
log.Printf("⚠️ [CANCEL_CLIENT] Annulation tardive avec ETA - Status: %s, Livreur: %s", currentStatus, livreurAssign) log.Printf("⚠️ [CANCEL_CLIENT] Annulation tardive avec ETA - Status: %s, Livreur: %s", currentStatus, livreurAssign)
response["message"] = "⚠️ Un livreur est en route vers votre adresse (ETA définie)" response["message"] = "⚠️ Un livreur est en route vers votre adresse (ETA définie)"
@@ -183,24 +147,18 @@ func CancelCommandByClient(c *gin.Context) {
"will_apply": true, "will_apply": true,
"penalty_amount": nextPenalty, "penalty_amount": nextPenalty,
"current_violations": cancelCount, "current_violations": cancelCount,
"current_points_weed": currentPointsWeed,
"current_points_zipette": currentPointsZipette,
"total_points": totalPoints,
"points_will_reset": true,
"message": fmt.Sprintf( "message": fmt.Sprintf(
"⚠️ ATTENTION: Une amende de %d points sera appliquée ET tous vos points (%d weed/hash + %d zipette = %d total) seront remis à zéro!", "⚠️ ATTENTION: Une amende de %d sera appliquée pour annulation tardive",
nextPenalty, currentPointsWeed, currentPointsZipette, totalPoints, nextPenalty,
), ),
"scale": gin.H{ "scale": gin.H{
"1st_cancel": "20 points + remise à zéro TOTALE", "1st_cancel": 20,
"2nd_cancel": "50 points + remise à zéro TOTALE", "2nd_cancel": 50,
"3rd_cancel": "100 points + remise à zéro TOTALE", "3rd_cancel": 100,
"4th+_cancel": "150 points + remise à zéro TOTALE", "4th+_cancel": 150,
"your_next": fmt.Sprintf("%d points + remise à zéro de tous vos %d points", nextPenalty, totalPoints),
}, },
} }
} else { } else {
// ️ CAS 2: LIVREUR ASSIGNÉ MAIS PAS EN ROUTE (PAS D'ETA) = PAS DE PÉNALITÉ
log.Printf("️ [CANCEL_CLIENT] Livreur assigné mais pas d'ETA - Annulation sans pénalité") log.Printf("️ [CANCEL_CLIENT] Livreur assigné mais pas d'ETA - Annulation sans pénalité")
response["message"] = "️ Un livreur est assigné mais n'est pas encore en route" response["message"] = "️ Un livreur est assigné mais n'est pas encore en route"
@@ -274,11 +232,8 @@ func CancelCommandByClient(c *gin.Context) {
if penalty > 0 { if penalty > 0 {
response["penalty"] = gin.H{ response["penalty"] = gin.H{
"penalty_points": penalty, "penalty_amount": penalty,
"points_weed_lost": pointsLost["weed"], "warning": "Une amende a été appliquée pour annulation tardive",
"points_zipette_lost": pointsLost["zipette"],
"total_points_lost": pointsLost["weed"] + pointsLost["zipette"],
"warning": "Une pénalité a été appliquée et vos points ont été remis à zéro",
} }
} else { } else {
response["info"] = "Aucune pénalité appliquée" response["info"] = "Aucune pénalité appliquée"
@@ -325,10 +280,6 @@ func GetMyCancellationHistory(c *gin.Context) {
}) })
} }
// ============================================
// LISTE DES COMMANDES ANNULÉES - VERSION SÉCURISÉE
// ============================================
func GetAllCancelledOrders(c *gin.Context) { func GetAllCancelledOrders(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -345,7 +296,6 @@ func GetAllCancelledOrders(c *gin.Context) {
return return
} }
// ✅ VALIDATION des paramètres
filterUsername := c.Query("username") filterUsername := c.Query("username")
if len(filterUsername) > 100 { if len(filterUsername) > 100 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Username trop long"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Username trop long"})
@@ -373,14 +323,14 @@ func GetAllCancelledOrders(c *gin.Context) {
} }
// ✅ ENRICHIR les données (sans exposer d'infos sensibles inutiles) // ✅ ENRICHIR les données (sans exposer d'infos sensibles inutiles)
var enrichedOrders []map[string]interface{} var enrichedOrders []map[string]any
for _, order := range cancelledOrders { for _, order := range cancelledOrders {
orderID, _ := order["id"].(int) orderID, _ := order["id"].(int)
items, _ := database.GetCommandItems(orderID) items, _ := database.GetCommandItems(orderID)
logs, _ := database.GetCommandLogs(orderID) logs, _ := database.GetCommandLogs(orderID)
var cancellationLog map[string]interface{} var cancellationLog map[string]any
for _, logEntry := range logs { for _, logEntry := range logs {
status, _ := logEntry["status"].(string) status, _ := logEntry["status"].(string)
if status == "cancelled" { if status == "cancelled" {
@@ -389,7 +339,7 @@ func GetAllCancelledOrders(c *gin.Context) {
} }
} }
enrichedOrder := map[string]interface{}{ enrichedOrder := map[string]any{
"id": order["id"], "id": order["id"],
"username": order["username"], "username": order["username"],
"total_prix": order["total_prix"], "total_prix": order["total_prix"],
@@ -419,10 +369,6 @@ func GetAllCancelledOrders(c *gin.Context) {
}) })
} }
// ============================================
// SUPPRESSION PAR CABINE - VERSION SÉCURISÉE
// ============================================
func DeleteCommandByCabine(c *gin.Context) { func DeleteCommandByCabine(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -447,12 +393,10 @@ func DeleteCommandByCabine(c *gin.Context) {
log.Printf("🗑️ [DELETE_COMMAND] %s (%s) supprime cmd %d", username, userRole, commandID) log.Printf("🗑️ [DELETE_COMMAND] %s (%s) supprime cmd %d", username, userRole, commandID)
// ✅ UTILISER LA FONCTION ATOMIQUE
err = database.DeleteCommandAtomic(commandID, username, userRole) err = database.DeleteCommandAtomic(commandID, username, userRole)
if err != nil { if err != nil {
log.Printf("❌ [DELETE_COMMAND] Erreur: %v", err) log.Printf("❌ [DELETE_COMMAND] Erreur: %v", err)
// ❌ Ne pas exposer les détails de l'erreur
if err.Error() == "commande non trouvée" { if err.Error() == "commande non trouvée" {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
} else { } else {
-4
View File
@@ -10,7 +10,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// GET /api/v1/categories — public
func GetCategories(c *gin.Context) { func GetCategories(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -27,7 +26,6 @@ func GetCategories(c *gin.Context) {
}) })
} }
// POST /api/v2/admin/protected/categories — admin
func CreateCategory(c *gin.Context) { func CreateCategory(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -111,7 +109,6 @@ func UpdateCategory(c *gin.Context) {
}) })
} }
// DELETE /api/v2/admin/protected/categories/:id — admin
func DeleteCategory(c *gin.Context) { func DeleteCategory(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -131,6 +128,5 @@ func DeleteCategory(c *gin.Context) {
return return
} }
log.Printf("✅ [CATEGORIES] Supprimée: %d", id)
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Catégorie supprimée"}) c.JSON(http.StatusOK, gin.H{"success": true, "message": "Catégorie supprimée"})
} }
+1 -19
View File
@@ -10,7 +10,6 @@ import (
) )
// GetCommandStatus - Statut temps réel d'une commande // GetCommandStatus - Statut temps réel d'une commande
// GET /api/v1/commands/:id/status
func GetCommandStatus(c *gin.Context) { func GetCommandStatus(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -28,16 +27,12 @@ func GetCommandStatus(c *gin.Context) {
return return
} }
log.Printf("📊 [STATUS] Client %s demande statut cmd %d", usernameStr, commandID)
// Récupérer la commande
command, err := database.GetCommandByID(commandID) command, err := database.GetCommandByID(commandID)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
return return
} }
// ✅ VÉRIFIER PROPRIÉTÉ
cmdUsername, _ := command["username"].(string) cmdUsername, _ := command["username"].(string)
if cmdUsername != usernameStr { if cmdUsername != usernameStr {
log.Printf("❌ [STATUS] Accès refusé - cmd de %s demandée par %s", cmdUsername, usernameStr) log.Printf("❌ [STATUS] Accès refusé - cmd de %s demandée par %s", cmdUsername, usernameStr)
@@ -47,10 +42,8 @@ func GetCommandStatus(c *gin.Context) {
return return
} }
// Récupérer ETA
etaData, _ := database.GetCommandETA(commandID) etaData, _ := database.GetCommandETA(commandID)
// Récupérer infos livreur (si assigné)
livreurInfo := gin.H{ livreurInfo := gin.H{
"assigned": false, "assigned": false,
} }
@@ -63,7 +56,6 @@ func GetCommandStatus(c *gin.Context) {
} }
} }
// Mapper le statut en message lisible
statusMessage := getStatusMessage(command["status"].(string)) statusMessage := getStatusMessage(command["status"].(string))
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@@ -80,7 +72,6 @@ func GetCommandStatus(c *gin.Context) {
} }
// GetMyCommandsWithTracking - Liste des commandes avec suivi // GetMyCommandsWithTracking - Liste des commandes avec suivi
// GET /api/v1/my-commands
func GetMyCommandsWithTracking(c *gin.Context) { func GetMyCommandsWithTracking(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -147,7 +138,6 @@ func GetMyCommandsWithTracking(c *gin.Context) {
} }
// GetCommandTracking - Suivi détaillé d'une commande // GetCommandTracking - Suivi détaillé d'une commande
// GET /api/v1/commands/:id/tracking
func GetCommandTracking(c *gin.Context) { func GetCommandTracking(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -174,13 +164,10 @@ func GetCommandTracking(c *gin.Context) {
return return
} }
// Récupérer logs
logs, _ := database.GetCommandLogs(commandID) logs, _ := database.GetCommandLogs(commandID)
// ETA
etaData, _ := database.GetCommandETA(commandID) etaData, _ := database.GetCommandETA(commandID)
// Timeline (basé sur les logs)
timeline := buildTimeline(logs) timeline := buildTimeline(logs)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@@ -194,11 +181,6 @@ func GetCommandTracking(c *gin.Context) {
}) })
} }
// ============================================
// HELPERS
// ============================================
// getStatusMessage retourne un message lisible pour le client
func getStatusMessage(status string) string { func getStatusMessage(status string) string {
messages := map[string]string{ messages := map[string]string{
"pending": "⏳ En attente d'assignation", "pending": "⏳ En attente d'assignation",
@@ -219,7 +201,7 @@ func getStatusMessage(status string) string {
} }
// buildTimeline construit une timeline depuis les logs // buildTimeline construit une timeline depuis les logs
func buildTimeline(logs []map[string]interface{}) []gin.H { func buildTimeline(logs []map[string]any) []gin.H {
timeline := make([]gin.H, 0) timeline := make([]gin.H, 0)
for _, logEntry := range logs { for _, logEntry := range logs {
+11 -18
View File
@@ -3,6 +3,7 @@ package handlers
import ( import (
"fmt" "fmt"
"gestion/db" "gestion/db"
"gestion/utils"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
@@ -42,9 +43,6 @@ func checkRateLimit(key string) bool {
return true return true
} }
// ============================================
// GESTION ADRESSE & ADMIN
// ============================================
func safeGetUsername(c *gin.Context) (string, error) { func safeGetUsername(c *gin.Context) (string, error) {
username, exists := c.Get("username") username, exists := c.Get("username")
if !exists { if !exists {
@@ -71,13 +69,11 @@ func validateAddress(address string) error {
} }
// UpdateCommandAddress met à jour l'adresse de livraison d'une commande // UpdateCommandAddress met à jour l'adresse de livraison d'une commande
// PUT /api/v1/admin/commands/:id/address
func UpdateCommandAddress(c *gin.Context) { func UpdateCommandAddress(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
// ✅ Vérification du rôle userRole := c.GetString("role")
if c.GetString("role") != "admin" { if !utils.CheckRoleAdmin(c, userRole) {
log.Printf("❌ [UPD_ADDR] Accès refusé - role=%s", c.GetString("role"))
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return return
} }
@@ -111,13 +107,11 @@ func UpdateCommandAddress(c *gin.Context) {
return return
} }
// ✅ Validation de l'adresse
if err := validateAddress(req.DeliveryAddress); err != nil { if err := validateAddress(req.DeliveryAddress); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
// ✅ Logs sanitizés
log.Printf("📝 [UPD_ADDR] Admin %s modifie cmd %d", adminUsername, commandID) log.Printf("📝 [UPD_ADDR] Admin %s modifie cmd %d", adminUsername, commandID)
command, err := database.GetCommandByID(commandID) command, err := database.GetCommandByID(commandID)
@@ -156,13 +150,11 @@ func UpdateCommandAddress(c *gin.Context) {
} }
// ProposeAddressChange propose une nouvelle adresse au client pour validation // ProposeAddressChange propose une nouvelle adresse au client pour validation
// POST /api/v2/admin/protected/orders/:id/propose-address
// POST /api/v1/cabine/commands/:id/propose-address
func ProposeAddressChange(c *gin.Context) { func ProposeAddressChange(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
userRole := c.GetString("role") userRole := c.GetString("role")
if userRole != "admin" && userRole != "cabine" { if !utils.CheckRoleAdmin(c, userRole) && !utils.CheckRoleCabine(c, userRole) {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return return
} }
@@ -218,7 +210,6 @@ func ProposeAddressChange(c *gin.Context) {
database.NotifyClient(clientUsername, commandID, "address_proposal", msg) database.NotifyClient(clientUsername, commandID, "address_proposal", msg)
} }
log.Printf("✅ [PROPOSE_ADDR] Commande %d - nouvelle adresse proposée par %s", commandID, staffUsername)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "Nouvelle adresse proposée au client", "message": "Nouvelle adresse proposée au client",
@@ -231,6 +222,11 @@ func ProposeAddressChange(c *gin.Context) {
func RespondToAddressProposal(c *gin.Context) { func RespondToAddressProposal(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
userRole := c.GetString("role")
if !utils.CheckRoleClient(c, userRole) {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
clientUsername, err := safeGetUsername(c) clientUsername, err := safeGetUsername(c)
if err != nil { if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"})
@@ -276,8 +272,7 @@ func GetAllCommands(c *gin.Context) {
username := c.Query("username") username := c.Query("username")
userRole := c.GetString("role") userRole := c.GetString("role")
if userRole != "admin" && userRole != "cabine" { if !utils.CheckRoleAdmin(c, userRole) && !utils.CheckRoleCabine(c, userRole) {
log.Printf("❌ [VALIDATE] Accès refusé - role=%s", userRole)
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return return
} }
@@ -725,8 +720,7 @@ func GetClientCommandsHistory(c *gin.Context) {
} else if client == nil { } else if client == nil {
log.Printf("⚠️ [HISTORY] client est NIL!") log.Printf("⚠️ [HISTORY] client est NIL!")
} else { } else {
log.Printf("✅ [HISTORY] Client récupéré: username=%s, point=%d, point_zipette=%d", log.Printf("✅ [HISTORY] Client récupéré: username=%s", client.Username)
client.Username, client.Point, client.PointZipette)
} }
resp := gin.H{ resp := gin.H{
@@ -760,7 +754,6 @@ func GetClientCommandsHistory(c *gin.Context) {
"prenom": client.Prenom, "prenom": client.Prenom,
"telephone": client.Telephone, "telephone": client.Telephone,
"total_commands": client.Command, "total_commands": client.Command,
"points": client.Point,
"pool_points": poolPoints, "pool_points": poolPoints,
"pool_names": poolNames, "pool_names": poolNames,
"penalties": client.Amende, "penalties": client.Amende,
@@ -13,7 +13,6 @@ import (
) )
// IPNWebhook - POST /api/v1/webhooks/nowpayments // IPNWebhook - POST /api/v1/webhooks/nowpayments
// Reçoit les callbacks de NowPayments lors des changements de statut
func IPNWebhook(c *gin.Context) { func IPNWebhook(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
np, ok := c.MustGet("nowpayments").(*services.NowPaymentsClient) np, ok := c.MustGet("nowpayments").(*services.NowPaymentsClient)
@@ -44,7 +43,6 @@ func IPNWebhook(c *gin.Context) {
payment, err := database.GetCryptoPaymentByNowPaymentID(payload.PaymentID.String()) payment, err := database.GetCryptoPaymentByNowPaymentID(payload.PaymentID.String())
if err != nil || payment == nil { if err != nil || payment == nil {
log.Printf("[IPN] paiement introuvable: %s", payload.PaymentID.String()) log.Printf("[IPN] paiement introuvable: %s", payload.PaymentID.String())
// 200 pour éviter les retries NowPayments
c.JSON(http.StatusOK, gin.H{"ok": true}) c.JSON(http.StatusOK, gin.H{"ok": true})
return return
} }
+30 -76
View File
@@ -1,25 +1,18 @@
// ============================================
// handlers/delivery_handlers.go
// 🔧 VERSION MODIFIÉE avec ETA automatique
// ============================================
package handlers package handlers
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"gestion/db" "gestion/db"
"gestion/utils"
"log" "log"
"math"
"net/http" "net/http"
"slices"
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// ============================================
// 🔧 GetMyDeliveries
// ============================================
func GetMyDeliveries(c *gin.Context) { func GetMyDeliveries(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -46,7 +39,6 @@ func GetMyDeliveries(c *gin.Context) {
return return
} }
// ✨ FILTRAGE (SANS TÉLÉPHONE)
filteredCommands := make([]gin.H, len(commands)) filteredCommands := make([]gin.H, len(commands))
for i, cmd := range commands { for i, cmd := range commands {
commandID, _ := cmd["id"].(int) commandID, _ := cmd["id"].(int)
@@ -172,10 +164,6 @@ func GetDeliveryDetails(c *gin.Context) {
}) })
} }
// ============================================
// 🔧 UpdateDeliveryStatus - VERSION MODIFIÉE
// ✅ CALCUL AUTOMATIQUE ETA lors du passage en "en_route"
// ============================================
func UpdateDeliveryStatus(c *gin.Context) { func UpdateDeliveryStatus(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -215,7 +203,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
return return
} }
// ✅ VÉRIFIER PROPRIÉTÉ
livreurAssign, _ := command["livreur_assign"].(string) livreurAssign, _ := command["livreur_assign"].(string)
if livreurAssign != usernameStr { if livreurAssign != usernameStr {
log.Printf("❌ Accès refusé - assigné à %s", livreurAssign) log.Printf("❌ Accès refusé - assigné à %s", livreurAssign)
@@ -225,7 +212,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
return return
} }
// ✅ STATUTS VALIDES POUR LIVREUR (correspondant à la DB)
validStatuses := []string{ validStatuses := []string{
"assigned", "assigned",
"en_route", "en_route",
@@ -234,15 +220,7 @@ func UpdateDeliveryStatus(c *gin.Context) {
"cancelled", "cancelled",
} }
isValid := false if !slices.Contains(validStatuses, req.Status) {
for _, s := range validStatuses {
if req.Status == s {
isValid = true
break
}
}
if !isValid {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"error": "Statut invalide", "error": "Statut invalide",
"valid_statuses": validStatuses, "valid_statuses": validStatuses,
@@ -261,7 +239,7 @@ func UpdateDeliveryStatus(c *gin.Context) {
destLon, _ := command["dest_longitude"].(float64) destLon, _ := command["dest_longitude"].(float64)
if destLat != 0 && destLon != 0 { if destLat != 0 && destLon != 0 {
distance := calculateDistance(req.Latitude, req.Longitude, destLat, destLon) distance := utils.CalculateDistance(req.Latitude, req.Longitude, destLat, destLon)
log.Printf("📍 [GPS] Distance: %.2f m", distance) log.Printf("📍 [GPS] Distance: %.2f m", distance)
if distance > 100 { if distance > 100 {
@@ -295,10 +273,8 @@ func UpdateDeliveryStatus(c *gin.Context) {
if req.Status == "en_route" { if req.Status == "en_route" {
log.Printf("🚗 [STATUS_LIVREUR] Passage en 'en_route' - Calcul ETA...") log.Printf("🚗 [STATUS_LIVREUR] Passage en 'en_route' - Calcul ETA...")
// Récupérer les coordonnées destination
var destLat, destLon float64 var destLat, destLon float64
// 1. Essayer le cache Redis
destCacheKey := fmt.Sprintf("command:destination:%d", commandID) destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
destData, err := db.Redis.Get(db.RedisCtx, destCacheKey).Result() destData, err := db.Redis.Get(db.RedisCtx, destCacheKey).Result()
if err == nil && destData != "" { if err == nil && destData != "" {
@@ -326,22 +302,29 @@ func UpdateDeliveryStatus(c *gin.Context) {
} }
} }
// 3. Calculer l'ETA depuis la position du livreur
if destLat != 0 && destLon != 0 { if destLat != 0 && destLon != 0 {
etaMinutes = database.CalculateETAForDeliveryman(usernameStr, destLat, destLon) etaMinutes = database.CalculateETAForDeliveryman(usernameStr, destLat, destLon)
// Définir l'ETA dans Redis
if err := database.SetCommandETA(commandID, etaMinutes); err != nil { if err := database.SetCommandETA(commandID, etaMinutes); err != nil {
log.Printf("⚠️ [STATUS_LIVREUR] Erreur définition ETA: %v", err) log.Printf("⚠️ [STATUS_LIVREUR] Erreur définition ETA: %v", err)
} else { } else {
log.Printf("✅ [STATUS_LIVREUR] ETA défini: %d minutes", etaMinutes) log.Printf("✅ [STATUS_LIVREUR] ETA défini: %d minutes", etaMinutes)
if etaMinutes >= 60 {
h := etaMinutes / 60
m := etaMinutes % 60
if m > 0 {
etaMessage = fmt.Sprintf("Arrivée prévue dans %dh%02d", h, m)
} else {
etaMessage = fmt.Sprintf("Arrivée prévue dans %dh", h)
}
} else {
etaMessage = fmt.Sprintf("Arrivée prévue dans %d minutes", etaMinutes) etaMessage = fmt.Sprintf("Arrivée prévue dans %d minutes", etaMinutes)
} }
}
} else { } else {
log.Printf("⚠️ [STATUS_LIVREUR] Coordonnées destination manquantes - ETA par défaut") log.Printf("⚠️ [STATUS_LIVREUR] Coordonnées destination manquantes - ETA par défaut")
etaMinutes = 30 // Fallback etaMinutes = 30
database.SetCommandETA(commandID, etaMinutes) database.SetCommandETA(commandID, etaMinutes)
etaMessage = "Arrivée prévue dans 30 minutes (estimation par défaut)"
} }
// Mettre à jour le statut du livreur en "delivering" // Mettre à jour le statut du livreur en "delivering"
@@ -352,7 +335,7 @@ func UpdateDeliveryStatus(c *gin.Context) {
// Log // Log
message := req.Notes message := req.Notes
if message == "" { if message == "" {
message = getDeliveryStatusMessage(req.Status) message = utils.GetDeliveryStatusMessage(req.Status)
} }
if etaMessage != "" { if etaMessage != "" {
message += fmt.Sprintf(" - %s", etaMessage) message += fmt.Sprintf(" - %s", etaMessage)
@@ -366,9 +349,21 @@ func UpdateDeliveryStatus(c *gin.Context) {
switch req.Status { switch req.Status {
case "en_route": case "en_route":
if etaMinutes > 0 { if etaMinutes > 0 {
clientMsg = fmt.Sprintf("Votre commande #%d est en route ! Arrivée dans ~%d min", commandID, etaMinutes) var etaStr string
if etaMinutes >= 60 {
h := etaMinutes / 60
m := etaMinutes % 60
if m > 0 {
etaStr = fmt.Sprintf("%dh%02d", h, m)
} else { } else {
clientMsg = fmt.Sprintf("Votre commande #%d est en route !", commandID) etaStr = fmt.Sprintf("%dh", h)
}
} else {
etaStr = fmt.Sprintf("%d min", etaMinutes)
}
clientMsg = fmt.Sprintf("🛵 Votre commande #%d est en route ! Arrivée dans ~%s", commandID, etaStr)
} else {
clientMsg = fmt.Sprintf("🛵 Votre commande #%d est en route !", commandID)
} }
case "arrived": case "arrived":
clientMsg = fmt.Sprintf("🛵 Votre livreur est là ! Il sera chez vous dans 5 minutes (commande #%d)", commandID) clientMsg = fmt.Sprintf("🛵 Votre livreur est là ! Il sera chez vous dans 5 minutes (commande #%d)", commandID)
@@ -412,44 +407,3 @@ func UpdateDeliveryStatus(c *gin.Context) {
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
} }
// ============================================
// HELPERS
// ============================================
func calculateDistance(lat1, lon1, lat2, lon2 float64) float64 {
const earthRadiusKm = 6371
const metersPerKm = 1000
lat1Rad := degreesToRadians(lat1)
lon1Rad := degreesToRadians(lon1)
lat2Rad := degreesToRadians(lat2)
lon2Rad := degreesToRadians(lon2)
dLat := lat2Rad - lat1Rad
dLon := lon2Rad - lon1Rad
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(lat1Rad)*math.Cos(lat2Rad)*
math.Sin(dLon/2)*math.Sin(dLon/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return earthRadiusKm * c * metersPerKm
}
func degreesToRadians(degrees float64) float64 {
return degrees * math.Pi / 180
}
func getDeliveryStatusMessage(status string) string {
messages := map[string]string{
"assigned": "Commande assignée",
"en_route": "En route vers le client",
"arrived": "Arrivé à destination",
"livre": "Livraison effectuée",
"cancelled": "Livraison annulée",
}
if msg, ok := messages[status]; ok {
return msg
}
return fmt.Sprintf("Statut changé: %s", status)
}
+2 -55
View File
@@ -8,6 +8,7 @@ package handlers
import ( import (
"fmt" "fmt"
"gestion/db" "gestion/db"
"gestion/utils"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
@@ -16,12 +17,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// ============================================
// 📊 GET DELIVERY PERSON DETAILS
// ============================================
// GetDeliveryPersonDetails récupère les détails complets d'un livreur // GetDeliveryPersonDetails récupère les détails complets d'un livreur
// GET /api/v2/admin/protected/delivery-persons/:username
func GetDeliveryPersonDetails(c *gin.Context) { func GetDeliveryPersonDetails(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -39,11 +35,6 @@ func GetDeliveryPersonDetails(c *gin.Context) {
return return
} }
log.Printf("👤 [GET_DELIVERY_DETAILS] Récupération détails pour: %s", username)
// ============================================
// ÉTAPE 1: Récupérer les infos de base du livreur
// ============================================
livreur, err := database.GetUserByUsername(username) livreur, err := database.GetUserByUsername(username)
if err != nil { if err != nil {
log.Printf("❌ [GET_DELIVERY_DETAILS] Livreur non trouvé: %v", err) log.Printf("❌ [GET_DELIVERY_DETAILS] Livreur non trouvé: %v", err)
@@ -64,9 +55,6 @@ func GetDeliveryPersonDetails(c *gin.Context) {
return return
} }
// ============================================
// ÉTAPE 2: Récupérer le statut et la position GPS
// ============================================
status, err := database.GetDeliveryPersonStatus(username) status, err := database.GetDeliveryPersonStatus(username)
if err != nil { if err != nil {
log.Printf("⚠️ [GET_DELIVERY_DETAILS] Impossible de récupérer le statut: %v", err) log.Printf("⚠️ [GET_DELIVERY_DETAILS] Impossible de récupérer le statut: %v", err)
@@ -83,22 +71,13 @@ func GetDeliveryPersonDetails(c *gin.Context) {
} }
} }
// ============================================
// ÉTAPE 3: Récupérer les stats de livraison
// ============================================
queueSize, _ := database.GetDeliverymanQueueSize(username) queueSize, _ := database.GetDeliverymanQueueSize(username)
currentCommand, _ := database.GetCurrentCommand(username) currentCommand, _ := database.GetCurrentCommand(username)
// Compter les livraisons
totalDeliveries, _ := database.CountDeliveriesByStatus(username, "") totalDeliveries, _ := database.CountDeliveriesByStatus(username, "")
completedDeliveries, _ := database.CountDeliveriesByStatus(username, "approved") completedDeliveries, _ := database.CountDeliveriesByStatus(username, "approved")
pendingDeliveries, _ := database.CountDeliveriesByStatus(username, "assigned,en_route,livre") pendingDeliveries, _ := database.CountDeliveriesByStatus(username, "assigned,en_route,livre")
log.Printf("✅ [GET_DELIVERY_DETAILS] Détails récupérés pour %s", username)
// ============================================
// RÉPONSE
// ============================================
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"deliveryman": gin.H{ "deliveryman": gin.H{
@@ -116,19 +95,12 @@ func GetDeliveryPersonDetails(c *gin.Context) {
}) })
} }
// ============================================
// 🔄 UPDATE DELIVERY PERSON STATUS
// ============================================
// UpdateDeliveryPersonStatusAdmin modifie le statut d'un livreur (Admin) // UpdateDeliveryPersonStatusAdmin modifie le statut d'un livreur (Admin)
// PUT /api/v2/admin/protected/delivery-persons/:username/status
func UpdateDeliveryPersonStatusAdmin(c *gin.Context) { func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
// ✅ SÉCURITÉ: Admin seulement
userRole := c.GetString("role") userRole := c.GetString("role")
if userRole != "admin" { if !utils.CheckRoleAdmin(c, userRole) {
log.Printf("❌ [UPDATE_DELIVERY_STATUS] Accès refusé - role=%s", userRole)
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return return
} }
@@ -172,9 +144,6 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
log.Printf("📝 [UPDATE_DELIVERY_STATUS] Modification: %s → %s", username, req.Status) log.Printf("📝 [UPDATE_DELIVERY_STATUS] Modification: %s → %s", username, req.Status)
// ============================================
// Vérifier que le livreur existe
// ============================================
livreur, err := database.GetUserByUsername(username) livreur, err := database.GetUserByUsername(username)
if err != nil { if err != nil {
log.Printf("❌ [UPDATE_DELIVERY_STATUS] Livreur non trouvé") log.Printf("❌ [UPDATE_DELIVERY_STATUS] Livreur non trouvé")
@@ -189,9 +158,6 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
return return
} }
// ============================================
// Mettre à jour le statut
// ============================================
err = database.UpdateDeliveryPersonStatus(username, req.Status) err = database.UpdateDeliveryPersonStatus(username, req.Status)
if err != nil { if err != nil {
log.Printf("❌ [UPDATE_DELIVERY_STATUS] Erreur mise à jour: %v", err) log.Printf("❌ [UPDATE_DELIVERY_STATUS] Erreur mise à jour: %v", err)
@@ -215,16 +181,10 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
}) })
} }
// ============================================
// 📊 GET DELIVERY PERSON STATS
// ============================================
// GetDeliveryPersonStats récupère les statistiques d'un livreur // GetDeliveryPersonStats récupère les statistiques d'un livreur
// GET /api/v2/admin/protected/delivery-persons/:username/stats
func GetDeliveryPersonStats(c *gin.Context) { func GetDeliveryPersonStats(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
// ✅ SÉCURITÉ: Admin seulement
userRole := c.GetString("role") userRole := c.GetString("role")
if userRole != "admin" { if userRole != "admin" {
log.Printf("❌ [GET_DELIVERY_STATS] Accès refusé - role=%s", userRole) log.Printf("❌ [GET_DELIVERY_STATS] Accès refusé - role=%s", userRole)
@@ -240,9 +200,6 @@ func GetDeliveryPersonStats(c *gin.Context) {
log.Printf("📊 [GET_DELIVERY_STATS] Calcul stats pour: %s", username) log.Printf("📊 [GET_DELIVERY_STATS] Calcul stats pour: %s", username)
// ============================================
// Vérifier que le livreur existe
// ============================================
livreur, err := database.GetUserByUsername(username) livreur, err := database.GetUserByUsername(username)
if err != nil { if err != nil {
log.Printf("❌ [GET_DELIVERY_STATS] Livreur non trouvé") log.Printf("❌ [GET_DELIVERY_STATS] Livreur non trouvé")
@@ -306,12 +263,7 @@ func GetDeliveryPersonStats(c *gin.Context) {
}) })
} }
// ============================================
// 📜 GET DELIVERY PERSON HISTORY
// ============================================
// GetDeliveryPersonHistory récupère l'historique des livraisons d'un livreur // GetDeliveryPersonHistory récupère l'historique des livraisons d'un livreur
// GET /api/v2/admin/protected/delivery-persons/:username/history?limit=20&offset=0
func GetDeliveryPersonHistory(c *gin.Context) { func GetDeliveryPersonHistory(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -394,10 +346,6 @@ func GetDeliveryPersonHistory(c *gin.Context) {
}) })
} }
// ============================================
// 📍 UPDATE DELIVERY PERSON LOCATION
// ============================================
// UpdateDeliveryPersonLocationAdmin modifie la position GPS d'un livreur (Admin) // UpdateDeliveryPersonLocationAdmin modifie la position GPS d'un livreur (Admin)
// PUT /api/v2/admin/protected/delivery-persons/:username/location // PUT /api/v2/admin/protected/delivery-persons/:username/location
func UpdateDeliveryPersonLocationAdmin(c *gin.Context) { func UpdateDeliveryPersonLocationAdmin(c *gin.Context) {
@@ -579,7 +527,6 @@ func RemoveCommandFromQueue(c *gin.Context) {
if err != nil { if err != nil {
log.Printf("⚠️ [REMOVE_FROM_QUEUE] Impossible de réinitialiser le statut: %v", err) log.Printf("⚠️ [REMOVE_FROM_QUEUE] Impossible de réinitialiser le statut: %v", err)
} else { } else {
// Retirer l'assignation du livreur
database.UpdateCommandLivreur(commandID, "") database.UpdateCommandLivreur(commandID, "")
log.Printf("✅ [REMOVE_FROM_QUEUE] Commande réinitialisée en 'pending'") log.Printf("✅ [REMOVE_FROM_QUEUE] Commande réinitialisée en 'pending'")
} }
-8
View File
@@ -18,11 +18,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// ============================================
// GET /api/v1/orders/:id/eta
// ✅ CORRECTION: ETA visible UNIQUEMENT si status >= en_route
// ============================================
func GetOrderETA(c *gin.Context) { func GetOrderETA(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
geoService := c.MustGet("geoService").(*services.GeoService) geoService := c.MustGet("geoService").(*services.GeoService)
@@ -49,9 +44,6 @@ func GetOrderETA(c *gin.Context) {
return return
} }
log.Printf("📊 [ETA] START - commandID=%d, username=%s", commandID, username.(string))
// 3️⃣ RÉCUPÉRER LA COMMANDE
command, err := database.GetCommandByID(commandID) command, err := database.GetCommandByID(commandID)
if err != nil { if err != nil {
log.Printf("❌ [ETA] Commande %d non trouvée", commandID) log.Printf("❌ [ETA] Commande %d non trouvée", commandID)
-8
View File
@@ -22,8 +22,6 @@ import (
// ============================================ // ============================================
// GeocodeAddress convertit une adresse en coordonnées GPS // GeocodeAddress convertit une adresse en coordonnées GPS
// POST /api/v1/geocode
// Body: {"address": "1600 Amphitheatre Parkway, Mountain View, CA"}
func GeocodeAddress(c *gin.Context) { func GeocodeAddress(c *gin.Context) {
geoService := c.MustGet("geoService").(*services.GeoService) geoService := c.MustGet("geoService").(*services.GeoService)
@@ -59,10 +57,6 @@ func GeocodeAddress(c *gin.Context) {
}) })
} }
// ============================================
// RECHERCHE DU LIVREUR LE PLUS PROCHE
// ============================================
// FindNearestDeliveryPerson trouve le livreur le plus proche d'une adresse // FindNearestDeliveryPerson trouve le livreur le plus proche d'une adresse
func FindNearestDeliveryPerson(c *gin.Context) { func FindNearestDeliveryPerson(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -181,8 +175,6 @@ func FindNearestDeliveryPerson(c *gin.Context) {
// ============================================ // ============================================
// GetAllDeliveryDistances retourne tous les livreurs triés par distance // GetAllDeliveryDistances retourne tous les livreurs triés par distance
// POST /api/v2/admin/protected/delivery/distances
// Body: {"address": "123 Main St"} ou {"latitude": 48.8566, "longitude": 2.3522}
func GetAllDeliveryDistances(c *gin.Context) { func GetAllDeliveryDistances(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
geoService := c.MustGet("geoService").(*services.GeoService) geoService := c.MustGet("geoService").(*services.GeoService)
+48
View File
@@ -6,9 +6,11 @@
package handlers package handlers
import ( import (
"fmt"
"gestion/db" "gestion/db"
"log" "log"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -127,3 +129,49 @@ func GetCommandNavigationLinks(c *gin.Context) {
"navigation_links": links, "navigation_links": links,
}) })
} }
// GetLivreurNavLink retourne le lien Waze App pour une livraison assignée au livreur connecté
// GET /api/v1/livreur/deliveries/:id/nav-link
func GetLivreurNavLink(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
username := c.GetString("username")
commandID, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"})
return
}
command, err := database.GetCommandByID(commandID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
return
}
livreurAssign, _ := command["livreur_assign"].(string)
if livreurAssign != username {
c.JSON(http.StatusForbidden, gin.H{"error": "Cette commande ne vous est pas assignée"})
return
}
// Priorité : coordonnées GPS de la destination
var wazeLink string
destLat, hasLat := command["dest_latitude"].(float64)
destLon, hasLon := command["dest_longitude"].(float64)
if hasLat && hasLon && destLat != 0 && destLon != 0 {
wazeLink = fmt.Sprintf("waze://?ll=%.6f,%.6f&navigate=yes", destLat, destLon)
log.Printf("🗺️ [NAV_LINK] Lien coords pour cmd %d: %s", commandID, wazeLink)
} else if adresse, _ := command["adresse"].(string); adresse != "" {
wazeLink = fmt.Sprintf("waze://?q=%s&navigate=yes", url.QueryEscape(adresse))
log.Printf("🗺️ [NAV_LINK] Lien adresse pour cmd %d: %s", commandID, wazeLink)
} else {
c.JSON(http.StatusNotFound, gin.H{"error": "Aucune destination disponible pour cette commande"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"command_id": commandID,
"waze_app": wazeLink,
})
}
+2 -2
View File
@@ -83,7 +83,7 @@ func GetMyCompletedOrders(c *gin.Context) {
response["client_stats"] = gin.H{ response["client_stats"] = gin.H{
"username": client.Username, "username": client.Username,
"total_commands": client.Command, "total_commands": client.Command,
"points": client.Point, "points_extra": client.PointsExtra,
"pool_points": poolPoints, "pool_points": poolPoints,
"pool_names": poolNames, "pool_names": poolNames,
"penalties": client.Amende, "penalties": client.Amende,
@@ -169,7 +169,7 @@ func GetMyCompletedOrdersWithItems(c *gin.Context) {
"prenom": client.Prenom, "prenom": client.Prenom,
"telephone": client.Telephone, "telephone": client.Telephone,
"total_commands": client.Command, "total_commands": client.Command,
"points": client.Point, "points_extra": client.PointsExtra,
"penalties": client.Amende, "penalties": client.Amende,
} }
} }
-182
View File
@@ -9,54 +9,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// RegisterPushToken enregistre le push token Expo d'un client
// POST /api/v1/push-token
func RegisterPushToken(c *gin.Context) {
clientID := c.GetInt("client_id")
if clientID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
var req struct {
PushToken string `json:"push_token" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.SaveClientPushToken(clientID, req.PushToken); err != nil {
log.Printf("❌ [PUSH_TOKEN] Erreur sauvegarde token pour client %d: %v", clientID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"})
return
}
log.Printf("✅ [PUSH_TOKEN] Token enregistré pour client %d", clientID)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// UnregisterPushToken supprime le push token d'un client (au logout)
// DELETE /api/v1/push-token
func UnregisterPushToken(c *gin.Context) {
clientID := c.GetInt("client_id")
if clientID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteClientPushToken(clientID); err != nil {
log.Printf("❌ [PUSH_TOKEN] Erreur suppression token pour client %d: %v", clientID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"})
return
}
log.Printf("✅ [PUSH_TOKEN] Token supprimé pour client %d", clientID)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// GetClientNotifications retourne les notifications du client connecté // GetClientNotifications retourne les notifications du client connecté
// GET /api/v1/notifications // GET /api/v1/notifications
func GetClientNotifications(c *gin.Context) { func GetClientNotifications(c *gin.Context) {
@@ -107,54 +59,6 @@ func GetClientNotifications(c *gin.Context) {
}) })
} }
// RegisterLivreurPushToken enregistre le push token Expo d'un livreur
// POST /api/v1/livreur/push-token
func RegisterLivreurPushToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
var req struct {
PushToken string `json:"push_token" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.SaveUserPushToken(username, req.PushToken); err != nil {
log.Printf("❌ [LIVREUR_PUSH_TOKEN] Erreur sauvegarde token pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"})
return
}
log.Printf("✅ [LIVREUR_PUSH_TOKEN] Token enregistré pour livreur %s", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// UnregisterLivreurPushToken supprime le push token d'un livreur (au logout)
// DELETE /api/v1/livreur/push-token
func UnregisterLivreurPushToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteUserPushToken(username); err != nil {
log.Printf("❌ [LIVREUR_PUSH_TOKEN] Erreur suppression token pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"})
return
}
log.Printf("✅ [LIVREUR_PUSH_TOKEN] Token supprimé pour livreur %s", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// GetLivreurNotifications retourne les notifications du livreur connecté // GetLivreurNotifications retourne les notifications du livreur connecté
// GET /api/v1/livreur/notifications // GET /api/v1/livreur/notifications
func GetLivreurNotifications(c *gin.Context) { func GetLivreurNotifications(c *gin.Context) {
@@ -245,92 +149,6 @@ func MarkLivreurNotificationsRead(c *gin.Context) {
}) })
} }
// RegisterAdminPushToken enregistre le push token d'un admin
// POST /api/v2/admin/protected/push-token
func RegisterAdminPushToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
var req struct {
PushToken string `json:"push_token" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.SaveUserPushToken(username, req.PushToken); err != nil {
log.Printf("❌ [ADMIN_PUSH_TOKEN] Erreur sauvegarde pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"})
return
}
log.Printf("✅ [ADMIN_PUSH_TOKEN] Token enregistré pour admin %s", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// UnregisterAdminPushToken supprime le push token d'un admin (au logout)
// DELETE /api/v2/admin/protected/push-token
func UnregisterAdminPushToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteUserPushToken(username); err != nil {
log.Printf("❌ [ADMIN_PUSH_TOKEN] Erreur suppression pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"})
return
}
log.Printf("✅ [ADMIN_PUSH_TOKEN] Token supprimé pour admin %s", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// RegisterCabinePushToken enregistre le push token d'un agent cabine
// POST /api/v1/cabine/push-token
func RegisterCabinePushToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
var req struct {
PushToken string `json:"push_token" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.SaveUserPushToken(username, req.PushToken); err != nil {
log.Printf("❌ [CABINE_PUSH_TOKEN] Erreur sauvegarde pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"})
return
}
log.Printf("✅ [CABINE_PUSH_TOKEN] Token enregistré pour cabine %s", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// UnregisterCabinePushToken supprime le push token d'un agent cabine (au logout)
// DELETE /api/v1/cabine/push-token
func UnregisterCabinePushToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteUserPushToken(username); err != nil {
log.Printf("❌ [CABINE_PUSH_TOKEN] Erreur suppression pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"})
return
}
log.Printf("✅ [CABINE_PUSH_TOKEN] Token supprimé pour cabine %s", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// MarkNotificationsRead marque toutes les notifications comme lues // MarkNotificationsRead marque toutes les notifications comme lues
// POST /api/v1/notifications/read // POST /api/v1/notifications/read
func MarkNotificationsRead(c *gin.Context) { func MarkNotificationsRead(c *gin.Context) {
+12 -11
View File
@@ -310,6 +310,12 @@ func ValidateBasket(c *gin.Context) {
} }
req.DeliveryAddress = cmd.DeliveryAddress req.DeliveryAddress = cmd.DeliveryAddress
// Vérifier que le client a lié son compte Telegram
if _, linked, err := database.GetClientTelegramChatID(usernameStr); err != nil || !linked {
c.JSON(http.StatusForbidden, gin.H{"error": "Vous devez lier votre compte Telegram avant de commander"})
return
}
log.Printf("🛒 [CHECKOUT] Début checkout pour: %s", usernameStr) log.Printf("🛒 [CHECKOUT] Début checkout pour: %s", usernameStr)
// ============================================ // ============================================
@@ -430,7 +436,7 @@ func ValidateBasket(c *gin.Context) {
command, err := database.CreateCommandWithAddress(usernameStr, req.DeliveryAddress) command, err := database.CreateCommandWithAddress(usernameStr, req.DeliveryAddress)
if err != nil { if err != nil {
if referralUsed > 0 { if referralUsed > 0 {
_ = database.RestoreReferralBalance(usernameStr, referralUsed) _ = database.CreditClientReferral(usernameStr, referralUsed)
} }
log.Printf("❌ [CHECKOUT] Erreur création commande: %v", err) log.Printf("❌ [CHECKOUT] Erreur création commande: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création commande"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création commande"})
@@ -464,7 +470,7 @@ func ValidateBasket(c *gin.Context) {
// Annuler la commande et restaurer le panier / parrainage // Annuler la commande et restaurer le panier / parrainage
_ = database.CancelCryptoCommand(commandID) _ = database.CancelCryptoCommand(commandID)
if referralUsed > 0 { if referralUsed > 0 {
_ = database.RestoreReferralBalance(usernameStr, referralUsed) _ = database.CreditClientReferral(usernameStr, referralUsed)
} }
log.Printf("❌ [CHECKOUT] Erreur création paiement NowPayments: %v", err) log.Printf("❌ [CHECKOUT] Erreur création paiement NowPayments: %v", err)
c.JSON(http.StatusBadGateway, gin.H{"error": "Impossible d'initier le paiement crypto"}) c.JSON(http.StatusBadGateway, gin.H{"error": "Impossible d'initier le paiement crypto"})
@@ -520,14 +526,9 @@ func ValidateBasket(c *gin.Context) {
if err == nil { if err == nil {
log.Printf("📍 [CHECKOUT] Adresse géocodée: %.6f,%.6f", location.Latitude, location.Longitude) log.Printf("📍 [CHECKOUT] Adresse géocodée: %.6f,%.6f", location.Latitude, location.Longitude)
activeLivreurs, err := database.GetAllActiveDeliveryPersons() usernames, errEligible := database.GetEligibleDeliverymenForCommand(commandID)
if err == nil && len(activeLivreurs) > 0 { if errEligible == nil && len(usernames) > 0 {
log.Printf("🚚 [CHECKOUT] %d livreurs actifs disponibles", len(activeLivreurs)) log.Printf("🚚 [CHECKOUT] %d livreur(s) éligible(s) disponibles", len(usernames))
usernames := make([]string, len(activeLivreurs))
for i, livreur := range activeLivreurs {
usernames[i] = livreur.Username
}
nearest, err := geoService.FindNearestDeliveryPersonFast(services.Coordinates{ nearest, err := geoService.FindNearestDeliveryPersonFast(services.Coordinates{
Latitude: location.Latitude, Latitude: location.Latitude,
@@ -599,7 +600,7 @@ func ValidateBasket(c *gin.Context) {
log.Printf("⚠️ [CHECKOUT] Aucun livreur trouvé: %v", err) log.Printf("⚠️ [CHECKOUT] Aucun livreur trouvé: %v", err)
} }
} else { } else {
log.Printf("⚠️ [CHECKOUT] Aucun livreur actif disponible") log.Printf("⚠️ [CHECKOUT] Aucun livreur éligible disponible")
} }
} else { } else {
log.Printf("⚠️ [CHECKOUT] Erreur géocodage: %v", err) log.Printf("⚠️ [CHECKOUT] Erreur géocodage: %v", err)
+1 -37
View File
@@ -12,7 +12,6 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/gabriel-vasile/mimetype" "github.com/gabriel-vasile/mimetype"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -654,41 +653,12 @@ func UpdateProduct(c *gin.Context) {
log.Printf("🔄 [UpdateProduct] %s met à jour produit #%d", username, id) log.Printf("🔄 [UpdateProduct] %s met à jour produit #%d", username, id)
// ✅ UPDATE PRODUIT if err := database.UpdateProduct(id, updateData.Name, updateData.Category, updateData.Description, updateData.Unit, updateData.Stock, updateData.Prices); err != nil {
updateQuery := `
UPDATE products
SET name = $1, category = $2, description = $3, stock = $4, unit = $5, updated_at = $6
WHERE id = $7
`
_, err = database.Exec(updateQuery,
updateData.Name,
updateData.Category,
updateData.Description,
updateData.Stock,
updateData.Unit,
time.Now(),
id,
)
if err != nil {
log.Printf("❌ [UpdateProduct] Erreur UPDATE: %v", err) log.Printf("❌ [UpdateProduct] Erreur UPDATE: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise à jour"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise à jour"})
return return
} }
// ✅ UPDATE PRIX
database.Exec(`DELETE FROM product_prices WHERE product_id = $1`, id)
for _, price := range updateData.Prices {
_, err := database.Exec(`
INSERT INTO product_prices (product_id, quantity, price)
VALUES ($1, $2, $3)
`, id, price.Quantity, price.Price)
if err != nil {
log.Printf("❌ [UpdateProduct] Erreur prix: %v", err)
}
}
// ✅ RÉCUPÉRER LE PRODUIT MIS À JOUR // ✅ RÉCUPÉRER LE PRODUIT MIS À JOUR
updatedProduct, _ := database.GetProductByID(id) updatedProduct, _ := database.GetProductByID(id)
media, _ := database.GetMediaByProductID(id) media, _ := database.GetMediaByProductID(id)
@@ -702,10 +672,6 @@ func UpdateProduct(c *gin.Context) {
}) })
} }
// ============================================
// DELETE MEDIA - VERSION SÉCURISÉE
// ============================================
func DeleteMedia(c *gin.Context) { func DeleteMedia(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
@@ -756,8 +722,6 @@ func DeleteMedia(c *gin.Context) {
}) })
} }
// handlers/product_handlers_SECURED.go
func UploadMedia(c *gin.Context) { func UploadMedia(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
+1 -1
View File
@@ -962,7 +962,7 @@ func ResetClientPointAdmin(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
extraPoolKey := "" extraPoolKey := ""
if req.Pool >= 2 { if req.Pool >= 0 {
if settings, err := database.GetSettings(); err == nil && req.Pool < len(settings.PointsPools) { if settings, err := database.GetSettings(); err == nil && req.Pool < len(settings.PointsPools) {
extraPoolKey = settings.PointsPools[req.Pool].Key extraPoolKey = settings.PointsPools[req.Pool].Key
} }
+18 -2
View File
@@ -2,8 +2,11 @@ package handlers
import ( import (
"gestion/db" "gestion/db"
"gestion/models"
"gestion/services"
"log" "log"
"net/http" "net/http"
"os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -60,7 +63,7 @@ func GetSettings(c *gin.Context) {
func UpdateSettings(c *gin.Context) { func UpdateSettings(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
var req db.AppSettings var req models.AppSettings
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Paramètres invalides"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Paramètres invalides"})
return return
@@ -72,7 +75,20 @@ func UpdateSettings(c *gin.Context) {
return return
} }
log.Printf("✅ [SETTINGS] Mise à jour: penalties=%v, pools=%d", req.PenaltiesEnabled, len(req.PointsPools)) // Recharger le service Telegram si le token/username a changé
if services.TelegramBot != nil {
services.TelegramBot.Reload(req.TelegramBotToken, req.TelegramBotUsername)
if req.TelegramBotToken != "" {
log.Printf("✅ [SETTINGS] Service Telegram rechargé (username: %s)", req.TelegramBotUsername)
if webhookURL := os.Getenv("TELEGRAM_WEBHOOK_URL"); webhookURL != "" {
if err := services.TelegramBot.SetWebhook(webhookURL); err != nil {
log.Printf("⚠️ [SETTINGS] Erreur enregistrement webhook Telegram: %v", err)
} else {
log.Printf("✅ [SETTINGS] Webhook Telegram enregistré: %s", webhookURL)
}
}
}
}
c.JSON(http.StatusOK, gin.H{"success": true, "settings": req}) c.JSON(http.StatusOK, gin.H{"success": true, "settings": req})
} }
+292
View File
@@ -0,0 +1,292 @@
package handlers
import (
"gestion/db"
"gestion/models"
"gestion/services"
"log"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func TelegramWebhook(c *gin.Context) {
// Vérification du secret webhook
secret := c.GetHeader("X-Telegram-Bot-Api-Secret-Token")
if services.TelegramBot == nil || !services.TelegramBot.ValidateWebhookSecret(secret) {
c.Status(http.StatusUnauthorized)
return
}
var update models.TgUpdate
if err := c.ShouldBindJSON(&update); err != nil {
c.Status(http.StatusBadRequest)
return
}
if update.Message == nil {
c.Status(http.StatusOK)
return
}
text := strings.TrimSpace(update.Message.Text)
chatID := update.Message.Chat.ID
// Commande /start <token> — liaison de compte
if token, ok := strings.CutPrefix(text, "/start "); ok {
token = strings.TrimSpace(token)
handleLinkAccount(c, token, chatID)
return
}
// Commande /start sans token — message d'accueil
if text == "/start" {
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID,
"👋 <b>Bienvenue !</b>\n\nPour lier votre compte, générez un token depuis l'application et envoyez <code>/start &lt;token&gt;</code>.")
}
}
c.Status(http.StatusOK)
}
func handleLinkAccount(c *gin.Context, token string, chatID int64) {
if token == "" {
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID, "❌ Token manquant. Générez un nouveau token depuis l'application.")
}
c.Status(http.StatusOK)
return
}
database := c.MustGet("database").(*db.Database)
username, role, err := db.ValidateAndConsumeLinkToken(token)
if err != nil {
log.Printf("⚠️ [TELEGRAM_LINK] Token invalide: %v", err)
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID, "❌ Token invalide ou expiré. Générez un nouveau token depuis l'application.")
}
c.Status(http.StatusOK)
return
}
// Enregistrer le chat_id selon le rôle
var saveErr error
switch role {
case "client":
saveErr = database.SaveClientTelegramChatID(username, chatID)
default:
saveErr = database.SaveUserTelegramChatID(username, chatID)
}
if saveErr != nil {
log.Printf("❌ [TELEGRAM_LINK] Erreur sauvegarde chat_id pour %s (%s): %v", username, role, saveErr)
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID, "❌ Une erreur est survenue. Réessayez.")
}
c.Status(http.StatusOK)
return
}
log.Printf("✅ [TELEGRAM_LINK] Compte %s (%s) lié au chat_id %d", username, role, chatID)
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID,
"✅ <b>Compte lié avec succès !</b>\n\nVous recevrez désormais vos notifications ici.")
}
c.Status(http.StatusOK)
}
func GenerateClientLinkToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"})
return
}
token, err := db.GenerateLinkToken(username, "client")
if err != nil {
log.Printf("❌ [TELEGRAM] Erreur génération token pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
"message": "/start " + token,
"expires_in": 600,
})
}
func GenerateLivreurLinkToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"})
return
}
token, err := db.GenerateLinkToken(username, "livreur")
if err != nil {
log.Printf("❌ [TELEGRAM] Erreur génération token pour livreur %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
"message": "/start " + token,
"expires_in": 600,
})
}
func GenerateAdminLinkToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"})
return
}
// Récupérer le rôle réel depuis la DB
database := c.MustGet("database").(*db.Database)
user, err := database.GetUserByUsername(username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Utilisateur introuvable"})
return
}
token, err := db.GenerateLinkToken(username, user.Role)
if err != nil {
log.Printf("❌ [TELEGRAM] Erreur génération token pour admin %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
"message": "/start " + token,
"expires_in": 600,
})
}
// GET /api/v1/telegram/status
func GetClientTelegramStatus(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
_, linked, err := database.GetClientTelegramChatID(username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur vérification"})
return
}
c.JSON(http.StatusOK, gin.H{
"linked": linked,
"enabled": services.TelegramBot != nil && services.TelegramBot.IsConfigured(),
})
}
// GET /api/v1/livreur/telegram/status
func GetLivreurTelegramStatus(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
_, linked, err := database.GetUserTelegramChatID(username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur vérification"})
return
}
c.JSON(http.StatusOK, gin.H{
"linked": linked,
"enabled": services.TelegramBot != nil && services.TelegramBot.IsConfigured(),
})
}
// ============================================
// DÉLIAISON TELEGRAM
// ============================================
// DELETE /api/v1/telegram/unlink
func UnlinkClientTelegram(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteClientTelegramChatID(username); err != nil {
log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur déliaison"})
return
}
log.Printf("✅ [TELEGRAM_UNLINK] Compte client %s délié", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// DELETE /api/v1/livreur/telegram/unlink
func UnlinkLivreurTelegram(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteUserTelegramChatID(username); err != nil {
log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur déliaison"})
return
}
log.Printf("✅ [TELEGRAM_UNLINK] Compte livreur %s délié", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// DELETE /api/v2/admin/protected/telegram/unlink
func UnlinkAdminTelegram(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteUserTelegramChatID(username); err != nil {
log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur déliaison"})
return
}
log.Printf("✅ [TELEGRAM_UNLINK] Compte admin %s délié", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
+10 -28
View File
@@ -16,6 +16,7 @@ import (
"gestion/db" "gestion/db"
"gestion/models" "gestion/models"
"gestion/utils"
) )
// ============================================ // ============================================
@@ -99,12 +100,12 @@ func UpdateMyProfile(c *gin.Context) {
// Mise à jour du téléphone // Mise à jour du téléphone
if req.Telephone != "" && req.Telephone != client.Telephone { if req.Telephone != "" && req.Telephone != client.Telephone {
if !validatePhoneNumber(req.Telephone) { if !utils.ValidatePhoneNumber(req.Telephone) {
log.Printf("❌ [UPDATE_MY_PROFILE] Téléphone invalide: %s", req.Telephone) log.Printf("❌ [UPDATE_MY_PROFILE] Téléphone invalide: %s", req.Telephone)
c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
return return
} }
normalizedPhone := normalizePhoneNumber(req.Telephone) normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
// Vérifier que le téléphone n'est pas déjà utilisé // Vérifier que le téléphone n'est pas déjà utilisé
if existingClient, _ := database.GetClientByTelephone(normalizedPhone); existingClient != nil && existingClient.ID != clientID { if existingClient, _ := database.GetClientByTelephone(normalizedPhone); existingClient != nil && existingClient.ID != clientID {
@@ -202,8 +203,8 @@ func UpdateClientByAdmin(c *gin.Context) {
} }
// ✅ LOG DEBUG - État initial // ✅ LOG DEBUG - État initial
log.Printf("📊 [UPDATE_CLIENT_ADMIN] État initial - Command: %d, Point: %d, PointZipette: %d, Amende: %.2f", log.Printf("📊 [UPDATE_CLIENT_ADMIN] État initial - Command: %d, Amende: %.2f",
client.Command, client.Point, client.PointZipette, client.Amende) client.Command, client.Amende)
// Vérifier si des modifications sont demandées // Vérifier si des modifications sont demandées
hasChanges := false hasChanges := false
@@ -254,12 +255,12 @@ func UpdateClientByAdmin(c *gin.Context) {
// Mise à jour du téléphone // Mise à jour du téléphone
if req.Telephone != "" && req.Telephone != client.Telephone { if req.Telephone != "" && req.Telephone != client.Telephone {
if !validatePhoneNumber(req.Telephone) { if !utils.ValidatePhoneNumber(req.Telephone) {
log.Printf("❌ [UPDATE_CLIENT_ADMIN] Téléphone invalide: %s", req.Telephone) log.Printf("❌ [UPDATE_CLIENT_ADMIN] Téléphone invalide: %s", req.Telephone)
c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
return return
} }
normalizedPhone := normalizePhoneNumber(req.Telephone) normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
// Vérifier que le téléphone n'est pas déjà utilisé // Vérifier que le téléphone n'est pas déjà utilisé
if existingClient, _ := database.GetClientByTelephone(normalizedPhone); existingClient != nil && existingClient.ID != clientID { if existingClient, _ := database.GetClientByTelephone(normalizedPhone); existingClient != nil && existingClient.ID != clientID {
@@ -272,29 +273,12 @@ func UpdateClientByAdmin(c *gin.Context) {
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Téléphone modifié: %s", normalizedPhone) log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Téléphone modifié: %s", normalizedPhone)
} }
// ✅ CORRECTION: Mise à jour du compteur de commandes (Admin uniquement)
// Vérifier explicitement si le champ est présent (même si valeur = 0)
if req.Command != nil && *req.Command != client.Command { if req.Command != nil && *req.Command != client.Command {
client.Command = *req.Command client.Command = *req.Command
hasChanges = true hasChanges = true
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Commandes modifiées: %d → %d", client.Command, *req.Command) log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Commandes modifiées: %d → %d", client.Command, *req.Command)
} }
// ✅ CORRECTION: Mise à jour des points weed/hash (Admin uniquement)
if req.Point != nil && *req.Point != client.Point {
client.Point = *req.Point
hasChanges = true
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Points Weed modifiés: %d → %d", client.Point, *req.Point)
}
// ✅ CORRECTION CRITIQUE: Mise à jour des points zipette (Admin uniquement)
if req.PointsZipette != nil && *req.PointsZipette != client.PointZipette {
client.PointZipette = *req.PointsZipette
hasChanges = true
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Points Zipette modifiés: %d → %d", client.PointZipette, *req.PointsZipette)
}
// ✅ CORRECTION: Mise à jour des amendes (Admin uniquement)
if req.Amende != nil && *req.Amende != client.Amende { if req.Amende != nil && *req.Amende != client.Amende {
client.Amende = *req.Amende client.Amende = *req.Amende
hasChanges = true hasChanges = true
@@ -310,9 +294,8 @@ func UpdateClientByAdmin(c *gin.Context) {
return return
} }
// ✅ LOG DEBUG - État avant sauvegarde log.Printf("📊 [UPDATE_CLIENT_ADMIN] État avant save - Command: %d, Amende: %.2f",
log.Printf("📊 [UPDATE_CLIENT_ADMIN] État avant save - Command: %d, Point: %d, PointZipette: %d, Amende: %.2f", client.Command, client.Amende)
client.Command, client.Point, client.PointZipette, client.Amende)
// Sauvegarder les modifications // Sauvegarder les modifications
if err := database.UpdateClient(client); err != nil { if err := database.UpdateClient(client); err != nil {
@@ -452,8 +435,7 @@ func sanitizeClient(client *models.Client) gin.H {
"prenom": client.Prenom, "prenom": client.Prenom,
"telephone": client.Telephone, "telephone": client.Telephone,
"command": client.Command, "command": client.Command,
"point": client.Point, "points_extra": client.PointsExtra,
"points_zipette": client.PointZipette, // ✅ AJOUTÉ
"amende": client.Amende, "amende": client.Amende,
} }
} }
@@ -352,6 +352,12 @@ func StartDelivery(c *gin.Context) {
fmt.Sprintf("Livraison démarrée par %s", usernameStr), fmt.Sprintf("Livraison démarrée par %s", usernameStr),
usernameStr) usernameStr)
// Notifier le client
if clientUsername, _ := command["username"].(string); clientUsername != "" {
msg := fmt.Sprintf("🛵 Votre commande #%d est en route !", commandID)
database.NotifyClient(clientUsername, commandID, "en_route", msg)
}
log.Printf("✅ [START] Livraison %d démarrée", commandID) log.Printf("✅ [START] Livraison %d démarrée", commandID)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
+2 -2
View File
@@ -1,7 +1,7 @@
package handlers package handlers
import ( import (
"gestion/db" "gestion/models"
"regexp" "regexp"
) )
@@ -37,7 +37,7 @@ type zoneCheckResult struct {
// Les zones sont lues depuis la DB (settings.PostalZones). // Les zones sont lues depuis la DB (settings.PostalZones).
// Code postal introuvable → OK = false (refus). // Code postal introuvable → OK = false (refus).
// Code postal hors de toutes les zones → OK = false (refus). // Code postal hors de toutes les zones → OK = false (refus).
func checkDeliveryZone(deliveryAddress string, total float64, zones []db.PostalZone) zoneCheckResult { func checkDeliveryZone(deliveryAddress string, total float64, zones []models.PostalZone) zoneCheckResult {
code := extractPostalCode(deliveryAddress) code := extractPostalCode(deliveryAddress)
if code == "" { if code == "" {
return zoneCheckResult{PostalCode: "", ZoneName: "inconnue", MinAmount: 0, OK: false} return zoneCheckResult{PostalCode: "", ZoneName: "inconnue", MinAmount: 0, OK: false}
+30 -68
View File
@@ -47,6 +47,36 @@ func main() {
geoService := services.NewGeoService(db.Redis, db.RedisCtx) geoService := services.NewGeoService(db.Redis, db.RedisCtx)
log.Println("✅ Service de géolocalisation initialisé") log.Println("✅ Service de géolocalisation initialisé")
// Initialisation du service Telegram
telegramService := services.NewTelegramService()
if telegramService.IsConfigured() {
log.Println("✅ Service Telegram initialisé")
if webhookURL := os.Getenv("TELEGRAM_WEBHOOK_URL"); webhookURL != "" {
if err := telegramService.SetWebhook(webhookURL); err != nil {
log.Printf("⚠️ [TELEGRAM] Erreur enregistrement webhook: %v", err)
}
}
} else {
log.Println("️ Service Telegram désactivé (TELEGRAM_BOT_TOKEN non défini)")
}
// Migration: ajout des colonnes telegram_chat_id
database.MigrateAddTelegramColumns()
// Priorité DB > .env pour la config Telegram
if dbSettings, err := database.GetSettings(); err == nil {
telegramService.Reload(dbSettings.TelegramBotToken, dbSettings.TelegramBotUsername)
if dbSettings.TelegramBotToken != "" {
log.Printf("✅ [TELEGRAM] Config chargée depuis la DB (username: %s)", dbSettings.TelegramBotUsername)
// Enregistrer le webhook si l'URL est configurée (et pas déjà fait depuis l'env)
if webhookURL := os.Getenv("TELEGRAM_WEBHOOK_URL"); webhookURL != "" {
if err := telegramService.SetWebhook(webhookURL); err != nil {
log.Printf("⚠️ [TELEGRAM] Erreur enregistrement webhook (DB reload): %v", err)
}
}
}
}
// ============================================ // ============================================
// 🧹 NETTOYAGE INITIAL DES COMMANDES INVALIDES // 🧹 NETTOYAGE INITIAL DES COMMANDES INVALIDES
// ============================================ // ============================================
@@ -128,7 +158,6 @@ func main() {
c.Next() c.Next()
}) })
// Middleware NowPayments : injecte le client si crypto activé dans les settings
r.Use(func(c *gin.Context) { r.Use(func(c *gin.Context) {
settings, err := database.GetSettings() settings, err := database.GetSettings()
if err == nil && settings.CryptoPaymentEnabled && settings.NowPaymentsAPIKey != "" { if err == nil && settings.CryptoPaymentEnabled && settings.NowPaymentsAPIKey != "" {
@@ -146,74 +175,7 @@ func main() {
// ============================================ // ============================================
routes.SetupRoutes(r, database, geoService) routes.SetupRoutes(r, database, geoService)
// ============================================
// LANCEMENT DU SERVEUR
// ============================================
printServerInfo()
if err := r.Run(":8080"); err != nil { if err := r.Run(":8080"); err != nil {
log.Fatalf("❌ Erreur au lancement du serveur : %v", err) log.Fatalf("❌ Erreur au lancement du serveur : %v", err)
} }
} }
// printServerInfo affiche les infos du serveur au démarrage
func printServerInfo() {
log.Println("")
log.Println("═══════════════════════════════════════════════════════════")
log.Println("🚀 Serveur lancé sur http://localhost:8080")
log.Println("═══════════════════════════════════════════════════════════")
log.Println("")
log.Println("📡 API Endpoints disponibles:")
log.Println("")
log.Println(" 🌍 PUBLIC:")
log.Println(" GET /api/v1/health - Health check")
log.Println(" GET /api/v1/products - Liste produits")
log.Println("")
log.Println(" 👤 CLIENTS:")
log.Println(" POST /api/v1/auth/register - Inscription")
log.Println(" POST /api/v1/auth/login - Connexion")
log.Println(" POST /api/v1/checkout - ✅ PASSER COMMANDE (avec auto-assign)")
log.Println(" GET /api/v1/my-commands - Mes commandes avec suivi")
log.Println(" GET /api/v1/commands/:id/status - ✅ Statut temps réel")
log.Println(" GET /api/v1/commands/:id/tracking - ✅ Suivi détaillé")
log.Println(" POST /api/v1/commands/:id/approve - Confirmer livraison")
log.Println("")
log.Println(" 🚗 LIVREURS:")
log.Println(" GET /api/v1/livreur/deliveries - ✅ Mes livraisons (SANS téléphone client)")
log.Println(" GET /api/v1/livreur/deliveries/:id - ✅ Détail livraison filtrée")
log.Println(" PUT /api/v1/livreur/deliveries/:id/status - MAJ statut (avec GPS)")
log.Println(" POST /api/v1/livreur/location/update - MAJ position GPS")
log.Println(" GET /api/v1/livreur/queue - Ma queue de livraisons")
log.Println("")
log.Println(" 🔐 ADMIN:")
log.Println(" POST /api/v2/admin/auth/login - Connexion admin")
log.Println(" GET /api/v2/admin/protected/orders - Toutes les commandes")
log.Println(" POST /api/v2/admin/protected/orders/:id/auto-assign - ✅ Auto-assign GPS")
log.Println(" POST /api/v2/admin/protected/commands/auto-assign-all - ✅ Assigner toutes")
log.Println(" GET /api/v2/admin/protected/delivery/queues - Queues livreurs")
log.Println("")
log.Println("⏰ Workers & Services actifs:")
log.Println(" - ✅ AutoAssignmentCron (5min) - Assignation backup")
log.Println(" - ✅ QueueCleanupScheduler (5min) - 🧹 Nettoyage commandes invalides")
log.Println(" - ✅ NotificationWorker (30s) - Notifications ETA")
log.Println(" - ✅ StockCleanupWorker (5min) - Nettoyage stock")
log.Println("")
log.Println("🎯 Fonctionnalités activées:")
log.Println(" 1. ✅ Auto-assignation au checkout (immédiate)")
log.Println(" 2. ✅ Cron job backup (toutes les 5 min)")
log.Println(" 3. ✅ Validation stricte avant ajout à la queue")
log.Println(" 4. ✅ Nettoyage automatique des commandes invalides")
log.Println(" 5. ✅ Vue livreur filtrée (sans téléphone)")
log.Println(" 6. ✅ Vue client avec suivi temps réel")
log.Println(" 7. ✅ Gestion automatique statut BUSY (queue >= 10)")
log.Println("")
log.Println("🔒 Règles de validation automatique:")
log.Println(" - Username non vide ✓")
log.Println(" - Adresse de livraison non vide ✓")
log.Println(" - Coordonnées GPS valides (lat != 0, lng != 0) ✓")
log.Println(" - Date de création valide ✓")
log.Println(" - Prix total > 0 (recommandé) ✓")
log.Println("")
log.Println("═══════════════════════════════════════════════════════════")
log.Println("")
}
@@ -3,6 +3,7 @@ package middleware
import ( import (
"fmt" "fmt"
"gestion/db" "gestion/db"
"gestion/models"
"log" "log"
"net/http" "net/http"
"time" "time"
@@ -30,7 +31,7 @@ func OrderHoursMiddleware(c *gin.Context) {
} }
sched := settings.DeliverySchedule sched := settings.DeliverySchedule
var day db.DaySchedule var day models.DaySchedule
switch weekday { switch weekday {
case time.Monday: case time.Monday:
day = sched.Monday day = sched.Monday
+54
View File
@@ -0,0 +1,54 @@
package models
import "github.com/golang-jwt/jwt/v5"
type ClientClaims struct {
ClientID int `json:"client_id"`
Username string `json:"username"`
Role string `json:"role"`
SessionID string `json:"session_id"`
jwt.RegisteredClaims
}
type AdminClaims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
SessionID string `json:"session_id"`
jwt.RegisteredClaims
}
// ============================================
// STRUCTURES REQUÊTE / RÉPONSE
// ============================================
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type RegisterClientRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Password string `json:"password" binding:"required,min=8"`
Nom string `json:"nom" binding:"required,min=2,max=100"`
Prenom string `json:"prenom" binding:"required,min=2,max=100"`
Telephone string `json:"telephone" binding:"required"`
}
type RegisterAdminRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Password string `json:"password" binding:"required,min=8"`
Role string `json:"role" binding:"required,oneof=admin cabine livreur"`
}
type LoginResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
User any `json:"user"`
}
type ProfileResponse struct {
Username string `json:"username"`
Role string `json:"role"`
}
+1 -3
View File
@@ -11,9 +11,7 @@ type Client struct {
Prenom string `json:"prenom"` Prenom string `json:"prenom"`
Telephone string `json:"telephone"` Telephone string `json:"telephone"`
Command int `json:"command"` Command int `json:"command"`
Point int `json:"point"` PointsExtra map[string]int `json:"points_extra"`
PointZipette int `json:"points_zipette"`
PointsExtra map[string]int `json:"points_extra"` // pools[2+]
Amende float64 `json:"amende"` Amende float64 `json:"amende"`
CancellationsCount int `json:"cancellations_count"` CancellationsCount int `json:"cancellations_count"`
LastPenaltyReason string `json:"last_penalty_reason"` LastPenaltyReason string `json:"last_penalty_reason"`
+80
View File
@@ -0,0 +1,80 @@
package models
type PostalZone struct {
Name string `json:"name"`
MinAmount float64 `json:"min_amount"`
Codes []string `json:"codes"`
}
// PointsTier représente un palier du barème de points
// Si Max == 0, il n'y a pas de borne supérieure (illimité)
type PointsTier struct {
Min float64 `json:"min"`
Max float64 `json:"max"` // 0 = illimité
Points int `json:"points"`
}
// DaySchedule représente les horaires de livraison pour un jour de la semaine
type DaySchedule struct {
Enabled bool `json:"enabled"`
OpenTime string `json:"open_time"` // ex: "09:00"
CloseTime string `json:"close_time"` // ex: "20:00"
}
// DeliverySchedule représente les horaires de livraison pour chaque jour
type DeliverySchedule struct {
Monday DaySchedule `json:"monday"`
Tuesday DaySchedule `json:"tuesday"`
Wednesday DaySchedule `json:"wednesday"`
Thursday DaySchedule `json:"thursday"`
Friday DaySchedule `json:"friday"`
Saturday DaySchedule `json:"saturday"`
Sunday DaySchedule `json:"sunday"`
}
type PointsPool struct {
Key string `json:"key"` // identifiant interne (ex: "pool_0")
Name string `json:"name"` // nom affiché (ex: "Cannabis", "Accessoires")
Categories []string `json:"categories"` // catégories de produits assignées à ce pool
Tiers []PointsTier `json:"tiers"` // barème de points
}
// PenaltyTier représente un palier d'amende : à partir de MinCancel annulations, Amount est appliqué
type PenaltyTier struct {
MinCancel int `json:"min_cancel"` // nombre d'annulations à partir duquel ce palier s'applique
Amount int `json:"amount"` // montant de l'amende
}
// CategoryRoute associe un livreur à une ou plusieurs catégories de produits
type CategoryRoute struct {
DeliverymanUsername string `json:"deliveryman_username"`
Categories []string `json:"categories"`
}
// DeliveryModeConfig configure le mode d'assignation des livreurs
// Mode "single" → un seul livreur, toutes catégories confondues
// Mode "category_based" → chaque livreur gère ses catégories dédiées
type DeliveryModeConfig struct {
Mode string `json:"mode"` // "single" | "category_based"
CategoryRoutes []CategoryRoute `json:"category_routes"` // utilisé uniquement en mode category_based
}
// AppSettings contient les paramètres globaux de l'application
type AppSettings struct {
PenaltiesEnabled bool `json:"penalties_enabled"`
ShowAmendeScore bool `json:"show_amende_score"` // afficher le score d'amendes aux clients/cabine
PenaltyTiers []PenaltyTier `json:"penalty_tiers"` // barème des amendes (liste configurable)
PointsEnabled bool `json:"points_enabled"` // afficher/activer le système de points
PointsPools []PointsPool `json:"points_pools"` // types de points personnalisés
ReferralEnabled bool `json:"referral_enabled"` // activer/désactiver le système de parrainage
CryptoPaymentEnabled bool `json:"crypto_payment_enabled"` // activer/désactiver le paiement crypto
CryptoOnly bool `json:"crypto_only"` // forcer le paiement crypto uniquement (pas d'espèces)
NowPaymentsAPIKey string `json:"nowpayments_api_key"` // clé API NowPayments
NowPaymentsIPNSecret string `json:"nowpayments_ipn_secret"` // secret IPN NowPayments
NowPaymentsCurrencies []string `json:"nowpayments_currencies"` // cryptos acceptées (ex: ["btc","eth","ltc"])
DeliverySchedule DeliverySchedule `json:"delivery_schedule"` // horaires de livraison par jour
PostalZones []PostalZone `json:"postal_zones"` // zones de livraison avec minimum de commande
TelegramBotToken string `json:"telegram_bot_token"` // token du bot Telegram (BotFather)
TelegramBotUsername string `json:"telegram_bot_username"` // username du bot (sans @)
DeliveryMode DeliveryModeConfig `json:"delivery_mode"` // mode d'assignation des livreurs
}
+21
View File
@@ -0,0 +1,21 @@
package models
type TgUpdate struct {
UpdateID int `json:"update_id"`
Message *TgMessage `json:"message"`
}
type TgMessage struct {
MessageID int `json:"message_id"`
Chat TgChat `json:"chat"`
Text string `json:"text"`
}
type TgChat struct {
ID int64 `json:"id"`
}
type TelegramLinkData struct {
Username string `json:"username"`
Role string `json:"role"` // "client" | "livreur" | "admin" | "cabine"
}
-2
View File
@@ -21,7 +21,5 @@ type AdminUpdateClientRequest struct {
Prenom string `json:"prenom,omitempty"` Prenom string `json:"prenom,omitempty"`
Telephone string `json:"telephone,omitempty"` Telephone string `json:"telephone,omitempty"`
Command *int `json:"command,omitempty"` Command *int `json:"command,omitempty"`
Point *int `json:"point,omitempty"`
PointsZipette *int `json:"points_zipette,omitempty"`
Amende *float64 `json:"amende,omitempty"` Amende *float64 `json:"amende,omitempty"`
} }
+23 -13
View File
@@ -97,9 +97,10 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
cartGroupV1.GET("/notifications", handlers.GetClientNotifications) cartGroupV1.GET("/notifications", handlers.GetClientNotifications)
cartGroupV1.POST("/notifications/read", handlers.MarkNotificationsRead) cartGroupV1.POST("/notifications/read", handlers.MarkNotificationsRead)
// 📱 PUSH TOKEN // 🤖 TELEGRAM CLIENT
cartGroupV1.POST("/push-token", handlers.RegisterPushToken) cartGroupV1.POST("/telegram/link-token", handlers.GenerateClientLinkToken)
cartGroupV1.DELETE("/push-token", handlers.UnregisterPushToken) cartGroupV1.GET("/telegram/status", handlers.GetClientTelegramStatus)
cartGroupV1.DELETE("/telegram/unlink", handlers.UnlinkClientTelegram)
// 👤 PROFIL CLIENT // 👤 PROFIL CLIENT
cartGroupV1.GET("/profile", handlers.GetMyProfile) // ✅ Récupérer mon profil cartGroupV1.GET("/profile", handlers.GetMyProfile) // ✅ Récupérer mon profil
@@ -117,6 +118,11 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
// ============================================ // ============================================
router.POST("/api/v1/webhooks/nowpayments", handlers.IPNWebhook) router.POST("/api/v1/webhooks/nowpayments", handlers.IPNWebhook)
// ============================================
// 🤖 WEBHOOK TELEGRAM - PUBLIC (sécurisé par secret header)
// ============================================
router.POST("/webhook/telegram", handlers.TelegramWebhook)
// ============================================ // ============================================
// 🌍 GÉOCODAGE PUBLIC (v1) // 🌍 GÉOCODAGE PUBLIC (v1)
// ============================================ // ============================================
@@ -146,14 +152,14 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
adminGroupV2 := router.Group("/api/v2/admin/protected") adminGroupV2 := router.Group("/api/v2/admin/protected")
adminGroupV2.Use(middleware.AdminMiddleware) adminGroupV2.Use(middleware.AdminMiddleware)
{ {
// PUSH TOKEN ADMIN
adminGroupV2.POST("/push-token", handlers.RegisterAdminPushToken)
adminGroupV2.DELETE("/push-token", handlers.UnregisterAdminPushToken)
// 🔔 NOTIFICATIONS ADMIN // 🔔 NOTIFICATIONS ADMIN
adminGroupV2.GET("/notifications", handlers.GetLivreurNotifications) adminGroupV2.GET("/notifications", handlers.GetLivreurNotifications)
adminGroupV2.POST("/notifications/read", handlers.MarkLivreurNotificationsRead) adminGroupV2.POST("/notifications/read", handlers.MarkLivreurNotificationsRead)
// 🤖 TELEGRAM ADMIN
adminGroupV2.POST("/telegram/link-token", handlers.GenerateAdminLinkToken)
adminGroupV2.DELETE("/telegram/unlink", handlers.UnlinkAdminTelegram)
// ============================================ // ============================================
// CLIENT - GESTION // CLIENT - GESTION
// ============================================ // ============================================
@@ -285,14 +291,14 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
cabineGroupV1.POST("/add/address", handlers.AddAddress) cabineGroupV1.POST("/add/address", handlers.AddAddress)
cabineGroupV1.DELETE("/delete/address", handlers.DeleteAddress) cabineGroupV1.DELETE("/delete/address", handlers.DeleteAddress)
cabineGroupV1.GET("/addresses", handlers.GetAllAddress) cabineGroupV1.GET("/addresses", handlers.GetAllAddress)
// PUSH TOKEN CABINE
cabineGroupV1.POST("/push-token", handlers.RegisterCabinePushToken)
cabineGroupV1.DELETE("/push-token", handlers.UnregisterCabinePushToken)
// 🔔 NOTIFICATIONS CABINE // 🔔 NOTIFICATIONS CABINE
cabineGroupV1.GET("/notifications", handlers.GetLivreurNotifications) cabineGroupV1.GET("/notifications", handlers.GetLivreurNotifications)
cabineGroupV1.POST("/notifications/read", handlers.MarkLivreurNotificationsRead) cabineGroupV1.POST("/notifications/read", handlers.MarkLivreurNotificationsRead)
// 🤖 TELEGRAM CABINE
cabineGroupV1.POST("/telegram/link-token", handlers.GenerateAdminLinkToken)
cabineGroupV1.DELETE("/telegram/unlink", handlers.UnlinkAdminTelegram)
cabineGroupV1.GET("/commands/:id/items", handlers.ShowItems) cabineGroupV1.GET("/commands/:id/items", handlers.ShowItems)
cabineGroupV1.POST("/commands/:id/confirm-reception", handlers.StaffApproveDelivery) cabineGroupV1.POST("/commands/:id/confirm-reception", handlers.StaffApproveDelivery)
cabineGroupV1.POST("/commands/:id/assign", handlers.AssignDeliveryPerson) cabineGroupV1.POST("/commands/:id/assign", handlers.AssignDeliveryPerson)
@@ -331,6 +337,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
livreurGroupV1.GET("/deliveries/:id", handlers.GetDeliveryDetails) // ✅ Détail filtré livreurGroupV1.GET("/deliveries/:id", handlers.GetDeliveryDetails) // ✅ Détail filtré
livreurGroupV1.POST("/deliveries/:id/start", handlers.StartDelivery) livreurGroupV1.POST("/deliveries/:id/start", handlers.StartDelivery)
livreurGroupV1.PUT("/deliveries/:id/status", handlers.UpdateDeliveryStatus) // ✅ Avec GPS livreurGroupV1.PUT("/deliveries/:id/status", handlers.UpdateDeliveryStatus) // ✅ Avec GPS
livreurGroupV1.GET("/deliveries/:id/nav-link", handlers.GetLivreurNavLink) // Lien Waze App
// ============================================ // ============================================
// POSITION GPS // POSITION GPS
@@ -362,7 +369,10 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
// ============================================ // ============================================
livreurGroupV1.GET("/notifications", handlers.GetLivreurNotifications) livreurGroupV1.GET("/notifications", handlers.GetLivreurNotifications)
livreurGroupV1.POST("/notifications/read", handlers.MarkLivreurNotificationsRead) livreurGroupV1.POST("/notifications/read", handlers.MarkLivreurNotificationsRead)
livreurGroupV1.POST("/push-token", handlers.RegisterLivreurPushToken)
livreurGroupV1.DELETE("/push-token", handlers.UnregisterLivreurPushToken) // 🤖 TELEGRAM LIVREUR
livreurGroupV1.POST("/telegram/link-token", handlers.GenerateLivreurLinkToken)
livreurGroupV1.GET("/telegram/status", handlers.GetLivreurTelegramStatus)
livreurGroupV1.DELETE("/telegram/unlink", handlers.UnlinkLivreurTelegram)
} }
} }
+129
View File
@@ -0,0 +1,129 @@
package services
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"time"
)
// TelegramBot est l'instance globale accessible depuis le package db
var TelegramBot *TelegramService
type TelegramService struct {
botToken string
webhookSecret string
BotUsername string
}
func NewTelegramService() *TelegramService {
svc := &TelegramService{
botToken: os.Getenv("TELEGRAM_BOT_TOKEN"),
webhookSecret: os.Getenv("TELEGRAM_WEBHOOK_SECRET"),
BotUsername: os.Getenv("TELEGRAM_BOT_USERNAME"),
}
TelegramBot = svc
return svc
}
func (t *TelegramService) IsConfigured() bool {
return t.botToken != ""
}
// Reload met à jour le token et le username (appelé après UpdateSettings)
func (t *TelegramService) Reload(token, username string) {
if token != "" {
t.botToken = token
}
if username != "" {
t.BotUsername = username
}
}
func (t *TelegramService) ValidateWebhookSecret(header string) bool {
if t.webhookSecret == "" {
return true // pas de secret configuré = accepté
}
return header == t.webhookSecret
}
// SendMessage envoie un message texte (HTML) à un chat Telegram
func (t *TelegramService) SendMessage(chatID int64, text string) error {
if !t.IsConfigured() {
return fmt.Errorf("telegram non configuré")
}
payload := map[string]interface{}{
"chat_id": chatID,
"text": text,
"parse_mode": "HTML",
}
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", t.botToken)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return fmt.Errorf("création requête: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("envoi: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("telegram API status %d", resp.StatusCode)
}
return nil
}
// SetWebhook enregistre l'URL webhook auprès de Telegram
func (t *TelegramService) SetWebhook(webhookURL string) error {
if !t.IsConfigured() {
return fmt.Errorf("telegram non configuré")
}
payload := map[string]interface{}{
"url": webhookURL,
"allowed_updates": []string{"message"},
}
if t.webhookSecret != "" {
payload["secret_token"] = t.webhookSecret
}
body, err := json.Marshal(payload)
if err != nil {
return err
}
url := fmt.Sprintf("https://api.telegram.org/bot%s/setWebhook", t.botToken)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("setWebhook status %d", resp.StatusCode)
}
log.Printf("✅ [TELEGRAM] Webhook enregistré: %s", webhookURL)
return nil
}
+60
View File
@@ -0,0 +1,60 @@
package utils
import (
"crypto/rand"
"encoding/hex"
"regexp"
"strings"
"github.com/gin-gonic/gin"
)
func GenerateSessionID() string {
bytes := make([]byte, 16)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
func ValidatePhoneNumber(phone string) bool {
clean := regexp.MustCompile(`[\s\-\(\)]`).ReplaceAllString(phone, "")
validFormat := regexp.MustCompile(`^(\+33|0)[1-9]\d{8}$`)
return validFormat.MatchString(clean)
}
func NormalizePhoneNumber(phone string) string {
clean := regexp.MustCompile(`[\s\-\(\)]`).ReplaceAllString(phone, "")
if strings.HasPrefix(clean, "0") {
return "+33" + clean[1:]
}
return clean
}
func CheckRoleAdmin(c *gin.Context, role string) bool {
if role == "admin" {
return true
}
return false
}
func CheckRoleClient(c *gin.Context, role string) bool {
if role == "client" {
return true
}
return false
}
func CheckRoleCabine(c *gin.Context, role string) bool {
if role == "cabine" {
return true
}
return false
}
func CheckRoleLivreur(c *gin.Context, role string) bool {
if role == "livreur" {
return true
}
return false
}
+14
View File
@@ -0,0 +1,14 @@
package utils
import (
"gestion/db"
)
func CheckCommand(commandID int, database *db.Database) bool {
_, err := database.GetCommandByID(commandID)
if err != nil {
return false
}
return true
}
+44
View File
@@ -0,0 +1,44 @@
package utils
import (
"fmt"
"math"
)
func CalculateDistance(lat1, lon1, lat2, lon2 float64) float64 {
const earthRadiusKm = 6371
const metersPerKm = 1000
lat1Rad := DegreesToRadians(lat1)
lon1Rad := DegreesToRadians(lon1)
lat2Rad := DegreesToRadians(lat2)
lon2Rad := DegreesToRadians(lon2)
dLat := lat2Rad - lat1Rad
dLon := lon2Rad - lon1Rad
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(lat1Rad)*math.Cos(lat2Rad)*
math.Sin(dLon/2)*math.Sin(dLon/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return earthRadiusKm * c * metersPerKm
}
func DegreesToRadians(degrees float64) float64 {
return degrees * math.Pi / 180
}
func GetDeliveryStatusMessage(status string) string {
messages := map[string]string{
"assigned": "Commande assignée",
"en_route": "En route vers le client",
"arrived": "Arrivé à destination",
"livre": "Livraison effectuée",
"cancelled": "Livraison annulée",
}
if msg, ok := messages[status]; ok {
return msg
}
return fmt.Sprintf("Statut changé: %s", status)
}
+4 -9
View File
@@ -126,18 +126,13 @@ func tryAssignCommandWithPriority(
Longitude: location.Longitude, Longitude: location.Longitude,
} }
// 2. Récupérer livreurs actifs // 2. Récupérer livreurs éligibles (selon le mode single/category_based)
activeLivreurs, err := database.GetAllActiveDeliveryPersons() usernames, err := database.GetEligibleDeliverymenForCommand(commandID)
if err != nil || len(activeLivreurs) == 0 { if err != nil || len(usernames) == 0 {
log.Printf("⚠️ [CRON] Cmd %d - Aucun livreur disponible", commandID) log.Printf("⚠️ [CRON] Cmd %d - Aucun livreur éligible disponible", commandID)
return false return false
} }
usernames := make([]string, len(activeLivreurs))
for i, livreur := range activeLivreurs {
usernames[i] = livreur.Username
}
// 3. Trouver le plus proche // 3. Trouver le plus proche
nearest, err := geoService.FindNearestDeliveryPersonFast(targetCoords, usernames) nearest, err := geoService.FindNearestDeliveryPersonFast(targetCoords, usernames)
if err != nil { if err != nil {
+2 -1
View File
@@ -148,7 +148,8 @@ func PointsSyncWorker(database *db.Database) {
} }
// Mettre à jour dans la DB principale // Mettre à jour dans la DB principale
err = database.AddClientPoints(username, points) pool := "pool_0"
err = database.AddClientPointsByCategory(username, points, pool)
if err != nil { if err != nil {
log.Printf("⚠️ Erreur sync points pour %s: %v", username, err) log.Printf("⚠️ Erreur sync points pour %s: %v", username, err)
continue continue