chore: add crypto payment
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gestion/db"
|
||||
"gestion/services"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 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)
|
||||
if !ok || np == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "paiement crypto non configuré"})
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "impossible de lire le corps"})
|
||||
return
|
||||
}
|
||||
|
||||
sig := c.GetHeader("x-nowpayments-sig")
|
||||
if !np.VerifyIPN(body, sig) {
|
||||
log.Printf("[IPN] signature invalide")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "signature invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
var payload services.IPNPayload
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "payload invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
payAmount, _ := payload.PayAmount.Float64()
|
||||
if err := database.UpdateCryptoPaymentStatus(payment.ID, payload.PaymentStatus, payAmount); err != nil {
|
||||
log.Printf("[IPN] erreur mise à jour paiement %d: %v", payment.ID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur base de données"})
|
||||
return
|
||||
}
|
||||
|
||||
switch payload.PaymentStatus {
|
||||
case "finished", "confirmed":
|
||||
if err := database.ActivateCryptoCommand(payment.CommandID); err != nil {
|
||||
log.Printf("[IPN] erreur activation commande %d: %v", payment.CommandID, err)
|
||||
} else {
|
||||
log.Printf("[IPN] commande %d activée (paiement %s confirmé)", payment.CommandID, payload.PaymentID.String())
|
||||
}
|
||||
case "failed", "expired":
|
||||
if err := database.CancelCryptoCommand(payment.CommandID); err != nil {
|
||||
log.Printf("[IPN] erreur annulation commande %d: %v", payment.CommandID, err)
|
||||
} else {
|
||||
log.Printf("[IPN] commande %d annulée (paiement %s)", payment.CommandID, payload.PaymentStatus)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// GetCommandPaymentStatus - GET /api/v1/commands/:id/payment-status
|
||||
// Retourne le statut du paiement crypto d'une commande (polling côté client)
|
||||
func GetCommandPaymentStatus(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
commandID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "id de commande invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
payment, err := database.GetCryptoPaymentByCommandID(commandID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur base de données"})
|
||||
return
|
||||
}
|
||||
if payment == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "aucun paiement crypto pour cette commande"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"command_id": payment.CommandID,
|
||||
"payment_status": payment.Status,
|
||||
"pay_address": payment.PayAddress,
|
||||
"pay_amount": payment.PayAmount,
|
||||
"pay_currency": payment.PayCurrency,
|
||||
"price_amount": payment.PriceAmount,
|
||||
"price_currency": payment.PriceCurrency,
|
||||
})
|
||||
}
|
||||
@@ -295,6 +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)
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.DeliveryAddress == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse de livraison requise"})
|
||||
@@ -411,6 +413,20 @@ func ValidateBasket(c *gin.Context) {
|
||||
log.Printf("✅ [CHECKOUT] Crédit parrainage -%.2f€ débité pour %s", referralUsed, usernameStr)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -430,6 +446,56 @@ func ValidateBasket(c *gin.Context) {
|
||||
|
||||
log.Printf("✅ [CHECKOUT] Commande %d créée", commandID)
|
||||
|
||||
// ============================================
|
||||
// PAIEMENT CRYPTO - créer le paiement 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.RestoreReferralBalance(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()
|
||||
_, _ = database.CreateCryptoPayment(commandID, payResp.PaymentID.String(), payResp.Status, payResp.PriceCurrency, payResp.PayCurrency, payResp.PayAddress, priceAmt, payAmt)
|
||||
|
||||
log.Printf("✅ [CHECKOUT] Commande %d en attente paiement crypto (%s)", commandID, req.PayCurrency)
|
||||
c.JSON(http.StatusOK, 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)
|
||||
|
||||
@@ -566,3 +632,12 @@ func ValidateBasket(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user