196 lines
6.6 KiB
Go
196 lines
6.6 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, monto int64, err error) {
|
|
raw, err := GetBoldPaymentLinkStatus(cfg, linkID)
|
|
if err != nil {
|
|
return "", "", 0, err
|
|
}
|
|
var result BoldLinkStatus
|
|
if err := json.Unmarshal(raw, &result); err != nil {
|
|
return "", "", 0, fmt.Errorf("bold: parse link status: %w", err)
|
|
}
|
|
return result.Status, result.TransactionID, result.Total, nil
|
|
}
|
|
|
|
// CheckBoldLinkPaid es un wrapper de compatibilidad sobre CheckBoldLinkStatus.
|
|
// Devuelve (pagado, transactionID, monto, error).
|
|
func CheckBoldLinkPaid(cfg *models.BoldConfig, linkID string) (bool, string, int64, error) {
|
|
st, txID, monto, err := CheckBoldLinkStatus(cfg, linkID)
|
|
if err != nil {
|
|
return false, "", 0, err
|
|
}
|
|
return st == "PAID" || st == "APPROVED", txID, monto, nil
|
|
}
|