Config y perfiles¶
Configuración Spring del backend: archivos application*.yml, perfiles, variables de entorno y bloque AFIP. Todo sale de src/main/resources/application*.yml (+ el application-realdb.yml de test). Para la persistencia ver Persistencia; para el cliente AFIP, Cliente AFIP.
Secretos siempre por env var
DB y certificado AFIP nunca van en el repo. Las contraseñas se inyectan por variable de entorno. El único archivo con un password literal es application-realdb.yml, que está gitignored y es solo para validar mapeos contra una DB de prod restaurada en local.
Perfiles¶
| Perfil | Archivo | DB | AFIP | Uso |
|---|---|---|---|---|
| base | application.yml |
— | HOMOLOGACION | Config común a todos. Default: dev. |
dev |
application-dev.yml |
SQL Server local (localhost:1433, db tifacturaonline) |
hereda HOMOLOGACION | Desarrollo. Log a DEBUG. |
prod |
application-prod.yml |
SQL Server del cliente (todo por env) | PRODUCCION | Producción. Log a INFO. |
realdb |
application-realdb.yml (test, gitignored) |
prod restaurada local | — | Validación de mapeos JPA (read-only). |
| (test) | src/test/resources/application.yml |
H2 | — | Tests; pisa al base en el classpath de test. |
Perfil por defecto: spring.profiles.default: dev. Para activar otro: --spring.profiles.active=prod o SPRING_PROFILES_ACTIVE=prod.
application.yml (base)¶
spring:
application:
name: tifactura-spring
profiles:
default: dev
datasource:
hikari:
maximum-pool-size: ${DB_POOL_MAX:20}
connection-timeout: ${DB_CONN_TIMEOUT_MS:30000}
leak-detection-threshold: 60000
quartz:
wait-for-jobs-to-complete-on-shutdown: true
jpa:
hibernate:
ddl-auto: none
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
open-in-view: false
database-platform: com.tipre.tifacturaonlinemanager.action.db.SQLServerDialect
cxf:
path: /TiFacturaOnlineManagerWS
cockpit:
auth:
user: ${COCKPIT_USER:}
password: ${COCKPIT_PASS:}
admin-secret: ${COCKPIT_ADMIN_SECRET:}
revalidar-arca-al-guardar: ${COCKPIT_REVALIDAR_ARCA:false}
runner-enabled: ${COCKPIT_RUNNER_ENABLED:false}
jobs-alerta-umbral: 3
afip:
mode: HOMOLOGACION
wsaa-url: https://wsaahomo.afip.gov.ar/ws/services/LoginCms
wsfe-url: https://wswhomo.afip.gob.ar/wsfev1/service.asmx
service: wsfe
dstdn: cn=wsaahomo,o=afip,c=ar,serialNumber=CUIT 33693450239
ticket-time-ms: 43200000 # 12h
tls-insecure: true
cuit: ${AFIP_CUIT:}
comprobantes-por-request: 1
connect-timeout-ms: 15000
read-timeout-ms: 60000 # WSFE (FECAESolicitar y consultas)
wsaa-read-timeout-ms: 30000 # WSAA (LoginCms)
retry:
max-attempts: ${AFIP_RETRY_MAX_ATTEMPTS:1}
backoff-ms: ${AFIP_RETRY_BACKOFF_MS:500}
logging:
level:
com.tipre.tifacturaonlinemanager: INFO
Pool JDBC (HikariCP)¶
El pool ahora es explícito (hardening F1). El default de Hikari era 10 conexiones; con 10 workers de Quartz, las emisiones V2 (cada una puede tener 2 conexiones en vuelo por el REQUIRES_NEW del persist-before-emit) y el polling del cockpit, 10 se agotan sin que haya ningún bug. Sube a 20 por default.
| Property | Env var | Default | Efecto |
|---|---|---|---|
spring.datasource.hikari.maximum-pool-size |
DB_POOL_MAX |
20 |
Tamaño máximo del pool JDBC. |
spring.datasource.hikari.connection-timeout |
DB_CONN_TIMEOUT_MS |
30000 (30 s) |
Espera máxima por una conexión del pool. Es el default efectivo de Hikari; bajarlo sería observable bajo carga. |
spring.datasource.hikari.leak-detection-threshold |
— | 60000 (60 s) |
Solo diagnóstico: loguea un WARN con stacktrace si una conexión se retiene más de 60 s. No cierra nada. |
Dimensionar el pool a la flota de POS (hallazgo del stress test)
El default 20 alcanza para un parque chico, pero no para una flota. La prueba de carga mostró que con 200 POS emitiendo en paralelo el pool entra en connection-starvation: cada emisión en vuelo retiene una conexión (el persist-before-emit toma una en su REQUIRES_NEW), así que con menos conexiones que POS activos las emisiones se encolan esperando el pool. El perfil de stress usa DB_POOL_MAX=220 (200 POS + headroom). La regla para producción: dimensioná DB_POOL_MAX al pico de POS simultáneos (y hacelo coherente con el max worker threads de SQL Server). Ver Pruebas de carga.
Shutdown ordenado¶
spring.quartz.wait-for-jobs-to-complete-on-shutdown: true (hardening F1): al frenar el servicio (NSSM stop / SIGTERM), Spring espera a que los jobs Quartz en vuelo terminen antes de bajar el contexto. Matar la reconciliación de NC o un informe CAEA a mitad de camino es recuperable (persist-before-emit) pero evitable.
El graceful shutdown HTTP ya es default
No hay server.shutdown ni spring.lifecycle.timeout-per-shutdown-phase en el .yml: el graceful shutdown del server HTTP (server.shutdown=graceful, 30 s por fase) es default desde Spring Boot 3.4. Lo único que se agrega acá es esperar a los jobs Quartz.
OPS: subir el grace de NSSM
Para que NSSM no mate el proceso antes de que terminen los jobs, subí el timeout del stop:
application-dev.yml¶
spring:
datasource:
url: jdbc:sqlserver://localhost:1433;databaseName=tifacturaonline;encrypt=true;trustServerCertificate=true
username: ${DB_USER:sa}
password: ${DB_PASS:}
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
logging:
level:
com.tipre.tifacturaonlinemanager: DEBUG
# DEV/test: el runner de validación del cockpit está ACTIVO (es el ambiente de pruebas).
cockpit:
runner-enabled: true
# Credenciales de dev para el cockpit. Cambiar en prod por env (COCKPIT_USER / COCKPIT_PASS).
auth:
user: admin
password: changeme
AFIP queda en HOMOLOGACION (heredado del base). DB local: usuario default sa, password vacío por default. El runner del cockpit queda activo (cockpit.runner-enabled: true). Las credenciales de auth del cockpit traen los defaults de dev admin / changeme (cambiar en prod por env var).
application-prod.yml¶
spring:
datasource:
url: jdbc:sqlserver://${DB_HOST}:${DB_PORT:1433};databaseName=${DB_NAME};encrypt=true;trustServerCertificate=true
username: ${DB_USER}
password: ${DB_PASS}
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
afip:
mode: PRODUCCION
wsaa-url: https://wsaa.afip.gov.ar/ws/services/LoginCms
wsfe-url: https://servicios1.afip.gov.ar/wsfev1/service.asmx
dstdn: cn=wsaa,o=afip,c=ar,serialNumber=CUIT 33693450239
logging:
level:
com.tipre.tifacturaonlinemanager: INFO
# PROD: el runner de validación queda DESHABILITADO (la página es solo monitoreo, no emite nada).
cockpit:
runner-enabled: false
En prod todos los hosts y secretos de DB son env vars sin default → si faltan, la app no levanta (intencional: nada hardcodeado). El runner del cockpit queda apagado (cockpit.runner-enabled: false): aunque alguien pegue a /api/cockpit/test/* por URL, esos endpoints responden 403.
Fail-fast de credenciales del cockpit en prod (hardening F2)
En perfil prod, arrancar el cockpit con las credenciales default de dev (admin / changeme, o COCKPIT_USER / COCKPIT_PASS en blanco) aborta el arranque: SecurityConfig.warnIfDefaultCredentials() tira IllegalStateException si detecta credenciales default con el perfil prod activo. Arrancar un cockpit de prod con admin/changeme es peor que no arrancar. En cualquier otro perfil (dev/test) sigue siendo un WARN, no un fallo. Esto no viola la regla "no bootloop por AFIP caído": eso aplica a fallas transitorias externas; unas env vars sin setear son misconfig determinista del operador (item del checklist del runbook de deploy).
Variables de entorno¶
| Variable | Perfil | Default | Efecto |
|---|---|---|---|
DB_USER |
dev / prod | sa (dev) / sin default (prod) |
Usuario de la DB. |
DB_PASS |
dev / prod | vacío (dev) / sin default (prod) | Password de la DB. |
DB_HOST |
prod | sin default | Host SQL Server. |
DB_PORT |
prod | 1433 |
Puerto SQL Server. |
DB_NAME |
prod | sin default | Nombre de la base. |
AFIP_CUIT |
base | vacío | CUIT del emisor (afip.cuit). |
DB_POOL_MAX |
base | 20 |
Tamaño máximo del pool Hikari (spring.datasource.hikari.maximum-pool-size). |
DB_CONN_TIMEOUT_MS |
base | 30000 |
Espera máxima por conexión del pool (...hikari.connection-timeout). |
AFIP_RETRY_MAX_ATTEMPTS |
base | 1 |
Reintentos del transporte SOAP read-only (afip.retry.max-attempts). 1 = sin reintentos (comportamiento actual). |
AFIP_RETRY_BACKOFF_MS |
base | 500 |
Backoff entre reintentos read-only (afip.retry.backoff-ms). |
COCKPIT_USER |
base | vacío → admin (dev) |
Usuario del cockpit (cockpit.auth.user). Login del SecurityFilterChain. |
COCKPIT_PASS |
base | vacío → changeme (dev) |
Password del cockpit (cockpit.auth.password). Con credenciales default: WARN al arrancar en dev/test; en prod el arranque aborta (fail-fast, ver arriba). |
COCKPIT_ADMIN_SECRET |
base | vacío | Secret del ABM del cockpit (header X-Cockpit-Secret). Vacío → ABM deshabilitado (fail-closed). |
COCKPIT_REVALIDAR_ARCA |
base | false |
Si true, al alta/edición de SucPosPV revalida los PV (CAE/CAEA) contra AFIP antes de guardar. |
COCKPIT_RUNNER_ENABLED |
base | false |
Habilita el runner de validación del cockpit (/api/cockpit/test/*). Fail-closed: si false, esos endpoints responden 403. El perfil dev lo pone en true; prod en false. |
LOG_DIR |
logback | logs |
Directorio de los archivos de log. |
SPRING_PROFILES_ACTIVE |
Spring | dev |
Perfil activo. |
El cert AFIP NO es env var
El certificado .p12 (path, clave y alias) no sale del .yml ni de env vars: vive en la tabla EntesFacturadores por comercio (certP12Path, certP12Password, certP12Alias). Ver Modelo de dominio y Cliente AFIP.
Config AFIP (afip.*)¶
Estas properties las consume AfipClientConfig (vía @Value con los mismos defaults). El bloque base es HOMOLOGACION; application-prod.yml lo sobreescribe a PRODUCCION.
| Property | HOMOLOGACION (base) | PRODUCCION (prod) |
|---|---|---|
afip.mode |
HOMOLOGACION |
PRODUCCION |
afip.wsaa-url |
https://wsaahomo.afip.gov.ar/ws/services/LoginCms |
https://wsaa.afip.gov.ar/ws/services/LoginCms |
afip.wsfe-url |
https://wswhomo.afip.gob.ar/wsfev1/service.asmx |
https://servicios1.afip.gov.ar/wsfev1/service.asmx |
afip.dstdn |
cn=wsaahomo,o=afip,c=ar,serialNumber=CUIT 33693450239 |
cn=wsaa,o=afip,c=ar,serialNumber=CUIT 33693450239 |
afip.service |
wsfe |
wsfe |
afip.ticket-time-ms |
43200000 (12 h) |
idem |
afip.tls-insecure |
true |
idem (heredado; sobrescribible) |
afip.cuit |
${AFIP_CUIT:} |
idem |
afip.comprobantes-por-request |
1 |
idem |
afip.connect-timeout-ms |
15000 (15 s) |
idem |
afip.read-timeout-ms |
60000 (60 s) — WSFE |
idem |
afip.wsaa-read-timeout-ms |
30000 (30 s) — WSAA |
idem |
afip.retry.max-attempts |
${AFIP_RETRY_MAX_ATTEMPTS:1} |
idem |
afip.retry.backoff-ms |
${AFIP_RETRY_BACKOFF_MS:500} |
idem |
Timeouts configurables (hardening F1)
afip.connect-timeout-ms, afip.read-timeout-ms y afip.wsaa-read-timeout-ms son los timeouts del transporte HTTPS hacia ARCA. Antes estaban hardcodeados en los transportes; ahora salen por property, con los mismos valores históricos como default → si no los tocás, el comportamiento es idéntico. Los consume AfipClientConfig al construir cada HttpsSoapTransport / HttpsWsaaSoapTransport. WSFE usa el read-timeout general (60 s, cubre FECAESolicitar y las consultas); WSAA (LoginCms) usa su propio read-timeout (30 s).
El retry es SOLO para consultas read-only
afip.retry.* habilita reintentos con backoff únicamente en los clientes read-only (FEDummy, FECompUltimoAutorizado, FECompConsultar), que reciben un HttpsSoapTransport envuelto en RetryingSoapTransport. Con max-attempts=1 (default) el decorador es pass-through — cero reintentos, comportamiento actual EXACTO. El retry nunca se aplica a: FECAESolicitar (emisión, no idempotente — su recuperación es app-level: persist-before-emit + rama-3), CAEA (WsfeCaeaClient, cliente mixto con writes), ni WSAA (AFIP rechaza el re-login con un TA vigente). Activar 2 recién tras un soak en QA. Caveat: ultimoAutorizado corre dentro del NumeracionLock (rama-4) → cada retry sostiene el lock hasta un read-timeout más.
tls-insecure = true
Con afip.tls-insecure: true el cliente AFIP hace trust-all del cert y no verifica el hostname de ARCA — es el equivalente al Https.java del legacy (que tampoco validaba). Está forzado como en prod legacy. Para volver a TLS verificado, poné false (en el perfil que corresponda). En AfipClientConfig el default del @Value es false, pero el application.yml lo fuerza a true.
CXF (SOAP)¶
cxf.path: /TiFacturaOnlineManagerWS monta el CXFServlet. CxfSoapConfig publica 3 endpoints bajo ese path, cada uno sirviendo el WSDL baseline de producción:
| Endpoint | URL resultante | WSDL |
|---|---|---|
TrxWsEndpoint |
/TiFacturaOnlineManagerWS/TrxWS |
wsdl/TrxWS.wsdl |
CaeaWsEndpoint |
/TiFacturaOnlineManagerWS/CaeaWS |
wsdl/CaeaWS.wsdl |
TfomWsEndpoint |
/TiFacturaOnlineManagerWS/TiFacturaOnlineManagerWS |
wsdl/TiFacturaOnlineManagerWS.wsdl |
Detalle en API SOAP.
Cockpit¶
cockpit.auth.user / cockpit.auth.password (env COCKPIT_USER / COCKPIT_PASS) son las credenciales de autenticación del cockpit (D5). Las consume SecurityConfig, que arma un SecurityFilterChain con un InMemoryUserDetailsManager (rol COCKPIT, password BCrypt): todo lo que cae bajo /api/cockpit/**, /cockpit, /cockpit-test, /cockpit-prod y /cockpit.html queda autenticado (HTTP Basic para la API, form login para la UI); los endpoints de facturación (/TiFacturaOnlineManagerWS/**, /ws/**) quedan abiertos. Si user/pass vienen vacíos se usan los defaults de dev (admin / changeme) y se loguea un WARN al arrancar — en prod hay que setearlos por env.
cockpit.admin-secret (env COCKPIT_ADMIN_SECRET) es un candado extra sobre el ABM de SucPosPV, que opera vía header X-Cockpit-Secret dentro de la ruta ya autenticada: Spring Security verifica user/pass primero, y recién después el controller chequea el secret. Es fail-closed: si no hay secret configurado, el ABM responde deshabilitado. cockpit.revalidar-arca-al-guardar (env COCKPIT_REVALIDAR_ARCA, default false) controla si se revalidan los PV contra AFIP al guardar. cockpit.runner-enabled (env COCKPIT_RUNNER_ENABLED, default false) habilita el runner de validación (/api/cockpit/test/*): también es fail-closed (si false, esos endpoints responden 403), activo en dev y apagado en prod. cockpit.jobs-alerta-umbral (default 3) es el umbral de la alerta roja de jobs: un job entra en alerta cuando sus últimas N ejecuciones consecutivas terminaron en ERROR (bitácora JobEjecucion). La consumen el cockpit y sus endpoints; el lado del job en Jobs Quartz. Ver Cockpit.
Logging¶
Configurado en src/main/resources/logback-spring.xml (sobre los defaults de Spring Boot). Variable LOG_DIR (default logs).
| Appender / logger | Archivo | Rotación | Notas |
|---|---|---|---|
CONSOLE |
— | — | Salida estándar (root). |
APP_FILE |
${LOG_DIR}/tifactura.log |
10 MB/archivo, 30 días, cap 500 MB | Log general (root). |
TRAFFIC_FILE |
${LOG_DIR}/traffic.log |
20 MB/archivo, 30 días, cap 2 GB | Tráfico POS↔servicio + gateway↔ARCA, payloads JSON/XML completos. |
logger TRAFFIC |
→ TRAFFIC_FILE |
— | additivity=false: el tráfico va solo a traffic.log, no ensucia consola ni app log. Lo alimenta TrafficLoggingFilter. |
Niveles del paquete com.tipre.tifacturaonlinemanager: INFO en base/prod, DEBUG en dev.
Config operativa fuera del YAML
Mail, URLs y flags operativos no están en el application.yml: siguen saliendo de la tabla Parametro (config que el cliente cambia en caliente). El .yml solo tiene infraestructura y secretos.
Por dónde seguir¶
- Backend — stack y build.
- Persistencia — datasource y dialecto.
- Cliente AFIP — cómo se usa el bloque
afip.*. - Setup de desarrollo — levantar con el perfil dev.