chore: build
Backend - Build & Lint / build (push) Failing after 16m17s

This commit is contained in:
Nuxgrid
2026-07-08 21:54:00 +02:00
parent 5cd8ada828
commit 12a7facc45
19 changed files with 93 additions and 611 deletions
+9 -2
View File
@@ -3101,6 +3101,13 @@ Le système tente d'abord toutes les clés disponibles en rotation, puis bascule
## 📋 Changelog
### v5.5.0 — 2026-07-08
- **Fix — amende annulation livreur (`ApplyCancellationPenalty`)** : l'amende appliquée quand un livreur marque le client absent écrasait le montant existant au lieu de l'additionner, et n'était pas protégée par un verrou (`FOR UPDATE`). Elle est désormais cumulative et transactionnelle, cohérente avec le chemin d'annulation client (`CancelAtomic`).
- **Fix — mot de passe loggé en clair** : la modification d'un client par un admin (`PUT /admin/protected/clients/:id`) journalisait le corps de requête complet, y compris le nouveau mot de passe. Le log ne contient plus de donnée sensible.
- **Fix — IDOR consultation d'alerte police** : un livreur pouvait consulter le détail de l'alerte d'un autre livreur en devinant l'ID (`GET /api/v1/livreur/alert/:id`). L'accès est désormais restreint à ses propres alertes ; admin et cabine conservent l'accès complet.
- **Nettoyage — code mort** : suppression des handlers et fonctions utilitaires non routés/non appelés (ancien module `handlers/cabine.go`, `RegisterClient`, `GetCurrentClient`, `GetCurrentAdmin`, `GetMyCompletedOrders`, `GetRealtimeStats`, `StartPaymentChecker`, et helpers internes associés), identifiés via `staticcheck` et `deadcode`.
### v5.4.0 — 2026-05-18
- **Rotation automatique des clés TomTom** : jusqu'à 3 clés configurables (`TOMTOM_API_KEY_1/2/3`). En cas de quota dépassé (403/429), le système passe à la clé suivante automatiquement sans interruption. Fallback Haversine si toutes les clés sont épuisées.
@@ -3127,8 +3134,8 @@ Le système tente d'abord toutes les clés disponibles en rotation, puis bascule
---
**Documentation mise à jour le :** 2026-06-11
**Version API :** 5.4.0
**Documentation mise à jour le :** 2026-07-08
**Version API :** 5.5.0
**Technologies :** Go 1.24, Gin, PostgreSQL 16, Redis 7, React 19, Expo 54, TomTom API, ModSecurity WAF
**Déploiement :** Docker Compose · Nginx + ModSecurity OWASP CRS · TLS 1.2/1.3
**Base URL prod :** `https://mln-uber.club`
+1 -1
View File
@@ -15,7 +15,7 @@ func (d *Database) CheckAddress(addressByUser *models.Command) error {
return fmt.Errorf("checkAddress: %w", result.Error)
}
addressByUser.DeliveryAddress = correction.CorrectAddress
return fmt.Errorf("Adresse invalide %s", correction.CorrectAddress)
return fmt.Errorf("adresse invalide %s", correction.CorrectAddress)
}
func (d *Database) AddAddress(CorrectAddressByAdmin string, InvalidAddressByAdmin string) error {
-12
View File
@@ -57,18 +57,6 @@ type basketItem struct {
RewardPoolKey string `gorm:"column:reward_pool_key"`
}
func (d *Database) fetchBasketItems(username string) ([]basketItem, float64, error) {
var items []basketItem
if err := d.GDB.Table("baskets").Select("product_id, quantity, price, is_reward, reward_pool_key").Where("username = ?", username).Scan(&items).Error; err != nil {
return nil, 0, fmt.Errorf("erreur récupération panier: %w", err)
}
total := 0.0
for _, item := range items {
total += item.Price
}
return items, total, nil
}
// validateCommandStatus vérifie si le statut est valide
func validateCommandStatus(status string) error {
validStatuses := map[string]bool{
+48 -45
View File
@@ -27,25 +27,6 @@ func (d *Database) GetClientCancellationsCount(username string) (int, error) {
return result.Count, nil
}
// IncrementClientCancellationsCount incrémente le compteur d'annulations
func (d *Database) IncrementClientCancellationsCount(username string) error {
result := d.GDB.Model(&models.Client{}).Where("username = ?", username).Updates(map[string]any{
"cancellations_count": gorm.Expr("COALESCE(cancellations_count, 0) + 1"),
})
if result.Error != nil {
log.Printf("❌ [IncrementCancellations] Erreur: %v", result.Error)
return fmt.Errorf("erreur incrémentation: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("client non trouvé")
}
cacheKey := fmt.Sprintf("client:%s", username)
Redis.Del(RedisCtx, cacheKey)
return nil
}
// 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 {
@@ -64,6 +45,16 @@ func penaltyForCount(count int, tiers []models.PenaltyTier) int {
return sorted[len(sorted)-1].Amount
}
// penaltyTiers charge le barème de pénalités configuré, avec repli sur le barème par défaut si les settings sont indisponibles
func (d *Database) penaltyTiers(logCtx string) []models.PenaltyTier {
settings, err := d.GetSettings()
if err != nil {
log.Printf("⚠️ [%s] Impossible de charger les settings, barème par défaut: %v", logCtx, err)
settings = DefaultSettings()
}
return settings.PenaltyTiers
}
// 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)
@@ -71,13 +62,7 @@ func (d *Database) CalculateCancellationPenalty(username string) (int, error) {
return 0, err
}
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)
penalty := penaltyForCount(count, d.penaltyTiers("CalculatePenalty"))
log.Printf("💰 [CalculatePenalty] Client %s - Annulations: %d → Pénalité: %d points",
username, count, penalty)
@@ -85,30 +70,48 @@ func (d *Database) CalculateCancellationPenalty(username string) (int, error) {
return penalty, nil
}
// ApplyCancellationPenalty applique une pénalité et incrémente le compteur d'annulations
// ApplyCancellationPenalty applique une pénalité (cumulative) et incrémente le compteur d'annulations.
// Verrouillée via FOR UPDATE pour éviter qu'un appel concurrent (même client, deux livraisons en parallèle)
// calcule la pénalité sur un compteur pas encore à jour, et l'amende s'additionne au lieu d'écraser
// le solde existant (cohérent avec CancelCommandAtomic pour l'annulation côté client).
func (d *Database) ApplyCancellationPenalty(username string) (int, error) {
penalty, err := d.CalculateCancellationPenalty(username)
tiers := d.penaltyTiers("ApplyCancellationPenalty")
var penalty int
err := d.GDB.Transaction(func(tx *gorm.DB) error {
var count int
if err := tx.Raw(`
SELECT COALESCE(cancellations_count, 0) FROM clients
WHERE username = ? FOR UPDATE`, username).Scan(&count).Error; err != nil {
return fmt.Errorf("erreur récupération compteur: %w", err)
}
penalty = penaltyForCount(count, tiers)
log.Printf("⚠️ [ApplyCancellationPenalty] Client %s - Pénalité calculée: %d points", username, penalty)
result := tx.Exec(`
UPDATE clients
SET cancellations_count = COALESCE(cancellations_count, 0) + 1,
amende = amende + ?,
updated_at = CURRENT_TIMESTAMP
WHERE username = ?`, penalty, username)
if result.Error != nil {
log.Printf("❌ [ApplyCancellationPenalty] Erreur UPDATE: %v", result.Error)
return fmt.Errorf("erreur application pénalité: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("client non trouvé")
}
log.Printf("✅ [ApplyCancellationPenalty] Amende %d appliquée à %s", penalty, username)
return nil
})
if err != nil {
return 0, err
}
log.Printf("⚠️ [ApplyCancellationPenalty] Client %s - Pénalité calculée: %d points", username, penalty)
if err := d.IncrementClientCancellationsCount(username); err != nil {
return 0, err
}
result := d.GDB.Model(&models.Client{}).Where("username = ?", username).Update("amende", float64(penalty))
if result.Error != nil {
log.Printf("❌ [ApplyCancellationPenalty] Erreur UPDATE: %v", result.Error)
return 0, fmt.Errorf("erreur application pénalité: %w", result.Error)
}
if result.RowsAffected == 0 {
return 0, fmt.Errorf("client non trouvé")
}
log.Printf("✅ [ApplyCancellationPenalty] Amende %d appliquée à %s", penalty, username)
cacheKey := fmt.Sprintf("client:%s", username)
Redis.Del(RedisCtx, cacheKey)
+1 -1
View File
@@ -43,7 +43,7 @@ func GenerateLinkToken(username, role string) (string, error) {
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 "", fmt.Errorf("redis set: %w", err)
}
return token, nil
}
+9
View File
@@ -92,6 +92,15 @@ func GetAlert(c *gin.Context) {
return
}
// Un livreur ne peut consulter que ses propres alertes — admin/cabine gardent l'accès complet pour le dispatch
if userRole == "livreur" {
username, exists := c.Get("username")
if !exists || alert.Username != username.(string) {
c.JSON(http.StatusForbidden, gin.H{"error": "Cette alerte ne vous appartient pas"})
return
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"alert": alert,
-162
View File
@@ -73,112 +73,6 @@ func generateAdminToken(user *models.User) (string, error) {
return tokenString, nil
}
// RegisterClient crée un nouveau compte client
func RegisterClient(c *gin.Context) {
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{
"error": "Données invalides",
})
return
}
// Sanitize text inputs
req.Username = utils.StripHTML(req.Username)
req.Nom = utils.StripHTML(req.Nom)
req.Prenom = utils.StripHTML(req.Prenom)
// Validation téléphone
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",
})
return
}
normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
database := c.MustGet("database").(*db.Database)
// Vérifier username unique
if existingClient, _ := database.GetClientByUsername(req.Username); existingClient != nil {
log.Printf("❌ [REGISTER_CLIENT] Username déjà utilisé: %s", req.Username)
c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"})
return
}
// Vérifier téléphone unique
if existingClient, _ := database.GetClientByTelephone(normalizedPhone); existingClient != nil {
log.Printf("❌ [REGISTER_CLIENT] Téléphone déjà utilisé: %s", normalizedPhone)
c.JSON(http.StatusConflict, gin.H{"error": "Ce numéro de téléphone est déjà utilisé"})
return
}
// Hasher le mot de passe
hashed, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
log.Printf("❌ [REGISTER_CLIENT] Erreur bcrypt: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur traitement mot de passe"})
return
}
// Créer le client
client := &models.Client{
Username: req.Username,
Password: string(hashed),
Nom: strings.TrimSpace(req.Nom),
Prenom: strings.TrimSpace(req.Prenom),
Telephone: normalizedPhone,
CreatedAt: time.Now(),
}
if err := database.CreateClient(client); err != nil {
log.Printf("❌ [REGISTER_CLIENT] Erreur création: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création client"})
return
}
// Générer le token
token, err := generateClientToken(client)
if err != nil {
log.Printf("❌ [REGISTER_CLIENT] Erreur génération token: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
return
}
// Sauvegarder le token
expiresAt := time.Now().Add(clientTokenDuration)
if err := database.SaveToken(client.ID, "client", token, expiresAt); err != nil {
log.Printf("❌ [REGISTER_CLIENT] Erreur SaveToken: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement token"})
return
}
// Créer la session Redis
sessionID := uuid.New().String()
if err := database.CreateClientSession(client.ID, client.Username, sessionID); err != nil {
log.Printf("⚠️ [REGISTER_CLIENT] Erreur session Redis: %v", err)
}
client.Password = ""
c.JSON(http.StatusCreated, models.LoginResponse{
AccessToken: token,
TokenType: "Bearer",
ExpiresIn: int(clientTokenDuration.Seconds()),
User: gin.H{
"id": client.ID,
"username": client.Username,
"nom": client.Nom,
"prenom": client.Prenom,
"telephone": client.Telephone,
"role": "client",
"session_id": sessionID,
},
})
}
// AdminCreateClient crée un client depuis l'interface admin (sans session ni token)
func AdminCreateClient(c *gin.Context) {
if userRole := c.GetString("role"); userRole != "admin" {
@@ -602,62 +496,6 @@ func LogoutAdmin(c *gin.Context) {
// HELPERS
// ============================================
// GetCurrentClient récupère le client actuel
// GET /api/v1/profile/client
func GetCurrentClient(c *gin.Context) {
clientID := c.GetInt("client_id")
database := c.MustGet("database").(*db.Database)
client, err := database.GetClientByID(clientID)
if err != nil {
log.Printf("❌ [GET_CURRENT_CLIENT] Client non trouvé: ID=%d", clientID)
c.JSON(http.StatusNotFound, gin.H{"error": "Client non trouvé"})
return
}
client.Password = ""
log.Printf("✅ [GET_CURRENT_CLIENT] Client récupéré: %s", client.Username)
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,
"amende": client.Amende,
"points_extra": client.PointsExtra,
},
})
}
// GetCurrentAdmin récupère l'admin/user actuel
// GET /api/v1/profile/admin
func GetCurrentAdmin(c *gin.Context) {
userID := c.GetInt("user_id")
database := c.MustGet("database").(*db.Database)
user, err := database.GetUserByID(userID)
if err != nil {
log.Printf("❌ [GET_CURRENT_ADMIN] User non trouvé: ID=%d", userID)
c.JSON(http.StatusNotFound, gin.H{"error": "Utilisateur non trouvé"})
return
}
user.Password = ""
log.Printf("✅ [GET_CURRENT_ADMIN] User récupéré: %s", user.Username)
c.JSON(http.StatusOK, gin.H{
"user": models.ProfileResponse{
Username: user.Username,
Role: user.Role,
},
})
}
// GetAllUsers récupère tous les utilisateurs (Admin only)
func GetAllUsers(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
-165
View File
@@ -1,165 +0,0 @@
package handlers
import (
"gestion/db"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
func GetLivreurPosition(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
livreurUsername := c.Param("username")
userRole := c.GetString("role")
if userRole != "admin" && userRole != "cabine" {
c.JSON(http.StatusForbidden, gin.H{
"error": "Accès refusé - Réservé aux administrateurs et cabines",
})
return
}
if livreurUsername == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Username livreur requis"})
return
}
position, err := database.GetLivreurPosition(livreurUsername)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"livreur": livreurUsername,
"message": "Position non disponible (GPS désactivé ou livraison terminée)",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"livreur": livreurUsername,
"position": position,
})
}
func GetDeliveryIssues(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
status := c.Query("status")
userRole := c.GetString("role")
if userRole != "admin" && userRole != "cabine" {
c.JSON(http.StatusForbidden, gin.H{
"error": "Accès refusé - Réservé aux administrateurs et cabines",
})
return
}
issues, err := database.GetDeliveryIssues(status)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération problèmes",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"issues": issues,
"count": len(issues),
})
}
func CreateDeliveryIssue(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
var req struct {
CommandID int `json:"command_id" binding:"required"`
IssueType string `json:"issue_type" binding:"required"`
Description string `json:"description" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
return
}
cabineUsername, _ := c.Get("username")
issue, err := database.CreateDeliveryIssue(
req.CommandID,
req.IssueType,
req.Description,
cabineUsername.(string),
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur création problème",
})
return
}
c.JSON(http.StatusCreated, gin.H{
"success": true,
"message": "Problème enregistré",
"issue": issue,
})
}
func UpdateDeliveryIssue(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
issueID, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"})
return
}
var req struct {
Status string `json:"status"`
Resolution string `json:"resolution"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
return
}
cabineUsername, _ := c.Get("username")
err = database.UpdateDeliveryIssue(issueID, req.Status, req.Resolution, cabineUsername.(string))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur mise à jour",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Problème mis à jour",
})
}
func GetCommandLogs(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
commandID, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID commande invalide"})
return
}
logs, err := database.GetCommandLogs(commandID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération logs",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"logs": logs,
"count": len(logs),
})
}
+1 -1
View File
@@ -209,7 +209,7 @@ func buildTimeline(logs []map[string]any) []gin.H {
for _, logEntry := range logs {
status, _ := logEntry["status"].(string)
message, _ := logEntry["message"].(string)
createdAt, _ := logEntry["created_at"]
createdAt := logEntry["created_at"]
timeline = append(timeline, gin.H{
"status": status,
+20 -4
View File
@@ -627,6 +627,20 @@ func GetMyDeliveryStats(c *gin.Context) {
ORDER BY year, month_num
`, usernameStr).Scan(&monthRows)
type TodayRow struct {
Count int `gorm:"column:count"`
Revenue float64 `gorm:"column:revenue"`
}
var todayRow TodayRow
gdb.Raw(`
SELECT COUNT(*) AS count,
COALESCE(SUM(total_prix - COALESCE(referral_used, 0)), 0) AS revenue
FROM commandes
WHERE livreur_assign = ?
AND status IN ('livre', 'approved')
AND DATE(updated_at) = CURRENT_DATE
`, usernameStr).Scan(&todayRow)
monthNames := [13]string{"", "Jan", "Fév", "Mar", "Avr", "Mai", "Jun", "Jul", "Aoû", "Sep", "Oct", "Nov", "Déc"}
byDay := make([]gin.H, len(dayRows))
@@ -661,9 +675,11 @@ func GetMyDeliveryStats(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"by_day": byDay,
"by_week": byWeek,
"by_month": byMonth,
"success": true,
"by_day": byDay,
"by_week": byWeek,
"by_month": byMonth,
"today_count": todayRow.Count,
"today_revenue": todayRow.Revenue,
})
}
-74
View File
@@ -10,80 +10,6 @@ import (
"github.com/gin-gonic/gin"
)
func GetMyCompletedOrders(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
// ✅ SÉCURITÉ: Récupérer depuis JWT validé
username, exists := c.Get("username")
if !exists {
log.Printf("❌ [HISTORY] Utilisateur non authentifié")
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Authentification requise",
})
return
}
usernameStr := username.(string)
log.Printf("📚 [HISTORY] Récupération historique pour: %s", usernameStr)
// ✅ Récupérer les commandes terminées (approved)
commands, err := database.GetCompletedCommandsByUsername(usernameStr)
if err != nil {
log.Printf("❌ [HISTORY] Erreur: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération de l'historique",
})
return
}
log.Printf("✅ [HISTORY] %d commandes terminées trouvées", len(commands))
// ✅ Récupérer les infos client pour statistiques
client, err := database.GetClientByUsername(usernameStr)
// ✅ Récupérer les noms et clés des pools de points
poolNames := []string{"Pool 1", "Pool 2"}
var poolKeys []string
if settings, sErr := database.GetSettings(); sErr == nil && len(settings.PointsPools) > 0 {
poolNames = make([]string, len(settings.PointsPools))
poolKeys = make([]string, len(settings.PointsPools))
for i, p := range settings.PointsPools {
poolNames[i] = p.Name
poolKeys[i] = p.Key
}
}
response := gin.H{
"success": true,
"commands": commands,
"count": len(commands),
}
if err == nil && client != nil {
// Construire le tableau depuis points_extra[poolKey] pour tous les pools (stockage dynamique)
poolPoints := make([]int, len(poolKeys))
for i, key := range poolKeys {
if key != "" {
poolPoints[i] = client.PointsExtra[key]
}
}
log.Printf("📊 [HISTORY] pool_names=%v pool_keys=%v pool_points=%v extra=%v",
poolNames, poolKeys, poolPoints, client.PointsExtra)
response["client_stats"] = gin.H{
"username": client.Username,
"total_commands": client.Command,
"points_extra": client.PointsExtra,
"pool_points": poolPoints,
"pool_names": poolNames,
"penalties": client.Amende,
}
}
c.JSON(http.StatusOK, response)
}
// GetMyCompletedOrdersWithItems récupère l'historique avec les détails des items
// GET /api/v1/my-commands/history/detailed
func GetMyCompletedOrdersWithItems(c *gin.Context) {
@@ -947,36 +947,6 @@ func SubtractClientPointsAdmin(c *gin.Context) {
})
}
func GetRealtimeStats(c *gin.Context) {
userRole := c.GetString("role")
if userRole != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
stats, err := db.Redis.HGetAll(db.RedisCtx, "stats:realtime").Result()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération des statistiques",
})
return
}
if len(stats) == 0 {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Aucune statistique disponible pour le moment",
"stats": map[string]interface{}{},
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"stats": stats,
})
}
func refreshETAForActivDelivery(username string, lat, lon float64) {
// 1. Récupérer le statut actuel du livreur
statusKey := fmt.Sprintf("delivery:status:%s", username)
-52
View File
@@ -1,52 +0,0 @@
// ============================================
// handlers/traffic_handlers.go - COMPLET
// ============================================
package handlers
import (
"encoding/json"
"strconv"
)
// getFloatFromMap récupère un float64 depuis une map avec différents types
func getFloatFromMap(m map[string]any, key string) (float64, bool) {
value, exists := m[key]
if !exists || value == nil {
return 0, false
}
switch v := value.(type) {
case float64:
return v, true
case float32:
return float64(v), true
case int:
return float64(v), true
case int64:
return float64(v), true
case int32:
return float64(v), true
case json.Number:
f, err := v.Float64()
if err != nil {
return 0, false
}
return f, true
case string:
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return 0, false
}
return f, true
case []byte:
s := string(v)
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, false
}
return f, true
default:
return 0, false
}
}
@@ -198,9 +198,6 @@ func UpdateClientByAdmin(c *gin.Context) {
return
}
// ✅ LOG DEBUG - Voir ce qui est reçu
log.Printf("📝 [UPDATE_CLIENT_ADMIN] Requête reçue: %+v", req)
// Récupérer le client actuel
client, err := database.GetClientByID(clientID)
if err != nil {
@@ -172,7 +172,7 @@ func (acs *AddressCorrectionService) queryNominatim(query string, limit int) ([]
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Nominatim status %d", resp.StatusCode)
return nil, fmt.Errorf("nominatim status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
+3 -19
View File
@@ -32,29 +32,13 @@ func NormalizePhoneNumber(phone string) string {
}
func CheckRoleAdmin(c *gin.Context, role string) bool {
if role == "admin" {
return true
}
return false
return role == "admin"
}
func CheckRoleClient(c *gin.Context, role string) bool {
if role == "client" {
return true
}
return false
return role == "client"
}
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
return role == "cabine"
}
-14
View File
@@ -1,14 +0,0 @@
package utils
import (
"gestion/db"
)
func CheckCommand(commandID int, database *db.Database) bool {
_, err := database.GetCommandByID(commandID)
if err != nil {
return false
}
return true
}
-15
View File
@@ -3,21 +3,10 @@ package utils
import (
"crypto/rand"
"fmt"
"gestion/db"
"gestion/models"
"math/big"
"os"
"path/filepath"
)
// --------------------------------------------
// MEDIA
// --------------------------------------------
func GetMediaForProduct(productID int) ([]models.Media, error) {
return db.DB.GetMediaByProductID(productID)
}
// --------------------------------------------
// FILES NAMES
// --------------------------------------------
@@ -28,7 +17,3 @@ func GenerateUniqueFileName(productName string, originalFileName string) string
ext := filepath.Ext(originalFileName)
return fmt.Sprintf("%s_%s%s", productName, randomString, ext)
}
func DeleteOldFile(filePath string) error {
return os.Remove(filePath)
}
@@ -7,16 +7,6 @@ import (
"time"
)
func StartPaymentChecker(database *db.Database, np *services.NowPaymentsClient, interval time.Duration) {
go func() {
for {
time.Sleep(interval)
checkPendingCryptoPayments(database, np)
}
}()
log.Printf("[CRON] payment checker démarré (toutes les %s)", interval)
}
// StartDynamicPaymentChecker démarre un checker qui recharge le client NowPayments à chaque tick
// (permet de prendre en compte les changements de clé API sans redémarrer)
func StartDynamicPaymentChecker(database *db.Database, clientFn func() *services.NowPaymentsClient, interval time.Duration) {