ip
This commit is contained in:
@@ -167,10 +167,10 @@ func SeedPlantillasBase() {
|
|||||||
db := app.Http.Database.DB
|
db := app.Http.Database.DB
|
||||||
|
|
||||||
plantillasBase := []struct {
|
plantillasBase := []struct {
|
||||||
nombre string
|
nombre string
|
||||||
asunto string
|
asunto string
|
||||||
tipo string
|
tipo string
|
||||||
cuerpo string
|
cuerpo string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
nombre: "Pago recibido (base)",
|
nombre: "Pago recibido (base)",
|
||||||
@@ -271,7 +271,7 @@ func MigrateRenovaciones() {
|
|||||||
&models.Cliente{},
|
&models.Cliente{},
|
||||||
&models.PlantillaCorreo{},
|
&models.PlantillaCorreo{},
|
||||||
&models.NotificacionRegla{},
|
&models.NotificacionRegla{},
|
||||||
&models.Contrato{}, // incluye columnas enlace_pago y enlace_pago_link_id
|
&models.Contrato{}, // incluye columnas enlace_pago y enlace_pago_link_id
|
||||||
&models.NotificacionLog{},
|
&models.NotificacionLog{},
|
||||||
&models.SmtpConfig{},
|
&models.SmtpConfig{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -663,11 +663,11 @@ func SeedSaas() {
|
|||||||
var sub models.Submodules
|
var sub models.Submodules
|
||||||
if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil {
|
if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil {
|
||||||
sub = models.Submodules{
|
sub = models.Submodules{
|
||||||
Title: e.title,
|
Title: e.title,
|
||||||
Description: e.desc,
|
Description: e.desc,
|
||||||
Url: e.url,
|
Url: e.url,
|
||||||
ModuleId: modulo.ID,
|
ModuleId: modulo.ID,
|
||||||
ModifiedAt: time.Now(),
|
ModifiedAt: time.Now(),
|
||||||
}
|
}
|
||||||
if err := db.Create(&sub).Error; err != nil {
|
if err := db.Create(&sub).Error; err != nil {
|
||||||
log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err)
|
log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err)
|
||||||
|
|||||||
+23
-10
@@ -203,9 +203,11 @@ func periodicidadAMeses(p string) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarcarContratoPagado marca el contrato como pagado y renueva la fecha de vencimiento
|
// MarcarContratoPagado marca el contrato como pagado y renueva la fecha de vencimiento.
|
||||||
// según la periodicidad del servicio renovable asociado.
|
// Retorna (true, nil) si el contrato pasó de no-pagado a pagado en esta llamada.
|
||||||
func MarcarContratoPagado(contratoID uint) error {
|
// Retorna (false, nil) si ya estaba pagado (idempotente).
|
||||||
|
// WHERE pago_confirmado = false garantiza atomicidad ante llamadas concurrentes.
|
||||||
|
func MarcarContratoPagado(contratoID uint) (bool, error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
// Cargar contrato con servicios para calcular nueva fecha de vencimiento
|
// Cargar contrato con servicios para calcular nueva fecha de vencimiento
|
||||||
@@ -213,12 +215,20 @@ func MarcarContratoPagado(contratoID uint) error {
|
|||||||
if err := app.Http.Database.DB.Preload("Servicios").First(&c, contratoID).Error; err != nil {
|
if err := app.Http.Database.DB.Preload("Servicios").First(&c, contratoID).Error; err != nil {
|
||||||
log.Printf("[PAGO] No se pudo cargar contrato %d para renovar: %v", contratoID, err)
|
log.Printf("[PAGO] No se pudo cargar contrato %d para renovar: %v", contratoID, err)
|
||||||
// Fallback: solo marcar pago sin renovar fecha
|
// Fallback: solo marcar pago sin renovar fecha
|
||||||
return app.Http.Database.DB.Model(&Contrato{}).Where("id = ?", contratoID).Updates(map[string]interface{}{
|
result := app.Http.Database.DB.Model(&Contrato{}).
|
||||||
"pago_confirmado": true,
|
Where("id = ? AND pago_confirmado = false", contratoID).
|
||||||
"fecha_pago": now,
|
Updates(map[string]interface{}{
|
||||||
"enlace_pago": "",
|
"pago_confirmado": true,
|
||||||
"enlace_pago_link_id": "",
|
"fecha_pago": now,
|
||||||
}).Error
|
"enlace_pago": "",
|
||||||
|
"enlace_pago_link_id": "",
|
||||||
|
})
|
||||||
|
return result.RowsAffected > 0, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ya estaba pagado — nada que hacer
|
||||||
|
if c.PagoConfirmado {
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
updates := map[string]interface{}{
|
updates := map[string]interface{}{
|
||||||
@@ -254,7 +264,10 @@ func MarcarContratoPagado(contratoID uint) error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.Http.Database.DB.Model(&Contrato{}).Where("id = ?", contratoID).Updates(updates).Error
|
result := app.Http.Database.DB.Model(&Contrato{}).
|
||||||
|
Where("id = ? AND pago_confirmado = false", contratoID).
|
||||||
|
Updates(updates)
|
||||||
|
return result.RowsAffected > 0, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEstadoPago devuelve si el contrato tiene pago confirmado (para polling desde el frontend).
|
// GetEstadoPago devuelve si el contrato tiene pago confirmado (para polling desde el frontend).
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ func TestCicloNotificaciones(t *testing.T) {
|
|||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
t.Run("paso2_pago_confirmado", func(t *testing.T) {
|
t.Run("paso2_pago_confirmado", func(t *testing.T) {
|
||||||
// 2a. Simular llegada del webhook: marcar contrato como pagado
|
// 2a. Simular llegada del webhook: marcar contrato como pagado
|
||||||
if err := models.MarcarContratoPagado(contrato.ID); err != nil {
|
if _, err := models.MarcarContratoPagado(contrato.ID); err != nil {
|
||||||
t.Fatalf("MarcarContratoPagado: %v", err)
|
t.Fatalf("MarcarContratoPagado: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -299,12 +299,6 @@ func EnviarCorreoConfirmacionPago(contratoID uint, pasarela ...string) {
|
|||||||
}
|
}
|
||||||
regla := ®las[0]
|
regla := ®las[0]
|
||||||
|
|
||||||
// Idempotencia: si ya se envió hoy para esta regla+cliente, no reenviar
|
|
||||||
if models.YaEnviadoHoy(contrato.ClienteID, regla.ID) {
|
|
||||||
log.Printf("[PAGO-CORREO] correo de confirmación ya enviado hoy para cliente %d", contrato.ClienteID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := EnviarNotificacionGrupo(regla, &contrato.Cliente, []models.Contrato{*contrato}, pas); err != nil {
|
if err := EnviarNotificacionGrupo(regla, &contrato.Cliente, []models.Contrato{*contrato}, pas); err != nil {
|
||||||
log.Printf("[PAGO-CORREO] error enviando correo de confirmación para contrato %d: %v", contratoID, err)
|
log.Printf("[PAGO-CORREO] error enviando correo de confirmación para contrato %d: %v", contratoID, err)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -118,13 +118,15 @@ func BoldWebhook(c *fiber.Ctx) error {
|
|||||||
if referencia != "" {
|
if referencia != "" {
|
||||||
var contratoID uint
|
var contratoID uint
|
||||||
if _, err := fmt.Sscanf(referencia, "contrato-%d", &contratoID); err == nil && contratoID > 0 {
|
if _, err := fmt.Sscanf(referencia, "contrato-%d", &contratoID); err == nil && contratoID > 0 {
|
||||||
if err := models.MarcarContratoPagado(contratoID); err != nil {
|
if ok, err := models.MarcarContratoPagado(contratoID); err != nil {
|
||||||
log.Printf("[BOLD] Webhook: error marcando contrato %d como pagado: %v", contratoID, err)
|
log.Printf("[BOLD] Webhook: error marcando contrato %d como pagado: %v", contratoID, err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[BOLD] Webhook: contrato %d marcado como pagado", contratoID)
|
log.Printf("[BOLD] Webhook: contrato %d marcado como pagado (nuevo=%v)", contratoID, ok)
|
||||||
// Notificar SaaS externos + enviar correo de confirmación (goroutine, no bloquea respuesta)
|
if ok {
|
||||||
go services.DispatchSaasPaymentNotification(contratoID, payerEmail, "bold", float64(monto), "COP")
|
// Primer registro del pago: notificar SaaS + enviar correo
|
||||||
go services.EnviarCorreoConfirmacionPago(contratoID, "bold")
|
go services.DispatchSaasPaymentNotification(contratoID, payerEmail, "bold", float64(monto), "COP")
|
||||||
|
go services.EnviarCorreoConfirmacionPago(contratoID, "bold")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,13 +320,14 @@ func DlocalWebhook(c *fiber.Ctx) error {
|
|||||||
if orderID != "" {
|
if orderID != "" {
|
||||||
var contratoID uint
|
var contratoID uint
|
||||||
if _, err := fmt.Sscanf(orderID, "contrato-%d", &contratoID); err == nil && contratoID > 0 {
|
if _, err := fmt.Sscanf(orderID, "contrato-%d", &contratoID); err == nil && contratoID > 0 {
|
||||||
if err := models.MarcarContratoPagado(contratoID); err != nil {
|
if ok, err := models.MarcarContratoPagado(contratoID); err != nil {
|
||||||
log.Printf("[DLOCAL] Webhook: error marcando contrato %d como pagado: %v", contratoID, err)
|
log.Printf("[DLOCAL] Webhook: error marcando contrato %d como pagado: %v", contratoID, err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[DLOCAL] Webhook: contrato %d marcado como pagado", contratoID)
|
log.Printf("[DLOCAL] Webhook: contrato %d marcado como pagado (nuevo=%v)", contratoID, ok)
|
||||||
// Notificar SaaS externos + enviar correo de confirmación (goroutine, no bloquea respuesta)
|
if ok {
|
||||||
go services.DispatchSaasPaymentNotification(contratoID, payerEmail, "dlocal", monto, moneda)
|
go services.DispatchSaasPaymentNotification(contratoID, payerEmail, "dlocal", monto, moneda)
|
||||||
go services.EnviarCorreoConfirmacionPago(contratoID, "dlocal")
|
go services.EnviarCorreoConfirmacionPago(contratoID, "dlocal")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,8 +406,8 @@ func DlocalRegistrarPago(c *fiber.Ctx) error {
|
|||||||
if b.Estado == "PAID" && referencia != "" {
|
if b.Estado == "PAID" && referencia != "" {
|
||||||
var contratoID uint
|
var contratoID uint
|
||||||
if _, err := fmt.Sscanf(referencia, "contrato-%d", &contratoID); err == nil && contratoID > 0 {
|
if _, err := fmt.Sscanf(referencia, "contrato-%d", &contratoID); err == nil && contratoID > 0 {
|
||||||
if err := models.MarcarContratoPagado(contratoID); err != nil {
|
if _, markErr := models.MarcarContratoPagado(contratoID); markErr != nil {
|
||||||
log.Printf("[DLOCAL] Registro manual: error marcando contrato %d: %v", contratoID, err)
|
log.Printf("[DLOCAL] Registro manual: error marcando contrato %d: %v", contratoID, markErr)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[DLOCAL] Registro manual: contrato %d marcado como pagado", contratoID)
|
log.Printf("[DLOCAL] Registro manual: contrato %d marcado como pagado", contratoID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,8 +78,9 @@ func PagoExitosoPage(c *fiber.Ctx) error {
|
|||||||
// Marcar contrato si aún no está confirmado
|
// Marcar contrato si aún no está confirmado
|
||||||
var contratoID uint
|
var contratoID uint
|
||||||
if _, err := fmt.Sscanf(ref, "contrato-%d", &contratoID); err == nil && contratoID > 0 {
|
if _, err := fmt.Sscanf(ref, "contrato-%d", &contratoID); err == nil && contratoID > 0 {
|
||||||
_ = models.MarcarContratoPagado(contratoID)
|
if ok, _ := models.MarcarContratoPagado(contratoID); ok {
|
||||||
go services.EnviarCorreoConfirmacionPago(contratoID, "bold")
|
go services.EnviarCorreoConfirmacionPago(contratoID, "bold")
|
||||||
|
}
|
||||||
// Intentar enriquecer con datos del webhook log si ya llegó
|
// Intentar enriquecer con datos del webhook log si ya llegó
|
||||||
go func() {
|
go func() {
|
||||||
if wLogs, err := models.GetBoldWebhookLogsByRef(ref); err == nil {
|
if wLogs, err := models.GetBoldWebhookLogsByRef(ref); err == nil {
|
||||||
@@ -161,8 +162,9 @@ func verificarPago(ref string) (confirmado bool, fechaPago string) {
|
|||||||
for _, l := range dlocalLogs {
|
for _, l := range dlocalLogs {
|
||||||
if l.Estado == "PAID" || l.Estado == "AUTHORIZED" {
|
if l.Estado == "PAID" || l.Estado == "AUTHORIZED" {
|
||||||
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via dlocal_payment_log (id=%d)", contratoID, l.ID)
|
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via dlocal_payment_log (id=%d)", contratoID, l.ID)
|
||||||
_ = models.MarcarContratoPagado(contratoID)
|
if ok, _ := models.MarcarContratoPagado(contratoID); ok {
|
||||||
go services.EnviarCorreoConfirmacionPago(contratoID, "dlocal")
|
go services.EnviarCorreoConfirmacionPago(contratoID, "dlocal")
|
||||||
|
}
|
||||||
return true, l.CreatedAt.Format("02/01/2006 15:04")
|
return true, l.CreatedAt.Format("02/01/2006 15:04")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,8 +179,9 @@ func verificarPago(ref string) (confirmado bool, fechaPago string) {
|
|||||||
log.Printf("[PAGO-ESTADO] Bold API error para link %s: %v", contrato.EnlacePagoLinkID, boldApiErr)
|
log.Printf("[PAGO-ESTADO] Bold API error para link %s: %v", contrato.EnlacePagoLinkID, boldApiErr)
|
||||||
} else if paid {
|
} else if paid {
|
||||||
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via Bold API (payment_id=%s)", contratoID, paymentID)
|
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via Bold API (payment_id=%s)", contratoID, paymentID)
|
||||||
_ = models.MarcarContratoPagado(contratoID)
|
if ok, _ := models.MarcarContratoPagado(contratoID); ok {
|
||||||
go services.EnviarCorreoConfirmacionPago(contratoID, "bold")
|
go services.EnviarCorreoConfirmacionPago(contratoID, "bold")
|
||||||
|
}
|
||||||
if paymentID != "" && !models.IsBoldNotificationDuplicate("api-check-"+paymentID) {
|
if paymentID != "" && !models.IsBoldNotificationDuplicate("api-check-"+paymentID) {
|
||||||
_ = models.SaveBoldWebhookLog(models.BoldWebhookLog{
|
_ = models.SaveBoldWebhookLog(models.BoldWebhookLog{
|
||||||
NotificationID: "api-check-" + paymentID,
|
NotificationID: "api-check-" + paymentID,
|
||||||
@@ -203,8 +206,9 @@ func verificarPago(ref string) (confirmado bool, fechaPago string) {
|
|||||||
log.Printf("[PAGO-ESTADO] dLocal API error para order_id %s: %v", ref, dlocalApiErr)
|
log.Printf("[PAGO-ESTADO] dLocal API error para order_id %s: %v", ref, dlocalApiErr)
|
||||||
} else if paid {
|
} else if paid {
|
||||||
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via dLocal API (payment_id=%s)", contratoID, paymentID)
|
log.Printf("[PAGO-ESTADO] Contrato %d confirmado via dLocal API (payment_id=%s)", contratoID, paymentID)
|
||||||
_ = models.MarcarContratoPagado(contratoID)
|
if ok, _ := models.MarcarContratoPagado(contratoID); ok {
|
||||||
go services.EnviarCorreoConfirmacionPago(contratoID, "dlocal")
|
go services.EnviarCorreoConfirmacionPago(contratoID, "dlocal")
|
||||||
|
}
|
||||||
notifID := "api-check-" + ref
|
notifID := "api-check-" + ref
|
||||||
if !models.IsDlocalNotificationDuplicate(notifID) {
|
if !models.IsDlocalNotificationDuplicate(notifID) {
|
||||||
_ = models.SaveDlocalPaymentLog(models.DlocalPaymentLog{
|
_ = models.SaveDlocalPaymentLog(models.DlocalPaymentLog{
|
||||||
|
|||||||
@@ -181,7 +181,8 @@ func EjecutarTestCiclo(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// ── PASO 2: Simular recepción de pago ─────────────────────────────────────
|
// ── PASO 2: Simular recepción de pago ─────────────────────────────────────
|
||||||
paso("paso 2 — marcar contrato como pagado", func() error {
|
paso("paso 2 — marcar contrato como pagado", func() error {
|
||||||
return models.MarcarContratoPagado(contrato.ID)
|
_, err := models.MarcarContratoPagado(contrato.ID)
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
|
|
||||||
paso("paso 2 — verificar pago_confirmado en DB", func() error {
|
paso("paso 2 — verificar pago_confirmado en DB", func() error {
|
||||||
|
|||||||
@@ -290,8 +290,9 @@ func ValidarDlocalLog(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// Si el pago se confirmó ahora (no estaba marcado antes), actualizar contrato
|
// Si el pago se confirmó ahora (no estaba marcado antes), actualizar contrato
|
||||||
if pagoConfirmado && !contrato.PagoConfirmado {
|
if pagoConfirmado && !contrato.PagoConfirmado {
|
||||||
_ = models.MarcarContratoPagado(contratoID)
|
if ok, _ := models.MarcarContratoPagado(contratoID); ok {
|
||||||
go services.EnviarCorreoConfirmacionPago(contratoID, fuenteConfirmacion)
|
go services.EnviarCorreoConfirmacionPago(contratoID, fuenteConfirmacion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = models.UpdateDlocalPaymentLogDatos(uint(logID), payerEmail, monto)
|
_ = models.UpdateDlocalPaymentLogDatos(uint(logID), payerEmail, monto)
|
||||||
@@ -384,8 +385,9 @@ func ValidarBoldCallback(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// Actualizar contrato si se confirmó ahora
|
// Actualizar contrato si se confirmó ahora
|
||||||
if pagoConfirmado && !contrato.PagoConfirmado {
|
if pagoConfirmado && !contrato.PagoConfirmado {
|
||||||
_ = models.MarcarContratoPagado(contratoID)
|
if ok, _ := models.MarcarContratoPagado(contratoID); ok {
|
||||||
go services.EnviarCorreoConfirmacionPago(contratoID, fuente)
|
go services.EnviarCorreoConfirmacionPago(contratoID, fuente)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar estado del callback
|
// Actualizar estado del callback
|
||||||
@@ -513,8 +515,9 @@ func ValidarBoldLog(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// Si el pago se confirmó ahora (no estaba marcado antes), actualizar contrato
|
// Si el pago se confirmó ahora (no estaba marcado antes), actualizar contrato
|
||||||
if pagoConfirmado && !contrato.PagoConfirmado {
|
if pagoConfirmado && !contrato.PagoConfirmado {
|
||||||
_ = models.MarcarContratoPagado(contratoID)
|
if ok, _ := models.MarcarContratoPagado(contratoID); ok {
|
||||||
go services.EnviarCorreoConfirmacionPago(contratoID, fuenteConfirmacion)
|
go services.EnviarCorreoConfirmacionPago(contratoID, fuenteConfirmacion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar log con los mejores datos disponibles
|
// Actualizar log con los mejores datos disponibles
|
||||||
|
|||||||
Reference in New Issue
Block a user