209 lines
6.0 KiB
Go
209 lines
6.0 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
)
|
|
|
|
// DispatchPayload contiene las variables disponibles en el PayloadTemplate.
|
|
type DispatchPayload struct {
|
|
ContratoID uint
|
|
Referencia string
|
|
Email string
|
|
Monto float64
|
|
Moneda string
|
|
SaasID uint
|
|
SaasSlug string
|
|
Fuente string // dlocal | bold | manual
|
|
}
|
|
|
|
// DispatchSaasPaymentNotification notifica a todos los SaaS externos activos
|
|
// asociados al contrato. Se ejecuta en goroutine separada desde los webhooks.
|
|
//
|
|
// Cadena de vinculación:
|
|
// Contrato → contrato_servicios (m2m) → servicios.id
|
|
// servicios.id ↔ saas_productos.servicio_id → saas_productos.id
|
|
// saas_productos.id → saas_api_configs.saas_id (activo = true)
|
|
func DispatchSaasPaymentNotification(contratoID uint, payerEmail, fuente string, monto float64, moneda string) {
|
|
referencia := fmt.Sprintf("contrato-%d", contratoID)
|
|
|
|
// 1. Cargar contrato con sus servicios
|
|
contrato, err := models.GetContratoByID(contratoID)
|
|
if err != nil {
|
|
log.Printf("[DISPATCH] Error cargando contrato %d: %v", contratoID, err)
|
|
return
|
|
}
|
|
if len(contrato.Servicios) == 0 {
|
|
log.Printf("[DISPATCH] Contrato %d sin servicios — nada que despachar", contratoID)
|
|
return
|
|
}
|
|
|
|
// 2. Recopilar IDs de servicios del contrato
|
|
servicioIDs := make([]uint, len(contrato.Servicios))
|
|
for i, s := range contrato.Servicios {
|
|
servicioIDs[i] = s.ID
|
|
}
|
|
|
|
// 3. Buscar SaasProductos cuyo servicio_id esté en los servicios del contrato
|
|
var saasProductos []models.SaasProducto
|
|
if err := app.Http.Database.DB.
|
|
Where("servicio_id IN ? AND activo = ?", servicioIDs, true).
|
|
Find(&saasProductos).Error; err != nil {
|
|
log.Printf("[DISPATCH] Error buscando SaasProductos para contrato %d: %v", contratoID, err)
|
|
return
|
|
}
|
|
if len(saasProductos) == 0 {
|
|
log.Printf("[DISPATCH] Contrato %d: ningún SaasProducto vinculado a sus servicios", contratoID)
|
|
return
|
|
}
|
|
|
|
// 4. Recopilar IDs de SaaS
|
|
saasIDs := make([]uint, len(saasProductos))
|
|
saasMap := make(map[uint]models.SaasProducto, len(saasProductos))
|
|
for i, sp := range saasProductos {
|
|
saasIDs[i] = sp.ID
|
|
saasMap[sp.ID] = sp
|
|
}
|
|
|
|
// 5. Obtener configuraciones API activas para esos SaaS, filtrando por pasarela
|
|
configs, err := models.GetSaasApiConfigsBySaasIDs(saasIDs, fuente) // fuente = "dlocal" | "bold"
|
|
if err != nil {
|
|
log.Printf("[DISPATCH] Error obteniendo SaasApiConfigs: %v", err)
|
|
return
|
|
}
|
|
if len(configs) == 0 {
|
|
log.Printf("[DISPATCH] Contrato %d: ninguna SaasApiConfig activa encontrada", contratoID)
|
|
return
|
|
}
|
|
|
|
// 6. Enviar notificación a cada endpoint
|
|
for _, cfg := range configs {
|
|
saas := saasMap[cfg.SaasID]
|
|
payload := DispatchPayload{
|
|
ContratoID: contratoID,
|
|
Referencia: referencia,
|
|
Email: payerEmail,
|
|
Monto: monto,
|
|
Moneda: moneda,
|
|
SaasID: saas.ID,
|
|
SaasSlug: saas.Slug,
|
|
Fuente: fuente,
|
|
}
|
|
dispatchOne(cfg, payload)
|
|
}
|
|
}
|
|
|
|
// dispatchOne realiza la llamada HTTP a un endpoint e inserta el log correspondiente.
|
|
func dispatchOne(cfg models.SaasApiConfig, payload DispatchPayload) {
|
|
logEntry := models.SaasDispatchLog{
|
|
SaasApiConfigID: cfg.ID,
|
|
ContratoID: payload.ContratoID,
|
|
Referencia: payload.Referencia,
|
|
PayerEmail: payload.Email,
|
|
Fuente: payload.Fuente,
|
|
Intentos: 1,
|
|
}
|
|
|
|
// Renderizar payload template
|
|
body, err := renderTemplate(cfg.PayloadTemplate, payload)
|
|
if err != nil {
|
|
logEntry.Estado = "failed"
|
|
logEntry.ErrorMsg = fmt.Sprintf("template error: %v", err)
|
|
_ = models.SaveSaasDispatchLog(&logEntry)
|
|
log.Printf("[DISPATCH] Config %d — error de template: %v", cfg.ID, err)
|
|
return
|
|
}
|
|
|
|
// Construir request
|
|
metodo := strings.ToUpper(cfg.Metodo)
|
|
if metodo == "" {
|
|
metodo = "POST"
|
|
}
|
|
|
|
timeout := cfg.TimeoutSeg
|
|
if timeout <= 0 {
|
|
timeout = 10
|
|
}
|
|
client := &http.Client{Timeout: time.Duration(timeout) * time.Second}
|
|
|
|
var reqBody io.Reader
|
|
if metodo != "GET" {
|
|
reqBody = bytes.NewBufferString(body)
|
|
}
|
|
|
|
req, err := http.NewRequest(metodo, cfg.EndpointURL, reqBody)
|
|
if err != nil {
|
|
logEntry.Estado = "failed"
|
|
logEntry.ErrorMsg = fmt.Sprintf("error creando request: %v", err)
|
|
_ = models.SaveSaasDispatchLog(&logEntry)
|
|
log.Printf("[DISPATCH] Config %d — error creando request: %v", cfg.ID, err)
|
|
return
|
|
}
|
|
|
|
if metodo != "GET" {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
if cfg.ApiKeyHeader != "" && cfg.ApiKeyValue != "" {
|
|
req.Header.Set(cfg.ApiKeyHeader, cfg.ApiKeyValue)
|
|
}
|
|
|
|
// Ejecutar llamada
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
logEntry.Estado = "failed"
|
|
logEntry.ErrorMsg = fmt.Sprintf("error HTTP: %v", err)
|
|
_ = models.SaveSaasDispatchLog(&logEntry)
|
|
log.Printf("[DISPATCH] Config %d — error llamando %s: %v", cfg.ID, cfg.EndpointURL, err)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBytes, _ := io.ReadAll(resp.Body)
|
|
respBody := truncateStr(string(respBytes), 2000)
|
|
|
|
logEntry.HttpStatus = resp.StatusCode
|
|
logEntry.Respuesta = respBody
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
logEntry.Estado = "success"
|
|
} else {
|
|
logEntry.Estado = "failed"
|
|
}
|
|
|
|
_ = models.SaveSaasDispatchLog(&logEntry)
|
|
log.Printf("[DISPATCH] Config %d → %s %s — HTTP %d", cfg.ID, metodo, cfg.EndpointURL, resp.StatusCode)
|
|
}
|
|
|
|
// renderTemplate aplica text/template sobre el PayloadTemplate del config.
|
|
func renderTemplate(tmplStr string, data DispatchPayload) (string, error) {
|
|
if tmplStr == "" {
|
|
// Si no hay template, enviar payload JSON mínimo
|
|
return fmt.Sprintf(`{"contrato_id":%d,"referencia":"%s","email":"%s","monto":%.2f,"moneda":"%s","fuente":"%s"}`,
|
|
data.ContratoID, data.Referencia, data.Email, data.Monto, data.Moneda, data.Fuente), nil
|
|
}
|
|
t, err := template.New("payload").Parse(tmplStr)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := t.Execute(&buf, data); err != nil {
|
|
return "", err
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func truncateStr(s string, max int) string {
|
|
if len(s) <= max {
|
|
return s
|
|
}
|
|
return s[:max] + "…"
|
|
}
|