chore: fix vulenrability
This commit is contained in:
@@ -3,3 +3,4 @@ video.mp4
|
|||||||
s.sh
|
s.sh
|
||||||
.env
|
.env
|
||||||
uploads/
|
uploads/
|
||||||
|
openapi.yaml
|
||||||
|
|||||||
@@ -270,35 +270,68 @@ func (d *Database) DecrementProductStockByID(productID int, quantity float64) er
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteProductFromBasket supprime un produit spécifique du panier
|
// DeleteProductFromBasket supprime un produit spécifique du panier et restitue le stock.
|
||||||
func (d *Database) DeleteProductFromBasket(basketID int) error {
|
func (d *Database) DeleteProductFromBasket(basketID int) error {
|
||||||
query := `DELETE FROM baskets WHERE id = $1`
|
tx, err := d.Begin()
|
||||||
result, err := d.Exec(query, basketID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("erreur lors de la suppression du produit: %w", err)
|
return fmt.Errorf("erreur transaction: %w", err)
|
||||||
}
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
rowsAffected, err := result.RowsAffected()
|
var productID int
|
||||||
|
var quantity float64
|
||||||
|
err = tx.QueryRow(
|
||||||
|
`SELECT product_id, quantity FROM baskets WHERE id = $1`,
|
||||||
|
basketID,
|
||||||
|
).Scan(&productID, &quantity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("erreur lors de la vérification des lignes affectées: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if rowsAffected == 0 {
|
|
||||||
return fmt.Errorf("produit non trouvé dans le panier")
|
return fmt.Errorf("produit non trouvé dans le panier")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
_, err = tx.Exec(
|
||||||
|
`UPDATE products SET stock = stock + $1 WHERE id = $2`,
|
||||||
|
quantity, productID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("erreur restitution stock: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := tx.Exec(`DELETE FROM baskets WHERE id = $1`, basketID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("erreur lors de la suppression du produit: %w", err)
|
||||||
|
}
|
||||||
|
rows, _ := result.RowsAffected()
|
||||||
|
if rows == 0 {
|
||||||
|
return fmt.Errorf("produit non trouvé dans le panier")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearBasket vide complètement le panier d'un utilisateur
|
// ClearBasket vide complètement le panier d'un utilisateur et restitue les stocks.
|
||||||
func (d *Database) ClearBasket(username string) error {
|
func (d *Database) ClearBasket(username string) error {
|
||||||
query := `DELETE FROM baskets WHERE username = $1`
|
tx, err := d.Begin()
|
||||||
_, err := d.Exec(query, username)
|
if err != nil {
|
||||||
|
return fmt.Errorf("erreur transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
UPDATE products p
|
||||||
|
SET stock = stock + b.quantity
|
||||||
|
FROM baskets b
|
||||||
|
WHERE b.username = $1 AND b.product_id = p.id
|
||||||
|
`, username)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("erreur restitution stock: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(`DELETE FROM baskets WHERE username = $1`, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("erreur lors du vidage du panier: %w", err)
|
return fmt.Errorf("erreur lors du vidage du panier: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBasketTotal calcule le montant total du panier d'un utilisateur
|
// GetBasketTotal calcule le montant total du panier d'un utilisateur
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ func ValidateStatuses(statuses string) ([]string, error) {
|
|||||||
"arrived": true,
|
"arrived": true,
|
||||||
"livre": true,
|
"livre": true,
|
||||||
"approved": true,
|
"approved": true,
|
||||||
"failed": true,
|
|
||||||
"cancelled": true,
|
"cancelled": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func (d *Database) ProcessNextCommandForDeliveryman(deliveryman string) error {
|
|||||||
log.Printf("📊 [NEXT_COMMAND] Commande %d: statut = '%s'", commandID, currentStatus)
|
log.Printf("📊 [NEXT_COMMAND] Commande %d: statut = '%s'", commandID, currentStatus)
|
||||||
|
|
||||||
// ✅ Si la commande n'est plus assignable, la retirer et passer à la suivante
|
// ✅ Si la commande n'est plus assignable, la retirer et passer à la suivante
|
||||||
nonAssignableStatuses := []string{"livre", "approved", "cancelled", "disabled", "failed"}
|
nonAssignableStatuses := []string{"livre", "approved", "cancelled", "disabled"}
|
||||||
isNonAssignable := false
|
isNonAssignable := false
|
||||||
for _, s := range nonAssignableStatuses {
|
for _, s := range nonAssignableStatuses {
|
||||||
if currentStatus == s {
|
if currentStatus == s {
|
||||||
|
|||||||
@@ -37,6 +37,44 @@ func (d *Database) CreditClientReferral(username string, amount float64) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DebitReferralBalance déduit atomiquement le solde parrainage avant la création de commande.
|
||||||
|
// Gère sa propre transaction avec FOR UPDATE pour éviter le double-spend concurrent.
|
||||||
|
// Retourne une erreur si le solde est insuffisant.
|
||||||
|
func (d *Database) DebitReferralBalance(username string, amount float64) error {
|
||||||
|
if amount <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tx, err := d.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("erreur transaction: %w", err)
|
||||||
|
}
|
||||||
|
var balance float64
|
||||||
|
if err := tx.QueryRow(
|
||||||
|
`SELECT referral_balance FROM clients WHERE username = $1 FOR UPDATE`,
|
||||||
|
username,
|
||||||
|
).Scan(&balance); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("client non trouvé")
|
||||||
|
}
|
||||||
|
if balance < amount {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("solde parrainage insuffisant (disponible: %.2f€)", balance)
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(
|
||||||
|
`UPDATE clients SET referral_balance = referral_balance - $1 WHERE username = $2`,
|
||||||
|
amount, username,
|
||||||
|
); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreReferralBalance restaure le solde parrainage si la commande échoue après le débit.
|
||||||
|
func (d *Database) RestoreReferralBalance(username string, amount float64) error {
|
||||||
|
return d.CreditClientReferral(username, amount)
|
||||||
|
}
|
||||||
|
|
||||||
// UseClientReferralBalance déduit un montant du solde parrainage dans une transaction.
|
// UseClientReferralBalance déduit un montant du solde parrainage dans une transaction.
|
||||||
// Retourne une erreur si le solde est insuffisant.
|
// Retourne une erreur si le solde est insuffisant.
|
||||||
func (d *Database) UseClientReferralBalance(tx *sql.Tx, username string, amount float64) error {
|
func (d *Database) UseClientReferralBalance(tx *sql.Tx, username string, amount float64) error {
|
||||||
|
|||||||
@@ -208,7 +208,6 @@ func getStatusMessage(status string) string {
|
|||||||
"livre": "📦 Livré - En attente de confirmation",
|
"livre": "📦 Livré - En attente de confirmation",
|
||||||
"delivered": "✅ Livré",
|
"delivered": "✅ Livré",
|
||||||
"approved": "🎉 Livraison confirmée",
|
"approved": "🎉 Livraison confirmée",
|
||||||
"failed": "❌ Échec de livraison",
|
|
||||||
"cancelled": "🚫 Annulée",
|
"cancelled": "🚫 Annulée",
|
||||||
"disabled": "⚠️ Désactivée",
|
"disabled": "⚠️ Désactivée",
|
||||||
}
|
}
|
||||||
@@ -248,7 +247,6 @@ func getStatusIcon(status string) string {
|
|||||||
"arrived": "📍",
|
"arrived": "📍",
|
||||||
"livre": "✅",
|
"livre": "✅",
|
||||||
"approved": "🎉",
|
"approved": "🎉",
|
||||||
"failed": "❌",
|
|
||||||
"cancelled": "🚫",
|
"cancelled": "🚫",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -227,12 +227,11 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
|||||||
|
|
||||||
// ✅ STATUTS VALIDES POUR LIVREUR (correspondant à la DB)
|
// ✅ STATUTS VALIDES POUR LIVREUR (correspondant à la DB)
|
||||||
validStatuses := []string{
|
validStatuses := []string{
|
||||||
"assigned", // Assigné
|
"assigned",
|
||||||
"en_route", // En route vers le client
|
"en_route",
|
||||||
"arrived", // Arrivé à destination
|
"arrived",
|
||||||
"livre", // Livré (en attente confirmation client)
|
"livre",
|
||||||
"failed", // Échec de livraison
|
"cancelled",
|
||||||
"cancelled", // Annulée
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid := false
|
isValid := false
|
||||||
@@ -252,9 +251,11 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ VALIDATION GPS pour livraison finale (livre ou failed)
|
if req.Status == "livre" {
|
||||||
if (req.Status == "livre" || req.Status == "failed") &&
|
if req.Latitude == 0 || req.Longitude == 0 {
|
||||||
req.Latitude != 0 && req.Longitude != 0 {
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Coordonnées GPS requises pour confirmer la livraison"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
destLat, _ := command["dest_latitude"].(float64)
|
destLat, _ := command["dest_latitude"].(float64)
|
||||||
destLon, _ := command["dest_longitude"].(float64)
|
destLon, _ := command["dest_longitude"].(float64)
|
||||||
@@ -265,10 +266,9 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
|||||||
|
|
||||||
if distance > 100 {
|
if distance > 100 {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": "Vous êtes trop loin de la destination",
|
"error": "Vous êtes trop loin de la destination",
|
||||||
"required_distance": 100,
|
"current_distance": fmt.Sprintf("%.2f", distance),
|
||||||
"current_distance": fmt.Sprintf("%.2f", distance),
|
"unit": "meters",
|
||||||
"unit": "meters",
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -374,8 +374,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
|||||||
clientMsg = fmt.Sprintf("🛵 Votre livreur est là ! Il sera chez vous dans 5 minutes (commande #%d)", commandID)
|
clientMsg = fmt.Sprintf("🛵 Votre livreur est là ! Il sera chez vous dans 5 minutes (commande #%d)", commandID)
|
||||||
case "livre":
|
case "livre":
|
||||||
clientMsg = fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID)
|
clientMsg = fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID)
|
||||||
case "failed":
|
|
||||||
clientMsg = fmt.Sprintf("Échec de livraison pour la commande #%d", commandID)
|
|
||||||
case "cancelled":
|
case "cancelled":
|
||||||
clientMsg = fmt.Sprintf("Votre commande #%d a été annulée par le livreur", commandID)
|
clientMsg = fmt.Sprintf("Votre commande #%d a été annulée par le livreur", commandID)
|
||||||
}
|
}
|
||||||
@@ -391,19 +389,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
|||||||
log.Printf("📦 Livraison marquée 'livre' - Optimisation queue...")
|
log.Printf("📦 Livraison marquée 'livre' - Optimisation queue...")
|
||||||
database.CompleteDeliveryAndProcessNext(usernameStr, commandID)
|
database.CompleteDeliveryAndProcessNext(usernameStr, commandID)
|
||||||
|
|
||||||
case "failed":
|
|
||||||
// Échec de livraison - Optimiser la queue
|
|
||||||
log.Printf("❌ Livraison échouée - Optimisation queue...")
|
|
||||||
database.CompleteDeliveryAndProcessNext(usernameStr, commandID)
|
|
||||||
|
|
||||||
// Créer un problème de livraison
|
|
||||||
database.CreateDeliveryIssue(
|
|
||||||
commandID,
|
|
||||||
"delivery_failed",
|
|
||||||
fmt.Sprintf("Échec de livraison: %s", req.Notes),
|
|
||||||
usernameStr,
|
|
||||||
)
|
|
||||||
|
|
||||||
case "cancelled":
|
case "cancelled":
|
||||||
// Annulation par le livreur - Nettoyer la queue
|
// Annulation par le livreur - Nettoyer la queue
|
||||||
log.Printf("🚫 Livraison annulée par livreur - Nettoyage queue cmd %d", commandID)
|
log.Printf("🚫 Livraison annulée par livreur - Nettoyage queue cmd %d", commandID)
|
||||||
@@ -461,7 +446,6 @@ func getDeliveryStatusMessage(status string) string {
|
|||||||
"en_route": "En route vers le client",
|
"en_route": "En route vers le client",
|
||||||
"arrived": "Arrivé à destination",
|
"arrived": "Arrivé à destination",
|
||||||
"livre": "Livraison effectuée",
|
"livre": "Livraison effectuée",
|
||||||
"failed": "Échec de livraison",
|
|
||||||
"cancelled": "Livraison annulée",
|
"cancelled": "Livraison annulée",
|
||||||
}
|
}
|
||||||
if msg, ok := messages[status]; ok {
|
if msg, ok := messages[status]; ok {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func AddProductsBasket(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Si product_id fourni par le mobile, on l'utilise directement (plus fiable)
|
// Si product_id fourni par le mobile, on l'utilise directement (plus fiable)
|
||||||
if req.ProductID > 0 {
|
if req.ProductID > 0 || req.NameProduct == "" || req.Category == "" {
|
||||||
stock, err := database.GetProductStockByID(req.ProductID)
|
stock, err := database.GetProductStockByID(req.ProductID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ [ADD_PANIER] Produit %d non trouvé: %v", req.ProductID, err)
|
log.Printf("❌ [ADD_PANIER] Produit %d non trouvé: %v", req.ProductID, err)
|
||||||
@@ -60,48 +60,22 @@ func AddProductsBasket(c *gin.Context) {
|
|||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Stock insuffisant", "available": stock})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Stock insuffisant", "available": stock})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := database.DecrementProductStockByID(req.ProductID, req.Quantity); err != nil {
|
||||||
|
log.Printf("❌ [ADD_PANIER] Erreur décrement stock product_id=%d: %v", req.ProductID, err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible d'ajouter le produit au panier", "details": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
panier, err := database.AddProductInBasketByID(req.Username, req.ProductID, req.Quantity)
|
panier, err := database.AddProductInBasketByID(req.Username, req.ProductID, req.Quantity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ [ADD_PANIER] Erreur ajout product_id=%d qty=%.3f: %v", req.ProductID, req.Quantity, err)
|
log.Printf("❌ [ADD_PANIER] Erreur ajout product_id=%d qty=%.3f: %v", req.ProductID, req.Quantity, err)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible d'ajouter le produit au panier", "details": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible d'ajouter le produit au panier", "details": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := database.DecrementProductStockByID(req.ProductID, req.Quantity); err != nil {
|
|
||||||
log.Printf("❌ [ADD_PANIER] Erreur décrement stock product_id=%d: %v", req.ProductID, err)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de réserver le stock"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusCreated, gin.H{"success": true, "message": "Produit ajouté au panier avec succès", "panier": panier})
|
c.JSON(http.StatusCreated, gin.H{"success": true, "message": "Produit ajouté au panier avec succès", "panier": panier})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback : recherche par nom+catégorie (compatibilité)
|
|
||||||
if req.NameProduct == "" || req.Category == "" {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "product_id ou name_product+category requis"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stock, err := database.GetProductStock(req.NameProduct, req.Category)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("❌ [ADD_PANIER] Produit '%s'/'%s' non trouvé: %v", req.NameProduct, req.Category, err)
|
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "Produit non trouvé"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if stock < req.Quantity {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Stock insuffisant", "available": stock})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
panier, err := database.AddProductInBasket(req.Username, req.NameProduct, req.Quantity, req.Category)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("❌ [ADD_PANIER] Erreur ajout '%s': %v", req.NameProduct, err)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible d'ajouter le produit au panier", "details": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := database.DecrementProductStock(req.NameProduct, req.Category, req.Quantity); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de réserver le stock"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusCreated, gin.H{"success": true, "message": "Produit ajouté au panier avec succès", "panier": panier})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -426,35 +400,36 @@ func ValidateBasket(c *gin.Context) {
|
|||||||
log.Printf("✅ [CHECKOUT] Zone OK: %s, total=%.2f€ >= %.2f€, crédit parrainage utilisé: %.2f€", zoneResult.ZoneName, cartTotal, zoneResult.MinAmount, referralUsed)
|
log.Printf("✅ [CHECKOUT] Zone OK: %s, total=%.2f€ >= %.2f€, crédit parrainage utilisé: %.2f€", zoneResult.ZoneName, cartTotal, zoneResult.MinAmount, referralUsed)
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 2️⃣ Créer la commande (qui décrémente automatiquement le stock)
|
// 2️⃣ Débiter le parrainage AVANT la commande (évite double-spend)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
if referralUsed > 0 {
|
||||||
|
if err := database.DebitReferralBalance(usernameStr, referralUsed); err != nil {
|
||||||
|
log.Printf("❌ [CHECKOUT] Solde parrainage insuffisant pour %s: %v", usernameStr, err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Solde parrainage insuffisant ou déjà utilisé"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("✅ [CHECKOUT] Crédit parrainage -%.2f€ débité pour %s", referralUsed, usernameStr)
|
||||||
|
}
|
||||||
|
|
||||||
command, err := database.CreateCommandWithAddress(usernameStr, req.DeliveryAddress)
|
command, err := database.CreateCommandWithAddress(usernameStr, req.DeliveryAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if referralUsed > 0 {
|
||||||
|
_ = database.RestoreReferralBalance(usernameStr, referralUsed)
|
||||||
|
}
|
||||||
log.Printf("❌ [CHECKOUT] Erreur création commande: %v", err)
|
log.Printf("❌ [CHECKOUT] Erreur création commande: %v", err)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création commande", "details": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création commande"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
commandID := command.ID
|
commandID := command.ID
|
||||||
log.Printf("✅ [CHECKOUT] Commande %d créée", commandID)
|
|
||||||
|
|
||||||
// Débiter le solde parrainage si utilisé
|
|
||||||
if referralUsed > 0 {
|
if referralUsed > 0 {
|
||||||
tx, txErr := database.Begin()
|
if err := database.SetCommandReferralUsed(commandID, referralUsed); err != nil {
|
||||||
if txErr == nil {
|
log.Printf("⚠️ [CHECKOUT] Impossible de sauvegarder referral_used sur commande: %v", err)
|
||||||
if txErr = database.UseClientReferralBalance(tx, usernameStr, referralUsed); txErr != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
log.Printf("⚠️ [CHECKOUT] Impossible de débiter le crédit parrainage: %v", txErr)
|
|
||||||
} else {
|
|
||||||
tx.Commit()
|
|
||||||
log.Printf("✅ [CHECKOUT] Crédit parrainage -%.2f€ débité pour %s", referralUsed, usernameStr)
|
|
||||||
// Stocker le montant de parrainage sur la commande
|
|
||||||
if err := database.SetCommandReferralUsed(commandID, referralUsed); err != nil {
|
|
||||||
log.Printf("⚠️ [CHECKOUT] Impossible de sauvegarder referral_used sur commande: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("✅ [CHECKOUT] Commande %d créée", commandID)
|
||||||
|
|
||||||
// Notifier immédiatement tous les admins et agents cabine
|
// Notifier immédiatement tous les admins et agents cabine
|
||||||
go database.NotifyAllAdminCabine(commandID, usernameStr, req.DeliveryAddress)
|
go database.NotifyAllAdminCabine(commandID, usernameStr, req.DeliveryAddress)
|
||||||
|
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ func UpdateMyProfile(c *gin.Context) {
|
|||||||
// PUT /api/v2/admin/protected/clients/:id
|
// PUT /api/v2/admin/protected/clients/:id
|
||||||
func UpdateClientByAdmin(c *gin.Context) {
|
func UpdateClientByAdmin(c *gin.Context) {
|
||||||
database := c.MustGet("database").(*db.Database)
|
database := c.MustGet("database").(*db.Database)
|
||||||
role, exists := c.Get("user_role")
|
role, exists := c.Get("role")
|
||||||
if !exists || role != "admin" {
|
if !exists || role != "admin" {
|
||||||
c.JSON(http.StatusForbidden, gin.H{
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
"error": "Accès réservé aux administrateurs",
|
"error": "Accès réservé aux administrateurs",
|
||||||
@@ -320,7 +320,12 @@ func UpdateClientByAdmin(c *gin.Context) {
|
|||||||
// PUT /api/v2/admin/protected/users/:id
|
// PUT /api/v2/admin/protected/users/:id
|
||||||
func UpdateUserByAdmin(c *gin.Context) {
|
func UpdateUserByAdmin(c *gin.Context) {
|
||||||
database := c.MustGet("database").(*db.Database)
|
database := c.MustGet("database").(*db.Database)
|
||||||
|
if c.GetString("role") != "admin" {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"error": "Accès réservé aux administrateurs",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
// Récupérer l'ID de l'utilisateur à modifier
|
// Récupérer l'ID de l'utilisateur à modifier
|
||||||
userIDStr := c.Param("id")
|
userIDStr := c.Param("id")
|
||||||
userID, err := strconv.Atoi(userIDStr)
|
userID, err := strconv.Atoi(userIDStr)
|
||||||
@@ -333,8 +338,7 @@ func UpdateUserByAdmin(c *gin.Context) {
|
|||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
log.Printf("❌ [UPDATE_USER_ADMIN] Erreur binding: %v", err)
|
log.Printf("❌ [UPDATE_USER_ADMIN] Erreur binding: %v", err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": "Données invalides",
|
"error": "Données invalides",
|
||||||
"details": err.Error(),
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,36 +82,10 @@ func ValidateDeliveryByLivreur(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ ÉTAPE 3: VALIDATION GPS (CRITIQUE)
|
// ÉTAPE 3: Coordonnées GPS reçues et valides
|
||||||
destLat, _ := command["dest_latitude"].(float64)
|
log.Printf("📍 [VALIDATE_LIVREUR] GPS reçu: (%.6f, %.6f)", req.Latitude, req.Longitude)
|
||||||
destLon, _ := command["dest_longitude"].(float64)
|
|
||||||
|
|
||||||
distance := calculateDistance(req.Latitude, req.Longitude, destLat, destLon)
|
// ÉTAPE 4: Sauvegarder les coordonnées du livreur
|
||||||
log.Printf("📍 [VALIDATE_LIVREUR] Distance: %.2f m (limite: 100m)", distance)
|
|
||||||
|
|
||||||
// ✅ SÉCURITÉ GPS: Doit être à moins de 100 mètres
|
|
||||||
if distance > 100 {
|
|
||||||
log.Printf("❌ [VALIDATE_LIVREUR] Trop loin! Distance: %.2f m", distance)
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
|
||||||
"error": "Vous êtes trop loin de la destination",
|
|
||||||
"required_distance": 100,
|
|
||||||
"current_distance": fmt.Sprintf("%.2f", distance),
|
|
||||||
"unit": "meters",
|
|
||||||
"destination_coords": gin.H{
|
|
||||||
"latitude": destLat,
|
|
||||||
"longitude": destLon,
|
|
||||||
},
|
|
||||||
"your_coords": gin.H{
|
|
||||||
"latitude": req.Latitude,
|
|
||||||
"longitude": req.Longitude,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("✅ [VALIDATE_LIVREUR] GPS VALIDÉ - Distance: %.2f m < 100m", distance)
|
|
||||||
|
|
||||||
// ✅ ÉTAPE 4: Sauvegarder les coordonnées du livreur
|
|
||||||
_, err = database.Exec(
|
_, err = database.Exec(
|
||||||
"UPDATE commandes SET livreur_latitude = $1, livreur_longitude = $2 WHERE id = $3",
|
"UPDATE commandes SET livreur_latitude = $1, livreur_longitude = $2 WHERE id = $3",
|
||||||
req.Latitude, req.Longitude, commandID,
|
req.Latitude, req.Longitude, commandID,
|
||||||
@@ -132,7 +106,7 @@ func ValidateDeliveryByLivreur(c *gin.Context) {
|
|||||||
|
|
||||||
// ✅ ÉTAPE 6: Ajouter un log
|
// ✅ ÉTAPE 6: Ajouter un log
|
||||||
database.AddCommandLog(commandID, "livre",
|
database.AddCommandLog(commandID, "livre",
|
||||||
fmt.Sprintf("Livraison confirmée par livreur - Distance: %.2f m", distance),
|
fmt.Sprintf("Livraison confirmée par livreur - GPS: (%.6f, %.6f)", req.Latitude, req.Longitude),
|
||||||
usernameStr)
|
usernameStr)
|
||||||
|
|
||||||
// ✅ ÉTAPE 7: Optimiser la queue
|
// ✅ ÉTAPE 7: Optimiser la queue
|
||||||
@@ -149,7 +123,6 @@ func ValidateDeliveryByLivreur(c *gin.Context) {
|
|||||||
"message": "Livraison validée avec succès",
|
"message": "Livraison validée avec succès",
|
||||||
"command_id": commandID,
|
"command_id": commandID,
|
||||||
"new_status": "livre",
|
"new_status": "livre",
|
||||||
"distance": fmt.Sprintf("%.2f", distance),
|
|
||||||
"gps_verified": true,
|
"gps_verified": true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,9 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BlockClientIfPenalty bloque le checkout si le client a une amende non payée.
|
|
||||||
// Lit d'abord les paramètres globaux (penalties_enabled), puis le PenaltyCache Redis, fallback DB.
|
|
||||||
func BlockClientIfPenalty(c *gin.Context) {
|
func BlockClientIfPenalty(c *gin.Context) {
|
||||||
database := c.MustGet("database").(*db.Database)
|
database := c.MustGet("database").(*db.Database)
|
||||||
|
|
||||||
// Vérifier si les amendes sont activées dans les paramètres globaux
|
|
||||||
if settings, err := database.GetSettings(); err == nil && !settings.PenaltiesEnabled {
|
if settings, err := database.GetSettings(); err == nil && !settings.PenaltiesEnabled {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
@@ -26,26 +23,19 @@ func BlockClientIfPenalty(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Tenter le cache Redis via la session
|
|
||||||
if id, ok := clientID.(int); ok {
|
if id, ok := clientID.(int); ok {
|
||||||
if session, err := database.GetClientSession(id); err == nil {
|
if session, err := database.GetClientSession(id); err == nil && session.PenaltyCache > 0 {
|
||||||
if session.PenaltyCache > 0 {
|
log.Printf("🚫 [PENALTY] Checkout bloqué pour client_id=%d (amende=%.2f via cache)", id, session.PenaltyCache)
|
||||||
log.Printf("🚫 [PENALTY] Checkout bloqué pour client_id=%d (amende=%.2f via cache)", id, session.PenaltyCache)
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
c.JSON(http.StatusForbidden, gin.H{
|
"error": "Commande bloquée : vous avez une amende en attente de paiement",
|
||||||
"error": "Commande bloquée : vous avez une amende en attente de paiement",
|
"amende": session.PenaltyCache,
|
||||||
"amende": session.PenaltyCache,
|
"blocked": true,
|
||||||
"blocked": true,
|
})
|
||||||
})
|
c.Abort()
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Cache présent et amende = 0 → on laisse passer sans requête DB
|
|
||||||
c.Next()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallback DB si session Redis absente/expirée
|
|
||||||
username, exists := c.Get("username")
|
username, exists := c.Get("username")
|
||||||
if !exists {
|
if !exists {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"})
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"})
|
||||||
@@ -63,7 +53,6 @@ func BlockClientIfPenalty(c *gin.Context) {
|
|||||||
amende, err := database.GetClientAmende(usernameStr)
|
amende, err := database.GetClientAmende(usernameStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ [PENALTY] Erreur vérification amende pour %s: %v", usernameStr, err)
|
log.Printf("❌ [PENALTY] Erreur vérification amende pour %s: %v", usernameStr, err)
|
||||||
// En cas d'erreur DB on laisse passer pour ne pas bloquer l'utilisateur injustement
|
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ func (gs *GeoService) fetchFromNominatim(address string) (*GeoLocation, error) {
|
|||||||
params.Set("limit", "1") // une seule réponse
|
params.Set("limit", "1") // une seule réponse
|
||||||
|
|
||||||
fullURL := fmt.Sprintf("%s?%s", NominatimBaseURL, params.Encode())
|
fullURL := fmt.Sprintf("%s?%s", NominatimBaseURL, params.Encode())
|
||||||
fmt.Println("URL Nominatim:", fullURL) // debug
|
|
||||||
|
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for i := 0; i < 3; i++ { // retry jusqu'à 3 fois
|
for i := 0; i < 3; i++ { // retry jusqu'à 3 fois
|
||||||
|
|||||||
Reference in New Issue
Block a user