Saltar a contenido

Modelo de dominio

Entidades JPA y enums-tabla AFIP del backend. Todas son POJOs anotados con jakarta.persistence.*, viven en com.tipre.tifacturaonlinemanager.model.** y mapean tablas que administra el cliente (hbm2ddl=none). Para cómo se persisten ver Persistencia; para las reglas de negocio que las atan, Ciclo de facturación e Invariantes.

Fidelidad al esquema legacy

Los nombres de tabla/columna se respetan literalmente (PascalCase) con @Table/@Column. No hay snake_case: la naming strategy es PhysicalNamingStrategyStandardImpl. Varias entidades vienen generadas por Hibernate Tools del legacy y conservan imports Jackson 1.x (org.codehaus.jackson) que no se migran en Fase 1.

Mapa de entidades

Entidad Tabla Paquete Rol
Trx Trx model.trx Comprobante / transacción de facturación. Raíz del agregado.
TrxDetalle TrxDetalle model.trx Ítem de línea del comprobante.
AlicuotaIva Iva model.trx Alícuota de IVA del comprobante.
Tributo Tributos model.trx Tributo (otro impuesto) del comprobante.
Opcional Opcionales model.trx Dato opcional AFIP (clave/valor).
ComprobanteAsociado ComprobantesAsociados model.trx Comprobante asociado (NC/ND → factura original).
Caea Caeas model.trx CAEA otorgado por AFIP.
CaeaSinMovimiento CaeaSinMovimientos model.trx Informe "sin movimiento" de un PV bajo un CAEA.
EnteFacturador EntesFacturadores model Comercio emisor (ABM multi-comercio: N entes, cada uno con su CUIT, cert AFIP y TA).
SucPosPV SucPosPV model Mapeo sucursal+POS ↔ PV CAE/CAEA de AFIP, con dueño (idEnteFacturador).
Tarea Tareas model.tarea Definición de un job Quartz.
JobEjecucion JobEjecucion model.job Bitácora de cada corrida de un job.
TrxErrorLog TrxErrorLog model.error Log de errores para la grilla del cockpit.
TipoComprobante TiposComprobante model.afip Tipo de comprobante AFIP (enum-tabla).
TipoDocumento TiposDocumento model.afip Tipo de documento del receptor.
TipoConcepto TiposConcepto model.afip Concepto (productos/servicios/ambos).
TipoIva TiposIva model.afip Alícuota IVA catálogo.
TipoMoneda TiposMonedas model.afip Moneda.
TipoTributo TiposTributo model.afip Tipo de tributo.

Trx — el comprobante

Raíz del agregado de facturación. Id autogenerado (IDENTITY). Conserva tanto los datos de entrada (receptor, importes, ítems) como los valores devueltos por AFIP (cae, caeFchVto, fchProceso, caeBarCode, caea, caeaFchTope, resultado).

Campos clave (@Table(name = "Trx")):

Campo Columna Tipo Nota
id id Integer PK IDENTITY.
enteFacturador idEnteFacturador @ManyToOne LAZY, nullable=false Comercio emisor.
ptoVta ptoVta int Punto de venta.
tipoComprobante idTipoComprobante @ManyToOne LAZY, nullable=false
tipoConcepto idTipoConcepto @ManyToOne LAZY, nullable=false
tipoDocumento idTipoDocumento @ManyToOne LAZY, nullable=false
nroDocumento nroDocumento long DNI o CUIT/CUIL del receptor.
razonSocial, direccion, localidad, condicionIva idem String Datos del receptor.
comprobante comprobante Long Número de comprobante.
comprobanteFecha comprobanteFecha String Formato YYYYMMDD.
importeTotal, importeTotConc, importeNeto, importeOpEx, importeTributo, importeIva idem double Totales (neto no gravado / gravado / exento / suma tributos / suma IVA).
fechaServicioDesde/Hasta, fechaVencimientoPago idem String YYYYMMDD.
tipoMoneda idTipoMoneda @ManyToOne LAZY, nullable=false
montoCotizacion montoCotizacion double 1 para PES.
cae, caeFchVto, fchProceso, caeBarCode idem String Valores devueltos por AFIP.
caea, caeaFchTope idem String Si se facturó por CAEA.
resultado resultado String Resultado AFIP.
cbteFchHsGen cbteFchHsGen String Fecha-hora generación.
condicionIVAReceptorId condicionIVAReceptorId int Condición IVA del receptor (RG nueva).
email, medioPago, medioPagoCuota/Cupon/Autorizacion, observaciones idem String Datos comerciales.
comprobanteRelacionado comprobanteRelacionado String Para NC/ND.
nroTicketPos, nroSuc, nroPos idem String / Integer Origen POS.
version, reintentos, tipofacturacion, trxoriginal idem String/Integer/Long Metadatos de procesamiento.

Relaciones @OneToMany (LAZY, mappedBy="trx", cascade=REMOVE): alicuotaIvas, tributos, opcionales, comprobantesAsociados, trxDetalles (este último con @OrderBy("id")).

Lógica embebida en la entidad

Trx tiene un @PreUpdate que, si hay cae y caeFchVto, calcula caeBarCode con calculoDigitoVerificador(cuit, codComp, ptoVta, cae, vtoCae) (algoritmo del código de barras AFIP). También expone helpers @Transient: getFullComprobante(), getFullFullComprobante(), getComprobanteFechaDMY/YMD(), getCaeFchVtoDMY(), getMedioPagoFull().

Hijos de Trx

TrxDetalle (@Table(name = "TrxDetalle"))

Ítem de línea. PK id IDENTITY. trx (@ManyToOne LAZY, nullable=false, @JsonIgnore+@XmlTransient), tipoIva (@ManyToOne). Campos: ean, descripcion (String); cantidad, precioNetoUnitario, impuestoInternoUnitario, precioTotal (Double).

AlicuotaIva (@Table(name = "Iva"))

PK id IDENTITY. trx (@ManyToOne LAZY, nullable=false), tipoIva (@ManyToOne). Campos: baseImponible, importe (double).

Tributo (@Table(name = "Tributos"))

PK id IDENTITY. trx (@ManyToOne LAZY), tipoTributo (@ManyToOne). Campos: descripcion (String), baseImponible, alicuota, importe (double). Helper @Transient getDescripcionFull() (anexa (%alicuota) si > 0).

Opcional (@Table(name = "Opcionales"))

Ojo con la PK: el campo identidad se llama uid y mapea la columna uid (IDENTITY). El campo id (String) es el código del dato opcional AFIP, no la PK. valor (String) es el contenido. trx (@ManyToOne LAZY, nullable=false).

CAEA

Caea (@Table(name = "Caeas"))

CAEA otorgado por AFIP. PK natural: caea (String), no autogenerada. enteFacturador (@ManyToOne LAZY, nullable=false, @JsonIgnore/@XmlTransient). Campos: periodo, orden (Integer); fchVigDesde, fchVigHasta, fchTopeInf, fchProceso (String); movimientos (Integer, default 0, @JsonIgnore/@XmlTransient). Constantes: PERIODO_FORMAT_YYYYMM, FCH_FORMAT_YYYYMMDD, QUINCENA_1=1, QUINCENA_2=2.

CaeaSinMovimiento (@Table(name = "CaeaSinMovimientos"))

Informe "sin movimiento" de un PV bajo un CAEA. Clave compuesta vía @IdClass(CaeaSinMovimientoId.class): caea (String) + enteFacturador (@ManyToOne LAZY, parte del id) + ptoVta (int). Campo fchProceso (Date, default new Date()).

EnteFacturador (@Table(name = "EntesFacturadores"))

Comercio emisor. PK id IDENTITY. Es donde vive la config AFIP por comercio: el cert ya no sale del .yml. El sistema es multi-comercio: hay N entes (no uno solo), cada uno con su CUIT, su certificado .p12, su TA de WSAA y sus SucPosPV. Es una entidad de ABM: se da de alta, edita y habilita/deshabilita (baja lógica vía habilitado) desde el cockpit, bajo /api/cockpit/comercios. El flag habilitado define qué entes participan de la renovación de TA y de la emisión.

Campo Columna Nota
razonSocial razonSocial (unique)
cuit cuit (unique en DB) helper @Transient getCuitAsLong(). UNIQUE filtrado por la migración (ver nota); la entidad NO declara unique=true.
direccion, condicionIva, ingresosBrutos, inicioActividades idem Datos fiscales.
backGroundFacturaPath idem PDF (getPdf): path de filesystem al PDF template del comprobante — el fondo/branding del comercio sobre el que PdfFacturaService appendea el texto. Ver Email y PDF.
layout idem (int) PDF: copias impresas — 1=ORIGINAL, 2=ORIGINAL+DUPLICADO, 0=sin PDF configurado (el generador rechaza layout fuera de 1..2).
backGroundHeaderPath, emailPath idem Heredados del legacy: header y plantilla de email. emailPath es para el email (pendiente); el PDF del comprobante no usa estos dos.
token, sign idem (length=2000) TA de WSAA cacheado (@XmlTransient).
expirationTime expirationTime (@Temporal TIMESTAMP) Vencimiento del TA.
habilitado habilitado (booleanbit)
certP12Path, certP12Password, certP12Alias idem (@XmlTransient) Certificado AFIP por comercio (antes en afip.cert.*). Si alias vacío se usa el CUIT.

CUIT único por comercio (a nivel DB)

El cuit es único, porque el POS v2 identifica su comercio por CUIT (getCaea?cuit=NN) y el server resuelve el ente por CUIT: la relación debe ser 1:1. La unicidad la impone SOLO la DB, vía el índice UNIQUE filtrado UQ_EntesFacturadores_cuit ON EntesFacturadores(cuit) WHERE cuit IS NOT NULL (migración add-EntesFacturadores-cuit-unique.sql). El mapeo JPA deja cuit como columna plana sin unique=true — coherente con hbm2ddl=none (el DDL lo aplica el cliente, no lo genera Hibernate) y con que H2 de tests no restringe. El filtro WHERE cuit IS NOT NULL tolera varias filas con CUIT nulo. Ver Persistencia.

Certificado AFIP write-only

El certificado (y los secretos del TA: token, sign) se guardan pero no se devuelven. En la entidad, los getters de certP12Path/Password/Alias, token y sign llevan @XmlTransient (no salen por SOAP/JAXB). En el ABM del cockpit, el DTO de salida EnteFacturadorView omite certP12Password@XmlTransient no frena a Jackson, así que la garantía es la lista blanca del DTO, no la anotación— y expone en su lugar un booleano derivado certConfigurado. El POST/PUT del cert acepta la clave; ninguna respuesta JSON la refleja. Por la misma razón el toString() de la entidad redacta token y sign (imprime [REDACTED] / null, nunca el valor): el toString se loguea y el TA no debe llegar a logs ni traza.

SucPosPV (@Table(name = "SucPosPV"))

Relaciona Sucursal + POS físico con los nros de PV de AFIP. PK id IDENTITY. Lleva el dueño comercial del POS (idEnteFacturador), porque siendo multi-comercio dos comercios pueden compartir el mismo (suc, pos) y por eso ese par ya NO identifica al comercio. El lookup pasa a ser por (ente, suc, pos).

Campo Columna Nota
suc, pos suc, pos (nullable=false) Sucursal y POS físico.
pvcae pvcae (nullable=false) PV de AFIP para CAE.
pvcaea pvcaea (Integer, nullable) PV de AFIP para CAEA.
idEnteFacturador idEnteFacturador (Integer) Dueño comercial del POS. Scalar int (mismo patrón que suc/pos/pvcae, NO @ManyToOne). En DB es FK a EntesFacturadores y NOT NULL; el mapeo JPA lo deja nullable porque el DDL (FK + NOT NULL) lo aplica la migración (hbm2ddl=none) y H2 de tests no lo restringe.
validadoArcaPvcae validadoArcaPVCae (@Temporal TIMESTAMP) Fecha validación del PV CAE contra AFIP (FEParamGetPtosVenta). null = no validado.
validadoArcaPvcaea validadoArcaPVCaea (@Temporal TIMESTAMP) Idem CAEA.
fechaUltObtencionPos fFechaUltObtencionPOS (@Temporal TIMESTAMP) Handoff de una sola vez: getCaea lo sella; el ABM lo resetea a null ante cualquier cambio.

Los UNIQUE llevan el ente adelante

Los tres índices UNIQUE de SucPosPV se recrearon con idEnteFacturador como primera columna: (idEnteFacturador, suc, pos), (idEnteFacturador, pvcae) y (idEnteFacturador, pvcaea) —este último filtrado WHERE pvcaea IS NOT NULL—. Así dos comercios pueden compartir (suc, pos) (o un pvcae) sin chocar, pero un mismo comercio no puede duplicarlos. En Trx —que ya tenía idEnteFacturador— los dos UNIQUE de NextGen (UQ_Trx_nextgen, UQ_Trx_pv_comprobante_aprobado, ambos WHERE version='V2') también pasaron a llevar el ente adelante. NO se agrega UNIQUE sobre V1 (data legacy con numeración repetida legítima). El DDL vive en src/main/resources/db/migration/add-ente-SucPosPV-step*.sql y add-ente-Trx-index.sql.

Tarea / JobEjecucion / TrxErrorLog

Tarea (@Table(name = "Tareas"))

Definición de un job Quartz. PK id (String, @NotNull). Campos: descripcion, programacion (cron, @NotNull), jobProcessor (clase del job, @NotNull).

JobEjecucion (@Table(name = "JobEjecucion"))

Bitácora de una corrida de un job (la graba CommonJob al terminar; el cockpit muestra la última por job). PK id IDENTITY. Campos: tareaId (String, nullable=false, length=100), jobNombre (String, length=200), inicio (@Temporal TIMESTAMP, nullable=false), fin (@Temporal TIMESTAMP), resultado ('OK'|'ERROR', length=10), error (String, length=2000).

TrxErrorLog (@Table(name = "TrxErrorLog"))

Error de transacción para la grilla del cockpit. Dos orígenes: 'AFIP' (servicio→ARCA, lo graba CockpitMetrics.recordError) y 'POS' (POS→servicio, lo graba TrafficLoggingFilter). PK id IDENTITY. Campos: fecha (@Temporal TIMESTAMP, nullable=false), origen ('POS'|'AFIP', length=10), tipo (CAE/CAEA/TOKEN/CONSULTA o SOAP/REST, length=30), mensaje (raw, length=2000), mensajeCurado (mensaje humano extraído del raw por MensajeCurador.curar() — SOAP fault / JSON de error / rechazo AFIP; length=500, nullable), referencia (URI POS o detalle AFIP, length=300). El mensajeCurado lo consumen la grilla del cockpit y el futuro job de alarmas, que filtra/notifica sin re-parsear XML. La columna la agrega la migración add-TrxErrorLog-mensajeCurado.sql (ver Persistencia).

Enums-tabla AFIP

No son enum de Java

Son entidades JPA (@Entity) que mapean tablas de catálogo AFIP. Funcionan como enums de dominio pero viven en la base. PK no autogenerada (la define AFIP), salvo donde se indique.

Entidad Tabla PK Campos
TipoComprobante TiposComprobante id (Integer) descripcion, letra, codigo, leyenda (String); discriminaIva, discriminaTributos, usaSubtotal (booleanbit). Constructor TipoComprobante(Integer id).
TipoDocumento TiposDocumento id (Integer) descripcion (unique).
TipoConcepto TiposConcepto id (Integer) descripcion (unique).
TipoIva TiposIva id (Integer) descripcion (unique), alicuota (double).
TipoMoneda TiposMonedas id (String) descripcion (unique).
TipoTributo TiposTributo id (Integer) descripcion (unique).

TipoComprobante, TipoDocumento, TipoConcepto y TipoMoneda llevan @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) (serialización JSON con proxies lazy de Hibernate).

Por dónde seguir