TiFactura · Notificador AFIP

Seis facturadores, un solo servicio.

De dónde partimos, cómo quedó unificado, en qué se diferencia cada cliente, y —lo más importante— cómo configurar cada uno sin que quede una sola duda.

6 clientes · 1 jar 5/6 verificados contra BD real selección por TIFACTURA_CLIENT cockpit en vivo →

01 — De dónde partió

Seis proyectos casi gemelos, cada uno con su propio jar

Cada cliente (DINO, ALMACOR, BECERRA, TIZIANO, CARACOL, CYRE) era un proyecto Maven separado. El ~90 % del código era idéntico entre los seis; la variación real vivía en apenas 5 archivos. El problema no era el tamaño: era que cada cambio había que hacerlo seis veces. Un ajuste regulatorio de AFIP (como la RG 5782) significaba tocar, compilar y desplegar seis repos a mano — y con eso, seis oportunidades de que uno quedara distinto.

1 cambio regulatorio (RG 5782) DINO jar propio entity · converter scheduler · repo ALMACOR jar propio ~90% igual BECERRA jar propio ~90% igual TIZIANO jar propio ~90% igual CARACOL jar propio ~90% igual CYRE jar propio ~90% igual
Antes — un cambio arriba se convertía en seis despliegues idénticos abajo. La duplicación era el costo, no la funcionalidad.

02 — Cómo se unificó

Un núcleo compartido + un borde chico por cliente

El servicio unificado tiene un núcleo que se escribe una sola vez (el armado del comprobante, el scheduler que notifica a AFIP, el cliente HTTP al Manager) y que no sabe nada de clientes: trabaja contra un contrato neutral, ComprobanteRecord. Cada cliente aporta solo dos piezas chicas —cómo lee su tabla y qué impuestos emite— que se enchufan según la variable TIFACTURA_CLIENT. El mismo jar arranca como DINO, como CARACOL o como cualquiera de los seis.

UN SOLO JAR · TIFACTURA_CLIENT=<cliente> NÚCLEO COMPARTIDO TrxAssembler · ComprobanteNotifier Manager client · contrato ComprobanteRecord se escribe 1 sola vez RG 5782 · reconciliación · fixes → para los 6 Read-model del cliente mapea su tabla (de_tresu / detresu) Estrategia de tributos qué impuestos emite Manager → AFIP comprobante electrónico seleccionadas por TIFACTURA_CLIENT
Después — el núcleo (centro) es común a los seis; solo las dos ranuras de la izquierda cambian por cliente, y las elige la variable de entorno.

El recorrido de un comprobante

Puesto en fila, se ve qué es compartido (una vez para todos) y qué es por cliente:

Base del cliente ComprobanteRecord contrato neutral TrxAssembler + estrategia tributos Manager → AFIP reader por cliente arma compartido Trx tributos por cliente · resto compartido
El único borde por cliente (en bronce) es leer su tabla y elegir sus tributos. Todo lo del medio — fechas, documento, IVAs, notas de crédito, envío — se comparte.

03 — Arquitectura técnica

Cómo está armado el servicio, por dentro

Stack: Spring Boot 2.4.1 sobre Java 8, contra SQL Server (driver mssql-jdbc), empaquetado como jar ejecutable con Maven (com.tipre:tifactura-sql-rest, versión CalVer — hoy 20260804, la misma que se ve en el cockpit y en /status). Un solo Application.java con @SpringBootApplication + @EnableScheduling; nada de perfiles Spring por cliente — la selección es por property, ver más abajo.

Estructura de paquetes

com.tipre.tifactura.sql.rest
Application.java                    # @SpringBootApplication, @EnableScheduling
contract/
  ComprobanteRecord                 # read-model normalizado (interface)
  TributoStrategy                   # qué tributos emite el cliente (interface)
core/
  convert/
    TrxAssembler                    # arma el Trx — compartido, sin lógica de cliente
    ComprobanteConverter            # elige la TributoStrategy según tifactura.client
  read/
    ComprobanteReader               # puerto de lectura, esquema-agnóstico
  schedule/
    ComprobanteNotifier             # scheduler único (reemplaza los 6 TresuNotifier)
  status/   StatusHolder            # estado en memoria para /status y el cockpit
  startup/  StartupConfigLogger     # vuelca la config al log, secretos redactados
client/
  dino/ almacor/ becerra/ tiziano/ caracol/ cyre/  # 1 ComprobanteReader por cliente
  g1/ g2/ g3/                       # las 3 TributoStrategy reales
schema/retail/
  Almacor|Becerra|Tiziano|Caracol|CyreTresu # @Entity + su *Repository
model/
  sql/  Tresu                     # entidad original de DINO, implementa ComprobanteRecord
  trx/  Trx, Tributo, AlicuotaIva, ...  # DTO que viaja al Manager
web/
  StatusController                  # GET /status
  CockpitController                 # GET /cockpit, /, /doc
  client/ TiFacturaOnlineManagerClient  # cliente HTTP hacia el Manager

Las tres costuras

El núcleo compartido no tiene ni un if (cliente == "dino"). Toda la variación entra por tres puntos de extensión, cada uno resuelto por Spring según tifactura.client:

Costura A — Read-model por cliente

ComprobanteReader + entidad @Entity
Absorbe nombres de columna, @Formula, y decisiones de esquema (el /100 de IIBB vive acá). Cada entidad implementa ComprobanteRecord delegando a sus propios getters.

Costura B — Estrategia de tributos

TributoStrategy (G1 / G2 / G3)
Absorbe qué impuestos se emiten. TrxAssembler le delega solo el bloque de tributos; el resto del Trx lo arma él mismo, igual para los 6.

Selección — tifactura.client

@ConditionalOnProperty + switch en ComprobanteConverter
Una sola property decide las dos costuras a la vez. Valor inválido → el servicio no arranca (falla explícita).
ComprobanteNotifier @Scheduled(cron) ComprobanteReader del cliente activo ComprobanteConverter TrxAssembler + TributoStrategy ManagerClient POST fecaeaSolicitar Manager → AFIP Comprobante Record Trx StatusHolder — cuenta procesados/errores y guarda el resultado de la corrida (en memoria, sin BD)
Cada instancia solo tiene registrado en el contexto de Spring el ComprobanteReader de su propio cliente (los otros 5 quedan fuera por @ConditionalOnProperty); todo lo demás del flujo es exactamente el mismo bean para los seis.

El contrato ComprobanteRecord

Es la interfaz que hace posible que TrxAssembler no sepa nada de clientes. Expone getters neutrales — identidad del comprobante (id, ptoVta, tipoFactura, nroComprobante, cuit, fechaComprobante, caea, clientType), reconciliación RG 5782/CAE-CAEA (nroSuc, nroPos, nroTicket), comprobante asociado para NC/ND, importes, tributos (IIBB, Com.Ind., Percepción IVA, Impuesto Interno) y el desglose de IVA (21/10,5/exento). Cada entidad @Entity por cliente implementa la interfaz delegando a sus propios getters existentes — un getter que no aplica a esa familia de esquema devuelve null/0, nunca lanza.

Selección por tifactura.client

La property tifactura.client (env TIFACTURA_CLIENT) decide dos cosas a la vez, en dos lugares distintos del código:

Cómo agregar un cliente nuevo

El patrón ya se repitió cinco veces (ALMACOR, BECERRA, TIZIANO, CARACOL, CYRE detrás de DINO); agregar un séptimo cliente es mecánico:

  1. Entidad @Entity que mapee la tabla real del cliente e implemente ComprobanteRecord (schema/retail/<Cliente>Tresu.java) — copiar el patrón de AlmacorTresu si el esquema es de_tresu (prefijo húngaro) o de CaracolTresu si es detresu (columnas planas), con sus @Formula donde haga falta.
  2. Su *Repository (Spring Data JPA) con findPV(), findInvoicesToNotify(), updateFechaAfip().
  3. Su ComprobanteReader (client/<cliente>/<Cliente>ComprobanteReader.java) implementando el puerto, con @ConditionalOnProperty(name = "tifactura.client", havingValue = "<cliente>").
  4. Mapear cliente→estrategia en el switch de ComprobanteConverter — reusar G1/G2/G3 si el impuesto que emite ya existe, o sumar una G4 si aparece una combinación nueva.
  5. Golden de caracterización en src/test/resources/golden/<cliente>/: casos representativos (factura A/B, NC/ND, con/sin percepción, CUIT/DNI/sin documento) capturados contra el comportamiento del converter original de ese cliente, para congelar el "antes" antes de tocar nada.
La fidelidad de los 6 clientes actuales está cubierta por esa misma batería de golden tests de caracterización (src/test/resources/golden/, un set por cliente) más una revisión adversarial doble ("Judgment Day") contra los seis proyectos originales — veredicto registrado en PLAN-UNIFICACION.md: el Trx resultante coincide con el original salvo, exactamente, los cambios deliberados de la Sección 5. Un cliente nuevo se da por migrado cuando su golden queda verde, no antes.

04 — Los seis clientes, con nombre y apellido

Qué hace distinto cada uno

Esta es la tabla maestra. El color del borde indica la base: TipreRetail · de_tresu o TipreSucursales · detresu.

Matriz por cliente
Cliente Base · tabla Impuestos que emite Cambios respecto de su versión vieja Verificado BD real
DINO TipreRetail · de_tresu IIBB · Com.Ind. · Percepción IVA — es la base de referencia (ya mandaba todo bien)
ALMACOR TipreRetail · de_tresu IIBB · Com.Ind. · Impuesto Interno — ya mandaba NroSuc/Pos/Ticket y CbteFchHsGen
BECERRA TipreRetail · de_tresu IIBB · Com.Ind. · Impuesto Interno + NroSuc/Pos/Ticket
TIZIANO TipreRetail · de_tresu IIBB · Com.Ind. · Impuesto Interno + NroSuc/Pos/Ticket · total desde fprecioTotal (preservado)
CARACOL TipreSucursales · detresu IIBB · Com.Ind. · Percepción IVA + Impuesto Interno + NroSuc/Pos/Ticket · IIBB ÷100 (corregido) falta BD propia
CYRE TipreSucursales · detresu IIBB · Com.Ind. · Impuesto Interno + NroSuc/Pos/Ticket · IIBB ÷100 (corregido)

Lo mismo, agrupado por nombre real

Donde varios clientes comparten una característica, acá están juntos — sin códigos, con nombre:

Emite Percepción IVA

DINO
Único que emite solo percepción IVA.

Emite Impuesto Interno

ALMACOR · BECERRA · TIZIANO · CYRE
Mismo set de tributos (comparten estrategia internamente).

Emite ambos

CARACOL
Percepción IVA + Impuesto Interno a la vez.

Base TipreRetail de_tresu

DINO · ALMACOR · BECERRA · TIZIANO
Columnas con prefijo (iid, fnetofiscal1, ccuit…).

Base TipreSucursales detresu

CARACOL · CYRE
Columnas planas (id, netofiscal1, cuit…).

IIBB ÷100 corregido

CARACOL · CYRE
Antes enviaban la alícuota ×100 (era un bug).

Nota técnica (para el equipo de desarrollo): internamente la estrategia de tributos se llama G1 (Percepción IVA = DINO), G2 (Impuesto Interno = ALMACOR, BECERRA, TIZIANO, CYRE) y G3 (ambos = CARACOL). Para configurar un cliente no necesitás saber esto — solo el nombre del cliente. Los G1/G2/G3 son detalle de implementación.

05 — Ajustes y correcciones

Qué cambió a propósito al unificar

Unificar preservó el comportamiento de cada cliente byte por byte, salvo estos cambios deliberados — decididos, no accidentales. Cada uno quedó auditado contra un test que congela el "antes" y el "esperado con el cambio".

Cambios deliberados
CambioQué esA quién afectaTipo
CbteFchHsGen siempre La fecha/hora real de generación (RG 5782, obligatoria) ahora se informa siempre, sin depender de un flag. Los 6 (antes 4 lo condicionaban a modo_dual) mejora
NroSuc / NroPos / NroTicketPos La identidad física del ticket (para la reconciliación CAE-CAEA) ahora se envía siempre. BECERRA · TIZIANO · CARACOL · CYRE (no los mandaban) mejora
IIBB ÷100 La alícuota de Ingresos Brutos se divide por 100, como toda la flota. Antes se enviaba ×100. CARACOL · CYRE bug corregido
Fecha de comprobante asociado Preservado como está hoy (no se setea en NC/ND). Pendiente de datos de producción para decidir. CARACOL · CYRE preservado
importeTotal desde fprecioTotal Preservado: TIZIANO toma el total de una columna, no lo recalcula. Falta confirmar contra prod que coincide con la suma. TIZIANO preservado
Los dos "preservados" son deuda de datos, no de código. Se resuelven con una consulta a la base de producción de esos clientes (¿existe la columna de fecha en detresu? ¿fprecioTotal = suma de líneas?) o viendo cómo responde AFIP. Hasta entonces, el servicio hace exactamente lo que hacía antes.

06 — Instalación

Poner una instancia en pie, por cliente

Cada cliente es una carpeta con el mismo jar adentro y su propio config al lado. El jar nunca lleva credenciales ni datos de un cliente en particular — todo lo que lo distingue vive fuera, en config\application-prod.yml.

Layout de instalación

C:\TiFactura\<cliente>\
  tifactura-sql-rest-20260804.jar   # el mismo binario para los 6 clientes
  config\
    application-prod.yml            # copiado de config-template\ y editado para ESTE cliente
  run.bat                            # opcional — ver Sección 07
  log\                                 # se crea solo al primer arranque

De dónde sale el jar

Requisitos

07 — Ejecución / operación

Arrancarlo y saber que está vivo

Con run.bat

Pensado para no tener que acordarse de nada. Parado en la carpeta que tiene el jar al lado:

Con java -jar directo

Sirve para el layout de instalación de la Sección 06 (jar + config\ al lado, sin target\). Hay que estar parado en esa carpeta al ejecutar:

cd C:\TiFactura\almacor
java -jar tifactura-sql-rest-20260804.jar
Si se corre desde otro directorio, o como servicio Windows (donde el directorio de trabajo no es necesariamente la carpeta de instalación), Spring Boot no va a encontrar .\config\application-prod.yml solo. Hay que decírselo explícito con --spring.config.additional-location:
java -jar tifactura-sql-rest-20260804.jar --spring.config.additional-location=file:C:/TiFactura/almacor/config/

El log de arranque

Al levantar, StartupConfigLogger vuelca en el log toda la configuración efectiva (perfil activo + cada property de la app), una sola vez, apenas el contexto de Spring está listo. Cualquier clave cuyo nombre contenga password, secret, token, credential u otro marcador equivalente se imprime como **** — nunca en texto plano. Es la forma más rápida de confirmar, con solo mirar el log, qué cliente es esta instancia y contra qué está apuntando.

Dónde quedan los logs

Consola (STDOUT) y archivo, según logback.xml: rotación diaria o cada 50 MB, con 30 días de historial. La ruta del archivo la fija logging.file.name — en la plantilla de prod es ./log/tifactura.log, relativa a la carpeta desde donde se ejecuta el jar (por eso importa el directorio de trabajo, igual que con el config externo).

08 — Configuración por cliente

Cómo levantar cada uno (sin dudas)

Toda la config entra por variables de entorno — no hay nada hardcodeado. Para que una instancia sea un cliente u otro, cambian solo tres cosas: qué cliente es, a qué base pega, y qué IDs de tributo usa. El resto es común.

Lo que cambia por cliente

Diferencias de configuración
Cliente TIFACTURA_CLIENT DATABASE_NAME ID_TRIBUTO_PERCIVA ID_TRIBUTO_IMPINTERNO
DINOdinoTipreRetailusa ✓no usa —
ALMACORalmacorTipreRetailno usa —usa ✓
BECERRAbecerraTipreRetailno usa —usa ✓
TIZIANOtizianoTipreRetailno usa —usa ✓
CARACOLcaracolTipreSucursalesusa ✓usa ✓
CYREcyreTipreSucursalesno usa —usa ✓

Los IDs marcados "no usa" pueden quedar sin definir (toman 0 por defecto): esa estrategia no se ejecuta para ese cliente. Definir el que corresponde con el valor real de ese cliente en su base.

Lo que es común a todos

# Base de datos (host/puerto propios de cada cliente)
DATABASE_HOST=...   DATABASE_PORT=1433
DATABASE_USER=...   DATABASE_PASSWORD=...
# Operación
NOTIFY_TO_AFIP_CRON_SCHEDULER=0 0/5 * * * ?
TIFACTURAONLINEMANAGER_SERVICE_URL=http://.../manager
PROCESS_MAX_RESULTS=50   LOGGING_FILE_NAME=/var/log/tifactura.log
# Tributos/IVA comunes (IDs reales de AFIP en Manager)
ID_ENTE_FACTURADOR=   ID_TRIBUTO_ING_BRUTO=   ID_TRIBUTO_COMIND=
ID_IVA_0=   ID_IVA_105=   ID_IVA_21=

Dos formas de fijar estas mismas claves, según cómo se instale (ver Sección 06): variables de entorno (arriba, las consume application-prod.properties empaquetado en el jar) o directamente en config\application-prod.yml externo, con notación anidada YAML. Son la misma configuración — el YML externo, cuando existe, tiene prioridad y no necesita ninguna variable de entorno.

Referencia completa — cada clave del YAML

config\application-prod.yml
Clave (YAML)Env var equivalenteQué es¿Cambia por cliente?
tifactura.clientTIFACTURA_CLIENTCuál de los 6 clientes es esta instancia — decide el ComprobanteReader y la TributoStrategy (Sección 03).
spring.datasource.urlDATABASE_HOST/PORT/NAMECadena JDBC hacia SQL Server (host, puerto, nombre de base).
spring.datasource.usernameDATABASE_USERUsuario de esa base.
spring.datasource.passwordDATABASE_PASSWORDPassword de esa base. Nunca va dentro del jar ni se commitea.
notify.to.afip.cron.schedulerNOTIFY_TO_AFIP_CRON_SCHEDULERExpresión cron de cuándo corre ComprobanteNotifier.no (normalmente)
tifacturaonlinemanager.service.urlTIFACTURAONLINEMANAGER_SERVICE_URLURL base del TiFacturaOnline Manager al que se notifica.no (por Manager, no por cliente)
process.max.resultsPROCESS_MAX_RESULTSTope de comprobantes que se traen por punto de venta en cada corrida.no
id.ente.facturadorID_ENTE_FACTURADORId del ente facturador (comercio) en el Manager/AFIP.no (uno por comercio)
id.tributo.ing.brutoID_TRIBUTO_ING_BRUTOId AFIP del tributo Ingresos Brutos.no
id.tributo.comindID_TRIBUTO_COMINDId AFIP del tributo Comercio e Industria.no
id.tributo.percivaID_TRIBUTO_PERCIVAId AFIP de Percepción IVA.solo lo usan DINO y CARACOL
id.tributo.impinternoID_TRIBUTO_IMPINTERNOId AFIP de Impuesto Interno.todos menos DINO
id.iva.0 / id.iva.105 / id.iva.21ID_IVA_0 / ID_IVA_105 / ID_IVA_21Ids AFIP de las tres alícuotas de IVA (0 %, 10,5 %, 21 %).no
logging.file.nameLOGGING_FILE_NAMERuta del archivo de log de esta instancia.no (salvo que convenga separarlos)

Las únicas claves que de verdad distinguen a un cliente de otro son las cuatro primeras filas (marcadas "sí") más id.tributo.perciva / id.tributo.impinterno según qué tributo emita — coincide exactamente con la tabla de la subsección siguiente.

Bloque específico, cliente por cliente

Cada tarjeta muestra solo lo que distingue a ese cliente. Combinalo con el bloque común de arriba.

DINOTipreRetail
Emite: Percepción IVA
TIFACTURA_CLIENT=dino
DATABASE_NAME=TipreRetail
ID_TRIBUTO_PERCIVA=<real>
# ID_TRIBUTO_IMPINTERNO no aplica
ALMACORTipreRetail
Emite: Impuesto Interno
TIFACTURA_CLIENT=almacor
DATABASE_NAME=TipreRetail
ID_TRIBUTO_IMPINTERNO=<real>
# ID_TRIBUTO_PERCIVA no aplica
BECERRATipreRetail
Emite: Impuesto Interno
TIFACTURA_CLIENT=becerra
DATABASE_NAME=TipreRetail
ID_TRIBUTO_IMPINTERNO=<real>
# ID_TRIBUTO_PERCIVA no aplica
TIZIANOTipreRetail
Emite: Impuesto Interno
TIFACTURA_CLIENT=tiziano
DATABASE_NAME=TipreRetail
ID_TRIBUTO_IMPINTERNO=<real>
# ID_TRIBUTO_PERCIVA no aplica
CARACOLTipreSucursales
Emite: Percepción IVA + Impuesto Interno
TIFACTURA_CLIENT=caracol
DATABASE_NAME=TipreSucursales
ID_TRIBUTO_PERCIVA=<real>
ID_TRIBUTO_IMPINTERNO=<real>
CYRETipreSucursales
Emite: Impuesto Interno
TIFACTURA_CLIENT=cyre
DATABASE_NAME=TipreSucursales
ID_TRIBUTO_IMPINTERNO=<real>
# ID_TRIBUTO_PERCIVA no aplica
Verificar al arrancar: el servicio loguea toda su config al iniciar (con el password redactado), y expone GET /status — ahí ves en vivo qué cliente sos, qué tributos emite, contra qué base pega y cuándo es la próxima notificación. Si TIFACTURA_CLIENT tiene un valor inválido, el servicio no arranca (falla claro, no silencioso).

09 — El cockpit

Ver el estado en vivo, sin entrar a la base

Cada instancia expone un tablero liviano en /cockpit (y el dato crudo en /status). Muestra de un vistazo qué cliente es, qué impuestos emite, contra qué base pega, y cómo salió la última corrida. Se autorefresca solo cada 60 s. Así se ve corriendo como ALMACOR:

Es parte del propio jar — no depende de nada externo (la librería del tablero, ArrowJS, también viaja adentro). Cambiás TIFACTURA_CLIENT y el mismo cockpit muestra otro cliente. Link en vivo: /cockpit.

El refresh NO impacta en la base, sin importar su tamaño. El endpoint /status que consulta el cockpit no toca la base de datos: solo lee configuración ya cargada en memoria y contadores en memoria que el scheduler actualiza. Podés refrescarlo cada segundo, con muchas pestañas abiertas, y una TipreRetail de millones de filas no se entera — no hay query, no hay lock, no hay demora. Lo único que consulta la base es el scheduler (según el cron), con las mismas queries que ya corrían los 6 servicios originales — la unificación no agregó ni una consulta.

Endpoints

Rutas que sirve el propio jar
RutaQué devuelve¿Toca la base?
GET /cockpitEl tablero visual (HTML) de esta sección: cliente activo, tributos, próxima notificación, resultado de la última corrida y la versión del build.no
GET /Redirige a /cockpit (misma respuesta).no
GET /docEste manual — el mismo doc.html autocontenido, servido por la app.no
GET /statusEl JSON crudo detrás del cockpit: client, version, cron, nextNotification, datos de la última corrida (lastRunStarted/Finished/Processed/Errors/Outcome) y el bloque config (esquema, estrategia de tributos, datasource, IDs — sin passwords).no

Ninguna de las cuatro rutas ejecuta una consulta SQL: /status y /cockpit leen únicamente StatusHolder (en memoria, lo actualiza el scheduler) y properties ya resueltas al arrancar — cero carga y cero bloqueos sobre la base del cliente, la tengan del tamaño que la tengan. /status también expone tifactura.version (la misma versión del pom.xml que se ve como pill en el cockpit), así que un vistazo alcanza para saber qué build está corriendo cada instancia.

10 — Troubleshooting

Cuando algo no arranca o no cierra

Problemas frecuentes
SíntomaQué está pasandoQué hacer
No arranca, falla apenas se ejecuta Falta config\application-prod.yml al lado del jar. El perfil prod está fijo en application.properties (spring.profiles.active=prod) — sin el YML externo, cae al application-prod.properties empaquetado, que espera las variables de entorno DATABASE_HOST/etc., y sin ellas Spring no puede resolver los placeholders. Si usás run.bat, el primer arranque ya lo crea solo (Sección 07) y se frena para que lo edites. Si corrés java -jar a mano, copiá config-template\application-prod.yml a config\application-prod.yml y editalo antes de levantar el servicio.
"Unknown tifactura.client" al arrancar tifactura.client tiene un valor que no es ninguno de los 6 reconocidos. ComprobanteConverter lanza IllegalStateException en su constructor — es intencional: preferible que no arranque a que arranque sin estrategia de tributos. Revisar el valor exacto de tifactura.client en el YML — debe ser, en minúsculas: dino, almacor, becerra, tiziano, caracol o cyre.
"Invalid column name" al consultar La base real de ese cliente no tiene alguna columna que el read-model espera (por ejemplo, las columnas de reconciliación nropos/nroticket o inropos/inroticket — ver Sección 11, siguen sin confirmar en las 4 bases de producción). Verificar el esquema real de esa base con el integration test correspondiente, pasando -Dit.db=true (por ejemplo mvn -Dit.db=true test apuntando al datasource de ese cliente) antes de poner la instancia en producción.
Password en texto plano config\application-prod.yml queda fuera del jar y fuera del control de versiones (la carpeta config\ local está en .gitignore) — es el único lugar donde debe vivir la contraseña real de la base. Nunca hardcodearla en application-*.properties ni commitearla: es justamente lo que se corrigió en application-dev.properties (antes traía credenciales de desarrollo hardcodeadas).

11 — Estado

Dónde está parado hoy