# Capacidad: Endpoint de entrada (XTRF → SIF)

Contrato HTTP por el que XTRF entrega una factura al SIF. Decisiones de referencia: **D1** (síncrono
para validar y registrar, asíncrono para el resto), **D17** (idempotencia), **D31** (token
previsto-pero-desactivado).

## ADDED Requirements

### Requirement: Contrato HTTP del endpoint de alta

El SIF DEBE exponer `POST /api/v1/facturas` que acepta `Content-Type: application/json` codificado en
UTF-8, y DEBE responder `202 Accepted` cuando la factura ha quedado registrada y encolada.

El endpoint NO DEBE realizar ninguna llamada a la AEAT, ni generar el PDF, ni contactar con XTRF
dentro del ciclo de la petición HTTP.

El cuerpo de la respuesta `202` DEBE incluir: `id_registro`, `ref_externa`, `num_serie_factura`,
`huella` y `estado` (siempre `PENDIENTE_ENVIO`).

#### Scenario: Factura válida recibida por primera vez

- **WHEN** XTRF envía un JSON válido de una factura que el SIF no conoce
- **THEN** el SIF valida el payload y las reglas fiscales, genera el registro de facturación con su
  huella encadenada, lo persiste con estado `PENDIENTE_ENVIO`
- **AND** responde `202 Accepted` con el `id_registro`, la `ref_externa` y la `huella`
- **AND** el tiempo total de la petición no depende de la disponibilidad de la AEAT ni del share CIFS

#### Scenario: Método o ruta incorrectos

- **WHEN** se recibe `GET`, `PUT` o `DELETE` sobre `/api/v1/facturas`
- **THEN** el SIF responde `405 Method Not Allowed` con cabecera `Allow: POST`

#### Scenario: Cuerpo no es JSON válido

- **WHEN** el cuerpo no se puede decodificar como JSON
- **THEN** el SIF responde `400 Bad Request` con `codigo = "JSON_INVALIDO"` y el mensaje del error de
  parseo
- **AND** no se crea ninguna factura ni ningún registro

---

### Requirement: Idempotencia frente a triggers repetidos

XTRF PUEDE repetir el trigger para la misma factura. El SIF DEBE tratar la repetición como idempotente
y NO DEBE generar un segundo registro de facturación para la misma factura.

La clave de idempotencia DEBE ser el identificador de la factura en XTRF, almacenado en
`facturas.clave_idempotencia` con restricción `UNIQUE`.

#### Scenario: Trigger repetido con payload idéntico

- **WHEN** llega una factura cuya `clave_idempotencia` ya existe y cuyo `json_hash` coincide con el
  almacenado
- **THEN** el SIF responde `200 OK` (no `202`) con los datos del registro **ya existente**
- **AND** no crea ningún registro nuevo
- **AND** no altera el estado del registro existente

#### Scenario: Trigger repetido con payload DISTINTO

- **WHEN** llega una factura cuya `clave_idempotencia` ya existe pero cuyo `json_hash` **difiere** del
  almacenado
- **THEN** el SIF responde `409 Conflict` con `codigo = "PAYLOAD_DIVERGENTE"`
- **AND** abre una incidencia de severidad alta que incluye ambos hashes
- **AND** no crea ningún registro nuevo

> Justificación: un mismo id de factura con contenido distinto significa que la factura se modificó
> después de haberse registrado. Eso NO se resuelve creando otro alta; se resuelve con una
> subsanación manual (D16), que es una decisión humana.

#### Scenario: Dos peticiones simultáneas de la misma factura

- **WHEN** dos peticiones con la misma `clave_idempotencia` llegan en paralelo
- **THEN** exactamente una crea el registro y responde `202`
- **AND** la otra recibe `200 OK` con el registro creado por la primera, resuelto por la violación de
  la restricción `UNIQUE`, nunca por una comprobación previa de tipo *check-then-act*

---

### Requirement: Validación del payload

El SIF DEBE validar la estructura del payload antes de aplicar cualquier regla fiscal, y DEBE
rechazar con `400` cualquier payload al que le falte un campo obligatorio o cuyo tipo no corresponda.

La respuesta de error DEBE enumerar **todos** los problemas detectados, no solo el primero.

#### Scenario: Falta un campo obligatorio

- **WHEN** el JSON no incluye el número de factura, la fecha de expedición, el importe total o las
  líneas de desglose
- **THEN** el SIF responde `400 Bad Request` con `codigo = "PAYLOAD_INCOMPLETO"` y la lista de campos
  ausentes
- **AND** no persiste nada salvo una entrada en `auditoria`

#### Scenario: El payload es estructuralmente válido pero fiscalmente inválido

- **WHEN** el payload tiene todos los campos pero incumple una regla fiscal (ver
  `../validacion-fiscal/spec.md`)
- **THEN** el SIF responde `422 Unprocessable Entity` con `codigo = "VALIDACION_FISCAL"` y la lista de
  reglas incumplidas, cada una con su identificador `VF-nn` y el código de error AEAT que evitaría
- **AND** no se genera registro de facturación ni se avanza la cadena

> La distinción 400/422 importa operativamente: `400` es un problema de integración (el trigger manda
> algo mal formado), `422` es un problema de datos de la factura (alguien tiene que corregir la
> factura en XTRF).

---

### Requirement: Tipos de factura admitidos en fase 1

El SIF DEBE aceptar únicamente `TipoFactura` **F1** y **F2**. Cualquier otro valor DEBE rechazarse con
un mensaje explícito de fuera de alcance, no con un error genérico de tipo.

#### Scenario: Llega una rectificativa

- **WHEN** el payload indica un tipo de factura R1, R2, R3, R4, R5 o F3
- **THEN** el SIF responde `422` con `codigo = "FUERA_DE_ALCANCE_FASE1"` y el mensaje
  "Las facturas rectificativas y sustitutivas no están implementadas en la fase 1"
- **AND** registra el intento en `auditoria` para dimensionar la fase 2

---

### Requirement: Autenticación por token, prevista y desactivada

El endpoint DEBE leer la cabecera `X-Verifactu-Token` y compararla con `configuracion.endpoint.token`
mediante `hash_equals()`.

Mientras `configuracion.auth.habilitada` sea `false`, el endpoint DEBE atender la petición aunque el
token falte o sea incorrecto, y DEBE dejar constancia en `auditoria` de que se atendió sin
autenticar.

#### Scenario: Flag desactivado, sin token

- **WHEN** `auth.habilitada = false` y la petición no lleva `X-Verifactu-Token`
- **THEN** la petición se procesa con normalidad
- **AND** se escribe en `auditoria` una entrada con `accion = "ENDPOINT_SIN_AUTENTICAR"` y la IP de
  origen

#### Scenario: Flag activado, token incorrecto

- **WHEN** `auth.habilitada = true` y el token falta o no coincide
- **THEN** el SIF responde `401 Unauthorized` sin cuerpo descriptivo
- **AND** no revela si el token existe, si es de longitud incorrecta ni ningún otro detalle

#### Scenario: Activación del flag sin cambios de código

- **WHEN** un administrador cambia `auth.habilitada` de `false` a `true` desde la UI
- **THEN** la protección pasa a aplicarse en la siguiente petición
- **AND** no se requiere despliegue, reinicio del pool FPM ni modificación de rutas

---

### Requirement: Límite de tamaño y cuota de protección

El endpoint DEBE rechazar cuerpos mayores de **2 MiB** con `413 Payload Too Large`.

El SIF DEBE mantener una cuota diaria configurable (`endpoint.max_facturas_dia`, por defecto **200**).
Al superarse, DEBE pausar la cola y abrir una incidencia en lugar de seguir aceptando y enviando.

#### Scenario: Se supera la cuota diaria

- **WHEN** el número de facturas aceptadas en el día natural supera `endpoint.max_facturas_dia`
- **THEN** el SIF responde `429 Too Many Requests` a las siguientes
- **AND** pone `control_flujo.cola_pausada = 1` con `motivo_pausa = "CUOTA_DIARIA_SUPERADA"`
- **AND** abre una incidencia de severidad alta

> Con un volumen real de ~20 facturas/mes, una cuota de 200/día es holgadísima para la operación
> normal y sin embargo corta en seco un abuso del endpoint público (RIESGO-3).

---

### Requirement: Mapeo JSON → RegistroAlta (PROVISIONAL hasta DEP-1)

El SIF DEBE implementar el mapeo del JSON de XTRF a los campos del `RegistroAlta` mediante una capa de
traducción aislada y sustituible (`MapeadorXtrf`), de forma que congelar el mapeo definitivo cuando
llegue el JSON real (**DEP-1**) NO requiera tocar el motor de registro, la huella ni el envío.

La tabla siguiente es **provisional y a confirmar contra el JSON real**. Se basa en los campos que un
gestor como XTRF exporta y en lo observado en las facturas reales del share.

| Campo del `RegistroAlta` | Origen previsto en el JSON de XTRF | Nota |
|---|---|---|
| `IDEmisorFactura` | **No viene del JSON** — sale de `configuracion.obligado.nif` | Mono-emisor (D del alcance). DEBE validarse que coincide con `Cabecera/ObligadoEmision/NIF` |
| `NumSerieFactura` | `invoice.number` | Formato observado `195/2026`. **Ojo:** el nombre de fichero usa `195_2026`. No son el mismo dato |
| `FechaExpedicionFactura` | `invoice.date` | Convertir a `DD-MM-YYYY` |
| `RefExterna` | Generado por el SIF (`AL-{id_registro}`) | **No** se toma del JSON (D17) |
| `NombreRazonEmisor` | `configuracion.obligado.razon_social` | |
| `TipoFactura` | Derivado: `F1` si hay destinatario identificado, `F2` si no | A confirmar |
| `DescripcionOperacion` | `invoice.description` o literal configurable | Obligatorio, máx. 500 |
| `Destinatarios/IDDestinatario/NombreRazon` | `invoice.customer.name` | Máx. 120 |
| `…/NIF` **o** `…/IDOtro` | `invoice.customer.taxId` + `invoice.customer.country` | Ver regla de decisión abajo |
| `Desglose/DetalleDesglose[]` | Agregación de `invoice.lines[]` | **Agregación fiscal, no líneas** |
| `CuotaTotal` | `invoice.taxTotal` | Contrastar con la suma del desglose |
| `ImporteTotal` | `invoice.grandTotal` | Contrastar con la suma del desglose |
| `FechaHoraHusoGenRegistro` | Generado por el SIF en el momento del registro | ISO 8601 con huso |
| Idioma de la plantilla PDF | `invoice.language` | Fallback `en`, luego `es` |
| Moneda | `invoice.currency` | Ver escenario de moneda abajo |

**Regla de decisión NIF vs IDOtro** (a confirmar, pero fiscalmente determinada):

- Cliente con país `ES` y NIF español → `<NIF>` (9 caracteres, sin prefijo `ES`).
- Cliente de otro Estado miembro con NIF-IVA → `<IDOtro>` con `IDType=02` y `CodigoPais` del país.
- Cliente de fuera de la UE → `<IDOtro>` con `IDType` = `04` (documento oficial de identificación) o
  `06` (otro documento probatorio), según el dato disponible.

#### Scenario: El número de factura del JSON no coincide con el del nombre de fichero

- **WHEN** el JSON indica `195/2026` y el fichero PDF de XTRF se llama `195_2026-Cliente.pdf`
- **THEN** el SIF usa `195/2026` como `NumSerieFactura` en el registro y en el QR
- **AND** usa el nombre de fichero para localizar el destino en el share (ver `../pdf-qr/spec.md`)
- **AND** no intenta derivar uno del otro sin la regla confirmada en **DEP-7**

#### Scenario: Cliente español identificado por NIF

- **WHEN** el JSON trae un cliente con país `ES` y `taxId = "ESB03975844"`
- **THEN** el SIF emite `<NIF>B03975844</NIF>`, eliminando el prefijo `ES`
- **AND** valida que la longitud resultante es exactamente 9 caracteres

#### Scenario: Cliente intracomunitario con NIF-IVA

- **WHEN** el JSON trae un cliente alemán con `taxId = "DE125657245"`
- **THEN** el SIF emite `<IDOtro><CodigoPais>DE</CodigoPais><IDType>02</IDType><ID>DE125657245</ID></IDOtro>`
- **AND** aplica las restricciones de `IDType=02` descritas en `../validacion-fiscal/spec.md`

#### Scenario: Moneda distinta de EUR

- **WHEN** el JSON indica una moneda distinta de `EUR`
- **THEN** el SIF rechaza la factura con `422` y `codigo = "MONEDA_NO_SOPORTADA"`
- **AND** el mensaje indica que el registro de facturación se expresa en euros y que la conversión
  debe hacerse en XTRF antes del trigger

> El `RegistroAlta` no tiene campo de moneda: todos los importes se entienden en euros. Aceptar otra
> moneda sin conversión produciría un registro fiscalmente falso. Es preferible rechazar de forma
> ruidosa. **[A VERIFICAR contra DEP-1: si XTRF factura en otras divisas, esto es una decisión de
> negocio que hay que elevar.]**

---

### Requirement: Respuesta de error uniforme

Todas las respuestas de error DEBEN tener la misma forma:

```json
{
  "error": true,
  "codigo": "VALIDACION_FISCAL",
  "mensaje": "texto legible",
  "detalles": [ { "campo": "...", "regla": "VF-12", "mensaje": "...", "codigo_aeat": 1245 } ],
  "id_peticion": "uuid-para-correlacionar-con-los-logs"
}
```

#### Scenario: Correlación de un error con el log

- **WHEN** XTRF recibe cualquier respuesta de error
- **THEN** el campo `id_peticion` aparece también en `http.log` y en `auditoria`
- **AND** permite reconstruir la petición completa sin volcar el payload en la respuesta
