up
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
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] + "…"
|
||||
}
|
||||
Reference in New Issue
Block a user