chore: refacto

This commit is contained in:
2026-03-27 22:23:46 +01:00
parent 75bf4caaa1
commit 5380abe8ed
67 changed files with 1751 additions and 2573 deletions
+32 -173
View File
@@ -1,14 +1,12 @@
package handlers
import (
"crypto/rand"
"encoding/hex"
"gestion/db"
"gestion/models"
"gestion/utils"
"log"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
@@ -19,109 +17,16 @@ import (
"golang.org/x/crypto/bcrypt"
)
// ============================================
// TYPES JWT CLAIMS (Duplicés dans middleware aussi)
// ============================================
type ClientClaims struct {
ClientID int `json:"client_id"`
Username string `json:"username"`
Role string `json:"role"`
SessionID string `json:"session_id"`
jwt.RegisteredClaims
}
type AdminClaims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
SessionID string `json:"session_id"`
jwt.RegisteredClaims
}
// ============================================
// STRUCTURES REQUÊTE / RÉPONSE
// ============================================
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type RegisterClientRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Password string `json:"password" binding:"required,min=8"`
Nom string `json:"nom" binding:"required,min=2,max=100"`
Prenom string `json:"prenom" binding:"required,min=2,max=100"`
Telephone string `json:"telephone" binding:"required"`
}
type RegisterAdminRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Password string `json:"password" binding:"required,min=8"`
Role string `json:"role" binding:"required,oneof=admin cabine livreur"`
}
type LoginResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
User interface{} `json:"user"`
}
type ProfileResponse struct {
Username string `json:"username"`
Role string `json:"role"`
}
// ============================================
// VARIABLES & CONSTANTS
// ============================================
var (
clientTokenDuration = 5 * time.Hour
adminTokenDuration = 10 * time.Hour
userJWTSecret = []byte(os.Getenv("USER_JWT_SECRET")) // ✅ Pour clients
adminJWTSecret = []byte(os.Getenv("ADMIN_JWT_SECRET")) // ✅ Pour admin/cabine/livreur
userJWTSecret = []byte(os.Getenv("USER_JWT_SECRET"))
adminJWTSecret = []byte(os.Getenv("ADMIN_JWT_SECRET"))
)
// ============================================
// UTILITAIRES
// ============================================
func generateSessionID() string {
bytes := make([]byte, 16)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
// validatePhoneNumber valide le format du numéro de téléphone
func validatePhoneNumber(phone string) bool {
// Supprimer espaces/tirets
clean := regexp.MustCompile(`[\s\-\(\)]`).ReplaceAllString(phone, "")
// Format international ou national
validFormat := regexp.MustCompile(`^(\+33|0)[1-9]\d{8}$`)
return validFormat.MatchString(clean)
}
func normalizePhoneNumber(phone string) string {
clean := regexp.MustCompile(`[\s\-\(\)]`).ReplaceAllString(phone, "")
// Convertir 06... en +336...
if strings.HasPrefix(clean, "0") {
return "+33" + clean[1:]
}
return clean
}
// ============================================
// GENERATION TOKENS
// ============================================
func generateClientToken(client *models.Client) (string, error) {
sessionID := generateSessionID()
claims := ClientClaims{
sessionID := utils.GenerateSessionID()
claims := models.ClientClaims{
ClientID: client.ID,
Username: client.Username,
Role: "client",
@@ -135,7 +40,7 @@ func generateClientToken(client *models.Client) (string, error) {
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(userJWTSecret) // ✅ UTILISER userJWTSecret (CLIENT)
tokenString, err := token.SignedString(userJWTSecret)
if err != nil {
return "", err
}
@@ -143,8 +48,8 @@ func generateClientToken(client *models.Client) (string, error) {
}
func generateAdminToken(user *models.User) (string, error) {
sessionID := generateSessionID()
claims := AdminClaims{
sessionID := utils.GenerateSessionID()
claims := models.AdminClaims{
UserID: user.ID,
Username: user.Username,
Role: user.Role, // ← "admin" ou "cabine" ou "livreur"
@@ -158,21 +63,16 @@ func generateAdminToken(user *models.User) (string, error) {
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(adminJWTSecret) // ✅ UTILISER adminJWTSecret (ADMIN/CABINE/LIVREUR)
tokenString, err := token.SignedString(adminJWTSecret)
if err != nil {
return "", err
}
return tokenString, nil
}
// ============================================
// HANDLERS AUTHENTIFICATION CLIENT
// ============================================
// RegisterClient crée un nouveau compte client
// POST /api/v1/auth/register
func RegisterClient(c *gin.Context) {
var req RegisterClientRequest
var req models.RegisterClientRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("❌ [REGISTER_CLIENT] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
@@ -183,7 +83,7 @@ func RegisterClient(c *gin.Context) {
}
// Validation téléphone
if !validatePhoneNumber(req.Telephone) {
if !utils.ValidatePhoneNumber(req.Telephone) {
log.Printf("❌ [REGISTER_CLIENT] Téléphone invalide: %s", req.Telephone)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Numéro de téléphone invalide",
@@ -191,7 +91,7 @@ func RegisterClient(c *gin.Context) {
return
}
normalizedPhone := normalizePhoneNumber(req.Telephone)
normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
database := c.MustGet("database").(*db.Database)
// Vérifier username unique
@@ -256,9 +156,7 @@ func RegisterClient(c *gin.Context) {
client.Password = ""
log.Printf("✅ [REGISTER_CLIENT] Client créé: %s (ID=%d)", client.Username, client.ID)
c.JSON(http.StatusCreated, LoginResponse{
c.JSON(http.StatusCreated, models.LoginResponse{
AccessToken: token,
TokenType: "Bearer",
ExpiresIn: int(clientTokenDuration.Seconds()),
@@ -275,20 +173,19 @@ func RegisterClient(c *gin.Context) {
}
// AdminCreateClient crée un client depuis l'interface admin (sans session ni token)
// POST /api/v2/admin/protected/clients
func AdminCreateClient(c *gin.Context) {
var req RegisterClientRequest
var req models.RegisterClientRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
return
}
if !validatePhoneNumber(req.Telephone) {
if !utils.ValidatePhoneNumber(req.Telephone) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
return
}
normalizedPhone := normalizePhoneNumber(req.Telephone)
normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
database := c.MustGet("database").(*db.Database)
if existing, _ := database.GetClientByUsername(req.Username); existing != nil {
@@ -322,8 +219,6 @@ func AdminCreateClient(c *gin.Context) {
return
}
log.Printf("✅ [ADMIN_CREATE_CLIENT] Client créé par admin: %s (ID=%d)", client.Username, client.ID)
c.JSON(http.StatusCreated, gin.H{
"message": "Client créé avec succès",
"client": gin.H{
@@ -337,9 +232,8 @@ func AdminCreateClient(c *gin.Context) {
}
// LoginClient authentifie un client
// POST /api/v1/auth/login
func LoginClient(c *gin.Context) {
var req LoginRequest
var req models.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("❌ [LOGIN_CLIENT] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
@@ -361,7 +255,6 @@ func LoginClient(c *gin.Context) {
return
}
// ✅ LIGNE 302 CORRIGÉE - Gérer l'erreur !
token, err := generateClientToken(client)
if err != nil {
log.Printf("❌ [LOGIN_CLIENT] Erreur génération token: %v", err)
@@ -382,9 +275,7 @@ func LoginClient(c *gin.Context) {
log.Printf("⚠️ [LOGIN_CLIENT] Erreur session Redis: %v", err)
}
log.Printf("✅ [LOGIN_CLIENT] Client authentifié: %s", req.Username)
c.JSON(http.StatusOK, LoginResponse{
c.JSON(http.StatusOK, models.LoginResponse{
AccessToken: token,
TokenType: "Bearer",
ExpiresIn: int(clientTokenDuration.Seconds()),
@@ -402,7 +293,6 @@ func LoginClient(c *gin.Context) {
}
// ChangePassword permet à un client de changer son mot de passe
// PUT /api/v1/auth/change-password
func ChangePassword(c *gin.Context) {
var req struct {
CurrentPassword string `json:"current_password" binding:"required"`
@@ -442,16 +332,13 @@ func ChangePassword(c *gin.Context) {
return
}
log.Printf("✅ [CHANGE_PASSWORD] Mot de passe changé: ID=%d", clientID)
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Mot de passe mis à jour avec succès"})
}
// LogoutClient déconnecte un client
// POST /api/v1/auth/logout
func LogoutClient(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
// Invalider la session Redis
clientID, hasClientID := c.Get("client_id")
if hasClientID && clientID != nil {
if err := database.InvalidateSession(clientID.(int)); err != nil {
@@ -459,26 +346,18 @@ func LogoutClient(c *gin.Context) {
}
}
// Révoquer le JWT token
authHeader := c.GetHeader("Authorization")
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
database.RevokeToken(tokenStr)
}
log.Printf("✅ [LOGOUT_CLIENT] Client déconnecté")
c.JSON(http.StatusOK, gin.H{"message": "Déconnexion réussie"})
}
// ============================================
// HANDLERS AUTHENTIFICATION ADMIN
// ============================================
// RegisterAdmin crée un nouvel utilisateur admin/cabine/livreur
// POST /api/v1/auth/admin/register
func RegisterAdmin(c *gin.Context) {
var req RegisterAdminRequest
var req models.RegisterAdminRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("❌ [REGISTER_ADMIN] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
@@ -487,7 +366,6 @@ func RegisterAdmin(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
// Vérifier que l'user n'existe pas
if existingUser, _ := database.GetUserByUsername(req.Username); existingUser != nil {
log.Printf("❌ [REGISTER_ADMIN] Username déjà utilisé: %s", req.Username)
c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"})
@@ -507,9 +385,6 @@ func RegisterAdmin(c *gin.Context) {
return
}
log.Printf("✅ [REGISTER_ADMIN] User créé: %s (role=%s, ID=%d)", user.Username, user.Role, user.ID)
// Générer le token
token, err := generateAdminToken(user)
if err != nil {
log.Printf("❌ [REGISTER_ADMIN] Erreur génération token: %v", err)
@@ -517,11 +392,6 @@ func RegisterAdmin(c *gin.Context) {
return
}
log.Printf("✅ [REGISTER_ADMIN] Token généré: %s...", token[:50])
// ============================================
// ✅ CRITICAL FIX: ENREGISTRER LE TOKEN EN DB
// ============================================
expiresAt := time.Now().Add(adminTokenDuration)
if err := database.SaveToken(user.ID, user.Role, token, expiresAt); err != nil {
log.Printf("❌ [REGISTER_ADMIN] Erreur SaveToken: %v", err)
@@ -529,11 +399,9 @@ func RegisterAdmin(c *gin.Context) {
return
}
log.Printf("✅ [REGISTER_ADMIN] Token enregistré en DB pour user ID: %d", user.ID)
user.Password = ""
c.JSON(http.StatusCreated, LoginResponse{
c.JSON(http.StatusCreated, models.LoginResponse{
AccessToken: token,
TokenType: "Bearer",
ExpiresIn: int(adminTokenDuration.Seconds()),
@@ -542,10 +410,8 @@ func RegisterAdmin(c *gin.Context) {
}
// LoginAdmin authentifie un admin/cabine/livreur
// POST /api/v1/auth/admin/login
// ✅ AMÉLIORÉ: Meilleure gestion d'erreurs
func LoginAdmin(c *gin.Context) {
var req LoginRequest
var req models.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("❌ [LOGIN_ADMIN] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
@@ -561,14 +427,12 @@ func LoginAdmin(c *gin.Context) {
return
}
// Vérifier que c'est un admin/cabine/livreur
if user.Role != "admin" && user.Role != "cabine" && user.Role != "livreur" {
log.Printf("❌ [LOGIN_ADMIN] Rôle invalide: %s", user.Role)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Accès non autorisé"})
return
}
// Vérifier le mot de passe
if user.Password == "" {
log.Printf("❌ [LOGIN_ADMIN] Mot de passe vide en base")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Identifiants invalides"})
@@ -581,7 +445,6 @@ func LoginAdmin(c *gin.Context) {
return
}
// Générer le token
token, _ := generateAdminToken(user)
expiresAt := time.Now().Add(adminTokenDuration)
@@ -591,9 +454,7 @@ func LoginAdmin(c *gin.Context) {
return
}
log.Printf("✅ [LOGIN_ADMIN] User authentifié: %s (role=%s)", user.Username, user.Role)
c.JSON(http.StatusOK, LoginResponse{
c.JSON(http.StatusOK, models.LoginResponse{
AccessToken: token,
TokenType: "Bearer",
ExpiresIn: int(adminTokenDuration.Seconds()),
@@ -606,7 +467,6 @@ func LoginAdmin(c *gin.Context) {
}
// LogoutAdmin déconnecte un admin/cabine/livreur
// POST /api/v1/auth/admin/logout
func LogoutAdmin(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -645,14 +505,14 @@ func GetCurrentClient(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"client": gin.H{
"id": client.ID,
"username": client.Username,
"nom": client.Nom,
"prenom": client.Prenom,
"telephone": client.Telephone,
"command": client.Command,
"point": client.Point,
"amende": client.Amende,
"id": client.ID,
"username": client.Username,
"nom": client.Nom,
"prenom": client.Prenom,
"telephone": client.Telephone,
"command": client.Command,
"amende": client.Amende,
"points_extra": client.PointsExtra,
},
})
}
@@ -675,7 +535,7 @@ func GetCurrentAdmin(c *gin.Context) {
log.Printf("✅ [GET_CURRENT_ADMIN] User récupéré: %s", user.Username)
c.JSON(http.StatusOK, gin.H{
"user": ProfileResponse{
"user": models.ProfileResponse{
Username: user.Username,
Role: user.Role,
},
@@ -795,8 +655,7 @@ func GetAllClients(c *gin.Context) {
"prenom": cl.Prenom,
"telephone": cl.Telephone,
"command": cl.Command,
"point": cl.Point,
"points_zipette": cl.PointZipette,
"points_extra": cl.PointsExtra,
"amende": cl.Amende,
"cancellations_count": cl.CancellationsCount,
"last_penalty_reason": cl.LastPenaltyReason,
+22 -52
View File
@@ -9,8 +9,10 @@ import (
"encoding/json"
"fmt"
"gestion/db"
"gestion/utils"
"log"
"net/http"
"slices"
"strconv"
"time"
@@ -22,7 +24,6 @@ import (
// ============================================
// SetCommandDestinationCoordinates stocke les coordonnées destination en Redis
// POST /api/v2/admin/protected/orders/:id/set-destination
func SetCommandDestinationCoordinates(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -69,15 +70,10 @@ func SetCommandDestinationCoordinates(c *gin.Context) {
return
}
// Vérifier que la commande existe
command, err := database.GetCommandByID(commandID)
if err != nil {
if !utils.CheckCommand(commandID, database) {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
return
}
log.Printf("Command: %v", command)
// Stocker en Redis avec format JSON
destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
coordsJSON, _ := json.Marshal(map[string]float64{
"lat": req.Latitude,
@@ -134,12 +130,12 @@ func GetClientProfile(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"client": gin.H{
"id": client.ID,
"username": client.Username,
"command": client.Command,
"point": client.Point,
"amende": client.Amende,
"created_at": client.CreatedAt,
"id": client.ID,
"username": client.Username,
"command": client.Command,
"amende": client.Amende,
"points_extra": client.PointsExtra,
"created_at": client.CreatedAt,
},
})
}
@@ -170,8 +166,8 @@ func GetClientFullHistory(c *gin.Context) {
"client": gin.H{
"username": client.Username,
"total_commands": client.Command,
"points": client.Point,
"amende": client.Amende,
"points_extra": client.PointsExtra,
},
"commands": commands,
"count": len(commands),
@@ -209,15 +205,7 @@ func UpdateCommandAddressCabine(c *gin.Context) {
status, _ := command["status"].(string)
allowedStatuses := []string{"pending", "", "assigned"}
isAllowed := false
for _, s := range allowedStatuses {
if status == s {
isAllowed = true
break
}
}
if !isAllowed {
if !slices.Contains(allowedStatuses, status) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Impossible de modifier l'adresse d'une commande en cours ou terminée",
"current_status": status,
@@ -288,10 +276,6 @@ func GetLivreurPosition(c *gin.Context) {
})
}
// ============================================
// 4. DELIVERY TRACKING CLIENT (SANS GPS)
// ============================================
func GetDeliveryTrackingClient(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -394,10 +378,6 @@ func GetDeliveryTracking(c *gin.Context) {
c.JSON(http.StatusOK, response)
}
// ============================================
// 6. DELIVERY ISSUES
// ============================================
func GetDeliveryIssues(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -630,24 +610,6 @@ func ForceValidateDelivery(c *gin.Context) {
return
}
validStatuses := []string{"assigned", "in_transit", "en_route", "en_cours", "pending", "priority"}
isValidStatus := false
for _, vs := range validStatuses {
if status == vs {
isValidStatus = true
break
}
}
if !isValidStatus && status != "livre" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Commande ne peut pas être validée de force dans ce statut",
"current_status": status,
"valid_statuses": validStatuses,
})
return
}
if status == "livre" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Cette commande a déjà été validée",
@@ -656,6 +618,16 @@ func ForceValidateDelivery(c *gin.Context) {
return
}
validStatuses := []string{"assigned", "en_route", "pending", "priority"}
if !slices.Contains(validStatuses, status) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Commande ne peut pas être validée de force dans ce statut",
"current_status": status,
"valid_statuses": validStatuses,
})
return
}
err = database.UpdateCommandStatus(commandID, "livre")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
@@ -668,7 +640,6 @@ func ForceValidateDelivery(c *gin.Context) {
clientUsername, _ := command["username"].(string)
livreurAssign, _ := command["livreur_assign"].(string)
// Notifier le client
if clientUsername != "" {
clientMsg := fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID)
database.NotifyClient(clientUsername, commandID, "livre", clientMsg)
@@ -678,12 +649,11 @@ func ForceValidateDelivery(c *gin.Context) {
log.Printf("⚠️ Erreur compteur commandes: %v", err)
}
if err := database.AddClientPoints(clientUsername, 10); err != nil {
if err := database.AddClientPointsByCategory(clientUsername, 10, ""); err != nil {
log.Printf("⚠️ Erreur ajout points: %v", err)
}
if livreurAssign != "" {
log.Printf("📦 Commande %d validée de force par admin - Optimisation queue de %s...", commandID, livreurAssign)
err := database.CompleteDeliveryAndProcessNext(livreurAssign, commandID)
if err != nil {
log.Printf("⚠️ Erreur optimisation: %v", err)
+15 -71
View File
@@ -18,10 +18,6 @@ import (
"github.com/gin-gonic/gin"
)
// ============================================
// RATE LIMITING
// ============================================
var (
cancelRateLimitMap = make(map[string][]time.Time)
cancelMaxRequests = 5 // Max 5 annulations
@@ -49,16 +45,10 @@ func checkCancelRateLimit(key string) bool {
return true
}
// ============================================
// HELPERS DE SÉCURITÉ
// ============================================
func validateReason(reason string) string {
// Limiter la longueur
if len(reason) > 500 {
reason = reason[:500]
}
// Sanitizer
reason = strings.Map(func(r rune) rune {
if r < 32 || r == 127 {
return -1
@@ -73,10 +63,6 @@ func validateReason(reason string) string {
return reason
}
// ============================================
// 1️⃣ ANNULATION PAR LE CLIENT - VERSION SÉCURISÉE
// ============================================
func CancelCommandByClient(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -115,19 +101,12 @@ func CancelCommandByClient(c *gin.Context) {
req.Reason = validateReason(req.Reason)
log.Printf("🚫 [CANCEL_CLIENT] Client %s annule cmd %d (force=%v)", username, commandID, req.Force)
// ============================================
// UTILISER LA FONCTION ATOMIQUE
// ============================================
penalty, pointsLost, err := database.CancelCommandAtomic(commandID, username, req.Reason, req.Force)
penalty, err := database.CancelCommandAtomic(commandID, username, req.Reason, req.Force)
if err != nil {
log.Printf("❌ [CANCEL_CLIENT] Erreur: %v", err)
// ✅ GESTION SPÉCIALE POUR "confirmation requise"
if err.Error() == "confirmation requise" {
// ✅ RÉCUPÉRER LES INFORMATIONS DE LA COMMANDE
command, errCmd := database.GetCommandByID(commandID)
if errCmd != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
@@ -137,28 +116,14 @@ func CancelCommandByClient(c *gin.Context) {
livreurAssign, _ := command["livreur_assign"].(string)
currentStatus, _ := command["status"].(string)
// ✅ VÉRIFIER SI ETA EXISTE (VERSION CORRIGÉE)
hasETA := false
if livreurAssign != "" {
// ✅ FIX: Utiliser la nouvelle fonction qui vérifie VRAIMENT l'ETA
hasETA = database.CheckCommandETAExistsAndValid(commandID)
}
// ✅ CALCULER LA PÉNALITÉ QUI SERA APPLIQUÉE
nextPenalty, _ := database.CalculateCancellationPenalty(username)
cancelCount, _ := database.GetClientCancellationsCount(username)
// ✅ RÉCUPÉRER LES POINTS ACTUELS
client, _ := database.GetClientByUsername(username)
currentPointsWeed := 0
currentPointsZipette := 0
if client != nil {
currentPointsWeed = client.Point
currentPointsZipette = client.PointZipette
}
totalPoints := currentPointsWeed + currentPointsZipette
// ✅ CONSTRUIRE LA RÉPONSE EN FONCTION DE hasETA
response := gin.H{
"success": false,
"warning": true,
@@ -170,7 +135,6 @@ func CancelCommandByClient(c *gin.Context) {
}
if hasETA {
// ⚠️ CAS 1: LIVREUR EN ROUTE (ETA définie) = PÉNALITÉ TOTALE
log.Printf("⚠️ [CANCEL_CLIENT] Annulation tardive avec ETA - Status: %s, Livreur: %s", currentStatus, livreurAssign)
response["message"] = "⚠️ Un livreur est en route vers votre adresse (ETA définie)"
@@ -180,27 +144,21 @@ func CancelCommandByClient(c *gin.Context) {
"has_eta": true,
}
response["penalty_warning"] = gin.H{
"will_apply": true,
"penalty_amount": nextPenalty,
"current_violations": cancelCount,
"current_points_weed": currentPointsWeed,
"current_points_zipette": currentPointsZipette,
"total_points": totalPoints,
"points_will_reset": true,
"will_apply": true,
"penalty_amount": nextPenalty,
"current_violations": cancelCount,
"message": fmt.Sprintf(
"⚠️ ATTENTION: Une amende de %d points sera appliquée ET tous vos points (%d weed/hash + %d zipette = %d total) seront remis à zéro!",
nextPenalty, currentPointsWeed, currentPointsZipette, totalPoints,
"⚠️ ATTENTION: Une amende de %d sera appliquée pour annulation tardive",
nextPenalty,
),
"scale": gin.H{
"1st_cancel": "20 points + remise à zéro TOTALE",
"2nd_cancel": "50 points + remise à zéro TOTALE",
"3rd_cancel": "100 points + remise à zéro TOTALE",
"4th+_cancel": "150 points + remise à zéro TOTALE",
"your_next": fmt.Sprintf("%d points + remise à zéro de tous vos %d points", nextPenalty, totalPoints),
"1st_cancel": 20,
"2nd_cancel": 50,
"3rd_cancel": 100,
"4th+_cancel": 150,
},
}
} else {
// ️ CAS 2: LIVREUR ASSIGNÉ MAIS PAS EN ROUTE (PAS D'ETA) = PAS DE PÉNALITÉ
log.Printf("️ [CANCEL_CLIENT] Livreur assigné mais pas d'ETA - Annulation sans pénalité")
response["message"] = "️ Un livreur est assigné mais n'est pas encore en route"
@@ -274,11 +232,8 @@ func CancelCommandByClient(c *gin.Context) {
if penalty > 0 {
response["penalty"] = gin.H{
"penalty_points": penalty,
"points_weed_lost": pointsLost["weed"],
"points_zipette_lost": pointsLost["zipette"],
"total_points_lost": pointsLost["weed"] + pointsLost["zipette"],
"warning": "Une pénalité a été appliquée et vos points ont été remis à zéro",
"penalty_amount": penalty,
"warning": "Une amende a été appliquée pour annulation tardive",
}
} else {
response["info"] = "Aucune pénalité appliquée"
@@ -325,10 +280,6 @@ func GetMyCancellationHistory(c *gin.Context) {
})
}
// ============================================
// LISTE DES COMMANDES ANNULÉES - VERSION SÉCURISÉE
// ============================================
func GetAllCancelledOrders(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -345,7 +296,6 @@ func GetAllCancelledOrders(c *gin.Context) {
return
}
// ✅ VALIDATION des paramètres
filterUsername := c.Query("username")
if len(filterUsername) > 100 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Username trop long"})
@@ -373,14 +323,14 @@ func GetAllCancelledOrders(c *gin.Context) {
}
// ✅ ENRICHIR les données (sans exposer d'infos sensibles inutiles)
var enrichedOrders []map[string]interface{}
var enrichedOrders []map[string]any
for _, order := range cancelledOrders {
orderID, _ := order["id"].(int)
items, _ := database.GetCommandItems(orderID)
logs, _ := database.GetCommandLogs(orderID)
var cancellationLog map[string]interface{}
var cancellationLog map[string]any
for _, logEntry := range logs {
status, _ := logEntry["status"].(string)
if status == "cancelled" {
@@ -389,7 +339,7 @@ func GetAllCancelledOrders(c *gin.Context) {
}
}
enrichedOrder := map[string]interface{}{
enrichedOrder := map[string]any{
"id": order["id"],
"username": order["username"],
"total_prix": order["total_prix"],
@@ -419,10 +369,6 @@ func GetAllCancelledOrders(c *gin.Context) {
})
}
// ============================================
// SUPPRESSION PAR CABINE - VERSION SÉCURISÉE
// ============================================
func DeleteCommandByCabine(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -447,12 +393,10 @@ func DeleteCommandByCabine(c *gin.Context) {
log.Printf("🗑️ [DELETE_COMMAND] %s (%s) supprime cmd %d", username, userRole, commandID)
// ✅ UTILISER LA FONCTION ATOMIQUE
err = database.DeleteCommandAtomic(commandID, username, userRole)
if err != nil {
log.Printf("❌ [DELETE_COMMAND] Erreur: %v", err)
// ❌ Ne pas exposer les détails de l'erreur
if err.Error() == "commande non trouvée" {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
} else {
-4
View File
@@ -10,7 +10,6 @@ import (
"github.com/gin-gonic/gin"
)
// GET /api/v1/categories — public
func GetCategories(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -27,7 +26,6 @@ func GetCategories(c *gin.Context) {
})
}
// POST /api/v2/admin/protected/categories — admin
func CreateCategory(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -111,7 +109,6 @@ func UpdateCategory(c *gin.Context) {
})
}
// DELETE /api/v2/admin/protected/categories/:id — admin
func DeleteCategory(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -131,6 +128,5 @@ func DeleteCategory(c *gin.Context) {
return
}
log.Printf("✅ [CATEGORIES] Supprimée: %d", id)
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Catégorie supprimée"})
}
+1 -19
View File
@@ -10,7 +10,6 @@ import (
)
// GetCommandStatus - Statut temps réel d'une commande
// GET /api/v1/commands/:id/status
func GetCommandStatus(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -28,16 +27,12 @@ func GetCommandStatus(c *gin.Context) {
return
}
log.Printf("📊 [STATUS] Client %s demande statut cmd %d", usernameStr, commandID)
// Récupérer la commande
command, err := database.GetCommandByID(commandID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
return
}
// ✅ VÉRIFIER PROPRIÉTÉ
cmdUsername, _ := command["username"].(string)
if cmdUsername != usernameStr {
log.Printf("❌ [STATUS] Accès refusé - cmd de %s demandée par %s", cmdUsername, usernameStr)
@@ -47,10 +42,8 @@ func GetCommandStatus(c *gin.Context) {
return
}
// Récupérer ETA
etaData, _ := database.GetCommandETA(commandID)
// Récupérer infos livreur (si assigné)
livreurInfo := gin.H{
"assigned": false,
}
@@ -63,7 +56,6 @@ func GetCommandStatus(c *gin.Context) {
}
}
// Mapper le statut en message lisible
statusMessage := getStatusMessage(command["status"].(string))
c.JSON(http.StatusOK, gin.H{
@@ -80,7 +72,6 @@ func GetCommandStatus(c *gin.Context) {
}
// GetMyCommandsWithTracking - Liste des commandes avec suivi
// GET /api/v1/my-commands
func GetMyCommandsWithTracking(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -147,7 +138,6 @@ func GetMyCommandsWithTracking(c *gin.Context) {
}
// GetCommandTracking - Suivi détaillé d'une commande
// GET /api/v1/commands/:id/tracking
func GetCommandTracking(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -174,13 +164,10 @@ func GetCommandTracking(c *gin.Context) {
return
}
// Récupérer logs
logs, _ := database.GetCommandLogs(commandID)
// ETA
etaData, _ := database.GetCommandETA(commandID)
// Timeline (basé sur les logs)
timeline := buildTimeline(logs)
c.JSON(http.StatusOK, gin.H{
@@ -194,11 +181,6 @@ func GetCommandTracking(c *gin.Context) {
})
}
// ============================================
// HELPERS
// ============================================
// getStatusMessage retourne un message lisible pour le client
func getStatusMessage(status string) string {
messages := map[string]string{
"pending": "⏳ En attente d'assignation",
@@ -219,7 +201,7 @@ func getStatusMessage(status string) string {
}
// buildTimeline construit une timeline depuis les logs
func buildTimeline(logs []map[string]interface{}) []gin.H {
func buildTimeline(logs []map[string]any) []gin.H {
timeline := make([]gin.H, 0)
for _, logEntry := range logs {
+11 -18
View File
@@ -3,6 +3,7 @@ package handlers
import (
"fmt"
"gestion/db"
"gestion/utils"
"log"
"net/http"
"strconv"
@@ -42,9 +43,6 @@ func checkRateLimit(key string) bool {
return true
}
// ============================================
// GESTION ADRESSE & ADMIN
// ============================================
func safeGetUsername(c *gin.Context) (string, error) {
username, exists := c.Get("username")
if !exists {
@@ -71,13 +69,11 @@ func validateAddress(address string) error {
}
// UpdateCommandAddress met à jour l'adresse de livraison d'une commande
// PUT /api/v1/admin/commands/:id/address
func UpdateCommandAddress(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
// ✅ Vérification du rôle
if c.GetString("role") != "admin" {
log.Printf("❌ [UPD_ADDR] Accès refusé - role=%s", c.GetString("role"))
userRole := c.GetString("role")
if !utils.CheckRoleAdmin(c, userRole) {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
@@ -111,13 +107,11 @@ func UpdateCommandAddress(c *gin.Context) {
return
}
// ✅ Validation de l'adresse
if err := validateAddress(req.DeliveryAddress); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// ✅ Logs sanitizés
log.Printf("📝 [UPD_ADDR] Admin %s modifie cmd %d", adminUsername, commandID)
command, err := database.GetCommandByID(commandID)
@@ -156,13 +150,11 @@ func UpdateCommandAddress(c *gin.Context) {
}
// ProposeAddressChange propose une nouvelle adresse au client pour validation
// POST /api/v2/admin/protected/orders/:id/propose-address
// POST /api/v1/cabine/commands/:id/propose-address
func ProposeAddressChange(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
userRole := c.GetString("role")
if userRole != "admin" && userRole != "cabine" {
if !utils.CheckRoleAdmin(c, userRole) && !utils.CheckRoleCabine(c, userRole) {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
@@ -218,7 +210,6 @@ func ProposeAddressChange(c *gin.Context) {
database.NotifyClient(clientUsername, commandID, "address_proposal", msg)
}
log.Printf("✅ [PROPOSE_ADDR] Commande %d - nouvelle adresse proposée par %s", commandID, staffUsername)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Nouvelle adresse proposée au client",
@@ -231,6 +222,11 @@ func ProposeAddressChange(c *gin.Context) {
func RespondToAddressProposal(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
userRole := c.GetString("role")
if !utils.CheckRoleClient(c, userRole) {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
clientUsername, err := safeGetUsername(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"})
@@ -276,8 +272,7 @@ func GetAllCommands(c *gin.Context) {
username := c.Query("username")
userRole := c.GetString("role")
if userRole != "admin" && userRole != "cabine" {
log.Printf("❌ [VALIDATE] Accès refusé - role=%s", userRole)
if !utils.CheckRoleAdmin(c, userRole) && !utils.CheckRoleCabine(c, userRole) {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
@@ -725,8 +720,7 @@ func GetClientCommandsHistory(c *gin.Context) {
} else if client == nil {
log.Printf("⚠️ [HISTORY] client est NIL!")
} else {
log.Printf("✅ [HISTORY] Client récupéré: username=%s, point=%d, point_zipette=%d",
client.Username, client.Point, client.PointZipette)
log.Printf("✅ [HISTORY] Client récupéré: username=%s", client.Username)
}
resp := gin.H{
@@ -760,7 +754,6 @@ func GetClientCommandsHistory(c *gin.Context) {
"prenom": client.Prenom,
"telephone": client.Telephone,
"total_commands": client.Command,
"points": client.Point,
"pool_points": poolPoints,
"pool_names": poolNames,
"penalties": client.Amende,
@@ -13,7 +13,6 @@ import (
)
// IPNWebhook - POST /api/v1/webhooks/nowpayments
// Reçoit les callbacks de NowPayments lors des changements de statut
func IPNWebhook(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
np, ok := c.MustGet("nowpayments").(*services.NowPaymentsClient)
@@ -44,7 +43,6 @@ func IPNWebhook(c *gin.Context) {
payment, err := database.GetCryptoPaymentByNowPaymentID(payload.PaymentID.String())
if err != nil || payment == nil {
log.Printf("[IPN] paiement introuvable: %s", payload.PaymentID.String())
// 200 pour éviter les retries NowPayments
c.JSON(http.StatusOK, gin.H{"ok": true})
return
}
+31 -77
View File
@@ -1,25 +1,18 @@
// ============================================
// handlers/delivery_handlers.go
// 🔧 VERSION MODIFIÉE avec ETA automatique
// ============================================
package handlers
import (
"encoding/json"
"fmt"
"gestion/db"
"gestion/utils"
"log"
"math"
"net/http"
"slices"
"strconv"
"github.com/gin-gonic/gin"
)
// ============================================
// 🔧 GetMyDeliveries
// ============================================
func GetMyDeliveries(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -46,7 +39,6 @@ func GetMyDeliveries(c *gin.Context) {
return
}
// ✨ FILTRAGE (SANS TÉLÉPHONE)
filteredCommands := make([]gin.H, len(commands))
for i, cmd := range commands {
commandID, _ := cmd["id"].(int)
@@ -172,10 +164,6 @@ func GetDeliveryDetails(c *gin.Context) {
})
}
// ============================================
// 🔧 UpdateDeliveryStatus - VERSION MODIFIÉE
// ✅ CALCUL AUTOMATIQUE ETA lors du passage en "en_route"
// ============================================
func UpdateDeliveryStatus(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -215,7 +203,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
return
}
// ✅ VÉRIFIER PROPRIÉTÉ
livreurAssign, _ := command["livreur_assign"].(string)
if livreurAssign != usernameStr {
log.Printf("❌ Accès refusé - assigné à %s", livreurAssign)
@@ -225,7 +212,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
return
}
// ✅ STATUTS VALIDES POUR LIVREUR (correspondant à la DB)
validStatuses := []string{
"assigned",
"en_route",
@@ -234,15 +220,7 @@ func UpdateDeliveryStatus(c *gin.Context) {
"cancelled",
}
isValid := false
for _, s := range validStatuses {
if req.Status == s {
isValid = true
break
}
}
if !isValid {
if !slices.Contains(validStatuses, req.Status) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Statut invalide",
"valid_statuses": validStatuses,
@@ -261,7 +239,7 @@ func UpdateDeliveryStatus(c *gin.Context) {
destLon, _ := command["dest_longitude"].(float64)
if destLat != 0 && destLon != 0 {
distance := calculateDistance(req.Latitude, req.Longitude, destLat, destLon)
distance := utils.CalculateDistance(req.Latitude, req.Longitude, destLat, destLon)
log.Printf("📍 [GPS] Distance: %.2f m", distance)
if distance > 100 {
@@ -295,10 +273,8 @@ func UpdateDeliveryStatus(c *gin.Context) {
if req.Status == "en_route" {
log.Printf("🚗 [STATUS_LIVREUR] Passage en 'en_route' - Calcul ETA...")
// Récupérer les coordonnées destination
var destLat, destLon float64
// 1. Essayer le cache Redis
destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
destData, err := db.Redis.Get(db.RedisCtx, destCacheKey).Result()
if err == nil && destData != "" {
@@ -326,22 +302,29 @@ func UpdateDeliveryStatus(c *gin.Context) {
}
}
// 3. Calculer l'ETA depuis la position du livreur
if destLat != 0 && destLon != 0 {
etaMinutes = database.CalculateETAForDeliveryman(usernameStr, destLat, destLon)
// Définir l'ETA dans Redis
if err := database.SetCommandETA(commandID, etaMinutes); err != nil {
log.Printf("⚠️ [STATUS_LIVREUR] Erreur définition ETA: %v", err)
} else {
log.Printf("✅ [STATUS_LIVREUR] ETA défini: %d minutes", etaMinutes)
etaMessage = fmt.Sprintf("Arrivée prévue dans %d minutes", etaMinutes)
if etaMinutes >= 60 {
h := etaMinutes / 60
m := etaMinutes % 60
if m > 0 {
etaMessage = fmt.Sprintf("Arrivée prévue dans %dh%02d", h, m)
} else {
etaMessage = fmt.Sprintf("Arrivée prévue dans %dh", h)
}
} else {
etaMessage = fmt.Sprintf("Arrivée prévue dans %d minutes", etaMinutes)
}
}
} else {
log.Printf("⚠️ [STATUS_LIVREUR] Coordonnées destination manquantes - ETA par défaut")
etaMinutes = 30 // Fallback
etaMinutes = 30
database.SetCommandETA(commandID, etaMinutes)
etaMessage = "Arrivée prévue dans 30 minutes (estimation par défaut)"
}
// Mettre à jour le statut du livreur en "delivering"
@@ -352,7 +335,7 @@ func UpdateDeliveryStatus(c *gin.Context) {
// Log
message := req.Notes
if message == "" {
message = getDeliveryStatusMessage(req.Status)
message = utils.GetDeliveryStatusMessage(req.Status)
}
if etaMessage != "" {
message += fmt.Sprintf(" - %s", etaMessage)
@@ -366,9 +349,21 @@ func UpdateDeliveryStatus(c *gin.Context) {
switch req.Status {
case "en_route":
if etaMinutes > 0 {
clientMsg = fmt.Sprintf("Votre commande #%d est en route ! Arrivée dans ~%d min", commandID, etaMinutes)
var etaStr string
if etaMinutes >= 60 {
h := etaMinutes / 60
m := etaMinutes % 60
if m > 0 {
etaStr = fmt.Sprintf("%dh%02d", h, m)
} else {
etaStr = fmt.Sprintf("%dh", h)
}
} else {
etaStr = fmt.Sprintf("%d min", etaMinutes)
}
clientMsg = fmt.Sprintf("🛵 Votre commande #%d est en route ! Arrivée dans ~%s", commandID, etaStr)
} else {
clientMsg = fmt.Sprintf("Votre commande #%d est en route !", commandID)
clientMsg = fmt.Sprintf("🛵 Votre commande #%d est en route !", commandID)
}
case "arrived":
clientMsg = fmt.Sprintf("🛵 Votre livreur est là ! Il sera chez vous dans 5 minutes (commande #%d)", commandID)
@@ -412,44 +407,3 @@ func UpdateDeliveryStatus(c *gin.Context) {
c.JSON(http.StatusOK, response)
}
// ============================================
// HELPERS
// ============================================
func calculateDistance(lat1, lon1, lat2, lon2 float64) float64 {
const earthRadiusKm = 6371
const metersPerKm = 1000
lat1Rad := degreesToRadians(lat1)
lon1Rad := degreesToRadians(lon1)
lat2Rad := degreesToRadians(lat2)
lon2Rad := degreesToRadians(lon2)
dLat := lat2Rad - lat1Rad
dLon := lon2Rad - lon1Rad
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(lat1Rad)*math.Cos(lat2Rad)*
math.Sin(dLon/2)*math.Sin(dLon/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return earthRadiusKm * c * metersPerKm
}
func degreesToRadians(degrees float64) float64 {
return degrees * math.Pi / 180
}
func getDeliveryStatusMessage(status string) string {
messages := map[string]string{
"assigned": "Commande assignée",
"en_route": "En route vers le client",
"arrived": "Arrivé à destination",
"livre": "Livraison effectuée",
"cancelled": "Livraison annulée",
}
if msg, ok := messages[status]; ok {
return msg
}
return fmt.Sprintf("Statut changé: %s", status)
}
+2 -55
View File
@@ -8,6 +8,7 @@ package handlers
import (
"fmt"
"gestion/db"
"gestion/utils"
"log"
"net/http"
"strconv"
@@ -16,12 +17,7 @@ import (
"github.com/gin-gonic/gin"
)
// ============================================
// 📊 GET DELIVERY PERSON DETAILS
// ============================================
// GetDeliveryPersonDetails récupère les détails complets d'un livreur
// GET /api/v2/admin/protected/delivery-persons/:username
func GetDeliveryPersonDetails(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -39,11 +35,6 @@ func GetDeliveryPersonDetails(c *gin.Context) {
return
}
log.Printf("👤 [GET_DELIVERY_DETAILS] Récupération détails pour: %s", username)
// ============================================
// ÉTAPE 1: Récupérer les infos de base du livreur
// ============================================
livreur, err := database.GetUserByUsername(username)
if err != nil {
log.Printf("❌ [GET_DELIVERY_DETAILS] Livreur non trouvé: %v", err)
@@ -64,9 +55,6 @@ func GetDeliveryPersonDetails(c *gin.Context) {
return
}
// ============================================
// ÉTAPE 2: Récupérer le statut et la position GPS
// ============================================
status, err := database.GetDeliveryPersonStatus(username)
if err != nil {
log.Printf("⚠️ [GET_DELIVERY_DETAILS] Impossible de récupérer le statut: %v", err)
@@ -83,22 +71,13 @@ func GetDeliveryPersonDetails(c *gin.Context) {
}
}
// ============================================
// ÉTAPE 3: Récupérer les stats de livraison
// ============================================
queueSize, _ := database.GetDeliverymanQueueSize(username)
currentCommand, _ := database.GetCurrentCommand(username)
// Compter les livraisons
totalDeliveries, _ := database.CountDeliveriesByStatus(username, "")
completedDeliveries, _ := database.CountDeliveriesByStatus(username, "approved")
pendingDeliveries, _ := database.CountDeliveriesByStatus(username, "assigned,en_route,livre")
log.Printf("✅ [GET_DELIVERY_DETAILS] Détails récupérés pour %s", username)
// ============================================
// RÉPONSE
// ============================================
c.JSON(http.StatusOK, gin.H{
"success": true,
"deliveryman": gin.H{
@@ -116,19 +95,12 @@ func GetDeliveryPersonDetails(c *gin.Context) {
})
}
// ============================================
// 🔄 UPDATE DELIVERY PERSON STATUS
// ============================================
// UpdateDeliveryPersonStatusAdmin modifie le statut d'un livreur (Admin)
// PUT /api/v2/admin/protected/delivery-persons/:username/status
func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
// ✅ SÉCURITÉ: Admin seulement
userRole := c.GetString("role")
if userRole != "admin" {
log.Printf("❌ [UPDATE_DELIVERY_STATUS] Accès refusé - role=%s", userRole)
if !utils.CheckRoleAdmin(c, userRole) {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
@@ -172,9 +144,6 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
log.Printf("📝 [UPDATE_DELIVERY_STATUS] Modification: %s → %s", username, req.Status)
// ============================================
// Vérifier que le livreur existe
// ============================================
livreur, err := database.GetUserByUsername(username)
if err != nil {
log.Printf("❌ [UPDATE_DELIVERY_STATUS] Livreur non trouvé")
@@ -189,9 +158,6 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
return
}
// ============================================
// Mettre à jour le statut
// ============================================
err = database.UpdateDeliveryPersonStatus(username, req.Status)
if err != nil {
log.Printf("❌ [UPDATE_DELIVERY_STATUS] Erreur mise à jour: %v", err)
@@ -215,16 +181,10 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
})
}
// ============================================
// 📊 GET DELIVERY PERSON STATS
// ============================================
// GetDeliveryPersonStats récupère les statistiques d'un livreur
// GET /api/v2/admin/protected/delivery-persons/:username/stats
func GetDeliveryPersonStats(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
// ✅ SÉCURITÉ: Admin seulement
userRole := c.GetString("role")
if userRole != "admin" {
log.Printf("❌ [GET_DELIVERY_STATS] Accès refusé - role=%s", userRole)
@@ -240,9 +200,6 @@ func GetDeliveryPersonStats(c *gin.Context) {
log.Printf("📊 [GET_DELIVERY_STATS] Calcul stats pour: %s", username)
// ============================================
// Vérifier que le livreur existe
// ============================================
livreur, err := database.GetUserByUsername(username)
if err != nil {
log.Printf("❌ [GET_DELIVERY_STATS] Livreur non trouvé")
@@ -306,12 +263,7 @@ func GetDeliveryPersonStats(c *gin.Context) {
})
}
// ============================================
// 📜 GET DELIVERY PERSON HISTORY
// ============================================
// GetDeliveryPersonHistory récupère l'historique des livraisons d'un livreur
// GET /api/v2/admin/protected/delivery-persons/:username/history?limit=20&offset=0
func GetDeliveryPersonHistory(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -394,10 +346,6 @@ func GetDeliveryPersonHistory(c *gin.Context) {
})
}
// ============================================
// 📍 UPDATE DELIVERY PERSON LOCATION
// ============================================
// UpdateDeliveryPersonLocationAdmin modifie la position GPS d'un livreur (Admin)
// PUT /api/v2/admin/protected/delivery-persons/:username/location
func UpdateDeliveryPersonLocationAdmin(c *gin.Context) {
@@ -579,7 +527,6 @@ func RemoveCommandFromQueue(c *gin.Context) {
if err != nil {
log.Printf("⚠️ [REMOVE_FROM_QUEUE] Impossible de réinitialiser le statut: %v", err)
} else {
// Retirer l'assignation du livreur
database.UpdateCommandLivreur(commandID, "")
log.Printf("✅ [REMOVE_FROM_QUEUE] Commande réinitialisée en 'pending'")
}
-8
View File
@@ -18,11 +18,6 @@ import (
"github.com/gin-gonic/gin"
)
// ============================================
// GET /api/v1/orders/:id/eta
// ✅ CORRECTION: ETA visible UNIQUEMENT si status >= en_route
// ============================================
func GetOrderETA(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
geoService := c.MustGet("geoService").(*services.GeoService)
@@ -49,9 +44,6 @@ func GetOrderETA(c *gin.Context) {
return
}
log.Printf("📊 [ETA] START - commandID=%d, username=%s", commandID, username.(string))
// 3️⃣ RÉCUPÉRER LA COMMANDE
command, err := database.GetCommandByID(commandID)
if err != nil {
log.Printf("❌ [ETA] Commande %d non trouvée", commandID)
-8
View File
@@ -22,8 +22,6 @@ import (
// ============================================
// GeocodeAddress convertit une adresse en coordonnées GPS
// POST /api/v1/geocode
// Body: {"address": "1600 Amphitheatre Parkway, Mountain View, CA"}
func GeocodeAddress(c *gin.Context) {
geoService := c.MustGet("geoService").(*services.GeoService)
@@ -59,10 +57,6 @@ func GeocodeAddress(c *gin.Context) {
})
}
// ============================================
// RECHERCHE DU LIVREUR LE PLUS PROCHE
// ============================================
// FindNearestDeliveryPerson trouve le livreur le plus proche d'une adresse
func FindNearestDeliveryPerson(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -181,8 +175,6 @@ func FindNearestDeliveryPerson(c *gin.Context) {
// ============================================
// GetAllDeliveryDistances retourne tous les livreurs triés par distance
// POST /api/v2/admin/protected/delivery/distances
// Body: {"address": "123 Main St"} ou {"latitude": 48.8566, "longitude": 2.3522}
func GetAllDeliveryDistances(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
geoService := c.MustGet("geoService").(*services.GeoService)
+48
View File
@@ -6,9 +6,11 @@
package handlers
import (
"fmt"
"gestion/db"
"log"
"net/http"
"net/url"
"strconv"
"github.com/gin-gonic/gin"
@@ -127,3 +129,49 @@ func GetCommandNavigationLinks(c *gin.Context) {
"navigation_links": links,
})
}
// GetLivreurNavLink retourne le lien Waze App pour une livraison assignée au livreur connecté
// GET /api/v1/livreur/deliveries/:id/nav-link
func GetLivreurNavLink(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
username := c.GetString("username")
commandID, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"})
return
}
command, err := database.GetCommandByID(commandID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
return
}
livreurAssign, _ := command["livreur_assign"].(string)
if livreurAssign != username {
c.JSON(http.StatusForbidden, gin.H{"error": "Cette commande ne vous est pas assignée"})
return
}
// Priorité : coordonnées GPS de la destination
var wazeLink string
destLat, hasLat := command["dest_latitude"].(float64)
destLon, hasLon := command["dest_longitude"].(float64)
if hasLat && hasLon && destLat != 0 && destLon != 0 {
wazeLink = fmt.Sprintf("waze://?ll=%.6f,%.6f&navigate=yes", destLat, destLon)
log.Printf("🗺️ [NAV_LINK] Lien coords pour cmd %d: %s", commandID, wazeLink)
} else if adresse, _ := command["adresse"].(string); adresse != "" {
wazeLink = fmt.Sprintf("waze://?q=%s&navigate=yes", url.QueryEscape(adresse))
log.Printf("🗺️ [NAV_LINK] Lien adresse pour cmd %d: %s", commandID, wazeLink)
} else {
c.JSON(http.StatusNotFound, gin.H{"error": "Aucune destination disponible pour cette commande"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"command_id": commandID,
"waze_app": wazeLink,
})
}
+2 -2
View File
@@ -83,7 +83,7 @@ func GetMyCompletedOrders(c *gin.Context) {
response["client_stats"] = gin.H{
"username": client.Username,
"total_commands": client.Command,
"points": client.Point,
"points_extra": client.PointsExtra,
"pool_points": poolPoints,
"pool_names": poolNames,
"penalties": client.Amende,
@@ -169,7 +169,7 @@ func GetMyCompletedOrdersWithItems(c *gin.Context) {
"prenom": client.Prenom,
"telephone": client.Telephone,
"total_commands": client.Command,
"points": client.Point,
"points_extra": client.PointsExtra,
"penalties": client.Amende,
}
}
-182
View File
@@ -9,54 +9,6 @@ import (
"github.com/gin-gonic/gin"
)
// RegisterPushToken enregistre le push token Expo d'un client
// POST /api/v1/push-token
func RegisterPushToken(c *gin.Context) {
clientID := c.GetInt("client_id")
if clientID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
var req struct {
PushToken string `json:"push_token" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.SaveClientPushToken(clientID, req.PushToken); err != nil {
log.Printf("❌ [PUSH_TOKEN] Erreur sauvegarde token pour client %d: %v", clientID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"})
return
}
log.Printf("✅ [PUSH_TOKEN] Token enregistré pour client %d", clientID)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// UnregisterPushToken supprime le push token d'un client (au logout)
// DELETE /api/v1/push-token
func UnregisterPushToken(c *gin.Context) {
clientID := c.GetInt("client_id")
if clientID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteClientPushToken(clientID); err != nil {
log.Printf("❌ [PUSH_TOKEN] Erreur suppression token pour client %d: %v", clientID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"})
return
}
log.Printf("✅ [PUSH_TOKEN] Token supprimé pour client %d", clientID)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// GetClientNotifications retourne les notifications du client connecté
// GET /api/v1/notifications
func GetClientNotifications(c *gin.Context) {
@@ -107,54 +59,6 @@ func GetClientNotifications(c *gin.Context) {
})
}
// RegisterLivreurPushToken enregistre le push token Expo d'un livreur
// POST /api/v1/livreur/push-token
func RegisterLivreurPushToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
var req struct {
PushToken string `json:"push_token" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.SaveUserPushToken(username, req.PushToken); err != nil {
log.Printf("❌ [LIVREUR_PUSH_TOKEN] Erreur sauvegarde token pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"})
return
}
log.Printf("✅ [LIVREUR_PUSH_TOKEN] Token enregistré pour livreur %s", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// UnregisterLivreurPushToken supprime le push token d'un livreur (au logout)
// DELETE /api/v1/livreur/push-token
func UnregisterLivreurPushToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteUserPushToken(username); err != nil {
log.Printf("❌ [LIVREUR_PUSH_TOKEN] Erreur suppression token pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"})
return
}
log.Printf("✅ [LIVREUR_PUSH_TOKEN] Token supprimé pour livreur %s", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// GetLivreurNotifications retourne les notifications du livreur connecté
// GET /api/v1/livreur/notifications
func GetLivreurNotifications(c *gin.Context) {
@@ -245,92 +149,6 @@ func MarkLivreurNotificationsRead(c *gin.Context) {
})
}
// RegisterAdminPushToken enregistre le push token d'un admin
// POST /api/v2/admin/protected/push-token
func RegisterAdminPushToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
var req struct {
PushToken string `json:"push_token" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.SaveUserPushToken(username, req.PushToken); err != nil {
log.Printf("❌ [ADMIN_PUSH_TOKEN] Erreur sauvegarde pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"})
return
}
log.Printf("✅ [ADMIN_PUSH_TOKEN] Token enregistré pour admin %s", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// UnregisterAdminPushToken supprime le push token d'un admin (au logout)
// DELETE /api/v2/admin/protected/push-token
func UnregisterAdminPushToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteUserPushToken(username); err != nil {
log.Printf("❌ [ADMIN_PUSH_TOKEN] Erreur suppression pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"})
return
}
log.Printf("✅ [ADMIN_PUSH_TOKEN] Token supprimé pour admin %s", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// RegisterCabinePushToken enregistre le push token d'un agent cabine
// POST /api/v1/cabine/push-token
func RegisterCabinePushToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
var req struct {
PushToken string `json:"push_token" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.SaveUserPushToken(username, req.PushToken); err != nil {
log.Printf("❌ [CABINE_PUSH_TOKEN] Erreur sauvegarde pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"})
return
}
log.Printf("✅ [CABINE_PUSH_TOKEN] Token enregistré pour cabine %s", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// UnregisterCabinePushToken supprime le push token d'un agent cabine (au logout)
// DELETE /api/v1/cabine/push-token
func UnregisterCabinePushToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteUserPushToken(username); err != nil {
log.Printf("❌ [CABINE_PUSH_TOKEN] Erreur suppression pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"})
return
}
log.Printf("✅ [CABINE_PUSH_TOKEN] Token supprimé pour cabine %s", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// MarkNotificationsRead marque toutes les notifications comme lues
// POST /api/v1/notifications/read
func MarkNotificationsRead(c *gin.Context) {
+14 -13
View File
@@ -295,8 +295,8 @@ func ValidateBasket(c *gin.Context) {
var req struct {
DeliveryAddress string `json:"delivery_address" binding:"required"`
UseReferralBalance bool `json:"use_referral_balance"`
PaymentMethod string `json:"payment_method"` // "cash" (défaut) ou "crypto"
PayCurrency string `json:"pay_currency"` // ex: "btc", "eth", "ltc" (requis si crypto)
PaymentMethod string `json:"payment_method"` // "cash" (défaut) ou "crypto"
PayCurrency string `json:"pay_currency"` // ex: "btc", "eth", "ltc" (requis si crypto)
}
if err := c.ShouldBindJSON(&req); err != nil || req.DeliveryAddress == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse de livraison requise"})
@@ -310,6 +310,12 @@ func ValidateBasket(c *gin.Context) {
}
req.DeliveryAddress = cmd.DeliveryAddress
// Vérifier que le client a lié son compte Telegram
if _, linked, err := database.GetClientTelegramChatID(usernameStr); err != nil || !linked {
c.JSON(http.StatusForbidden, gin.H{"error": "Vous devez lier votre compte Telegram avant de commander"})
return
}
log.Printf("🛒 [CHECKOUT] Début checkout pour: %s", usernameStr)
// ============================================
@@ -430,7 +436,7 @@ func ValidateBasket(c *gin.Context) {
command, err := database.CreateCommandWithAddress(usernameStr, req.DeliveryAddress)
if err != nil {
if referralUsed > 0 {
_ = database.RestoreReferralBalance(usernameStr, referralUsed)
_ = database.CreditClientReferral(usernameStr, referralUsed)
}
log.Printf("❌ [CHECKOUT] Erreur création commande: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création commande"})
@@ -464,7 +470,7 @@ func ValidateBasket(c *gin.Context) {
// Annuler la commande et restaurer le panier / parrainage
_ = database.CancelCryptoCommand(commandID)
if referralUsed > 0 {
_ = database.RestoreReferralBalance(usernameStr, referralUsed)
_ = database.CreditClientReferral(usernameStr, referralUsed)
}
log.Printf("❌ [CHECKOUT] Erreur création paiement NowPayments: %v", err)
c.JSON(http.StatusBadGateway, gin.H{"error": "Impossible d'initier le paiement crypto"})
@@ -520,14 +526,9 @@ func ValidateBasket(c *gin.Context) {
if err == nil {
log.Printf("📍 [CHECKOUT] Adresse géocodée: %.6f,%.6f", location.Latitude, location.Longitude)
activeLivreurs, err := database.GetAllActiveDeliveryPersons()
if err == nil && len(activeLivreurs) > 0 {
log.Printf("🚚 [CHECKOUT] %d livreurs actifs disponibles", len(activeLivreurs))
usernames := make([]string, len(activeLivreurs))
for i, livreur := range activeLivreurs {
usernames[i] = livreur.Username
}
usernames, errEligible := database.GetEligibleDeliverymenForCommand(commandID)
if errEligible == nil && len(usernames) > 0 {
log.Printf("🚚 [CHECKOUT] %d livreur(s) éligible(s) disponibles", len(usernames))
nearest, err := geoService.FindNearestDeliveryPersonFast(services.Coordinates{
Latitude: location.Latitude,
@@ -599,7 +600,7 @@ func ValidateBasket(c *gin.Context) {
log.Printf("⚠️ [CHECKOUT] Aucun livreur trouvé: %v", err)
}
} else {
log.Printf("⚠️ [CHECKOUT] Aucun livreur actif disponible")
log.Printf("⚠️ [CHECKOUT] Aucun livreur éligible disponible")
}
} else {
log.Printf("⚠️ [CHECKOUT] Erreur géocodage: %v", err)
+1 -37
View File
@@ -12,7 +12,6 @@ import (
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gabriel-vasile/mimetype"
"github.com/gin-gonic/gin"
@@ -654,41 +653,12 @@ func UpdateProduct(c *gin.Context) {
log.Printf("🔄 [UpdateProduct] %s met à jour produit #%d", username, id)
// ✅ UPDATE PRODUIT
updateQuery := `
UPDATE products
SET name = $1, category = $2, description = $3, stock = $4, unit = $5, updated_at = $6
WHERE id = $7
`
_, err = database.Exec(updateQuery,
updateData.Name,
updateData.Category,
updateData.Description,
updateData.Stock,
updateData.Unit,
time.Now(),
id,
)
if err != nil {
if err := database.UpdateProduct(id, updateData.Name, updateData.Category, updateData.Description, updateData.Unit, updateData.Stock, updateData.Prices); err != nil {
log.Printf("❌ [UpdateProduct] Erreur UPDATE: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise à jour"})
return
}
// ✅ UPDATE PRIX
database.Exec(`DELETE FROM product_prices WHERE product_id = $1`, id)
for _, price := range updateData.Prices {
_, err := database.Exec(`
INSERT INTO product_prices (product_id, quantity, price)
VALUES ($1, $2, $3)
`, id, price.Quantity, price.Price)
if err != nil {
log.Printf("❌ [UpdateProduct] Erreur prix: %v", err)
}
}
// ✅ RÉCUPÉRER LE PRODUIT MIS À JOUR
updatedProduct, _ := database.GetProductByID(id)
media, _ := database.GetMediaByProductID(id)
@@ -702,10 +672,6 @@ func UpdateProduct(c *gin.Context) {
})
}
// ============================================
// DELETE MEDIA - VERSION SÉCURISÉE
// ============================================
func DeleteMedia(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -756,8 +722,6 @@ func DeleteMedia(c *gin.Context) {
})
}
// handlers/product_handlers_SECURED.go
func UploadMedia(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
+1 -1
View File
@@ -962,7 +962,7 @@ func ResetClientPointAdmin(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
extraPoolKey := ""
if req.Pool >= 2 {
if req.Pool >= 0 {
if settings, err := database.GetSettings(); err == nil && req.Pool < len(settings.PointsPools) {
extraPoolKey = settings.PointsPools[req.Pool].Key
}
+30 -14
View File
@@ -2,8 +2,11 @@ package handlers
import (
"gestion/db"
"gestion/models"
"gestion/services"
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
)
@@ -27,18 +30,18 @@ func GetPublicSettings(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"penalties_enabled": settings.PenaltiesEnabled,
"show_amende_score": settings.ShowAmendeScore,
"points_enabled": settings.PointsEnabled,
"points_separated": len(settings.PointsPools) > 1,
"pool_names": poolNames,
"pool_keys": poolKeys,
"referral_enabled": settings.ReferralEnabled,
"delivery_schedule": settings.DeliverySchedule,
"crypto_payment_enabled": settings.CryptoPaymentEnabled,
"crypto_only": settings.CryptoOnly,
"nowpayments_currencies": settings.NowPaymentsCurrencies,
"success": true,
"penalties_enabled": settings.PenaltiesEnabled,
"show_amende_score": settings.ShowAmendeScore,
"points_enabled": settings.PointsEnabled,
"points_separated": len(settings.PointsPools) > 1,
"pool_names": poolNames,
"pool_keys": poolKeys,
"referral_enabled": settings.ReferralEnabled,
"delivery_schedule": settings.DeliverySchedule,
"crypto_payment_enabled": settings.CryptoPaymentEnabled,
"crypto_only": settings.CryptoOnly,
"nowpayments_currencies": settings.NowPaymentsCurrencies,
})
}
@@ -60,7 +63,7 @@ func GetSettings(c *gin.Context) {
func UpdateSettings(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
var req db.AppSettings
var req models.AppSettings
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Paramètres invalides"})
return
@@ -72,7 +75,20 @@ func UpdateSettings(c *gin.Context) {
return
}
log.Printf("✅ [SETTINGS] Mise à jour: penalties=%v, pools=%d", req.PenaltiesEnabled, len(req.PointsPools))
// Recharger le service Telegram si le token/username a changé
if services.TelegramBot != nil {
services.TelegramBot.Reload(req.TelegramBotToken, req.TelegramBotUsername)
if req.TelegramBotToken != "" {
log.Printf("✅ [SETTINGS] Service Telegram rechargé (username: %s)", req.TelegramBotUsername)
if webhookURL := os.Getenv("TELEGRAM_WEBHOOK_URL"); webhookURL != "" {
if err := services.TelegramBot.SetWebhook(webhookURL); err != nil {
log.Printf("⚠️ [SETTINGS] Erreur enregistrement webhook Telegram: %v", err)
} else {
log.Printf("✅ [SETTINGS] Webhook Telegram enregistré: %s", webhookURL)
}
}
}
}
c.JSON(http.StatusOK, gin.H{"success": true, "settings": req})
}
+292
View File
@@ -0,0 +1,292 @@
package handlers
import (
"gestion/db"
"gestion/models"
"gestion/services"
"log"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func TelegramWebhook(c *gin.Context) {
// Vérification du secret webhook
secret := c.GetHeader("X-Telegram-Bot-Api-Secret-Token")
if services.TelegramBot == nil || !services.TelegramBot.ValidateWebhookSecret(secret) {
c.Status(http.StatusUnauthorized)
return
}
var update models.TgUpdate
if err := c.ShouldBindJSON(&update); err != nil {
c.Status(http.StatusBadRequest)
return
}
if update.Message == nil {
c.Status(http.StatusOK)
return
}
text := strings.TrimSpace(update.Message.Text)
chatID := update.Message.Chat.ID
// Commande /start <token> — liaison de compte
if token, ok := strings.CutPrefix(text, "/start "); ok {
token = strings.TrimSpace(token)
handleLinkAccount(c, token, chatID)
return
}
// Commande /start sans token — message d'accueil
if text == "/start" {
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID,
"👋 <b>Bienvenue !</b>\n\nPour lier votre compte, générez un token depuis l'application et envoyez <code>/start &lt;token&gt;</code>.")
}
}
c.Status(http.StatusOK)
}
func handleLinkAccount(c *gin.Context, token string, chatID int64) {
if token == "" {
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID, "❌ Token manquant. Générez un nouveau token depuis l'application.")
}
c.Status(http.StatusOK)
return
}
database := c.MustGet("database").(*db.Database)
username, role, err := db.ValidateAndConsumeLinkToken(token)
if err != nil {
log.Printf("⚠️ [TELEGRAM_LINK] Token invalide: %v", err)
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID, "❌ Token invalide ou expiré. Générez un nouveau token depuis l'application.")
}
c.Status(http.StatusOK)
return
}
// Enregistrer le chat_id selon le rôle
var saveErr error
switch role {
case "client":
saveErr = database.SaveClientTelegramChatID(username, chatID)
default:
saveErr = database.SaveUserTelegramChatID(username, chatID)
}
if saveErr != nil {
log.Printf("❌ [TELEGRAM_LINK] Erreur sauvegarde chat_id pour %s (%s): %v", username, role, saveErr)
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID, "❌ Une erreur est survenue. Réessayez.")
}
c.Status(http.StatusOK)
return
}
log.Printf("✅ [TELEGRAM_LINK] Compte %s (%s) lié au chat_id %d", username, role, chatID)
if services.TelegramBot != nil {
services.TelegramBot.SendMessage(chatID,
"✅ <b>Compte lié avec succès !</b>\n\nVous recevrez désormais vos notifications ici.")
}
c.Status(http.StatusOK)
}
func GenerateClientLinkToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"})
return
}
token, err := db.GenerateLinkToken(username, "client")
if err != nil {
log.Printf("❌ [TELEGRAM] Erreur génération token pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
"message": "/start " + token,
"expires_in": 600,
})
}
func GenerateLivreurLinkToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"})
return
}
token, err := db.GenerateLinkToken(username, "livreur")
if err != nil {
log.Printf("❌ [TELEGRAM] Erreur génération token pour livreur %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
"message": "/start " + token,
"expires_in": 600,
})
}
func GenerateAdminLinkToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"})
return
}
// Récupérer le rôle réel depuis la DB
database := c.MustGet("database").(*db.Database)
user, err := database.GetUserByUsername(username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Utilisateur introuvable"})
return
}
token, err := db.GenerateLinkToken(username, user.Role)
if err != nil {
log.Printf("❌ [TELEGRAM] Erreur génération token pour admin %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
"message": "/start " + token,
"expires_in": 600,
})
}
// GET /api/v1/telegram/status
func GetClientTelegramStatus(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
_, linked, err := database.GetClientTelegramChatID(username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur vérification"})
return
}
c.JSON(http.StatusOK, gin.H{
"linked": linked,
"enabled": services.TelegramBot != nil && services.TelegramBot.IsConfigured(),
})
}
// GET /api/v1/livreur/telegram/status
func GetLivreurTelegramStatus(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
_, linked, err := database.GetUserTelegramChatID(username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur vérification"})
return
}
c.JSON(http.StatusOK, gin.H{
"linked": linked,
"enabled": services.TelegramBot != nil && services.TelegramBot.IsConfigured(),
})
}
// ============================================
// DÉLIAISON TELEGRAM
// ============================================
// DELETE /api/v1/telegram/unlink
func UnlinkClientTelegram(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteClientTelegramChatID(username); err != nil {
log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur déliaison"})
return
}
log.Printf("✅ [TELEGRAM_UNLINK] Compte client %s délié", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// DELETE /api/v1/livreur/telegram/unlink
func UnlinkLivreurTelegram(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteUserTelegramChatID(username); err != nil {
log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur déliaison"})
return
}
log.Printf("✅ [TELEGRAM_UNLINK] Compte livreur %s délié", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// DELETE /api/v2/admin/protected/telegram/unlink
func UnlinkAdminTelegram(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteUserTelegramChatID(username); err != nil {
log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur déliaison"})
return
}
log.Printf("✅ [TELEGRAM_UNLINK] Compte admin %s délié", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
+17 -35
View File
@@ -16,6 +16,7 @@ import (
"gestion/db"
"gestion/models"
"gestion/utils"
)
// ============================================
@@ -99,12 +100,12 @@ func UpdateMyProfile(c *gin.Context) {
// Mise à jour du téléphone
if req.Telephone != "" && req.Telephone != client.Telephone {
if !validatePhoneNumber(req.Telephone) {
if !utils.ValidatePhoneNumber(req.Telephone) {
log.Printf("❌ [UPDATE_MY_PROFILE] Téléphone invalide: %s", req.Telephone)
c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
return
}
normalizedPhone := normalizePhoneNumber(req.Telephone)
normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
// Vérifier que le téléphone n'est pas déjà utilisé
if existingClient, _ := database.GetClientByTelephone(normalizedPhone); existingClient != nil && existingClient.ID != clientID {
@@ -202,8 +203,8 @@ func UpdateClientByAdmin(c *gin.Context) {
}
// ✅ LOG DEBUG - État initial
log.Printf("📊 [UPDATE_CLIENT_ADMIN] État initial - Command: %d, Point: %d, PointZipette: %d, Amende: %.2f",
client.Command, client.Point, client.PointZipette, client.Amende)
log.Printf("📊 [UPDATE_CLIENT_ADMIN] État initial - Command: %d, Amende: %.2f",
client.Command, client.Amende)
// Vérifier si des modifications sont demandées
hasChanges := false
@@ -254,12 +255,12 @@ func UpdateClientByAdmin(c *gin.Context) {
// Mise à jour du téléphone
if req.Telephone != "" && req.Telephone != client.Telephone {
if !validatePhoneNumber(req.Telephone) {
if !utils.ValidatePhoneNumber(req.Telephone) {
log.Printf("❌ [UPDATE_CLIENT_ADMIN] Téléphone invalide: %s", req.Telephone)
c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
return
}
normalizedPhone := normalizePhoneNumber(req.Telephone)
normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
// Vérifier que le téléphone n'est pas déjà utilisé
if existingClient, _ := database.GetClientByTelephone(normalizedPhone); existingClient != nil && existingClient.ID != clientID {
@@ -272,29 +273,12 @@ func UpdateClientByAdmin(c *gin.Context) {
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Téléphone modifié: %s", normalizedPhone)
}
// ✅ CORRECTION: Mise à jour du compteur de commandes (Admin uniquement)
// Vérifier explicitement si le champ est présent (même si valeur = 0)
if req.Command != nil && *req.Command != client.Command {
client.Command = *req.Command
hasChanges = true
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Commandes modifiées: %d → %d", client.Command, *req.Command)
}
// ✅ CORRECTION: Mise à jour des points weed/hash (Admin uniquement)
if req.Point != nil && *req.Point != client.Point {
client.Point = *req.Point
hasChanges = true
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Points Weed modifiés: %d → %d", client.Point, *req.Point)
}
// ✅ CORRECTION CRITIQUE: Mise à jour des points zipette (Admin uniquement)
if req.PointsZipette != nil && *req.PointsZipette != client.PointZipette {
client.PointZipette = *req.PointsZipette
hasChanges = true
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Points Zipette modifiés: %d → %d", client.PointZipette, *req.PointsZipette)
}
// ✅ CORRECTION: Mise à jour des amendes (Admin uniquement)
if req.Amende != nil && *req.Amende != client.Amende {
client.Amende = *req.Amende
hasChanges = true
@@ -310,9 +294,8 @@ func UpdateClientByAdmin(c *gin.Context) {
return
}
// ✅ LOG DEBUG - État avant sauvegarde
log.Printf("📊 [UPDATE_CLIENT_ADMIN] État avant save - Command: %d, Point: %d, PointZipette: %d, Amende: %.2f",
client.Command, client.Point, client.PointZipette, client.Amende)
log.Printf("📊 [UPDATE_CLIENT_ADMIN] État avant save - Command: %d, Amende: %.2f",
client.Command, client.Amende)
// Sauvegarder les modifications
if err := database.UpdateClient(client); err != nil {
@@ -446,15 +429,14 @@ func UpdateUserByAdmin(c *gin.Context) {
func sanitizeClient(client *models.Client) gin.H {
return gin.H{
"id": client.ID,
"username": client.Username,
"nom": client.Nom,
"prenom": client.Prenom,
"telephone": client.Telephone,
"command": client.Command,
"point": client.Point,
"points_zipette": client.PointZipette, // ✅ AJOUTÉ
"amende": client.Amende,
"id": client.ID,
"username": client.Username,
"nom": client.Nom,
"prenom": client.Prenom,
"telephone": client.Telephone,
"command": client.Command,
"points_extra": client.PointsExtra,
"amende": client.Amende,
}
}
@@ -352,6 +352,12 @@ func StartDelivery(c *gin.Context) {
fmt.Sprintf("Livraison démarrée par %s", usernameStr),
usernameStr)
// Notifier le client
if clientUsername, _ := command["username"].(string); clientUsername != "" {
msg := fmt.Sprintf("🛵 Votre commande #%d est en route !", commandID)
database.NotifyClient(clientUsername, commandID, "en_route", msg)
}
log.Printf("✅ [START] Livraison %d démarrée", commandID)
c.JSON(http.StatusOK, gin.H{
+2 -2
View File
@@ -1,7 +1,7 @@
package handlers
import (
"gestion/db"
"gestion/models"
"regexp"
)
@@ -37,7 +37,7 @@ type zoneCheckResult struct {
// Les zones sont lues depuis la DB (settings.PostalZones).
// Code postal introuvable → OK = false (refus).
// Code postal hors de toutes les zones → OK = false (refus).
func checkDeliveryZone(deliveryAddress string, total float64, zones []db.PostalZone) zoneCheckResult {
func checkDeliveryZone(deliveryAddress string, total float64, zones []models.PostalZone) zoneCheckResult {
code := extractPostalCode(deliveryAddress)
if code == "" {
return zoneCheckResult{PostalCode: "", ZoneName: "inconnue", MinAmount: 0, OK: false}