Saltar a contenido

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:

nssm set TipreTiFactura AppStopMethodConsole 45000

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