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 (boolean → bit) |
|
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 (boolean → bit). 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¶
- Persistencia — dialecto, flush MANUAL, naming, DDL.
- Ciclo de facturación — cómo se usan estas entidades.
- Invariantes — reglas que no se rompen.