> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nubidoc.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Difactu: duplicación y perfil regional

> Contratos candidatos, compatibilidad y límites de aceptación de R4

## Estado de la entrega — 7 de septiembre de 2026

<Warning>
  Son dos entregas de backend pendientes de integración y aceptación extremo a
  extremo. Publicar estas ramas y su documentación no significa desplegarlas.
  El consumidor Difactu todavía no conecta la recuperación idempotente descrita
  aquí. No se ha aplicado la migración a la base compartida de desarrollo.
</Warning>

## Duplicación identificada por operación

`POST /api/invoice/duplicate/{id}` admite un objeto JSON con `operation_key`
opcional y `date` opcional. La clave admite de 16 a 128 caracteres ASCII:
letras, números, guion y guion bajo. Es sensible a mayúsculas y pertenece a la
cuenta autenticada. No es un identificador de factura ni un permiso de acceso.

```json theme={null}
{
  "operation_key": "duplicate_example_20260907_01",
  "date": "2026-09-07"
}
```

* La primera petición guarda reserva, factura borrador, líneas y vínculo al
  resultado en una transacción. Repetir la misma operación devuelve esa copia.
* Cambiar el origen o la fecha explícita con la misma clave devuelve HTTP 409
  `operation_key_conflict`, sin crear otra factura.
* Omitir la fecha es una identidad distinta de indicarla explícitamente; no se
  sustituye por la fecha de hoy en la identidad de operación.
* Una clave consumida no se libera al marcar su registro como eliminado. Si la
  copia deja de estar disponible, se responde HTTP 409
  `duplicate_operation_result_unavailable`, sin recrearla.
* Sin clave se conserva la duplicación anterior: dos peticiones pueden crear
  dos copias. El botón del backoffice no gana deduplicación automáticamente.

El JSON malformado, los cuerpos que no sean objeto y las fechas de calendario
inválidas se rechazan antes de copiar. Los parámetros de query/formulario no
suplantan los campos del JSON. El camino antiguo sin clave conserva sus alias
de fecha. Los fallos inesperados mantienen un HTTP 500 sanitizado: un fallo de
transporte no prueba que no hubiera commit.

### Consultar sin repetir la escritura

`GET /api/invoice/duplicateoperation?operation_key=...` consulta la vinculación
de la cuenta autenticada sin crear una factura. La operación OpenAPI es
`getInvoiceDuplicateOperation`; se incorpora al perfil MCP interno `difactu`,
no al perfil público `difactu-chatgpt`. El llamante debe conservar la misma clave
para recuperar una operación incierta; crear otra clave no es un reintento.

### Migración y comprobaciones

La migración `20260907_create_invoice_duplicate_operation.sql` crea el registro
transaccional y su unicidad por cuenta/clave. No debe eliminarse ese registro
como limpieza de reintentos: conserva la identidad de operaciones consumidas.
Para volver a un consumidor anterior, este puede seguir omitiendo la clave;
no hay que borrar las vinculaciones ya guardadas.

`tests/experimental/invoice-duplicate-model-acceptance.php --run-isolated --port=43307` es opt-in. Verifica el datadir antes de DDL, crea un esquema
sintético por ejecución y llama al modelo real desde procesos PHP separados.
Las tablas auxiliares omiten FKs antiguas únicamente en el fixture; el ledger
ejecuta la migración real con todas sus restricciones. Dos rondas completas
acreditan concurrencia, acuse descartado y proceso nuevo, consulta sin escritura,
rollback tras copiar líneas, aislamiento de cuentas y originales intactos.
La repetición reforzada tarda 12,139 s. No prueba HTTP autenticado/MCP ni cambio
de día; no sustituye las pruebas finales de la aplicación.

## Región fiscal de Ceuta y Melilla

La resolución conserva `ES-CE` y `ES-ML` guardados por el alta, en lugar de
degradarlos al perfil peninsular. El contrato candidato devuelve:

| Región            | Perfil                  | Etiqueta | Tipo por defecto | Configuración requerida |
| ----------------- | ----------------------- | -------- | ---------------- | ----------------------- |
| `ES`              | `es_iva`                | IVA      | Conservado       | No                      |
| `ES-CN`           | `es_canarias_igic`      | IGIC     | Conservado       | No                      |
| `ES-CE` / `ES-ML` | `es_ceuta_melilla_ipsi` | IPSI     | `null`           | Sí                      |

Es un contrato de software, no una decisión sobre el tipo aplicable a una
actividad. No inventa un porcentaje, no convierte pendiente en exento ni cambia
los impuestos de documentos existentes o la fiscalidad de la suscripción.

Con `catalog_mode=empty`, el alta inicial puede completarse sin sembrar tipos
IPSI ni conceptos. `reduced` y `advanced` devuelven HTTP 409
`tax_configuration_required`, con `can_continue_without_catalog: true`, antes
de iniciar la transacción. El rechazo conserva preferencias, catálogo y estado
de onboarding anteriores; no guarda parcialmente la región solicitada.

La selección y persistencia del impuesto por concepto siguen pendientes en el
plan R4. Las pruebas del controlador y las respuestas HTTP simuladas del
frontend no acreditan soporte fiscal completo desde la aplicación conectada.
