diff --git a/backend/gestion/db/db_cancel_command.go b/backend/gestion/db/db_cancel_command.go
index 21741fec..ea7f3ad7 100644
--- a/backend/gestion/db/db_cancel_command.go
+++ b/backend/gestion/db/db_cancel_command.go
@@ -10,6 +10,7 @@ import (
"database/sql"
"fmt"
"log"
+ "slices"
"time"
)
@@ -17,17 +18,15 @@ import (
// 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)
- // â
TRANSACTION
tx, err := d.Begin()
if err != nil {
- return 0, nil, fmt.Errorf("erreur transaction: %w", err)
+ return 0, fmt.Errorf("erreur transaction: %w", err)
}
defer tx.Rollback()
- // â
SELECT FOR UPDATE - Verrouiller la ligne
var currentStatus, cmdUsername, livreurAssign string
err = tx.QueryRow(`
SELECT status, username, COALESCE(livreur_assign, '')
@@ -37,163 +36,101 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f
`, commandID).Scan(¤tStatus, &cmdUsername, &livreurAssign)
if err == sql.ErrNoRows {
- return 0, nil, fmt.Errorf("commande non trouvée")
+ return 0, fmt.Errorf("commande non trouvée")
}
if err != nil {
- return 0, nil, err
+ return 0, err
}
log.Printf("đ [CancelAtomic] TrouvĂ©e - status=%s, owner=%s, livreur=%s", currentStatus, cmdUsername, livreurAssign)
- // â
VĂRIFIER PROPRIĂTĂ
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"}
- for _, s := range nonCancellableStatuses {
- if currentStatus == s {
- return 0, nil, fmt.Errorf("impossible d'annuler")
- }
+ if slices.Contains(nonCancellableStatuses, currentStatus) {
+ return 0, 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
-
if livreurAssign != "" {
- // Cas 1: Statut en_route ou arrived = toujours tardif
if currentStatus == "en_route" || currentStatus == "arrived" {
isLateCancel = true
log.Printf("â ïž [CancelAtomic] Annulation TARDIVE dĂ©tectĂ©e - Statut: %s", currentStatus)
+ } else if d.CheckCommandETAExistsAndValid(commandID) {
+ isLateCancel = true
+ log.Printf("â ïž [CancelAtomic] Annulation TARDIVE dĂ©tectĂ©e - ETA dĂ©finie")
} else {
- // Cas 2: Pour les autres statuts, vérifier si une ETA existe
- hasRealETA := d.CheckCommandETAExistsAndValid(commandID)
- if hasRealETA {
- isLateCancel = true
- log.Printf("â ïž [CancelAtomic] Annulation TARDIVE dĂ©tectĂ©e - ETA dĂ©finie")
- } 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 {
log.Printf("â
[CancelAtomic] Annulation SANS PĂNALITĂ - Aucun livreur assignĂ©")
}
- // â
SI ANNULATION TARDIVE SANS CONFIRMATION
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(`
UPDATE commandes
SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP
WHERE id = $1 AND status = $2 AND username = $3
`, commandID, currentStatus, username)
-
if err != nil {
- return 0, nil, err
+ return 0, err
}
-
- rows, _ := result.RowsAffected()
- if rows == 0 {
- return 0, nil, fmt.Errorf("commande déjà modifiée")
+ if rows, _ := result.RowsAffected(); rows == 0 {
+ return 0, fmt.Errorf("commande déjà modifiée")
}
-
log.Printf("â
[CancelAtomic] Statut mis Ă jour: %s â cancelled", currentStatus)
- // â
REMBOURSER LE STOCK ATOMIQUEMENT
- _, err = tx.Exec(`
+ if _, err = tx.Exec(`
UPDATE products p
SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP
FROM command_items ci
WHERE ci.command_id = $1 AND ci.product_id = p.id
- `, commandID)
-
- if err != nil {
+ `, commandID); err != nil {
log.Printf("â ïž [CancelAtomic] Erreur remboursement stock: %v", err)
} else {
log.Printf("â
[CancelAtomic] Stock remboursé")
}
- // ============================================
- // APPLIQUER PĂNALITĂ SI ANNULATION TARDIVE
- // ============================================
penalty := 0
- pointsLost := map[string]int{"weed": 0, "zipette": 0}
-
- if isLateCancel && force {
+ if isLateCancel {
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(¤tPointsWeed, ¤tPointsZipette)
-
- if err == nil {
- pointsLost["weed"] = currentPointsWeed
- pointsLost["zipette"] = currentPointsZipette
-
- // â
CALCULER PĂNALITĂ
- penalty, _ = d.CalculateCancellationPenalty(username)
-
- // â
APPLIQUER: Remettre points à 0 + Ajouter pénalité + Incrémenter compteur
- _, err = tx.Exec(`
- UPDATE clients
- SET point = 0,
- point_zipette = 0,
- amende = amende + $1,
- cancellations_count = COALESCE(cancellations_count, 0) + 1,
- updated_at = CURRENT_TIMESTAMP
- WHERE username = $2
- `, penalty, username)
-
- if err != nil {
- log.Printf("â [CancelAtomic] Erreur pĂ©nalitĂ©: %v", err)
- } else {
- log.Printf("â ïž [CancelAtomic] PĂ©nalitĂ©: %d pts, Points perdus: weed=%d, zipette=%d",
- penalty, pointsLost["weed"], pointsLost["zipette"])
- }
+ penalty, _ = d.CalculateCancellationPenalty(username)
+ if _, err = tx.Exec(`
+ UPDATE clients
+ SET amende = amende + $1,
+ cancellations_count = COALESCE(cancellations_count, 0) + 1,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE username = $2
+ `, penalty, username); err != nil {
+ log.Printf("â [CancelAtomic] Erreur pĂ©nalitĂ©: %v", err)
+ } else {
+ log.Printf("â ïž [CancelAtomic] PĂ©nalitĂ©: %d appliquĂ©e Ă %s", penalty, username)
}
}
- // â
LOG
- _, err = tx.Exec(`
+ if _, err = tx.Exec(`
INSERT INTO command_logs (command_id, status, message, author, created_at)
- VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)
- `, commandID, "cancelled",
- fmt.Sprintf("Annulée par %s - Raison: %s", username, reason),
- username)
-
- if err != nil {
+ VALUES ($1, 'cancelled', $2, $3, CURRENT_TIMESTAMP)
+ `, commandID, fmt.Sprintf("Annulée par %s - Raison: %s", username, reason), username); err != nil {
log.Printf("â ïž [CancelAtomic] Erreur log: %v", err)
}
- // â
COMMIT
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 != "" {
go func() {
- err := d.CleanupCompletedCommandFromQueue(commandID, livreurAssign)
- if err != nil {
+ if err := d.CleanupCompletedCommandFromQueue(commandID, livreurAssign); err != nil {
log.Printf("â ïž [CancelAtomic] Erreur cleanup queue: %v", err)
}
}()
}
-
- // â
INVALIDER CACHES (async)
go func() {
Redis.Del(RedisCtx,
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)
-
- return penalty, pointsLost, nil
+ return penalty, nil
}
// ============================================
diff --git a/backend/gestion/db/db_clients.go b/backend/gestion/db/db_clients.go
index 892eacdc..2fd3d80a 100644
--- a/backend/gestion/db/db_clients.go
+++ b/backend/gestion/db/db_clients.go
@@ -10,8 +10,8 @@ import (
)
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)
- VALUES ($1, $2, $3, $4, $5, 0, 0, 0, 0.0, $6, CURRENT_TIMESTAMP)
+ query := `INSERT INTO clients (username, password, nom, prenom, telephone, command, amende, must_change_password, created_at)
+ VALUES ($1, $2, $3, $4, $5, 0, 0.0, $6, CURRENT_TIMESTAMP)
RETURNING id, created_at`
err := d.QueryRow(query, client.Username, client.Password, client.Nom, client.Prenom, client.Telephone, client.MustChangePassword).Scan(
@@ -29,9 +29,10 @@ func (d *Database) CreateClient(client *models.Client) error {
// GetClientByID récupÚre un client par son ID
func (d *Database) GetClientByID(id int) (*models.Client, error) {
var client models.Client
- query := `SELECT id, username, password, nom, prenom, telephone, command, point, point_zipette, amende, created_at
+ query := `SELECT id, username, password, nom, prenom, telephone, command, amende, COALESCE(points_extra, '{}'::jsonb), created_at
FROM clients WHERE id = $1`
+ var pointsExtraJSON []byte
err := d.QueryRow(query, id).Scan(
&client.ID,
&client.Username,
@@ -40,9 +41,8 @@ func (d *Database) GetClientByID(id int) (*models.Client, error) {
&client.Prenom,
&client.Telephone,
&client.Command,
- &client.Point,
- &client.PointZipette,
&client.Amende,
+ &pointsExtraJSON,
&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)
}
+ if len(pointsExtraJSON) > 0 {
+ json.Unmarshal(pointsExtraJSON, &client.PointsExtra)
+ }
+
return &client, nil
}
// GetAllClients récupÚre tous les clients
func (d *Database) GetAllClients() ([]*models.Client, error) {
- query := `SELECT id, username, password, nom, prenom, telephone, command, 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`
rows, err := d.Query(query)
@@ -79,8 +83,6 @@ func (d *Database) GetAllClients() ([]*models.Client, error) {
&client.Prenom,
&client.Telephone,
&client.Command,
- &client.Point,
- &client.PointZipette,
&client.Amende,
&client.ReferralBalance,
&pointsExtraJSON,
@@ -107,8 +109,8 @@ func (d *Database) GetAllClients() ([]*models.Client, error) {
func (d *Database) UpdateClient(client *models.Client) error {
query := `UPDATE clients
SET username = $1, password = $2, nom = $3, prenom = $4, telephone = $5,
- command = $6, point = $7, point_zipette = $8, amende = $9
- WHERE id = $10`
+ command = $6, amende = $7
+ WHERE id = $8`
result, err := d.Exec(query,
client.Username,
@@ -117,8 +119,6 @@ func (d *Database) UpdateClient(client *models.Client) error {
client.Prenom,
client.Telephone,
client.Command,
- client.Point,
- client.PointZipette,
client.Amende,
client.ID,
)
@@ -135,13 +135,11 @@ func (d *Database) UpdateClient(client *models.Client) error {
return fmt.Errorf("client non trouvé")
}
- log.Printf("â
Client mis Ă jour: %s (ID: %d)", client.Username, client.ID)
return nil
}
// DeleteClient supprime un client
func (d *Database) DeleteClient(id int) error {
- // â
MODIFIĂ : Supprimer tous les tokens du client avec le user_type "client"
_ = d.RevokeAllUserTokens(id, "client")
query := `DELETE FROM clients WHERE id = $1`
@@ -160,7 +158,6 @@ func (d *Database) DeleteClient(id int) error {
return fmt.Errorf("client non trouvé")
}
- log.Printf("â
Client supprimé (ID: %d)", id)
return nil
}
@@ -182,7 +179,6 @@ func (d *Database) UpdateClientPassword(clientID int, hashedPassword string) err
return fmt.Errorf("client non trouvé")
}
- log.Printf("â
Mot de passe client mis Ă jour (ID: %d)", clientID)
return nil
}
@@ -204,7 +200,6 @@ func (d *Database) UpdateClientPasswordAndClearFlag(clientID int, hashedPassword
return fmt.Errorf("client non trouvé")
}
- log.Printf("â
Mot de passe client mis Ă jour + must_change_password=false (ID: %d)", clientID)
return nil
}
@@ -239,8 +234,7 @@ func (d *Database) GetClientStats(clientID int) (map[string]interface{}, error)
"total_commands": totalCommands,
"pending_commands": pendingCommands,
"completed_commands": completedCommands,
- "points": client.Point,
- "points_zipette": client.PointZipette,
+ "points_extra": client.PointsExtra,
"amende": client.Amende,
"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)
}
- // Réinitialiser les pénalités
query := `UPDATE clients
SET amende = 0, updated_at = CURRENT_TIMESTAMP
WHERE username = $1`
@@ -298,9 +291,6 @@ func (d *Database) PayClientPenalties(username string, amountPaid float64) error
return fmt.Errorf("client non trouvé")
}
- log.Printf("â
[PayClientPenalties] Pénalités réglées pour %s", username)
-
- // Invalider le cache Redis du client
cacheKey := fmt.Sprintf("client:%s", username)
Redis.Del(RedisCtx, cacheKey)
@@ -329,27 +319,22 @@ func (d *Database) IncrementClientCommandCount(username string) error {
return nil
}
-// â
NOUVELLE FONCTION: Ajouter des points selon la catégorie
-func (d *Database) AddClientPointsByCategory(username string, points int, category string) error {
- var query string
-
- if category == "zipette&co" {
- query = `UPDATE clients
- SET point_zipette = point_zipette + $1
- WHERE username = $2`
- log.Printf("đ [ADD_POINTS] Ajout de %d points ZIPETTE Ă %s", points, username)
- } else {
- query = `UPDATE clients
- SET point = point + $1
- WHERE username = $2`
- log.Printf("đ [ADD_POINTS] Ajout de %d points WEED/HASH Ă %s", points, username)
+func (d *Database) AddClientPointsByCategory(username string, points int, poolKey string) error {
+ if poolKey == "" {
+ poolKey = "pool_0"
}
-
- result, err := d.Exec(query, points, username)
+ result, err := d.Exec(`
+ UPDATE clients
+ SET points_extra = jsonb_set(
+ COALESCE(points_extra, '{}'::jsonb),
+ ARRAY[$2],
+ to_jsonb(COALESCE((points_extra->>$2)::int, 0) + $3)
+ ), updated_at = CURRENT_TIMESTAMP
+ WHERE username = $1
+ `, username, poolKey, points)
if err != nil {
return fmt.Errorf("erreur lors de l'ajout de points: %w", err)
}
-
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("erreur lors de la vérification: %w", err)
@@ -357,19 +342,31 @@ func (d *Database) AddClientPointsByCategory(username string, points int, catego
if rowsAffected == 0 {
return fmt.Errorf("client non trouvé")
}
-
- log.Printf("â
%d points (%s) ajoutés au client %s (EN DB)", points, category, username)
+ log.Printf("â
%d points (key=%s) ajoutés au client %s", points, poolKey, username)
return nil
}
-// â
ANCIENNE FONCTION CONSERVĂE POUR COMPATIBILITĂ (utilise weed/hash par dĂ©faut)
-func (d *Database) AddClientPoints(username string, points int) error {
- return d.AddClientPointsByCategory(username, points, "weed_hash")
+func (d *Database) CalculateAndAddPointsForCommand(commandID int, username string) (int, error) {
+ tx, err := d.Begin()
+ if err != nil {
+ return 0, fmt.Errorf("erreur transaction: %w", err)
+ }
+ defer tx.Rollback()
+
+ points, _, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, username)
+ if err != nil {
+ return 0, err
+ }
+
+ if err := tx.Commit(); err != nil {
+ return 0, fmt.Errorf("erreur commit: %w", err)
+ }
+ return points, nil
}
func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error) {
client := &models.Client{}
- query := `SELECT id, username, password, nom, prenom, telephone, command, point, point_zipette, amende, created_at
+ query := `SELECT id, username, password, nom, prenom, telephone, command, amende, created_at
FROM clients WHERE telephone = $1`
err := d.QueryRow(query, telephone).Scan(
@@ -380,8 +377,6 @@ func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error
&client.Prenom,
&client.Telephone,
&client.Command,
- &client.Point,
- &client.PointZipette,
&client.Amende,
&client.CreatedAt,
)
@@ -400,7 +395,7 @@ func (d *Database) GetClientByTelephone(telephone string) (*models.Client, error
func (d *Database) GetClientByUsername(username string) (*models.Client, error) {
client := &models.Client{}
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`
err := d.QueryRow(query, username).Scan(
@@ -411,8 +406,6 @@ func (d *Database) GetClientByUsername(username string) (*models.Client, error)
&client.Prenom,
&client.Telephone,
&client.Command,
- &client.Point,
- &client.PointZipette,
&client.Amende,
&client.MustChangePassword,
&pointsExtraJSON,
@@ -485,23 +478,25 @@ func (d *Database) CheckClientCanOrder(username string) (bool, float64, error) {
}
// ResetClientPoint réinitialise les points d'un client.
-// poolIdx=0 â point, poolIdx=1 â point_zipette, poolIdx=-1 â tous
-// poolIdx>=2 â points_extra[extraPoolKey]
+// extraPoolKey != "" â reset points_extra[extraPoolKey] uniquement
+// extraPoolKey == "" (poolIdx=-1) â reset total points_extra
func (d *Database) ResetClientPoint(username string, poolIdx int, extraPoolKey string) error {
var query string
switch {
- case poolIdx == 0:
- query = `UPDATE clients SET point = 0, updated_at = CURRENT_TIMESTAMP WHERE username = $1`
- case poolIdx == 1:
- query = `UPDATE clients SET point_zipette = 0, updated_at = CURRENT_TIMESTAMP WHERE username = $1`
- case poolIdx >= 2 && extraPoolKey != "":
+ case extraPoolKey != "":
_, err := d.Exec(
`UPDATE clients SET points_extra = points_extra - $2, updated_at = CURRENT_TIMESTAMP WHERE username = $1`,
username, extraPoolKey,
)
+ if err != nil {
+ log.Printf("â [ResetClientPointAdmin] Erreur UPDATE extra: %v", err)
+ } else {
+ cacheKey := fmt.Sprintf("client:%s", username)
+ Redis.Del(RedisCtx, cacheKey)
+ }
return err
- default: // -1 â reset total
- query = `UPDATE clients SET point = 0, point_zipette = 0, points_extra = '{}'::jsonb, updated_at = CURRENT_TIMESTAMP WHERE username = $1`
+ default: // -1 ou poolIdx sans clĂ© â reset total
+ query = `UPDATE clients SET points_extra = '{}'::jsonb, updated_at = CURRENT_TIMESTAMP WHERE username = $1`
}
result, err := d.Exec(query, username)
@@ -649,240 +644,6 @@ func (d *Database) GetClientPenaltiesStats() (map[string]interface{}, error) {
return result, nil
}
-// â
FONCTION MODIFIĂE: Calculer les points sans cumuler entre catĂ©gories
-// Ă remplacer dans db/clients.go Ă partir de la ligne 498
-
-// Ă remplacer dans db/clients.go
-
-func (d *Database) CalculatePointsForCommand(commandID int) (int, string, error) {
- query := `
- SELECT p.category, ci.prix, ci.quantite
- FROM command_items ci
- JOIN products p ON ci.product_id = p.id
- WHERE ci.command_id = $1
- `
-
- rows, err := d.Query(query, commandID)
- if err != nil {
- return 0, "", fmt.Errorf("erreur récupération items: %w", err)
- }
- defer rows.Close()
-
- zipetteTotal := 0.0
- weedTotal := 0.0
- grosSemiTotal := 0.0
-
- for rows.Next() {
- var category string
- var prix float64
- var quantite int
-
- if err := rows.Scan(&category, &prix, &quantite); err != nil {
- log.Printf("â ïž Erreur scan item: %v", err)
- continue
- }
-
- itemTotal := prix // Prix déjà calculé pour la quantité
- categoryLower := strings.ToLower(category)
-
- if categoryLower == "zipette&co" || categoryLower == "zipette_co" {
- zipetteTotal += itemTotal
- } else if categoryLower == "gros&semi" || categoryLower == "gros_semi" {
- grosSemiTotal += itemTotal
- } else {
- weedTotal += itemTotal
- }
- }
-
- log.Printf("đ° [CALC_POINTS] Cmd %d - Zipette: %.2fâŹ, Weed: %.2fâŹ, GrosSemi: %.2fâŹ",
- commandID, zipetteTotal, weedTotal, grosSemiTotal)
-
- // â
CALCUL POINTS WEED&HASH
- weedPoints := 0
- if weedTotal > 0 {
- switch {
- case weedTotal >= 30 && weedTotal <= 50:
- weedPoints = 1
- case weedTotal >= 60 && weedTotal <= 150:
- weedPoints = 2
- case weedTotal >= 160 && weedTotal <= 300:
- weedPoints = 3
- case weedTotal >= 310 && weedTotal <= 400:
- weedPoints = 5
- case weedTotal >= 400:
- weedPoints = 10
- }
- if weedPoints > 0 {
- log.Printf("đ [CALC_POINTS] Weed: %.2f⏠â %d points", weedTotal, weedPoints)
- }
- }
-
- // â
CALCUL POINTS ZIPETTE&CO
- zipettePoints := 0
- if zipetteTotal > 0 {
- switch {
- case zipetteTotal >= 30 && zipetteTotal <= 100:
- zipettePoints = 1
- case zipetteTotal >= 110 && zipetteTotal <= 200:
- zipettePoints = 2
- case zipetteTotal >= 210:
- zipettePoints = 3
- }
- if zipettePoints > 0 {
- log.Printf("đ [CALC_POINTS] Zipette: %.2f⏠â %d points", zipetteTotal, zipettePoints)
- }
- }
-
- totalPoints := zipettePoints + weedPoints
-
- if totalPoints == 0 {
- if grosSemiTotal > 0 && zipetteTotal == 0 && weedTotal == 0 {
- log.Printf("âčïž [CALC_POINTS] Cmd %d - CatĂ©gorie GROS&SEMI uniquement (%.2fâŹ) â 0 points",
- commandID, grosSemiTotal)
- return 0, "gros&semi", nil
- }
- log.Printf("âčïž [CALC_POINTS] Cmd %d - Aucun montant Ă©ligible aux points", commandID)
- return 0, "unknown", nil
- }
-
- var dominantCategory string
- if zipetteTotal >= weedTotal {
- dominantCategory = "zipette&co"
- } else {
- dominantCategory = "weed&hash"
- }
-
- log.Printf("â
[CALC_POINTS] Cmd %d - Total: %d points (Zipette: %d, Weed: %d)",
- commandID, totalPoints, zipettePoints, weedPoints)
-
- return totalPoints, dominantCategory, nil
-}
-
-// â
FONCTION CORRIGĂE: Calculer et ajouter les points sĂ©parĂ©ment avec le BON BARĂME
-func (d *Database) CalculateAndAddPointsForCommand(commandID int, username string) (int, error) {
- query := `
- SELECT p.category, ci.prix, ci.quantite
- FROM command_items ci
- JOIN products p ON ci.product_id = p.id
- WHERE ci.command_id = $1
- `
-
- rows, err := d.Query(query, commandID)
- if err != nil {
- return 0, fmt.Errorf("erreur récupération items: %w", err)
- }
- defer rows.Close()
-
- zipetteTotal := 0.0
- weedTotal := 0.0
- grosSemiTotal := 0.0
-
- for rows.Next() {
- var category string
- var prix float64
- var quantite int
-
- if err := rows.Scan(&category, &prix, &quantite); err != nil {
- log.Printf("â ïž Erreur scan item: %v", err)
- continue
- }
-
- // â
prix contient déjà le total pour la quantité
- itemTotal := prix
- categoryLower := strings.ToLower(category)
-
- if categoryLower == "zipette&co" || categoryLower == "zipette_co" {
- zipetteTotal += itemTotal
- } else if categoryLower == "gros&semi" || categoryLower == "gros_semi" {
- grosSemiTotal += itemTotal
- } else {
- weedTotal += itemTotal
- }
- }
-
- log.Printf("đ° [CALC_POINTS] Cmd %d - Zipette: %.2fâŹ, Weed: %.2fâŹ, GrosSemi: %.2fâŹ",
- commandID, zipetteTotal, weedTotal, grosSemiTotal)
-
- // â
CALCUL POINTS WEED&HASH - NOUVEAU BARĂME
- // De 30 à 50⏠-> 1 point
- // De 60 à 150⏠-> 2 points
- // De 160 à 300⏠-> 3 points
- // De 310 à 400⏠-> 5 points
- // 400⏠et + -> 10 points
- weedPoints := 0
- if weedTotal > 0 {
- switch {
- case weedTotal >= 30 && weedTotal <= 50:
- weedPoints = 1
- case weedTotal >= 60 && weedTotal <= 150:
- weedPoints = 2
- case weedTotal >= 160 && weedTotal <= 300:
- weedPoints = 3
- case weedTotal >= 310 && weedTotal <= 400:
- weedPoints = 5
- case weedTotal >= 400:
- weedPoints = 10
- }
- if weedPoints > 0 {
- log.Printf("đ [CALC_POINTS] Weed: %.2f⏠â %d points", weedTotal, weedPoints)
- }
- }
-
- // â
CALCUL POINTS ZIPETTE&CO - NOUVEAU BARĂME
- // De 30 à 100⏠-> 1 point
- // De 110 à 200⏠-> 2 points (je suppose que c'est 110 et non 1100)
- // De 210⏠et + -> 3 points
- zipettePoints := 0
- if zipetteTotal > 0 {
- switch {
- case zipetteTotal >= 30 && zipetteTotal <= 100:
- zipettePoints = 1
- case zipetteTotal >= 110 && zipetteTotal <= 200:
- zipettePoints = 2
- case zipetteTotal >= 210:
- zipettePoints = 3
- }
- if zipettePoints > 0 {
- log.Printf("đ [CALC_POINTS] Zipette: %.2f⏠â %d points", zipetteTotal, zipettePoints)
- }
- }
-
- totalPoints := 0
-
- // â
AJOUTER LES POINTS SĂPARĂMENT PAR CATĂGORIE
- if zipettePoints > 0 {
- if err := d.AddClientPointsByCategory(username, zipettePoints, "zipette&co"); err != nil {
- log.Printf("â Erreur ajout points Zipette: %v", err)
- } else {
- totalPoints += zipettePoints
- log.Printf("â
%d points ZIPETTE ajoutés à %s", zipettePoints, username)
- }
- }
-
- if weedPoints > 0 {
- if err := d.AddClientPointsByCategory(username, weedPoints, "weed&hash"); err != nil {
- log.Printf("â Erreur ajout points Weed: %v", err)
- } else {
- totalPoints += weedPoints
- log.Printf("â
%d points WEED ajoutés à %s", weedPoints, username)
- }
- }
-
- if totalPoints == 0 {
- if grosSemiTotal > 0 && zipetteTotal == 0 && weedTotal == 0 {
- log.Printf("âčïž [CALC_POINTS] Cmd %d - CatĂ©gorie GROS&SEMI uniquement â 0 points", commandID)
- } else {
- log.Printf("âčïž [CALC_POINTS] Cmd %d - Aucun point Ă©ligible (Weed: %.2fâŹ, Zipette: %.2fâŹ)",
- commandID, weedTotal, zipetteTotal)
- }
- }
-
- log.Printf("â
[CALC_POINTS] Cmd %d - Total: %d points (Zipette: %d, Weed: %d)",
- commandID, totalPoints, zipettePoints, weedPoints)
-
- return totalPoints, nil
-}
-
func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int, username string) (int, string, error) {
log.Printf("đ° [CalcPointsTx] START - cmd=%d, user=%s", commandID, username)
@@ -982,8 +743,6 @@ func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int,
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 {
if poolPts[i] == 0 {
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])
}
- // 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)
return totalPoints, pointCategory, nil
@@ -1066,22 +802,3 @@ func (d *Database) CanUserAccessCommand(
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
-}
diff --git a/backend/gestion/db/db_command_items.go b/backend/gestion/db/db_command_items.go
index 3c6c9784..e762a3bb 100644
--- a/backend/gestion/db/db_command_items.go
+++ b/backend/gestion/db/db_command_items.go
@@ -216,7 +216,7 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err
c.referral_used,
c.livreur_assign,
c.created_at as command_created_at,
- COALESCE(p.category, 'weed_hash') as category
+ p.category
FROM command_items ci
LEFT JOIN commandes c ON ci.command_id = c.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
}
-// ============================================
-// GET COMMAND ITEMS BY USERNAME - VERSION SĂCURISĂE + FIX NULL
-// ============================================
-
func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]interface{}, error) {
- log.Printf("đŠ [GetCommandItemsByUsername] START - username=%s", username)
-
- // â
VALIDATION
if err := validateUsername(username); err != nil {
log.Printf("â [GetCommandItemsByUsername] %v", err)
return nil, err
@@ -352,7 +345,7 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte
for rows.Next() {
var id, commandID int
var quantite float64
- var productID sql.NullInt64 // â
FIX: NullInt64
+ var productID sql.NullInt64
var produit, clientUsername, clientNom, clientPrenom, clientTelephone string
var deliveryAddress, status sql.NullString
var prix, totalPrix float64
@@ -370,28 +363,26 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte
return nil, fmt.Errorf("erreur scan: %w", err)
}
- // â
CONVERTIR NullInt64
productIDValue := 0
if productID.Valid {
productIDValue = int(productID.Int64)
}
item := map[string]interface{}{
- "id": id,
- "command_id": commandID,
- "produit": produit,
- "product_id": productIDValue, // â
FIX
- "quantite": quantite,
- "prix": prix,
- "client_username": clientUsername,
- "client_nom": clientNom,
- "client_prenom": clientPrenom,
- "client_telephone": clientTelephone,
- "delivery_address": deliveryAddress.String,
- "status": status.String,
- "created_at": createdAt,
- "updated_at": updatedAt,
- // Infos commande
+ "id": id,
+ "command_id": commandID,
+ "produit": produit,
+ "product_id": productIDValue,
+ "quantite": quantite,
+ "prix": prix,
+ "client_username": clientUsername,
+ "client_nom": clientNom,
+ "client_prenom": clientPrenom,
+ "client_telephone": clientTelephone,
+ "delivery_address": deliveryAddress.String,
+ "status": status.String,
+ "created_at": createdAt,
+ "updated_at": updatedAt,
"command_status": commandStatus.String,
"command_address": commandAddress.String,
"total_prix": totalPrix,
@@ -410,10 +401,6 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte
return items, nil
}
-// ============================================
-// DELETE COMMAND ITEM - ADMIN ONLY
-// ============================================
-
func (d *Database) DeleteCommandItem(commandID, itemID int) error {
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] Item %d supprimé de la commande %d", itemID, commandID)
return nil
}
-// ============================================
-// UPDATE COMMAND ITEM STATUS - VERSION SĂCURISĂE
-// ============================================
-
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 {
log.Printf("â [UpdateCommandItemStatus] %v", err)
return err
@@ -473,7 +452,6 @@ func (d *Database) UpdateCommandItemStatus(itemID int, status string) error {
return err
}
- // â
VĂRIFIER QUE L'ITEM EXISTE
var exists bool
checkQuery := `SELECT EXISTS(SELECT 1 FROM command_items WHERE id = $1)`
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)
}
- // â
UPDATE
query := `UPDATE command_items
SET status = $1, updated_at = CURRENT_TIMESTAMP
WHERE id = $2`
@@ -507,6 +484,5 @@ func (d *Database) UpdateCommandItemStatus(itemID int, status string) error {
return fmt.Errorf("item non trouvé")
}
- log.Printf("â
Statut item %d mis Ă jour: %s", itemID, status)
return nil
}
diff --git a/backend/gestion/db/db_command_priority.go b/backend/gestion/db/db_command_priority.go
index def847b0..597b730e 100644
--- a/backend/gestion/db/db_command_priority.go
+++ b/backend/gestion/db/db_command_priority.go
@@ -10,11 +10,11 @@ import (
"fmt"
"gestion/models"
"log"
+ "slices"
"time"
)
// 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) {
query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix,
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
if status != "" {
validStatuses := []string{"pending", "assigned", "en_route", "livre", "approved", "cancelled", "disabled"}
- isValid := false
- for _, vs := range validStatuses {
- if status == vs {
- isValid = true
- break
- }
- }
+
+ isValid := slices.Contains(validStatuses, status)
if !isValid {
return nil, fmt.Errorf("statut invalide: %s", status)
}
@@ -50,11 +45,8 @@ func (d *Database) GetAllCommandsOldestFirst(status, username string) ([]map[str
argPosition++
}
- // â
TRI PAR ANCIENNETĂ: Les plus anciennes d'abord (ASC)
query += " ORDER BY c.created_at ASC"
- log.Printf("đ [PRIORITY] Query: %s | Args: %v", query, args)
-
rows, err := d.Query(query, args...)
if err != nil {
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)
}
- log.Printf("â
[PRIORITY] %d commandes rĂ©cupĂ©rĂ©es (ordre: plus anciennes â plus rĂ©centes)", len(commands))
-
return commands, nil
}
@@ -145,9 +135,6 @@ func (d *Database) GetOldestPendingCommand() (map[string]interface{}, error) {
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
}
@@ -199,8 +186,6 @@ func (d *Database) GetPendingCommandsWithPriority() ([]*models.CommandPriority,
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
}
diff --git a/backend/gestion/db/db_commands.go b/backend/gestion/db/db_commands.go
index 92044f12..d7a4b63c 100644
--- a/backend/gestion/db/db_commands.go
+++ b/backend/gestion/db/db_commands.go
@@ -5,6 +5,7 @@ import (
"fmt"
"gestion/models"
"log"
+ "slices"
"strings"
"time"
)
@@ -42,6 +43,35 @@ func validateAddress(address string) error {
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
func validateCommandStatus(status string) error {
validStatuses := map[string]bool{
@@ -63,48 +93,22 @@ func validateCommandStatus(status string) error {
}
func (d *Database) CreateCommand(username string) (*models.Command, error) {
- // Récupérer le client pour obtenir son adresse
var adresse string
clientQuery := `SELECT username FROM clients WHERE username = $1`
err := d.QueryRow(clientQuery, username).Scan(&adresse)
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"
}
- // Récupérer les items du panier
- basketQuery := `SELECT product_id, quantity, price FROM baskets WHERE username = $1`
- rows, err := d.Query(basketQuery, username)
+ basketItems, totalPrix, err := d.fetchBasketItems(username)
if err != nil {
- return nil, fmt.Errorf("erreur lors de la récupération du panier: %w", 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
+ return nil, err
}
if len(basketItems) == 0 {
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)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
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)
}
- // Insérer les items de la commande
for _, item := range basketItems {
- // Récupérer le nom du produit
productName, err := d.GetProductNameByID(item.ProductID)
if err != nil {
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`
_, err = d.Exec(clearBasketQuery, username)
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) {
- // â
SĂCURITĂ: Validation des entrĂ©es
if err := validateUsername(username); err != nil {
return nil, err
}
@@ -162,9 +162,6 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
return nil, err
}
- log.Printf("đ [CreateCommandWithAddress] START - user=%s", username)
-
- // Récupérer les infos du client
client, err := d.GetClientByUsername(username)
if err != nil {
log.Printf("â ïž Client non trouvĂ©: %v", err)
@@ -179,51 +176,26 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
clientTelephone = sanitizeString(client.Telephone)
}
- // Récupérer les items du panier
- basketQuery := `SELECT product_id, quantity, price FROM baskets WHERE username = $1`
- rows, err := d.Query(basketQuery, username)
+ basketItems, totalPrix, err := d.fetchBasketItems(username)
if err != nil {
log.Printf("â Erreur query basket: %v", err)
- return nil, fmt.Errorf("erreur récupération panier: %w", 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
+ return nil, err
}
if len(basketItems) == 0 {
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 {
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)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, created_at, updated_at`
@@ -235,13 +207,9 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
&commandID, &createdAt, &updatedAt,
)
if err != nil {
- log.Printf("â Erreur INSERT commande: %v", 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 {
productName, err := d.GetProductNameByID(item.ProductID)
if err != nil || productName == "" {
@@ -265,7 +233,6 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
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`
result, err := d.Exec(stockQuery, item.Quantity, item.ProductID)
if err != nil {
@@ -275,28 +242,22 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
rowsAffected, _ := result.RowsAffected()
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)
}
}
- // Vider le panier
clearBasketQuery := `DELETE FROM baskets WHERE username = $1`
_, err = d.Exec(clearBasketQuery, username)
if err != nil {
log.Printf("â ïž Erreur vidage panier: %v", err)
}
- // Ajouter un log de commande
sanitizedAddress := sanitizeLogMessage(deliveryAddress)
d.AddCommandLog(commandID, "created",
fmt.Sprintf("Commande créée - Adresse: %s - Total: %.2f⏠- Client: %s %s",
sanitizedAddress, totalPrix, sanitizeLogMessage(clientNom), sanitizeLogMessage(clientPrenom)),
username)
- log.Printf("đ SUCCĂS - Commande %d | User: %s | Total: %.2fâŹ", commandID, username, totalPrix)
-
command := &models.Command{
ID: commandID,
Username: username,
@@ -310,9 +271,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
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]interface{}, error) {
- // â
SĂCURITĂ: Validation des paramĂštres
+func (d *Database) GetAllCommands(status, username string) ([]map[string]any, error) {
if username != "" {
if err := validateUsername(username); err != nil {
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,
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
WHERE 1=1`
args := []interface{}{}
argPosition := 1
- // Filtrage du statut
if status == "" {
query += ` AND c.status IN ('pending', 'assigned', 'en_route', 'arrived', 'livre')`
} else {
@@ -343,16 +302,13 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
argPosition++
}
- // Filtrage du username
if username != "" {
query += fmt.Sprintf(" AND c.username = $%d", argPosition)
args = append(args, username)
argPosition++
}
- query += " ORDER BY c.created_at DESC LIMIT 1000" // â
Protection DoS
-
- log.Printf("đ [GetAllCommands] Query avec %d args", len(args))
+ query += " ORDER BY c.created_at DESC LIMIT 1000"
rows, err := d.Query(query, args...)
if err != nil {
@@ -360,7 +316,7 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
}
defer rows.Close()
- var commands []map[string]interface{}
+ var commands []map[string]any
for rows.Next() {
var id int
var username, status, adresse string
@@ -369,16 +325,16 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
var addressProposalStatus string
var totalPrix float64
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 {
return nil, fmt.Errorf("erreur lors du scan de la commande: %w", err)
}
- // â
SĂCURITĂ: Sanitization de l'adresse
adresse = sanitizeString(adresse)
- command := map[string]interface{}{
+ command := map[string]any{
"id": id,
"username": username,
"status": status,
@@ -387,6 +343,7 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
"created_at": createdAt,
"updated_at": updatedAt,
"address_proposal_status": addressProposalStatus,
+ "client_order_number": clientOrderNumber,
}
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)
}
- log.Printf("â
[GetAllCommands] %d commandes récupérées", len(commands))
-
return commands, nil
}
@@ -427,11 +382,11 @@ func (d *Database) SetCommandReferralUsed(commandID int, amount float64) error {
return err
}
-func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) {
- // â
Déjà sécurisé avec paramÚtre $1
- query := `SELECT id, username, status, adresse, total_prix, livreur_assign, created_at, updated_at,
- proposed_address, address_proposal_status, referral_used
- FROM commandes WHERE id = $1`
+func (d *Database) GetCommandByID(id int) (map[string]any, error) {
+ query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign, c.created_at, c.updated_at,
+ c.proposed_address, c.address_proposal_status, c.referral_used,
+ (SELECT COUNT(*) FROM commandes c2 WHERE c2.username = c.username AND c2.id <= c.id) AS client_order_number
+ FROM commandes c WHERE c.id = $1`
var commandID int
var username, status, adresse string
@@ -440,6 +395,7 @@ func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) {
var addressProposalStatus string
var totalPrix, referralUsed float64
var createdAt, updatedAt time.Time
+ var clientOrderNumber int
err := d.QueryRow(query, id).Scan(
&commandID,
@@ -453,6 +409,7 @@ func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) {
&proposedAddress,
&addressProposalStatus,
&referralUsed,
+ &clientOrderNumber,
)
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)
}
- command := map[string]interface{}{
+ command := map[string]any{
"id": commandID,
"username": username,
"status": status,
@@ -472,6 +429,7 @@ func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) {
"updated_at": updatedAt,
"address_proposal_status": addressProposalStatus,
"referral_used": referralUsed,
+ "client_order_number": clientOrderNumber,
}
if livreurAssign.Valid {
@@ -505,7 +463,6 @@ func (d *Database) GetCommandAddress(commandID int) (string, error) {
}
func (d *Database) UpdateCommandAddress(commandID int, deliveryAddress string) error {
- // â
SĂCURITĂ: Validation de l'adresse
if len(deliveryAddress) > 500 {
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 {
var query string
if accepted {
- // Remplace l'adresse par la proposition
query = `UPDATE commandes
SET adresse = proposed_address, proposed_address = NULL,
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
func (d *Database) UpdateCommandStatus(commandID int, status string) error {
- // â
SĂCURITĂ: Validation du statut
- 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 fmt.Errorf("statut invalide: %s", status)
+ if err := validateCommandStatus(status); err != nil {
+ return err
}
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
}
-// AddCommandLog ajoute un log pour une commande
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)
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)
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)
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
-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
FROM command_logs
WHERE command_id = $1
@@ -662,11 +606,11 @@ func (d *Database) GetCommandLogs(commandID int) ([]map[string]interface{}, erro
if err != nil {
// 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)
- return []map[string]interface{}{}, nil
+ return []map[string]any{}, nil
}
defer rows.Close()
- var logs []map[string]interface{}
+ var logs []map[string]any
for rows.Next() {
var id, commandID int
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)
}
- logEntry := map[string]interface{}{
+ logEntry := map[string]any{
"id": id,
"command_id": commandID,
"status": status,
@@ -695,108 +639,14 @@ func (d *Database) GetCommandLogs(commandID int) ([]map[string]interface{}, erro
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 {
- // Supprimer les caractĂšres de contrĂŽle et limiter la longueur
sanitized := strings.Map(func(r rune) rune {
if r < 32 || r == 127 {
- return -1 // Supprimer les caractĂšres de contrĂŽle
+ return -1
}
return r
}, message)
- // Limiter Ă 1000 caractĂšres
if len(sanitized) > 1000 {
sanitized = sanitized[:1000]
}
@@ -805,17 +655,13 @@ func sanitizeLogMessage(message string) string {
}
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()
if err != nil {
log.Printf("â [ValidateAtomic] Erreur dĂ©but transaction: %v", 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 totalPrix float64
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",
currentStatus, cmdUsername, livreurAssign)
- // â
ĂTAPE 3: VĂ©rifier que le statut permet la validation
validStatuses := []string{"assigned", "en_route", "pending", "livre"}
- isValid := false
- for _, s := range validStatuses {
- if currentStatus == s {
- isValid = true
- break
- }
- }
+ isValid := slices.Contains(validStatuses, currentStatus)
if !isValid {
log.Printf("â [ValidateAtomic] 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" {
log.Printf("â ïž [ValidateAtomic] Commande %d dĂ©jĂ approuvĂ©e", commandID)
return 0, fmt.Errorf("commande déjà approuvée")
}
- // â
ĂTAPE 5: UPDATE avec vĂ©rification du statut (protection race condition)
result, err := tx.Exec(`
UPDATE commandes
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")
}
- log.Printf("â
[ValidateAtomic] Statut mis Ă jour: %s â approved", currentStatus)
-
- // â
ĂTAPE 6: Calculer et ajouter les points au client
totalPoints := 0
if 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)
if err != nil {
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)
- // â
ĂTAPE 7: IncrĂ©menter le compteur de commandes du client
_, err = tx.Exec(`
UPDATE clients
SET command = command + 1, updated_at = CURRENT_TIMESTAMP
@@ -901,13 +733,11 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (
`, cmdUsername)
if err != nil {
log.Printf("â ïž [ValidateAtomic] Erreur incrĂ©mentation compteur: %v", err)
- // On ne bloque pas pour ça
} else {
log.Printf("â
[ValidateAtomic] Compteur commandes incrémenté pour %s", cmdUsername)
}
}
- // â
ĂTAPE 8: Ajouter un log dans command_logs
_, err = tx.Exec(`
INSERT INTO command_logs (command_id, status, message, author, created_at)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)
@@ -917,10 +747,8 @@ func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (
if err != nil {
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 {
log.Printf("â [ValidateAtomic] Erreur COMMIT: %v", 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",
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 != "" {
log.Printf("đŠ [ValidateAtomic] Optimisation queue pour livreur: %s", livreurAssign)
go func() {
- // Async pour ne pas bloquer la réponse
err := d.CompleteDeliveryAndProcessNext(livreurAssign, commandID)
if err != nil {
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() {
- // Async pour ne pas bloquer
commandCacheKey := fmt.Sprintf("command:%d", commandID)
Redis.Del(RedisCtx, commandCacheKey)
diff --git a/backend/gestion/db/db_delivery.go b/backend/gestion/db/db_delivery.go
index bcb1eade..698d5905 100644
--- a/backend/gestion/db/db_delivery.go
+++ b/backend/gestion/db/db_delivery.go
@@ -4,11 +4,12 @@ import (
"database/sql"
"fmt"
"log"
+ "slices"
"time"
)
// 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
FROM users
WHERE role = 'livreur'
@@ -20,7 +21,7 @@ func (d *Database) GetAvailableDeliveryPersons() ([]map[string]interface{}, erro
}
defer rows.Close()
- var livreurs []map[string]interface{}
+ var livreurs []map[string]any
for rows.Next() {
var id int
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)
}
- livreur := map[string]interface{}{
+ livreur := map[string]any{
"id": id,
"username": username,
"total": total,
@@ -47,17 +48,13 @@ func (d *Database) GetAvailableDeliveryPersons() ([]map[string]interface{}, erro
func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) error {
log.Printf("đŠ [AssignDeliveryPerson] START - commandID=%d, livreur=%s", commandID, livreurUsername)
- // â
DĂMARRER UNE TRANSACTION
tx, err := d.Begin()
if err != nil {
log.Printf("â Erreur dĂ©marrage transaction: %v", 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
checkQuery := `SELECT role FROM users WHERE username = $1 FOR UPDATE`
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")
}
- 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 currentLivreur sql.NullString
statusQuery := `SELECT status, livreur_assign
FROM commandes
WHERE id = $1
- FOR UPDATE` // â ïž VERROUILLAGE CRITIQUE
+ FOR UPDATE`
err = tx.QueryRow(statusQuery, commandID).Scan(¤tStatus, ¤tLivreur)
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)
}
- 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"}
- isValidStatus := false
- for _, vs := range validStatusesForAssignment {
- if currentStatus == vs {
- isValidStatus = true
- break
- }
- }
+ isValidStatus := slices.Contains(validStatusesForAssignment, currentStatus)
if !isValidStatus {
log.Printf("â Statut invalide pour assignation: %s", 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
SET livreur_assign = $1,
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é)")
}
- 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)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)`
@@ -163,28 +130,22 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e
// Non bloquant
}
- // ============================================
- // â
COMMIT de la transaction
- // ============================================
err = tx.Commit()
if err != nil {
log.Printf("â Erreur COMMIT: %v", 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
}
// 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
FROM commandes
WHERE livreur_assign = $1`
- args := []interface{}{livreurUsername}
+ args := []any{livreurUsername}
if status != "" {
query += " AND status = $2"
@@ -199,7 +160,7 @@ func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status stri
}
defer rows.Close()
- var commands []map[string]interface{}
+ var commands []map[string]any
for rows.Next() {
var id int
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)
}
- command := map[string]interface{}{
+ command := map[string]any{
"id": id,
"username": username,
"status": status,
@@ -228,7 +189,6 @@ func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status stri
return commands, nil
}
-// â
NOUVELLE MĂTHODE: IncrementLivreurDeliveryCount incrĂ©mente le compteur de livraisons d'un livreur
func (d *Database) IncrementLivreurDeliveryCount(livreurUsername string) error {
query := `UPDATE users
SET livraison = livraison + 1,
@@ -249,14 +209,10 @@ func (d *Database) IncrementLivreurDeliveryCount(livreurUsername string) error {
return fmt.Errorf("livreur non trouvé")
}
- log.Printf("â
Livraison incrémentée pour le livreur: %s", livreurUsername)
return nil
}
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()
if err != nil {
log.Printf("â Erreur dĂ©marrage transaction: %v", err)
@@ -264,16 +220,13 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
}
defer tx.Rollback()
- // ============================================
- // ĂTAPE 1: VĂ©rifier et VERROUILLER la commande
- // ============================================
var commandUsername, currentStatus string
var livreurAssign sql.NullString
checkQuery := `SELECT username, status, livreur_assign
FROM commandes
WHERE id = $1
- FOR UPDATE` // â ïž VERROUILLAGE CRITIQUE
+ FOR UPDATE`
err = tx.QueryRow(checkQuery, commandID).Scan(&commandUsername, ¤tStatus, &livreurAssign)
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)
}
- // ============================================
- // ĂTAPE 2: Validations mĂ©tier
- // ============================================
-
- // â
Vérifier que c'est bien la commande du client
if commandUsername != clientUsername {
return fmt.Errorf("cette commande ne vous appartient pas")
}
- // â
Vérifier le statut
if currentStatus != "livre" {
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
SET status = 'approved',
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")
}
- log.Printf(" â
Statut mis Ă jour: livre â approved")
-
- // ============================================
- // ĂTAPE 4: IncrĂ©menter le compteur du livreur (ATOMIQUE)
- // ============================================
if livreurAssign.Valid && livreurAssign.String != "" {
incrementQuery := `UPDATE users
SET livraison = livraison + 1,
@@ -337,7 +274,6 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
result, err := tx.Exec(incrementQuery, livreurAssign.String)
if err != nil {
log.Printf("â ïž Erreur incrĂ©mentation livreur: %v", err)
- // Non bloquant mais on continue dans la transaction
} else {
rows, _ := result.RowsAffected()
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)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)`
@@ -357,20 +290,13 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
clientUsername)
if err != nil {
log.Printf("â ïž Erreur ajout log: %v", err)
- // Non bloquant
}
- // ============================================
- // â
COMMIT de la transaction
- // ============================================
err = tx.Commit()
if err != nil {
log.Printf("â Erreur COMMIT: %v", 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
}
diff --git a/backend/gestion/db/db_delivery_issues.go b/backend/gestion/db/db_delivery_issues.go
index 7cf6bb07..4922718c 100644
--- a/backend/gestion/db/db_delivery_issues.go
+++ b/backend/gestion/db/db_delivery_issues.go
@@ -8,13 +8,13 @@ import (
func (d *Database) GetDeliveryIssues(status string) ([]models.DeliveryIssue, error) {
query := `
- SELECT id, command_id, issue_type, description, status,
+ SELECT id, command_id, issue_type, description, status,
reported_by, COALESCE(resolved_by, ''), COALESCE(resolution, ''),
created_at, updated_at
FROM delivery_issues
`
- var args []interface{}
+ var args []any
if status != "" {
query += " WHERE status = $1"
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)
}
- log.Printf("â
ProblÚme créé: ID=%d, Type=%s, Commande=%d", issue.ID, issueType, commandID)
return &issue, nil
}
diff --git a/backend/gestion/db/db_delivery_manage.go b/backend/gestion/db/db_delivery_manage.go
index d70d0b44..ff09a12b 100644
--- a/backend/gestion/db/db_delivery_manage.go
+++ b/backend/gestion/db/db_delivery_manage.go
@@ -19,10 +19,6 @@ var allowedStatuses = map[string]bool{
"busy": true,
}
-// ============================================
-// đ STATISTIQUES LIVREUR
-// ============================================
-
// CountDeliveriesByStatus compte les livraisons d'un livreur par statut
func (d *Database) CountDeliveriesByStatus(livreurUsername string, statuses string) (int, error) {
if statuses == "" {
@@ -231,17 +227,6 @@ func (d *Database) GetDeliveryPersonHistory(livreurUsername string, limit, offse
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
func (d *Database) GetDeliveryPersonStatus(livreurUsername string) (string, error) {
statusKey := fmt.Sprintf("delivery:status:%s", livreurUsername)
@@ -277,10 +262,6 @@ func (d *Database) UpdateDeliveryPersonStatus(livreurUsername string, status str
return nil
}
-// ============================================
-// đŠ GESTION QUEUE LIVREUR
-// ============================================
-
// GetDeliverymanQueueSize récupÚre la taille de la queue d'un livreur
func (d *Database) GetDeliverymanQueueSize(livreurUsername string) (int, error) {
queueKey := fmt.Sprintf("delivery:queue:%s", livreurUsername)
@@ -312,10 +293,6 @@ func (d *Database) GetDeliverymanQueue(livreurUsername string) ([]int, error) {
return queue, nil
}
-// ============================================
-// đ§ FONCTIONS UTILITAIRES
-// ============================================
-
// UpdateCommandLivreur met à jour le livreur assigné à une commande
func (d *Database) UpdateCommandLivreur(commandID int, livreurUsername string) error {
query := `UPDATE commandes
@@ -338,19 +315,15 @@ func (d *Database) UpdateCommandLivreur(commandID int, livreurUsername string) e
return nil
}
-// ============================================
-// đ STATISTIQUES SYSTĂME
-// ============================================
-
// 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
livreurs, err := d.GetAvailableDeliveryPersons()
if err != nil {
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 {
username := livreur["username"].(string)
@@ -361,7 +334,7 @@ func (d *Database) GetAllDeliveryPersonsStats() ([]map[string]interface{}, error
queueSize, _ := d.GetDeliverymanQueueSize(username)
status, _ := d.GetDeliveryPersonStatus(username)
- statEntry := map[string]interface{}{
+ statEntry := map[string]any{
"username": username,
"total_deliveries": totalDeliveries,
"completed_deliveries": completedDeliveries,
@@ -375,10 +348,6 @@ func (d *Database) GetAllDeliveryPersonsStats() ([]map[string]interface{}, error
return stats, nil
}
-// ============================================
-// đ RECHERCHE & FILTRAGE
-// ============================================
-
// GetDeliveryPersonsByStatus récupÚre les livreurs par statut
func (d *Database) GetDeliveryPersonsByStatus(status string) ([]string, error) {
// Récupérer tous les livreurs
@@ -411,10 +380,6 @@ func (d *Database) GetAvailableDeliveryPersonsCount() (int, error) {
return len(availableLivreurs), nil
}
-// ============================================
-// đïž SUPPRESSION & NETTOYAGE
-// ============================================
-
// ClearDeliveryPersonData supprime toutes les données d'un livreur (admin uniquement)
func (d *Database) ClearDeliveryPersonData(livreurUsername string) error {
log.Printf("đïž [ClearDeliveryData] Nettoyage donnĂ©es pour: %s", livreurUsername)
diff --git a/backend/gestion/db/db_delivery_mode.go b/backend/gestion/db/db_delivery_mode.go
new file mode 100644
index 00000000..d880f369
--- /dev/null
+++ b/backend/gestion/db/db_delivery_mode.go
@@ -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
+}
diff --git a/backend/gestion/db/db_gps_link.go b/backend/gestion/db/db_gps_link.go
index dbab9cf2..3a33b86c 100644
--- a/backend/gestion/db/db_gps_link.go
+++ b/backend/gestion/db/db_gps_link.go
@@ -7,7 +7,6 @@ package db
import (
"fmt"
- "net/url"
)
// MapLinks contient les liens vers différentes plateformes de cartographie
@@ -22,126 +21,48 @@ type MapLinks struct {
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
func (d *Database) GenerateMapLinks(lat, lon float64, label string) MapLinks {
- // Encoder le label pour l'URL
- encodedLabel := url.QueryEscape(label)
-
return MapLinks{
- // Google Maps (Web)
- 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,
- ),
+ WazeApp: wazeAppLink(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 {
- switch platform {
- 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,
- )
- }
+ return wazeAppLink(toLat, toLon)
}
// 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) {
- // 1. Récupérer la position du livreur
- livreurLat, livreurLon, err := d.GetDeliveryPersonLocation(deliverymanUsername)
+ _, _, err := d.GetDeliveryPersonLocation(deliverymanUsername)
if err != nil {
return nil, fmt.Errorf("position livreur non disponible: %w", err)
}
- // 2. Récupérer la destination de la commande
command, err := d.GetCommandByID(commandID)
if err != nil {
return nil, fmt.Errorf("commande non trouvée: %w", err)
}
var destLat, destLon float64
- if command["dest_latitude"] != nil {
- if latVal, ok := command["dest_latitude"].(float64); ok {
- destLat = latVal
- }
+ if v, ok := command["dest_latitude"].(float64); ok {
+ destLat = v
}
- 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 {
return nil, fmt.Errorf("coordonnées destination invalides")
}
- // 3. Générer les liens de navigation
return map[string]string{
- "google_maps": d.GenerateNavigationLink(livreurLat, livreurLon, destLat, destLon, "google"),
- "waze": d.GenerateNavigationLink(livreurLat, livreurLon, destLat, destLon, "waze"),
- "apple_maps": d.GenerateNavigationLink(livreurLat, livreurLon, destLat, destLon, "apple"),
+ "waze_app": wazeAppLink(destLat, destLon),
}, nil
}
diff --git a/backend/gestion/db/db_history.go b/backend/gestion/db/db_history.go
index 36f87d4e..6c77dfcd 100644
--- a/backend/gestion/db/db_history.go
+++ b/backend/gestion/db/db_history.go
@@ -9,17 +9,14 @@ import (
"database/sql"
"fmt"
"log"
+ "maps"
"time"
)
// GetCompletedCommandsByUsername récupÚre toutes les commandes terminées (approved) d'un utilisateur
-// â
Retourne uniquement les commandes avec status = "approved"
-// â
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)
-
+func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string]any, error) {
query := `
- SELECT
+ SELECT
id,
username,
status,
@@ -40,7 +37,7 @@ func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string
}
defer rows.Close()
- var commands []map[string]interface{}
+ var commands []map[string]any
for rows.Next() {
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)
}
- command := map[string]interface{}{
+ command := map[string]any{
"id": id,
"username": username,
"status": status,
@@ -74,7 +71,6 @@ func (d *Database) GetCompletedCommandsByUsername(username string) ([]map[string
"updated_at": updatedAt,
}
- // Ajouter livreur_assign seulement s'il n'est pas NULL
if livreurAssign.Valid {
command["livreur_assign"] = livreurAssign.String
} 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)
}
- log.Printf("â
[GetCompletedCommands] %d commandes terminées trouvées pour %s", len(commands), username)
return commands, nil
}
// 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]interface{}, error) {
- log.Printf("đ [GetCompletedWithItems] START - username=%s", username)
-
- // 1. Récupérer les commandes terminées
+func (d *Database) GetCompletedCommandsWithItems(username string) ([]map[string]any, error) {
commands, err := d.GetCompletedCommandsByUsername(username)
if err != nil {
return nil, err
}
- // 2. Pour chaque commande, récupérer les items
- var enrichedCommands []map[string]interface{}
+ var enrichedCommands []map[string]any
for _, command := range commands {
commandID, ok := command["id"].(int)
@@ -117,31 +107,24 @@ func (d *Database) GetCompletedCommandsWithItems(username string) ([]map[string]
items, err := d.GetCommandItems(commandID)
if err != nil {
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]interface{})
- for k, v := range command {
- enrichedCommand[k] = v
- }
+ enrichedCommand := make(map[string]any)
+ maps.Copy(enrichedCommand, command)
enrichedCommand["items"] = items
enrichedCommand["items_count"] = len(items)
enrichedCommands = append(enrichedCommands, enrichedCommand)
}
- log.Printf("â
[GetCompletedWithItems] %d commandes enrichies", len(enrichedCommands))
return enrichedCommands, nil
}
// 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]interface{}, error) {
- log.Printf("đ [GetCommandsStats] START - username=%s", username)
-
+func (d *Database) GetCommandsStatsByUsername(username string) (map[string]any, error) {
query := `
- SELECT
+ SELECT
COUNT(*) FILTER (WHERE status = 'approved') as approved_count,
COUNT(*) FILTER (WHERE status = 'pending') as pending_count,
COUNT(*) FILTER (WHERE status = 'assigned') as assigned_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)
}
- stats := map[string]interface{}{
+ stats := map[string]any{
"approved_count": approvedCount,
"pending_count": pendingCount,
"assigned_count": assignedCount,
@@ -183,19 +166,15 @@ func (d *Database) GetCommandsStatsByUsername(username string) (map[string]inter
"total_spent": totalSpent,
}
- log.Printf("â
[GetCommandsStats] Stats calculĂ©es: total=%d, approved=%d, spent=%.2fâŹ",
- totalCount, approvedCount, totalSpent)
-
return stats, nil
}
// 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]interface{}, error) {
+func (d *Database) GetCommandsByStatus(username, status string) ([]map[string]any, error) {
log.Printf("đ [GetCommandsByStatus] START - username=%s, status=%s", username, status)
query := `
- SELECT
+ SELECT
id,
username,
status,
@@ -211,12 +190,11 @@ func (d *Database) GetCommandsByStatus(username, status string) ([]map[string]in
rows, err := d.Query(query, username, status)
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)
}
defer rows.Close()
- var commands []map[string]interface{}
+ var commands []map[string]any
for rows.Next() {
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)
}
- command := map[string]interface{}{
+ command := map[string]any{
"id": id,
"username": username,
"status": status,
@@ -260,21 +238,16 @@ func (d *Database) GetCommandsByStatus(username, status string) ([]map[string]in
}
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)
}
-
- log.Printf("â
[GetCommandsByStatus] %d commandes trouvées", len(commands))
return commands, nil
}
// 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]interface{}, error) {
- log.Printf("đ [GetRecentCompleted] START - username=%s, limit=%d", username, limit)
+func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[string]any, error) {
query := `
- SELECT
+ SELECT
id,
username,
status,
@@ -296,7 +269,7 @@ func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[s
}
defer rows.Close()
- var commands []map[string]interface{}
+ var commands []map[string]any
for rows.Next() {
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)
}
- command := map[string]interface{}{
+ command := map[string]any{
"id": id,
"username": username,
"status": status,
@@ -340,10 +313,8 @@ func (d *Database) GetRecentCompletedOrders(username string, limit int) ([]map[s
}
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)
}
- log.Printf("â
[GetRecentCompleted] %d commandes récentes trouvées", len(commands))
return commands, nil
}
diff --git a/backend/gestion/db/db_init.go b/backend/gestion/db/db_init.go
index 51db1936..7b021b5e 100644
--- a/backend/gestion/db/db_init.go
+++ b/backend/gestion/db/db_init.go
@@ -2,7 +2,6 @@ package db
import (
"database/sql"
- "encoding/json"
"fmt"
"log"
"os"
@@ -197,37 +196,6 @@ func InitDB() *Database {
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
go database.cleanExpiredTokensPeriodically()
@@ -267,8 +235,6 @@ func (db *Database) createTables() error {
prenom VARCHAR(100) NOT NULL,
telephone VARCHAR(20) NOT NULL UNIQUE,
command INTEGER DEFAULT 0,
- point INTEGER DEFAULT 0,
- point_zipette INTEGER DEFAULT 0,
amende NUMERIC(10,2) DEFAULT 0.0,
cancel_commande INTEGER DEFAULT 0,
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_status ON command_items(status);`,
`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_token ON jwt_tokens(token);`,
`CREATE INDEX IF NOT EXISTS idx_jwt_date_fin ON jwt_tokens(date_fin);`,
diff --git a/backend/gestion/db/db_jwt_tokens.go b/backend/gestion/db/db_jwt_tokens.go
index 79cb8434..640de53a 100644
--- a/backend/gestion/db/db_jwt_tokens.go
+++ b/backend/gestion/db/db_jwt_tokens.go
@@ -79,7 +79,7 @@ func (d *Database) RevokeAllUserTokens(userID int, userType string) error {
}
// 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
FROM jwt_tokens
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()
- var tokens []map[string]interface{}
+ var tokens []map[string]any
for rows.Next() {
var id int
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)
}
- tokens = append(tokens, map[string]interface{}{
+ tokens = append(tokens, map[string]any{
"id": id,
"token": token[:20] + "...", // Tronquer pour la sécurité
"date_save": dateSave,
@@ -114,7 +114,7 @@ func (d *Database) GetUserActiveTokens(userID int, userType string) ([]map[strin
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
FROM jwt_tokens
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)
}
- tokenInfo := map[string]interface{}{
+ tokenInfo := map[string]any{
"user_id": userID,
"user_type": userType,
"date_save": dateSave,
diff --git a/backend/gestion/db/db_media.go b/backend/gestion/db/db_media.go
index 0be19f85..54140cf0 100644
--- a/backend/gestion/db/db_media.go
+++ b/backend/gestion/db/db_media.go
@@ -5,13 +5,17 @@ import (
"fmt"
"gestion/models"
"log"
+ "slices"
"strings"
"time"
)
-// ============================================
-// VALIDATION HELPERS
-// ============================================
+type MediaInterface interface {
+ GetProductID() int
+ GetType() string
+ GetURL() string
+ SetID(int)
+}
// validateMediaID vérifie la validité d'un ID média
func validateMediaID(mediaID int) error {
@@ -40,13 +44,9 @@ func validateMediaType(mediaType string) error {
validTypes := []string{"image", "video"}
mediaType = strings.ToLower(strings.TrimSpace(mediaType))
-
- for _, valid := range validTypes {
- if mediaType == valid {
- return nil
- }
+ if slices.Contains(validTypes, mediaType) {
+ return nil
}
-
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
- if strings.Contains(url, "..") {
+ if strings.Contains(url, "..") || strings.Contains(url, "...") || strings.Contains(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
// ============================================
-func (db *Database) CreateMedia(media interface{}) error {
+func (db *Database) CreateMedia(media any) error {
log.Printf("đ [CreateMedia] START - Type: %T", media)
- type MediaInterface interface {
- GetProductID() int
- GetType() string
- GetURL() string
- SetID(int)
- }
-
// â
TYPE ASSERTION SĂCURISĂE
m, ok := media.(MediaInterface)
if !ok {
@@ -115,32 +108,33 @@ func (db *Database) CreateMedia(media interface{}) error {
return fmt.Errorf("type de média invalide")
}
- // â
VALIDATION COMPLĂTE
productID := m.GetProductID()
mediaType := m.GetType()
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 {
log.Printf("â [CreateMedia] %v", err)
return err
}
- // Valider le type
if err := validateMediaType(mediaType); err != nil {
log.Printf("â [CreateMedia] %v", err)
return err
}
- // Valider l'URL
if err := validateMediaURL(mediaURL); err != nil {
log.Printf("â [CreateMedia] %v", 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
checkQuery := `SELECT EXISTS(SELECT 1 FROM products WHERE id = $1)`
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)
}
- // â
INSĂRER LE MĂDIA
query := `INSERT INTO media (product_id, url, type, created_at)
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)
return fmt.Errorf("erreur création média: %w", err)
}
-
- m.SetID(mediaID)
+ if mi, ok := m.(MediaInterface); ok {
+ mi.SetID(mediaID)
+ }
log.Printf("â
[CreateMedia] Média créé: ID=%d, Type=%s", mediaID, mediaType)
-
return nil
}
@@ -274,9 +267,6 @@ func (db *Database) GetMediaByProductID(productID int) ([]models.Media, error) {
// ============================================
func (db *Database) UpdateMedia(media *models.Media) error {
- log.Printf("đ [UpdateMedia] START - ID=%d", media.ID)
-
- // â
VALIDATION
if media == 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
- 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)
+ if err := db.CheckMediaExists(media); err != nil {
+ log.Printf("â [UpdateMedia] %v", err)
+ return err
}
// â
UPDATE
@@ -331,33 +314,33 @@ func (db *Database) UpdateMedia(media *models.Media) error {
return nil
}
-// ============================================
-// DELETE MEDIA - VERSION SĂCURISĂE
-// ============================================
+func (db *Database) CheckMediaExists(media *models.Media) error {
+ 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 {
- log.Printf("đïž [DeleteMedia] START - ID=%d", mediaID)
-
- // â
VALIDATION
if err := validateMediaID(mediaID); err != nil {
log.Printf("â [DeleteMedia] %v", err)
return err
}
- // â
VĂRIFIER QUE LE MĂDIA EXISTE
- var exists bool
- checkQuery := `SELECT EXISTS(SELECT 1 FROM media WHERE id = $1)`
- err := db.QueryRow(checkQuery, mediaID).Scan(&exists)
- 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)
+ media := models.Media{ID: mediaID}
+ if err := db.CheckMediaExists(&media); err != nil {
+ log.Printf("â [DeleteMedia] %v", err)
+ return err
}
- // â
DELETE
query := `DELETE FROM media WHERE id = $1`
result, err := db.Exec(query, mediaID)
@@ -377,20 +360,14 @@ func (db *Database) DeleteMedia(mediaID int) error {
return nil
}
-// ============================================
-// DELETE MEDIA BY PRODUCT ID - VERSION SĂCURISĂE
-// ============================================
-
func (db *Database) DeleteMediaByProductID(productID int) error {
log.Printf("đïž [DeleteMediaByProductID] START - ProductID=%d", productID)
- // â
VALIDATION
if err := validateProductID(productID); err != nil {
log.Printf("â [DeleteMediaByProductID] %v", err)
return err
}
- // â
VĂRIFIER QUE LE PRODUIT EXISTE
var exists bool
checkQuery := `SELECT EXISTS(SELECT 1 FROM products WHERE id = $1)`
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)
}
- // â
DELETE
query := `DELETE FROM media WHERE product_id = $1`
result, err := db.Exec(query, productID)
diff --git a/backend/gestion/db/db_notifications.go b/backend/gestion/db/db_notifications.go
index 758a1f37..31d77058 100644
--- a/backend/gestion/db/db_notifications.go
+++ b/backend/gestion/db/db_notifications.go
@@ -1,12 +1,10 @@
package db
import (
- "bytes"
- "database/sql"
"encoding/json"
"fmt"
+ "gestion/services"
"log"
- "net/http"
"time"
)
@@ -14,7 +12,7 @@ func (d *Database) NotifyClient(username string, commandID int, notifType, messa
// Sauvegarder la notification dans Redis
notifKey := fmt.Sprintf("notifications:%s", username)
- notification := map[string]interface{}{
+ notification := map[string]any{
"command_id": commandID,
"type": notifType,
"message": message,
@@ -26,90 +24,22 @@ func (d *Database) NotifyClient(username string, commandID int, notifType, messa
Redis.LPush(RedisCtx, notifKey, notifJSON)
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
- // Envoyer push notification si le client a un token enregistré
- pushToken, err := d.GetClientPushToken(username)
- if err == nil && pushToken != "" {
- go sendExpoPush(pushToken, "Uber Stup", message, commandID, notifType)
+ // Diffusion Telegram si le client a lié son compte
+ if services.TelegramBot != nil && services.TelegramBot.IsConfigured() {
+ if chatID, ok, err := d.GetClientTelegramChatID(username); err == nil && ok {
+ go services.TelegramBot.SendMessage(chatID, fmt.Sprintf("đ Notification\n\n%s", message))
+ }
}
log.Printf("đŹ Notification envoyĂ©e Ă %s: %s", username, message)
return nil
}
-func sendExpoPush(token, title, body string, commandID int, notifType string) {
- 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
+// NotifyLivreur envoie une notification in-app (Redis) Ă un livreur
func (d *Database) NotifyLivreur(username string, commandID int, notifType, message string) error {
notifKey := fmt.Sprintf("notifications:%s", username)
- notification := map[string]interface{}{
+ notification := map[string]any{
"command_id": commandID,
"type": notifType,
"message": message,
@@ -121,10 +51,11 @@ func (d *Database) NotifyLivreur(username string, commandID int, notifType, mess
Redis.LPush(RedisCtx, notifKey, notifJSON)
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
- // Push notification si le livreur a un token enregistré
- pushToken, err := d.GetUserPushToken(username)
- if err == nil && pushToken != "" {
- go sendExpoPushWithChannel(pushToken, "Nouvelle commande assignée", message, commandID, notifType, "deliveries")
+ // Diffusion Telegram si le livreur a lié son compte
+ if services.TelegramBot != nil && services.TelegramBot.IsConfigured() {
+ if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok {
+ go services.TelegramBot.SendMessage(chatID, fmt.Sprintf("đ Notification\n\n%s", 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.
func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryAddr string) {
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 {
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)
- notification := map[string]interface{}{
+ notification := map[string]any{
"command_id": commandID,
"type": "new_order",
"message": msg,
@@ -154,29 +85,34 @@ func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryA
}
notifJSON, _ := json.Marshal(notification)
- sent := 0
+ count := 0
for rows.Next() {
- var username, token string
- if err := rows.Scan(&username, &token); err != nil {
+ var username string
+ if err := rows.Scan(&username); err != nil {
continue
}
notifKey := fmt.Sprintf("notifications:%s", username)
Redis.LPush(RedisCtx, notifKey, notifJSON)
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
- if token != "" {
- go sendExpoPushWithChannel(token, "Nouvelle commande", msg, commandID, "new_order", "orders")
- sent++
+ // Diffusion Telegram individuelle
+ if services.TelegramBot != nil && services.TelegramBot.IsConfigured() {
+ if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok {
+ capturedChatID := chatID
+ capturedMsg := msg
+ go services.TelegramBot.SendMessage(capturedChatID, fmt.Sprintf("đ Nouvelle commande\n\n%s", capturedMsg))
+ }
}
+ count++
}
- log.Printf("đŹ [ADMIN_NOTIF] Notif Redis + push (%d tokens) pour commande #%d", sent, commandID)
+ log.Printf("đŹ [ADMIN_NOTIF] Notif Redis (%d users) pour commande #%d", count, commandID)
}
// NotifyAllAdminCabineAlert envoie une notification Redis + push Ă tous les admins/cabines
// lors du déclenchement d'une alerte par un livreur.
func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alertMessage string) {
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 {
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()
- title := "đš Alerte livreur"
body := fmt.Sprintf("%s â livreur : %s", alertMessage, livreurUsername)
- notification := map[string]interface{}{
+ notification := map[string]any{
"alert_id": alertID,
"type": "alert",
"message": body,
@@ -196,22 +131,27 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert
}
notifJSON, _ := json.Marshal(notification)
- sent := 0
+ count := 0
for rows.Next() {
- var username, token string
- if err := rows.Scan(&username, &token); err != nil {
+ var username string
+ if err := rows.Scan(&username); err != nil {
continue
}
notifKey := fmt.Sprintf("notifications:%s", username)
Redis.LPush(RedisCtx, notifKey, notifJSON)
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
- if token != "" {
- go sendExpoPushWithChannel(token, title, body, alertID, "alert", "orders")
- sent++
+ // Diffusion Telegram individuelle
+ if services.TelegramBot != nil && services.TelegramBot.IsConfigured() {
+ if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok {
+ capturedChatID := chatID
+ capturedBody := body
+ go services.TelegramBot.SendMessage(capturedChatID, fmt.Sprintf("đš Alerte livreur\n\n%s", capturedBody))
+ }
}
+ count++
}
- log.Printf("đš [ALERT_NOTIF] Notif Redis + push (%d tokens) pour alerte #%d de %s", sent, alertID, livreurUsername)
+ log.Printf("đš [ALERT_NOTIF] Notif Redis (%d users) pour alerte #%d de %s", count, alertID, livreurUsername)
}
// AddDeliveryRating ajoute une note pour un livreur
diff --git a/backend/gestion/db/db_product.go b/backend/gestion/db/db_product.go
index ad1e5da6..09c19c1f 100644
--- a/backend/gestion/db/db_product.go
+++ b/backend/gestion/db/db_product.go
@@ -9,7 +9,7 @@ import (
)
// 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] Valeur: %+v", product)
@@ -27,7 +27,6 @@ func (db *Database) CreateProduct(product interface{}) error {
p, ok := product.(ProductInterface)
if !ok {
- // Vérifier si c'est un pointeur vers models.Product
if prodPtr, isPtr := product.(*models.Product); isPtr {
log.Printf("â
[DB CreateProduct] C'est un *models.Product, utilisons-le directement")
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
}
-func (db *Database) UpdateProduct(productID int, product interface{}) error {
- // à implémenter selon vos besoins
+func (db *Database) UpdateProduct(productID int, name, category, description, unit string, stock float64, prices []models.ProductPrice) error {
+ _, 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
}
diff --git a/backend/gestion/db/db_queue_auto_next.go b/backend/gestion/db/db_queue_auto_next.go
index 83f9b599..6bc17060 100644
--- a/backend/gestion/db/db_queue_auto_next.go
+++ b/backend/gestion/db/db_queue_auto_next.go
@@ -10,10 +10,10 @@ import (
"fmt"
"gestion/models"
"log"
+ "slices"
)
// 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 {
log.Printf("đ [NEXT_COMMAND] Traitement prochaine commande pour %s", deliveryman)
@@ -34,9 +34,6 @@ func (d *Database) ProcessNextCommandForDeliveryman(deliveryman string) error {
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 {
commandID := extractCommandID(cmdIDStr)
if commandID <= 0 {
@@ -74,32 +71,16 @@ func (d *Database) ProcessNextCommandForDeliveryman(deliveryman string) error {
currentStatus, _ := command["status"].(string)
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"}
- isNonAssignable := false
- 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)
+ if slices.Contains(nonAssignableStatuses, currentStatus) {
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)
- // â
Mettre à jour le statut basé sur la queue
d.UpdateDeliverymanStatusBasedOnQueue(deliveryman)
- // Ajouter un log
d.AddCommandLog(commandID, "next_in_queue",
fmt.Sprintf("Commande suivante dans la queue de %s", deliveryman),
"system")
@@ -107,9 +88,6 @@ func (d *Database) ProcessNextCommandForDeliveryman(deliveryman string) error {
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
d.SetDeliveryPersonStatus(deliveryman, "available", 0)
d.UpdateDeliverymanStatusBasedOnQueue(deliveryman)
@@ -138,10 +116,8 @@ func (d *Database) RemoveCommandFromDeliverymanQueue(deliveryman string, command
log.Printf("â
[RemoveFromDeliverymanQueue] Commande %d retirée", commandID)
}
- // Supprimer les données
Redis.Del(RedisCtx, commandKey)
- // Décrémenter le compteur
counterKey := fmt.Sprintf("queue:deliveryman:%s:count", deliveryman)
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
-// Ă appeler depuis les handlers d'annulation et de livraison
func (d *Database) CleanupCompletedCommandFromQueue(commandID int, deliveryman string) error {
log.Printf("đ§č [CLEANUP] Nettoyage commande %d pour %s", commandID, deliveryman)
diff --git a/backend/gestion/db/db_referral.go b/backend/gestion/db/db_referral.go
index 8e7df660..ecea58ab 100644
--- a/backend/gestion/db/db_referral.go
+++ b/backend/gestion/db/db_referral.go
@@ -70,13 +70,7 @@ func (d *Database) DebitReferralBalance(username string, amount float64) error {
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.
-// Retourne une erreur si le solde est insuffisant.
func (d *Database) UseClientReferralBalance(tx *sql.Tx, username string, amount float64) error {
if amount <= 0 {
return nil
diff --git a/backend/gestion/db/db_sanction_client.go b/backend/gestion/db/db_sanction_client.go
index 43d1fb0e..53935b9f 100644
--- a/backend/gestion/db/db_sanction_client.go
+++ b/backend/gestion/db/db_sanction_client.go
@@ -7,7 +7,9 @@ package db
import (
"fmt"
+ "gestion/models"
"log"
+ "sort"
)
// 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é")
}
- log.Printf("â
[IncrementCancellations] Compteur incrémenté pour %s", username)
-
- // Invalider le cache Redis
cacheKey := fmt.Sprintf("client:%s", username)
Redis.Del(RedisCtx, cacheKey)
return nil
}
-// CalculateCancellationPenalty calcule la pénalité selon l'historique
-// 1Ăšre fois: 20 points
-// 2Ăšme fois: 50 points
-// 3Ăšme fois: 100 points
-// 4Ăšme+ fois: 150 points
+// penaltyForCount retourne le montant du palier applicable pour un nombre d'annulations donné
+func penaltyForCount(count int, tiers []models.PenaltyTier) int {
+ if len(tiers) == 0 {
+ return 0
+ }
+ sorted := make([]models.PenaltyTier, len(tiers))
+ copy(sorted, tiers)
+ sort.Slice(sorted, func(i, j int) bool {
+ return sorted[i].MinCancel > sorted[j].MinCancel
+ })
+ for _, t := range sorted {
+ if count >= t.MinCancel {
+ return t.Amount
+ }
+ }
+ return sorted[len(sorted)-1].Amount
+}
+
+// CalculateCancellationPenalty calcule la pénalité selon l'historique et le barÚme configuré
func (d *Database) CalculateCancellationPenalty(username string) (int, error) {
count, err := d.GetClientCancellationsCount(username)
if err != nil {
return 0, err
}
- var penalty int
- switch {
- case count == 0:
- penalty = 20 // PremiĂšre annulation tardive
- 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
+ settings, err := d.GetSettings()
+ if err != nil {
+ log.Printf("â ïž [CalculatePenalty] Impossible de charger les settings, barĂšme par dĂ©faut: %v", err)
+ settings = DefaultSettings()
}
+ penalty := penaltyForCount(count, settings.PenaltyTiers)
+
log.Printf("đ° [CalculatePenalty] Client %s - Annulations: %d â PĂ©nalitĂ©: %d points",
username, count, penalty)
return penalty, nil
}
-// ApplyCancellationPenalty applique une pĂ©nalitĂ©, incrĂ©mente le compteur et REMET TOUS LES POINTS Ă ZĂRO
-// â
MODIFIĂ: RĂ©cupĂšre les points AVANT de les remettre Ă zĂ©ro pour le log dans le handler
+// ApplyCancellationPenalty applique une pénalité et incrémente le compteur d'annulations
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)
if err != nil {
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)
- // â
ĂTAPE 1: IncrĂ©menter le compteur d'annulations
if err := d.IncrementClientCancellationsCount(username); err != nil {
return 0, err
}
- // â
ĂTAPE 2: Mettre l'amende au montant de la pĂ©nalitĂ© ET remettre TOUS les points Ă zĂ©ro
query := `UPDATE clients
SET amende = $1,
- point = 0,
- point_zipette = 0,
updated_at = CURRENT_TIMESTAMP
WHERE username = $2`
@@ -124,9 +124,8 @@ func (d *Database) ApplyCancellationPenalty(username string) (int, error) {
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)
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
-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)
if err != nil {
return nil, err
}
- // Calculer la prochaine pénalité
- var nextPenalty int
- switch count {
- case 0:
- nextPenalty = 20
- case 1:
- nextPenalty = 50
- case 2:
- nextPenalty = 100
- default:
- nextPenalty = 150
- }
+ settings, _ := d.GetSettings()
- // Récupérer l'amende actuelle
client, err := d.GetClientByUsername(username)
var currentAmende float64
if err == nil {
currentAmende = client.Amende
}
- return map[string]interface{}{
+ return map[string]any{
"cancellations_count": count,
"current_amende": currentAmende,
"next_penalty": nextPenalty,
- "penalty_scale": map[string]int{
- "1st": 20,
- "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",
+ "penalty_tiers": settings.PenaltyTiers,
+ "warning": "Une amende sera appliquée lors de la prochaine annulation tardive",
}, nil
}
diff --git a/backend/gestion/db/db_settings.go b/backend/gestion/db/db_settings.go
index 4ab836d3..97f46ae0 100644
--- a/backend/gestion/db/db_settings.go
+++ b/backend/gestion/db/db_settings.go
@@ -3,52 +3,20 @@ package db
import (
"encoding/json"
"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)
-func DefaultDeliverySchedule() DeliverySchedule {
- day := DaySchedule{Enabled: true, OpenTime: "09:00", CloseTime: "20:00"}
- return DeliverySchedule{
+func DefaultDeliverySchedule() models.DeliverySchedule {
+ day := models.DaySchedule{Enabled: true, OpenTime: "09:00", CloseTime: "20:00"}
+ return models.DeliverySchedule{
Monday: day, Tuesday: day, Wednesday: day, Thursday: 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
-func CalcPointsFromTiers(total float64, tiers []PointsTier) int {
+func CalcPointsFromTiers(total float64, tiers []models.PointsTier) int {
for _, t := range tiers {
if total >= t.Min && (t.Max == 0 || total <= t.Max) {
return t.Points
@@ -57,44 +25,25 @@ func CalcPointsFromTiers(total float64, tiers []PointsTier) int {
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
-func DefaultSettings() AppSettings {
- return AppSettings{
+func DefaultSettings() models.AppSettings {
+ return models.AppSettings{
PenaltiesEnabled: true,
ShowAmendeScore: true,
- PointsEnabled: true,
- ReferralEnabled: true,
- PointsPools: []PointsPool{
+ PenaltyTiers: []models.PenaltyTier{
+ {MinCancel: 0, Amount: 20},
+ {MinCancel: 1, Amount: 50},
+ {MinCancel: 2, Amount: 100},
+ {MinCancel: 3, Amount: 150},
+ },
+ PointsEnabled: true,
+ ReferralEnabled: true,
+ PointsPools: []models.PointsPool{
{
Key: "pool_0",
Name: "Pool 1",
Categories: []string{},
- Tiers: []PointsTier{
+ Tiers: []models.PointsTier{
{Min: 30, Max: 50, Points: 1},
{Min: 60, Max: 150, Points: 2},
{Min: 160, Max: 300, Points: 3},
@@ -106,15 +55,19 @@ func DefaultSettings() AppSettings {
Key: "pool_1",
Name: "Pool 2",
Categories: []string{},
- Tiers: []PointsTier{
+ Tiers: []models.PointsTier{
{Min: 30, Max: 100, Points: 1},
{Min: 110, Max: 200, Points: 2},
{Min: 210, Max: 0, Points: 3},
},
},
},
+ DeliveryMode: models.DeliveryModeConfig{
+ Mode: "single",
+ CategoryRoutes: []models.CategoryRoute{},
+ },
DeliverySchedule: DefaultDeliverySchedule(),
- PostalZones: []PostalZone{
+ PostalZones: []models.PostalZone{
{Name: "Zone 30âŹ", MinAmount: 30, Codes: []string{"44000", "44100", "44200", "44300"}},
{Name: "Zone 50âŹ", MinAmount: 50, Codes: []string{
"44400", "44880", "44120", "44230", "44115",
@@ -128,7 +81,7 @@ func DefaultSettings() AppSettings {
}
// 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()
rows, err := d.Query(`SELECT key, value FROM app_settings`)
@@ -150,7 +103,7 @@ func (d *Database) GetSettings() (AppSettings, error) {
case "points_enabled":
settings.PointsEnabled = value == "true"
case "points_pools":
- var pools []PointsPool
+ var pools []models.PointsPool
if err := json.Unmarshal([]byte(value), &pools); err == nil {
settings.PointsPools = pools
}
@@ -170,22 +123,31 @@ func (d *Database) GetSettings() (AppSettings, error) {
settings.NowPaymentsCurrencies = currencies
}
case "delivery_schedule":
- var sched DeliverySchedule
+ var sched models.DeliverySchedule
if err := json.Unmarshal([]byte(value), &sched); err == nil {
settings.DeliverySchedule = sched
}
case "postal_zones":
- var zones []PostalZone
+ var zones []models.PostalZone
if err := json.Unmarshal([]byte(value), &zones); err == nil {
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()
}
// 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 {
if b {
return "true"
@@ -194,7 +156,7 @@ func (d *Database) UpdateSettings(s AppSettings) error {
}
if s.PointsPools == nil {
- s.PointsPools = []PointsPool{}
+ s.PointsPools = []models.PointsPool{}
}
// S'assurer que chaque pool a des slices non-nil
for i := range s.PointsPools {
@@ -202,7 +164,7 @@ func (d *Database) UpdateSettings(s AppSettings) error {
s.PointsPools[i].Categories = []string{}
}
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)})
if s.PostalZones == nil {
- s.PostalZones = []PostalZone{}
+ s.PostalZones = []models.PostalZone{}
}
zonesJSON, err := json.Marshal(s.PostalZones)
if err != nil {
return fmt.Errorf("erreur sérialisation postal_zones: %w", err)
}
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 {
if _, err = tx.Exec(upsert, p[0], p[1]); err != nil {
diff --git a/backend/gestion/db/db_telegram.go b/backend/gestion/db/db_telegram.go
new file mode 100644
index 00000000..44ed1210
--- /dev/null
+++ b/backend/gestion/db/db_telegram.go
@@ -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")
+}
diff --git a/backend/gestion/db/db_users.go b/backend/gestion/db/db_users.go
index a8b9c7da..639dc0ac 100644
--- a/backend/gestion/db/db_users.go
+++ b/backend/gestion/db/db_users.go
@@ -9,8 +9,8 @@ import (
)
func (d *Database) CreateUser(user *models.User) error {
- query := `INSERT INTO users (username, password, role, created_at, updated_at)
- VALUES ($1, $2, $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
+ query := `INSERT INTO users (username, password, role, created_at, updated_at)
+ VALUES ($1, $2, $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, created_at, updated_at`
var createdAt, updatedAt time.Time
@@ -28,7 +28,7 @@ func (d *Database) CreateUser(user *models.User) error {
// GetAllUsers récupÚre tous les utilisateurs
func (d *Database) GetAllUsers() ([]*models.User, error) {
- query := `SELECT id, username, password, role, created_at, updated_at
+ query := `SELECT id, username, password, role, created_at, updated_at
FROM users ORDER BY created_at DESC`
rows, err := d.Query(query)
@@ -100,8 +100,8 @@ func (d *Database) GetAllDeliveryMen() ([]*models.User, error) {
// UpdateUser met Ă jour un utilisateur existant
func (d *Database) UpdateUser(user *models.User) error {
- query := `UPDATE users
- SET username = $1, password = $2, role = $3, updated_at = CURRENT_TIMESTAMP
+ query := `UPDATE users
+ SET username = $1, password = $2, role = $3, updated_at = CURRENT_TIMESTAMP
WHERE id = $4`
result, err := d.Exec(query, user.Username, user.Password, user.Role, user.ID)
@@ -123,7 +123,6 @@ func (d *Database) UpdateUser(user *models.User) error {
// DeleteUser supprime un utilisateur
func (d *Database) DeleteUser(id int) error {
- // Récupérer le rÎle de l'utilisateur avant de le supprimer
var role string
err := d.QueryRow(`SELECT role FROM users WHERE id = $1`, id).Scan(&role)
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)
}
- // â
MODIFIĂ : Supprimer tous les tokens de l'utilisateur
_ = d.RevokeAllUserTokens(id, role)
query := `DELETE FROM users WHERE id = $1`
@@ -159,7 +157,7 @@ func (d *Database) DeleteUser(id int) error {
// GetUserByID récupÚre un utilisateur par son ID
func (d *Database) GetUserByID(id int) (*models.User, error) {
user := &models.User{}
- query := `SELECT id, username, password, role, created_at, updated_at
+ query := `SELECT id, username, password, role, created_at, updated_at
FROM users WHERE id = $1`
var createdAt, updatedAt time.Time
@@ -184,7 +182,7 @@ func (d *Database) GetUserByID(id int) (*models.User, error) {
func (d *Database) GetUserByUsername(username string) (*models.User, error) {
user := &models.User{}
- query := `SELECT id, username, password, role
+ query := `SELECT id, username, password, role
FROM users WHERE username = $1`
err := d.QueryRow(query, username).Scan(
@@ -194,7 +192,6 @@ func (d *Database) GetUserByUsername(username string) (*models.User, error) {
&user.Role,
)
- // â
Vérifier sql.ErrNoRows et retourner une erreur
if err == sql.ErrNoRows {
return nil, fmt.Errorf("utilisateur non trouvé")
}
diff --git a/backend/gestion/db/redis_deliveryman_assignment.go b/backend/gestion/db/redis_deliveryman_assignment.go
index d0922b9e..e9d1b308 100644
--- a/backend/gestion/db/redis_deliveryman_assignment.go
+++ b/backend/gestion/db/redis_deliveryman_assignment.go
@@ -9,7 +9,6 @@ import (
)
// 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) {
keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result()
if err != nil || len(keys) == 0 {
@@ -30,7 +29,6 @@ func (d *Database) FindLeastLoadedDeliveryman() (string, error) {
var status models.DeliveryPersonStatus
json.Unmarshal([]byte(data), &status)
- // â
Ignorer les livreurs offline
if status.Status == "offline" {
continue
}
@@ -70,7 +68,6 @@ func (d *Database) FindLeastLoadedDeliveryman() (string, error) {
}
// FindAvailableOrLeastLoadedDeliveryman trouve un livreur disponible ou le moins chargé
-// â
Respecte le statut BUSY
func (d *Database) FindAvailableOrLeastLoadedDeliveryman() (string, string, int, error) {
keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result()
if err != nil || len(keys) == 0 {
@@ -96,7 +93,6 @@ func (d *Database) FindAvailableOrLeastLoadedDeliveryman() (string, string, int,
continue
}
- // â
NOUVEAU: Vérifier si le livreur peut accepter
if !d.CanDeliverymanAcceptCommands(username) {
continue
}
@@ -104,7 +100,6 @@ func (d *Database) FindAvailableOrLeastLoadedDeliveryman() (string, string, int,
queueKey := fmt.Sprintf("queue:deliveryman:%s", username)
queueSize, _ := Redis.ZCard(RedisCtx, queueKey).Result()
- // Priorité: available > busy avec moins de commandes
if status.Status == "available" && queueSize == 0 {
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)
-// â ïž Ă utiliser uniquement pour assignation forcĂ©e par admin
func (d *Database) GetLeastLoadedDeliverymanForced() (string, int64, error) {
activeUsernames, err := d.GetAllActiveDeliverymenUsernames()
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
}
@@ -157,7 +148,6 @@ func (d *Database) AreAllDeliverymenAtCapacity() (bool, int, error) {
return false, 0, fmt.Errorf("aucun livreur actif")
}
- // Si un seul livreur, jamais à capacité max
if len(activeUsernames) == 1 {
return false, 1, nil
}
diff --git a/backend/gestion/db/redis_deliveryman_management.go b/backend/gestion/db/redis_deliveryman_management.go
index e50fa8c2..8a6a0992 100644
--- a/backend/gestion/db/redis_deliveryman_management.go
+++ b/backend/gestion/db/redis_deliveryman_management.go
@@ -10,9 +10,8 @@ import (
)
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)
- location := map[string]interface{}{
+ location := map[string]any{
"latitude": lat,
"longitude": lon,
"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)
- // 2ïžâŁ â
NOUVEAU: Auto-initialiser/synchroniser le statut
statusKey := fmt.Sprintf("delivery:status:%s", username)
statusData, err := Redis.Get(RedisCtx, statusKey).Result()
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)
d.SetDeliveryPersonStatus(username, "available", 0)
} else {
- // â
Statut existe â VĂ©rifier s'il faut le rĂ©activer
var status models.DeliveryPersonStatus
json.Unmarshal([]byte(statusData), &status)
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)
d.SetDeliveryPersonStatus(username, "available", 0)
} else {
- // â
Statut actif â Synchroniser basĂ© sur la queue
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)
}
- var location map[string]interface{}
+ var location map[string]any
if err := json.Unmarshal([]byte(data), &location); err != nil {
return 0, 0, fmt.Errorf("erreur parsing JSON Redis: %w", err)
}
- // Extraire latitude avec gestion de type robuste
var lat, lon float64
if v, ok := location["latitude"]; ok && v != nil {
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 {
switch val := v.(type) {
case float64:
diff --git a/backend/gestion/db/redis_eta_management.go b/backend/gestion/db/redis_eta_management.go
index 98bd3b37..187d7a51 100644
--- a/backend/gestion/db/redis_eta_management.go
+++ b/backend/gestion/db/redis_eta_management.go
@@ -3,7 +3,6 @@ package db
import (
"fmt"
"gestion/services"
- "log"
"strconv"
"time"
@@ -16,7 +15,7 @@ func (d *Database) SetCommandETA(commandID, minutes int) error {
now := time.Now()
arrivalTime := now.Add(time.Duration(minutes) * time.Minute)
- eta := map[string]interface{}{
+ eta := map[string]any{
"command_id": commandID,
"total_eta_minutes": minutes,
"eta_minutes": minutes,
@@ -37,9 +36,6 @@ func (d *Database) SetCommandETA(commandID, minutes int) error {
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
}
@@ -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
}
@@ -121,15 +116,12 @@ func (d *Database) SendETANotification(commandID int, notifType string) {
channel := fmt.Sprintf("notifications:command:%d", commandID)
Redis.Publish(RedisCtx, channel, message)
-
- log.Printf("đą Notification envoyĂ©e: %s", message)
}
// CalculateETAForDeliveryman calcule l'ETA entre un livreur et une destination
func (d *Database) CalculateETAForDeliveryman(deliveryman string, destLat, destLng float64) int {
livreurLat, livreurLng, err := d.GetDeliveryPersonLocation(deliveryman)
if err != nil {
- log.Printf("â ïž Position livreur %s non trouvĂ©e, utilisation ETA par dĂ©faut", deliveryman)
return services.MinETA
}
@@ -145,8 +137,6 @@ func (d *Database) CalculateETAForDeliveryman(deliveryman string, destLat, destL
distance := services.CalculateDistance(from, to)
eta := services.CalculateETA(distance)
- log.Printf("đ ETA calculĂ© pour %s: %.2f km -> %d min", deliveryman, distance, eta)
-
return eta
}
@@ -163,7 +153,7 @@ func (d *Database) CalculateETABetweenPoints(lat1, lng1, lat2, lng2 float64) int
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)
}
@@ -173,7 +163,7 @@ func (d *Database) SetCommandETAWithDetails(commandID, totalETA, queuePosition i
now := time.Now()
arrivalTime := now.Add(time.Duration(totalETA) * time.Minute)
- eta := map[string]interface{}{
+ eta := map[string]any{
"command_id": commandID,
"total_eta_minutes": totalETA,
"queue_position": queuePosition,
@@ -189,8 +179,5 @@ func (d *Database) SetCommandETAWithDetails(commandID, totalETA, queuePosition i
Redis.Expire(RedisCtx, key, 4*time.Hour)
- log.Printf("â
ETA détaillé pour commande %d: Total=%dmin Position=%d",
- commandID, totalETA, queuePosition)
-
return nil
}
diff --git a/backend/gestion/db/redis_position.go b/backend/gestion/db/redis_position.go
index 763d73d8..3973f4fa 100644
--- a/backend/gestion/db/redis_position.go
+++ b/backend/gestion/db/redis_position.go
@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"gestion/models"
- "log"
"math"
"time"
)
@@ -74,10 +73,6 @@ func (d *Database) UpdateLivreurPosition(authUsername string, lat, lon float64,
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)
d.PublishDeliveryPersonLocationUpdate(authUsername, lat, lon)
@@ -110,8 +105,3 @@ func isValidCoordinates(lat, lon float64) bool {
lat >= -90 && lat <= 90 &&
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
-}
diff --git a/backend/gestion/db/redis_pubsub.go b/backend/gestion/db/redis_pubsub.go
index 561bba4b..dcb82596 100644
--- a/backend/gestion/db/redis_pubsub.go
+++ b/backend/gestion/db/redis_pubsub.go
@@ -11,7 +11,7 @@ import (
func (d *Database) PublishCommandEvent(commandID int, eventType, message string) {
channel := fmt.Sprintf("events:command:%d", commandID)
- event := map[string]interface{}{
+ event := map[string]any{
"command_id": commandID,
"type": eventType,
"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
func (d *Database) PublishDeliveryPersonLocationUpdate(username string, lat, lon float64) {
- message := map[string]interface{}{
+ message := map[string]any{
"type": "location_update",
"username": username,
"latitude": lat,
diff --git a/backend/gestion/db/redis_queue_assignment.go b/backend/gestion/db/redis_queue_assignment.go
index 285c3b16..4013c126 100644
--- a/backend/gestion/db/redis_queue_assignment.go
+++ b/backend/gestion/db/redis_queue_assignment.go
@@ -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)
}
- // â
MODIFIĂ: ETA = temps de trajet direct uniquement
totalETA := estimatedTravelTime
var lat, lng float64
@@ -86,9 +85,6 @@ func (d *Database) AssignCommandToDeliverymanQueue(commandID int, deliveryman st
deliveryman, currentQueueSize+1, limitInfo, totalETA),
"system")
- log.Printf("â
Commande %d -> Queue %s (pos: %d%s, ETA trajet: %d min)",
- commandID, deliveryman, currentQueueSize+1, limitInfo, totalETA)
-
return nil
}
@@ -168,13 +164,10 @@ func (d *Database) AssignCommandToDeliverymanQueueWithCoords(commandID int, deli
return fmt.Errorf("erreur ajout Ă la queue: %w", err)
}
- // ============================================
- // â
CORRECTION: Mettre Ă jour livreur_assign dans la DB
- // ============================================
- updateQuery := `UPDATE commandes
- SET livreur_assign = $1,
+ updateQuery := `UPDATE commandes
+ SET livreur_assign = $1,
status = 'assigned',
- updated_at = CURRENT_TIMESTAMP
+ updated_at = CURRENT_TIMESTAMP
WHERE id = $2`
_, err = d.Exec(updateQuery, deliveryman, commandID)
@@ -213,7 +206,6 @@ func (d *Database) AssignCommandToDeliverymanQueueWithCoords(commandID int, deli
return nil
}
-// 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 {
command, err := d.GetCommandByID(commandID)
diff --git a/backend/gestion/db/redis_queue_info.go b/backend/gestion/db/redis_queue_info.go
index 5c4e57bb..5a27b350 100644
--- a/backend/gestion/db/redis_queue_info.go
+++ b/backend/gestion/db/redis_queue_info.go
@@ -8,27 +8,22 @@ import (
)
// 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)
- // Nombre de commandes dans la queue
queueSize, _ := Redis.ZCard(RedisCtx, queueKey).Result()
- // Récupérer toutes les commandes
commandIDs, _ := Redis.ZRange(RedisCtx, queueKey, 0, -1).Result()
- // Vérifier le nombre de livreurs actifs pour déterminer la limite
activeCount, _ := d.CountActiveDeliverymen()
- // Récupérer les détails de chaque commande
- var commands []map[string]interface{}
+ var commands []map[string]any
for i, cmdIDStr := range commandIDs {
commandID := extractCommandID(cmdIDStr)
if commandID <= 0 {
continue
}
- // Récupérer les données de la commande
commandKey := fmt.Sprintf("queue:pending:%d", commandID)
data, err := Redis.Get(RedisCtx, commandKey).Result()
if err != nil {
@@ -40,7 +35,7 @@ func (d *Database) GetDeliverymanQueueInfo(deliveryman string) (map[string]inter
continue
}
- commands = append(commands, map[string]interface{}{
+ commands = append(commands, map[string]any{
"position": i + 1,
"command_id": commandID,
"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
if activeCount > 1 {
- // Plusieurs livreurs: limite de 10
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,
"queue_size": queueSize,
"commands": commands,
@@ -69,15 +61,13 @@ func (d *Database) GetDeliverymanQueueInfo(deliveryman string) (map[string]inter
}, nil
}
-func (d *Database) GetAllQueuesOverview() (map[string]interface{}, error) {
- overview := make(map[string]interface{})
+func (d *Database) GetAllQueuesOverview() (map[string]any, error) {
+ overview := make(map[string]any)
- // Queue générale
generalQueueSize, _ := Redis.ZCard(RedisCtx, "queue:pending:sorted").Result()
overview["general_queue"] = generalQueueSize
- // Queues par livreur avec détails
- deliverymanQueues := make(map[string]interface{})
+ deliverymanQueues := make(map[string]any)
keys, _ := Redis.Keys(RedisCtx, "queue:deliveryman:*").Result()
for _, key := range keys {
@@ -88,7 +78,7 @@ func (d *Database) GetAllQueuesOverview() (map[string]interface{}, error) {
username := key[len("queue:deliveryman:"):]
queueSize, _ := Redis.ZCard(RedisCtx, key).Result()
- deliverymanQueues[username] = map[string]interface{}{
+ deliverymanQueues[username] = map[string]any{
"queue_size": queueSize,
"can_accept_more": 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
-
- // Total
var totalPending int64 = generalQueueSize
for _, key := range keys {
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
-func (d *Database) GetQueueStats() (map[string]interface{}, error) {
+func (d *Database) GetQueueStats() (map[string]any, error) {
normalCount, _ := Redis.ZCard(RedisCtx, "queue:pending:sorted").Result()
priorityCount, _ := Redis.ZCard(RedisCtx, "queue:priority:sorted").Result()
- // Compter les commandes dans les queues des livreurs
var deliverymanQueueCount int64
keys, _ := Redis.Keys(RedisCtx, "queue:deliveryman:*").Result()
for _, key := range keys {
@@ -127,7 +114,6 @@ func (d *Database) GetQueueStats() (map[string]interface{}, error) {
deliverymanQueueCount += count
}
- // Calculer temps d'attente moyen
var totalWaitTime int64
var commandCount int64
@@ -154,7 +140,7 @@ func (d *Database) GetQueueStats() (map[string]interface{}, error) {
avgWaitTime = int(totalWaitTime / commandCount)
}
- stats := map[string]interface{}{
+ stats := map[string]any{
"total_pending": normalCount + priorityCount,
"general_queue": normalCount,
"priority_queue": priorityCount,
diff --git a/backend/gestion/db/redis_queue_management.go b/backend/gestion/db/redis_queue_management.go
index 33d93a72..13a5103a 100644
--- a/backend/gestion/db/redis_queue_management.go
+++ b/backend/gestion/db/redis_queue_management.go
@@ -16,8 +16,6 @@ import (
// ============================================
// UpdateDeliverymanStatusBasedOnQueue met Ă jour automatiquement le statut
-// â
Status = "busy" si queue >= 10
-// â
Status = "available" si queue < 10
func (d *Database) UpdateDeliverymanStatusBasedOnQueue(deliveryman string) error {
queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman)
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)
}
- // Récupérer le statut actuel
statusKey := fmt.Sprintf("delivery:status:%s", deliveryman)
data, err := Redis.Get(RedisCtx, statusKey).Result()
if err != nil {
@@ -38,18 +35,14 @@ func (d *Database) UpdateDeliverymanStatusBasedOnQueue(deliveryman string) error
return err
}
- // Déterminer le nouveau statut
var newStatus string
var currentCommand int
if queueSize >= MAX_COMMANDS_PER_DELIVERYMAN {
- // đŽ BUSY car queue pleine (10 commandes ou plus)
newStatus = "busy"
currentCommand = 0
log.Printf("đŽ [STATUS] %s -> BUSY (queue pleine: %d/10)", deliveryman, queueSize)
} 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 {
newStatus = "delivering"
currentCommand = status.CurrentCommand
@@ -62,19 +55,16 @@ func (d *Database) UpdateDeliverymanStatusBasedOnQueue(deliveryman string) error
}
}
- // Mettre Ă jour le statut
return d.SetDeliveryPersonStatus(deliveryman, newStatus, currentCommand)
}
// 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 {
- // 1. Vérifier le statut Redis
statusKey := fmt.Sprintf("delivery:status:%s", deliveryman)
data, err := Redis.Get(RedisCtx, statusKey).Result()
if err != nil {
log.Printf("â ïž [CHECK] Livreur %s sans statut Redis", deliveryman)
- return true // Fallback: autoriser si pas de statut
+ return true
}
var status models.DeliveryPersonStatus
@@ -82,9 +72,7 @@ func (d *Database) CanDeliverymanAcceptCommands(deliveryman string) bool {
return true
}
- // 2. Si offline, refuser
if status.Status == "offline" {
- log.Printf("â« [CHECK] %s REFUSĂ: offline", deliveryman)
return false
}
@@ -122,7 +110,6 @@ func (d *Database) GetAvailableDeliveryPersonsForAssignment() ([]models.Delivery
continue
}
- // â
Vérifier si le livreur peut accepter des commandes
if d.CanDeliverymanAcceptCommands(status.Username) {
available = append(available, status)
}
@@ -144,11 +131,9 @@ func (d *Database) AddToDeliverymanQueue(deliveryman string, queueItem models.Co
return fmt.Errorf("erreur serialisation: %w", err)
}
- // Sauvegarder les données de la commande
commandKey := fmt.Sprintf("queue:pending:%d", queueItem.CommandID)
Redis.Set(RedisCtx, commandKey, data, 24*time.Hour)
- // Ajouter Ă la queue sorted set du livreur (score = timestamp)
queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman)
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)
}
- // Incrémenter le compteur de commandes en attente pour ce livreur
counterKey := fmt.Sprintf("queue:deliveryman:%s:count", deliveryman)
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)
return nil
@@ -184,8 +165,6 @@ func (d *Database) AddCommandToQueue(commandID int) error {
if err != nil {
return fmt.Errorf("commande introuvable: %w", err)
}
-
- // Gestion des coordonnées (NULL safe)
var lat, lng float64
if command["dest_latitude"] != nil {
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
if tp, ok := command["total_prix"].(float64); ok {
totalPrice = tp
}
- // Récupérer l'adresse
var address string
if addr, ok := command["delivery_address"].(string); ok {
address = addr
@@ -221,7 +198,6 @@ func (d *Database) AddCommandToQueue(commandID int) error {
EstimatedETA: 0,
}
- // Ajouter à la queue générale
return d.AddToGeneralQueue(queueItem)
}
@@ -235,8 +211,6 @@ func (d *Database) AddCommandToSmartQueue(commandID int, address string) error {
if err != nil {
return fmt.Errorf("commande introuvable: %w", err)
}
-
- // Gestion des coordonnées (NULL safe)
var lat, lng float64
if command["dest_latitude"] != nil {
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
if tp, ok := command["total_prix"].(float64); ok {
totalPrice = tp
@@ -269,12 +242,10 @@ func (d *Database) AddCommandToSmartQueue(commandID int, address string) error {
// â
MODIFIĂ: Utiliser FindLeastLoadedDeliveryman qui respecte maintenant le statut
assignedDeliveryman, err := d.FindLeastLoadedDeliveryman()
if err != nil {
- // Aucun livreur trouvé, ajouter à la queue générale
log.Printf("â ïž Aucun livreur trouvĂ©, ajout Ă la queue gĂ©nĂ©rale")
return d.AddToGeneralQueue(queueItem)
}
- // Ajouter la commande à la queue spécifique du livreur (auto-update du statut)
err = d.AddToDeliverymanQueue(assignedDeliveryman, queueItem)
if err != nil {
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)
- // Publier l'événement
d.PublishCommandEvent(commandID, "queued",
fmt.Sprintf("En attente dans la queue de %s", assignedDeliveryman))
@@ -459,7 +429,6 @@ func (d *Database) ClearDeliverymanQueue(deliveryman string) error {
continue
}
- // Récupérer les données de la commande
commandKey := fmt.Sprintf("queue:pending:%d", commandID)
data, err := Redis.Get(RedisCtx, commandKey).Result()
if err != nil {
@@ -471,15 +440,12 @@ func (d *Database) ClearDeliverymanQueue(deliveryman string) error {
continue
}
- // Trouver un nouveau livreur
newDeliveryman, err := d.FindLeastLoadedDeliveryman()
if err != nil {
- // Fallback: queue générale
d.AddToGeneralQueue(queueItem)
continue
}
- // Réassigner à un autre livreur
if newDeliveryman != deliveryman {
d.AddToDeliverymanQueue(newDeliveryman, queueItem)
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, 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)
return nil
@@ -521,227 +484,125 @@ func (d *Database) SetDeliveryPersonStatus(username, status string, commandID in
// GetAvailableDeliveryPersonsRedis récupÚre les livreurs disponibles (LEGACY)
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
-
- for _, key := range keys {
- data, err := Redis.Get(RedisCtx, key).Result()
- if err != nil {
- continue
+ err := d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) {
+ if s.Status == "available" {
+ available = append(available, s)
}
-
- var status models.DeliveryPersonStatus
- json.Unmarshal([]byte(data), &status)
-
- if status.Status == "available" {
- available = append(available, status)
- }
- }
-
- return available, nil
+ })
+ return available, err
}
-// GetAllActiveDeliveryPersons - VERSION MISE à JOUR avec vérification capacité
+// GetAllActiveDeliveryPersons retourne les livreurs actifs pouvant accepter des commandes
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
-
- for _, key := range keys {
- data, err := Redis.Get(RedisCtx, key).Result()
- if err != nil {
- continue
+ err := d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) {
+ if s.Status != "offline" && d.CanDeliverymanAcceptCommands(s.Username) {
+ active = append(active, s)
}
-
- var status models.DeliveryPersonStatus
- 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
+ })
+ return active, err
}
// CountActiveDeliverymen compte le nombre de livreurs actifs (non offline)
func (d *Database) CountActiveDeliverymen() (int, error) {
- keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result()
- if err != nil {
- return 0, err
- }
-
count := 0
- 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" {
+ err := d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) {
+ if s.Status != "offline" {
count++
}
- }
-
- return count, nil
+ })
+ return count, err
}
func (d *Database) GetSingleActiveDeliveryman() (string, error) {
- keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result()
- if err != nil {
- return "", 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" {
- return status.Username, nil
+ found := ""
+ d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) { //nolint
+ if found == "" && s.Status != "offline" {
+ found = s.Username
}
+ })
+ if found == "" {
+ 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
func (d *Database) GetAllActiveDeliverymenUsernames() ([]string, error) {
- keys, err := Redis.Keys(RedisCtx, "delivery:status:*").Result()
- if err != nil {
- return nil, err
- }
-
- var activeUsernames []string
-
- for _, key := range keys {
- data, err := Redis.Get(RedisCtx, key).Result()
- if err != nil {
- continue
+ var usernames []string
+ err := d.iterDeliveryStatuses(func(s models.DeliveryPersonStatus) {
+ if s.Status != "offline" {
+ usernames = append(usernames, s.Username)
}
-
- var status models.DeliveryPersonStatus
- json.Unmarshal([]byte(data), &status)
-
- if status.Status != "offline" {
- activeUsernames = append(activeUsernames, status.Username)
- }
- }
-
- return activeUsernames, nil
+ })
+ return usernames, err
}
-// ============================================
-// đ SYNCHRONISATION DES STATUTS
-// ============================================
-
// SyncAllDeliverymanStatuses synchronise tous les statuts (à appeler au démarrage)
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()
if err != nil {
return err
}
-
- log.Println("đ [SYNC] Synchronisation des statuts livreurs...")
-
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
}
-
- // Mettre à jour le statut basé sur la queue
- d.UpdateDeliverymanStatusBasedOnQueue(status.Username)
+ fn(status)
}
-
- log.Println("â
[SYNC] Synchronisation terminée")
-
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
-}
diff --git a/backend/gestion/db/redis_session.go b/backend/gestion/db/redis_session.go
index c1395c4a..bb484301 100644
--- a/backend/gestion/db/redis_session.go
+++ b/backend/gestion/db/redis_session.go
@@ -78,7 +78,6 @@ func (d *Database) CreateClientSession(clientID int, username string, sessionID
// Charger les infos du client (points, penalty) dans le cache
if client, err := d.GetClientByUsername(username); err == nil {
- sessionData.PointsCache = int(client.Point)
sessionData.PenaltyCache = float64(client.Amende)
sessionJSON, _ := json.Marshal(sessionData)
Redis.Set(RedisCtx, sessionKey, sessionJSON, ttl)
diff --git a/backend/gestion/handlers/auth.go b/backend/gestion/handlers/auth.go
index c354945b..f563bfc2 100644
--- a/backend/gestion/handlers/auth.go
+++ b/backend/gestion/handlers/auth.go
@@ -1,14 +1,12 @@
package handlers
import (
- "crypto/rand"
- "encoding/hex"
"gestion/db"
"gestion/models"
+ "gestion/utils"
"log"
"net/http"
"os"
- "regexp"
"strconv"
"strings"
"time"
@@ -19,109 +17,16 @@ import (
"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 (
clientTokenDuration = 5 * time.Hour
adminTokenDuration = 10 * time.Hour
- userJWTSecret = []byte(os.Getenv("USER_JWT_SECRET")) // â
Pour clients
- adminJWTSecret = []byte(os.Getenv("ADMIN_JWT_SECRET")) // â
Pour admin/cabine/livreur
+ userJWTSecret = []byte(os.Getenv("USER_JWT_SECRET"))
+ 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) {
- sessionID := generateSessionID()
- claims := ClientClaims{
+ sessionID := utils.GenerateSessionID()
+ claims := models.ClientClaims{
ClientID: client.ID,
Username: client.Username,
Role: "client",
@@ -135,7 +40,7 @@ func generateClientToken(client *models.Client) (string, error) {
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
- tokenString, err := token.SignedString(userJWTSecret) // â
UTILISER userJWTSecret (CLIENT)
+ tokenString, err := token.SignedString(userJWTSecret)
if err != nil {
return "", err
}
@@ -143,8 +48,8 @@ func generateClientToken(client *models.Client) (string, error) {
}
func generateAdminToken(user *models.User) (string, error) {
- sessionID := generateSessionID()
- claims := AdminClaims{
+ sessionID := utils.GenerateSessionID()
+ claims := models.AdminClaims{
UserID: user.ID,
Username: user.Username,
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)
- tokenString, err := token.SignedString(adminJWTSecret) // â
UTILISER adminJWTSecret (ADMIN/CABINE/LIVREUR)
+ tokenString, err := token.SignedString(adminJWTSecret)
if err != nil {
return "", err
}
return tokenString, nil
}
-// ============================================
-// HANDLERS AUTHENTIFICATION CLIENT
-// ============================================
-
// RegisterClient crée un nouveau compte client
-// POST /api/v1/auth/register
func RegisterClient(c *gin.Context) {
- var req RegisterClientRequest
+ var req models.RegisterClientRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("â [REGISTER_CLIENT] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
@@ -183,7 +83,7 @@ func RegisterClient(c *gin.Context) {
}
// Validation téléphone
- if !validatePhoneNumber(req.Telephone) {
+ if !utils.ValidatePhoneNumber(req.Telephone) {
log.Printf("â [REGISTER_CLIENT] TĂ©lĂ©phone invalide: %s", req.Telephone)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Numéro de téléphone invalide",
@@ -191,7 +91,7 @@ func RegisterClient(c *gin.Context) {
return
}
- normalizedPhone := normalizePhoneNumber(req.Telephone)
+ normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
database := c.MustGet("database").(*db.Database)
// Vérifier username unique
@@ -256,9 +156,7 @@ func RegisterClient(c *gin.Context) {
client.Password = ""
- log.Printf("â
[REGISTER_CLIENT] Client créé: %s (ID=%d)", client.Username, client.ID)
-
- c.JSON(http.StatusCreated, LoginResponse{
+ c.JSON(http.StatusCreated, models.LoginResponse{
AccessToken: token,
TokenType: "Bearer",
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)
-// POST /api/v2/admin/protected/clients
func AdminCreateClient(c *gin.Context) {
- var req RegisterClientRequest
+ var req models.RegisterClientRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
return
}
- if !validatePhoneNumber(req.Telephone) {
+ if !utils.ValidatePhoneNumber(req.Telephone) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
return
}
- normalizedPhone := normalizePhoneNumber(req.Telephone)
+ normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
database := c.MustGet("database").(*db.Database)
if existing, _ := database.GetClientByUsername(req.Username); existing != nil {
@@ -322,8 +219,6 @@ func AdminCreateClient(c *gin.Context) {
return
}
- log.Printf("â
[ADMIN_CREATE_CLIENT] Client créé par admin: %s (ID=%d)", client.Username, client.ID)
-
c.JSON(http.StatusCreated, gin.H{
"message": "Client créé avec succÚs",
"client": gin.H{
@@ -337,9 +232,8 @@ func AdminCreateClient(c *gin.Context) {
}
// LoginClient authentifie un client
-// POST /api/v1/auth/login
func LoginClient(c *gin.Context) {
- var req LoginRequest
+ var req models.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("â [LOGIN_CLIENT] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
@@ -361,7 +255,6 @@ func LoginClient(c *gin.Context) {
return
}
- // â
LIGNE 302 CORRIGĂE - GĂ©rer l'erreur !
token, err := generateClientToken(client)
if err != nil {
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] Client authentifié: %s", req.Username)
-
- c.JSON(http.StatusOK, LoginResponse{
+ c.JSON(http.StatusOK, models.LoginResponse{
AccessToken: token,
TokenType: "Bearer",
ExpiresIn: int(clientTokenDuration.Seconds()),
@@ -402,7 +293,6 @@ func LoginClient(c *gin.Context) {
}
// ChangePassword permet Ă un client de changer son mot de passe
-// PUT /api/v1/auth/change-password
func ChangePassword(c *gin.Context) {
var req struct {
CurrentPassword string `json:"current_password" binding:"required"`
@@ -442,16 +332,13 @@ func ChangePassword(c *gin.Context) {
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"})
}
// LogoutClient déconnecte un client
-// POST /api/v1/auth/logout
func LogoutClient(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
- // Invalider la session Redis
clientID, hasClientID := c.Get("client_id")
if hasClientID && clientID != 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")
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
database.RevokeToken(tokenStr)
}
- log.Printf("â
[LOGOUT_CLIENT] Client déconnecté")
-
c.JSON(http.StatusOK, gin.H{"message": "Déconnexion réussie"})
}
-// ============================================
-// HANDLERS AUTHENTIFICATION ADMIN
-// ============================================
-
// RegisterAdmin crée un nouvel utilisateur admin/cabine/livreur
-// POST /api/v1/auth/admin/register
func RegisterAdmin(c *gin.Context) {
- var req RegisterAdminRequest
+ var req models.RegisterAdminRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("â [REGISTER_ADMIN] Erreur binding: %v", err)
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)
- // Vérifier que l'user n'existe pas
if existingUser, _ := database.GetUserByUsername(req.Username); existingUser != nil {
log.Printf("â [REGISTER_ADMIN] Username dĂ©jĂ utilisĂ©: %s", req.Username)
c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"})
@@ -507,9 +385,6 @@ func RegisterAdmin(c *gin.Context) {
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)
if err != nil {
log.Printf("â [REGISTER_ADMIN] Erreur gĂ©nĂ©ration token: %v", err)
@@ -517,11 +392,6 @@ func RegisterAdmin(c *gin.Context) {
return
}
- log.Printf("â
[REGISTER_ADMIN] Token généré: %s...", token[:50])
-
- // ============================================
- // â
CRITICAL FIX: ENREGISTRER LE TOKEN EN DB
- // ============================================
expiresAt := time.Now().Add(adminTokenDuration)
if err := database.SaveToken(user.ID, user.Role, token, expiresAt); err != nil {
log.Printf("â [REGISTER_ADMIN] Erreur SaveToken: %v", err)
@@ -529,11 +399,9 @@ func RegisterAdmin(c *gin.Context) {
return
}
- log.Printf("â
[REGISTER_ADMIN] Token enregistré en DB pour user ID: %d", user.ID)
-
user.Password = ""
- c.JSON(http.StatusCreated, LoginResponse{
+ c.JSON(http.StatusCreated, models.LoginResponse{
AccessToken: token,
TokenType: "Bearer",
ExpiresIn: int(adminTokenDuration.Seconds()),
@@ -542,10 +410,8 @@ func RegisterAdmin(c *gin.Context) {
}
// LoginAdmin authentifie un admin/cabine/livreur
-// POST /api/v1/auth/admin/login
-// â
AMĂLIORĂ: Meilleure gestion d'erreurs
func LoginAdmin(c *gin.Context) {
- var req LoginRequest
+ var req models.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("â [LOGIN_ADMIN] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
@@ -561,14 +427,12 @@ func LoginAdmin(c *gin.Context) {
return
}
- // Vérifier que c'est un admin/cabine/livreur
if user.Role != "admin" && user.Role != "cabine" && user.Role != "livreur" {
log.Printf("â [LOGIN_ADMIN] RĂŽle invalide: %s", user.Role)
c.JSON(http.StatusUnauthorized, gin.H{"error": "AccÚs non autorisé"})
return
}
- // Vérifier le mot de passe
if user.Password == "" {
log.Printf("â [LOGIN_ADMIN] Mot de passe vide en base")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Identifiants invalides"})
@@ -581,7 +445,6 @@ func LoginAdmin(c *gin.Context) {
return
}
- // Générer le token
token, _ := generateAdminToken(user)
expiresAt := time.Now().Add(adminTokenDuration)
@@ -591,9 +454,7 @@ func LoginAdmin(c *gin.Context) {
return
}
- log.Printf("â
[LOGIN_ADMIN] User authentifié: %s (role=%s)", user.Username, user.Role)
-
- c.JSON(http.StatusOK, LoginResponse{
+ c.JSON(http.StatusOK, models.LoginResponse{
AccessToken: token,
TokenType: "Bearer",
ExpiresIn: int(adminTokenDuration.Seconds()),
@@ -606,7 +467,6 @@ func LoginAdmin(c *gin.Context) {
}
// LogoutAdmin déconnecte un admin/cabine/livreur
-// POST /api/v1/auth/admin/logout
func LogoutAdmin(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -645,14 +505,14 @@ func GetCurrentClient(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"client": gin.H{
- "id": client.ID,
- "username": client.Username,
- "nom": client.Nom,
- "prenom": client.Prenom,
- "telephone": client.Telephone,
- "command": client.Command,
- "point": client.Point,
- "amende": client.Amende,
+ "id": client.ID,
+ "username": client.Username,
+ "nom": client.Nom,
+ "prenom": client.Prenom,
+ "telephone": client.Telephone,
+ "command": client.Command,
+ "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)
c.JSON(http.StatusOK, gin.H{
- "user": ProfileResponse{
+ "user": models.ProfileResponse{
Username: user.Username,
Role: user.Role,
},
@@ -795,8 +655,7 @@ func GetAllClients(c *gin.Context) {
"prenom": cl.Prenom,
"telephone": cl.Telephone,
"command": cl.Command,
- "point": cl.Point,
- "points_zipette": cl.PointZipette,
+ "points_extra": cl.PointsExtra,
"amende": cl.Amende,
"cancellations_count": cl.CancellationsCount,
"last_penalty_reason": cl.LastPenaltyReason,
diff --git a/backend/gestion/handlers/cabine.go b/backend/gestion/handlers/cabine.go
index 02d6f3f7..5ba313af 100644
--- a/backend/gestion/handlers/cabine.go
+++ b/backend/gestion/handlers/cabine.go
@@ -9,8 +9,10 @@ import (
"encoding/json"
"fmt"
"gestion/db"
+ "gestion/utils"
"log"
"net/http"
+ "slices"
"strconv"
"time"
@@ -22,7 +24,6 @@ import (
// ============================================
// SetCommandDestinationCoordinates stocke les coordonnées destination en Redis
-// POST /api/v2/admin/protected/orders/:id/set-destination
func SetCommandDestinationCoordinates(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -69,15 +70,10 @@ func SetCommandDestinationCoordinates(c *gin.Context) {
return
}
- // Vérifier que la commande existe
- command, err := database.GetCommandByID(commandID)
- if err != nil {
+ if !utils.CheckCommand(commandID, database) {
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)
coordsJSON, _ := json.Marshal(map[string]float64{
"lat": req.Latitude,
@@ -134,12 +130,12 @@ func GetClientProfile(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"client": gin.H{
- "id": client.ID,
- "username": client.Username,
- "command": client.Command,
- "point": client.Point,
- "amende": client.Amende,
- "created_at": client.CreatedAt,
+ "id": client.ID,
+ "username": client.Username,
+ "command": client.Command,
+ "amende": client.Amende,
+ "points_extra": client.PointsExtra,
+ "created_at": client.CreatedAt,
},
})
}
@@ -170,8 +166,8 @@ func GetClientFullHistory(c *gin.Context) {
"client": gin.H{
"username": client.Username,
"total_commands": client.Command,
- "points": client.Point,
"amende": client.Amende,
+ "points_extra": client.PointsExtra,
},
"commands": commands,
"count": len(commands),
@@ -209,15 +205,7 @@ func UpdateCommandAddressCabine(c *gin.Context) {
status, _ := command["status"].(string)
allowedStatuses := []string{"pending", "", "assigned"}
- isAllowed := false
- for _, s := range allowedStatuses {
- if status == s {
- isAllowed = true
- break
- }
- }
-
- if !isAllowed {
+ if !slices.Contains(allowedStatuses, status) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Impossible de modifier l'adresse d'une commande en cours ou terminée",
"current_status": status,
@@ -288,10 +276,6 @@ func GetLivreurPosition(c *gin.Context) {
})
}
-// ============================================
-// 4. DELIVERY TRACKING CLIENT (SANS GPS)
-// ============================================
-
func GetDeliveryTrackingClient(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -394,10 +378,6 @@ func GetDeliveryTracking(c *gin.Context) {
c.JSON(http.StatusOK, response)
}
-// ============================================
-// 6. DELIVERY ISSUES
-// ============================================
-
func GetDeliveryIssues(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -630,24 +610,6 @@ func ForceValidateDelivery(c *gin.Context) {
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" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Cette commande a déjà été validée",
@@ -656,6 +618,16 @@ func ForceValidateDelivery(c *gin.Context) {
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")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
@@ -668,7 +640,6 @@ func ForceValidateDelivery(c *gin.Context) {
clientUsername, _ := command["username"].(string)
livreurAssign, _ := command["livreur_assign"].(string)
- // Notifier le client
if clientUsername != "" {
clientMsg := fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID)
database.NotifyClient(clientUsername, commandID, "livre", clientMsg)
@@ -678,12 +649,11 @@ func ForceValidateDelivery(c *gin.Context) {
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)
}
if livreurAssign != "" {
- log.Printf("đŠ Commande %d validĂ©e de force par admin - Optimisation queue de %s...", commandID, livreurAssign)
err := database.CompleteDeliveryAndProcessNext(livreurAssign, commandID)
if err != nil {
log.Printf("â ïž Erreur optimisation: %v", err)
diff --git a/backend/gestion/handlers/cancel_command.go b/backend/gestion/handlers/cancel_command.go
index 8f3be5f2..e98d3fec 100644
--- a/backend/gestion/handlers/cancel_command.go
+++ b/backend/gestion/handlers/cancel_command.go
@@ -18,10 +18,6 @@ import (
"github.com/gin-gonic/gin"
)
-// ============================================
-// RATE LIMITING
-// ============================================
-
var (
cancelRateLimitMap = make(map[string][]time.Time)
cancelMaxRequests = 5 // Max 5 annulations
@@ -49,16 +45,10 @@ func checkCancelRateLimit(key string) bool {
return true
}
-// ============================================
-// HELPERS DE SĂCURITĂ
-// ============================================
-
func validateReason(reason string) string {
- // Limiter la longueur
if len(reason) > 500 {
reason = reason[:500]
}
- // Sanitizer
reason = strings.Map(func(r rune) rune {
if r < 32 || r == 127 {
return -1
@@ -73,10 +63,6 @@ func validateReason(reason string) string {
return reason
}
-// ============================================
-// 1ïžâŁ ANNULATION PAR LE CLIENT - VERSION SĂCURISĂE
-// ============================================
-
func CancelCommandByClient(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -115,19 +101,12 @@ func CancelCommandByClient(c *gin.Context) {
req.Reason = validateReason(req.Reason)
- log.Printf("đ« [CANCEL_CLIENT] Client %s annule cmd %d (force=%v)", username, commandID, req.Force)
-
- // ============================================
- // UTILISER LA FONCTION ATOMIQUE
- // ============================================
- penalty, pointsLost, err := database.CancelCommandAtomic(commandID, username, req.Reason, req.Force)
+ penalty, err := database.CancelCommandAtomic(commandID, username, req.Reason, req.Force)
if err != nil {
log.Printf("â [CANCEL_CLIENT] Erreur: %v", err)
- // â
GESTION SPĂCIALE POUR "confirmation requise"
if err.Error() == "confirmation requise" {
- // â
RĂCUPĂRER LES INFORMATIONS DE LA COMMANDE
command, errCmd := database.GetCommandByID(commandID)
if errCmd != nil {
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)
currentStatus, _ := command["status"].(string)
- // â
VĂRIFIER SI ETA EXISTE (VERSION CORRIGĂE)
hasETA := false
if livreurAssign != "" {
- // â
FIX: Utiliser la nouvelle fonction qui vérifie VRAIMENT l'ETA
hasETA = database.CheckCommandETAExistsAndValid(commandID)
}
- // â
CALCULER LA PĂNALITĂ QUI SERA APPLIQUĂE
nextPenalty, _ := database.CalculateCancellationPenalty(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{
"success": false,
"warning": true,
@@ -170,7 +135,6 @@ func CancelCommandByClient(c *gin.Context) {
}
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)
response["message"] = "â ïž Un livreur est en route vers votre adresse (ETA dĂ©finie)"
@@ -180,27 +144,21 @@ func CancelCommandByClient(c *gin.Context) {
"has_eta": true,
}
response["penalty_warning"] = gin.H{
- "will_apply": true,
- "penalty_amount": nextPenalty,
- "current_violations": cancelCount,
- "current_points_weed": currentPointsWeed,
- "current_points_zipette": currentPointsZipette,
- "total_points": totalPoints,
- "points_will_reset": true,
+ "will_apply": true,
+ "penalty_amount": nextPenalty,
+ "current_violations": cancelCount,
"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!",
- nextPenalty, currentPointsWeed, currentPointsZipette, totalPoints,
+ "â ïž ATTENTION: Une amende de %d sera appliquĂ©e pour annulation tardive",
+ nextPenalty,
),
"scale": gin.H{
- "1st_cancel": "20 points + remise à zéro TOTALE",
- "2nd_cancel": "50 points + remise à zéro TOTALE",
- "3rd_cancel": "100 points + remise à zéro TOTALE",
- "4th+_cancel": "150 points + remise à zéro TOTALE",
- "your_next": fmt.Sprintf("%d points + remise à zéro de tous vos %d points", nextPenalty, totalPoints),
+ "1st_cancel": 20,
+ "2nd_cancel": 50,
+ "3rd_cancel": 100,
+ "4th+_cancel": 150,
},
}
} 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Ă©")
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 {
response["penalty"] = gin.H{
- "penalty_points": penalty,
- "points_weed_lost": pointsLost["weed"],
- "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",
+ "penalty_amount": penalty,
+ "warning": "Une amende a été appliquée pour annulation tardive",
}
} else {
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) {
database := c.MustGet("database").(*db.Database)
@@ -345,7 +296,6 @@ func GetAllCancelledOrders(c *gin.Context) {
return
}
- // â
VALIDATION des paramĂštres
filterUsername := c.Query("username")
if len(filterUsername) > 100 {
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)
- var enrichedOrders []map[string]interface{}
+ var enrichedOrders []map[string]any
for _, order := range cancelledOrders {
orderID, _ := order["id"].(int)
items, _ := database.GetCommandItems(orderID)
logs, _ := database.GetCommandLogs(orderID)
- var cancellationLog map[string]interface{}
+ var cancellationLog map[string]any
for _, logEntry := range logs {
status, _ := logEntry["status"].(string)
if status == "cancelled" {
@@ -389,7 +339,7 @@ func GetAllCancelledOrders(c *gin.Context) {
}
}
- enrichedOrder := map[string]interface{}{
+ enrichedOrder := map[string]any{
"id": order["id"],
"username": order["username"],
"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) {
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)
- // â
UTILISER LA FONCTION ATOMIQUE
err = database.DeleteCommandAtomic(commandID, username, userRole)
if err != nil {
log.Printf("â [DELETE_COMMAND] Erreur: %v", err)
- // â Ne pas exposer les dĂ©tails de l'erreur
if err.Error() == "commande non trouvée" {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
} else {
diff --git a/backend/gestion/handlers/categories.go b/backend/gestion/handlers/categories.go
index 088f1732..66cd0e8f 100644
--- a/backend/gestion/handlers/categories.go
+++ b/backend/gestion/handlers/categories.go
@@ -10,7 +10,6 @@ import (
"github.com/gin-gonic/gin"
)
-// GET /api/v1/categories â public
func GetCategories(c *gin.Context) {
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) {
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) {
database := c.MustGet("database").(*db.Database)
@@ -131,6 +128,5 @@ func DeleteCategory(c *gin.Context) {
return
}
- log.Printf("â
[CATEGORIES] Supprimée: %d", id)
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Catégorie supprimée"})
}
diff --git a/backend/gestion/handlers/client_tracking.go b/backend/gestion/handlers/client_tracking.go
index 96a56c5b..2f1ddb14 100644
--- a/backend/gestion/handlers/client_tracking.go
+++ b/backend/gestion/handlers/client_tracking.go
@@ -10,7 +10,6 @@ import (
)
// GetCommandStatus - Statut temps réel d'une commande
-// GET /api/v1/commands/:id/status
func GetCommandStatus(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -28,16 +27,12 @@ func GetCommandStatus(c *gin.Context) {
return
}
- log.Printf("đ [STATUS] Client %s demande statut cmd %d", usernameStr, commandID)
-
- // Récupérer la commande
command, err := database.GetCommandByID(commandID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
return
}
- // â
VĂRIFIER PROPRIĂTĂ
cmdUsername, _ := command["username"].(string)
if 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
}
- // Récupérer ETA
etaData, _ := database.GetCommandETA(commandID)
- // Récupérer infos livreur (si assigné)
livreurInfo := gin.H{
"assigned": false,
}
@@ -63,7 +56,6 @@ func GetCommandStatus(c *gin.Context) {
}
}
- // Mapper le statut en message lisible
statusMessage := getStatusMessage(command["status"].(string))
c.JSON(http.StatusOK, gin.H{
@@ -80,7 +72,6 @@ func GetCommandStatus(c *gin.Context) {
}
// GetMyCommandsWithTracking - Liste des commandes avec suivi
-// GET /api/v1/my-commands
func GetMyCommandsWithTracking(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -147,7 +138,6 @@ func GetMyCommandsWithTracking(c *gin.Context) {
}
// GetCommandTracking - Suivi détaillé d'une commande
-// GET /api/v1/commands/:id/tracking
func GetCommandTracking(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -174,13 +164,10 @@ func GetCommandTracking(c *gin.Context) {
return
}
- // Récupérer logs
logs, _ := database.GetCommandLogs(commandID)
- // ETA
etaData, _ := database.GetCommandETA(commandID)
- // Timeline (basé sur les logs)
timeline := buildTimeline(logs)
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 {
messages := map[string]string{
"pending": "âł En attente d'assignation",
@@ -219,7 +201,7 @@ func getStatusMessage(status string) string {
}
// 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)
for _, logEntry := range logs {
diff --git a/backend/gestion/handlers/commands.go b/backend/gestion/handlers/commands.go
index 6299941b..bb355292 100644
--- a/backend/gestion/handlers/commands.go
+++ b/backend/gestion/handlers/commands.go
@@ -3,6 +3,7 @@ package handlers
import (
"fmt"
"gestion/db"
+ "gestion/utils"
"log"
"net/http"
"strconv"
@@ -42,9 +43,6 @@ func checkRateLimit(key string) bool {
return true
}
-// ============================================
-// GESTION ADRESSE & ADMIN
-// ============================================
func safeGetUsername(c *gin.Context) (string, error) {
username, exists := c.Get("username")
if !exists {
@@ -71,13 +69,11 @@ func validateAddress(address string) error {
}
// UpdateCommandAddress met Ă jour l'adresse de livraison d'une commande
-// PUT /api/v1/admin/commands/:id/address
func UpdateCommandAddress(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
- // â
Vérification du rÎle
- if c.GetString("role") != "admin" {
- log.Printf("â [UPD_ADDR] AccĂšs refusĂ© - role=%s", c.GetString("role"))
+ userRole := c.GetString("role")
+ if !utils.CheckRoleAdmin(c, userRole) {
c.JSON(http.StatusForbidden, gin.H{"error": "AccÚs refusé"})
return
}
@@ -111,13 +107,11 @@ func UpdateCommandAddress(c *gin.Context) {
return
}
- // â
Validation de l'adresse
if err := validateAddress(req.DeliveryAddress); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
- // â
Logs sanitizés
log.Printf("đ [UPD_ADDR] Admin %s modifie cmd %d", adminUsername, commandID)
command, err := database.GetCommandByID(commandID)
@@ -156,13 +150,11 @@ func UpdateCommandAddress(c *gin.Context) {
}
// 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) {
database := c.MustGet("database").(*db.Database)
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é"})
return
}
@@ -218,7 +210,6 @@ func ProposeAddressChange(c *gin.Context) {
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{
"success": true,
"message": "Nouvelle adresse proposée au client",
@@ -231,6 +222,11 @@ func ProposeAddressChange(c *gin.Context) {
func RespondToAddressProposal(c *gin.Context) {
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)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"})
@@ -276,8 +272,7 @@ func GetAllCommands(c *gin.Context) {
username := c.Query("username")
userRole := c.GetString("role")
- if userRole != "admin" && userRole != "cabine" {
- log.Printf("â [VALIDATE] AccĂšs refusĂ© - role=%s", userRole)
+ if !utils.CheckRoleAdmin(c, userRole) && !utils.CheckRoleCabine(c, userRole) {
c.JSON(http.StatusForbidden, gin.H{"error": "AccÚs refusé"})
return
}
@@ -725,8 +720,7 @@ func GetClientCommandsHistory(c *gin.Context) {
} else if client == nil {
log.Printf("â ïž [HISTORY] client est NIL!")
} else {
- log.Printf("â
[HISTORY] Client récupéré: username=%s, point=%d, point_zipette=%d",
- client.Username, client.Point, client.PointZipette)
+ log.Printf("â
[HISTORY] Client récupéré: username=%s", client.Username)
}
resp := gin.H{
@@ -760,7 +754,6 @@ func GetClientCommandsHistory(c *gin.Context) {
"prenom": client.Prenom,
"telephone": client.Telephone,
"total_commands": client.Command,
- "points": client.Point,
"pool_points": poolPoints,
"pool_names": poolNames,
"penalties": client.Amende,
diff --git a/backend/gestion/handlers/crypto_payment.go b/backend/gestion/handlers/crypto_payment.go
index eeb5e96b..5f06d42a 100644
--- a/backend/gestion/handlers/crypto_payment.go
+++ b/backend/gestion/handlers/crypto_payment.go
@@ -13,7 +13,6 @@ import (
)
// IPNWebhook - POST /api/v1/webhooks/nowpayments
-// Reçoit les callbacks de NowPayments lors des changements de statut
func IPNWebhook(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
np, ok := c.MustGet("nowpayments").(*services.NowPaymentsClient)
@@ -44,7 +43,6 @@ func IPNWebhook(c *gin.Context) {
payment, err := database.GetCryptoPaymentByNowPaymentID(payload.PaymentID.String())
if err != nil || payment == nil {
log.Printf("[IPN] paiement introuvable: %s", payload.PaymentID.String())
- // 200 pour éviter les retries NowPayments
c.JSON(http.StatusOK, gin.H{"ok": true})
return
}
diff --git a/backend/gestion/handlers/deleviry.go b/backend/gestion/handlers/deleviry.go
index 19cb29dc..91959b19 100644
--- a/backend/gestion/handlers/deleviry.go
+++ b/backend/gestion/handlers/deleviry.go
@@ -1,25 +1,18 @@
-// ============================================
-// handlers/delivery_handlers.go
-// đ§ VERSION MODIFIĂE avec ETA automatique
-// ============================================
-
package handlers
import (
"encoding/json"
"fmt"
"gestion/db"
+ "gestion/utils"
"log"
- "math"
"net/http"
+ "slices"
"strconv"
"github.com/gin-gonic/gin"
)
-// ============================================
-// đ§ GetMyDeliveries
-// ============================================
func GetMyDeliveries(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -46,7 +39,6 @@ func GetMyDeliveries(c *gin.Context) {
return
}
- // âš FILTRAGE (SANS TĂLĂPHONE)
filteredCommands := make([]gin.H, len(commands))
for i, cmd := range commands {
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) {
database := c.MustGet("database").(*db.Database)
@@ -215,7 +203,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
return
}
- // â
VĂRIFIER PROPRIĂTĂ
livreurAssign, _ := command["livreur_assign"].(string)
if livreurAssign != usernameStr {
log.Printf("â AccĂšs refusĂ© - assignĂ© Ă %s", livreurAssign)
@@ -225,7 +212,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
return
}
- // â
STATUTS VALIDES POUR LIVREUR (correspondant Ă la DB)
validStatuses := []string{
"assigned",
"en_route",
@@ -234,15 +220,7 @@ func UpdateDeliveryStatus(c *gin.Context) {
"cancelled",
}
- isValid := false
- for _, s := range validStatuses {
- if req.Status == s {
- isValid = true
- break
- }
- }
-
- if !isValid {
+ if !slices.Contains(validStatuses, req.Status) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Statut invalide",
"valid_statuses": validStatuses,
@@ -261,7 +239,7 @@ func UpdateDeliveryStatus(c *gin.Context) {
destLon, _ := command["dest_longitude"].(float64)
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)
if distance > 100 {
@@ -295,10 +273,8 @@ func UpdateDeliveryStatus(c *gin.Context) {
if req.Status == "en_route" {
log.Printf("đ [STATUS_LIVREUR] Passage en 'en_route' - Calcul ETA...")
- // Récupérer les coordonnées destination
var destLat, destLon float64
- // 1. Essayer le cache Redis
destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
destData, err := db.Redis.Get(db.RedisCtx, destCacheKey).Result()
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 {
etaMinutes = database.CalculateETAForDeliveryman(usernameStr, destLat, destLon)
- // Définir l'ETA dans Redis
if err := database.SetCommandETA(commandID, etaMinutes); err != nil {
log.Printf("â ïž [STATUS_LIVREUR] Erreur dĂ©finition ETA: %v", err)
} else {
log.Printf("â
[STATUS_LIVREUR] ETA défini: %d minutes", etaMinutes)
- etaMessage = fmt.Sprintf("Arrivée prévue dans %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)
+ }
}
} else {
log.Printf("â ïž [STATUS_LIVREUR] CoordonnĂ©es destination manquantes - ETA par dĂ©faut")
- etaMinutes = 30 // Fallback
+ etaMinutes = 30
database.SetCommandETA(commandID, etaMinutes)
- etaMessage = "Arrivée prévue dans 30 minutes (estimation par défaut)"
}
// Mettre Ă jour le statut du livreur en "delivering"
@@ -352,7 +335,7 @@ func UpdateDeliveryStatus(c *gin.Context) {
// Log
message := req.Notes
if message == "" {
- message = getDeliveryStatusMessage(req.Status)
+ message = utils.GetDeliveryStatusMessage(req.Status)
}
if etaMessage != "" {
message += fmt.Sprintf(" - %s", etaMessage)
@@ -366,9 +349,21 @@ func UpdateDeliveryStatus(c *gin.Context) {
switch req.Status {
case "en_route":
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 {
+ 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)
+ clientMsg = fmt.Sprintf("đ” Votre commande #%d est en route !", commandID)
}
case "arrived":
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)
}
-
-// ============================================
-// 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)
-}
diff --git a/backend/gestion/handlers/delivery_admin.go b/backend/gestion/handlers/delivery_admin.go
index 50be7352..4bc3cfe0 100644
--- a/backend/gestion/handlers/delivery_admin.go
+++ b/backend/gestion/handlers/delivery_admin.go
@@ -8,6 +8,7 @@ package handlers
import (
"fmt"
"gestion/db"
+ "gestion/utils"
"log"
"net/http"
"strconv"
@@ -16,12 +17,7 @@ import (
"github.com/gin-gonic/gin"
)
-// ============================================
-// đ GET DELIVERY PERSON DETAILS
-// ============================================
-
// GetDeliveryPersonDetails récupÚre les détails complets d'un livreur
-// GET /api/v2/admin/protected/delivery-persons/:username
func GetDeliveryPersonDetails(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -39,11 +35,6 @@ func GetDeliveryPersonDetails(c *gin.Context) {
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)
if err != nil {
log.Printf("â [GET_DELIVERY_DETAILS] Livreur non trouvĂ©: %v", err)
@@ -64,9 +55,6 @@ func GetDeliveryPersonDetails(c *gin.Context) {
return
}
- // ============================================
- // ĂTAPE 2: RĂ©cupĂ©rer le statut et la position GPS
- // ============================================
status, err := database.GetDeliveryPersonStatus(username)
if err != nil {
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)
currentCommand, _ := database.GetCurrentCommand(username)
- // Compter les livraisons
totalDeliveries, _ := database.CountDeliveriesByStatus(username, "")
completedDeliveries, _ := database.CountDeliveriesByStatus(username, "approved")
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{
"success": true,
"deliveryman": gin.H{
@@ -116,19 +95,12 @@ func GetDeliveryPersonDetails(c *gin.Context) {
})
}
-// ============================================
-// đ UPDATE DELIVERY PERSON STATUS
-// ============================================
-
// UpdateDeliveryPersonStatusAdmin modifie le statut d'un livreur (Admin)
-// PUT /api/v2/admin/protected/delivery-persons/:username/status
func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
- // â
SĂCURITĂ: Admin seulement
userRole := c.GetString("role")
- if userRole != "admin" {
- log.Printf("â [UPDATE_DELIVERY_STATUS] AccĂšs refusĂ© - role=%s", userRole)
+ if !utils.CheckRoleAdmin(c, userRole) {
c.JSON(http.StatusForbidden, gin.H{"error": "AccÚs refusé"})
return
}
@@ -172,9 +144,6 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
log.Printf("đ [UPDATE_DELIVERY_STATUS] Modification: %s â %s", username, req.Status)
- // ============================================
- // Vérifier que le livreur existe
- // ============================================
livreur, err := database.GetUserByUsername(username)
if err != nil {
log.Printf("â [UPDATE_DELIVERY_STATUS] Livreur non trouvĂ©")
@@ -189,9 +158,6 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
return
}
- // ============================================
- // Mettre Ă jour le statut
- // ============================================
err = database.UpdateDeliveryPersonStatus(username, req.Status)
if err != nil {
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
-// GET /api/v2/admin/protected/delivery-persons/:username/stats
func GetDeliveryPersonStats(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
- // â
SĂCURITĂ: Admin seulement
userRole := c.GetString("role")
if userRole != "admin" {
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)
- // ============================================
- // Vérifier que le livreur existe
- // ============================================
livreur, err := database.GetUserByUsername(username)
if err != nil {
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
-// GET /api/v2/admin/protected/delivery-persons/:username/history?limit=20&offset=0
func GetDeliveryPersonHistory(c *gin.Context) {
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)
// PUT /api/v2/admin/protected/delivery-persons/:username/location
func UpdateDeliveryPersonLocationAdmin(c *gin.Context) {
@@ -579,7 +527,6 @@ func RemoveCommandFromQueue(c *gin.Context) {
if err != nil {
log.Printf("â ïž [REMOVE_FROM_QUEUE] Impossible de rĂ©initialiser le statut: %v", err)
} else {
- // Retirer l'assignation du livreur
database.UpdateCommandLivreur(commandID, "")
log.Printf("â
[REMOVE_FROM_QUEUE] Commande réinitialisée en 'pending'")
}
diff --git a/backend/gestion/handlers/eta.go b/backend/gestion/handlers/eta.go
index cf057e61..e1565463 100644
--- a/backend/gestion/handlers/eta.go
+++ b/backend/gestion/handlers/eta.go
@@ -18,11 +18,6 @@ import (
"github.com/gin-gonic/gin"
)
-// ============================================
-// GET /api/v1/orders/:id/eta
-// â
CORRECTION: ETA visible UNIQUEMENT si status >= en_route
-// ============================================
-
func GetOrderETA(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
geoService := c.MustGet("geoService").(*services.GeoService)
@@ -49,9 +44,6 @@ func GetOrderETA(c *gin.Context) {
return
}
- log.Printf("đ [ETA] START - commandID=%d, username=%s", commandID, username.(string))
-
- // 3ïžâŁ RĂCUPĂRER LA COMMANDE
command, err := database.GetCommandByID(commandID)
if err != nil {
log.Printf("â [ETA] Commande %d non trouvĂ©e", commandID)
diff --git a/backend/gestion/handlers/geoloca.go b/backend/gestion/handlers/geoloca.go
index 23da5bf5..d3278eec 100644
--- a/backend/gestion/handlers/geoloca.go
+++ b/backend/gestion/handlers/geoloca.go
@@ -22,8 +22,6 @@ import (
// ============================================
// 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) {
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
func FindNearestDeliveryPerson(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -181,8 +175,6 @@ func FindNearestDeliveryPerson(c *gin.Context) {
// ============================================
// 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) {
database := c.MustGet("database").(*db.Database)
geoService := c.MustGet("geoService").(*services.GeoService)
diff --git a/backend/gestion/handlers/gps.go b/backend/gestion/handlers/gps.go
index 70295780..0e88af82 100644
--- a/backend/gestion/handlers/gps.go
+++ b/backend/gestion/handlers/gps.go
@@ -6,9 +6,11 @@
package handlers
import (
+ "fmt"
"gestion/db"
"log"
"net/http"
+ "net/url"
"strconv"
"github.com/gin-gonic/gin"
@@ -127,3 +129,49 @@ func GetCommandNavigationLinks(c *gin.Context) {
"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,
+ })
+}
diff --git a/backend/gestion/handlers/history.go b/backend/gestion/handlers/history.go
index fe791077..902e8ae5 100644
--- a/backend/gestion/handlers/history.go
+++ b/backend/gestion/handlers/history.go
@@ -83,7 +83,7 @@ func GetMyCompletedOrders(c *gin.Context) {
response["client_stats"] = gin.H{
"username": client.Username,
"total_commands": client.Command,
- "points": client.Point,
+ "points_extra": client.PointsExtra,
"pool_points": poolPoints,
"pool_names": poolNames,
"penalties": client.Amende,
@@ -169,7 +169,7 @@ func GetMyCompletedOrdersWithItems(c *gin.Context) {
"prenom": client.Prenom,
"telephone": client.Telephone,
"total_commands": client.Command,
- "points": client.Point,
+ "points_extra": client.PointsExtra,
"penalties": client.Amende,
}
}
diff --git a/backend/gestion/handlers/notifications.go b/backend/gestion/handlers/notifications.go
index b4ecd904..e26d2337 100644
--- a/backend/gestion/handlers/notifications.go
+++ b/backend/gestion/handlers/notifications.go
@@ -9,54 +9,6 @@ import (
"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é
// GET /api/v1/notifications
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é
// GET /api/v1/livreur/notifications
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
// POST /api/v1/notifications/read
func MarkNotificationsRead(c *gin.Context) {
diff --git a/backend/gestion/handlers/panier.go b/backend/gestion/handlers/panier.go
index 1c40ba53..26cd9196 100644
--- a/backend/gestion/handlers/panier.go
+++ b/backend/gestion/handlers/panier.go
@@ -295,8 +295,8 @@ func ValidateBasket(c *gin.Context) {
var req struct {
DeliveryAddress string `json:"delivery_address" binding:"required"`
UseReferralBalance bool `json:"use_referral_balance"`
- PaymentMethod string `json:"payment_method"` // "cash" (défaut) ou "crypto"
- PayCurrency string `json:"pay_currency"` // ex: "btc", "eth", "ltc" (requis si crypto)
+ PaymentMethod string `json:"payment_method"` // "cash" (défaut) ou "crypto"
+ PayCurrency string `json:"pay_currency"` // ex: "btc", "eth", "ltc" (requis si crypto)
}
if err := c.ShouldBindJSON(&req); err != nil || req.DeliveryAddress == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse de livraison requise"})
@@ -310,6 +310,12 @@ func ValidateBasket(c *gin.Context) {
}
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)
// ============================================
@@ -430,7 +436,7 @@ func ValidateBasket(c *gin.Context) {
command, err := database.CreateCommandWithAddress(usernameStr, req.DeliveryAddress)
if err != nil {
if referralUsed > 0 {
- _ = database.RestoreReferralBalance(usernameStr, referralUsed)
+ _ = database.CreditClientReferral(usernameStr, referralUsed)
}
log.Printf("â [CHECKOUT] Erreur crĂ©ation commande: %v", err)
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
_ = database.CancelCryptoCommand(commandID)
if referralUsed > 0 {
- _ = database.RestoreReferralBalance(usernameStr, referralUsed)
+ _ = database.CreditClientReferral(usernameStr, referralUsed)
}
log.Printf("â [CHECKOUT] Erreur crĂ©ation paiement NowPayments: %v", err)
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 {
log.Printf("đ [CHECKOUT] Adresse gĂ©ocodĂ©e: %.6f,%.6f", location.Latitude, location.Longitude)
- activeLivreurs, err := database.GetAllActiveDeliveryPersons()
- if err == nil && len(activeLivreurs) > 0 {
- log.Printf("đ [CHECKOUT] %d livreurs actifs disponibles", len(activeLivreurs))
-
- usernames := make([]string, len(activeLivreurs))
- for i, livreur := range activeLivreurs {
- usernames[i] = livreur.Username
- }
+ usernames, errEligible := database.GetEligibleDeliverymenForCommand(commandID)
+ if errEligible == nil && len(usernames) > 0 {
+ log.Printf("đ [CHECKOUT] %d livreur(s) Ă©ligible(s) disponibles", len(usernames))
nearest, err := geoService.FindNearestDeliveryPersonFast(services.Coordinates{
Latitude: location.Latitude,
@@ -599,7 +600,7 @@ func ValidateBasket(c *gin.Context) {
log.Printf("â ïž [CHECKOUT] Aucun livreur trouvĂ©: %v", err)
}
} else {
- log.Printf("â ïž [CHECKOUT] Aucun livreur actif disponible")
+ log.Printf("â ïž [CHECKOUT] Aucun livreur Ă©ligible disponible")
}
} else {
log.Printf("â ïž [CHECKOUT] Erreur gĂ©ocodage: %v", err)
diff --git a/backend/gestion/handlers/product.go b/backend/gestion/handlers/product.go
index 92160ff7..6b7fd506 100644
--- a/backend/gestion/handlers/product.go
+++ b/backend/gestion/handlers/product.go
@@ -12,7 +12,6 @@ import (
"path/filepath"
"strconv"
"strings"
- "time"
"github.com/gabriel-vasile/mimetype"
"github.com/gin-gonic/gin"
@@ -654,41 +653,12 @@ func UpdateProduct(c *gin.Context) {
log.Printf("đ [UpdateProduct] %s met Ă jour produit #%d", username, id)
- // â
UPDATE PRODUIT
- 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 {
+ if err := database.UpdateProduct(id, updateData.Name, updateData.Category, updateData.Description, updateData.Unit, updateData.Stock, updateData.Prices); err != nil {
log.Printf("â [UpdateProduct] Erreur UPDATE: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise Ă jour"})
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
updatedProduct, _ := database.GetProductByID(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) {
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) {
database := c.MustGet("database").(*db.Database)
diff --git a/backend/gestion/handlers/redis_services.go b/backend/gestion/handlers/redis_services.go
index 237f250e..6a193328 100644
--- a/backend/gestion/handlers/redis_services.go
+++ b/backend/gestion/handlers/redis_services.go
@@ -962,7 +962,7 @@ func ResetClientPointAdmin(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
extraPoolKey := ""
- if req.Pool >= 2 {
+ if req.Pool >= 0 {
if settings, err := database.GetSettings(); err == nil && req.Pool < len(settings.PointsPools) {
extraPoolKey = settings.PointsPools[req.Pool].Key
}
diff --git a/backend/gestion/handlers/settings.go b/backend/gestion/handlers/settings.go
index ffc68446..6bd783eb 100644
--- a/backend/gestion/handlers/settings.go
+++ b/backend/gestion/handlers/settings.go
@@ -2,8 +2,11 @@ package handlers
import (
"gestion/db"
+ "gestion/models"
+ "gestion/services"
"log"
"net/http"
+ "os"
"github.com/gin-gonic/gin"
)
@@ -27,18 +30,18 @@ func GetPublicSettings(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{
- "success": true,
- "penalties_enabled": settings.PenaltiesEnabled,
- "show_amende_score": settings.ShowAmendeScore,
- "points_enabled": settings.PointsEnabled,
- "points_separated": len(settings.PointsPools) > 1,
- "pool_names": poolNames,
- "pool_keys": poolKeys,
- "referral_enabled": settings.ReferralEnabled,
- "delivery_schedule": settings.DeliverySchedule,
- "crypto_payment_enabled": settings.CryptoPaymentEnabled,
- "crypto_only": settings.CryptoOnly,
- "nowpayments_currencies": settings.NowPaymentsCurrencies,
+ "success": true,
+ "penalties_enabled": settings.PenaltiesEnabled,
+ "show_amende_score": settings.ShowAmendeScore,
+ "points_enabled": settings.PointsEnabled,
+ "points_separated": len(settings.PointsPools) > 1,
+ "pool_names": poolNames,
+ "pool_keys": poolKeys,
+ "referral_enabled": settings.ReferralEnabled,
+ "delivery_schedule": settings.DeliverySchedule,
+ "crypto_payment_enabled": settings.CryptoPaymentEnabled,
+ "crypto_only": settings.CryptoOnly,
+ "nowpayments_currencies": settings.NowPaymentsCurrencies,
})
}
@@ -60,7 +63,7 @@ func GetSettings(c *gin.Context) {
func UpdateSettings(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
- var req db.AppSettings
+ var req models.AppSettings
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "ParamĂštres invalides"})
return
@@ -72,7 +75,20 @@ func UpdateSettings(c *gin.Context) {
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})
}
diff --git a/backend/gestion/handlers/telegram.go b/backend/gestion/handlers/telegram.go
new file mode 100644
index 00000000..fcf94917
--- /dev/null
+++ b/backend/gestion/handlers/telegram.go
@@ -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 â 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,
+ "đ Bienvenue !\n\nPour lier votre compte, gĂ©nĂ©rez un token depuis l'application et envoyez /start <token>.")
+ }
+ }
+
+ 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,
+ "â
Compte lié avec succÚs !\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})
+}
diff --git a/backend/gestion/handlers/update_profile.go b/backend/gestion/handlers/update_profile.go
index 02d059a1..c95bb331 100644
--- a/backend/gestion/handlers/update_profile.go
+++ b/backend/gestion/handlers/update_profile.go
@@ -16,6 +16,7 @@ import (
"gestion/db"
"gestion/models"
+ "gestion/utils"
)
// ============================================
@@ -99,12 +100,12 @@ func UpdateMyProfile(c *gin.Context) {
// Mise à jour du téléphone
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)
c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
return
}
- normalizedPhone := normalizePhoneNumber(req.Telephone)
+ normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
// Vérifier que le téléphone n'est pas déjà utilisé
if existingClient, _ := database.GetClientByTelephone(normalizedPhone); existingClient != nil && existingClient.ID != clientID {
@@ -202,8 +203,8 @@ func UpdateClientByAdmin(c *gin.Context) {
}
// â
LOG DEBUG - Ătat initial
- log.Printf("đ [UPDATE_CLIENT_ADMIN] Ătat initial - Command: %d, Point: %d, PointZipette: %d, Amende: %.2f",
- client.Command, client.Point, client.PointZipette, client.Amende)
+ log.Printf("đ [UPDATE_CLIENT_ADMIN] Ătat initial - Command: %d, Amende: %.2f",
+ client.Command, client.Amende)
// Vérifier si des modifications sont demandées
hasChanges := false
@@ -254,12 +255,12 @@ func UpdateClientByAdmin(c *gin.Context) {
// Mise à jour du téléphone
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)
c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
return
}
- normalizedPhone := normalizePhoneNumber(req.Telephone)
+ normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
// Vérifier que le téléphone n'est pas déjà utilisé
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)
}
- // â
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 {
client.Command = *req.Command
hasChanges = true
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 {
client.Amende = *req.Amende
hasChanges = true
@@ -310,9 +294,8 @@ func UpdateClientByAdmin(c *gin.Context) {
return
}
- // â
LOG DEBUG - Ătat avant sauvegarde
- log.Printf("đ [UPDATE_CLIENT_ADMIN] Ătat avant save - Command: %d, Point: %d, PointZipette: %d, Amende: %.2f",
- client.Command, client.Point, client.PointZipette, client.Amende)
+ log.Printf("đ [UPDATE_CLIENT_ADMIN] Ătat avant save - Command: %d, Amende: %.2f",
+ client.Command, client.Amende)
// Sauvegarder les modifications
if err := database.UpdateClient(client); err != nil {
@@ -446,15 +429,14 @@ func UpdateUserByAdmin(c *gin.Context) {
func sanitizeClient(client *models.Client) gin.H {
return gin.H{
- "id": client.ID,
- "username": client.Username,
- "nom": client.Nom,
- "prenom": client.Prenom,
- "telephone": client.Telephone,
- "command": client.Command,
- "point": client.Point,
- "points_zipette": client.PointZipette, // â
AJOUTĂ
- "amende": client.Amende,
+ "id": client.ID,
+ "username": client.Username,
+ "nom": client.Nom,
+ "prenom": client.Prenom,
+ "telephone": client.Telephone,
+ "command": client.Command,
+ "points_extra": client.PointsExtra,
+ "amende": client.Amende,
}
}
diff --git a/backend/gestion/handlers/validation_deleviry.go b/backend/gestion/handlers/validation_deleviry.go
index 670bcb07..545201a0 100644
--- a/backend/gestion/handlers/validation_deleviry.go
+++ b/backend/gestion/handlers/validation_deleviry.go
@@ -352,6 +352,12 @@ func StartDelivery(c *gin.Context) {
fmt.Sprintf("Livraison démarrée par %s", 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)
c.JSON(http.StatusOK, gin.H{
diff --git a/backend/gestion/handlers/zones.go b/backend/gestion/handlers/zones.go
index aeb26ef6..7eef8d81 100644
--- a/backend/gestion/handlers/zones.go
+++ b/backend/gestion/handlers/zones.go
@@ -1,7 +1,7 @@
package handlers
import (
- "gestion/db"
+ "gestion/models"
"regexp"
)
@@ -37,7 +37,7 @@ type zoneCheckResult struct {
// Les zones sont lues depuis la DB (settings.PostalZones).
// Code postal introuvable â 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)
if code == "" {
return zoneCheckResult{PostalCode: "", ZoneName: "inconnue", MinAmount: 0, OK: false}
diff --git a/backend/gestion/main.go b/backend/gestion/main.go
index 57b2ff40..e0419225 100644
--- a/backend/gestion/main.go
+++ b/backend/gestion/main.go
@@ -47,6 +47,36 @@ func main() {
geoService := services.NewGeoService(db.Redis, db.RedisCtx)
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
// ============================================
@@ -128,7 +158,6 @@ func main() {
c.Next()
})
- // Middleware NowPayments : injecte le client si crypto activé dans les settings
r.Use(func(c *gin.Context) {
settings, err := database.GetSettings()
if err == nil && settings.CryptoPaymentEnabled && settings.NowPaymentsAPIKey != "" {
@@ -146,74 +175,7 @@ func main() {
// ============================================
routes.SetupRoutes(r, database, geoService)
- // ============================================
- // LANCEMENT DU SERVEUR
- // ============================================
- printServerInfo()
-
if err := r.Run(":8080"); err != nil {
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("")
-}
diff --git a/backend/gestion/middleware/clock_middleware.go b/backend/gestion/middleware/clock_middleware.go
index 860ad12b..6c6c7af5 100644
--- a/backend/gestion/middleware/clock_middleware.go
+++ b/backend/gestion/middleware/clock_middleware.go
@@ -3,6 +3,7 @@ package middleware
import (
"fmt"
"gestion/db"
+ "gestion/models"
"log"
"net/http"
"time"
@@ -30,7 +31,7 @@ func OrderHoursMiddleware(c *gin.Context) {
}
sched := settings.DeliverySchedule
- var day db.DaySchedule
+ var day models.DaySchedule
switch weekday {
case time.Monday:
day = sched.Monday
diff --git a/backend/gestion/models/auth.go b/backend/gestion/models/auth.go
new file mode 100644
index 00000000..990f8853
--- /dev/null
+++ b/backend/gestion/models/auth.go
@@ -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"`
+}
diff --git a/backend/gestion/models/client.go b/backend/gestion/models/client.go
index dfeca26a..8e6e410a 100644
--- a/backend/gestion/models/client.go
+++ b/backend/gestion/models/client.go
@@ -10,10 +10,8 @@ type Client struct {
Nom string `json:"nom"`
Prenom string `json:"prenom"`
Telephone string `json:"telephone"`
- Command int `json:"command"`
- Point int `json:"point"`
- PointZipette int `json:"points_zipette"`
- PointsExtra map[string]int `json:"points_extra"` // pools[2+]
+ Command int `json:"command"`
+ PointsExtra map[string]int `json:"points_extra"`
Amende float64 `json:"amende"`
CancellationsCount int `json:"cancellations_count"`
LastPenaltyReason string `json:"last_penalty_reason"`
diff --git a/backend/gestion/models/settings.go b/backend/gestion/models/settings.go
new file mode 100644
index 00000000..60b27c60
--- /dev/null
+++ b/backend/gestion/models/settings.go
@@ -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
+}
diff --git a/backend/gestion/models/telegram.go b/backend/gestion/models/telegram.go
new file mode 100644
index 00000000..86b29227
--- /dev/null
+++ b/backend/gestion/models/telegram.go
@@ -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"
+}
diff --git a/backend/gestion/models/update_profile.go b/backend/gestion/models/update_profile.go
index 77403449..11821a05 100644
--- a/backend/gestion/models/update_profile.go
+++ b/backend/gestion/models/update_profile.go
@@ -20,8 +20,6 @@ type AdminUpdateClientRequest struct {
Nom string `json:"nom,omitempty"`
Prenom string `json:"prenom,omitempty"`
Telephone string `json:"telephone,omitempty"`
- Command *int `json:"command,omitempty"`
- Point *int `json:"point,omitempty"`
- PointsZipette *int `json:"points_zipette,omitempty"`
- Amende *float64 `json:"amende,omitempty"`
+ Command *int `json:"command,omitempty"`
+ Amende *float64 `json:"amende,omitempty"`
}
diff --git a/backend/gestion/routes/routes.go b/backend/gestion/routes/routes.go
index aca5b234..9bee1fd9 100644
--- a/backend/gestion/routes/routes.go
+++ b/backend/gestion/routes/routes.go
@@ -97,9 +97,10 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
cartGroupV1.GET("/notifications", handlers.GetClientNotifications)
cartGroupV1.POST("/notifications/read", handlers.MarkNotificationsRead)
- // đ± PUSH TOKEN
- cartGroupV1.POST("/push-token", handlers.RegisterPushToken)
- cartGroupV1.DELETE("/push-token", handlers.UnregisterPushToken)
+ // đ€ TELEGRAM CLIENT
+ cartGroupV1.POST("/telegram/link-token", handlers.GenerateClientLinkToken)
+ cartGroupV1.GET("/telegram/status", handlers.GetClientTelegramStatus)
+ cartGroupV1.DELETE("/telegram/unlink", handlers.UnlinkClientTelegram)
// đ€ PROFIL CLIENT
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)
+ // ============================================
+ // đ€ WEBHOOK TELEGRAM - PUBLIC (sĂ©curisĂ© par secret header)
+ // ============================================
+ router.POST("/webhook/telegram", handlers.TelegramWebhook)
+
// ============================================
// đ 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.Use(middleware.AdminMiddleware)
{
- // PUSH TOKEN ADMIN
- adminGroupV2.POST("/push-token", handlers.RegisterAdminPushToken)
- adminGroupV2.DELETE("/push-token", handlers.UnregisterAdminPushToken)
-
// đ NOTIFICATIONS ADMIN
adminGroupV2.GET("/notifications", handlers.GetLivreurNotifications)
adminGroupV2.POST("/notifications/read", handlers.MarkLivreurNotificationsRead)
+ // đ€ TELEGRAM ADMIN
+ adminGroupV2.POST("/telegram/link-token", handlers.GenerateAdminLinkToken)
+ adminGroupV2.DELETE("/telegram/unlink", handlers.UnlinkAdminTelegram)
+
// ============================================
// CLIENT - GESTION
// ============================================
@@ -285,14 +291,14 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
cabineGroupV1.POST("/add/address", handlers.AddAddress)
cabineGroupV1.DELETE("/delete/address", handlers.DeleteAddress)
cabineGroupV1.GET("/addresses", handlers.GetAllAddress)
- // PUSH TOKEN CABINE
- cabineGroupV1.POST("/push-token", handlers.RegisterCabinePushToken)
- cabineGroupV1.DELETE("/push-token", handlers.UnregisterCabinePushToken)
-
// đ NOTIFICATIONS CABINE
cabineGroupV1.GET("/notifications", handlers.GetLivreurNotifications)
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.POST("/commands/:id/confirm-reception", handlers.StaffApproveDelivery)
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.POST("/deliveries/:id/start", handlers.StartDelivery)
livreurGroupV1.PUT("/deliveries/:id/status", handlers.UpdateDeliveryStatus) // â
Avec GPS
+ livreurGroupV1.GET("/deliveries/:id/nav-link", handlers.GetLivreurNavLink) // Lien Waze App
// ============================================
// POSITION GPS
@@ -362,7 +369,10 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
// ============================================
livreurGroupV1.GET("/notifications", handlers.GetLivreurNotifications)
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)
}
}
diff --git a/backend/gestion/services/telegram.go b/backend/gestion/services/telegram.go
new file mode 100644
index 00000000..f3a0042d
--- /dev/null
+++ b/backend/gestion/services/telegram.go
@@ -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
+}
diff --git a/backend/gestion/utils/auth.go b/backend/gestion/utils/auth.go
new file mode 100644
index 00000000..a8e2ac07
--- /dev/null
+++ b/backend/gestion/utils/auth.go
@@ -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
+}
diff --git a/backend/gestion/utils/cabine.go b/backend/gestion/utils/cabine.go
new file mode 100644
index 00000000..9595db8f
--- /dev/null
+++ b/backend/gestion/utils/cabine.go
@@ -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
+}
diff --git a/backend/gestion/utils/delivery.go b/backend/gestion/utils/delivery.go
new file mode 100644
index 00000000..fc58aede
--- /dev/null
+++ b/backend/gestion/utils/delivery.go
@@ -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)
+}
diff --git a/backend/gestion/workers/cron_auto_assign.go b/backend/gestion/workers/cron_auto_assign.go
index 78a9d18d..d0c0d46d 100644
--- a/backend/gestion/workers/cron_auto_assign.go
+++ b/backend/gestion/workers/cron_auto_assign.go
@@ -126,18 +126,13 @@ func tryAssignCommandWithPriority(
Longitude: location.Longitude,
}
- // 2. Récupérer livreurs actifs
- activeLivreurs, err := database.GetAllActiveDeliveryPersons()
- if err != nil || len(activeLivreurs) == 0 {
- log.Printf("â ïž [CRON] Cmd %d - Aucun livreur disponible", commandID)
+ // 2. Récupérer livreurs éligibles (selon le mode single/category_based)
+ usernames, err := database.GetEligibleDeliverymenForCommand(commandID)
+ if err != nil || len(usernames) == 0 {
+ log.Printf("â ïž [CRON] Cmd %d - Aucun livreur Ă©ligible disponible", commandID)
return false
}
- usernames := make([]string, len(activeLivreurs))
- for i, livreur := range activeLivreurs {
- usernames[i] = livreur.Username
- }
-
// 3. Trouver le plus proche
nearest, err := geoService.FindNearestDeliveryPersonFast(targetCoords, usernames)
if err != nil {
diff --git a/backend/gestion/workers/redis_worker.go b/backend/gestion/workers/redis_worker.go
index 393ef24f..0a42b3b3 100644
--- a/backend/gestion/workers/redis_worker.go
+++ b/backend/gestion/workers/redis_worker.go
@@ -148,7 +148,8 @@ func PointsSyncWorker(database *db.Database) {
}
// Mettre Ă jour dans la DB principale
- err = database.AddClientPoints(username, points)
+ pool := "pool_0"
+ err = database.AddClientPointsByCategory(username, points, pool)
if err != nil {
log.Printf("â ïž Erreur sync points pour %s: %v", username, err)
continue