# Capacidad: Envío a la AEAT y ciclo de vida de errores

Cliente del servicio web VERI\*FACTU, control de flujo, tratamiento de las dos vías de error y máquina
de estados del registro. Decisiones de referencia: **D10**–**D16**, **D27**.

Fuente: `docs/aeat-esquemas/SistemaFacturacion.wsdl` y XSD, `docs/aeat-esquemas/HALLAZGOS.md`,
`docs/aeat-especificaciones/Veri-Factu_Descripcion_SWeb.pdf`,
`docs/aeat-especificaciones/Validaciones_Errores_VERIFACTU_1.2.2.pdf`.

## ADDED Requirements

### Requirement: Construcción manual del XML

El SIF DEBE construir el sobre SOAP con `XMLWriter`, sin la extensión `php-soap`, respetando:

1. **Namespaces sin `V1.0`**: `https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd`
   para la raíz `RegFactuSistemaFacturacion` y `…/SuministroInformacion.xsd` para los registros.
2. `elementFormDefault="qualified"`: todos los elementos hijos cualificados.
3. **Orden posicional obligatorio** de la `<sequence>`: los 31 elementos de `RegistroAlta` en el orden
   del esquema. El esquema es posicional, no un mapa de campos.
4. **`IDVersion` = `1.0` como primer elemento de cada `RegistroAlta`**, y **NO** en la `Cabecera`.
5. Importes y fechas como **string con patrón**, nunca como `decimal` ni `xs:date`.
6. Codificación **UTF-8**.

#### Scenario: Cabecera sin IDVersion

- **WHEN** se construye el envío
- **THEN** la `Cabecera` contiene solo `ObligadoEmision` (y opcionalmente `RemisionVoluntaria`)
- **AND** no contiene `IDVersion` en ninguna posición

#### Scenario: IDVersion dentro del registro

- **WHEN** se serializa un `RegistroAlta`
- **THEN** su primer elemento hijo es `<IDVersion>1.0</IDVersion>`

#### Scenario: El XML generado valida contra el XSD

- **WHEN** se genera el XML de cualquier registro
- **THEN** valida contra `SuministroLR.xsd` con los XSD locales y el catálogo que redirige
  `xmldsig-core-schema.xsd` a la copia local
- **AND** la validación no requiere acceso a la red

> `SuministroInformacion.xsd` importa el esquema XMLDSig **por URL remota** a w3.org. La validación
> local debe usar un catálogo XML que apunte a `docs/aeat-esquemas/xmldsig-core-schema.xsd`, o
> dependerá de la disponibilidad de w3.org.

---

### Requirement: Transporte HTTP con mTLS y SOAPAction vacía

El envío DEBE hacerse por cURL sobre HTTPS con certificado cliente, incluyendo **obligatoriamente** la
cabecera HTTP `SOAPAction: ""` — presente y vacía.

Opciones de cURL requeridas: `CURLOPT_SSLCERT`, `CURLOPT_SSLKEY`, `CURLOPT_SSLKEYPASSWD`,
`CURLOPT_SSLCERTTYPE = 'PEM'`, `CURLOPT_TIMEOUT` (60 s por defecto, configurable),
`CURLOPT_CONNECTTIMEOUT` (15 s), verificación de par y host **activada**.

#### Scenario: Cabecera SOAPAction presente y vacía

- **WHEN** se realiza cualquier llamada al servicio
- **THEN** la petición incluye literalmente `SOAPAction: ""`
- **AND** no se omite ni se sustituye por el nombre de la operación

> El WSDL declara `soapAction=""` en **las tres** operaciones de los dos bindings. Algunas librerías
> omiten la cabecera o inventan un valor, y eso rompe la llamada.

#### Scenario: Verificación TLS activa

- **WHEN** se configura el cliente HTTP
- **THEN** `CURLOPT_SSL_VERIFYPEER` y `CURLOPT_SSL_VERIFYHOST` están activados
- **AND** no existe ninguna opción de configuración que permita desactivarlos

---

### Requirement: Selección de entorno por configuración

El cambio entre preproducción y producción DEBE ser **pura configuración**, sin despliegue.

| Entorno | Endpoint SOAP |
|---|---|
| Pruebas | `https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP` |
| Producción | `https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP` |

Con certificado de **sello** los hosts son `prewww10.aeat.es` y `www10.agenciatributaria.gob.es`.

El SIF DEBE usar exclusivamente el binding **`sfVerifactu`**, nunca `sfRequerimiento`.

#### Scenario: Arranque en pruebas

- **WHEN** el sistema se instala por primera vez
- **THEN** `configuracion.aeat.entorno` vale `PRUEBAS`
- **AND** todas las llamadas van a `prewww1.aeat.es`

#### Scenario: Paso a producción

- **WHEN** un administrador cambia `aeat.entorno` a `PRODUCCION`
- **THEN** el cambio exige doble confirmación y queda en `auditoria`
- **AND** la UI muestra de forma permanente y destacada en qué entorno está operando el sistema
- **AND** no se requiere ningún cambio de código

#### Scenario: El entorno del QR es independiente

- **WHEN** se cambia el entorno del servicio SOAP
- **THEN** el entorno de la URL base del QR se cambia **también**, como parámetro distinto
- **AND** la UI advierte si ambos no son coherentes, porque son hosts diferentes
  (`prewww2.aeat.es` / `www2.agenciatributaria.gob.es`)

---

### Requirement: Control de flujo obligatorio

El SIF DEBE leer `TiempoEsperaEnvio` de **cada** respuesta y esperar ese número de segundos desde el
envío anterior antes del siguiente. El valor inicial es **60**.

`TiempoEsperaEnvio` está tipado como **string** con patrón `\d{0,4}`. Si llega vacío o no numérico, el
SIF DEBE conservar el valor anterior y registrar un aviso.

#### Scenario: Espera entre envíos

- **WHEN** una respuesta trae `TiempoEsperaEnvio = 60` a las 10:00:00
- **THEN** `control_flujo.proximo_envio_permitido_at` queda en 10:01:00
- **AND** el worker no envía nada antes de esa marca, aunque se dispare cada minuto

#### Scenario: Valor anómalo

- **WHEN** la respuesta trae `TiempoEsperaEnvio` vacío
- **THEN** se conserva el valor vigente
- **AND** se registra un aviso en el canal `aeat`

#### Scenario: Arranque en frío

- **WHEN** el sistema no ha enviado nunca
- **THEN** `tiempo_espera_actual_s` vale 60 y el primer envío puede hacerse de inmediato

---

### Requirement: Los cuatro desenlaces de una llamada

El cliente DEBE distinguir cuatro desenlaces y asignar el estado correspondiente:

| Desenlace | Detección | Estado del registro |
|---|---|---|
| Respuesta normal | HTTP 200 + `RespuestaRegFactuSistemaFacturacion` | Según `EstadoRegistro` |
| `SoapFault` de `Client` | `Fault` con `faultcode` `soapenv:Client` | `RECHAZADO_ESTRUCTURA` — **no reintentar** |
| `SoapFault` de `Server` | `Fault` con `faultcode` `soapenv:Server` | `ESTADO_INDETERMINADO` |
| Sin respuesta útil | Timeout, error TLS, HTTP ≠ 200, cuerpo no parseable | `ESTADO_INDETERMINADO` |

El envío completo DEBE registrarse en `envios` con el XML enviado y la respuesta cruda, sea cual sea
el desenlace.

#### Scenario: Fault de cliente

- **WHEN** la AEAT responde con `faultcode = soapenv:Client`
- **THEN** el registro pasa a `RECHAZADO_ESTRUCTURA`
- **AND** el `faultstring` se guarda y se muestra en la UI
- **AND** el sistema **no** ofrece reintentar: ofrece corregir

#### Scenario: Fault de servidor

- **WHEN** la AEAT responde con `faultcode = soapenv:Server`
- **THEN** el registro pasa a `ESTADO_INDETERMINADO`
- **AND** la cola queda bloqueada por cabecera hasta que se resuelva

#### Scenario: Timeout

- **WHEN** la llamada supera `CURLOPT_TIMEOUT` sin respuesta
- **THEN** el registro pasa a `ESTADO_INDETERMINADO`
- **AND** **no** se reintenta automáticamente

> El rechazo completo por estructura o por error sintáctico en la cabecera llega como `SoapFault`, no
> como respuesta con `EstadoEnvio=Incorrecto` (`VAL` apdo. 4.1). Y el WSDL **no declara ningún
> `wsdl:fault`**, así que el fault no viene tipado y hay que parsearlo a mano.

---

### Requirement: Interpretación de la respuesta normal

De la respuesta el SIF DEBE extraer y persistir: `CSV` (nivel de envío, solo si el envío no se
rechaza en bloque), `DatosPresentacion` (`NIFPresentador`, `TimestampPresentacion`),
`TiempoEsperaEnvio`, `EstadoEnvio` y, por cada `RespuestaLinea`, la `RefExterna`, el
`EstadoRegistro`, el `CodigoErrorRegistro` y la `DescripcionErrorRegistro`.

La correlación respuesta ↔ registro DEBE hacerse por **`RefExterna`**, que la AEAT devuelve tal cual.

`EstadoRegistro` de suministro admite `Correcto`, `AceptadoConErrores` e `Incorrecto`.

#### Scenario: Registro aceptado

- **WHEN** `EstadoEnvio = Correcto` y la línea trae `EstadoRegistro = Correcto`
- **THEN** el registro pasa a `CORRECTO`
- **AND** se almacena el CSV del envío
- **AND** se actualiza `control_flujo.ultimo_contacto_aeat_at`

#### Scenario: Registro aceptado con errores

- **WHEN** la línea trae `EstadoRegistro = AceptadoConErrores` con `CodigoErrorRegistro = 2001`
- **THEN** el registro pasa a `ACEPTADO_CON_ERRORES` con `requiere_subsanacion = 1`
- **AND** la UI muestra la descripción traducida del catálogo
- **AND** ofrece la acción manual de subsanar

#### Scenario: Registro aceptado con error exceptuado

- **WHEN** el código es **2004** o **2009**
- **THEN** el registro pasa a `ACEPTADO_CON_ERRORES` con `requiere_subsanacion = 0`
- **AND** la UI **no** ofrece la acción de subsanar

#### Scenario: Registro rechazado

- **WHEN** la línea trae `EstadoRegistro = Incorrecto`
- **THEN** el registro pasa a `RECHAZADO` con su código y descripción
- **AND** la cola **no** se bloquea: el rechazo es un desenlace conocido

#### Scenario: Registro duplicado

- **WHEN** la respuesta trae el bloque `RegistroDuplicado` con código **3000**
- **THEN** se almacenan `IdPeticionRegistroDuplicado` y `EstadoRegistroDuplicado`
- **AND** se distingue el `EstadoRegistroDuplicado` (valores en **femenino**: `Correcta`,
  `AceptadaConErrores`, `Anulada`) del `EstadoRegistro` del nivel superior
- **AND** si el duplicado consta como `Anulada`, se abre incidencia de severidad máxima

> Hay **tres enumeraciones de estado casi homónimas** y distintas: la de suministro, la de duplicado
> (en femenino) y la de consulta (con `Anulado`). Reutilizar un solo enum rompe el parseo.

---

### Requirement: FIFO estricto con bloqueo de cabecera

Los registros DEBEN enviarse en orden estricto de `cadena_orden`.

Un registro en `ESTADO_INDETERMINADO` en cabeza de cola DEBE **bloquear** el avance. Los estados
terminales `CORRECTO`, `ACEPTADO_CON_ERRORES`, `RECHAZADO` y `RECHAZADO_ESTRUCTURA` **no** bloquean.

#### Scenario: Un rechazo no atasca la cola

- **WHEN** el registro 10 queda `RECHAZADO` y el 11 está pendiente
- **THEN** el worker envía el 11 en el siguiente ciclo permitido por el control de flujo
- **AND** el 10 permanece a la espera de una acción manual

#### Scenario: Un resultado desconocido sí atasca la cola

- **WHEN** el registro 10 queda en `ESTADO_INDETERMINADO`
- **THEN** el worker **no** envía el 11 ni ningún posterior
- **AND** la UI muestra el motivo del bloqueo y el registro que lo causa

> Un rechazo no rompe la cadena: el eslabón es interno al SIF y la AEAT solo valida el **formato** de
> la huella del registro anterior, no su existencia. En cambio, seguir enviando cuando no se sabe si
> el registro anterior quedó o no registrado construye estado sobre una incógnita.

---

### Requirement: Consulta obligatoria antes de cualquier reenvío

Ante un registro en `ESTADO_INDETERMINADO`, el SIF NO DEBE reenviar. DEBE exigir primero una
`ConsultaFactuSistemaFacturacion` y decidir según el resultado:

| Resultado | Acción permitida |
|---|---|
| `SinDatos` | Reenviar como **ALTA** ordinaria |
| Existe, `Correcto` | Marcar `CORRECTO`. No reenviar |
| Existe, `AceptadoConErrores` | Marcar como tal. La subsanación es decisión manual aparte |
| Existe, `Anulado` | **Parar y alertar.** No tocar. Fuera de alcance en fase 1 |

La consulta exige `PeriodoImputacion` (`Ejercicio` + `Periodo`), que es el único filtro obligatorio.
Toda consulta DEBE registrarse en `consultas_aeat`.

#### Scenario: La AEAT no tiene el registro

- **WHEN** la consulta devuelve `ResultadoConsulta = SinDatos`
- **THEN** el sistema habilita el reenvío como ALTA ordinaria
- **AND** el registro reenviado conserva su huella y su eslabón originales, porque el registro no ha
  cambiado

#### Scenario: La AEAT sí lo tiene

- **WHEN** la consulta lo encuentra con `EstadoRegistro = Correcto`
- **THEN** el registro pasa a `CORRECTO` sin reenviar nada
- **AND** se desbloquea la cola

#### Scenario: Aparece como anulado

- **WHEN** la consulta lo encuentra con `EstadoRegistro = Anulado`
- **THEN** el sistema no ofrece ninguna acción automática
- **AND** abre incidencia de severidad máxima advirtiendo de que un alta de subsanación **reactivaría**
  la factura anulada

#### Scenario: No hay reenvío sin consulta

- **WHEN** un operador intenta reenviar un registro en `ESTADO_INDETERMINADO` sin consulta previa
- **THEN** la interfaz rechaza la acción
- **AND** indica que debe ejecutarse primero la consulta

---

### Requirement: Las tres operativas de alta

El SIF DEBE implementar exactamente tres operativas, con estas combinaciones de campos:

| Operativa | `Subsanacion` | `RechazoPrevio` | Disparo |
|---|---|---|---|
| **ALTA** | ausente o `N` | ausente o `N` | Automático |
| **ALTA POR RECHAZO** | `S` | `X` | **Manual** |
| **ALTA DE SUBSANACIÓN** | `S` | ausente o `N` | **Manual** |

Las operativas "sin registro previo" quedan **fuera de alcance**: solo tienen sentido migrando desde
un SIF NO VERI\*FACTU.

#### Scenario: Alta por rechazo tras un rechazo

- **WHEN** un operador lanza un alta por rechazo sobre un registro `RECHAZADO`
- **THEN** el nuevo registro lleva `Subsanacion = S` y `RechazoPrevio = X`
- **AND** el sistema verifica antes que el registro original está efectivamente en `RECHAZADO`

#### Scenario: Ninguna operativa manual se dispara sola

- **WHEN** un registro queda en `RECHAZADO` o en `ACEPTADO_CON_ERRORES`
- **THEN** el worker **no** genera ningún alta por rechazo ni de subsanación
- **AND** ambas requieren acción explícita de un operador, con motivo escrito y registro en auditoría

---

### Requirement: Indicador de incidencia técnica

Cuando la remisión se haya visto interrumpida por una incidencia técnica registrada (caída de red,
corte eléctrico, parada del SIF), el primer envío posterior DEBE informar
`Cabecera/RemisionVoluntaria/Incidencia = S`.

En operación normal el campo NO se informa (equivale a `N`).

#### Scenario: Primer envío tras una interrupción

- **WHEN** el sistema detecta que hubo una parada registrada desde el último envío correcto
- **THEN** el siguiente envío incluye `<Incidencia>S</Incidencia>`
- **AND** los envíos posteriores vuelven a omitir el campo

---

### Requirement: Lote de un registro, con soporte hasta 1000

`envio.max_registros` DEBE valer **1** por defecto, y el generador DEBE soportar hasta **1000**
`RegistroFactura` por envío.

#### Scenario: Envío ordinario

- **WHEN** hay tres registros pendientes y `max_registros = 1`
- **THEN** se hacen tres envíos separados, respetando el control de flujo entre ellos

#### Scenario: Límite superior

- **WHEN** `max_registros` se configura por encima de 1000
- **THEN** el sistema rechaza el valor
- **AND** indica que el máximo del diseño de registro es 1000 (código de error 4114)

---

### Requirement: Persistencia probatoria de cada envío

De cada envío DEBEN persistirse en la base de datos, de forma íntegra: el **sobre SOAP enviado**, la
**respuesta cruda** recibida, el desenlace, el CSV y los datos de presentación.

Estos datos NO DEBEN guardarse únicamente en ficheros de log rotables.

#### Scenario: Reconstrucción posterior

- **WHEN** se consulta un registro remitido hace un año
- **THEN** puede recuperarse el XML exacto que se envió y la respuesta exacta que se recibió
- **AND** ambos siguen disponibles aunque los logs de esa fecha ya hayan rotado
