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 }