diff --git a/pkg/models/contrato.go b/pkg/models/contrato.go index fc844a7..ad1a254 100644 --- a/pkg/models/contrato.go +++ b/pkg/models/contrato.go @@ -22,8 +22,11 @@ type Contrato struct { // Enlace de pago Bold: único por ciclo de pago. // Se reutiliza en múltiples notificaciones del mismo ciclo. // Se anula (vacía) cuando SALE_APPROVED llega y se registra el pago. - EnlacePago string `json:"enlace_pago" gorm:"column:enlace_pago;type:text"` - EnlacePagoLinkID string `json:"enlace_pago_link_id" gorm:"column:enlace_pago_link_id;type:varchar(64)"` + EnlacePago string `json:"enlace_pago" gorm:"column:enlace_pago;type:text"` + EnlacePagoLinkID string `json:"enlace_pago_link_id" gorm:"column:enlace_pago_link_id;type:varchar(64)"` + // Confirmación de pago + PagoConfirmado bool `json:"pago_confirmado" gorm:"column:pago_confirmado;default:false"` + FechaPago *time.Time `json:"fecha_pago" gorm:"column:fecha_pago"` } func (Contrato) TableName() string { return "contratos" } @@ -156,3 +159,23 @@ func LimpiarEnlacePago(contratoID uint) error { "enlace_pago_link_id": "", }).Error } + +// MarcarContratoPagado marca el contrato como pagado al recibir confirmación de la pasarela. +func MarcarContratoPagado(contratoID uint) error { + now := time.Now() + return app.Http.Database.DB.Model(&Contrato{}).Where("id = ?", contratoID).Updates(map[string]interface{}{ + "pago_confirmado": true, + "fecha_pago": now, + "enlace_pago": "", + "enlace_pago_link_id": "", + }).Error +} + +// GetEstadoPago devuelve si el contrato tiene pago confirmado (para polling desde el frontend). +func GetEstadoPago(contratoID uint) (bool, *time.Time, error) { + var c Contrato + if err := app.Http.Database.DB.Select("pago_confirmado", "fecha_pago").First(&c, contratoID).Error; err != nil { + return false, nil, err + } + return c.PagoConfirmado, c.FechaPago, nil +} diff --git a/pkg/services/query_runner_service.go b/pkg/services/query_runner_service.go index 6c53e5c..779942e 100644 --- a/pkg/services/query_runner_service.go +++ b/pkg/services/query_runner_service.go @@ -1,6 +1,7 @@ package services import ( + "context" "database/sql" "fmt" "strings" @@ -14,6 +15,9 @@ import ( "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) +// connTimeout es el tiempo máximo para establecer una conexión a la DB externa. +const connTimeout = 8 * time.Second + // QueryResult contiene el resultado de una consulta SQL. type QueryResult struct { Columns []string `json:"columns"` @@ -39,16 +43,16 @@ func openDynamicDB(c models.ConxDb) (*sql.DB, error) { switch { case strings.Contains(driver, "postgres"): driverName = "postgres" - dsn = fmt.Sprintf("host=%s port=%s user=%s password=%s sslmode=disable", host, port, user, pass) + dsn = fmt.Sprintf("host=%s port=%s user=%s password=%s sslmode=disable connect_timeout=8", host, port, user, pass) case strings.Contains(driver, "mysql") || strings.Contains(driver, "mariadb"): driverName = "mysql" - dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/", user, pass, host, port) + dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/?timeout=8s&readTimeout=20s&writeTimeout=20s", user, pass, host, port) case strings.Contains(driver, "sqlite"): driverName = "sqlite3" dsn = host // para sqlite el host es la ruta del archivo case strings.Contains(driver, "sqlserver") || strings.Contains(driver, "mssql"): driverName = "sqlserver" - dsn = fmt.Sprintf("sqlserver://%s:%s@%s:%s", user, pass, host, port) + dsn = fmt.Sprintf("sqlserver://%s:%s@%s:%s?dial+timeout=8", user, pass, host, port) default: return nil, fmt.Errorf("driver no soportado: %s", driver) } @@ -59,6 +63,13 @@ func openDynamicDB(c models.ConxDb) (*sql.DB, error) { } db.SetConnMaxLifetime(30 * time.Second) db.SetMaxOpenConns(2) + // Verificar conectividad inmediatamente para fallar rápido en lugar de bloquear al hacer la primera query + ctx, cancel := context.WithTimeout(context.Background(), connTimeout) + defer cancel() + if err := db.PingContext(ctx); err != nil { + db.Close() + return nil, fmt.Errorf("no se pudo conectar al servidor de base de datos: %w", err) + } return db, nil } @@ -169,7 +180,9 @@ func ListDatabases(conx models.ConxDb) ([]string, error) { return []string{"main"}, nil } - rows, err := db.Query(query) + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + rows, err := db.QueryContext(ctx, query) if err != nil { return nil, err } @@ -253,13 +266,19 @@ func openDynamicDBWithName(c models.ConxDb, dbName string) (*sql.DB, error) { var dsn string switch { case strings.Contains(driver, "postgres"): - dsn = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", host, port, user, pass, dbName) + dsn = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable connect_timeout=8", host, port, user, pass, dbName) db, err := sql.Open("postgres", dsn) if err != nil { return nil, err } db.SetConnMaxLifetime(30 * time.Second) db.SetMaxOpenConns(2) + ctx, cancel := context.WithTimeout(context.Background(), connTimeout) + defer cancel() + if err := db.PingContext(ctx); err != nil { + db.Close() + return nil, fmt.Errorf("no se pudo conectar a la base de datos '%s': %w", dbName, err) + } return db, nil default: return openDynamicDB(c) diff --git a/pkg/services/renovacion_service.go b/pkg/services/renovacion_service.go index 30cd948..5cff912 100644 --- a/pkg/services/renovacion_service.go +++ b/pkg/services/renovacion_service.go @@ -291,7 +291,7 @@ func obtenerEnlaceBold(contrato *models.Contrato) string { }, Description: desc, Reference: fmt.Sprintf("contrato-%d", contrato.ID), - CallbackURL: cfg.CallbackUrl, + CallbackURL: cfg.CallbackUrl + fmt.Sprintf("?ref=contrato-%d", contrato.ID), } result, err := CreateBoldPaymentLink(cfg, req) diff --git a/resources/views/pago_exitoso.html b/resources/views/pago_exitoso.html new file mode 100644 index 0000000..eb95322 --- /dev/null +++ b/resources/views/pago_exitoso.html @@ -0,0 +1,93 @@ +
Estamos confirmando con la pasarela. Esto puede tomar unos segundos.
++ Tu pago está siendo procesado. Recibirás una confirmación por correo en breve. +
+Si realizaste el pago, puede tardar algunos minutos en reflejarse.
+ + Volver al inicio + +