# Capacidad: Registro de facturación, huella y encadenamiento

El núcleo del SIF. Decisiones de referencia: **D7** (serialización única), **D8** (escritor único),
**D9** (primer registro y error 2007), **D24** (inmutabilidad).

Fuente: `docs/aeat-especificaciones/Veri-Factu_especificaciones_huella_hash_registros.pdf` (v0.1.2),
`docs/aeat-esquemas/HALLAZGOS.md` §3, `docs/boe/BOE-A-2023-24840.pdf` arts. 8 y 9.

## ADDED Requirements

### Requirement: Cálculo de la huella según la especificación oficial

El SIF DEBE calcular la huella de cada registro de alta concatenando **ocho campos, en este orden
exacto**, con el formato `nombre=valor` separado por `&`:

```
IDEmisorFactura · NumSerieFactura · FechaExpedicionFactura · TipoFactura
· CuotaTotal · ImporteTotal · Huella (la del REGISTRO ANTERIOR) · FechaHoraHusoGenRegistro
```

La cadena resultante DEBE codificarse en **UTF-8**, aplicarse **SHA-256**, y expresarse en
**hexadecimal, mayúsculas, 64 caracteres**.

Reglas de valor:
- Se eliminan los espacios al inicio y al final de cada valor.
- Si un campo no aparece o aparece sin valor, se escribe el nombre y el `=` **sin nada detrás**.
- Los valores usados DEBEN ser **exactamente** los que se escriben en el XML (ver requisito de
  serialización única).

#### Scenario: Vector oficial 1 — primer registro de alta

- **WHEN** se calcula la huella de un registro con `IDEmisorFactura=89890001K`,
  `NumSerieFactura=12345678/G33`, `FechaExpedicionFactura=01-01-2024`, `TipoFactura=F1`,
  `CuotaTotal=12.35`, `ImporteTotal=123.45`, sin huella anterior y
  `FechaHoraHusoGenRegistro=2024-01-01T19:20:30+01:00`
- **THEN** la cadena generada es
  `IDEmisorFactura=89890001K&NumSerieFactura=12345678/G33&FechaExpedicionFactura=01-01-2024&TipoFactura=F1&CuotaTotal=12.35&ImporteTotal=123.45&Huella=&FechaHoraHusoGenRegistro=2024-01-01T19:20:30+01:00`
- **AND** la huella es `3C464DAF61ACB827C65FDA19F352A4E3BDC2C640E9E9FC4CC058073F38F12F60`

#### Scenario: Vector oficial 2 — registro con anterior

- **WHEN** se calcula la huella con `NumSerieFactura=12345679/G34`,
  `FechaHoraHusoGenRegistro=2024-01-01T19:20:35+01:00` y huella anterior
  `3C464DAF61ACB827C65FDA19F352A4E3BDC2C640E9E9FC4CC058073F38F12F60`
- **THEN** la huella es `F7B94CFD8924EDFF273501B01EE5153E4CE8F259766F88CF6ACB8935802A2B97`

#### Scenario: Vector oficial 3 — anulación (motor preparado para fase 2)

- **WHEN** se calcula la huella de un registro de **anulación** con
  `IDEmisorFacturaAnulada=89890001K`, `NumSerieFacturaAnulada=12345679/G34`,
  `FechaExpedicionFacturaAnulada=01-01-2024`, huella anterior
  `F7B94CFD8924EDFF273501B01EE5153E4CE8F259766F88CF6ACB8935802A2B97` y
  `FechaHoraHusoGenRegistro=2024-01-01T19:20:40+01:00`
- **THEN** la huella es `177547C0D57AC74748561D054A9CEC14B4C4EA23D1BEFD6F2E69E3A388F90C68`

> Los tres vectores son de `HUELLA` apdo. 6, págs. 10–12, y fueron **verificados computacionalmente
> el 2026-09-01**: los tres reproducen la huella publicada. Son **tests de aceptación obligatorios**;
> el bloque de implementación de la huella no se da por terminado sin ellos en verde.

#### Scenario: Campo con espacios sobrantes

- **WHEN** `NumSerieFactura` llega como `" 12345678 / G33 "`
- **THEN** el valor usado en la cadena es `12345678 / G33`
- **AND** los espacios **interiores** se conservan; solo se recortan los de los extremos

---

### Requirement: Serialización única para huella y XML

El SIF DEBE derivar la cadena de la huella y el XML del registro **del mismo objeto de valor
canonizado**. NO DEBE existir ninguna ruta de código que formatee un importe o una fecha por separado
para la huella y para el XML.

Canonicalización obligatoria:
- Importes: **siempre 2 decimales**, punto decimal, sin separador de miles, signo `-` solo si el valor
  es negativo.
- `FechaExpedicionFactura`, `FechaOperacion`: **`DD-MM-YYYY`**.
- `FechaHoraHusoGenRegistro`: **ISO 8601 con desplazamiento explícito** (`+01:00` / `+02:00`), nunca
  `Z`, nunca sin huso.

#### Scenario: El importe se escribe igual en los dos sitios

- **WHEN** el importe total es `123.1`
- **THEN** tanto la cadena de la huella como el XML contienen `123.10`
- **AND** un test verifica que el valor extraído del XML generado coincide carácter a carácter con el
  valor usado en la cadena de la huella

#### Scenario: Prevención del error 2000

- **WHEN** se ejecuta la batería de tests de serialización
- **THEN** para cada registro generado se recalcula la huella **releyendo el XML producido**
- **AND** el resultado coincide con la huella almacenada
- **AND** cualquier discrepancia falla el test, porque es exactamente lo que la AEAT marcaría como
  código **2000**

---

### Requirement: Encadenamiento como choice de dos ramas

`Encadenamiento` es obligatorio (`1..1`) y DEBE expresarse como **una de dos ramas excluyentes**:

- Primer registro: `<Encadenamiento><PrimerRegistro>S</PrimerRegistro></Encadenamiento>`
- Resto: `<Encadenamiento><RegistroAnterior>` con `IDEmisorFactura`, `NumSerieFactura`,
  `FechaExpedicionFactura` y `Huella` `</RegistroAnterior></Encadenamiento>`

`PrimerRegistro` admite **únicamente el valor `S`**. **No existe `PrimerRegistro=N`**: la ausencia de
primer registro se expresa poniendo la otra rama, no un `N`.

#### Scenario: Segundo registro de la cadena

- **WHEN** se genera un registro y `cadena_estado.inicializada = 1`
- **THEN** el XML lleva la rama `RegistroAnterior` con los cuatro campos del registro previo
- **AND** no aparece el elemento `PrimerRegistro` en ninguna forma

#### Scenario: Intento de emitir PrimerRegistro=N

- **WHEN** cualquier ruta de código intenta construir `<PrimerRegistro>N</PrimerRegistro>`
- **THEN** el generador lanza excepción antes de producir XML

---

### Requirement: Escritor único de cadena

La generación de un registro DEBE realizarse dentro de una transacción que comience adquiriendo el
cerrojo de la fila única de `cadena_estado` mediante `SELECT ... FOR UPDATE`.

Dentro del cerrojo y en este orden: leer el estado de cadena, calcular la huella, insertar el registro,
actualizar `cadena_estado`. El `COMMIT` libera el cerrojo.

Todas las tablas DEBEN usar el motor **InnoDB**.

#### Scenario: Dos facturas simultáneas

- **WHEN** dos peticiones HTTP generan registros al mismo tiempo
- **THEN** ambos registros obtienen `cadena_orden` consecutivos y sin huecos
- **AND** la `huella_anterior` del segundo es exactamente la `huella` del primero
- **AND** en ningún caso dos registros comparten la misma `huella_anterior`

#### Scenario: Fallo a mitad de la escritura

- **WHEN** la inserción del registro falla después de haber calculado la huella
- **THEN** la transacción hace `ROLLBACK`
- **AND** `cadena_estado` queda **exactamente** como estaba
- **AND** no se consume ningún valor de `cadena_orden`

---

### Requirement: Gestión del primer registro de la cadena

El SIF DEBE emitir `PrimerRegistro=S` **únicamente** cuando `cadena_estado.inicializada = 0`. Tras
insertar el primer registro, la bandera pasa a `1` y NO DEBE volver a `0` por ninguna vía automática.

La identidad de la cadena ante la AEAT es la terna (**NIF del obligado**, **`IdSistemaInformatico`**,
**`NumeroInstalacion`**). Los tres valores DEBEN ser inmutables una vez remitido el primer registro
real a producción.

#### Scenario: Primerísimo registro del sistema

- **WHEN** se genera el primer registro con la cadena sin inicializar
- **THEN** el XML lleva `<PrimerRegistro>S</PrimerRegistro>`
- **AND** la cadena de la huella lleva `Huella=` sin valor
- **AND** tras el commit, `cadena_estado.inicializada = 1`

#### Scenario: Intento de reinicializar la cadena

- **WHEN** un administrador intenta poner `inicializada = 0` desde la UI
- **THEN** el sistema exige doble confirmación y un motivo escrito
- **AND** registra la acción en `auditoria` con severidad máxima
- **AND** muestra una advertencia explícita de que reiniciar la cadena provocará el código 2007 si la
  AEAT ya tiene registros de esta terna

---

### Requirement: Tratamiento del error 2007

Al recibir el código **2007** ("No debe informarse como primer registro, existen facturas emitidas con
el obligado emisión y el sistema informático actual"), el SIF DEBE:

1. Dejar el registro en `ACEPTADO_CON_ERRORES` con `requiere_subsanacion = 1`.
2. **Pausar la cola completa** (`control_flujo.cola_pausada = 1`,
   `motivo_pausa = "ERROR_2007_CADENA"`).
3. Abrir una incidencia de severidad **máxima**.
4. **No ejecutar ninguna acción correctora automática.**

La recuperación DEBE ser manual y DEBE exigir una consulta previa a la AEAT.

#### Scenario: Se recibe el 2007

- **WHEN** la respuesta de la AEAT trae `CodigoErrorRegistro = 2007`
- **THEN** la cola queda pausada y ningún registro adicional se envía
- **AND** la UI muestra una incidencia que explica que el estado local de la cadena se ha perdido
- **AND** ofrece como única acción siguiente lanzar `ConsultaFactuSistemaFacturacion`

#### Scenario: No hay reintento automático

- **WHEN** un registro está en `ACEPTADO_CON_ERRORES` por el código 2007
- **THEN** el worker no genera ningún registro de subsanación por su cuenta
- **AND** la subsanación solo puede iniciarla un operador desde la interfaz

> Un alta de subsanación sobre una factura anulada **la reactiva** (`HALLAZGOS` §12.1, consecuencia
> "OK (5)"). Aunque en fase 1 no hay anulaciones, la prohibición de automatizar se implanta desde el
> principio para que la fase 2 no tenga que reescribir el motor de reintentos.

---

### Requirement: Inmutabilidad del registro

Las columnas de contenido de `registros_facturacion` DEBEN ser inmutables tras el `INSERT`, protegidas
por un trigger `BEFORE UPDATE` que aborte con `SIGNAL SQLSTATE '45000'` ante cualquier cambio.

`DELETE` sobre la tabla DEBE bloquearse con un trigger equivalente.

Solo son mutables: `estado`, `requiere_subsanacion`, `codigo_error_aeat`, `descripcion_error_aeat`,
`csv`, `subsanado_por_registro_id`, `intentos`, `lease_hasta`.

#### Scenario: Intento de modificar una huella

- **WHEN** se ejecuta `UPDATE registros_facturacion SET huella = '...' WHERE id = 1`
- **THEN** MariaDB aborta con el error `registros_facturacion: contenido inmutable`
- **AND** la fila queda intacta

#### Scenario: Actualización legítima de estado

- **WHEN** el worker actualiza `estado` de `ENVIANDO` a `CORRECTO` y rellena `csv`
- **THEN** la operación se completa sin error

#### Scenario: Intento de borrado

- **WHEN** se ejecuta `DELETE FROM registros_facturacion`
- **THEN** la operación se aborta y no se borra ninguna fila

---

### Requirement: La subsanación crea un registro nuevo, no muta el anterior

Una subsanación DEBE materializarse como un **registro de facturación nuevo**, con su propio
`cadena_orden`, su propia huella, su propio eslabón de cadena y su propia `RefExterna`, conservando el
**mismo identificador de factura** (`IDEmisorFactura` + `NumSerieFactura` + `FechaExpedicionFactura`).

El registro antiguo pasa a `SUBSANADO` y se conserva íntegro, con
`subsanado_por_registro_id` apuntando al nuevo.

#### Scenario: Alta de subsanación

- **WHEN** un operador lanza una subsanación sobre el registro 41
- **THEN** se crea el registro 87 con `tipo_operacion = ALTA_DE_SUBSANACION`, `Subsanacion = S` y
  `RechazoPrevio` ausente
- **AND** el registro 87 tiene el mismo `NumSerieFactura` y `FechaExpedicionFactura` que el 41
- **AND** el registro 87 encadena con el **último** registro de la cadena, no con el 41
- **AND** el registro 41 queda en `SUBSANADO` con `subsanado_por_registro_id = 87`
- **AND** el contenido del registro 41 no se modifica en ningún campo

#### Scenario: Alta por rechazo

- **WHEN** un operador lanza un alta por rechazo sobre un registro `RECHAZADO`
- **THEN** el nuevo registro lleva `Subsanacion = S` y `RechazoPrevio = X`
- **AND** cumple las reglas de coherencia VF-29 y VF-30

---

### Requirement: Verificación de integridad de la cadena

El SIF DEBE ofrecer una verificación de integridad que recorra los registros en orden de
`cadena_orden` y compruebe, para cada uno:

1. Que la huella almacenada **coincide con el recálculo** a partir de `cadena_string`.
2. Que `cadena_string` **coincide con el recálculo** a partir de los campos del registro.
3. Que `huella_anterior` coincide con la `huella` del registro de `cadena_orden` inmediatamente
   anterior.
4. Que no hay huecos ni duplicados en `cadena_orden`.
5. Que exactamente un registro tiene `primer_registro = 1`, y es el de menor `cadena_orden`.

El resultado DEBE persistirse en `verificaciones_integridad`.

#### Scenario: Cadena íntegra

- **WHEN** se ejecuta la verificación sobre una cadena correcta de N registros
- **THEN** el resultado es `OK` con el número de registros verificados
- **AND** queda registrada la ejecución con marca de tiempo

#### Scenario: Eslabón roto

- **WHEN** la `huella_anterior` de un registro no coincide con la huella del anterior
- **THEN** el resultado es `FALLO`, indicando el `cadena_orden` exacto de la rotura
- **AND** se abre una incidencia de severidad máxima
- **AND** la cola queda pausada

#### Scenario: Verificación programada

- **WHEN** se dispara el timer diario `verifactu-integridad`
- **THEN** la verificación se ejecuta sobre la cadena completa
- **AND** con el volumen previsto (~240 registros/año) la ejecución completa es viable sin muestreo

---

### Requirement: Sello de tiempo del registro

`FechaHoraHusoGenRegistro` DEBE generarse en el instante de creación del registro, con la zona
`Europe/Madrid` y desplazamiento explícito, y NO DEBE ser posterior a la hora del sistema de la AEAT.

#### Scenario: Reloj sincronizado

- **WHEN** se genera un registro el 1 de septiembre a las 10:15:30 hora local de verano
- **THEN** el valor es `2026-09-01T10:15:30+02:00`
- **AND** no se usa `Z` ni se omite el huso

#### Scenario: Reloj desincronizado

- **WHEN** la comprobación diaria de salud detecta que NTP no está sincronizado
- **THEN** se abre una incidencia
- **AND** el mensaje explica que un reloj adelantado provoca el código **2004** en todos los registros
  generados
