# Capacidad: Generación del PDF de la factura y del QR tributario

El PDF que genera el SIF **es la factura legal** y sustituye al de XTRF. Decisiones de referencia:
**D3** (PDF antes del envío), **D18** (mPDF), **D19** (plantilla traducible), **D20** (fallo del PDF),
**D22** (QR), **D23** (exposición del directorio).

Fuente: `docs/aeat-especificaciones/DetalleEspecificacTecnCodigoQRfactura.pdf` (v0.5.0, 10/12/2025).

## ADDED Requirements

### Requirement: Contenido y formato de la URL del QR

La URL del QR DEBE construirse con **exactamente cuatro parámetros, en este orden**: `nif`,
`numserie`, `fecha`, `importe`.

| Parámetro | Contenido | Formato |
|---|---|---|
| `nif` | NIF del obligado a expedir | 9 caracteres |
| `numserie` | Nº de serie + nº de factura | Máx. 60, ASCII 32–126 |
| `fecha` | Fecha de expedición | `DD-MM-AAAA` |
| `importe` | Importe total | Punto decimal, 2 decimales |

Cada **valor** DEBE codificarse con URL encoding en UTF-8 (`rawurlencode`), no la URL completa.

Los valores DEBEN ser **idénticos** a los del registro de facturación correspondiente.

#### Scenario: URL de una factura real

- **WHEN** se genera el QR de la factura `195/2026` del obligado `B12345678`, fecha `28-08-2026`,
  importe `937.44`, en entorno de pruebas
- **THEN** la URL es
  `https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR?nif=B12345678&numserie=195%2F2026&fecha=28-08-2026&importe=937.44`
- **AND** la barra del número de factura aparece codificada como `%2F`

#### Scenario: Número de factura con ampersand

- **WHEN** el número de factura contiene `&`
- **THEN** se codifica como `%26`
- **AND** no rompe la separación de parámetros de la URL

#### Scenario: El importe usa punto, nunca coma

- **WHEN** el importe total es 937,44 €
- **THEN** el parámetro es `importe=937.44`
- **AND** nunca `937,44`, que produciría el error de validación 2005 del servicio de cotejo

#### Scenario: Coherencia con el registro

- **WHEN** se genera el QR
- **THEN** `numserie`, `fecha` e `importe` coinciden carácter a carácter con `NumSerieFactura`,
  `FechaExpedicionFactura` e `ImporteTotal` del registro
- **AND** un test lo verifica de forma automática

---

### Requirement: El QR nunca lleva el parámetro `formato`

La URL incrustada en el código QR NO DEBE incluir el parámetro `formato=json` bajo ninguna
circunstancia.

#### Scenario: Parámetro prohibido

- **WHEN** se construye la URL del QR
- **THEN** no contiene `formato` en ninguna forma
- **AND** tampoco se añade `idioma`, que es opcional y no se usa en fase 1

> El documento oficial lo marca en mayúsculas: *"IMPORTANTE: este parámetro nunca podrá incorporarse
> en la «URL» que va en el código «QR» de la factura"*.

---

### Requirement: Características gráficas del código QR

El código QR DEBE cumplir:

- Norma **ISO/IEC 18004:2015**.
- Nivel de corrección de errores **M (medio)**.
- Tamaño entre **30 × 30 mm y 40 × 40 mm**. Valor por defecto: **40 × 40 mm**.
- Zona muda (espacio en blanco) de **al menos 2 mm** en los cuatro lados. Valor por defecto: **6 mm**,
  que es el recomendado.
- Contraste suficiente con el fondo.

#### Scenario: Generación con los parámetros correctos

- **WHEN** se genera el QR
- **THEN** el nivel de corrección es M, no L ni Q ni H
- **AND** el módulo resultante se escala a 40 mm en el PDF, no en píxeles

#### Scenario: Tamaño fuera de rango

- **WHEN** se configura un tamaño de QR menor de 30 mm o mayor de 40 mm
- **THEN** el sistema rechaza el valor indicando el rango legal

---

### Requirement: Ubicación y leyendas en la factura

El código QR DEBE situarse **al principio de la factura**, en la primera página, **una sola vez**,
aunque la factura tenga varias páginas. En formato vertical va arriba, preferiblemente centrado.

DEBEN acompañarlo dos textos:

- **Encima**: `QR tributario:`
- **Debajo**: `Factura verificable en la sede electrónica de la AEAT` (o la forma corta `VERI*FACTU`)

Ambos DEBEN ir **siempre en castellano y de forma literal**, cualquiera que sea el idioma de la
factura, con tipo de letra y tamaño legibles, **iguales o superiores** a los del resto de datos de la
factura.

#### Scenario: Factura en alemán

- **WHEN** se genera una factura con `idioma = de`
- **THEN** el cuerpo de la factura se traduce al alemán
- **AND** los textos `QR tributario:` y `Factura verificable en la sede electrónica de la AEAT`
  aparecen en castellano, sin traducir
- **AND** puede añadirse debajo una glosa traducida en cuerpo menor, sin sustituir al literal

#### Scenario: Factura de 19 páginas

- **WHEN** la factura ocupa 19 páginas
- **THEN** el QR aparece únicamente en la página 1
- **AND** las páginas siguientes repiten la cabecera de la tabla pero no el QR

#### Scenario: El QR es el primero de la factura

- **WHEN** la factura contuviera otros códigos QR para otros fines
- **THEN** el QR tributario es el primero y ocupa un lugar preeminente, claramente diferenciado

---

### Requirement: Plantilla PDF traducible

La plantilla DEBE implementarse con Twig, con catálogos de traducción por idioma. El idioma lo indica
el JSON de XTRF.

Orden de resolución: idioma indicado → `en` → `es`.

#### Scenario: Idioma sin catálogo

- **WHEN** el JSON indica un idioma para el que no hay catálogo
- **THEN** la factura se genera en inglés
- **AND** se registra un aviso indicando el idioma no soportado, para poder añadirlo

#### Scenario: Idioma ausente

- **WHEN** el JSON no indica idioma
- **THEN** se usa inglés
- **AND** las leyendas VERI\*FACTU siguen en castellano

#### Scenario: Caracteres no latinos

- **WHEN** la factura contiene líneas en griego, turco, checo, polaco, húngaro, lituano o letón
- **THEN** todos los caracteres se representan correctamente en el PDF
- **AND** no aparecen cajas vacías ni signos de interrogación

> Verificado sobre facturas reales del share: una sola factura puede mezclar todos esos idiomas en la
> tabla de líneas. Es el motivo de elegir mPDF (D18).

---

### Requirement: Fidelidad a la plantilla actual de XTRF

La plantilla DEBE reproducir la estructura de la factura actual de XTRF. **Congelada pendiente de
DEP-2**; lo ya observado en facturas reales y que la plantilla debe incluir:

- Cabecera con tipo de documento (`FACTURA` / `RECHNUNG` / …) y `# {numero}/{año}`.
- Bloque de cliente: razón social, identificador fiscal (`CIF:` / `VAT number:`), dirección, país.
- Fecha de expedición.
- Condiciones de pago y fecha de vencimiento.
- Datos de pago: SWIFT e IBAN.
- Tabla de líneas con columnas: referencia del cliente, combinación de idiomas, referencia interna,
  base imponible, impuesto, total.
- Bloque de totales: subtotal, descuentos con su descripción, impuesto y total.
- Paginación `n / N`.

#### Scenario: Comparación con el original

- **WHEN** se genera el PDF de una factura ya existente en el share
- **THEN** la disposición, los bloques y las columnas son reconocibles como la misma plantilla
- **AND** además incorpora el QR y las leyendas, que el original no tiene

---

### Requirement: Nomenclatura y despliegue del fichero

El PDF generado DEBE depositarse en
`/home/jboss/xtrf/03_Invoices/Customer_invoices/YYYY/MM/`, donde `YYYY` y `MM` corresponden a la fecha
de expedición de la factura.

El nombre del fichero DEBE ser el que XTRF usa para esa factura, de forma que el PDF del SIF
**sustituya** al de XTRF. La regla de saneado del nombre de cliente queda pendiente de **DEP-7**;
mientras tanto, el nombre destino DEBE tomarse del JSON de XTRF si este lo aporta.

El SIF DEBE conservar además **una copia propia e inmutable** en
`/var/lib/verifactu/pdf/{id_registro}.pdf`, con su SHA-256 registrado en `facturas.pdf_hash`.

#### Scenario: Despliegue correcto

- **WHEN** se genera el PDF de la factura `195/2026` con fecha 28-08-2026
- **THEN** se escribe en `/home/jboss/xtrf/03_Invoices/Customer_invoices/2026/08/`
- **AND** se guarda la copia propia en `/var/lib/verifactu/pdf/`
- **AND** `pdf_estado` pasa a `DESPLEGADO`

#### Scenario: Archivado del PDF original de XTRF

- **WHEN** el destino ya contiene un PDF generado por XTRF
- **THEN** el original se copia antes a `/var/lib/verifactu/pdf-originales/` conservando su nombre
- **AND** después se sobrescribe con el del SIF

#### Scenario: Colisión con otra factura

- **WHEN** el nombre de destino se resuelve a un fichero que corresponde a un **número de factura
  distinto**
- **THEN** el SIF **no** sobrescribe
- **AND** abre una incidencia de severidad alta
- **AND** deja `pdf_estado = FALLIDO`

#### Scenario: Regeneración de la misma factura

- **WHEN** se regenera el PDF de una factura que ya tenía uno desplegado del SIF
- **THEN** se sobrescribe, se recalcula el hash y se registra la acción en auditoría

---

### Requirement: El share CIFS puede fallar y eso no debe romper nada

El montaje es CIFS con opción `soft`, por lo que las escrituras pueden fallar de forma transitoria.
El SIF DEBE:

1. Verificar que el punto de montaje está montado **antes** de escribir.
2. Escribir a un fichero temporal en el mismo directorio y renombrar al nombre final.
3. Releer el fichero y verificar su SHA-256 tras la escritura.

#### Scenario: Share no montado

- **WHEN** el punto de montaje no está disponible
- **THEN** el despliegue no se intenta
- **AND** `pdf_estado` queda `GENERADO` (la copia propia sí existe) y se reintenta más tarde
- **AND** se abre incidencia si persiste

#### Scenario: Escritura incompleta

- **WHEN** la verificación posterior a la escritura da un hash distinto del esperado
- **THEN** el fichero se considera no desplegado
- **AND** se reintenta según la política de reintentos

---

### Requirement: Fallo del PDF tras un envío correcto a la AEAT

Si la generación o el despliegue del PDF falla, el registro de facturación **no se toca**: es
inmutable y sigue siendo válido.

`pdf_estado` DEBE reintentarse con retardo creciente (1, 5, 15, 60 y 360 minutos). Tras 5 fallos DEBE
abrirse una incidencia y ofrecerse la acción manual **"Regenerar PDF"**.

La notificación de **éxito** a XTRF NO DEBE enviarse mientras el PDF no esté desplegado.

#### Scenario: PDF falla, registro ya remitido

- **WHEN** el registro está `CORRECTO` en la AEAT y el PDF falla
- **THEN** el registro sigue `CORRECTO`
- **AND** `pdf_estado = PENDIENTE` con reintentos
- **AND** no se notifica éxito a XTRF
- **AND** la UI muestra la factura en la cola de pendientes con el motivo real

#### Scenario: Regeneración manual

- **WHEN** un operador pulsa "Regenerar PDF"
- **THEN** el PDF se reconstruye a partir del **registro**, no del JSON original
- **AND** el QR resultante es idéntico al que habría tenido, porque depende solo de datos congelados

#### Scenario: Nunca hay PDF sin registro

- **WHEN** se intenta generar un PDF para una factura sin registro de facturación
- **THEN** la operación se rechaza
- **AND** se registra el intento como anomalía

---

### Requirement: Exposición del directorio de facturas

`/var/www/html/verifactu/invoices` DEBE ser un **bind mount** de
`/home/jboss/xtrf/03_Invoices/Customer_invoices`, declarado en `/etc/fstab` con
`x-systemd.requires-mounts-for=/home/jboss/xtrf`.

El acceso HTTP directo a esa ruta DEBE estar **denegado** en el vhost (`Require all denied`).

Los PDFs DEBEN servirse a través de un controlador PHP que resuelva id de registro → ruta.

#### Scenario: Acceso directo denegado

- **WHEN** alguien solicita `https://verifactu.xtrf.abroadlink.com/invoices/2026/08/195_2026-Cliente.pdf`
- **THEN** Apache responde `403 Forbidden`
- **AND** no se sirve el fichero ni se lista el directorio

#### Scenario: Acceso desde la interfaz

- **WHEN** un usuario abre el PDF desde el detalle de un registro
- **THEN** el controlador `GET /registro/{id}/pdf` lo sirve por streaming
- **AND** el acceso queda registrado en `auditoria`

#### Scenario: El montaje sobrevive a un reinicio

- **WHEN** el servidor se reinicia
- **THEN** el bind mount se restablece automáticamente después del montaje CIFS del que depende
- **AND** el worker verifica el montaje antes de cada despliegue

> Con la UI y el endpoint públicos (decisión 6 del usuario), servir `/invoices` como directorio
> estático publicaría **todas las facturas de todos los clientes desde 2013**. La denegación en
> Apache es la única mitigación de RIESGO-2 que no depende de activar la autenticación.
