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 }