chore: refacto
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -17,17 +18,15 @@ import (
|
||||
// ANNULATION ATOMIQUE
|
||||
// ============================================
|
||||
|
||||
func (d *Database) CancelCommandAtomic(commandID int, username, reason string, force bool) (int, map[string]int, error) {
|
||||
func (d *Database) CancelCommandAtomic(commandID int, username, reason string, force bool) (int, error) {
|
||||
log.Printf("🔒 [CancelAtomic] START - cmd=%d, user=%s, force=%v", commandID, username, force)
|
||||
|
||||
// ✅ TRANSACTION
|
||||
tx, err := d.Begin()
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("erreur transaction: %w", err)
|
||||
return 0, fmt.Errorf("erreur transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// ✅ SELECT FOR UPDATE - Verrouiller la ligne
|
||||
var currentStatus, cmdUsername, livreurAssign string
|
||||
err = tx.QueryRow(`
|
||||
SELECT status, username, COALESCE(livreur_assign, '')
|
||||
@@ -37,163 +36,101 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f
|
||||
`, commandID).Scan(¤tStatus, &cmdUsername, &livreurAssign)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, nil, fmt.Errorf("commande non trouvée")
|
||||
return 0, fmt.Errorf("commande non trouvée")
|
||||
}
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
log.Printf("📋 [CancelAtomic] Trouvée - status=%s, owner=%s, livreur=%s", currentStatus, cmdUsername, livreurAssign)
|
||||
|
||||
// ✅ VÉRIFIER PROPRIÉTÉ
|
||||
if cmdUsername != username {
|
||||
return 0, nil, fmt.Errorf("commande ne vous appartient pas")
|
||||
return 0, fmt.Errorf("commande ne vous appartient pas")
|
||||
}
|
||||
|
||||
// ✅ VÉRIFIER STATUT
|
||||
nonCancellableStatuses := []string{"livre", "approved", "cancelled", "disabled"}
|
||||
for _, s := range nonCancellableStatuses {
|
||||
if currentStatus == s {
|
||||
return 0, nil, fmt.Errorf("impossible d'annuler")
|
||||
}
|
||||
if slices.Contains(nonCancellableStatuses, currentStatus) {
|
||||
return 0, fmt.Errorf("impossible d'annuler")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ✅ FIX: DÉTECTION CORRECTE DE L'ANNULATION TARDIVE
|
||||
// ============================================
|
||||
// Une annulation est tardive UNIQUEMENT si :
|
||||
// 1. Un livreur est assigné
|
||||
// 2. Le statut est "en_route" (livreur parti) OU "arrived" (livreur arrivé)
|
||||
// 3. OU une ETA a été définie (ce qui signifie que le livreur est en route)
|
||||
|
||||
isLateCancel := false
|
||||
|
||||
if livreurAssign != "" {
|
||||
// Cas 1: Statut en_route ou arrived = toujours tardif
|
||||
if currentStatus == "en_route" || currentStatus == "arrived" {
|
||||
isLateCancel = true
|
||||
log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - Statut: %s", currentStatus)
|
||||
} else if d.CheckCommandETAExistsAndValid(commandID) {
|
||||
isLateCancel = true
|
||||
log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - ETA définie")
|
||||
} else {
|
||||
// Cas 2: Pour les autres statuts, vérifier si une ETA existe
|
||||
hasRealETA := d.CheckCommandETAExistsAndValid(commandID)
|
||||
if hasRealETA {
|
||||
isLateCancel = true
|
||||
log.Printf("⚠️ [CancelAtomic] Annulation TARDIVE détectée - ETA définie")
|
||||
} else {
|
||||
log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Statut: %s, Pas d'ETA valide", currentStatus)
|
||||
}
|
||||
log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Statut: %s, Pas d'ETA valide", currentStatus)
|
||||
}
|
||||
} else {
|
||||
log.Printf("✅ [CancelAtomic] Annulation SANS PÉNALITÉ - Aucun livreur assigné")
|
||||
}
|
||||
|
||||
// ✅ SI ANNULATION TARDIVE SANS CONFIRMATION
|
||||
if isLateCancel && !force {
|
||||
return 0, nil, fmt.Errorf("confirmation requise")
|
||||
return 0, fmt.Errorf("confirmation requise")
|
||||
}
|
||||
|
||||
// ✅ UPDATE STATUT (avec vérification pour éviter race condition)
|
||||
result, err := tx.Exec(`
|
||||
UPDATE commandes
|
||||
SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1 AND status = $2 AND username = $3
|
||||
`, commandID, currentStatus, username)
|
||||
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return 0, nil, fmt.Errorf("commande déjà modifiée")
|
||||
if rows, _ := result.RowsAffected(); rows == 0 {
|
||||
return 0, fmt.Errorf("commande déjà modifiée")
|
||||
}
|
||||
|
||||
log.Printf("✅ [CancelAtomic] Statut mis à jour: %s → cancelled", currentStatus)
|
||||
|
||||
// ✅ REMBOURSER LE STOCK ATOMIQUEMENT
|
||||
_, err = tx.Exec(`
|
||||
if _, err = tx.Exec(`
|
||||
UPDATE products p
|
||||
SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP
|
||||
FROM command_items ci
|
||||
WHERE ci.command_id = $1 AND ci.product_id = p.id
|
||||
`, commandID)
|
||||
|
||||
if err != nil {
|
||||
`, commandID); err != nil {
|
||||
log.Printf("⚠️ [CancelAtomic] Erreur remboursement stock: %v", err)
|
||||
} else {
|
||||
log.Printf("✅ [CancelAtomic] Stock remboursé")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// APPLIQUER PÉNALITÉ SI ANNULATION TARDIVE
|
||||
// ============================================
|
||||
penalty := 0
|
||||
pointsLost := map[string]int{"weed": 0, "zipette": 0}
|
||||
|
||||
if isLateCancel && force {
|
||||
if isLateCancel {
|
||||
log.Printf("⚠️ [CancelAtomic] Annulation tardive confirmée - Application pénalité")
|
||||
|
||||
// ✅ RÉCUPÉRER LES POINTS ACTUELS
|
||||
var currentPointsWeed, currentPointsZipette int
|
||||
err := tx.QueryRow(`
|
||||
SELECT point, point_zipette FROM clients WHERE username = $1
|
||||
`, username).Scan(¤tPointsWeed, ¤tPointsZipette)
|
||||
|
||||
if err == nil {
|
||||
pointsLost["weed"] = currentPointsWeed
|
||||
pointsLost["zipette"] = currentPointsZipette
|
||||
|
||||
// ✅ CALCULER PÉNALITÉ
|
||||
penalty, _ = d.CalculateCancellationPenalty(username)
|
||||
|
||||
// ✅ APPLIQUER: Remettre points à 0 + Ajouter pénalité + Incrémenter compteur
|
||||
_, err = tx.Exec(`
|
||||
UPDATE clients
|
||||
SET point = 0,
|
||||
point_zipette = 0,
|
||||
amende = amende + $1,
|
||||
cancellations_count = COALESCE(cancellations_count, 0) + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = $2
|
||||
`, penalty, username)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("❌ [CancelAtomic] Erreur pénalité: %v", err)
|
||||
} else {
|
||||
log.Printf("⚠️ [CancelAtomic] Pénalité: %d pts, Points perdus: weed=%d, zipette=%d",
|
||||
penalty, pointsLost["weed"], pointsLost["zipette"])
|
||||
}
|
||||
penalty, _ = d.CalculateCancellationPenalty(username)
|
||||
if _, err = tx.Exec(`
|
||||
UPDATE clients
|
||||
SET amende = amende + $1,
|
||||
cancellations_count = COALESCE(cancellations_count, 0) + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = $2
|
||||
`, penalty, username); err != nil {
|
||||
log.Printf("❌ [CancelAtomic] Erreur pénalité: %v", err)
|
||||
} else {
|
||||
log.Printf("⚠️ [CancelAtomic] Pénalité: %d appliquée à %s", penalty, username)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ LOG
|
||||
_, err = tx.Exec(`
|
||||
if _, err = tx.Exec(`
|
||||
INSERT INTO command_logs (command_id, status, message, author, created_at)
|
||||
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)
|
||||
`, commandID, "cancelled",
|
||||
fmt.Sprintf("Annulée par %s - Raison: %s", username, reason),
|
||||
username)
|
||||
|
||||
if err != nil {
|
||||
VALUES ($1, 'cancelled', $2, $3, CURRENT_TIMESTAMP)
|
||||
`, commandID, fmt.Sprintf("Annulée par %s - Raison: %s", username, reason), username); err != nil {
|
||||
log.Printf("⚠️ [CancelAtomic] Erreur log: %v", err)
|
||||
}
|
||||
|
||||
// ✅ COMMIT
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, nil, fmt.Errorf("erreur commit: %w", err)
|
||||
return 0, fmt.Errorf("erreur commit: %w", err)
|
||||
}
|
||||
|
||||
// ✅ NETTOYER QUEUE (async, après commit)
|
||||
if livreurAssign != "" {
|
||||
go func() {
|
||||
err := d.CleanupCompletedCommandFromQueue(commandID, livreurAssign)
|
||||
if err != nil {
|
||||
if err := d.CleanupCompletedCommandFromQueue(commandID, livreurAssign); err != nil {
|
||||
log.Printf("⚠️ [CancelAtomic] Erreur cleanup queue: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ✅ INVALIDER CACHES (async)
|
||||
go func() {
|
||||
Redis.Del(RedisCtx,
|
||||
fmt.Sprintf("command:%d", commandID),
|
||||
@@ -203,8 +140,7 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f
|
||||
}()
|
||||
|
||||
log.Printf("🎉 [CancelAtomic] SUCCÈS - Commande %d annulée", commandID)
|
||||
|
||||
return penalty, pointsLost, nil
|
||||
return penalty, nil
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetAvailableDeliveryPersons récupère tous les livreurs disponibles
|
||||
func (d *Database) GetAvailableDeliveryPersons() ([]map[string]interface{}, error) {
|
||||
func (d *Database) GetAvailableDeliveryPersons() ([]map[string]any, error) {
|
||||
query := `SELECT id, username, total, livraison
|
||||
FROM users
|
||||
WHERE role = 'livreur'
|
||||
@@ -20,7 +21,7 @@ func (d *Database) GetAvailableDeliveryPersons() ([]map[string]interface{}, erro
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var livreurs []map[string]interface{}
|
||||
var livreurs []map[string]any
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var username string
|
||||
@@ -31,7 +32,7 @@ func (d *Database) GetAvailableDeliveryPersons() ([]map[string]interface{}, erro
|
||||
return nil, fmt.Errorf("erreur lors du scan du livreur: %w", err)
|
||||
}
|
||||
|
||||
livreur := map[string]interface{}{
|
||||
livreur := map[string]any{
|
||||
"id": id,
|
||||
"username": username,
|
||||
"total": total,
|
||||
@@ -47,17 +48,13 @@ func (d *Database) GetAvailableDeliveryPersons() ([]map[string]interface{}, erro
|
||||
func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) error {
|
||||
log.Printf("📦 [AssignDeliveryPerson] START - commandID=%d, livreur=%s", commandID, livreurUsername)
|
||||
|
||||
// ✅ DÉMARRER UNE TRANSACTION
|
||||
tx, err := d.Begin()
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur démarrage transaction: %v", err)
|
||||
return fmt.Errorf("erreur démarrage transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback() // Rollback automatique si non commité
|
||||
defer tx.Rollback()
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 1: Vérifier le livreur (DANS la transaction)
|
||||
// ============================================
|
||||
var role string
|
||||
checkQuery := `SELECT role FROM users WHERE username = $1 FOR UPDATE`
|
||||
err = tx.QueryRow(checkQuery, livreurUsername).Scan(&role)
|
||||
@@ -74,18 +71,12 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e
|
||||
return fmt.Errorf("l'utilisateur n'est pas un livreur")
|
||||
}
|
||||
|
||||
log.Printf(" ✅ Livreur valide: %s", livreurUsername)
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 2: Vérifier et VERROUILLER la commande
|
||||
// ✅ FOR UPDATE empêche les modifications concurrentes
|
||||
// ============================================
|
||||
var currentStatus string
|
||||
var currentLivreur sql.NullString
|
||||
statusQuery := `SELECT status, livreur_assign
|
||||
FROM commandes
|
||||
WHERE id = $1
|
||||
FOR UPDATE` // ⚠️ VERROUILLAGE CRITIQUE
|
||||
FOR UPDATE`
|
||||
|
||||
err = tx.QueryRow(statusQuery, commandID).Scan(¤tStatus, ¤tLivreur)
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -97,33 +88,14 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e
|
||||
return fmt.Errorf("erreur lors de la vérification de la commande: %w", err)
|
||||
}
|
||||
|
||||
log.Printf(" ✅ Commande trouvée: status=%s, livreur_assign=%s",
|
||||
currentStatus, currentLivreur.String)
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 3: Vérifier que la commande est assignable
|
||||
// ============================================
|
||||
|
||||
// ✅ Vérifier le statut (pending ou assigned pour permettre la réassignation)
|
||||
validStatusesForAssignment := []string{"pending", "assigned"}
|
||||
isValidStatus := false
|
||||
for _, vs := range validStatusesForAssignment {
|
||||
if currentStatus == vs {
|
||||
isValidStatus = true
|
||||
break
|
||||
}
|
||||
}
|
||||
isValidStatus := slices.Contains(validStatusesForAssignment, currentStatus)
|
||||
|
||||
if !isValidStatus {
|
||||
log.Printf("❌ Statut invalide pour assignation: %s", currentStatus)
|
||||
return fmt.Errorf("commande en statut '%s', impossible d'assigner un livreur", currentStatus)
|
||||
}
|
||||
|
||||
log.Printf(" ✅ Statut valide pour assignation: %s", currentStatus)
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 4: Assigner le livreur (ATOMIQUE)
|
||||
// ============================================
|
||||
updateQuery := `UPDATE commandes
|
||||
SET livreur_assign = $1,
|
||||
status = 'assigned',
|
||||
@@ -147,11 +119,6 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e
|
||||
return fmt.Errorf("impossible d'assigner la commande (déjà assignée ou statut changé)")
|
||||
}
|
||||
|
||||
log.Printf(" ✅ Commande assignée au livreur: %s", livreurUsername)
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 5: Ajouter un log (DANS la transaction)
|
||||
// ============================================
|
||||
logQuery := `INSERT INTO command_logs (command_id, status, message, author, created_at)
|
||||
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)`
|
||||
|
||||
@@ -163,28 +130,22 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e
|
||||
// Non bloquant
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ✅ COMMIT de la transaction
|
||||
// ============================================
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur COMMIT: %v", err)
|
||||
return fmt.Errorf("erreur commit transaction: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("🎉 [AssignDeliveryPerson] SUCCÈS - Commande %d assignée à %s", commandID, livreurUsername)
|
||||
log.Printf(" Workflow: pending → ✅ assigned (TRANSACTION COMMITTED)")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDeliveryPersonCommands récupère les commandes assignées à un livreur
|
||||
func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status string) ([]map[string]interface{}, error) {
|
||||
func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status string) ([]map[string]any, error) {
|
||||
query := `SELECT id, username, status, adresse, total_prix, livreur_assign, created_at, updated_at
|
||||
FROM commandes
|
||||
WHERE livreur_assign = $1`
|
||||
|
||||
args := []interface{}{livreurUsername}
|
||||
args := []any{livreurUsername}
|
||||
|
||||
if status != "" {
|
||||
query += " AND status = $2"
|
||||
@@ -199,7 +160,7 @@ func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status stri
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var commands []map[string]interface{}
|
||||
var commands []map[string]any
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var username, status, adresse string
|
||||
@@ -212,7 +173,7 @@ func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status stri
|
||||
return nil, fmt.Errorf("erreur lors du scan: %w", err)
|
||||
}
|
||||
|
||||
command := map[string]interface{}{
|
||||
command := map[string]any{
|
||||
"id": id,
|
||||
"username": username,
|
||||
"status": status,
|
||||
@@ -228,7 +189,6 @@ func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status stri
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
// ✅ NOUVELLE MÉTHODE: IncrementLivreurDeliveryCount incrémente le compteur de livraisons d'un livreur
|
||||
func (d *Database) IncrementLivreurDeliveryCount(livreurUsername string) error {
|
||||
query := `UPDATE users
|
||||
SET livraison = livraison + 1,
|
||||
@@ -249,14 +209,10 @@ func (d *Database) IncrementLivreurDeliveryCount(livreurUsername string) error {
|
||||
return fmt.Errorf("livreur non trouvé")
|
||||
}
|
||||
|
||||
log.Printf("✅ Livraison incrémentée pour le livreur: %s", livreurUsername)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
|
||||
log.Printf("📝 [ApproveDelivery] START - commandID=%d, client=%s", commandID, clientUsername)
|
||||
|
||||
// ✅ DÉMARRER UNE TRANSACTION
|
||||
tx, err := d.Begin()
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur démarrage transaction: %v", err)
|
||||
@@ -264,16 +220,13 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 1: Vérifier et VERROUILLER la commande
|
||||
// ============================================
|
||||
var commandUsername, currentStatus string
|
||||
var livreurAssign sql.NullString
|
||||
|
||||
checkQuery := `SELECT username, status, livreur_assign
|
||||
FROM commandes
|
||||
WHERE id = $1
|
||||
FOR UPDATE` // ⚠️ VERROUILLAGE CRITIQUE
|
||||
FOR UPDATE`
|
||||
|
||||
err = tx.QueryRow(checkQuery, commandID).Scan(&commandUsername, ¤tStatus, &livreurAssign)
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -283,25 +236,14 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
|
||||
return fmt.Errorf("erreur lors de la vérification de la commande: %w", err)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 2: Validations métier
|
||||
// ============================================
|
||||
|
||||
// ✅ Vérifier que c'est bien la commande du client
|
||||
if commandUsername != clientUsername {
|
||||
return fmt.Errorf("cette commande ne vous appartient pas")
|
||||
}
|
||||
|
||||
// ✅ Vérifier le statut
|
||||
if currentStatus != "livre" {
|
||||
return fmt.Errorf("cette commande n'est pas encore livrée (statut actuel: %s)", currentStatus)
|
||||
}
|
||||
|
||||
log.Printf(" ✅ Validations OK: client=%s, status=%s", commandUsername, currentStatus)
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 3: Mettre à jour le statut (ATOMIQUE)
|
||||
// ============================================
|
||||
updateQuery := `UPDATE commandes
|
||||
SET status = 'approved',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
@@ -322,11 +264,6 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
|
||||
return fmt.Errorf("impossible d'approuver: statut changé ou commande introuvable")
|
||||
}
|
||||
|
||||
log.Printf(" ✅ Statut mis à jour: livre → approved")
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 4: Incrémenter le compteur du livreur (ATOMIQUE)
|
||||
// ============================================
|
||||
if livreurAssign.Valid && livreurAssign.String != "" {
|
||||
incrementQuery := `UPDATE users
|
||||
SET livraison = livraison + 1,
|
||||
@@ -337,7 +274,6 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
|
||||
result, err := tx.Exec(incrementQuery, livreurAssign.String)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Erreur incrémentation livreur: %v", err)
|
||||
// Non bloquant mais on continue dans la transaction
|
||||
} else {
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows > 0 {
|
||||
@@ -346,9 +282,6 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 5: Ajouter un log (DANS la transaction)
|
||||
// ============================================
|
||||
logQuery := `INSERT INTO command_logs (command_id, status, message, author, created_at)
|
||||
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)`
|
||||
|
||||
@@ -357,20 +290,13 @@ func (d *Database) ApproveDelivery(commandID int, clientUsername string) error {
|
||||
clientUsername)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Erreur ajout log: %v", err)
|
||||
// Non bloquant
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ✅ COMMIT de la transaction
|
||||
// ============================================
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur COMMIT: %v", err)
|
||||
return fmt.Errorf("erreur commit transaction: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("🎉 [ApproveDelivery] SUCCÈS - Commande %d approuvée par %s", commandID, clientUsername)
|
||||
log.Printf(" Workflow: livre → ✅ approved (TRANSACTION COMMITTED)")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);`,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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'")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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 <token></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})
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user