657 lines
23 KiB
Go
657 lines
23 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"gestion/db"
|
|
"gestion/models"
|
|
"gestion/services"
|
|
"gestion/utils"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type BasketsRequest struct {
|
|
Username string `json:"username"`
|
|
ProductID int `json:"product_id"`
|
|
NameProduct string `json:"name_product"`
|
|
Category string `json:"category"`
|
|
Quantity float64 `json:"quantity"`
|
|
}
|
|
|
|
// ============================================
|
|
// ✅ SÉCURISÉ: AddProductsBasket
|
|
// ============================================
|
|
// POST /api/v1/panier/add
|
|
func AddProductsBasket(c *gin.Context) {
|
|
database := c.MustGet("database").(*db.Database)
|
|
|
|
var req BasketsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
utils.BindErr(c, err)
|
|
return
|
|
}
|
|
|
|
username, ok := c.Get("username")
|
|
if !ok {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"})
|
|
return
|
|
}
|
|
req.Username = username.(string)
|
|
|
|
if req.Quantity <= 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Quantité invalide"})
|
|
return
|
|
}
|
|
if req.ProductID <= 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "product_id requis"})
|
|
return
|
|
}
|
|
|
|
if p, err := database.GetProductByID(req.ProductID); err == nil && p.ComingSoon {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Ce produit n'est pas encore disponible"})
|
|
return
|
|
}
|
|
|
|
panier, err := database.AddToBasket(req.Username, req.ProductID, req.Quantity)
|
|
if err != nil {
|
|
log.Printf("❌ [ADD_PANIER] product_id=%d qty=%.3f: %v", req.ProductID, req.Quantity, err)
|
|
if err.Error() == "stock insuffisant" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Stock insuffisant"})
|
|
return
|
|
}
|
|
if strings.Contains(err.Error(), "prix introuvable") {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Aucun prix configuré pour ce produit"})
|
|
return
|
|
}
|
|
utils.ServerErr(c, "Impossible d'ajouter le produit au panier", err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Produit ajouté au panier avec succès",
|
|
"panier": panier,
|
|
})
|
|
}
|
|
|
|
func GetAllBaskets(c *gin.Context) {
|
|
database := c.MustGet("database").(*db.Database)
|
|
username := c.Param("username")
|
|
|
|
log.Printf("📦 [GET_PANIER] Requête pour: %s", username)
|
|
|
|
if username == "" {
|
|
log.Printf("❌ [GET_PANIER] Username manquant")
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Le paramètre 'username' est requis"})
|
|
return
|
|
}
|
|
|
|
// ✅ SÉCURITÉ 1: Récupérer username depuis le contexte (du JWT validé)
|
|
authUsername, hasAuth := c.Get("username")
|
|
if !hasAuth {
|
|
log.Printf("❌ [GET_PANIER] Username manquant dans JWT")
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"})
|
|
return
|
|
}
|
|
|
|
authUsernameStr := authUsername.(string)
|
|
|
|
// ✅ SÉCURITÉ 2: Vérifier que c'est bien l'utilisateur de la session
|
|
if username != authUsernameStr {
|
|
log.Printf("❌ [GET_PANIER] ⚠️ TENTATIVE D'ACCÈS AU PANIER NON AUTORISÉE!")
|
|
log.Printf(" Username du JWT: %s", authUsernameStr)
|
|
log.Printf(" Username demandé: %s", username)
|
|
c.JSON(http.StatusForbidden, gin.H{
|
|
"error": "Vous ne pouvez accéder qu'à votre panier",
|
|
})
|
|
return
|
|
}
|
|
|
|
// ✅ SÉCURITÉ 3: Forcer l'utilisation du username du JWT
|
|
username = authUsernameStr
|
|
|
|
// ✅ SÉCURITÉ 4: Vérifier que c'est un CLIENT
|
|
_, err := database.GetClientByUsername(username)
|
|
if err != nil {
|
|
log.Printf("❌ [GET_PANIER] Client inexistant: %s", username)
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Utilisateur inexistant"})
|
|
return
|
|
}
|
|
|
|
baskets, err := database.GetAllProductsInBasket(username)
|
|
if err != nil {
|
|
utils.ServerErr(c, "Erreur lors de la récupération du panier", err)
|
|
return
|
|
}
|
|
|
|
var totalAmount float64
|
|
for _, item := range baskets {
|
|
totalAmount += item.Price // price = prix total de la ligne (cumul des ajouts)
|
|
}
|
|
|
|
log.Printf("✅ [GET_PANIER] Panier %s: %d articles, total=%.2f€", username, len(baskets), totalAmount)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Panier récupéré avec succès",
|
|
"panier": baskets,
|
|
"count": len(baskets),
|
|
"total_amount": totalAmount,
|
|
})
|
|
}
|
|
|
|
func DeleteProductFromBasket(c *gin.Context) {
|
|
database := c.MustGet("database").(*db.Database)
|
|
|
|
var req struct {
|
|
ID int `json:"id" binding:"required"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
utils.BindErr(c, err)
|
|
return
|
|
}
|
|
|
|
// ✅ SÉCURITÉ 1: Récupérer username depuis le contexte (du JWT validé)
|
|
authUsername, hasAuth := c.Get("username")
|
|
if !hasAuth {
|
|
log.Printf("❌ [DEL_PANIER] Username manquant dans JWT")
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"})
|
|
return
|
|
}
|
|
|
|
authUsernameStr := authUsername.(string)
|
|
|
|
log.Printf("🗑️ [DEL_PANIER] Suppression article: id=%d, client=%s", req.ID, authUsernameStr)
|
|
|
|
// ✅ SÉCURITÉ 2: Vérifier que l'article appartient à ce client
|
|
itemUsername, err := database.GetBasketItemOwner(req.ID)
|
|
|
|
if err != nil {
|
|
log.Printf("❌ [DEL_PANIER] Article non trouvé: id=%d", req.ID)
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Article non trouvé"})
|
|
return
|
|
}
|
|
|
|
if itemUsername != authUsernameStr {
|
|
log.Printf("❌ [DEL_PANIER] ⚠️ TENTATIVE DE SUPPRESSION NON AUTORISÉE!")
|
|
log.Printf(" Client JWT: %s", authUsernameStr)
|
|
log.Printf(" Propriétaire article: %s", itemUsername)
|
|
c.JSON(http.StatusForbidden, gin.H{
|
|
"error": "Vous ne pouvez supprimer que vos articles",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Supprimer l'article
|
|
err = database.DeleteProductFromBasket(req.ID)
|
|
if err != nil {
|
|
utils.ServerErr(c, "Erreur lors de la suppression", err)
|
|
return
|
|
}
|
|
|
|
log.Printf("✅ [DEL_PANIER] Article %d supprimé", req.ID)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Produit supprimé du panier avec succès",
|
|
"item_id": req.ID,
|
|
})
|
|
}
|
|
|
|
func ClearBasket(c *gin.Context) {
|
|
database := c.MustGet("database").(*db.Database)
|
|
|
|
// ✅ SÉCURITÉ 1: Récupérer username depuis le contexte (du JWT validé)
|
|
authUsername, hasAuth := c.Get("username")
|
|
if !hasAuth {
|
|
log.Printf("❌ [CLEAR_PANIER] Username manquant dans JWT")
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"})
|
|
return
|
|
}
|
|
|
|
authUsernameStr := authUsername.(string)
|
|
|
|
log.Printf("🧹 [CLEAR_PANIER] Vider panier de: %s", authUsernameStr)
|
|
|
|
baskets, err := database.GetAllProductsInBasket(authUsernameStr)
|
|
if err != nil {
|
|
log.Printf("❌ [CLEAR_PANIER] Erreur récupération: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur lors du vidage du panier",
|
|
})
|
|
return
|
|
}
|
|
|
|
err = database.ClearBasket(authUsernameStr)
|
|
if err != nil {
|
|
utils.ServerErr(c, "Erreur lors du vidage du panier", err)
|
|
return
|
|
}
|
|
|
|
log.Printf("✅ [CLEAR_PANIER] Panier %s vidé: %d articles supprimés", authUsernameStr, len(baskets))
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Panier vidé avec succès",
|
|
})
|
|
}
|
|
|
|
// POST /api/v1/checkout
|
|
func ValidateBasket(c *gin.Context) {
|
|
database := c.MustGet("database").(*db.Database)
|
|
geoService := c.MustGet("geoService").(*services.GeoService)
|
|
|
|
username, exists := c.Get("username")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Utilisateur non authentifié"})
|
|
return
|
|
}
|
|
usernameStr := username.(string)
|
|
|
|
lockKey := fmt.Sprintf("checkout_lock:%s", usernameStr)
|
|
locked, errLock := db.Redis.SetNX(db.RedisCtx, lockKey, "1", 30*time.Second).Result()
|
|
if errLock != nil || !locked {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "Un checkout est déjà en cours pour ce compte"})
|
|
return
|
|
}
|
|
defer db.Redis.Del(db.RedisCtx, lockKey)
|
|
|
|
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)
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil || req.DeliveryAddress == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse de livraison requise"})
|
|
return
|
|
}
|
|
|
|
cmd := &models.Command{DeliveryAddress: req.DeliveryAddress}
|
|
if err := database.CheckAddress(cmd); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse non reconnue", "corrected_address": cmd.DeliveryAddress})
|
|
return
|
|
}
|
|
req.DeliveryAddress = cmd.DeliveryAddress
|
|
|
|
// Vérifier que le client a lié son compte Telegram (seulement si les notifications sont activées)
|
|
if services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() {
|
|
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)
|
|
|
|
// ============================================
|
|
// 1️⃣ Vérifier que le panier n'est pas vide
|
|
// ============================================
|
|
items, err := database.GetBasketItems(usernameStr)
|
|
if err != nil {
|
|
utils.ServerErr(c, "Impossible de récupérer le panier", err)
|
|
return
|
|
}
|
|
|
|
if len(items) == 0 {
|
|
log.Printf("❌ [CHECKOUT] Panier vide")
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Panier vide"})
|
|
return
|
|
}
|
|
|
|
log.Printf("🛒 [CHECKOUT] Panier: %d articles", len(items))
|
|
|
|
// ============================================
|
|
// 1️⃣b Calculer le total et vérifier la présence d'au moins un article payant
|
|
// ============================================
|
|
var cartTotal float64
|
|
for _, item := range items {
|
|
if price, ok := item["price"].(float64); ok {
|
|
cartTotal += price
|
|
}
|
|
}
|
|
|
|
// Détecter si le panier contient un article récompense (prix 0)
|
|
hasRewardItem := false
|
|
for _, item := range items {
|
|
if price, ok := item["price"].(float64); ok && price == 0 {
|
|
hasRewardItem = true
|
|
break
|
|
}
|
|
}
|
|
// Si récompense présente mais aucun produit payant → refuser
|
|
if hasRewardItem && cartTotal <= 0 {
|
|
log.Printf("❌ [CHECKOUT] Panier contient uniquement des récompenses pour %s", usernameStr)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Vous devez commander au moins un produit de la boutique pour bénéficier de votre récompense"})
|
|
return
|
|
}
|
|
|
|
// Récupérer les paramètres globaux (zones + parrainage)
|
|
appSettings, _ := database.GetSettings()
|
|
|
|
// Récupérer le solde parrainage disponible (seulement si le système est activé)
|
|
var referralBalance float64
|
|
if req.UseReferralBalance && appSettings.ReferralEnabled {
|
|
referralBalance, _ = database.GetClientReferralBalance(usernameStr)
|
|
}
|
|
|
|
zoneResult := checkDeliveryZone(req.DeliveryAddress, cartTotal, appSettings.PostalZones)
|
|
if !zoneResult.OK {
|
|
if zoneResult.ZoneName == "inconnue" {
|
|
log.Printf("❌ [CHECKOUT] Aucun code postal trouvé dans l'adresse: %s", req.DeliveryAddress)
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "Adresse invalide : aucun code postal détecté",
|
|
})
|
|
} else if zoneResult.ZoneName == "hors zone" {
|
|
log.Printf("❌ [CHECKOUT] Code postal %s hors zone de livraison", zoneResult.PostalCode)
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "Livraison non disponible pour ce code postal",
|
|
"postal_code": zoneResult.PostalCode,
|
|
})
|
|
} else {
|
|
log.Printf("❌ [CHECKOUT] Total %.2f€ insuffisant pour %s (minimum %.2f€)", cartTotal, zoneResult.ZoneName, zoneResult.MinAmount)
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": fmt.Sprintf("Montant minimum de commande non atteint pour votre zone (%.0f€ minimum)", zoneResult.MinAmount),
|
|
"zone": zoneResult.ZoneName,
|
|
"minimum": zoneResult.MinAmount,
|
|
"cart_total": cartTotal,
|
|
"missing": zoneResult.MinAmount - cartTotal,
|
|
"postal_code": zoneResult.PostalCode,
|
|
"referral_balance": referralBalance,
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
// Règle parrainage : après déduction du crédit, le client doit toujours payer au minimum le seuil de zone.
|
|
// Ex : zone 50€, crédit 50€ → panier doit être >= 100€
|
|
var referralUsed float64
|
|
if req.UseReferralBalance && referralBalance > 0 {
|
|
effectivePayment := cartTotal - referralBalance
|
|
if effectivePayment < zoneResult.MinAmount {
|
|
needed := zoneResult.MinAmount + referralBalance
|
|
log.Printf("❌ [CHECKOUT] Crédit parrainage %.2f€ mais panier insuffisant: %.2f€ < %.2f€ requis", referralBalance, cartTotal, needed)
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": fmt.Sprintf("Avec %.2f€ de crédit parrainage, votre commande doit atteindre %.2f€ (minimum zone %.0f€ + crédit utilisé)", referralBalance, needed, zoneResult.MinAmount),
|
|
"zone": zoneResult.ZoneName,
|
|
"minimum": needed,
|
|
"cart_total": cartTotal,
|
|
"missing": needed - cartTotal,
|
|
"referral_balance": referralBalance,
|
|
"postal_code": zoneResult.PostalCode,
|
|
})
|
|
return
|
|
}
|
|
referralUsed = referralBalance
|
|
}
|
|
|
|
log.Printf("✅ [CHECKOUT] Zone OK: %s, total=%.2f€ >= %.2f€, crédit parrainage utilisé: %.2f€", zoneResult.ZoneName, cartTotal, zoneResult.MinAmount, referralUsed)
|
|
|
|
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)
|
|
}
|
|
|
|
// Vérifier que tous les produits du panier ont encore un prix actif
|
|
unavailable, err := database.GetUnavailableBasketItems(usernameStr)
|
|
if err != nil {
|
|
utils.ServerErr(c, "Erreur vérification produits", err)
|
|
return
|
|
}
|
|
if len(unavailable) > 0 {
|
|
log.Printf("❌ [CHECKOUT] Produits sans prix actif: %v", unavailable)
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "Certains produits de votre panier ne sont plus disponibles",
|
|
"products": unavailable,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Vérification option crypto
|
|
isCrypto := req.PaymentMethod == "crypto"
|
|
if isCrypto {
|
|
np, npOk := c.MustGet("nowpayments").(*services.NowPaymentsClient)
|
|
if !npOk || np == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Paiement crypto non disponible"})
|
|
return
|
|
}
|
|
if req.PayCurrency == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "pay_currency requis pour le paiement crypto (ex: btc, eth, ltc)"})
|
|
return
|
|
}
|
|
}
|
|
|
|
command, err := database.CreateCommandWithAddress(usernameStr, req.DeliveryAddress)
|
|
if err != nil {
|
|
if referralUsed > 0 {
|
|
_ = database.CreditClientReferral(usernameStr, referralUsed)
|
|
}
|
|
log.Printf("❌ [CHECKOUT] Erreur création commande: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création commande"})
|
|
return
|
|
}
|
|
commandID := command.ID
|
|
|
|
if referralUsed > 0 {
|
|
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)
|
|
|
|
// Pour le paiement crypto, le panier/stock sera décrémenté à la confirmation du webhook NowPayments.
|
|
if isCrypto {
|
|
np := c.MustGet("nowpayments").(*services.NowPaymentsClient)
|
|
ipnURL := fmt.Sprintf("%s/api/v1/webhooks/nowpayments", getBaseURL(c))
|
|
payReq := &services.CreatePaymentRequest{
|
|
PriceAmount: cartTotal,
|
|
PriceCurrency: "eur",
|
|
PayCurrency: req.PayCurrency,
|
|
OrderID: fmt.Sprintf("%d", commandID),
|
|
IPNCallbackURL: ipnURL,
|
|
}
|
|
payResp, err := np.CreatePayment(payReq)
|
|
if err != nil {
|
|
// Annuler la commande et restaurer le panier / parrainage
|
|
_ = database.CancelCryptoCommand(commandID)
|
|
if referralUsed > 0 {
|
|
_ = 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"})
|
|
return
|
|
}
|
|
|
|
// Passer la commande en 'pending_payment' (attente confirmation)
|
|
if _, err := database.DB.Exec(`UPDATE commandes SET status = 'pending_payment', payment_method = 'crypto', updated_at = NOW() WHERE id = $1`, commandID); err != nil {
|
|
log.Printf("⚠️ [CHECKOUT] Erreur mise à jour statut pending_payment: %v", err)
|
|
}
|
|
|
|
priceAmt, _ := payResp.PriceAmount.Float64()
|
|
payAmt, _ := payResp.PayAmount.Float64()
|
|
if _, err := database.CreateCryptoPayment(commandID, payResp.PaymentID.String(), payResp.Status, payResp.PriceCurrency, payResp.PayCurrency, payResp.PayAddress, priceAmt, payAmt); err != nil {
|
|
log.Printf("❌ [CHECKOUT] Erreur enregistrement paiement crypto (commande %d, nowpayment %s): %v", commandID, payResp.PaymentID.String(), err)
|
|
_ = database.CancelCryptoCommand(commandID)
|
|
if referralUsed > 0 {
|
|
_ = database.CreditClientReferral(usernameStr, referralUsed)
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur interne lors de l'enregistrement du paiement"})
|
|
return
|
|
}
|
|
|
|
log.Printf("✅ [CHECKOUT] Commande %d en attente paiement crypto (%s)", commandID, req.PayCurrency)
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"success": true,
|
|
"command_id": commandID,
|
|
"payment_method": "crypto",
|
|
"payment_status": payResp.Status,
|
|
"pay_address": payResp.PayAddress,
|
|
"pay_amount": payAmt,
|
|
"pay_currency": payResp.PayCurrency,
|
|
"price_amount": priceAmt,
|
|
"price_currency": payResp.PriceCurrency,
|
|
"message": "Commande créée - En attente de paiement crypto",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Notifier immédiatement tous les admins et agents cabine
|
|
go database.NotifyAllAdminCabine(commandID, usernameStr, req.DeliveryAddress)
|
|
|
|
// ============================================
|
|
// 3️⃣ Décrémenter le stock et vider le panier
|
|
// ============================================
|
|
err = database.ClearBasketOnCheckout(usernameStr)
|
|
if err != nil {
|
|
// Stock insuffisant au moment du checkout (concurrent) → annuler la commande
|
|
if strings.Contains(err.Error(), "stock insuffisant") {
|
|
_ = database.CancelCryptoCommand(commandID)
|
|
if referralUsed > 0 {
|
|
_ = database.CreditClientReferral(usernameStr, referralUsed)
|
|
}
|
|
log.Printf("❌ [CHECKOUT] Stock insuffisant au moment de la validation: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Désolé ! Tu as trop attendu pour passer commande! Le stock ou le produit n'est plus disponible, repasse commande"})
|
|
return
|
|
}
|
|
utils.ServerErr(c, "Impossible de valider le panier", err)
|
|
return
|
|
}
|
|
log.Printf("🧹 [CHECKOUT] Stock décrémenté et panier vidé")
|
|
|
|
// ============================================
|
|
// 4️⃣ Auto-assignation livreur (optionnel)
|
|
// ============================================
|
|
var assigned bool
|
|
var assignInfo gin.H
|
|
|
|
location, err := geoService.GeocodeAddress(req.DeliveryAddress)
|
|
if err == nil {
|
|
log.Printf("📍 [CHECKOUT] Adresse géocodée: %.6f,%.6f", location.Latitude, location.Longitude)
|
|
|
|
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,
|
|
Longitude: location.Longitude,
|
|
}, usernames)
|
|
|
|
if err == nil {
|
|
log.Printf("👤 [CHECKOUT] Livreur le plus proche: %s (%.2f km)", nearest.Username, nearest.Distance)
|
|
|
|
// ✅ CORRECTION: Utiliser CalculateETAWithTomTom au lieu de GetETAWithTraffic
|
|
travelTime, distance, err := services.CalculateETAWithTomTom(
|
|
nearest.Location,
|
|
services.Coordinates{
|
|
Latitude: location.Latitude,
|
|
Longitude: location.Longitude,
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
// Fallback sur ETA simple
|
|
travelTime = nearest.EstimatedTime
|
|
distance = nearest.Distance
|
|
log.Printf("⚠️ [CHECKOUT] Fallback ETA: %d min", travelTime)
|
|
}
|
|
|
|
log.Printf("⏱️ [CHECKOUT] ETA calculé: %d min, distance: %.2f km", travelTime, distance)
|
|
|
|
// Assigner la commande au livreur
|
|
err = database.AssignCommandToDeliverymanQueueWithCoords(
|
|
commandID,
|
|
nearest.Username,
|
|
travelTime,
|
|
location.Latitude,
|
|
location.Longitude,
|
|
req.DeliveryAddress,
|
|
)
|
|
|
|
if err != nil {
|
|
log.Printf("⚠️ [CHECKOUT] Erreur assignation: %v", err)
|
|
} else {
|
|
// Mettre à jour le statut du livreur
|
|
err = database.SetDeliveryPersonStatus(nearest.Username, "busy", commandID)
|
|
if err != nil {
|
|
log.Printf("⚠️ [CHECKOUT] Erreur mise à jour statut livreur: %v", err)
|
|
}
|
|
|
|
// Notifier le livreur de la nouvelle commande
|
|
notifMsg := fmt.Sprintf("Nouvelle commande #%d assignée - Livraison dans ~%d min (%.2f km)", commandID, travelTime, distance)
|
|
if referralUsed > 0 {
|
|
notifMsg += fmt.Sprintf(" | Parrainage client: -%.2f€", referralUsed)
|
|
}
|
|
if notifErr := database.NotifyLivreur(nearest.Username, commandID, "new_assignment", notifMsg); notifErr != nil {
|
|
log.Printf("⚠️ [CHECKOUT] Erreur notification livreur: %v", notifErr)
|
|
}
|
|
|
|
// Notifier le client
|
|
clientOrderID := database.GetClientOrderID(commandID)
|
|
clientMsg := fmt.Sprintf("Ta commande #%d est prise en compte ! Merci de rester branché et vigilant sur les notifs à venir.", clientOrderID)
|
|
database.NotifyClient(usernameStr, commandID, "assigned", clientMsg)
|
|
|
|
assigned = true
|
|
assignInfo = gin.H{
|
|
"username": nearest.Username,
|
|
"distance_km": distance,
|
|
"travel_time": travelTime,
|
|
}
|
|
log.Printf("✅ [CHECKOUT] Commande assignée à %s", nearest.Username)
|
|
}
|
|
} else {
|
|
log.Printf("⚠️ [CHECKOUT] Aucun livreur trouvé: %v", err)
|
|
}
|
|
} else {
|
|
log.Printf("⚠️ [CHECKOUT] Aucun livreur éligible disponible")
|
|
}
|
|
} else {
|
|
log.Printf("⚠️ [CHECKOUT] Erreur géocodage: %v", err)
|
|
}
|
|
|
|
// ============================================
|
|
// 5️⃣ Réponse
|
|
// ============================================
|
|
newBalance, _ := database.GetClientReferralBalance(usernameStr)
|
|
resp := gin.H{
|
|
"success": true,
|
|
"command_id": commandID,
|
|
"client_order_number": command.ClientOrderID,
|
|
"delivery_address": req.DeliveryAddress,
|
|
"status": "pending",
|
|
"referral_used": referralUsed,
|
|
"referral_balance": newBalance,
|
|
}
|
|
|
|
if assigned {
|
|
resp["message"] = "Commande créée et livreur assigné automatiquement"
|
|
resp["auto_assigned"] = true
|
|
resp["assigned_to"] = assignInfo
|
|
resp["status"] = "assigned"
|
|
log.Printf("✅ [CHECKOUT] Réponse 200 - Commande %d assignée", commandID)
|
|
} else {
|
|
resp["message"] = "Commande créée - En attente d'assignation"
|
|
resp["auto_assigned"] = false
|
|
log.Printf("✅ [CHECKOUT] Réponse 200 - Commande %d en attente", commandID)
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, resp)
|
|
}
|
|
|
|
// getBaseURL construit l'URL de base depuis la requête en cours
|
|
func getBaseURL(c *gin.Context) string {
|
|
scheme := "https"
|
|
if c.Request.TLS == nil && c.GetHeader("X-Forwarded-Proto") != "https" {
|
|
scheme = "http"
|
|
}
|
|
return fmt.Sprintf("%s://%s", scheme, c.Request.Host)
|
|
}
|