Email y PDF¶
El backend genera el PDF de la factura y (a futuro) lo enviará por email al cliente final. En el legacy esto vivía sobre Seam Mail (Facelets .xhtml) y Seam PDF/Renderer. Son dos piezas con estado distinto:
- PDF — IMPLEMENTADO. El PDF del comprobante se genera con PDFBox nativo + ZXing (port fiel del legacy
TrxToPdfHome), y la operación SOAPgetPdfestá cableada. Es lo que describe la mayor parte de esta página. - Email — PENDIENTE. El envío del correo (
sendEmail) sigue siendo un stub. Estaba especificado en SPEC-012 (Thymeleaf +JavaMailSender), pero todavía no está portado.
Estado del EMAIL: PENDIENTE de implementación
A diferencia del PDF (ya en código), el email todavía no está portado. Verificado contra el repo: no existen las clases EmailHome, DynamicMailSender ni plantillas en resources/templates/email/, y en pom.xml el bloque de dependencias de SPEC-012 (spring-boot-starter-mail, spring-boot-starter-thymeleaf) sigue comentado. La operación SOAP sendEmail lanza UnsupportedOperationException — ver API SOAP. La sección Email — SPEC-012 describe el diseño previsto, no código existente: tratalo como plan.
Estado del PDF: IMPLEMENTADO (2026-07-02)
El PDF sí está portado. getPdf re-lee la Trx, regenera el PDF on-demand con PDFBox y lo devuelve en base64; el documento nunca se persiste. En pom.xml están activas las dependencias PDFBox 2.0.24 + ZXing 3.5.3 (core + javase) + org.json. La sección PDF de esta página describe el subsistema real. Hay muestras reales generadas por getPdf sobre el template de producción en docs/pdf-muestra/ (repo del código).
iText y Flying Saucer NO se portaron
El path Seam-PDF/Renderer (createPDFDetalle, flying-saucer-pdf-itext5, itextpdf) era código muerto en el legacy: la migración lo descartó. Las properties itext.version y flyingsaucer.version quedan declaradas en pom.xml pero ninguna dependencia las usa. El PDF productivo sale únicamente del path PDFBox nativo.
Email — SPEC-012¶
Objetivo¶
Reemplazar el envío del legacy (EmailHome.send() → renderer.render("email/X.xhtml") + mail:mail-session de Seam) por Thymeleaf + JavaMailSender, construido dinámicamente desde la configuración en la tabla Parametro, preservando exactamente destinatarios, asunto, cuerpo y adjunto (el PDF).
Config dinámica desde Parametro¶
La config de mail NO vive en application.yml
Es una invariante del proyecto (§5.10): host/puerto/TLS/usuario/clave/from/baseUrl de mail salen de la tabla Parametro vía ParametroList, no del application.yml. El motivo: el cliente cambia esos valores en caliente sin redeploy. El application.yml lo dice explícito: "mail/URLs/flags operativos NO van acá: siguen saliendo de la tabla Parametro". Ver Configuración y perfiles.
El plan (SPEC-012) es un DynamicMailSender que arma un JavaMailSenderImpl por envío con los parámetros actuales de ParametroList (getMailHost, getMailPort, getMailUsuario, getMailClave, getMailTls, etc.). El flag TLS legacy se modela como mail.smtp.starttls.enable y vale true cuando el parámetro es "1".
Lo que se preserva (contrato con el cliente final)¶
- Plantillas por comercio: el legacy resuelve la plantilla por
enteFacturador.getEmailPath()(defaultsimple); hay variantessimple/factura/dino/ferniplast. Se portan a HTML Thymeleaf enresources/templates/email/, modelando solo el cuerpo (from/to/subject/adjunto pasan a ser parámetros del envío, no de la plantilla). - Destinatarios múltiples:
to/ccseparados por;(getToInList()/getCCInList()). - Imagen de pie embebida: el PNG de footer por comercio (
addInline). - Asunto y nombre del adjunto: se replican exactos del
.xhtmllegacy (suelen incluir tipo de comprobante, PV y número) — es contrato con el cliente. - Misma API pública de
EmailHome(setIdTrx/setTo/setCC/send/getPath/…) para no tocar el callerTiFacturaOnlineManagerWS.
Criterios de aceptación (SPEC-012)¶
EmailHome.send()produce un correo con el mismo from/to/cc/asunto/cuerpo/adjunto que el legacy, comparado contra un correo viejo del golden set.- Cambiar el host en la tabla
Parametrocambia el envío sin redeploy. to/cccon varios destinatarios separados por;funcionan.
PDF — implementado¶
Port fiel del legacy TrxToPdfHome a PDFBox 2.0 nativo (Seam EVENT-scoped → Spring stateless). El generador es PdfFacturaService (com.tipre.tifacturaonlinemanager.action.pdf), un @Component stateless. El PDF nunca se persiste: se regenera on-demand desde la fila Trx.
PdfFacturaService — el generador¶
generar(Integer idTrx) (anotado @Transactional(readOnly = true)) re-lee la Trx, valida la config de PDF y devuelve un PdfGenerado(String nombre, byte[] pdf) (nombre = fullFullComprobante + ".pdf").
La mecánica es idéntica al legacy: carga como documento base el PDF template del comercio (EnteFacturador.backGroundFacturaPath, un path de filesystem — asset por comercio, igual que el .p12) y appendea texto en coordenadas absolutas con las fuentes built-in Courier/Courier-Bold (PDType1Font). La alineación derecha se logra con padding de espacios porque la fuente es monoespaciada. Genera una página por Pagina y por copia ORIGINAL/DUPLICADO según layout, y mergea todo con PDFMergerUtility.
Fail-closed si el comercio no tiene PDF configurado
generar valida antes de intentar (guard nuevo — el legacy reventaba con NPE/IndexOutOfBounds):
backGroundFacturaPathnull/blank o el archivo no existe en el filesystem →GeneralExceptionPDF_NO_CONFIGURADO.layoutfuera de1..2→GeneralExceptionPDF_NO_CONFIGURADO(los entes creados por el ABM quedan conlayout=0y sin template hasta que se cargue).Trxinexistente →GeneralExceptionPDF_TRX_INEXISTENTE.
Maquetado — Paginator / Pagina / Linea¶
El detalle del comprobante se pagina con las clases del paquete action.pdf (port fiel del Paginator legacy):
| Clase | Rol |
|---|---|
Paginator |
Corta el detalle de la Trx en páginas. 51 líneas por página (LINES); si se pasa, corta en 49 + línea en blanco + "Continua en hoja N". Descripciones cortadas por palabra a 45 chars (60 si ningún detalle tiene EAN — la columna EAN libera espacio). Una línea en blanco tras cada ítem. Sin detalle → única línea "No contine Detalle" (sic — typo del legacy, replicado por paridad). |
Pagina |
Las líneas de una página + el subtotal arrastrado de las páginas previas. getSubtotal() = arrastre + suma de los totales de sus líneas. |
Linea |
POJO de una línea del detalle: cantidad, ean, descripcion, unitario, total. Línea vacía = separador; línea solo-descripción = continuación de una descripción larga o texto de control. |
Código de barras y QR — ZXing¶
- Code128: el código de barras del
caeBarCodepersistido (Code128Writer, imagen 150×80 estirada a 250×15 — igual que el legacy). - QR de AFIP:
QRCodeWritersobre la URLhttps://www.afip.gob.ar/fe/qr/?p={base64(json)}con los campos AFIP (ver,fecha,cuit,ptoVta,tipoCmp,nroCmp,importe,moneda,ctz,tipoCodAut,codAut).
PARIDAD del QR — bug de prod replicado a propósito
El legacy serializa la entidad TipoComprobante en el JSON (json.put("tipoCmp", trx.getTipoComprobante()), que org.json serializa como su toString()). El port usa org.json (el mismo repackage que org.primefaces.json del legacy) para producir el mismo QR, bug incluido. Corregirlo sería una divergencia observable respecto de producción. Los EnteFacturador.getCuit() y demás salen idénticos.
Template y branding por comercio¶
El aspecto del comprobante (logo, membrete, marcos) vive en el PDF template del comercio, no en el código: el generador solo appendea texto y las imágenes de barcode/QR sobre esa base. Los campos que lo configuran son de EnteFacturador:
| Campo | Rol en el PDF |
|---|---|
backGroundFacturaPath |
Path de filesystem al PDF template del comprobante (el fondo/branding del comercio). Es el documento base que se carga por página. |
layout |
Copias impresas: 1 = solo ORIGINAL, 2 = ORIGINAL + DUPLICADO. 0 = sin PDF configurado (el generador rechaza). |
Se administran desde el ABM de comercios del cockpit — ver Cockpit y Modelo de dominio.
backGroundHeaderPath NO lo usa este generador
EnteFacturador tiene también backGroundHeaderPath (header) y emailPath (plantilla de email), heredados del legacy. El PDF del comprobante usa solo backGroundFacturaPath + layout; emailPath es para el email (pendiente).
Reglas de contenido replicadas del legacy¶
El generador replica fielmente la lógica de negocio del comprobante legacy: leyenda MONOTRIBUTO (Ley 27.618), régimen de transparencia fiscal (Ley 27.743, solo comprobantes B a Consumidor Final), discriminación de IVA y tributos solo en la última página y según el tipo de comprobante, subtotales arrastrados entre páginas, y la letra/código/leyenda del TipoComprobante. Todo el mapeo PDFBox 1.8 → 2.0 (setTextMatrix absoluto en vez de setTextTranslation, showText, LosslessFactory, drawImage, Base64 en vez de DatatypeConverter) se hizo preservando el output.
Relación email ↔ PDF¶
El email depende del PDF: el adjunto del correo es el PDF del comprobante. Como el PDF ya está implementado (PdfFacturaService.generar), cuando se porte el email deberá reusar esa misma fuente para que el adjunto coincida exactamente con el documento que devuelve getPdf y que ve el cliente. Hoy el email es lo único pendiente de este par.
Por dónde seguir¶
- Configuración y perfiles — por qué la config de mail vive en
Parametroy no enapplication.yml. - El ciclo de facturación — cuándo, en el flujo, se emite el PDF y se manda el correo.
- Modelo de dominio —
EnteFacturador(emailPath, layout) yParametro.