up
This commit is contained in:
@@ -157,3 +157,33 @@ func ParseBoldWebhookEvent(rawBody []byte) (*BoldWebhookEvent, error) {
|
||||
}
|
||||
return &event, nil
|
||||
}
|
||||
|
||||
// ─── Consulta directa de estado de link ──────────────────────────────────────
|
||||
|
||||
// BoldLinkStatus refleja el campo status del payload de la API de Bold.
|
||||
type BoldLinkStatus struct {
|
||||
Payload struct {
|
||||
Status string `json:"status"` // ACTIVE | PAID | EXPIRED | CANCELLED
|
||||
PaymentID string `json:"payment_id"` // presente cuando está pagado
|
||||
Amount struct {
|
||||
Total int64 `json:"total_amount"`
|
||||
Currency string `json:"currency"`
|
||||
} `json:"amount"`
|
||||
Reference string `json:"reference"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
|
||||
// CheckBoldLinkPaid consulta la API de Bold para saber si un link ya fue pagado.
|
||||
// Devuelve (pagado bool, paymentID string, error).
|
||||
func CheckBoldLinkPaid(cfg *models.BoldConfig, linkID string) (bool, string, error) {
|
||||
raw, err := GetBoldPaymentLinkStatus(cfg, linkID)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
var result BoldLinkStatus
|
||||
if err := json.Unmarshal(raw, &result); err != nil {
|
||||
return false, "", fmt.Errorf("bold: parse link status: %w", err)
|
||||
}
|
||||
paid := result.Payload.Status == "PAID" || result.Payload.Status == "APPROVED"
|
||||
return paid, result.Payload.PaymentID, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -531,3 +534,131 @@ func CreatePago(cfg models.DlocalApi, pago PagoRequest) ([]byte, error) {
|
||||
|
||||
return responseBody, nil
|
||||
}
|
||||
|
||||
// ─── Webhook dLocal ───────────────────────────────────────────────────────────
|
||||
|
||||
// DlocalWebhookNotification es la estructura del JSON que dLocal envía en cada notificación.
|
||||
type DlocalWebhookNotification struct {
|
||||
ID string `json:"id"` // notification ID único
|
||||
Type string `json:"type"` // PAYMENT, SUBSCRIPTION_CHARGE, etc.
|
||||
Status string `json:"status"` // PAID, PENDING, REJECTED, CANCELLED, EXPIRED, AUTHORIZED
|
||||
CreatedDate time.Time `json:"created_date"`
|
||||
Payment struct {
|
||||
ID string `json:"id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
OrderID string `json:"order_id"`
|
||||
Payer struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
} `json:"payer"`
|
||||
} `json:"payment"`
|
||||
// Para notificaciones de suscripción
|
||||
Subscription struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"subscription"`
|
||||
Invoice struct {
|
||||
ID string `json:"id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
} `json:"invoice"`
|
||||
}
|
||||
|
||||
// ParseDlocalWebhookNotification parsea el body crudo en la estructura de notificación.
|
||||
func ParseDlocalWebhookNotification(rawBody []byte) (*DlocalWebhookNotification, error) {
|
||||
var n DlocalWebhookNotification
|
||||
if err := json.Unmarshal(rawBody, &n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
// VerifyDlocalSignature verifica la firma HMAC-SHA256 enviada por dLocal.
|
||||
// dLocal calcula: HMAC-SHA256(secretKey, rawBody) → hex.
|
||||
// El header es "X-dLocal-Signature".
|
||||
func VerifyDlocalSignature(rawBody []byte, signature string, cfg models.DlocalApi) bool {
|
||||
secret := cfg.AccessKeySecretdev
|
||||
if cfg.Modo == "prod" {
|
||||
secret = cfg.AccessKeySecret
|
||||
}
|
||||
if secret == "" {
|
||||
return false
|
||||
}
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write(rawBody)
|
||||
expected := hex.EncodeToString(mac.Sum(nil))
|
||||
return hmac.Equal([]byte(expected), []byte(signature))
|
||||
}
|
||||
|
||||
// ─── Consulta directa de pagos dLocal ────────────────────────────────────────
|
||||
|
||||
// dlocalPaymentResp mapea la respuesta de GET /v1/payments?order_id=...
|
||||
type dlocalPaymentResp struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"` // PAID, PENDING, REJECTED, CANCELLED, EXPIRED, AUTHORIZED
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
OrderID string `json:"order_id"`
|
||||
Payer struct {
|
||||
Email string `json:"email"`
|
||||
} `json:"payer"`
|
||||
}
|
||||
|
||||
type dlocalPaymentListResp struct {
|
||||
Data []dlocalPaymentResp `json:"data"`
|
||||
}
|
||||
|
||||
// CheckDlocalPaymentByOrderID busca en la API de dLocal un pago con el order_id dado
|
||||
// y devuelve (pagado bool, paymentID string, error).
|
||||
func CheckDlocalPaymentByOrderID(cfg models.DlocalApi, orderID string) (bool, string, error) {
|
||||
baseURL := cfg.UrlDev
|
||||
accessKeyID := cfg.AccessKeyIDdev
|
||||
accessKeySecret := cfg.AccessKeySecretdev
|
||||
if cfg.Modo == "prod" {
|
||||
baseURL = cfg.UrlProd
|
||||
accessKeyID = cfg.AccessKeyID
|
||||
accessKeySecret = cfg.AccessKeySecret
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/payments?order_id=%s", baseURL, orderID)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
authToken := fmt.Sprintf("Bearer %s:%s", accessKeyID, accessKeySecret)
|
||||
req.Header.Set("Authorization", authToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, "", fmt.Errorf("dlocal status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// La respuesta puede ser lista o un objeto directo
|
||||
var list dlocalPaymentListResp
|
||||
if err := json.Unmarshal(body, &list); err == nil && len(list.Data) > 0 {
|
||||
for _, p := range list.Data {
|
||||
if p.Status == "PAID" || p.Status == "AUTHORIZED" {
|
||||
return true, p.ID, nil
|
||||
}
|
||||
}
|
||||
return false, "", nil
|
||||
}
|
||||
// Si no es lista, intentar objeto directo
|
||||
var single dlocalPaymentResp
|
||||
if err := json.Unmarshal(body, &single); err == nil && single.ID != "" {
|
||||
paid := single.Status == "PAID" || single.Status == "AUTHORIZED"
|
||||
return paid, single.ID, nil
|
||||
}
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user