Saltar a contenido

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 SOAP getPdf está 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 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() (default simple); hay variantes simple/factura/dino/ferniplast. Se portan a HTML Thymeleaf en resources/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/cc separados por ; (getToInList()/getCCInList()).
  • Imagen de pie embebida: el PNG de footer por comercio (addInline).
  • Asunto y nombre del adjunto: se replican exactos del .xhtml legacy (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 caller TiFacturaOnlineManagerWS.

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 Parametro cambia el envío sin redeploy.
  • to/cc con 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):

  • backGroundFacturaPath null/blank o el archivo no existe en el filesystem → GeneralException PDF_NO_CONFIGURADO.
  • layout fuera de 1..2GeneralException PDF_NO_CONFIGURADO (los entes creados por el ABM quedan con layout=0 y sin template hasta que se cargue).
  • Trx inexistente → GeneralException PDF_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 caeBarCode persistido (Code128Writer, imagen 150×80 estirada a 250×15 — igual que el legacy).
  • QR de AFIP: QRCodeWriter sobre la URL https://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