665 lines
20 KiB
Go
665 lines
20 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Modelo que refleja la tabla en la base de datos
|
|
type DlocalApi struct {
|
|
ID uint `gorm:"primaryKey"`
|
|
Modo string
|
|
AccessKeyID string
|
|
AccessKeySecret string
|
|
AccessKeyIDdev string
|
|
AccessKeySecretdev string
|
|
UrlProd string
|
|
UrlDev string
|
|
}
|
|
|
|
// Estructura de configuración para usar en la llamada
|
|
type dlocalConfig struct {
|
|
Modo string
|
|
AccessKeyID string
|
|
AccessKeySecret string
|
|
AccessKeyIDdev string
|
|
AccessKeySecretdev string
|
|
UrlProd string
|
|
UrlDev string
|
|
}
|
|
|
|
// Obtiene la configuración desde la base de datos
|
|
func GetDlocalConfigFromDB(db *gorm.DB) (dlocalConfig, error) {
|
|
var api DlocalApi
|
|
if err := db.First(&api).Error; err != nil {
|
|
return dlocalConfig{}, err
|
|
}
|
|
|
|
return dlocalConfig{
|
|
Modo: api.Modo,
|
|
AccessKeyID: api.AccessKeyID,
|
|
AccessKeySecret: api.AccessKeySecret,
|
|
AccessKeyIDdev: api.AccessKeyIDdev,
|
|
AccessKeySecretdev: api.AccessKeySecretdev,
|
|
UrlProd: api.UrlProd,
|
|
UrlDev: api.UrlDev,
|
|
}, nil
|
|
}
|
|
|
|
type PlanRequest struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Country string `json:"country,omitempty"`
|
|
Currency string `json:"currency"`
|
|
Amount float64 `json:"amount"`
|
|
FrequencyType string `json:"frequency_type"`
|
|
FrequencyValue int `json:"frequency_value,omitempty"`
|
|
DayOfMonth int `json:"day_of_month,omitempty"`
|
|
NotificationURL string `json:"notification_url,omitempty"`
|
|
SuccessURL string `json:"success_url"`
|
|
BackURL string `json:"back_url"`
|
|
ErrorURL string `json:"error_url"`
|
|
}
|
|
|
|
type PlanUpdated struct {
|
|
Name string `json:"name"` // Plan name
|
|
Description string `json:"description"` // Plan description
|
|
Amount float64 `json:"amount"` // Plan amount
|
|
NotificationURL string `json:"notification_url"` // Webhook notification URL
|
|
BackURL string `json:"back_url"` // URL to return client to merchant site
|
|
SuccessURL string `json:"success_url"` // Redirect URL on payment success
|
|
ErrorURL string `json:"error_url"` // Redirect URL on payment error
|
|
}
|
|
|
|
type PagoRequest struct {
|
|
Currency string `json:"currency"`
|
|
Amount float64 `json:"amount"`
|
|
OrderID int `json:"order_id"`
|
|
Description string `json:"description"`
|
|
SuccessURL string `json:"success_url"`
|
|
BackURL string `json:"back_url"`
|
|
NotificationURL string `json:"notification_url"`
|
|
}
|
|
|
|
// Llama al endpoint /v1/me de DLocal usando la configuración
|
|
func GetDLocalMe(cfg dlocalConfig) ([]byte, error) {
|
|
// Determinar URL base según el modo
|
|
baseURL := cfg.UrlDev
|
|
AccessKeyID := cfg.AccessKeyIDdev
|
|
AccessKeySecret := cfg.AccessKeySecretdev
|
|
if cfg.Modo == "prod" {
|
|
baseURL = cfg.UrlProd
|
|
AccessKeyID = cfg.AccessKeyID
|
|
AccessKeySecret = cfg.AccessKeySecret
|
|
}
|
|
|
|
// Construir token de autorización
|
|
authToken := fmt.Sprintf("Bearer %s:%s", AccessKeyID, AccessKeySecret)
|
|
|
|
// Crear la solicitud HTTP
|
|
req, err := http.NewRequest("GET", baseURL+"/v1/me", bytes.NewBuffer([]byte("{}")))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", authToken)
|
|
|
|
// Ejecutar la solicitud
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Leer la respuesta
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return body, nil
|
|
}
|
|
|
|
// Función para crear el plan en DLocal
|
|
func CreatePlan(cfg models.DlocalApi, plan models.PlanRequest) ([]byte, error) {
|
|
// Determinar la URL base según el modo
|
|
baseURL := cfg.UrlDev
|
|
AccessKeyID := cfg.AccessKeyIDdev
|
|
AccessKeySecret := cfg.AccessKeySecretdev
|
|
if cfg.Modo == "prod" {
|
|
baseURL = cfg.UrlProd
|
|
AccessKeyID = cfg.AccessKeyID
|
|
AccessKeySecret = cfg.AccessKeySecret
|
|
}
|
|
// URL completa para el endpoint
|
|
url := baseURL + "/v1/subscription/plan"
|
|
|
|
// Construir el token de autorización
|
|
authToken := fmt.Sprintf("Bearer %s:%s", AccessKeyID, AccessKeySecret)
|
|
|
|
// Convertir el cuerpo de la solicitud a JSON
|
|
requestBody, err := json.Marshal(plan)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al convertir el cuerpo de la solicitud a JSON: %v", err)
|
|
}
|
|
|
|
// Crear la solicitud HTTP
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al crear la solicitud: %v", err)
|
|
}
|
|
|
|
// Agregar los encabezados a la solicitud
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", authToken)
|
|
|
|
// Configurar el cliente HTTP
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
// Ejecutar la solicitud
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al enviar la solicitud: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Leer la respuesta
|
|
responseBody, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al leer la respuesta: %v", err)
|
|
}
|
|
|
|
// Si la respuesta no es exitosa, devolver un error
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("error en la respuesta de DLocal: %s", string(responseBody))
|
|
}
|
|
|
|
// Devolver la respuesta exitosa
|
|
return responseBody, nil
|
|
}
|
|
|
|
func SeePlan(cfg models.DlocalApi, planID string) ([]byte, error) {
|
|
// Determinar la URL base según el modo
|
|
baseURL := cfg.UrlDev
|
|
accessKeyID := cfg.AccessKeyIDdev
|
|
accessKeySecret := cfg.AccessKeySecretdev
|
|
|
|
if cfg.Modo == "prod" {
|
|
baseURL = cfg.UrlProd
|
|
accessKeyID = cfg.AccessKeyID
|
|
accessKeySecret = cfg.AccessKeySecret
|
|
}
|
|
|
|
// Validar planID (opcional: depende de tus requisitos)
|
|
if planID == "" {
|
|
return nil, fmt.Errorf("planID no puede estar vacío")
|
|
}
|
|
|
|
// Construir la URL completa para el endpoint
|
|
url := fmt.Sprintf("%s/v1/subscription/plan/%s", baseURL, planID)
|
|
|
|
// Construir el token de autorización
|
|
authToken := fmt.Sprintf("Bearer %s:%s", accessKeyID, accessKeySecret)
|
|
|
|
// Crear la solicitud HTTP
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al crear la solicitud: %v", err)
|
|
}
|
|
|
|
// Agregar los encabezados a la solicitud
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", authToken)
|
|
|
|
// Configurar el cliente HTTP
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
// Ejecutar la solicitud
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al enviar la solicitud: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Leer la respuesta
|
|
responseBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al leer la respuesta: %v", err)
|
|
}
|
|
|
|
// Si la respuesta no es exitosa, devolver un error
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("error en la respuesta de DLocal (%d): %s", resp.StatusCode, string(responseBody))
|
|
}
|
|
|
|
// Devolver la respuesta exitosa
|
|
return responseBody, nil
|
|
}
|
|
|
|
func UpdatedPlan(cfg models.DlocalApi, plan PlanUpdated, planID string) ([]byte, error) {
|
|
// Determinar la URL base según el modo
|
|
baseURL := cfg.UrlDev
|
|
accessKeyID := cfg.AccessKeyIDdev
|
|
accessKeySecret := cfg.AccessKeySecretdev
|
|
|
|
if cfg.Modo == "prod" {
|
|
baseURL = cfg.UrlProd
|
|
accessKeyID = cfg.AccessKeyID
|
|
accessKeySecret = cfg.AccessKeySecret
|
|
}
|
|
|
|
// Construir la URL completa para el endpoint
|
|
url := fmt.Sprintf("%s/v1/subscription/plan/%s", baseURL, planID)
|
|
|
|
// Construir el token de autorización
|
|
authToken := fmt.Sprintf("Bearer %s:%s", accessKeyID, accessKeySecret)
|
|
|
|
// Convertir el cuerpo de la solicitud a JSON
|
|
requestBody, err := json.Marshal(plan)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al convertir el cuerpo de la solicitud a JSON: %v", err)
|
|
}
|
|
|
|
// Crear la solicitud HTTP
|
|
req, err := http.NewRequest("PATCH", url, bytes.NewBuffer(requestBody))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al crear la solicitud: %v", err)
|
|
}
|
|
|
|
// Agregar los encabezados a la solicitud
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", authToken)
|
|
|
|
// Configurar el cliente HTTP
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
// Ejecutar la solicitud
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al enviar la solicitud: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Leer la respuesta
|
|
responseBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al leer la respuesta: %v", err)
|
|
}
|
|
|
|
// Si la respuesta no es exitosa, devolver un error
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("error en la respuesta de DLocal (%d): %s", resp.StatusCode, string(responseBody))
|
|
}
|
|
|
|
// Devolver la respuesta exitosa
|
|
return responseBody, nil
|
|
}
|
|
|
|
func DeactivatePlan(cfg models.DlocalApi, planID string, subscriptionId string) ([]byte, error) {
|
|
// Determinar la URL base según el modo
|
|
baseURL := cfg.UrlDev
|
|
accessKeyID := cfg.AccessKeyIDdev
|
|
accessKeySecret := cfg.AccessKeySecretdev
|
|
|
|
if cfg.Modo == "prod" {
|
|
baseURL = cfg.UrlProd
|
|
accessKeyID = cfg.AccessKeyID
|
|
accessKeySecret = cfg.AccessKeySecret
|
|
}
|
|
|
|
// Construir la URL completa para el endpoint
|
|
url := fmt.Sprintf("%s/v1/subscription/plan/%s/subscription/%s/deactivate", baseURL, planID, subscriptionId)
|
|
|
|
// Construir el token de autorización
|
|
authToken := fmt.Sprintf("Bearer %s:%s", accessKeyID, accessKeySecret)
|
|
|
|
// Crear la solicitud HTTP
|
|
req, err := http.NewRequest("PATCH", url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al crear la solicitud: %v", err)
|
|
}
|
|
|
|
// Agregar los encabezados a la solicitud
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", authToken)
|
|
|
|
// Configurar el cliente HTTP
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
// Ejecutar la solicitud
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al enviar la solicitud: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Leer la respuesta
|
|
responseBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al leer la respuesta: %v", err)
|
|
}
|
|
|
|
// Si la respuesta no es exitosa, devolver un error
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("error en la respuesta de DLocal (%d): %s", resp.StatusCode, string(responseBody))
|
|
}
|
|
|
|
// Devolver la respuesta exitosa
|
|
return responseBody, nil
|
|
}
|
|
|
|
func SeePlanes(cfg models.DlocalApi) ([]byte, error) {
|
|
// Validar modo y establecer valores
|
|
var baseURL, accessKeyID, accessKeySecret string
|
|
|
|
switch cfg.Modo {
|
|
case "prod":
|
|
baseURL = cfg.UrlProd
|
|
accessKeyID = cfg.AccessKeyID
|
|
accessKeySecret = cfg.AccessKeySecret
|
|
default:
|
|
baseURL = cfg.UrlDev
|
|
accessKeyID = cfg.AccessKeyIDdev
|
|
accessKeySecret = cfg.AccessKeySecretdev
|
|
}
|
|
|
|
if baseURL == "" || accessKeyID == "" || accessKeySecret == "" {
|
|
return nil, fmt.Errorf("configuración incompleta para el entorno %q", cfg.Modo)
|
|
}
|
|
|
|
// Construir la URL del endpoint
|
|
url := fmt.Sprintf("%s/v1/subscription/plan/all", baseURL)
|
|
|
|
// Crear la solicitud HTTP
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al crear la solicitud: %w", err)
|
|
}
|
|
|
|
// Establecer encabezados
|
|
req.Header.Set("Content-Type", "application/json")
|
|
// En DLocal se usa generalmente Basic Auth codificado en base64, pero si realmente usan Bearer personalizado:
|
|
authToken := fmt.Sprintf("Bearer %s:%s", accessKeyID, accessKeySecret)
|
|
req.Header.Set("Authorization", authToken)
|
|
|
|
// Cliente HTTP con timeout
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
|
|
// Ejecutar solicitud
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al enviar la solicitud: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Leer respuesta
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al leer la respuesta: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("respuesta no exitosa (%d): %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
return body, nil
|
|
}
|
|
|
|
func SeeSubscription(cfg models.DlocalApi, subscriptionId string, invoiceId string) ([]byte, error) {
|
|
// Determinar la URL base según el modo
|
|
baseURL := cfg.UrlDev
|
|
accessKeyID := cfg.AccessKeyIDdev
|
|
accessKeySecret := cfg.AccessKeySecretdev
|
|
|
|
if cfg.Modo == "prod" {
|
|
baseURL = cfg.UrlProd
|
|
accessKeyID = cfg.AccessKeyID
|
|
accessKeySecret = cfg.AccessKeySecret
|
|
}
|
|
|
|
// Construir la URL completa para el endpoint
|
|
url := fmt.Sprintf("%s/v1/subscription/%s/execution/%s", baseURL, subscriptionId, invoiceId)
|
|
|
|
// Crear la solicitud HTTP
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al crear la solicitud: %w", err)
|
|
}
|
|
|
|
// Establecer encabezados
|
|
req.Header.Set("Content-Type", "application/json")
|
|
// En DLocal se usa generalmente Basic Auth codificado en base64, pero si realmente usan Bearer personalizado:
|
|
authToken := fmt.Sprintf("Bearer %s:%s", accessKeyID, accessKeySecret)
|
|
req.Header.Set("Authorization", authToken)
|
|
|
|
// Cliente HTTP con timeout
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
|
|
// Ejecutar solicitud
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al enviar la solicitud: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Leer respuesta
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al leer la respuesta: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("respuesta no exitosa (%d): %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
return body, nil
|
|
}
|
|
|
|
func CreatePago(cfg models.DlocalApi, pago PagoRequest) ([]byte, error) {
|
|
// Determinar la URL base y credenciales
|
|
baseURL := cfg.UrlDev
|
|
AccessKeyID := cfg.AccessKeyIDdev
|
|
AccessKeySecret := cfg.AccessKeySecretdev
|
|
if cfg.Modo == "prod" {
|
|
baseURL = cfg.UrlProd
|
|
AccessKeyID = cfg.AccessKeyID
|
|
AccessKeySecret = cfg.AccessKeySecret
|
|
}
|
|
|
|
// URL del endpoint de pagos
|
|
url := baseURL + "/v1/payments"
|
|
|
|
// Token de autenticación
|
|
authToken := fmt.Sprintf("Bearer %s:%s", AccessKeyID, AccessKeySecret)
|
|
|
|
// Convertir el cuerpo de la solicitud a JSON
|
|
requestBody, err := json.Marshal(pago)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al convertir el cuerpo de la solicitud a JSON: %v", err)
|
|
}
|
|
|
|
// Crear la solicitud HTTP
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al crear la solicitud: %v", err)
|
|
}
|
|
|
|
// Encabezados
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", authToken)
|
|
|
|
// Cliente HTTP con timeout
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
// Ejecutar la solicitud
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al enviar la solicitud: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Leer la respuesta
|
|
responseBody, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error al leer la respuesta: %v", err)
|
|
}
|
|
|
|
// Verificar si la respuesta fue exitosa
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
|
return nil, fmt.Errorf("error en la respuesta de DLocal: %s", string(responseBody))
|
|
}
|
|
|
|
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, monto float64, payerEmail string, error).
|
|
func CheckDlocalPaymentByOrderID(cfg models.DlocalApi, orderID string) (bool, string, float64, 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, "", 0, "", 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, "", 0, "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return false, "", 0, "", err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return false, "", 0, "", 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, p.Amount, p.Payer.Email, nil
|
|
}
|
|
}
|
|
return false, "", 0, "", 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, single.Amount, single.Payer.Email, nil
|
|
}
|
|
return false, "", 0, "", nil
|
|
}
|