2025-05-17 11:36:26 -04:00

372 lines
11 KiB
Go

package database
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"consumer/internal/logging"
"consumer/internal/models/entidad_db"
)
// Constantes y errores
const dbQueryTimeout = 5 * time.Second
var (
// Custom errors
ErrInvalidUUID = errors.New("ID de transacción inválido")
ErrInvalidMessage = errors.New("mensaje inválido")
ErrCompanyNotFound = errors.New("empresa no encontrada")
ErrAlreadyProcessed = errors.New("factura ya procesada y enviada a DLQ")
ErrNilPointer = errors.New("puntero nulo encontrado en los datos")
)
// DBManager maneja las operaciones de base de datos
type DBManager struct {
db *sql.DB
logger *logging.LoggerSystem
}
// NewDBManager crea un nuevo gestor de base de datos
func NewDBManager(dsn string, logger *logging.LoggerSystem) (*DBManager, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("error abriendo conexión a base de datos: %w", err)
}
// Configure connection pool
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(time.Hour)
// Test connection
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
return nil, fmt.Errorf("error pingueando base de datos: %w", err)
}
return &DBManager{
db: db,
logger: logger,
}, nil
}
// Close cierra la conexión a la base de datos
func (dm *DBManager) Close() error {
return dm.db.Close()
}
// isFacturaInDLQ comprueba si una factura ya fue procesada y enviada a DLQ
func (dm *DBManager) IsFacturaInDLQ(ctx context.Context, id string) (bool, error) {
var exists bool
ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
defer cancel()
err := dm.db.QueryRowContext(ctx,
"SELECT EXISTS(SELECT 1 FROM facturacion_dlq WHERE transaccion_id = $1)",
id).Scan(&exists)
if err != nil {
return false, fmt.Errorf("error verificando DLQ: %w", err)
}
return exists, nil
}
// getDatosFacturaEmpresa obtiene los datos de registro de empresa necesarios para la factura
func (dm *DBManager) GetDatosFacturaEmpresa(
ctx context.Context,
nit int64,
codigoSucursal int,
codigoPuntoVenta int,
) (entidad_db.DatosFactura, error) {
var datosFactura entidad_db.DatosFactura
ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
defer cancel()
const queryEmpresa = `
SELECT re.id, re.codigo_ambiente, re.codigo_modalidad, re.codigo_punto_venta,
re.codigo_sistema, re.codigo_sucursal, re.nit,
cu.cuis, cf.codigo, re.token_key, re.token_value, re.nombre_archivo_certificado, re.nombre_archivo_clave_privada
FROM registroEmpresa re
JOIN cuis cu ON cu.registro_empresa_id = re.id
JOIN cufd cf ON cf.cuis_id = cu.id
WHERE re.nit = $1 AND re.codigo_sucursal = $2 AND re.codigo_punto_venta = $3
ORDER BY cu.fecha_vigencia DESC, cf.fecha_vigencia DESC
LIMIT 1`
if err := dm.db.QueryRowContext(ctx, queryEmpresa,
nit, codigoSucursal, codigoPuntoVenta,
).Scan(
&datosFactura.RegistroEmpresaID,
&datosFactura.CodigoAmbiente,
&datosFactura.CodigoModalidad,
&datosFactura.CodigoPuntoVenta,
&datosFactura.CodigoSistema,
&datosFactura.CodigoSucursal,
&datosFactura.NIT,
&datosFactura.CUIS,
&datosFactura.CUFD,
&datosFactura.TokenKey,
&datosFactura.TokenValue,
&datosFactura.NombreArchivoCertificado,
&datosFactura.NombreArchivoClavePrivada,
); err != nil {
if err == sql.ErrNoRows {
return datosFactura, ErrCompanyNotFound
}
return datosFactura, fmt.Errorf("error buscando registroEmpresa: %w", err)
}
return datosFactura, nil
}
// getCUISDatosFactura obtiene el CUIS y datos relacionados
func (dm *DBManager) GetCUISDatosFactura(ctx context.Context, registroEmpresaID int) (int, string, string, error) {
var cuisID int
var cufd, codControl string
ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
defer cancel()
// Obtener CUIS ID
if err := dm.db.QueryRowContext(ctx,
`SELECT id FROM cuis WHERE registro_empresa_id = $1 ORDER BY fecha_vigencia DESC LIMIT 1`,
registroEmpresaID,
).Scan(&cuisID); err != nil {
return 0, "", "", fmt.Errorf("error buscando cuisID: %w", err)
}
// Obtener CUFD y código de control
if err := dm.db.QueryRowContext(ctx,
"SELECT codigo, codigo_control FROM cufd WHERE cuis_id = $1",
cuisID,
).Scan(&cufd, &codControl); err != nil {
return cuisID, "", "", fmt.Errorf("error buscando cufd: %w", err)
}
return cuisID, cufd, codControl, nil
}
// verificarExistenciaFactura verifica si una factura existe y obtiene sus datos
func (dm *DBManager) VerificarExistenciaFactura(ctx context.Context, id string) (
bool, string, string, time.Time, error) {
ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
defer cancel()
var exists bool
if err := dm.db.QueryRowContext(ctx,
"SELECT EXISTS(SELECT 1 FROM facturacion_facturas WHERE id=$1)",
id,
).Scan(&exists); err != nil {
return false, "", "", time.Time{}, fmt.Errorf("error verificando existencia: %w", err)
}
if !exists {
return false, "", "", time.Time{}, nil
}
var numeroFactura, estado string
var fechaEmision time.Time
err := dm.db.QueryRowContext(ctx, `
SELECT fecha_emision, estado
FROM facturacion_facturas WHERE id = $1`, id,
).Scan(&fechaEmision, &estado)
if err != nil {
return true, "", "", time.Time{}, fmt.Errorf("error obteniendo datos: %w", err)
}
return true, numeroFactura, estado, fechaEmision, nil
}
// registra una interacción con el servicio SOAP
func (dm *DBManager) RegistrarInteraccionServicio(ctx context.Context, id, tipoServicio, endpoint, reqBody, respBody string,
statusCode int, duracion int64, exitoso bool, vMsgRespSoap string) error {
ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
defer cancel()
_, err := dm.db.ExecContext(ctx, `
INSERT INTO facturacion_servicio_interacciones
(factura_id, tipo_servicio, endpoint, request_body, response_body, status_code, duracion_ms, exitoso, msg_soap)
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
id, tipoServicio, endpoint, reqBody, respBody, statusCode, duracion, exitoso, vMsgRespSoap)
if err != nil {
return fmt.Errorf("error registrando interacción: %w", err)
}
return nil
}
// crea un registro de factura pendiente
func (dm *DBManager) CrearFacturaPendiente(ctx context.Context, id string,
fechaEmision time.Time, estado string, vCuf string, vUrlFact string, vEmpresaID int, vCodDocSector int, vFactura string) error {
ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
defer cancel()
_, err := dm.db.ExecContext(ctx, `
INSERT INTO facturacion_facturas
(id, fecha_emision, estado, cuf, url, registro_empresa_id, codigo_documento_sector, factura)
VALUES($1,$2,$3,$4,$5,$6,$7,$8)`, id, fechaEmision, estado, vCuf, vUrlFact, vEmpresaID, vCodDocSector, vFactura)
if err != nil {
return fmt.Errorf("error creando factura pendiente: %w", err)
}
return nil
}
// obtiene el CUIS y datos relacionados
func (dm *DBManager) GetFacturacionFacturas(ctx context.Context, id_transaccion string) (string, int, string, error) {
var vRegEmpresaID int
var vCuf string
var vEstado string
ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
defer cancel()
// Obtenemos el ID del registro de la empresa
if err := dm.db.QueryRowContext(ctx,
`SELECT cuf, registro_empresa_id, estado FROM facturacion_facturas WHERE id = $1`, id_transaccion,
).Scan(&vCuf, &vRegEmpresaID, &vEstado); err != nil {
return "", 0, "", fmt.Errorf("error al buscar registro_empresa_id: %w", err)
}
return vCuf, vRegEmpresaID, vEstado, nil
}
// Obtiene los datos de la tabla RegistroEmpresa, cui, cufd
func (dm *DBManager) GetRegEmpresaCuisCuf(ctx context.Context, idRegCompra int) (entidad_db.DatosFactura, error) {
var datosFactura entidad_db.DatosFactura
ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
defer cancel()
const queryEmpresa = `
SELECT re.id, re.codigo_ambiente, re.codigo_modalidad, re.codigo_punto_venta,
re.codigo_sistema, re.codigo_sucursal, re.nit,
cu.cuis, cf.codigo, cf.codigo_control, re.token_key, re.token_value, re.nombre_archivo_clave_privada, re.nombre_archivo_certificado
FROM registroEmpresa re
JOIN cuis cu ON cu.registro_empresa_id = re.id
JOIN cufd cf ON cf.cuis_id = cu.id
WHERE re.id = $1 ORDER BY cu.fecha_vigencia DESC, cf.fecha_vigencia DESC LIMIT 1`
if err := dm.db.QueryRowContext(
ctx, queryEmpresa, idRegCompra,
).Scan(
&datosFactura.RegistroEmpresaID,
&datosFactura.CodigoAmbiente,
&datosFactura.CodigoModalidad,
&datosFactura.CodigoPuntoVenta,
&datosFactura.CodigoSistema,
&datosFactura.CodigoSucursal,
&datosFactura.NIT,
&datosFactura.CUIS,
&datosFactura.CUFD,
&datosFactura.CodigoControl,
&datosFactura.TokenKey,
&datosFactura.TokenValue,
&datosFactura.NombreArchivoClavePrivada,
&datosFactura.NombreArchivoCertificado,
); err != nil {
if err == sql.ErrNoRows {
return datosFactura, ErrCompanyNotFound
}
return datosFactura, fmt.Errorf("error buscando tablas registroEmpresa, cuis, cufd: %w", err)
}
return datosFactura, nil
}
// actualizarEstadoFactura actualiza el estado de una factura
func (dm *DBManager) ActualizarEstadoFactura(ctx context.Context, id, nuevoEstado, codigoAutorizacion string,
estadoAnterior string, detalles string, vFacturaFirmada string) error {
ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
defer cancel()
tx, err := dm.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("error iniciando transacción: %w", err)
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
var updateQuery string
var params []interface{}
if codigoAutorizacion != "" {
updateQuery = `UPDATE facturacion_facturas SET estado=$1, codigo_autorizacion=$2 WHERE id=$3`
params = []interface{}{nuevoEstado, codigoAutorizacion, id}
} else {
if vFacturaFirmada != "" {
updateQuery = `UPDATE facturacion_facturas SET estado=$1, factura_firmada=$2 WHERE id=$3`
params = []interface{}{nuevoEstado, vFacturaFirmada, id}
} else {
updateQuery = `UPDATE facturacion_facturas SET estado=$1 WHERE id=$2`
params = []interface{}{nuevoEstado, id}
}
}
if _, err = tx.ExecContext(ctx, updateQuery, params...); err != nil {
return fmt.Errorf("error actualizando estado: %w", err)
}
if _, err = tx.ExecContext(ctx, `
INSERT INTO facturacion_eventos_factura
(factura_id, estado_anterior, estado_nuevo, detalles)
VALUES($1,$2,$3,$4)`,
id, estadoAnterior, nuevoEstado, detalles); err != nil {
return fmt.Errorf("error registrando evento: %w", err)
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("error haciendo commit: %w", err)
}
return nil
}
// inserta un mensaje en la tabla de DLQ
func (dm *DBManager) InsertarEnDLQ(ctx context.Context, id string, mensaje string, estadoSoap string) error {
ctx, cancel := context.WithTimeout(ctx, dbQueryTimeout)
defer cancel()
// Verificar si ya existe
var exists bool
if err := dm.db.QueryRowContext(ctx,
"SELECT EXISTS(SELECT 1 FROM facturacion_dlq WHERE transaccion_id = $1)",
id).Scan(&exists); err != nil {
return fmt.Errorf("error verificando DLQ: %w", err)
}
// Si ya existe, no insertar nuevamente
if exists {
return nil
}
_, err := dm.db.ExecContext(ctx,
"INSERT INTO facturacion_dlq(transaccion_id, mensaje, estado) VALUES($1,$2,$3)",
id, mensaje, estadoSoap)
if err != nil {
return fmt.Errorf("error insertando en DLQ: %w", err)
}
return nil
}