chore: refacto

This commit is contained in:
2026-03-27 22:23:46 +01:00
parent 75bf4caaa1
commit 5380abe8ed
67 changed files with 1751 additions and 2573 deletions
+36 -100
View File
@@ -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(&currentStatus, &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(&currentPointsWeed, &currentPointsZipette)
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
}
// ============================================
+55 -338
View File
@@ -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
}
+16 -40
View File
@@ -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
}
+3 -18
View File
@@ -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
}
+67 -244
View File
@@ -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)
+12 -86
View File
@@ -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(&currentStatus, &currentLivreur)
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, &currentStatus, &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
}
+2 -3
View File
@@ -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
}
+3 -38
View File
@@ -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)
+94
View File
@@ -0,0 +1,94 @@
package db
import (
"fmt"
"log"
)
// GetCommandCategories retourne les catégories distinctes des produits d'une commande
func (d *Database) GetCommandCategories(commandID int) ([]string, error) {
query := `
SELECT DISTINCT p.category
FROM command_items ci
JOIN products p ON p.id = ci.product_id
WHERE ci.command_id = $1 AND p.category IS NOT NULL AND p.category != ''`
rows, err := d.Query(query, commandID)
if err != nil {
return nil, fmt.Errorf("erreur lecture catégories commande %d: %w", commandID, err)
}
defer rows.Close()
var categories []string
for rows.Next() {
var cat string
if err := rows.Scan(&cat); err == nil {
categories = append(categories, cat)
}
}
return categories, rows.Err()
}
// GetEligibleDeliverymenForCommand retourne les usernames des livreurs éligibles pour une commande.
// En mode "single" → tous les livreurs actifs.
// En mode "category_based" → le(s) livreur(s) assigné(s) aux catégories de la commande.
// Si aucune correspondance → fallback sur tous les livreurs actifs.
func (d *Database) GetEligibleDeliverymenForCommand(commandID int) ([]string, error) {
settings, err := d.GetSettings()
if err != nil {
log.Printf("⚠️ [DELIVERY_MODE] Erreur lecture settings: %v — fallback single", err)
}
allActive := func() ([]string, error) {
livreurs, err := d.GetAllActiveDeliveryPersons()
if err != nil {
return nil, err
}
names := make([]string, len(livreurs))
for i, l := range livreurs {
names[i] = l.Username
}
return names, nil
}
if settings.DeliveryMode.Mode != "category_based" || len(settings.DeliveryMode.CategoryRoutes) == 0 {
return allActive()
}
// Mode category_based : récupérer les catégories de la commande
categories, err := d.GetCommandCategories(commandID)
if err != nil || len(categories) == 0 {
log.Printf("⚠️ [DELIVERY_MODE] Cmd %d: catégories non trouvées — fallback single", commandID)
return allActive()
}
// Construire un set des catégories de la commande
catSet := make(map[string]bool, len(categories))
for _, c := range categories {
catSet[c] = true
}
// Trouver les livreurs dont la route intersecte les catégories
eligible := make(map[string]bool)
for _, route := range settings.DeliveryMode.CategoryRoutes {
for _, routeCat := range route.Categories {
if catSet[routeCat] {
eligible[route.DeliverymanUsername] = true
break
}
}
}
if len(eligible) == 0 {
log.Printf("⚠️ [DELIVERY_MODE] Cmd %d: aucun livreur pour catégories %v — fallback single", commandID, categories)
return allActive()
}
result := make([]string, 0, len(eligible))
for u := range eligible {
result = append(result, u)
}
log.Printf("🎯 [DELIVERY_MODE] Cmd %d: livreurs éligibles %v (catégories: %v)", commandID, result, categories)
return result, nil
}
+14 -93
View File
@@ -7,7 +7,6 @@ package db
import (
"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
}
+21 -50
View File
@@ -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
}
+1 -36
View File
@@ -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);`,
+5 -5
View File
@@ -79,7 +79,7 @@ func (d *Database) RevokeAllUserTokens(userID int, userType string) error {
}
// GetUserActiveTokens récupère tous les tokens actifs d'un utilisateur
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,
+43 -67
View File
@@ -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)
+42 -102
View File
@@ -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("🔔 <b>Notification</b>\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("🔔 <b>Notification</b>\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("🔔 <b>Nouvelle commande</b>\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("🚨 <b>Alerte livreur</b>\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
+23 -4
View File
@@ -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
}
+3 -28
View File
@@ -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)
-6
View File
@@ -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
+39 -52
View File
@@ -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
}
+51 -78
View File
@@ -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 {
+121
View File
@@ -0,0 +1,121 @@
package db
import (
"crypto/rand"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"gestion/models"
"log"
"time"
)
// MigrateAddTelegramColumns ajoute les colonnes telegram_chat_id si elles n'existent pas
func (d *Database) MigrateAddTelegramColumns() {
migrations := []string{
`ALTER TABLE clients ADD COLUMN IF NOT EXISTS telegram_chat_id BIGINT`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS telegram_chat_id BIGINT`,
}
for _, q := range migrations {
if _, err := d.Exec(q); err != nil {
log.Printf("⚠️ [TELEGRAM_MIGRATION] %v", err)
}
}
log.Println("✅ [TELEGRAM] Colonnes telegram_chat_id vérifiées")
}
const linkTokenTTL = 10 * time.Minute
// GenerateLinkToken crée un token aléatoire sécurisé et le stocke dans Redis (10 min)
func GenerateLinkToken(username, role string) (string, error) {
raw := make([]byte, 16)
if _, err := rand.Read(raw); err != nil {
return "", fmt.Errorf("génération token: %w", err)
}
token := hex.EncodeToString(raw)
data := models.TelegramLinkData{Username: username, Role: role}
val, err := json.Marshal(data)
if err != nil {
return "", err
}
key := fmt.Sprintf("telegram:link:%s", token)
if err := Redis.Set(RedisCtx, key, val, linkTokenTTL).Err(); err != nil {
return "", fmt.Errorf("Redis SET: %w", err)
}
return token, nil
}
// ValidateAndConsumeLinkToken valide le token, retourne les données, puis le supprime
func ValidateAndConsumeLinkToken(token string) (username, role string, err error) {
key := fmt.Sprintf("telegram:link:%s", token)
val, err := Redis.Get(RedisCtx, key).Bytes()
if err != nil {
return "", "", fmt.Errorf("token invalide ou expiré")
}
var data models.TelegramLinkData
if err := json.Unmarshal(val, &data); err != nil {
return "", "", fmt.Errorf("données corrompues")
}
Redis.Del(RedisCtx, key)
return data.Username, data.Role, nil
}
func (d *Database) SaveClientTelegramChatID(username string, chatID int64) error {
_, err := d.Exec(`UPDATE clients SET telegram_chat_id = $1 WHERE username = $2`, chatID, username)
return err
}
func (d *Database) GetClientTelegramChatID(username string) (int64, bool, error) {
var chatID sql.NullInt64
err := d.QueryRow(`SELECT telegram_chat_id FROM clients WHERE username = $1`, username).Scan(&chatID)
if err != nil {
return 0, false, err
}
return chatID.Int64, chatID.Valid, nil
}
func (d *Database) DeleteClientTelegramChatID(username string) error {
_, err := d.Exec(`UPDATE clients SET telegram_chat_id = NULL WHERE username = $1`, username)
return err
}
func (d *Database) SaveUserTelegramChatID(username string, chatID int64) error {
_, err := d.Exec(`UPDATE users SET telegram_chat_id = $1 WHERE username = $2`, chatID, username)
return err
}
func (d *Database) GetUserTelegramChatID(username string) (int64, bool, error) {
var chatID sql.NullInt64
err := d.QueryRow(`SELECT telegram_chat_id FROM users WHERE username = $1`, username).Scan(&chatID)
if err != nil {
return 0, false, err
}
return chatID.Int64, chatID.Valid, nil
}
func (d *Database) DeleteUserTelegramChatID(username string) error {
_, err := d.Exec(`UPDATE users SET telegram_chat_id = NULL WHERE username = $1`, username)
return err
}
// GetUserByTelegramChatID retrouve un utilisateur (clients + users) par chat_id
func (d *Database) GetUserByTelegramChatID(chatID int64) (username, role string, err error) {
err = d.QueryRow(`SELECT username FROM clients WHERE telegram_chat_id = $1`, chatID).Scan(&username)
if err == nil {
return username, "client", nil
}
err = d.QueryRow(`SELECT username, role FROM users WHERE telegram_chat_id = $1`, chatID).Scan(&username, &role)
if err == nil {
return username, role, nil
}
return "", "", fmt.Errorf("aucun compte lié à ce chat_id")
}
+7 -10
View File
@@ -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é")
}
@@ -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
}
@@ -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:
+3 -16
View File
@@ -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
}
-10
View File
@@ -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
}
+2 -2
View File
@@ -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,
+3 -11
View File
@@ -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)
+10 -24
View File
@@ -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,
+79 -218
View File
@@ -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
}
-1
View File
@@ -78,7 +78,6 @@ func (d *Database) CreateClientSession(clientID int, username string, sessionID
// Charger les infos du client (points, penalty) dans le cache
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)
+32 -173
View File
@@ -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,
+22 -52
View File
@@ -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)
+15 -71
View File
@@ -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 {
-4
View File
@@ -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"})
}
+1 -19
View File
@@ -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 {
+11 -18
View File
@@ -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,
@@ -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
}
+31 -77
View File
@@ -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)
}
+2 -55
View File
@@ -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'")
}
-8
View File
@@ -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)
-8
View File
@@ -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)
+48
View File
@@ -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,
})
}
+2 -2
View File
@@ -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,
}
}
-182
View File
@@ -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) {
+14 -13
View File
@@ -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)
+1 -37
View File
@@ -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)
+1 -1
View File
@@ -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
}
+30 -14
View File
@@ -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})
}
+292
View File
@@ -0,0 +1,292 @@
package handlers
import (
"gestion/db"
"gestion/models"
"gestion/services"
"log"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func TelegramWebhook(c *gin.Context) {
// Vérification du secret webhook
secret := c.GetHeader("X-Telegram-Bot-Api-Secret-Token")
if services.TelegramBot == nil || !services.TelegramBot.ValidateWebhookSecret(secret) {
c.Status(http.StatusUnauthorized)
return
}
var update models.TgUpdate
if err := c.ShouldBindJSON(&update); err != nil {
c.Status(http.StatusBadRequest)
return
}
if update.Message == nil {
c.Status(http.StatusOK)
return
}
text := strings.TrimSpace(update.Message.Text)
chatID := update.Message.Chat.ID
// Commande /start <token> — liaison de compte
if token, ok := strings.CutPrefix(text, "/start "); ok {
token = strings.TrimSpace(token)
handleLinkAccount(c, token, chatID)
return
}
// Commande /start sans token — message d'accueil
if text == "/start" {
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID,
"👋 <b>Bienvenue !</b>\n\nPour lier votre compte, générez un token depuis l'application et envoyez <code>/start &lt;token&gt;</code>.")
}
}
c.Status(http.StatusOK)
}
func handleLinkAccount(c *gin.Context, token string, chatID int64) {
if token == "" {
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID, "❌ Token manquant. Générez un nouveau token depuis l'application.")
}
c.Status(http.StatusOK)
return
}
database := c.MustGet("database").(*db.Database)
username, role, err := db.ValidateAndConsumeLinkToken(token)
if err != nil {
log.Printf("⚠️ [TELEGRAM_LINK] Token invalide: %v", err)
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID, "❌ Token invalide ou expiré. Générez un nouveau token depuis l'application.")
}
c.Status(http.StatusOK)
return
}
// Enregistrer le chat_id selon le rôle
var saveErr error
switch role {
case "client":
saveErr = database.SaveClientTelegramChatID(username, chatID)
default:
saveErr = database.SaveUserTelegramChatID(username, chatID)
}
if saveErr != nil {
log.Printf("❌ [TELEGRAM_LINK] Erreur sauvegarde chat_id pour %s (%s): %v", username, role, saveErr)
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID, "❌ Une erreur est survenue. Réessayez.")
}
c.Status(http.StatusOK)
return
}
log.Printf("✅ [TELEGRAM_LINK] Compte %s (%s) lié au chat_id %d", username, role, chatID)
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID,
"✅ <b>Compte lié avec succès !</b>\n\nVous recevrez désormais vos notifications ici.")
}
c.Status(http.StatusOK)
}
func GenerateClientLinkToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"})
return
}
token, err := db.GenerateLinkToken(username, "client")
if err != nil {
log.Printf("❌ [TELEGRAM] Erreur génération token pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
"message": "/start " + token,
"expires_in": 600,
})
}
func GenerateLivreurLinkToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"})
return
}
token, err := db.GenerateLinkToken(username, "livreur")
if err != nil {
log.Printf("❌ [TELEGRAM] Erreur génération token pour livreur %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
"message": "/start " + token,
"expires_in": 600,
})
}
func GenerateAdminLinkToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"})
return
}
// Récupérer le rôle réel depuis la DB
database := c.MustGet("database").(*db.Database)
user, err := database.GetUserByUsername(username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Utilisateur introuvable"})
return
}
token, err := db.GenerateLinkToken(username, user.Role)
if err != nil {
log.Printf("❌ [TELEGRAM] Erreur génération token pour admin %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
"message": "/start " + token,
"expires_in": 600,
})
}
// GET /api/v1/telegram/status
func GetClientTelegramStatus(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
_, linked, err := database.GetClientTelegramChatID(username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur vérification"})
return
}
c.JSON(http.StatusOK, gin.H{
"linked": linked,
"enabled": services.TelegramBot != nil && services.TelegramBot.IsConfigured(),
})
}
// GET /api/v1/livreur/telegram/status
func GetLivreurTelegramStatus(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
_, linked, err := database.GetUserTelegramChatID(username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur vérification"})
return
}
c.JSON(http.StatusOK, gin.H{
"linked": linked,
"enabled": services.TelegramBot != nil && services.TelegramBot.IsConfigured(),
})
}
// ============================================
// DÉLIAISON TELEGRAM
// ============================================
// DELETE /api/v1/telegram/unlink
func UnlinkClientTelegram(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteClientTelegramChatID(username); err != nil {
log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur déliaison"})
return
}
log.Printf("✅ [TELEGRAM_UNLINK] Compte client %s délié", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// DELETE /api/v1/livreur/telegram/unlink
func UnlinkLivreurTelegram(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteUserTelegramChatID(username); err != nil {
log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur déliaison"})
return
}
log.Printf("✅ [TELEGRAM_UNLINK] Compte livreur %s délié", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// DELETE /api/v2/admin/protected/telegram/unlink
func UnlinkAdminTelegram(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteUserTelegramChatID(username); err != nil {
log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur déliaison"})
return
}
log.Printf("✅ [TELEGRAM_UNLINK] Compte admin %s délié", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
+17 -35
View File
@@ -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,
}
}
@@ -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{
+2 -2
View File
@@ -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}
+30 -68
View File
@@ -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("")
}
@@ -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
+54
View File
@@ -0,0 +1,54 @@
package models
import "github.com/golang-jwt/jwt/v5"
type ClientClaims struct {
ClientID int `json:"client_id"`
Username string `json:"username"`
Role string `json:"role"`
SessionID string `json:"session_id"`
jwt.RegisteredClaims
}
type AdminClaims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
SessionID string `json:"session_id"`
jwt.RegisteredClaims
}
// ============================================
// STRUCTURES REQUÊTE / RÉPONSE
// ============================================
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type RegisterClientRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Password string `json:"password" binding:"required,min=8"`
Nom string `json:"nom" binding:"required,min=2,max=100"`
Prenom string `json:"prenom" binding:"required,min=2,max=100"`
Telephone string `json:"telephone" binding:"required"`
}
type RegisterAdminRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Password string `json:"password" binding:"required,min=8"`
Role string `json:"role" binding:"required,oneof=admin cabine livreur"`
}
type LoginResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
User any `json:"user"`
}
type ProfileResponse struct {
Username string `json:"username"`
Role string `json:"role"`
}
+2 -4
View File
@@ -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"`
+80
View File
@@ -0,0 +1,80 @@
package models
type PostalZone struct {
Name string `json:"name"`
MinAmount float64 `json:"min_amount"`
Codes []string `json:"codes"`
}
// PointsTier représente un palier du barème de points
// Si Max == 0, il n'y a pas de borne supérieure (illimité)
type PointsTier struct {
Min float64 `json:"min"`
Max float64 `json:"max"` // 0 = illimité
Points int `json:"points"`
}
// DaySchedule représente les horaires de livraison pour un jour de la semaine
type DaySchedule struct {
Enabled bool `json:"enabled"`
OpenTime string `json:"open_time"` // ex: "09:00"
CloseTime string `json:"close_time"` // ex: "20:00"
}
// DeliverySchedule représente les horaires de livraison pour chaque jour
type DeliverySchedule struct {
Monday DaySchedule `json:"monday"`
Tuesday DaySchedule `json:"tuesday"`
Wednesday DaySchedule `json:"wednesday"`
Thursday DaySchedule `json:"thursday"`
Friday DaySchedule `json:"friday"`
Saturday DaySchedule `json:"saturday"`
Sunday DaySchedule `json:"sunday"`
}
type PointsPool struct {
Key string `json:"key"` // identifiant interne (ex: "pool_0")
Name string `json:"name"` // nom affiché (ex: "Cannabis", "Accessoires")
Categories []string `json:"categories"` // catégories de produits assignées à ce pool
Tiers []PointsTier `json:"tiers"` // barème de points
}
// PenaltyTier représente un palier d'amende : à partir de MinCancel annulations, Amount est appliqué
type PenaltyTier struct {
MinCancel int `json:"min_cancel"` // nombre d'annulations à partir duquel ce palier s'applique
Amount int `json:"amount"` // montant de l'amende
}
// CategoryRoute associe un livreur à une ou plusieurs catégories de produits
type CategoryRoute struct {
DeliverymanUsername string `json:"deliveryman_username"`
Categories []string `json:"categories"`
}
// DeliveryModeConfig configure le mode d'assignation des livreurs
// Mode "single" → un seul livreur, toutes catégories confondues
// Mode "category_based" → chaque livreur gère ses catégories dédiées
type DeliveryModeConfig struct {
Mode string `json:"mode"` // "single" | "category_based"
CategoryRoutes []CategoryRoute `json:"category_routes"` // utilisé uniquement en mode category_based
}
// AppSettings contient les paramètres globaux de l'application
type AppSettings struct {
PenaltiesEnabled bool `json:"penalties_enabled"`
ShowAmendeScore bool `json:"show_amende_score"` // afficher le score d'amendes aux clients/cabine
PenaltyTiers []PenaltyTier `json:"penalty_tiers"` // barème des amendes (liste configurable)
PointsEnabled bool `json:"points_enabled"` // afficher/activer le système de points
PointsPools []PointsPool `json:"points_pools"` // types de points personnalisés
ReferralEnabled bool `json:"referral_enabled"` // activer/désactiver le système de parrainage
CryptoPaymentEnabled bool `json:"crypto_payment_enabled"` // activer/désactiver le paiement crypto
CryptoOnly bool `json:"crypto_only"` // forcer le paiement crypto uniquement (pas d'espèces)
NowPaymentsAPIKey string `json:"nowpayments_api_key"` // clé API NowPayments
NowPaymentsIPNSecret string `json:"nowpayments_ipn_secret"` // secret IPN NowPayments
NowPaymentsCurrencies []string `json:"nowpayments_currencies"` // cryptos acceptées (ex: ["btc","eth","ltc"])
DeliverySchedule DeliverySchedule `json:"delivery_schedule"` // horaires de livraison par jour
PostalZones []PostalZone `json:"postal_zones"` // zones de livraison avec minimum de commande
TelegramBotToken string `json:"telegram_bot_token"` // token du bot Telegram (BotFather)
TelegramBotUsername string `json:"telegram_bot_username"` // username du bot (sans @)
DeliveryMode DeliveryModeConfig `json:"delivery_mode"` // mode d'assignation des livreurs
}
+21
View File
@@ -0,0 +1,21 @@
package models
type TgUpdate struct {
UpdateID int `json:"update_id"`
Message *TgMessage `json:"message"`
}
type TgMessage struct {
MessageID int `json:"message_id"`
Chat TgChat `json:"chat"`
Text string `json:"text"`
}
type TgChat struct {
ID int64 `json:"id"`
}
type TelegramLinkData struct {
Username string `json:"username"`
Role string `json:"role"` // "client" | "livreur" | "admin" | "cabine"
}
+2 -4
View File
@@ -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"`
}
+23 -13
View File
@@ -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)
}
}
+129
View File
@@ -0,0 +1,129 @@
package services
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"time"
)
// TelegramBot est l'instance globale accessible depuis le package db
var TelegramBot *TelegramService
type TelegramService struct {
botToken string
webhookSecret string
BotUsername string
}
func NewTelegramService() *TelegramService {
svc := &TelegramService{
botToken: os.Getenv("TELEGRAM_BOT_TOKEN"),
webhookSecret: os.Getenv("TELEGRAM_WEBHOOK_SECRET"),
BotUsername: os.Getenv("TELEGRAM_BOT_USERNAME"),
}
TelegramBot = svc
return svc
}
func (t *TelegramService) IsConfigured() bool {
return t.botToken != ""
}
// Reload met à jour le token et le username (appelé après UpdateSettings)
func (t *TelegramService) Reload(token, username string) {
if token != "" {
t.botToken = token
}
if username != "" {
t.BotUsername = username
}
}
func (t *TelegramService) ValidateWebhookSecret(header string) bool {
if t.webhookSecret == "" {
return true // pas de secret configuré = accepté
}
return header == t.webhookSecret
}
// SendMessage envoie un message texte (HTML) à un chat Telegram
func (t *TelegramService) SendMessage(chatID int64, text string) error {
if !t.IsConfigured() {
return fmt.Errorf("telegram non configuré")
}
payload := map[string]interface{}{
"chat_id": chatID,
"text": text,
"parse_mode": "HTML",
}
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", t.botToken)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return fmt.Errorf("création requête: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("envoi: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("telegram API status %d", resp.StatusCode)
}
return nil
}
// SetWebhook enregistre l'URL webhook auprès de Telegram
func (t *TelegramService) SetWebhook(webhookURL string) error {
if !t.IsConfigured() {
return fmt.Errorf("telegram non configuré")
}
payload := map[string]interface{}{
"url": webhookURL,
"allowed_updates": []string{"message"},
}
if t.webhookSecret != "" {
payload["secret_token"] = t.webhookSecret
}
body, err := json.Marshal(payload)
if err != nil {
return err
}
url := fmt.Sprintf("https://api.telegram.org/bot%s/setWebhook", t.botToken)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("setWebhook status %d", resp.StatusCode)
}
log.Printf("✅ [TELEGRAM] Webhook enregistré: %s", webhookURL)
return nil
}
+60
View File
@@ -0,0 +1,60 @@
package utils
import (
"crypto/rand"
"encoding/hex"
"regexp"
"strings"
"github.com/gin-gonic/gin"
)
func GenerateSessionID() string {
bytes := make([]byte, 16)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
func ValidatePhoneNumber(phone string) bool {
clean := regexp.MustCompile(`[\s\-\(\)]`).ReplaceAllString(phone, "")
validFormat := regexp.MustCompile(`^(\+33|0)[1-9]\d{8}$`)
return validFormat.MatchString(clean)
}
func NormalizePhoneNumber(phone string) string {
clean := regexp.MustCompile(`[\s\-\(\)]`).ReplaceAllString(phone, "")
if strings.HasPrefix(clean, "0") {
return "+33" + clean[1:]
}
return clean
}
func CheckRoleAdmin(c *gin.Context, role string) bool {
if role == "admin" {
return true
}
return false
}
func CheckRoleClient(c *gin.Context, role string) bool {
if role == "client" {
return true
}
return false
}
func CheckRoleCabine(c *gin.Context, role string) bool {
if role == "cabine" {
return true
}
return false
}
func CheckRoleLivreur(c *gin.Context, role string) bool {
if role == "livreur" {
return true
}
return false
}
+14
View File
@@ -0,0 +1,14 @@
package utils
import (
"gestion/db"
)
func CheckCommand(commandID int, database *db.Database) bool {
_, err := database.GetCommandByID(commandID)
if err != nil {
return false
}
return true
}
+44
View File
@@ -0,0 +1,44 @@
package utils
import (
"fmt"
"math"
)
func CalculateDistance(lat1, lon1, lat2, lon2 float64) float64 {
const earthRadiusKm = 6371
const metersPerKm = 1000
lat1Rad := DegreesToRadians(lat1)
lon1Rad := DegreesToRadians(lon1)
lat2Rad := DegreesToRadians(lat2)
lon2Rad := DegreesToRadians(lon2)
dLat := lat2Rad - lat1Rad
dLon := lon2Rad - lon1Rad
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(lat1Rad)*math.Cos(lat2Rad)*
math.Sin(dLon/2)*math.Sin(dLon/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return earthRadiusKm * c * metersPerKm
}
func DegreesToRadians(degrees float64) float64 {
return degrees * math.Pi / 180
}
func GetDeliveryStatusMessage(status string) string {
messages := map[string]string{
"assigned": "Commande assignée",
"en_route": "En route vers le client",
"arrived": "Arrivé à destination",
"livre": "Livraison effectuée",
"cancelled": "Livraison annulée",
}
if msg, ok := messages[status]; ok {
return msg
}
return fmt.Sprintf("Statut changé: %s", status)
}
+4 -9
View File
@@ -126,18 +126,13 @@ func tryAssignCommandWithPriority(
Longitude: location.Longitude,
}
// 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 {
+2 -1
View File
@@ -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