Files
soft_usite/pkg/services/bold_service.go
T
2026-05-02 09:41:58 -05:00

195 lines
6.5 KiB
Go

package services
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
const boldAPIBase = "https://integrations.api.bold.co"
// ─── Estructuras de request/response ─────────────────────────────────────────
// BoldPaymentLinkRequest es el payload para crear un link de pago en Bold.
type BoldPaymentLinkRequest struct {
AmountType string `json:"amount_type"`
Amount BoldAmountField `json:"amount"`
Description string `json:"description"`
CallbackURL string `json:"callback_url"`
PayerEmail string `json:"payer_email,omitempty"`
Reference string `json:"reference"`
}
// BoldAmountField representa el campo de monto (COP sin centavos).
type BoldAmountField struct {
Currency string `json:"currency"`
TotalAmount int64 `json:"total_amount"`
}
// BoldPaymentLinkResponse es la respuesta de la API al crear el link.
type BoldPaymentLinkResponse struct {
Payload struct {
PaymentLink string `json:"payment_link"`
URL string `json:"url"`
} `json:"payload"`
}
// BoldWebhookEvent es la estructura del JSON que Bold envía en cada notificación.
type BoldWebhookEvent struct {
ID string `json:"id"`
Type string `json:"type"`
Subject string `json:"subject"`
Data struct {
PaymentID string `json:"payment_id"`
Amount struct {
Total int64 `json:"total"`
Currency string `json:"currency"`
} `json:"amount"`
PayerEmail string `json:"payer_email"`
Metadata struct {
Reference string `json:"reference"`
} `json:"metadata"`
} `json:"data"`
}
// ─── Funciones del servicio ───────────────────────────────────────────────────
// boldAPIKey devuelve la API key correcta según el modo configurado.
func boldAPIKey(cfg *models.BoldConfig) string {
if cfg.Modo == "production" {
return cfg.ApiKeyProd
}
return cfg.ApiKeyTest
}
// boldSecretKey devuelve la secret key según el modo.
// En test la spec oficial indica cadena vacía.
func boldSecretKey(cfg *models.BoldConfig) string {
if cfg.Modo == "production" {
return cfg.SecretKeyProd
}
return cfg.SecretKeyTest
}
// CreateBoldPaymentLink crea un link de pago en Bold y devuelve la respuesta.
func CreateBoldPaymentLink(cfg *models.BoldConfig, req BoldPaymentLinkRequest) (*BoldPaymentLinkResponse, error) {
if req.CallbackURL == "" {
req.CallbackURL = cfg.CallbackUrl
}
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("bold: marshal request: %w", err)
}
httpReq, err := http.NewRequest("POST", boldAPIBase+"/online/link/v1", bytes.NewReader(body))
if err != nil {
return nil, err
}
httpReq.Header.Set("Authorization", "x-api-key "+boldAPIKey(cfg))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("bold: http call: %w", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bold API status %d: %s", resp.StatusCode, string(respBody))
}
var result BoldPaymentLinkResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("bold: unmarshal response: %w", err)
}
return &result, nil
}
// GetBoldPaymentLinkStatus consulta el estado de un payment link existente.
func GetBoldPaymentLinkStatus(cfg *models.BoldConfig, linkID string) ([]byte, error) {
url := fmt.Sprintf("%s/online/link/v1/%s", boldAPIBase, linkID)
httpReq, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
httpReq.Header.Set("Authorization", "x-api-key "+boldAPIKey(cfg))
httpReq.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
// VerifyBoldSignature verifica la firma HMAC-SHA256 del webhook.
// Algoritmo: encoded = base64(rawBody); computed = HMAC-SHA256(encoded, secretKey) en hex.
// En modo test la secretKey es cadena vacía.
func VerifyBoldSignature(rawBody []byte, signature string, cfg *models.BoldConfig) bool {
secret := boldSecretKey(cfg)
encoded := base64.StdEncoding.EncodeToString(rawBody)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(encoded))
computed := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(computed), []byte(signature))
}
// ParseBoldWebhookEvent parsea el JSON crudo del webhook en la estructura BoldWebhookEvent.
func ParseBoldWebhookEvent(rawBody []byte) (*BoldWebhookEvent, error) {
var event BoldWebhookEvent
if err := json.Unmarshal(rawBody, &event); err != nil {
return nil, err
}
return &event, nil
}
// ─── Consulta directa de estado de link ──────────────────────────────────────
// BoldLinkStatus refleja la respuesta de GET /online/link/v1/{payment_link}.
// La API de Bold devuelve los campos directamente en el root del JSON (no dentro de "payload").
type BoldLinkStatus struct {
ID string `json:"id"`
Status string `json:"status"` // ACTIVE | PROCESSING | PAID | REJECTED | CANCELLED | EXPIRED
TransactionID string `json:"transaction_id"` // ID de la transacción cuando está pagado
Reference string `json:"reference"`
Total int64 `json:"total"`
}
// CheckBoldLinkStatus consulta la API de Bold y devuelve el status del link.
// Posibles valores: ACTIVE, PROCESSING, PAID, REJECTED, CANCELLED, EXPIRED.
func CheckBoldLinkStatus(cfg *models.BoldConfig, linkID string) (status string, transactionID string, err error) {
raw, err := GetBoldPaymentLinkStatus(cfg, linkID)
if err != nil {
return "", "", err
}
var result BoldLinkStatus
if err := json.Unmarshal(raw, &result); err != nil {
return "", "", fmt.Errorf("bold: parse link status: %w", err)
}
return result.Status, result.TransactionID, nil
}
// CheckBoldLinkPaid es un wrapper de compatibilidad sobre CheckBoldLinkStatus.
func CheckBoldLinkPaid(cfg *models.BoldConfig, linkID string) (bool, string, error) {
st, txID, err := CheckBoldLinkStatus(cfg, linkID)
if err != nil {
return false, "", err
}
return st == "PAID" || st == "APPROVED", txID, nil
}