openapi: 3.1.0
info:
  title: ZarelaFact API
  version: 0.2.0
  summary: API JSON-first para emision, certificacion y recepcion e-CF DGII.
  description: |
    Contrato inicial de la API PSFE. Las rutas privadas se sirven bajo un
    ambiente DGII: `/TesteCF`, `/CerteCF` o `/eCF`. Los endpoints publicos DGII
    se mantienen sin autenticacion interactiva porque DGII y otros emisores
    deben poder entregar XML firmado directamente.

    La Partner API (`/partner/v1`) solo existe cuando el despliegue define
    `PSFE_PARTNER_API_ENABLED=true`; en caso contrario responde `404`. Toda
    respuesta JSON incluye `correlationId` (también en la cabecera
    `x-correlation-id`). Los errores usan `ErrorResponse`; un `429` incluye
    `Retry-After` en segundos.
servers:
  - url: https://api.zarelafact.com
    description: API ZarelaFact (cuenta directa y Partner)
tags:
  - name: Operacion
  - name: Documentos
  - name: Secuencias
  - name: Anulaciones
  - name: Inbound DGII
  - name: Certificacion
  - name: Publico DGII
  - name: Partner
security:
  - ApiKeyAuth: []
paths:
  /healthz:
    get:
      tags: [Operacion]
      security: []
      summary: Healthcheck publico del servicio.
      responses:
        '200':
          description: Servicio vivo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
  /readyz:
    get:
      tags: [Operacion]
      security: []
      summary: Readiness operativo con storage, migraciones y rate limit.
      responses:
        '200':
          description: Servicio listo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReadinessResponse'
        '503':
          description: Servicio no listo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReadinessResponse'
  /metrics:
    get:
      tags: [Operacion]
      security: []
      summary: Metricas Prometheus.
      responses:
        '200':
          description: Metricas en formato Prometheus text exposition.
          content:
            text/plain:
              schema:
                type: string
  /ri/{token}:
    get:
      tags: [Documentos]
      security: []
      summary: Enlace temporal a la representación impresa (PDF) que recibe el comprador por correo.
      description: >-
        Es el enlace del correo «Factura por correo al comprador», que la cuenta activa por
        ambiente. No lleva API key: el token firmado es la credencial y abre solo el PDF de
        ese e-CF, durante 30 días desde el envío y mientras su representación impresa siga
        entregable (e-CF aceptado o aceptado condicional por la DGII). Un enlace vencido
        responde `410` y uno inválido, o de un e-CF que ya no se entrega, `404`, ambos en
        texto plano para la persona que lo abre.
      parameters:
        - name: token
          in: path
          required: true
          schema: { type: string, maxLength: 1300 }
      responses:
        '200':
          description: PDF de la representación impresa, para abrir en el navegador.
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '404':
          description: RI_LINK_INVALID o RI_NOT_DELIVERABLE (cabecera X-Error-Code).
          content:
            text/plain:
              schema: { type: string }
        '410':
          description: RI_LINK_EXPIRED; el enlace venció.
          content:
            text/plain:
              schema: { type: string }
  /{environment}/documentos-ecf:
    parameters:
      - $ref: '#/components/parameters/Environment'
    get:
      tags: [Documentos]
      summary: Lista los e-CF que emitiste.
      description: >-
        Del más reciente al más antiguo (por fecha de envío a ZarelaFact), por cursor:
        repite con `cursor` igual al `nextCursor` hasta que llegue `null`; los filtros se
        mantienen. Con `issueDateFrom` e `issueDateTo` recorres un período completo por
        `FechaEmision` y descargas cada XML firmado con
        `GET /{environment}/documentos-ecf/{documentReference}/xml`. No incluye las
        validaciones previas (`?validate=true`) ni los comprobantes reemplazados al
        reiniciar la certificación. Requiere `ecf:read`, que trae la clave estándar (en
        eCF, con producción activa). Un filtro, `limit` o `cursor` inválido responde
        `400 INVALID_ISSUED_DOCUMENTS_QUERY`.
      parameters:
        - $ref: '#/components/parameters/IssuedDocumentsIssueDateFrom'
        - $ref: '#/components/parameters/IssuedDocumentsIssueDateTo'
        - $ref: '#/components/parameters/IssuedDocumentsType'
        - $ref: '#/components/parameters/IssuedDocumentsStatus'
        - $ref: '#/components/parameters/ReceivedDocumentsLimit'
        - $ref: '#/components/parameters/ReceivedDocumentsCursor'
      responses:
        '200':
          description: Una página de e-CF emitidos.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IssuedDocumentsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '423':
          $ref: '#/components/responses/Locked'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [Documentos]
      summary: Emite o pre-valida un e-CF.
      description: |
        Con `?validate=true` valida el mismo JSON sin firmar ni registrar una emision,
        sin crear TrackID y sin enviar a DGII. Sin `validate=true`, ZarelaFact
        requiere el eNCF asignado por el POS, persiste el documento y encola
        firma/envio asincrono para DGII.

        La emision corre la misma validacion que `validate=true` (estructura,
        calculos, cuadre y XSD oficial del tipo) antes de registrar el documento:
        si falla responde `422` y el eNCF queda libre. Un descuadre de montos por
        encima de la tolerancia DGII (RD$1 por linea; en los totales, RD$1 por
        cada linea del detalle) no es un `422`: la DGII lo deja Aceptado
        Condicional, asi que se emite con un aviso en `warnings`
        (`details.code = SQUARING_ABOVE_DGII_TOLERANCE`, con `declared`,
        `expected` y `toleranceCents`). Lo mismo con los subdescuentos y
        subrecargos de cada item, `MontoPeriodo` y `ValorPagar`.

        JSON DGII: `MontoItem` = cantidad x precio - `DescuentoMonto` +
        `RecargoMonto`; con `IndicadorMontoGravado` 1 la base de cada tasa es la
        suma de sus items dividida entre (1 + tasa); un descuento global con
        `IndicadorNorma1007` 1 no se resta de `MontoGravadoI1` (va en
        `ValorPagar`), divide la base I1 entre (1 + 18 % + tasas 002 y 004 del
        item) y exige `IndicadorMontoGravado` 1 (`details.code =
        NORMA_1007_REQUIRES_ITBIS_INCLUDED`); los items con
        `IndicadorFacturacion` 0 suman en `MontoNoFacturable`; `ITBIS1`, `ITBIS2` e
        `ITBIS3` deben ser 18, 16 y 0 (`details.code = ITBIS_RATE_MISMATCH`); la
        seccion `OtraMoneda` exige `TipoCambio` > 0 (`EXCHANGE_RATE_INVALID`) y sus
        totales se avisan si no cuadran con los de pesos ÷ `TipoCambio`; una seccion
        `Paginacion` enviada se valida contra `TotalPaginas` y las lineas del
        detalle (`PAGINATION_INVALID`; el XML de ZarelaFact nunca la declara); los impuestos
        adicionales se recalculan con la formula de la DGII (aviso
        `SQUARING_ABOVE_DGII_TOLERANCE` si no cuadran) y los codigos ISC 006-039 sin
        `MontoImpuestoSelectivoConsumoEspecifico` (salvo a granel) responden `422`
        (`details.code = ISC_SPECIFIC_AMOUNT_REQUIRED`); la tasa trimestral del ISC
        especifico no se comprueba; `TotalITBIS` va
        siempre que exista un `TotalITBISn`; `TotalITBISRetenido` y
        `TotalISRRetencion` son la suma de las lineas; el ITBIS retenido de una
        linea no supera su ITBIS (`ITBIS_RETENTION_EXCEEDS_LINE_ITBIS`), el E41 solo
        retiene ISR en servicios (`ISR_RETENTION_REQUIRES_SERVICE`) y la percepcion
        se avisa (`PERCEPTION_REGIME_NOT_IN_FORCE`); una nota de credito con
        `IndicadorNotaCredito` 1 no devuelve el ITBIS: se cuadra sin
        `TotalITBIS`/`TotalITBISn` y con `MontoTotal` sin ITBIS, como la emite el
        contrato simplificado (la DGII no publico otra representacion), y un ITBIS
        declarado se avisa (`CREDIT_NOTE_ITBIS_NOT_RETURNED`); las notas 33/34 solo
        admiten `CodigoModificacion` 1-3; `RNCComprador` e `IdentificadorExtranjero`
        se excluyen y una E32 de RD$250,000 o mas (y las notas que la modifican)
        requiere uno de los dos (`details.code = BUYER_ID_REQUIRED`); un monto,
        cantidad, precio o tasa con coma o con texto (`1,500.50`, `abc`) responde
        `422` en su campo (`details.code = DECIMAL_FORMAT_INVALID`): no se limpia
        ni se toma como 0, y un numero JSON valido vale lo mismo que su texto; las fechas
        `DD-MM-AAAA` deben existir en el calendario (`31-02-2026` responde `422`
        con `details.code = DATE_INVALID`).

        Contrato simplificado: las propiedades desconocidas responden `422`
        (`details.code = UNKNOWN_FIELD`); impuestos adicionales, varias formas de
        pago, `InformacionesAdicionales` y `Paginacion` no existen en el contrato
        simplificado y responden `422` (`details.code =
        SIMPLIFIED_CONTRACT_UNSUPPORTED`): esos comprobantes van en JSON DGII; `paymentType` `card` es contado
        (`TipoPago` 1) con `FormaPago` 3; una venta a credito (`credit`, `TipoPago`
        2) exige `dueDate`/`FechaLimitePago` salvo en E47 (`details.code =
        PAYMENT_DUE_DATE_REQUIRED`); `IndicadorEnvioDiferido` solo sale con
        `deferredSend: true` o con la autorizacion de envio diferido registrada en
        la cuenta; con `currency` distinta de DOP los montos estan en esa moneda,
        `exchangeRate` es obligatorio y el e-CF lleva `OtraMoneda`;
        `pricesIncludeTax: true` declara precios con ITBIS incluido
        (`IndicadorMontoGravado` 1, base = suma de los items / (1 + tasa),
        redondeo medio-arriba); `globalSurchargeAmount`/`globalSurchargePercent`
        es el recargo global (`TipoAjuste` R); y los nodos opcionales del XSD
        (orden de compra, factura y pedido internos, vendedor, contacto y correo
        del comprador, entrega, transporte y `DescripcionItem`) se validan con el
        XSD del tipo: si el tipo no tiene el nodo responde `422` en el campo.
        `issuer.phone` (y `TablaTelefonoEmisor` en JSON DGII) se emite como
        `TablaTelefonoEmisor`: hasta 3 telefonos con el formato `809-555-1234`;
        otro formato responde `422` (`details.code = ISSUER_PHONE_INVALID`).

        El RNC emisor debe coincidir con el de la cuenta fiscal autenticada; si no
        coincide responde `422` con un error en `issuer.rnc` (contrato simplificado)
        o `ECF.Encabezado.Emisor.RNCEmisor` (JSON DGII).

        La fecha de vencimiento de la secuencia (`FechaVencimientoSecuencia`) es
        obligatoria para E31, E33, E41, E43, E44, E45, E46 y E47 (XSD DGII) y no
        existe en E32/E34. ZarelaFact nunca la completa por omision: si falta o no es
        una fecha real responde `422`, tanto en preflight como al emitir, con un
        error en `sequenceExpirationDate` (contrato simplificado) o
        `ECF.Encabezado.IdDoc.FechaVencimientoSecuencia` (JSON DGII). En un E32 o E34
        el JSON DGII la rechaza con `422` y `details.code =
        SEQUENCE_EXPIRATION_DATE_NOT_APPLICABLE`; el contrato simplificado la omite
        con un aviso con ese codigo. El XML y el PDF nunca la llevan en esos tipos.
        Tambien responde `422` si es anterior a la fecha de emision (el eNCF ya
        estaba vencido, `details.code = SEQUENCE_EXPIRED_AT_ISSUE_DATE`) o si la
        cuenta registro el rango que contiene el eNCF con otro vencimiento
        (`details.code = SEQUENCE_EXPIRATION_RANGE_MISMATCH`); sin rango registrado
        no se compara. Sin bloquear, avisa en `warnings` si vence despues del 31-12
        del ano siguiente a la fecha de emision (`SEQUENCE_EXPIRATION_BEYOND_LEGAL_MAX`)
        o si ya vencio el dia del envio (`SEQUENCE_EXPIRED_AT_SEND_DATE`).

        La huella de replay del eNCF se calcula sobre el
        JSON tal como lo envia el ERP (antes de completarlo con el perfil del
        tenant); las huellas historicas siguen aceptandose.

        Con el padron de contribuyentes de la DGII cargado, el preflight y la
        emision agregan un aviso en `warnings` (nunca bloquean) cuando el RNC o la
        cedula del comprador no aparece en el padron
        (`details.code = BUYER_RNC_NOT_IN_DGII_REGISTRY`) o no esta ACTIVO
        (`details.code = BUYER_RNC_NOT_ACTIVE_IN_DGII`, con `details.status`). El
        digito verificador no es motivo de rechazo.

        Los marcadores `{{RNC}}`, `{{RAZON_SOCIAL}}`, `{{NOMBRE_COMERCIAL}}`,
        `{{FECHA_EMISION}}` y `{{FECHA_LIMITE_PAGO}}` se reemplazan con los datos
        de la cuenta (fechas en hora de RD) antes de validar. Cualquier otro
        marcador `{{...}}` que quede, como `{{NCF_MODIFICADO}}`, responde `422`
        con `details.code = UNRESOLVED_PLACEHOLDER` en el campo afectado.

        Con `Prefer: wait=N` (RFC 7240) la emision espera hasta N segundos a que
        la representacion impresa se pueda entregar: cuando la DGII devuelve el
        TrackID del e-CF, cuando responde Aceptado o Aceptado Condicional al
        resumen RFCE de una factura de consumo menor a RD$250,000 (Informe
        Tecnico e-CF v1.0, secciones 8 y 9.1) o si el documento queda en
        contingencia. Entonces responde `201` con `Preference-Applied`,
        `result.securityCode`, `result.qrUrl` y `result.printing` para imprimir;
        si no, el `202` de siempre con `Retry-After`. Firmar no basta. Un replay
        del mismo eNCF y JSON que ya se puede imprimir responde `201` sin
        esperar. La aceptacion final de un e-CF con TrackID sigue siendo
        asincrona.
      parameters:
        - name: validate
          in: query
          required: false
          schema:
            type: boolean
          description: Preflight sin efectos fiscales cuando es `true`.
        - $ref: '#/components/parameters/PreferWait'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EcfSubmitRequest'
            examples:
              dgiiShaped:
                summary: JSON con raiz ECF compatible DGII/XSD
                value:
                  ECF:
                    Encabezado:
                      Version: '1.0'
                      IdDoc:
                        TipoeCF: '31'
                        eNCF: E310000000001
                        FechaVencimientoSecuencia: 31-12-2028
                      Emisor:
                        RNCEmisor: '132327179'
                        RazonSocialEmisor: Industrias Demo SRL
                      Comprador:
                        RNCComprador: '101010101'
                        RazonSocialComprador: Cliente Demo
                      Totales:
                        MontoTotal: 1180.00
                    DetallesItems:
                      Item:
                        - NumeroLinea: 1
                          NombreItem: Servicio de integracion
                          CantidadItem: 1
                          PrecioUnitarioItem: 1000.00
                          MontoItem: 1000.00
              simplified:
                summary: Contrato simplificado con los campos obligatorios
                value:
                  type: E31
                  encf: E310000000011
                  sequenceExpirationDate: 31-12-2028
                  issueDate: '2026-10-01T10:30:00-04:00'
                  issuer:
                    rnc: '132327179'
                    legalName: EMPRESA DEMO SRL
                    address: Av. Winston Churchill 1099, Santo Domingo
                  buyer:
                    rnc: '130701601'
                    name: CLIENTE SRL
                  paymentType: credit
                  dueDate: '2026-10-31T10:30:00-04:00'
                  incomeType: '01'
                  lines:
                    - name: Servicio de soporte
                      quantity: 2
                      unitPrice: 1000
                      itbisRate: 18
                      itemKind: service
      responses:
        '200':
          description: Preflight completado cuando `validate=true`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitResponse'
        '201':
          $ref: '#/components/responses/SubmitSigned'
        '202':
          description: >-
            Documento registrado y encolado para procesamiento asincrono. Con
            `Prefer: wait=N`, la DGII no respondio dentro del plazo (o rechazo el
            resumen RFCE): trae `Retry-After` y `printing` llega en la consulta.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: eNCF existente con otro contenido o perteneciente a un rango anulado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Validacion fiscal fallida, al prevalidar o al emitir (por ejemplo RNC emisor distinto del de la cuenta autenticada, en `issuer.rnc` o `ECF.Encabezado.Emisor.RNCEmisor`; fecha de vencimiento de la secuencia ausente o invalida (o enviada en E32/E34 en el JSON DGII), en `sequenceExpirationDate` o `ECF.Encabezado.IdDoc.FechaVencimientoSecuencia` con `details.code` `SEQUENCE_EXPIRATION_DATE_REQUIRED`/`SEQUENCE_EXPIRATION_DATE_INVALID`/`SEQUENCE_EXPIRATION_DATE_NOT_APPLICABLE`, anterior a la fecha de emision (`SEQUENCE_EXPIRED_AT_ISSUE_DATE`) o distinta del vencimiento del rango registrado en la cuenta que contiene el eNCF (`SEQUENCE_EXPIRATION_RANGE_MISMATCH`); descuento o recargo global en monto con `pricesIncludeTax`, con `details.code` `GLOBAL_AMOUNT_WITH_PRICES_INCLUDE_TAX`; propiedad desconocida del contrato simplificado con `details.code` `UNKNOWN_FIELD`; comprador sin identificar en una E32 de RD$250,000 o mas con `details.code` `BUYER_ID_REQUIRED`; nota de débito o crédito sobre un e-NCF de la cuenta que la DGII rechazó o que se anuló, con `details.code` `REFERENCED_ENCF_NOT_ACCEPTED`; `CodigoModificacion` (o `reference.modificationCode`) 4, el reemplazo de un comprobante de papel serie B emitido en contingencia, en cualquier tipo, con `details.code` `CONTINGENCY_PAPER_REPLACEMENT_UNSUPPORTED` (ZarelaFact no lo emite; si la DGII no responde, el e-CF se emite igual y queda en contingencia electrónica, y ZarelaFact lo envía y reintenta solo); fuera de las notas, `InformacionReferencia` con código 5 fuera del 31 o sin un eNCF E32; nota sobre un e-CF de la cuenta emitida a otro comprador (`REFERENCED_BUYER_MISMATCH`), con una `FechaNCFModificado` distinta de su fecha de emisión (`REFERENCED_DATE_MISMATCH`) o, en una nota de crédito, que con las notas de crédito anteriores supera su monto total (`CREDIT_NOTES_EXCEED_ORIGINAL`); `FechaNCFModificado`/`reference.date` posterior a la fecha de emisión (`REFERENCE_DATE_AFTER_ISSUE_DATE`); en el JSON DGII, `IndicadorNotaCredito` distinto del que dan los días calendario entre `FechaNCFModificado` y `FechaEmision` (`CREDIT_NOTE_INDICATOR_MISMATCH`); `FechaHoraFirma`/`signatureDate` enviada por el cliente posterior a la hora actual de RD (margen de 2 minutos), en `ECF.FechaHoraFirma` o `signatureDate` con `details.code` `SIGNATURE_DATE_IN_FUTURE` (o `SIGNATURE_DATE_INVALID`); o XML que no cumple el XSD oficial, en `ECF`). Al emitir, un `422` no registra el documento. `errors` lista los campos.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitResponse'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '423':
          $ref: '#/components/responses/Locked'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /{environment}/documentos-ecf/status/batch:
    parameters:
      - $ref: '#/components/parameters/Environment'
    post:
      tags: [Documentos]
      summary: Consulta estados por lote.
      description: >-
        Maximo 100 documentos por solicitud; recomendado 50. Cada resultado trae
        `lastError` cuando el documento no avanza: el ultimo fallo de envio a la
        DGII (por ejemplo `DGII_CERTIFICATE_NOT_DELEGATED` con su `hint`, o
        `DGII_RECEPTION_REJECTED` con `dgiiMessages`) o, en contingencia,
        `DGII_UNAVAILABLE`. El mismo error va en `errors`. Con estado final
        (`accepted`, `accepted_conditional`, `rejected`) o `cancelled`,
        `lastError` es null.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StatusBatchRequest'
      responses:
        '200':
          description: Estados encontrados y no encontrados.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusBatchResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /{environment}/documentos-ecf/{documentReference}/xml:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - $ref: '#/components/parameters/DocumentReference'
    get:
      tags: [Documentos]
      summary: Descarga el XML firmado de un documento.
      description: >-
        Busca por `documentId` o por eNCF. Requiere el permiso `ecf:read` (incluido
        en las API keys nuevas). Responde `409 DOCUMENT_NOT_SIGNED` mientras el
        documento no está firmado. En `transmission_failed` el XML firmado se
        descarga como evidencia (`404` si nunca llegó a firmarse).
      responses:
        '200':
          description: XML firmado del e-CF.
          content:
            application/xml:
              schema: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
  /{environment}/documentos-ecf/{documentReference}/pdf:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - $ref: '#/components/parameters/DocumentReference'
    get:
      tags: [Documentos]
      summary: Descarga la representación impresa (PDF tamaño carta).
      description: >-
        El mismo PDF del portal, con QR, código de seguridad y fecha de firma.
        Busca por `documentId` o por eNCF y requiere `ecf:read`. Responde
        `409 DOCUMENT_NOT_SIGNED` mientras el documento no está firmado y
        `409 RI_AWAITING_DGII_RESPONSE` mientras la DGII no responde (TrackID del
        e-CF o respuesta del resumen RFCE; en contingencia se entrega con su
        leyenda). Si la DGII rechazó el resumen RFCE de una factura de consumo
        menor a RD$250,000 responde `409 RI_NOT_DELIVERABLE_REJECTED`, y un
        documento en `transmission_failed` (no llegó a la DGII) responde
        `409 RI_NOT_DELIVERABLE_NOT_TRANSMITTED`.
      responses:
        '200':
          description: PDF de la representación impresa.
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /{environment}/documentos-ecf/{documentId}/entrega-receptor/reconciliar:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - name: documentId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags: [Documentos]
      summary: Reconcilia una entrega e-CF incierta al receptor.
      description: |
        La entrega al comprador queda `uncertain` solo cuando, tras agotar sus
        reintentos (5), el receptor nunca respondió al envío. Persiste evidencia
        WORM y fija una sola vez `sent` o `dispatch_failed`. Para `sent` exige
        el ARECF firmado del receptor en `evidence.arecfXml`. Nunca reenvía el
        e-CF automáticamente.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FiscalReconciliationRequest'
      responses:
        '200':
          description: Entrega reconciliada o replay idempotente.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReconciliationResponse'
        '409':
          description: Estado no incierto, resultado conflictivo o evidencia incompatible.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /{environment}/contingencia:
    parameters:
      - $ref: '#/components/parameters/Environment'
    get:
      tags: [Documentos]
      summary: Lista documentos diferidos por contingencia DGII.
      description: >-
        Documentos que esperan a que la DGII vuelva (sin respuesta, timeout, 5xx o
        autenticacion DGII caida). No hay ventana fija: se reintentan con espera
        creciente (5 min, el doble cada vez, hasta 30 min) y se envian cuando la DGII
        responde. A las 72 h sin llegar a la DGII ZarelaFact avisa a su operador y
        sigue reintentando. Cada documento trae `since`, `attempts`, `lastError`
        (mensaje) y `lastDeferredAt`.
      responses:
        '200':
          description: Documentos actualmente en contingencia.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContingencyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /{environment}/secuencias:
    parameters:
      - $ref: '#/components/parameters/Environment'
    get:
      tags: [Secuencias]
      summary: Lista rangos eNCF del ambiente.
      responses:
        '200':
          description: Rangos eNCF.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SequenceRangesResponse'
    post:
      tags: [Secuencias]
      summary: Crea un rango eNCF activo para un tipo e-CF.
      description: |
        Registro operativo opcional. No es requisito para emitir ni para anular:
        el POS/ERP asigna el eNCF y la emision no reserva ni mueve el cursor del
        rango.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSequenceRangeRequest'
      responses:
        '201':
          description: Rango creado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SequenceRangeResponse'
        '409':
          description: Rango solapado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /{environment}/anulaciones-ecf/prevalidar:
    parameters:
      - $ref: '#/components/parameters/Environment'
    post:
      tags: [Anulaciones]
      summary: Pre-valida una ANECF sin enviarla a DGII.
      description: >-
        Construye la ANECF, valida su XSD y revisa los comprobantes del rango. Un e-CF
        firmado que nunca salió hacia la DGII ni hacia el comprador se puede anular;
        uno que llegó o pudo llegar bloquea el rango con `ANECF_DOCUMENT_CONFLICT`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnnulmentRequest'
      responses:
        '200':
          description: ANECF construida y validada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericOkResponse'
        '400':
          description: >-
            ANECF inválida, pedida en CerteCF (`ANECF_NOT_AVAILABLE_IN_CERTECF`; las
            anulaciones son de eCF y se prueban en TesteCF) o `ANECF_DOCUMENT_CONFLICT`
            con `details.conflicts[]` (`encf`, `documentId`, `status`, `reason`).
            `reason` es `document_already_submitted_or_dgii_validated`,
            `document_in_contingency` (se enviará a la DGII cuando se recupere),
            `document_dgii_submission_uncertain`, `document_dgii_submission_in_progress`
            o `document_already_delivered_to_receiver`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /{environment}/anulaciones-ecf:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - $ref: '#/components/parameters/IdempotencyKey'
    post:
      tags: [Anulaciones]
      summary: Crea una solicitud ANECF.
      description: >-
        Valida el rango como `prevalidar`, firma y envía la ANECF. Cuando la DGII la
        acepta, los comprobantes locales del rango que nunca llegaron a la DGII
        (firmados sin enviar) quedan `cancelled` con sus jobs y su e-NCF ya no se
        envía; la respuesta los lista en `coveredDocuments.cancelled`
        (`documentId`, `encf`). Los que pudieron llegar a la DGII van en
        `coveredDocuments.skipped` con su `reason` para revisión.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnnulmentRequest'
      responses:
        '200':
          description: ANECF construida sin envio o replay idempotente.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericOkResponse'
        '202':
          description: ANECF enviada y aceptada por la DGII (`status` `accepted`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericOkResponse'
        '400':
          description: >-
            ANECF inválida, pedida en CerteCF (`ANECF_NOT_AVAILABLE_IN_CERTECF`; las
            anulaciones son de eCF y se prueban en TesteCF) o `ANECF_DOCUMENT_CONFLICT`
            con `details.conflicts[].reason` (ver `prevalidar`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: >-
            `ANECF_IDEMPOTENCY_CONFLICT`, `ANECF_RANGE_RESERVED` o
            `ANECF_DOCUMENT_CONFLICT` (un comprobante del rango llegó o pudo llegar a la
            DGII o al comprador mientras se registraba la anulación; `details.documentId`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: >-
            La DGII rechazó la ANECF (`ok: false`, `status: rejected`, mensajes en
            `dgii.mensajes`). Es un rechazo definitivo: los rangos quedan libres y se
            puede corregir y enviar otra vez con otra `Idempotency-Key`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericOkResponse'
        '502':
          description: >-
            Resultado incierto (`status: uncertain`): sin respuesta de la DGII, o una
            anulación parcial («anuladas correctamente. Con excepción…»). No la reenvíes;
            concíliala con `POST .../reconciliar`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericOkResponse'
        '428':
          description: Falta `Idempotency-Key` para enviar la ANECF.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /{environment}/anulaciones-ecf/{annulmentId}:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - name: annulmentId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Anulaciones]
      summary: Consulta una solicitud ANECF.
      responses:
        '200':
          description: Estado actual de la ANECF.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericOkResponse'
  /{environment}/anulaciones-ecf/{annulmentId}/reconciliar:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - name: annulmentId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags: [Anulaciones]
      summary: Reconcilia una ANECF incierta con evidencia durable.
      description: |
        Solo un resultado `uncertain` puede reconciliarse. La evidencia se
        persiste como artifact WORM y el resultado queda inmutable; no vuelve
        a enviar automáticamente a DGII.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FiscalReconciliationRequest'
      responses:
        '200':
          description: ANECF reconciliada o replay idempotente.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReconciliationResponse'
        '409':
          description: Estado no incierto, outcome conflictivo o evidencia incompatible.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /{environment}/documentos-recibidos:
    parameters:
      - $ref: '#/components/parameters/Environment'
    get:
      tags: [Inbound DGII]
      summary: Lista los e-CF que tus proveedores te enviaron.
      description: >-
        Del más reciente al más antiguo, por cursor: repite con `cursor` igual al
        `nextCursor` hasta que llegue `null`. Requiere `ecf:read`, que trae la clave
        estándar (en eCF, con producción activa); responder la aprobación pide `tenant:admin`. Solo lista e-CF
        recibidos en `/fe/recepcion/api/ecf`: las ACECF que tus clientes envían sobre tus
        e-CF están en `GET /{environment}/aprobaciones-comerciales-recibidas` (y llegan con el
        webhook `document.commercial_approval_received`). Cada elemento
        trae la vista de la pantalla Recibidos del portal (`view`) y, mientras falte tu
        respuesta, `commercialApprovalDeadline`. Un filtro, `limit` o `cursor` inválido
        responde `400 INVALID_RECEIVED_DOCUMENTS_QUERY`.
      parameters:
        - $ref: '#/components/parameters/ReceivedDocumentsGroup'
        - $ref: '#/components/parameters/ReceivedDocumentsType'
        - $ref: '#/components/parameters/ReceivedDocumentsEncf'
        - $ref: '#/components/parameters/ReceivedDocumentsIssuerRnc'
        - $ref: '#/components/parameters/ReceivedDocumentsFrom'
        - $ref: '#/components/parameters/ReceivedDocumentsTo'
        - $ref: '#/components/parameters/ReceivedDocumentsLimit'
        - $ref: '#/components/parameters/ReceivedDocumentsCursor'
      responses:
        '200':
          description: Una página de e-CF recibidos.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReceivedDocumentsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '423':
          $ref: '#/components/responses/Locked'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /{environment}/documentos-recibidos/{inboundDocumentId}:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - $ref: '#/components/parameters/InboundDocumentId'
    get:
      tags: [Inbound DGII]
      summary: Consulta un e-CF recibido.
      description: >-
        El mismo elemento de la lista. Requiere `ecf:read`. Un documento de otra
        cuenta, de otro ambiente o una ACECF responde `404 RECEIVED_DOCUMENT_NOT_FOUND` (las
        ACECF están en `/{environment}/aprobaciones-comerciales-recibidas/{inboundDocumentId}`).
      responses:
        '200':
          description: e-CF recibido.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReceivedDocumentResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '423':
          $ref: '#/components/responses/Locked'
  /{environment}/documentos-recibidos/{inboundDocumentId}/xml:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - $ref: '#/components/parameters/InboundDocumentId'
    get:
      tags: [Inbound DGII]
      summary: Descarga el XML de un e-CF recibido.
      description: >-
        El XML tal como lo envió el proveedor, como archivo `RNCEmisor` + e-NCF `.xml`.
        Sale de su copia inmutable, que se conserva 10 años, después de comprobar su
        SHA-256. Requiere `ecf:read`. Un envío rechazado sin firma verificada no guarda su XML:
        responde `404 RECEIVED_DOCUMENT_XML_NOT_AVAILABLE`.
      responses:
        '200':
          description: XML del e-CF recibido.
          content:
            application/xml:
              schema: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '423':
          $ref: '#/components/responses/Locked'
  /{environment}/aprobaciones-comerciales-recibidas:
    parameters:
      - $ref: '#/components/parameters/Environment'
    get:
      tags: [Inbound DGII]
      summary: Lista las aprobaciones comerciales (ACECF) que tus clientes enviaron sobre tus e-CF.
      description: >-
        Las ACECF recibidas en `/fe/aprobacioncomercial/api/ecf`: la aceptación o el rechazo
        comercial que tu cliente (el comprador) envió sobre un e-CF que emitiste. Del más
        reciente al más antiguo, por cursor: repite con `cursor` igual al `nextCursor` hasta
        que llegue `null`. Requiere `ecf:read`, como `GET /{environment}/documentos-recibidos`.
        `documentId` enlaza el e-CF emitido con ese e-NCF en el mismo ambiente (`null` si no
        está entre tus emitidos). Una ACECF que el receptor no recibió (`status` `rejected` o
        `duplicate`, con `notReceived`) no es una decisión de tu cliente: `decision` es
        `null`. Un filtro, `limit` o `cursor` inválido responde
        `400 INVALID_RECEIVED_DOCUMENTS_QUERY`.
      parameters:
        - $ref: '#/components/parameters/ReceivedCommercialApprovalsEncf'
        - $ref: '#/components/parameters/ReceivedCommercialApprovalsBuyerRnc'
        - $ref: '#/components/parameters/ReceivedDocumentsFrom'
        - $ref: '#/components/parameters/ReceivedDocumentsTo'
        - $ref: '#/components/parameters/ReceivedDocumentsLimit'
        - $ref: '#/components/parameters/ReceivedDocumentsCursor'
      responses:
        '200':
          description: Una página de ACECF recibidas.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReceivedCommercialApprovalsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '423':
          $ref: '#/components/responses/Locked'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /{environment}/aprobaciones-comerciales-recibidas/{inboundDocumentId}:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - $ref: '#/components/parameters/ReceivedCommercialApprovalId'
    get:
      tags: [Inbound DGII]
      summary: Consulta una aprobación comercial (ACECF) recibida.
      description: >-
        El mismo elemento de la lista. Requiere `ecf:read`. Una ACECF de otra cuenta, de otro
        ambiente o un e-CF recibido responde `404 RECEIVED_COMMERCIAL_APPROVAL_NOT_FOUND`.
      responses:
        '200':
          description: ACECF recibida.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReceivedCommercialApprovalResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '423':
          $ref: '#/components/responses/Locked'
  /{environment}/aprobaciones-comerciales-recibidas/{inboundDocumentId}/xml:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - $ref: '#/components/parameters/ReceivedCommercialApprovalId'
    get:
      tags: [Inbound DGII]
      summary: Descarga el XML original de una aprobación comercial (ACECF) recibida.
      description: >-
        El ACECF firmado tal como lo envió tu cliente, como archivo `RNCComprador` + e-NCF
        `.xml`. Sale de su copia inmutable, que se conserva 10 años, después de comprobar su
        SHA-256 (si no cuadra, `500 RECEIVED_DOCUMENT_XML_INTEGRITY_FAILED`). Requiere
        `ecf:read`. Una ACECF rechazada sin firma verificada no guarda su XML: responde
        `404 RECEIVED_COMMERCIAL_APPROVAL_XML_NOT_AVAILABLE`.
      responses:
        '200':
          description: XML del ACECF recibido.
          content:
            application/xml:
              schema: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '423':
          $ref: '#/components/responses/Locked'
  /{environment}/inbound-dgii/{inboundId}/aprobacion-comercial:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - name: inboundId
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [Inbound DGII]
      summary: Consulta aprobacion comercial de un e-CF recibido.
      responses:
        '200':
          description: Estado de aprobacion comercial.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommercialApprovalStatusResponse'
    post:
      tags: [Inbound DGII]
      summary: Genera y opcionalmente despacha ACECF.
      description: >-
        Firma el ACECF, lo envía a la DGII y después al emisor del e-CF. La URL del
        emisor sale del directorio DGII (TesteCF y eCF), consultado con el token DGII de
        la cuenta; `targetUrl` solo vale si el emisor no está en el directorio o en
        CerteCF (si está, otra URL responde `422 ACECF_TARGET_URL_NOT_ALLOWED`).
        `commercialApproval.dispatch` trae el status del emisor y hasta 4 KB de su
        cuerpo (`responseTruncated`, `responseBytes`), nunca sus cabeceras. Si el emisor declaró autenticación (`urlOpcional` en el
        directorio), el ACECF lleva su token Bearer obtenido con semilla +
        validacioncertificado; un fallo ahí deja la entrega en `dispatch_failed`, no
        incierta. En CerteCF no hay directorio: el ACECF va solo a la DGII, responde
        `200` con `commercialApproval.status` `signed` y `targetSource`
        `not_applicable`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommercialApprovalRequest'
      responses:
        '200':
          description: ACECF generado sin despacho al emisor (también en CerteCF, con `targetSource` `not_applicable`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommercialApprovalSubmitResponse'
        '202':
          description: ACECF firmado y despachado a la contraparte.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommercialApprovalSubmitResponse'
        '409':
          description: >-
            `COMMERCIAL_APPROVAL_DECISION_CONFLICT` (ya se envió otra decisión),
            `ACECF_DGII_SUBMISSION_UNCERTAIN` (un envío anterior a la DGII quedó incierto
            y debe conciliarse antes de otro intento) o `RECEIVED_ECF_NOT_VALID_IN_DGII`
            (aprobación `accept` de un e-CF que Consulta Estado reporta rechazado o no
            encontrado: la DGII solo recibe aprobaciones comerciales de e-CF que aceptó;
            `details.dgiiValidity`). No se bloquea en CerteCF, donde no hay Consulta
            Estado, ni mientras la validez no se ha consultado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: >-
            `ACECF_DGII_REJECTED`: la DGII rechazó la aprobación comercial (HTTP 4xx con
            mensajes, o «Aprobación comercial rechazada», código 2). Es definitivo y queda
            registrado con `details.dgiiSubmission` (`httpStatus`, `dgiiCodigo`,
            `dgiiEstado`, `dgiiMessages`, `rejectedAt`); se puede corregir y enviar otra vez.
            `ACECF_TARGET_URL_NOT_ALLOWED`: el emisor está en el directorio DGII y
            `targetUrl` no es su URL registrada (`details.directoryTargetUrl`).
            `COMMERCIAL_APPROVAL_DATE_INVALID`: `approvalDate` no es una fecha real, es
            anterior a la `FechaEmision` del e-CF recibido o posterior a la hora actual de RD
            más 5 minutos (`details.reason`: `invalid`, `before_issue_date` o `in_future`); no
            se firmó ni se envió nada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: >-
            El emisor no recibió el ACECF (`CommercialApprovalSubmitResponse`):
            `retry_scheduled` si la causa es pasajera (408, 429, 5xx, su autenticación o la
            red; se reintenta solo con el mismo ACECF hasta 5 intentos, ver
            `commercialApproval.dispatch.nextAttemptAt`), `dispatch_failed` si fue definitiva
            o se agotaron los intentos (repetir la misma decisión lo reenvía), o `uncertain`
            si no se confirmó (consulta el estado y concilia antes de actuar). También
            `ACECF_DGII_SUBMISSION_UNCERTAIN`: no se pudo confirmar el envío a la DGII
            (`ErrorResponse`).
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/CommercialApprovalSubmitResponse'
                  - $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: >-
            `ACECF_DGII_CERTIFICATE_UNAVAILABLE` (no hay certificado activo para enviar a
            la DGII; no salió nada), `DIRECTORY_DGII_AUTH_FAILED` (no se obtuvo el token
            DGII para consultar el directorio), `DIRECTORY_ACCESS_TOKEN_MISSING` (consulta
            al directorio sin token) o `ACECF_SIGNING_FAILED`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /{environment}/inbound-dgii/{inboundId}/aprobacion-comercial/reconciliar:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - name: inboundId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags: [Inbound DGII]
      summary: Reconcilia una entrega ACECF incierta con evidencia durable.
      description: |
        Solo un outbox ACECF en `uncertain` puede reconciliarse. La operación
        no reintenta la red y queda inmutable después de fijar el outcome.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FiscalReconciliationRequest'
      responses:
        '200':
          description: ACECF reconciliado o replay idempotente.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReconciliationResponse'
        '409':
          description: Estado no incierto, outcome conflictivo o evidencia incompatible.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /{environment}/certificacion/prevalidar:
    parameters:
      - $ref: '#/components/parameters/Environment'
    post:
      tags: [Certificacion]
      summary: Prevalida un set de certificacion sin crear un run.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CertificationSetRequest'
      responses:
        '200':
          description: Set valido.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericOkResponse'
        '409':
          description: >-
            CERTIFICATION_OWN_SET_REQUIRED; `dgiiModel` antes de que la DGII acepte las
            aprobaciones comerciales. Las pruebas de datos usan el Excel de la DGII del
            contribuyente.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Errores de importacion o prevalidacion.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /{environment}/certificacion:
    parameters:
      - $ref: '#/components/parameters/Environment'
    post:
      tags: [Certificacion]
      summary: Crea un run asincrono de certificacion.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CertificationSetRequest'
      responses:
        '202':
          description: Run creado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CertificationRunResponse'
        '400':
          description: INVALID_CERTIFICATION_REQUEST; `dgiiModel` no es booleano o viene junto a un set propio.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: >-
            CERTIFICATE_NOT_ACTIVE (la cuenta no tiene un certificado activo),
            CERTIFICATION_OWN_SET_REQUIRED (`dgiiModel` antes de que la DGII acepte las
            aprobaciones comerciales: las pruebas de datos usan el Excel de la DGII del
            contribuyente) o CERTIFICATION_SET_ALREADY_USED (los e-NCF del set ya se enviaron).
            No se crea ningún comprobante.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Errores de importacion o prevalidacion.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /{environment}/certificacion/{runId}:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - name: runId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Certificacion]
      summary: Consulta el estado de un run de certificacion.
      responses:
        '200':
          description: Estado del run.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CertificationStatusResponse'
  /{environment}/certificacion/{runId}/evidencia:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - name: runId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Certificacion]
      summary: Descarga el evidence pack codificado en base64.
      responses:
        '200':
          description: JSON con el ZIP en `content` base64 y su SHA-256.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CertificationEvidenceResponse'
  /{environment}/certificacion/reiniciar:
    parameters:
      - $ref: '#/components/parameters/Environment'
    post:
      tags: [Certificacion]
      summary: Reinicia las pruebas de datos o la simulación.
      description: >-
        Detiene el run en curso y deja los comprobantes de esa parte como reemplazados (no se
        borran). El siguiente `POST /{environment}/certificacion` vuelve a enviar las pruebas de
        datos con los e-NCF del Excel de la DGII, o la simulación (`dgiiModel`) con un bloque nuevo.
        La postulación y la declaración jurada que firmaste no cambian. Solo en CerteCF.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [part]
              properties:
                part:
                  type: string
                  enum: [data, simulation]
                  description: '`data`: pruebas de datos. `simulation`: simulación.'
      responses:
        '200':
          description: Parte reiniciada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CertificationRestartResponse'
        '400':
          description: CERTIFICATION_PART_INVALID o propiedades no permitidas.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /{environment}/certificacion/aprobaciones-comerciales:
    parameters:
      - $ref: '#/components/parameters/Environment'
    get:
      tags: [Certificacion]
      summary: Consulta el último envío de las aprobaciones comerciales de prueba.
      responses:
        '200':
          description: El último envío, o `commercialApprovals` null si todavía no hay ninguno.
          content:
            application/json:
              schema:
                type: object
                required: [ok, commercialApprovals]
                properties:
                  ok: { type: boolean }
                  environment: { type: string }
                  commercialApprovals:
                    oneOf:
                      - type: 'null'
                      - $ref: '#/components/schemas/CommercialApprovalTests'
    post:
      tags: [Certificacion]
      summary: Envía las aprobaciones comerciales de prueba (paso 3 de CerteCF).
      description: >-
        Envía a la DGII, de una en una, las aprobaciones comerciales (ACECF) del Excel que
        descargaste en CerteCF con «DESCARGAR APROBACIONES COMERCIALES» (hoja
        ACEECF_Generadas), fila por fila y tal cual, firmadas con tu certificado. La DGII compara
        cada campo con ese Excel, incluida la fecha y hora de la aprobación (la de la descarga):
        usa el último que descargaste. Se detiene en el primer rechazo. Solo en CerteCF, con las
        pruebas de datos de e-CF aceptadas.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [workbookBase64]
              properties:
                workbookBase64:
                  type: string
                  description: El Excel de aprobaciones comerciales de la DGII en base64 (máximo 700 KB).
      responses:
        '202':
          description: Envío en cola; consulta su avance con GET.
          content:
            application/json:
              schema:
                type: object
                required: [ok, commercialApprovals]
                properties:
                  ok: { type: boolean }
                  environment: { type: string }
                  correlationId: { type: [string, 'null'] }
                  commercialApprovals: { $ref: '#/components/schemas/CommercialApprovalTests' }
        '400':
          description: COMMERCIAL_APPROVAL_SET_REQUIRED (falta el Excel) o propiedades no permitidas.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: >-
            COMMERCIAL_APPROVAL_SET_INVALID (no es el Excel de la DGII o una fila no cumple su
            formato) o COMMERCIAL_APPROVAL_SET_OTHER_TAXPAYER (el comprador del Excel es otro RNC).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: >-
            CERTIFICATION_DATA_TESTS_PENDING, CERTIFICATE_NOT_ACTIVE,
            COMMERCIAL_APPROVALS_IN_PROGRESS, CERTIFICATION_ENVIRONMENT_REQUIRED o
            CERTIFICATION_MANAGED_BY_PARTNER (tu proveedor de software lleva tu certificación).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /{environment}/certificacion/documentos/{documentId}/reenviar:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - name: documentId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags: [Certificacion]
      summary: Reenvía a la DGII un comprobante de las pruebas con un problema de envío.
      description: >-
        Para un comprobante que ya se firmó y la DGII no ha resuelto: sin respuesta
        (contingencia), un resumen RFCE que la DGII da por duplicado, consultas agotadas o sin
        novedad. Lo envía otra vez de inmediato, o consulta su estado si ya tiene TrackID. Un
        resumen que la DGII da por duplicado se consulta con ConsultaRFCE y se reenvía
        idéntico si la DGII no lo tiene; el E32 nunca se vuelve a firmar. Solo en CerteCF.
      responses:
        '202':
          description: Reenvío en cola.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CertificationResendResponse'
        '404':
          description: DOCUMENT_NOT_FOUND.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: >-
            DOCUMENT_ALREADY_ACCEPTED, DOCUMENT_REJECTED (reinicia esa parte de las pruebas),
            DOCUMENT_NOT_RESENDABLE (todavía no sale de la firma), DOCUMENT_SEND_IN_PROGRESS o
            DOCUMENT_SUPERSEDED.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /{environment}/fe/recepcion/api/ecf:
    parameters:
      - $ref: '#/components/parameters/Environment'
    post:
      tags: [Publico DGII]
      security: []
      summary: Recepcion publica de e-CF entrante.
      description: >-
        La DGII y los demás emisores envían el e-CF como archivo en el campo `xml` de un
        `multipart/form-data`; también se acepta el XML directo en el cuerpo, de hasta 5 MiB
        (mayor: `400 BODY_TOO_LARGE`, sin ARECF). La ruta no
        distingue mayúsculas y lleva el ambiente registrado en la DGII antes de `/fe`
        (`/CerteCF/fe/recepcion/api/ecf`): sin él, el e-CF recibe el ARECF `Estado` 1 con
        motivo 4 (o `400` si no se puede armar), y `?environment=` o cabeceras no cuentan. La respuesta es el ARECF firmado con el certificado del
        receptor: `Estado` 0 si se recibió y 1 con `CodigoMotivoNoRecibido` si no (1 error de
        especificación, 2 error de firma, 3 envío duplicado, 4 RNC comprador no corresponde).
        La firma es válida si sigue el «Firmado de e-CF» de la DGII: una sola firma, hija de
        la raíz, con `Reference URI=""` (todo el documento), transform enveloped-signature,
        C14N inclusivo, RSA-SHA256 y digest SHA-256, y un certificado vigente con el RNC, la
        cédula o el pasaporte de su titular en el SN (`serialNumber`); si no, motivo 2.
        Un XML mal formado (etiquetas, comentarios o CDATA sin cerrar, más de una raíz, más
        de 64 niveles, de 512 nombres de elemento distintos o de 10 000 elementos dentro de
        uno) es un error de especificación;
        uno sin `Signature` (fuera de comentarios), un error de firma.
        Repetir el mismo XML devuelve el mismo ARECF (`x-zarela-idempotent-replay: true`).
        Solo se recibe la raíz `ECF`: un `RFCE` va únicamente a la DGII y responde `400`
        `INVALID_XML_ROOT`. Si el comprador no es un contribuyente de ZarelaFact en ese
        ambiente, responde el ARECF `Estado` 1 con motivo 4, firmado con el certificado
        del RNC en otro ambiente si existe (`200`); sin ningún certificado con el que
        firmar responde `400` con ese ARECF sin firmar (`x-zarela-arecf-signed: false`).
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [xml]
              properties:
                xml:
                  type: string
                  format: binary
          application/xml:
            schema:
              type: string
      responses:
        '200':
          description: ARECF firmado (recibido o no recibido, según `Estado`).
          content:
            application/xml:
              schema:
                type: string
        '400':
          description: >-
            El envío no se pudo leer (UNSUPPORTED_CONTENT_TYPE, MISSING_XML_FIELD, INVALID_XML,
            INVALID_XML_ROOT para una raíz `RFCE`…), un XML sin los datos para armar un ARECF
            (INBOUND_TENANT_NOT_RESOLVED, o MISSING_SIGNATURE si además no trae firma; la
            respuesta es la misma sea o no cliente el RNC, sin `details`), o el
            ARECF motivo 4 sin firmar cuando el comprador
            no tiene ningún certificado en ZarelaFact (`application/xml`,
            `x-zarela-arecf-signed: false`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
            application/xml:
              schema:
                type: string
  /{environment}/fe/aprobacioncomercial/api/ecf:
    parameters:
      - $ref: '#/components/parameters/Environment'
    post:
      tags: [Publico DGII]
      security: []
      summary: Recepcion publica de ACECF.
      description: >-
        Como en la recepción de e-CF, el ACECF llega en el campo `xml` de un
        `multipart/form-data` o como XML directo, en la ruta con el ambiente; sin él
        responde `400`. La DGII lee el resultado en el código
        HTTP: 200 si se recibió y 400 si no (firma o formato inválidos; la firma se valida
        como en la recepción de e-CF). Una segunda
        aprobación del mismo e-CF se confirma con 200. Si el emisor del e-CF aprobado no es
        un contribuyente de ZarelaFact en ese ambiente, responde 400 con el acuse
        `Estado` 1 y motivo 4 (RNC comprador no corresponde). La recepción no depende del
        certificado del contribuyente: sin uno vigente el ACECF se recibe y se guarda igual,
        el acuse va sin firmar (`x-zarela-arecf-signed: false`) y se avisa al operador.
        El ACECF se compara con el e-CF emitido con ese e-NCF (RNCEmisor, RNCComprador y
        MontoTotal): si no hay e-CF emitido o algo no coincide se recibe igual y queda
        marcado en el documento recibido y en su evento.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [xml]
              properties:
                xml:
                  type: string
                  format: binary
          application/xml:
            schema:
              type: string
      responses:
        '200':
          description: ACECF recibido; el cuerpo es el acuse firmado.
          content:
            application/xml:
              schema:
                type: string
        '400':
          description: >-
            ACECF no recibido: el cuerpo es el acuse con `Estado` 1 y su motivo. Si el envío no
            se pudo leer, un ErrorResponse JSON.
          content:
            application/xml:
              schema:
                type: string
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /partner/v1/webhook-endpoints/{endpointId}:
    parameters:
      - name: endpointId
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [Partner]
      security: [{ PartnerKeyAuth: [] }]
      summary: Edita configuración o estado de un endpoint propio.
      description: Cambiar URL exige endpoint previamente pausado, sin entregas running y una URL no registrada por el Partner. Las entregas pendientes se preparan con la URL vigente; los filtros solo afectan eventos futuros.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              additionalProperties: false
              properties:
                status: { type: string, enum: [active, paused, disabled] }
                url: { type: string, format: uri, maxLength: 2048 }
                name: { type: string, minLength: 1, maxLength: 200 }
                eventTypes: { type: array, minItems: 1, items: { type: string, minLength: 1 } }
      responses:
        '200':
          description: Endpoint actualizado sin secretos.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PartnerWebhookEndpointResponse' }
        '409':
          description: WEBHOOK_EDIT_REQUIRES_PAUSE, WEBHOOK_DELIVERIES_IN_PROGRESS, WEBHOOK_URL_CONFLICT o WEBHOOK_ENDPOINT_RETIRED (el endpoint fue retirado con DELETE).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      tags: [Partner]
      security: [{ PartnerKeyAuth: [] }]
      summary: Deshabilita un endpoint conservando su historial.
      responses:
        '200':
          description: Baja lógica; no elimina entregas ni eventos.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PartnerWebhookEndpointResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/webhook-deliveries:
    get:
      tags: [Partner]
      security: [{ PartnerKeyAuth: [] }]
      summary: Lista entregas propias con paginación por cursor.
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - name: cursor
          in: query
          schema: { type: string, maxLength: 256 }
        - name: status
          in: query
          schema: { type: string, enum: [queued, running, succeeded, failed] }
        - name: endpointId
          in: query
          schema: { type: string, format: uuid }
        - name: fiscalAccountId
          in: query
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Entregas ordenadas por creación descendente; cursor opaco o null al terminar.
          content:
            application/json:
              schema:
                type: object
                required: [deliveries, nextCursor]
                properties:
                  deliveries:
                    type: array
                    items: { $ref: '#/components/schemas/PartnerWebhookDeliveryResponse' }
                  nextCursor: { type: [string, 'null'] }
                  correlationId: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/webhook-deliveries/{deliveryId}:
    get:
      tags: [Partner]
      security: [{ PartnerKeyAuth: [] }]
      summary: Consulta metadatos de una entrega propia.
      parameters:
        - name: deliveryId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Estado operativo sin payload fiscal, firma ni respuesta del receptor.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PartnerWebhookDeliveryResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/webhook-deliveries/{deliveryId}/replay:
    post:
      tags: [Partner]
      security: [{ PartnerKeyAuth: [] }]
      summary: Reencola una entrega fallida de un endpoint activo.
      description: Conserva deliveryId, eventId y contador de intentos. Rechaza entregas queued/running/succeeded; el ERP debe deduplicar por eventId.
      parameters:
        - name: deliveryId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '202':
          description: Entrega reencolada.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PartnerWebhookDeliveryResponse' }
        '409':
          description: WEBHOOK_REPLAY_NOT_ALLOWED; la entrega no está `failed` o su endpoint no está activo.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/certification/official-sets:
    get:
      tags: [Partner]
      security: [{ PartnerKeyAuth: [] }]
      summary: Descubre los sets oficiales de certificación habilitados.
      description: >-
        Devuelve referencias y huellas públicas; nunca expone rutas internas ni permite cargar
        sets arbitrarios. No incluye el set modelo de ZarelaFact (`dgii-model`), que solo se usa
        en la simulación: las pruebas de datos usan el Excel de la DGII del contribuyente,
        subido con `POST .../certification-test-sets`. `defaultRef` es null si PSFE no fijó uno.
      responses:
        '200':
          description: Catálogo activo y referencia recomendada.
          content:
            application/json:
              schema:
                type: object
                required: [defaultRef, sets]
                additionalProperties: false
                properties:
                  defaultRef: { type: [string, 'null'] }
                  correlationId: { type: string }
                  sets:
                    type: array
                    items:
                      type: object
                      required: [ref, sha256, format]
                      additionalProperties: false
                      properties:
                        ref: { type: string }
                        sha256: { type: string, pattern: '^[0-9a-f]{64}$' }
                        format: { type: string }
        '503':
          description: Catálogo oficial no disponible o inconsistente.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/billing/usage:
    get:
      tags: [Partner]
      security: [{ PartnerKeyAuth: [] }]
      summary: Consulta el consumo aceptado por DGII de todas las Cuentas fiscales del Partner en el periodo de cobro o en un mes.
      description: >-
        Requiere production:read. Solo cuenta documentos eCF en estado accepted o
        accepted_conditional según acceptedAt; acceptedAt es la fechaRecepcion
        informada por DGII y, si DGII no la envía, la hora en que PSFE registró la
        aceptación. Sin `month` devuelve el periodo de cobro vigente del Partner:
        con un plan pagado en línea, el ciclo de su suscripción (se renueva el
        mismo día cada mes, `periodBasis: subscription`), que es lo que consume el
        cupo del plan; sin suscripción, el mes calendario de República Dominicana
        (`periodBasis: calendar_month`). Con `month` devuelve ese mes calendario de
        RD. Es una consulta operativa, no un ledger cerrado, cargo ni factura.
      parameters:
        - name: month
          in: query
          description: Mes AAAA-MM en hora de República Dominicana. Si se omite, usa el periodo de cobro vigente.
          schema: { type: string, pattern: '^20[0-9]{2}-(0[1-9]|1[0-2])$' }
      responses:
        '200':
          description: Volumen agregado del Partner para el periodo o el mes solicitado.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [month, periodStart, periodEnd, periodBasis, timeZone, volumeBasis, environment, acceptedDocuments, countedStatuses, invoice]
                properties:
                  month: { type: string, description: 'Mes pedido, o el mes de RD en que empieza el periodo de cobro.' }
                  periodStart: { type: string, format: date-time, description: 'Inicio del periodo en UTC (un mes de RD empieza a las 04:00Z; un ciclo, el día y la hora en que se pagó).' }
                  periodEnd: { type: string, format: date-time }
                  periodBasis: { type: string, enum: [subscription, calendar_month] }
                  timeZone: { type: string, enum: [America/Santo_Domingo] }
                  volumeBasis: { type: string, enum: [partner_aggregate] }
                  environment: { type: string, enum: [ecf] }
                  acceptedDocuments: { type: integer, minimum: 0 }
                  countedStatuses:
                    type: array
                    items: { type: string, enum: [accepted, accepted_conditional] }
                  invoice: { type: boolean, enum: [false] }
                  correlationId: { type: string }
        '400':
          description: Mes o parámetros inválidos.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Lista las Cuentas fiscales del Partner.
      description: Colección paginada y aislada por la identidad de la credencial Partner.
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, default: 0 }
        - name: status
          in: query
          schema: { type: string, enum: [active, suspended] }
      responses:
        '200':
          description: Cuentas fiscales visibles para el Partner autenticado.
          content:
            application/json:
              schema:
                type: object
                required: [items, pagination]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/PartnerAccountResponse'
                  pagination:
                    type: object
                    required: [limit, offset, hasMore, nextOffset]
                    properties:
                      limit: { type: integer }
                      offset: { type: integer }
                      hasMore: { type: boolean }
                      nextOffset: { type: [integer, 'null'] }
                  correlationId: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/by-external-id/{externalTenantId}:
    put:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Provisiona o recupera una cuenta fiscal del ERP.
      description: >-
        El externalTenantId es la identidad estable del tenant en el ERP. La operacion es
        idempotente por pareja Partner + externalTenantId. Con el padron de
        contribuyentes de la DGII cargado, un alta nueva (o un RNC distinto del ya
        vinculado) exige que el RNC exista y este ACTIVO: si no, responde `422`
        `RNC_NOT_IN_DGII_REGISTRY` o `RNC_NOT_ACTIVE_IN_DGII` (con `details.status`).
        Reenviar el RNC ya vinculado no se bloquea. El digito verificador no es motivo
        de rechazo.
      parameters:
        - name: externalTenantId
          in: path
          required: true
          schema:
            type: string
            maxLength: 200
          description: Para el Partner Zarela ERP debe ser el UUID estable del tenant; otros Partners pueden usar un identificador opaco estable.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerProvisionRequest'
      responses:
        '200':
          description: Cuenta ya existente y sincronizada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerAccountResponse'
        '201':
          description: Cuenta creada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerAccountResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          description: >-
            `RNC_NOT_IN_DGII_REGISTRY` (el RNC no aparece en el padron DGII) o
            `RNC_NOT_ACTIVE_IN_DGII` (figura con otro estado, en `details.status`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /partner/v1/fiscal-accounts/{fiscalAccountId}:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Consulta la cuenta fiscal provisionada.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      responses:
        '200':
          description: Cuenta fiscal.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerAccountResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/profile:
    patch:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Actualiza los datos no identificadores de la cuenta fiscal.
      description: >-
        `fiscalEmail`, `phone`, `address`, `provinceCode` y `municipalityCode` (también
        los del alta) se usan para completar los datos del emisor que falten al emitir
        con el contrato simplificado; nunca sustituyen valores enviados en el documento.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerProfilePatch'
      responses:
        '200':
          description: Perfil actualizado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerAccountResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: RNC_CHANGE_REQUIRES_RECERTIFICATION; el RNC no se modifica mediante el perfil.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/status:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Obtiene el estado consolidado de certificacion y ambientes.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      responses:
        '200':
          description: Estado consolidado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerAccountResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-sessions:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Enlace temporal a la pantalla de pasos de la certificación, para incrustarla en el ERP.
      description: >-
        Devuelve la URL de la misma pantalla de pasos del dashboard Partner para esta Cuenta
        fiscal: certificado, postulación, Excel de la DGII, envíos con el avance de la DGII,
        aprobaciones comerciales, simulación, PDF, facturas de consumo y pasos en la DGII. Solo
        se puede mostrar en un iframe desde browserOrigin (CSP frame-ancestors) y solo sirve
        para la certificación de esa Cuenta fiscal. Vence en expiresInMinutes y no se renueva:
        pide uno cada vez que el usuario abra la certificación. Si ZarelaFact tiene configurados
        los orígenes de integración del Partner, browserOrigin debe ser uno de ellos.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [browserOrigin]
              properties:
                browserOrigin:
                  type: string
                  format: uri
                  maxLength: 2048
                  pattern: '^https://'
                  description: Origen HTTPS exacto de la UI del Partner que muestra el iframe.
                expiresInMinutes:
                  type: integer
                  minimum: 5
                  maximum: 480
                  default: 120
      responses:
        '201':
          description: Enlace creado.
          content:
            application/json:
              schema:
                type: object
                required: [fiscalAccountId, url, browserOrigin, expiresAt]
                properties:
                  fiscalAccountId: { type: string, format: uuid }
                  url:
                    type: string
                    format: uri
                    description: Página de ZarelaFact con la certificación; lleva el token firmado en la ruta.
                  browserOrigin: { type: string }
                  expiresAt: { type: string, format: date-time }
                  correlationId: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: BROWSER_ORIGIN_NOT_ALLOWED; browserOrigin no está entre los orígenes de integración del Partner.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: CERTIFICATION_EMBED_UNAVAILABLE; la certificación incrustable no está configurada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certificate-upload-sessions:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Crea una sesion de carga temporal para el certificado .p12/.pfx.
      description: >-
        El backend del ERP declara el browserOrigin HTTPS exacto de su propia UI y
        entrega el uploadToken al navegador autorizado. PSFE vincula CORS a esa
        sesión; no requiere registrar el dominio con el proveedor. El ERP nunca
        debe persistir el certificado ni la contraseña en su backend.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [browserOrigin]
              properties:
                browserOrigin:
                  type: string
                  format: uri
                  maxLength: 2048
                  pattern: '^https://'
                  description: Origen HTTPS exacto de la UI del Partner, sin ruta, query, fragmento, credenciales ni wildcard.
      responses:
        '201':
          description: Sesion temporal creada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerUploadSession'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certificate-rotation-requests:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Crea una sesión temporal para reemplazar el certificado del tenant.
      description: Usa el mismo flujo de carga segura y browserOrigin por sesión que la instalación inicial. La sesión por sí sola no reemplaza el certificado activo.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [browserOrigin]
              properties:
                browserOrigin:
                  type: string
                  format: uri
                  maxLength: 2048
                  pattern: '^https://'
                  description: Origen HTTPS exacto de la UI del Partner, sin ruta, query, fragmento, credenciales ni wildcard.
      responses:
        '201':
          description: Sesión de rotación creada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerUploadSession'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certificate:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Consulta metadata del certificado de CerteCF.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      responses:
        '200':
          description: Metadata sin material secreto.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificateResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/dgii-registration:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Datos que el contribuyente declara en su postulación DGII.
      description: >-
        Devuelve el software, el proveedor (ZARELA GROUP SRL, nombre comercial ZARELA GROUP,
        RNC 133816164) y las URLs
        de recepción y aprobación comercial que el tenant registra en el portal de
        certificación DGII para CerteCF y, al final, para eCF. Las URLs son la base del
        ambiente; DGII y los demás emisores les agregan `/fe/recepcion/api/ecf` y
        `/fe/aprobacioncomercial/api/ecf`. La URL de autenticación se deja en blanco.
        Requiere `certification:read`.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      responses:
        '200':
          description: Datos de postulación de la Cuenta fiscal.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerDgiiRegistration'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-xml-signatures:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Firma el XML de postulación o declaración jurada con el certificado del tenant.
      description: >-
        El ERP descarga del portal DGII el XML sin firmar y lo envía en Base64. PSFE lo
        firma con el certificado instalado de la Cuenta fiscal y devuelve el XML firmado
        para subirlo a DGII. Solo admite raíces `Postulacion*` (`postulation`) y
        `DeclaracionJurada*` (`sworn_declaration`), y el XML debe contener el RNC de la
        Cuenta fiscal: nunca firma e-CF, acuses ni semillas de autenticación. Requiere
        `certification:attest`. Firmar no registra la atestación: después de subir el
        archivo a DGII, registra `dgii_postulation_submitted` o `sworn_declaration_submitted`.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [documentType, xmlBase64]
              additionalProperties: false
              properties:
                documentType:
                  type: string
                  enum: [postulation, sworn_declaration]
                xmlBase64:
                  type: string
                  contentEncoding: base64
                  description: XML generado por el portal DGII, sin firmar.
      responses:
        '200':
          description: XML firmado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificationXmlSignature'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: CERTIFICATE_NOT_INSTALLED; carga primero el certificado del contribuyente.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: >-
            INVALID_CERTIFICATION_XML, CERTIFICATION_XML_TYPE_NOT_ALLOWED,
            CERTIFICATION_XML_RNC_MISMATCH o CERTIFICATION_XML_SIGNING_FAILED.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/certificate-upload-sessions/{uploadId}:
    put:
      tags: [Partner]
      security: []
      summary: Carga el certificado usando el token temporal.
      description: >-
        Este endpoint recibe el certificado y su contraseña mediante multipart/form-data,
        y el token temporal en `Authorization: Upload <uploadToken>`; no usa X-PARTNER-KEY.
        `401` indica token ausente, inválido, vencido o ya usado; `403 PARTNER_INACTIVE`
        indica que el Partner fue suspendido o deshabilitado durante la sesión. `422` indica
        un certificado que no se pudo leer (CERTIFICATE_PASSWORD_INVALID, CERTIFICATE_FILE_INVALID,
        CERTIFICATE_PRIVATE_KEY_MISSING, CERTIFICATE_KEY_PAIR_NOT_FOUND) o que no sirve para
        firmar e-CF: vencido o aún no válido (CERTIFICATE_EXPIRED, CERTIFICATE_NOT_YET_VALID),
        sin el RNC, la cédula o el pasaporte del titular en el SN (CERTIFICATE_HOLDER_ID_MISSING)
        o con un uso de clave sin firma digital ni no repudio (CERTIFICATE_KEY_USAGE_INVALID).
        Un error del archivo o de la contraseña no gasta la sesión: admite cinco intentos y
        cada error trae `details.attemptsRemaining`.
      parameters:
        - name: uploadId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: Authorization
          in: header
          required: true
          schema:
            type: string
            pattern: '^Upload .+$'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file, passphrase]
              additionalProperties: false
              properties:
                file:
                  type: string
                  format: binary
                passphrase:
                  type: string
                  format: password
      responses:
        '201':
          description: Certificado recibido y validado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificateResponse'
        '413':
          description: CERTIFICATE_TOO_LARGE; el archivo supera `maxBytes` de la sesión.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Crea o recupera el caso de certificacion CerteCF.
      description: >-
        Solo existe un Caso activo por Cuenta fiscal. Repetir la llamada con el mismo
        `officialSetRef` devuelve `200` con el Caso activo o, si ya fue aprobado, con
        el Caso aprobado. Un Caso `failed`, `blocked` o `rejected` sigue activo hasta
        que operaciones PSFE lo cierre; después se abre uno nuevo con otro set oficial.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [officialSetRef]
              additionalProperties: false
              properties:
                officialSetRef:
                  type: string
                  description: >-
                    El `testSetRef` del Excel de la DGII subido para esta Cuenta fiscal o un set
                    oficial habilitado por PSFE. `dgii-model` responde 422
                    CERTIFICATION_OWN_SET_REQUIRED, salvo que repita un Caso que ya lo usa.
      responses:
        '201':
          description: Caso creado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificationCase'
        '200':
          description: Caso activo existente, o Caso ya aprobado con el mismo officialSetRef.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificationCase'
        '409':
          description: CERTIFICATION_CASE_SET_CONFLICT; el Caso activo está vinculado a otro set oficial.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '422':
          description: >-
            CERTIFICATION_SET_NOT_OFFICIAL (la referencia no está en el catálogo configurado),
            CERTIFICATION_SET_ISSUER_MISMATCH (el set fue emitido para otro RNC; no se crea el
            Caso), CERTIFICATION_OWN_SET_REQUIRED (`dgii-model` no sirve para las pruebas de
            datos: sube el Excel de la DGII del contribuyente) o CERTIFICATION_SET_INVALID.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '503':
          description: >-
            CERTIFICATION_CATALOG_INVALID, CERTIFICATION_SET_NOT_CONFIGURED,
            CERTIFICATION_SET_UNAVAILABLE o CERTIFICATION_SET_INTEGRITY_FAILED; reintenta
            después o contacta a PSFE.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases/{caseId}:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Consulta el caso de certificacion.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
      responses:
        '200':
          description: Caso de certificacion.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificationCase'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases/{caseId}/runs:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Lista los runs recientes para recuperar un runId después de una respuesta perdida.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
      responses:
        '200':
          description: Runs del caso, más reciente primero.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerRunList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Ejecuta el set oficial de pruebas de CerteCF.
      description: PSFE selecciona el set oficial; el Partner no puede sustituirlo por casos arbitrarios.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
        - name: Idempotency-Key
          in: header
          required: true
          description: Identificador estable generado por el backend ERP para este intento lógico.
          schema:
            type: string
            minLength: 1
            maxLength: 200
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                correlationId: { type: string, maxLength: 200 }
                b2bSimulation:
                  type: boolean
                  description: >-
                    Simula la aprobación comercial (ACECF) al terminar los lotes. Por omisión es
                    `true` en las pruebas de datos y `false` en la simulación.
      responses:
        '202':
          description: Run encolado, o el mismo run si se repite la Idempotency-Key con los mismos parámetros (`replayed=true`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerRunStarted'
        '409':
          description: >-
            IDEMPOTENCY_CONFLICT (clave reutilizada con otros parámetros),
            CERTIFICATION_CASE_NOT_READY (el Caso no está en certificate_ready,
            application_pending ni ready_for_tests) o CERTIFICATION_SET_ALREADY_USED
            (los eNCF del set oficial ya existen en CerteCF; `details.usedEncfCount` y
            hasta 20 `details.encfs`). Este último no se resuelve reintentando: reinicia las
            pruebas de datos o sube el set nuevo de la DGII. CERTIFICATION_OWN_SET_REQUIRED: el
            Caso usa `dgii-model` y le tocan las pruebas de datos; sube el Excel de la DGII y
            cambia el Caso a ese set con `POST .../official-set`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '422':
          description: CERTIFICATION_SET_ISSUER_MISMATCH (el set no corresponde al RNC de la Cuenta fiscal) o CERTIFICATION_PREVALIDATION_FAILED (`details.results`).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '503':
          description: >-
            CERTIFICATION_CATALOG_INVALID, CERTIFICATION_SET_NOT_CONFIGURED,
            CERTIFICATION_SET_UNAVAILABLE o CERTIFICATION_SET_INTEGRITY_FAILED; el set oficial
            no está disponible y el run no se creó.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases/{caseId}/runs/{runId}:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Consulta el run de certificacion.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
        - $ref: '#/components/parameters/CertificationRunId'
      responses:
        '200':
          description: Estado del run.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificationRunStatus'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases/{caseId}/runs/{runId}/evidence:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Descarga la evidencia de certificacion en base64.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
        - $ref: '#/components/parameters/CertificationRunId'
      responses:
        '200':
          description: ZIP codificado en base64.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificationEvidenceResponse'
        '409':
          description: La evidencia todavía no está disponible.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases/{caseId}/runs/{runId}/consumer-summaries:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Descarga una copia de los resúmenes RFCE enviados de las Facturas de Consumo menores a RD$250,000.
      description: >-
        El resumen (RFCE) firmado que la DGII aceptó de cada Factura de Consumo menor a
        RD$250,000 del run. Cada RFCE resume una sola factura (formato RFCE 32 de la DGII), con el
        nombre con que se envió: RNC emisor + e-NCF + `.xml`. Sin `encf` devuelve un ZIP con todos;
        con `encf`, ese XML solo. Es una copia de lo que ZarelaFact envió al servicio de resúmenes de
        la DGII: no se sube en el portal (en «Facturas de consumo < 250Mil» va el e-CF de
        `consumer-invoices`).
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
        - $ref: '#/components/parameters/CertificationRunId'
        - name: encf
          in: query
          required: false
          schema: { type: string, example: E320000000012 }
          description: Solo el XML de esa factura, sin ZIP.
      responses:
        '200':
          description: ZIP (o XML suelto con `encf`) codificado en base64.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerRfceSummaryXmlResponse'
        '409':
          description: NO_RFCE_SUMMARIES (el run no tiene Facturas de Consumo menores a RD$250,000 aceptadas).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases/{caseId}/runs/{runId}/consumer-invoices:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Descarga el XML del e-CF de las Facturas de Consumo menores a RD$250,000.
      description: >-
        El XML firmado del e-CF completo (raíz ECF, XSD «e-CF 32») de cada Factura de Consumo
        menor a RD$250,000 aceptada en el run, con el nombre RNC emisor + e-NCF + `.xml`. Sin `encf`
        devuelve un ZIP; con `encf`, ese XML solo. Es lo que la DGII pide en «Facturas de consumo <
        250Mil» de CerteCF: cada XML por separado y del último run.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
        - $ref: '#/components/parameters/CertificationRunId'
        - name: encf
          in: query
          required: false
          schema: { type: string, example: E320000000012 }
          description: Solo el XML de esa factura, sin ZIP.
      responses:
        '200':
          description: ZIP (o XML suelto con `encf`) codificado en base64.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerRfceSummaryXmlResponse'
        '409':
          description: NO_CONSUMER_INVOICES (el run no tiene Facturas de Consumo menores a RD$250,000 aceptadas).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases/{caseId}/runs/{runId}/printed-pdfs:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Descarga los PDF de la representación impresa del run.
      description: >-
        La representación impresa (PDF con QR) de cada comprobante aceptado, para subirla al portal
        de la DGII (según su guía, los de la simulación). Devuelve un ZIP en base64 con un PDF por
        comprobante; `exceedsDgiiLimit` avisa si pasa de los 10 MB que admite el portal de la DGII.
        Con `selection=dgii`, un solo PDF por casilla del paso 5 de CerteCF (tipos 31, 32 de
        RD$250,000 o más, 33, 34, 41, 43, 44, 45, 46, 47 y 32 menor a RD$250,000), numerado y con
        el nombre de la casilla ("01 - Tipo 31 - E310009002001.pdf"); `slots` dice cuál va en cada una.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
        - $ref: '#/components/parameters/CertificationRunId'
        - name: selection
          in: query
          required: false
          schema: { type: string, enum: [all, dgii], default: all }
          description: '`dgii`: un PDF por casilla del paso 5 de la DGII. `all`: todos.'
      responses:
        '200':
          description: ZIP codificado en base64.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerPrintedPdfsResponse'
        '409':
          description: NO_PRINTED_PDFS (el run todavía no tiene representaciones impresas).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases/{caseId}/attestations:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Registra una atestacion humana del integrador.
      description: >-
        Una Atestación del contribuyente declara un paso externo completado; nunca
        equivale a una Aprobación DGII verificada. Cada `type` alimenta un campo del
        `checklist` (dgii_postulation_submitted → dgiiPostulationSubmitted,
        printed_representations_uploaded → printedRepresentationsUploaded,
        production_urls_submitted → productionUrlsSubmitted,
        sworn_declaration_submitted → swornDeclarationSubmitted,
        ofv_roles_configured → ofvRolesConfigured); la acción `submit_dgii_postulation`
        de `partnerActions` se cumple con `dgii_postulation_submitted`, que además mueve
        el Caso a `application_pending`. Con la evidencia del run lista y las cinco
        atestaciones registradas, el sistema registra la Aprobación DGII verificada,
        el Caso pasa a `approved` y producción (eCF) se activa sola si el certificado
        está vigente. Los pasos del sistema (certificate_validated,
        test_set_prevalidated, runner_passed, evidence_ready, dgii_approval_verified,
        production_active) devuelven `403 SYSTEM_ATTESTATION_FORBIDDEN`; un tipo
        desconocido devuelve `400 INVALID_ATTESTATION_TYPE`.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, actorRef]
              additionalProperties: false
              properties:
                type:
                  type: string
                  enum:
                    - dgii_postulation_submitted
                    - printed_representations_uploaded
                    - production_urls_submitted
                    - sworn_declaration_submitted
                    - ofv_roles_configured
                actorRef: { type: string, maxLength: 200 }
                occurredAt: { type: string, format: date-time }
                evidenceRefs: { type: array, maxItems: 50, items: { type: string, maxLength: 500 } }
                notes: { type: string, maxLength: 4000 }
      responses:
        '201':
          description: Atestacion registrada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificationCase'
        '200':
          description: Atestacion repetida idempotentemente.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificationCase'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-test-sets:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Lista los sets de pruebas DGII subidos para la Cuenta fiscal.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      responses:
        '200':
          description: Sets subidos, del más reciente al más antiguo.
          content:
            application/json:
              schema:
                type: object
                required: [fiscalAccountId, items]
                properties:
                  fiscalAccountId: { type: string, format: uuid }
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/PartnerCertificationTestSet' }
                  correlationId: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Sube el set de pruebas que la DGII entregó al contribuyente.
      description: >-
        Envía el archivo tal como lo descargó tu cliente del portal de certificación
        de la DGII: el Excel (.xlsx) o el JSON en `contentBase64`, o el JSON como
        objeto en `set`. Máximo 700 KB. ZarelaFact lo procesa, comprueba que el
        emisor sea el RNC de la Cuenta fiscal, que sus eNCF no se hayan emitido en
        CerteCF y que cada comprobante pase la prevalidación local. Responde un
        `testSetRef` para crear el Caso (`officialSetRef`). El mismo archivo
        devuelve el set ya guardado (`200`, `replayed: true`). Un Excel con
        columnas que ZarelaFact no reconoce responde `422 CERTIFICATION_SET_INVALID`
        con la lista de columnas en `details`.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                fileName: { type: string, maxLength: 200 }
                contentBase64:
                  type: string
                  description: Archivo .xlsx o .json en base64. Usa este campo o `set`, no ambos.
                set:
                  type: object
                  description: Set en JSON (`documents`). Usa este campo o `contentBase64`, no ambos.
      responses:
        '201':
          description: Set guardado.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PartnerCertificationTestSet' }
        '200':
          description: El mismo archivo ya estaba guardado.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PartnerCertificationTestSet' }
        '409':
          description: CERTIFICATION_SET_ALREADY_USED (sus eNCF ya se emitieron en CerteCF).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '413':
          description: CERTIFICATION_SET_TOO_LARGE (más de 700 KB).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '422':
          description: >-
            CERTIFICATION_SET_INVALID (no se pudo procesar), CERTIFICATION_SET_ISSUER_MISMATCH
            (el emisor no es el RNC de la Cuenta fiscal) o CERTIFICATION_PREVALIDATION_FAILED.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases/{caseId}/close:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Cierra el Caso para reiniciar las pruebas.
      description: >-
        Cierra el Caso salvo con un envío en curso (`prevalidating`, `running`),
        esperando a la DGII (`submitted_to_dgii`, `awaiting_dgii`), aprobado o
        bloqueado (ese lo cierra PSFE). Repetir la llamada devuelve el Caso cerrado
        con `replayed: true`. Después crea otro Caso con el `testSetRef` del set que vas
        a usar.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                reason: { type: string, maxLength: 1000 }
      responses:
        '200':
          description: 'Caso cerrado (closed = true).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificationCase'
        '409':
          description: CERTIFICATION_CASE_NOT_CLOSABLE (el Caso no está fallido ni rechazado).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases/{caseId}/restart:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Reinicia una parte de las pruebas sin cerrar el Caso.
      description: >-
        `part: data` reinicia las pruebas de datos y `part: simulation`, la simulación. Detiene
        el run en curso y deja los comprobantes de esa parte como reemplazados (no se borran):
        el siguiente run vuelve a enviar las pruebas de datos con los mismos e-NCF del set, o la
        simulación con un bloque nuevo. El Caso conserva su certificado y sus atestaciones
        (postulación, declaración jurada y las demás) y queda en `ready_for_tests`. No se
        reinicia un Caso aprobado, bloqueado o que espera a la DGII.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [part]
              properties:
                part: { type: string, enum: [data, simulation] }
      responses:
        '200':
          description: Caso listo para enviar otra vez, con `restart` y `parts`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificationCase'
        '409':
          description: CERTIFICATION_CASE_NOT_RESTARTABLE (aprobado, bloqueado o esperando a la DGII).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases/{caseId}/commercial-approvals:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Consulta el último envío de las aprobaciones comerciales de prueba de la Cuenta fiscal.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
      responses:
        '200':
          description: El último envío, o `commercialApprovals` null si todavía no hay ninguno.
          content:
            application/json:
              schema:
                type: object
                required: [caseId, commercialApprovals]
                properties:
                  caseId: { type: string, format: uuid }
                  correlationId: { type: string }
                  commercialApprovals:
                    oneOf:
                      - type: 'null'
                      - $ref: '#/components/schemas/CommercialApprovalTests'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Envía las aprobaciones comerciales de prueba (paso 3 de CerteCF).
      description: >-
        ZarelaFact envía a la DGII, de una en una, las aprobaciones comerciales (ACECF) del
        Excel que el contribuyente descargó en CerteCF con «DESCARGAR APROBACIONES COMERCIALES»
        (hoja ACEECF_Generadas), fila por fila y tal cual, firmadas con el certificado de la
        Cuenta fiscal. La DGII compara cada campo con ese Excel, incluida la fecha y hora de la
        aprobación (la de la descarga), así que va el último que descargó. Se detiene en el
        primer rechazo. Requiere las pruebas de datos de e-CF aceptadas y que el comprador del
        Excel sea la Cuenta fiscal. La misma `Idempotency-Key` con el mismo Excel devuelve el
        mismo envío (`replayed: true`).
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
        - name: Idempotency-Key
          in: header
          required: true
          description: Identificador estable generado por el backend ERP para este envío.
          schema:
            type: string
            minLength: 1
            maxLength: 200
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [workbookBase64]
              properties:
                correlationId: { type: string, maxLength: 200 }
                workbookBase64:
                  type: string
                  description: El Excel de aprobaciones comerciales de la DGII en base64 (máximo 700 KB).
      responses:
        '202':
          description: Envío en cola; consulta su avance con GET o en `commercialApprovals` del Caso.
          content:
            application/json:
              schema:
                type: object
                required: [caseId, replayed, commercialApprovals]
                properties:
                  caseId: { type: string, format: uuid }
                  correlationId: { type: string }
                  replayed: { type: boolean }
                  commercialApprovals: { $ref: '#/components/schemas/CommercialApprovalTests' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: >-
            CERTIFICATION_DATA_TESTS_PENDING, CERTIFICATE_NOT_ACTIVE,
            COMMERCIAL_APPROVALS_IN_PROGRESS o IDEMPOTENCY_CONFLICT.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '422':
          description: >-
            COMMERCIAL_APPROVAL_SET_INVALID (no es el Excel de la DGII o una fila no cumple su
            formato) o COMMERCIAL_APPROVAL_SET_OTHER_TAXPAYER (el comprador del Excel no es la
            Cuenta fiscal).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases/{caseId}/official-set:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Cambia el set de pruebas del Caso sin cerrarlo.
      description: >-
        Para cuando la DGII le entrega al contribuyente un set nuevo (por ejemplo, tras reiniciar
        todo desde cero en su portal): sube el Excel con `POST .../certification-test-sets` y
        cambia el Caso a ese `testSetRef`. Las pruebas de datos se
        reinician con el set nuevo: los comprobantes del set anterior quedan reemplazados y el
        siguiente run usa los e-NCF y los datos del set nuevo. El Caso conserva su certificado y
        sus atestaciones. Pedir el set que ya usa devuelve el Caso sin cambios.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [officialSetRef]
              properties:
                officialSetRef:
                  type: string
                  description: 'El `testSetRef` de un set subido para esta Cuenta fiscal.'
      responses:
        '200':
          description: Caso con el set nuevo, listo para enviar las pruebas de datos.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificationCase'
        '409':
          description: CERTIFICATION_CASE_NOT_RESTARTABLE (aprobado, bloqueado o esperando a la DGII).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '422':
          description: >-
            CERTIFICATION_SET_NOT_OFFICIAL, CERTIFICATION_SET_ISSUER_MISMATCH (el set no es de
            esta Cuenta fiscal) o CERTIFICATION_OWN_SET_REQUIRED (`dgii-model` no sirve para las
            pruebas de datos).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases/{caseId}/documents/{documentId}/resend:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Reenvía a la DGII un comprobante de las pruebas con un problema de envío.
      description: >-
        Para un comprobante del Caso que ya se firmó y la DGII no ha resuelto: sin respuesta
        (contingencia), un resumen RFCE que la DGII da por duplicado, consultas agotadas o sin
        novedad (`resendable: true` en el run). Lo envía otra vez de inmediato, o consulta su
        estado si ya tiene TrackID. Un resumen que la DGII da por duplicado se consulta con
        ConsultaRFCE y se reenvía idéntico si la DGII no lo tiene; el E32 nunca se vuelve a
        firmar. Un comprobante rechazado no se reenvía: reinicia esa parte de las pruebas.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
        - name: documentId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '202':
          description: Reenvío en cola.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificationResend'
        '409':
          description: >-
            DOCUMENT_ALREADY_ACCEPTED, DOCUMENT_REJECTED, DOCUMENT_NOT_RESENDABLE,
            DOCUMENT_SEND_IN_PROGRESS o DOCUMENT_SUPERSEDED.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/production-activation-requests:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Activa produccion de una Cuenta fiscal certificada.
      description: >-
        Normalmente no hace falta: producción se activa sola cuando el Caso queda
        `approved`. Úsalo para reintentar la activación (por ejemplo, tras cargar un
        certificado nuevo). Con un Caso `approved` y certificado vigente la respuesta
        trae `state: active`. Sin certificado vigente responde
        `409 CERTIFICATE_NOT_ACTIVE` y no queda ninguna solicitud pendiente. Si PSFE
        suspendió la Cuenta fiscal, la solicitud queda en `activation_requested`
        hasta la decisión de PSFE.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [certificationCaseId]
              additionalProperties: false
              properties:
                certificationCaseId: { type: string, format: uuid }
                notes: { type: string, maxLength: 4000 }
                evidenceRefs: { type: array, maxItems: 50, items: { type: string, maxLength: 500 } }
      responses:
        '201':
          description: Producción activada (o solicitud pendiente de PSFE si la Cuenta fiscal estaba suspendida).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerProductionState'
        '200':
          description: Solicitud pendiente repetida idempotentemente.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerProductionState'
        '409':
          description: CERTIFICATION_NOT_APPROVED (el Caso no está aprobado), CERTIFICATE_NOT_ACTIVE (sin certificado vigente) o PRODUCTION_STATE_CONFLICT (producción ya está activa).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/production:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Consulta la autorizacion de produccion.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      responses:
        '200':
          description: Estado de produccion.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerProductionState'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/production/documents:
    post:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Prevalida o emite un e-CF en produccion para la cuenta fiscal.
      description: El ERP conserva y asigna el eNCF. `validate=true` funciona antes de activar eCF y no reserva secuencia, firma, crea un job fiscal, envia a DGII ni emite webhooks `document.state_changed`; PSFE puede conservar un registro preflight_validated solo para auditoria. La emision normal exige produccion activa y es asincrona, y corre antes la misma validacion que `validate=true` (estructura, cuadre y XSD oficial); si falla responde `422` sin registrar el documento. Las reglas del JSON DGII y del contrato simplificado son las de `POST /{environment}/documentos-ecf`, igual que la cabecera `Prefer` con `wait=N` para recibir los datos de impresion en un `201`. Los marcadores `{{RNC}}`, `{{RAZON_SOCIAL}}`, `{{NOMBRE_COMERCIAL}}`, `{{FECHA_EMISION}}` y `{{FECHA_LIMITE_PAGO}}` se completan con los datos de la Cuenta fiscal; cualquier otro marcador `{{...}}` responde `422 UNRESOLVED_PLACEHOLDER`.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - name: validate
          in: query
          required: false
          schema: { type: boolean, default: false }
        - $ref: '#/components/parameters/PreferWait'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [document]
              properties:
                document:
                  $ref: '#/components/schemas/EcfSubmitRequest'
      responses:
        '200':
          description: Preflight valido.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitResponse'
        '201':
          $ref: '#/components/responses/SubmitSigned'
        '202':
          description: >-
            Documento encolado. Con `Prefer: wait=N`, la DGII no respondio
            dentro del plazo y trae `Retry-After`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitResponse'
        '409':
          description: ENCF_PAYLOAD_MISMATCH (salvo la version corregida de un e-CF rechazado con `secuenciaUtilizada` false, que se emite con `result.replacesDocumentId`), ENCF_REPLAY_UNVERIFIABLE o ENCF_ANNULLED para el eNCF enviado; ENCF_NOT_IN_REGISTERED_RANGE si la cuenta registro rangos de ese tipo en el ambiente y el eNCF no cae en ninguno (`details.registeredRanges`), o ENCF_RANGE_DISABLED si solo cae en un rango deshabilitado (tambien al prevalidar). Sin rangos registrados del tipo se emite con el aviso `ENCF_RANGE_NOT_REGISTERED` en `warnings`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '422':
          description: Validación fiscal fallida; `errors` lista los campos. Un RNC emisor distinto del de la Cuenta fiscal se reporta en `ECF.Encabezado.Emisor.RNCEmisor` o `issuer.rnc`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitResponse'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '423':
          description: PRODUCTION_NOT_ACTIVE; la emisión normal se intentó antes de autorizar producción. No aplica al preflight `validate=true`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/production/documents/{documentId}:
    get:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Consulta el estado de un documento de produccion.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - name: documentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Estado del documento.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerDocumentStatus'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/production/documents/{documentId}/xml:
    get:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Descarga el XML firmado de un documento de producción.
      description: Responde `409 DOCUMENT_NOT_SIGNED` mientras el documento no está firmado.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - name: documentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: XML firmado del e-CF.
          content:
            application/xml:
              schema: { type: string }
        '409':
          description: DOCUMENT_NOT_SIGNED.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/production/documents/{documentId}/pdf:
    get:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Descarga la representación impresa (PDF) de un documento de producción.
      description: Responde `409 DOCUMENT_NOT_SIGNED` mientras el documento no está firmado, `409 RI_AWAITING_DGII_RESPONSE` mientras la DGII no responde (TrackID del e-CF o respuesta del resumen RFCE), `409 RI_NOT_DELIVERABLE_REJECTED` si la DGII rechazó el resumen RFCE y `409 RI_NOT_DELIVERABLE_NOT_TRANSMITTED` si el documento está en `transmission_failed`.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - name: documentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: PDF tamaño carta con QR, código de seguridad y fecha de firma.
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '409':
          description: DOCUMENT_NOT_SIGNED, RI_AWAITING_DGII_RESPONSE, RI_NOT_DELIVERABLE_REJECTED o RI_NOT_DELIVERABLE_NOT_TRANSMITTED.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '422':
          description: RI_GENERATION_FAILED.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/production/annulments:
    get:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Lista las anulaciones (ANECF) del cliente en producción.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      responses:
        '200':
          description: Anulaciones, de la más reciente a la más antigua.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  correlationId: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Anula eNCF que el cliente no usará (ANECF).
      description: >-
        Firma y envía la ANECF a la DGII de producción. Requiere producción activa
        (`423 PRODUCTION_NOT_ACTIVE`) y la cabecera `Idempotency-Key`
        (`428 IDEMPOTENCY_KEY_REQUIRED`). No exige rangos registrados: la DGII
        comprueba que las secuencias sean del RNC. Responde como la anulación de la
        API directa: `202` si la DGII la acepta; `422` con el cuerpo de la anulación
        (`ok: false`, `status: rejected`, `dgii.mensajes`) si la DGII la rechaza, un
        rechazo definitivo que libera los rangos; y `502` con `status: uncertain` si
        no hubo respuesta o la anulación fue parcial (no la reenvíes). Un e-CF
        firmado que nunca salió hacia la DGII ni hacia el comprador se puede anular:
        al aceptarse la ANECF queda `cancelled` y no se envía
        (`coveredDocuments.cancelled`); lo que pudo llegar va en
        `coveredDocuments.skipped`.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - name: Idempotency-Key
          in: header
          required: true
          schema: { type: string, maxLength: 128 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ranges]
              properties:
                ranges:
                  type: array
                  description: Bloques a anular por tipo; desde y hasta son eNCF completos.
                  items:
                    type: object
                    required: [ecfType, sequences]
                    properties:
                      ecfType: { type: string, pattern: '^E[0-9]{2}$' }
                      sequences:
                        type: array
                        items:
                          type: object
                          required: [desde, hasta]
                          properties:
                            desde: { type: string }
                            hasta: { type: string }
      responses:
        '200':
          description: 'Anulación repetida con la misma clave (`idempotent: true`).'
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  correlationId: { type: string }
        '202':
          description: La DGII aceptó la anulación; `coveredDocuments` lista los comprobantes cancelados.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  correlationId: { type: string }
        '409':
          description: >-
            ANECF_RANGE_RESERVED, ANECF_IDEMPOTENCY_CONFLICT o ANECF_DOCUMENT_CONFLICT (un
            comprobante del rango llegó o pudo llegar a la DGII o al comprador).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '422':
          description: >-
            Datos inválidos. Un rechazo de la DGII también responde `422`, con el cuerpo
            de la anulación en vez de un error (ver la descripción).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '423':
          description: PRODUCTION_NOT_ACTIVE.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '428':
          description: IDEMPOTENCY_KEY_REQUIRED.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/production/annulments/prevalidate:
    post:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Prevalida una anulación sin enviarla.
      description: >-
        Construye la ANECF, valida el XSD y revisa los comprobantes del rango. Un e-CF
        firmado que nunca salió hacia la DGII ni hacia el comprador se puede anular; uno
        que llegó o pudo llegar responde `400 ANECF_DOCUMENT_CONFLICT` con
        `details.conflicts[].reason`: `document_already_submitted_or_dgii_validated`,
        `document_in_contingency`, `document_dgii_submission_uncertain`,
        `document_dgii_submission_in_progress` o `document_already_delivered_to_receiver`.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ranges]
              properties:
                ranges:
                  type: array
                  description: Bloques a anular por tipo; desde y hasta son eNCF completos.
                  items:
                    type: object
                    required: [ecfType, sequences]
                    properties:
                      ecfType: { type: string, pattern: '^E[0-9]{2}$' }
                      sequences:
                        type: array
                        items:
                          type: object
                          required: [desde, hasta]
                          properties:
                            desde: { type: string }
                            hasta: { type: string }
      responses:
        '200':
          description: ANECF construida y válida.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  correlationId: { type: string }
        '422':
          description: Error de validación.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/production/annulments/{annulmentId}:
    get:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Consulta una anulación.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - name: annulmentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Estado de la anulación y respuesta de la DGII.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  correlationId: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/production/sequences:
    get:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Lista los rangos autorizados registrados (opcional; anular no los exige).
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      responses:
        '200':
          description: Rangos registrados del ambiente eCF del cliente.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  correlationId: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Registra un rango de eNCF autorizado por la DGII.
      description: >-
        Registro opcional: anular no lo exige y ZarelaFact no numera los e-CF.
        Si ya hay un rango vigente del mismo tipo que no se solapa, ese queda
        cerrado.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ecfType, rangeStart, rangeEnd]
              properties:
                ecfType: { type: string, pattern: '^E[0-9]{2}$' }
                rangeStart: { type: integer, minimum: 1 }
                rangeEnd: { type: integer, minimum: 1 }
                encfLength: { type: integer, description: 'Longitud total del eNCF (13).' }
                expiresAt: { type: string, format: date-time }
      responses:
        '201':
          description: Rango registrado; `replacedRangeId` indica el rango que quedó cerrado.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  correlationId: { type: string }
        '409':
          description: SEQUENCE_RANGE_ACTIVE_EXISTS (se solapa con el rango vigente) o SEQUENCE_RANGE_OVERLAP (se solapa con un rango agotado o deshabilitado del mismo tipo y ambiente; los rangos autorizados no se repiten).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/production/inbound-documents:
    get:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Lista los e-CF que los proveedores enviaron al cliente.
      description: >-
        También llegan como webhook `document.received`. Del más reciente al más antiguo,
        con el cursor, los filtros y los elementos de `GET /{environment}/documentos-recibidos`
        de la API directa. Los campos de antes (`inboundDocumentId`, `encf`, `issuerRnc`,
        `receiverRnc`, `status`, `commercialApproval`, `receivedAt`) no cambian; se agregan
        `group`, `commercialApprovalDeadline`, `view` y `nextCursor`. Un `limit` no
        numérico usa 50.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/ReceivedDocumentsGroup'
        - $ref: '#/components/parameters/ReceivedDocumentsType'
        - $ref: '#/components/parameters/ReceivedDocumentsEncf'
        - $ref: '#/components/parameters/ReceivedDocumentsIssuerRnc'
        - $ref: '#/components/parameters/ReceivedDocumentsFrom'
        - $ref: '#/components/parameters/ReceivedDocumentsTo'
        - $ref: '#/components/parameters/ReceivedDocumentsLimit'
        - $ref: '#/components/parameters/ReceivedDocumentsCursor'
      responses:
        '200':
          description: e-CF recibidos con su estado de aprobación comercial.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerReceivedDocumentsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/production/inbound-documents/{inboundDocumentId}/commercial-approval:
    post:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Acepta o rechaza comercialmente un e-CF recibido (ACECF).
      description: >-
        Igual que la aprobación comercial de la API directa; requiere producción activa.
        El ACECF va a la DGII y después al emisor, cuya URL sale del directorio DGII
        consultado con el token DGII del cliente. `202` si llegó al emisor; `502` si el
        despacho al emisor falló o quedó incierto (el cuerpo trae `commercialApproval`)
        o si no se pudo confirmar el envío a la DGII
        (`ACECF_DGII_SUBMISSION_UNCERTAIN`).
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - name: inboundDocumentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [decision]
              properties:
                decision: { type: string, enum: [accept, reject] }
                rejectionReason: { type: string, maxLength: 250 }
      responses:
        '200':
          description: ACECF firmada y enviada (o registrada).
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  correlationId: { type: string }
        '202':
          description: ACECF firmada, aceptada por la DGII y entregada al emisor.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  correlationId: { type: string }
        '409':
          description: >-
            La aprobación ya se envió con otra decisión, o un envío anterior a la DGII
            quedó incierto (`ACECF_DGII_SUBMISSION_UNCERTAIN`).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '422':
          description: >-
            `ACECF_DGII_REJECTED`: la DGII rechazó la aprobación comercial; el detalle va en
            `details.dgiiSubmission.dgiiMessages`. Se puede corregir y enviar otra vez.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '423':
          description: PRODUCTION_NOT_ACTIVE.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '503':
          description: >-
            `ACECF_DGII_CERTIFICATE_UNAVAILABLE`, `DIRECTORY_DGII_AUTH_FAILED`,
            `DIRECTORY_ACCESS_TOKEN_MISSING` o `ACECF_SIGNING_FAILED`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/testecf/documents:
    post:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Prevalida o emite un e-CF de prueba en TesteCF, el sandbox de la cuenta fiscal.
      description: >-
        TesteCF es el sandbox de cada Cuenta fiscal: el mismo contrato que
        `POST /production/documents` (JSON DGII o simplificado, `validate=true`,
        `Prefer: wait=N`, idempotencia por eNCF), pero el e-CF va al ambiente de
        pruebas de la DGII. No exige producción activa ni plan: los comprobantes
        de prueba no cuentan en el plan ni se cobran. Firma con el certificado
        de la Cuenta fiscal (el mismo de CerteCF y eCF), así que el .p12 debe
        estar cargado. TesteCF es compartido por todos los contribuyentes: usa
        secuencias altas (por ejemplo E310009000001) para no chocar con eNCF ya
        usados. Los webhooks `document.state_changed` llegan con
        `environment: testecf`.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - name: validate
          in: query
          required: false
          schema: { type: boolean, default: false }
        - $ref: '#/components/parameters/PreferWait'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [document]
              properties:
                document:
                  $ref: '#/components/schemas/EcfSubmitRequest'
      responses:
        '200':
          description: Preflight válido.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitResponse'
        '201':
          $ref: '#/components/responses/SubmitSigned'
        '202':
          description: >-
            Documento encolado. Con `Prefer: wait=N`, la DGII no respondió
            dentro del plazo y trae `Retry-After`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitResponse'
        '409':
          description: ENCF_PAYLOAD_MISMATCH o ENCF_REPLAY_UNVERIFIABLE para el eNCF enviado; ENCF_NOT_IN_REGISTERED_RANGE o ENCF_RANGE_DISABLED si la Cuenta fiscal registro rangos de ese tipo y el eNCF no cae en uno vigente.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '422':
          description: Validación fiscal fallida; `errors` lista los campos.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/testecf/documents/{documentId}:
    get:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Consulta el estado de un documento de prueba (TesteCF).
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - name: documentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Estado del documento.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerDocumentStatus'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/testecf/documents/{documentId}/xml:
    get:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Descarga el XML firmado de un documento de prueba (TesteCF).
      description: Responde `409 DOCUMENT_NOT_SIGNED` mientras el documento no está firmado.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - name: documentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: XML firmado del e-CF.
          content:
            application/xml:
              schema: { type: string }
        '409':
          description: DOCUMENT_NOT_SIGNED.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/testecf/documents/{documentId}/pdf:
    get:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Descarga la representación impresa (PDF) de un documento de prueba (TesteCF).
      description: Responde `409 DOCUMENT_NOT_SIGNED` mientras el documento no está firmado, `409 RI_AWAITING_DGII_RESPONSE` mientras la DGII no responde y `409 RI_NOT_DELIVERABLE_REJECTED` si la DGII rechazó el resumen RFCE. El emisor impreso es el de la Cuenta fiscal.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - name: documentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: PDF tamaño carta con QR, código de seguridad y fecha de firma.
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '409':
          description: DOCUMENT_NOT_SIGNED, RI_AWAITING_DGII_RESPONSE, RI_NOT_DELIVERABLE_REJECTED o RI_NOT_DELIVERABLE_NOT_TRANSMITTED.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '422':
          description: RI_GENERATION_FAILED.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/testecf/documents/status/batch:
    post:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Reconcilia estados de documentos de prueba (TesteCF).
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StatusBatchRequest'
      responses:
        '200':
          description: Estados actuales.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusBatchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/production/documents/status/batch:
    post:
      tags: [Partner, Documentos]
      security:
        - PartnerKeyAuth: []
      summary: Reconcilia estados de documentos de produccion.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StatusBatchRequest'
      responses:
        '200':
          description: Estados actuales.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusBatchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/webhook-endpoints:
    get:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Lista webhooks del Partner.
      responses:
        '200':
          description: Endpoints configurados.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerWebhookEndpointsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Registra un endpoint webhook del ERP.
      description: La URL normalizada identifica un endpoint dentro del Partner. Repetir la creación conserva configuración y secreto; devuelve 200 sin secreto y no reactiva un endpoint pausado/deshabilitado.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerWebhookEndpointRequest'
      responses:
        '201':
          description: Endpoint creado; el secreto se muestra una sola vez.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerWebhookEndpointResponse'
        '200':
          description: Endpoint existente; replayed=true, sin signingSecret.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerWebhookEndpointResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/webhook-endpoints/{endpointId}/test:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Encola una entrega webhook de prueba.
      description: |
        Crea una entrega durable `webhook.test` para una cuenta fiscal del Partner.
        La entrega se firma con el secreto vigente y la procesa el worker; nunca
        expone el secreto ni crea un evento fiscal real.
      parameters:
        - name: endpointId
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PartnerWebhookTestRequest'
      responses:
        '202':
          description: Entrega de prueba encolada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerWebhookTestResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /partner/v1/webhook-endpoints/{endpointId}/rotate-secret:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Rota el secreto de un webhook.
      description: Durante overlapSeconds las entregas Partner llevan dos firmas v1 (actual y anterior). El receptor debe aceptar cualquiera. Una rotación activa repetida antes de vencer la ventana devuelve 409; la recuperación controlada exige pausar primero.
      parameters:
        - name: endpointId
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                overlapSeconds: { type: integer, minimum: 1, maximum: 604800, default: 86400 }
      responses:
        '200':
          description: Endpoint actualizado; el nuevo secreto se muestra una sola vez.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerWebhookEndpointResponse'
        '409':
          description: WEBHOOK_ROTATION_IN_PROGRESS; espera a que expire la ventana o pausa para recuperación controlada.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY
      description: API key server-to-server del ambiente.
    PartnerKeyAuth:
      type: apiKey
      in: header
      name: X-PARTNER-KEY
      description: Credencial server-to-server del ERP/Partner. Nunca se expone al usuario final.
  parameters:
    PartnerFiscalAccountId:
      name: fiscalAccountId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Identificador de la cuenta fiscal PSFE, opaco para el ERP.
    CertificationCaseId:
      name: caseId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    CertificationRunId:
      name: runId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    DocumentReference:
      name: documentReference
      in: path
      required: true
      description: '`documentId` (UUID) o eNCF del documento, por ejemplo E310000000001.'
      schema: { type: string }
    InboundDocumentId:
      name: inboundDocumentId
      in: path
      required: true
      description: '`inboundDocumentId` del e-CF recibido (lista, webhook `document.received`).'
      schema: { type: string, format: uuid }
    IssuedDocumentsIssueDateFrom:
      name: issueDateFrom
      in: query
      required: false
      description: e-CF con `FechaEmision` desde este día (incluido), `AAAA-MM-DD`.
      schema: { type: string, format: date, examples: ['2026-09-01'] }
    IssuedDocumentsIssueDateTo:
      name: issueDateTo
      in: query
      required: false
      description: e-CF con `FechaEmision` hasta este día (incluido), `AAAA-MM-DD`.
      schema: { type: string, format: date, examples: ['2026-09-30'] }
    IssuedDocumentsType:
      name: documentType
      in: query
      required: false
      description: Tipo de e-CF, por ejemplo E31.
      schema: { type: string, pattern: '^[Ee][0-9]{2}$' }
    IssuedDocumentsStatus:
      name: status
      in: query
      required: false
      description: >-
        Estado del documento, el mismo `status` de la consulta por lote (por ejemplo
        `accepted`, `accepted_conditional`, `rejected`, `pending_dgii_status`, `signed`,
        `contingency`, `transmission_failed` o `cancelled`). `preflight_validated` no es un
        e-CF emitido y responde `400`.
      schema: { type: string, pattern: '^[a-z_]+$' }
    ReceivedCommercialApprovalId:
      name: inboundDocumentId
      in: path
      required: true
      description: '`inboundDocumentId` de la ACECF recibida (lista, webhook `document.commercial_approval_received`).'
      schema: { type: string, format: uuid }
    ReceivedCommercialApprovalsEncf:
      name: encf
      in: query
      required: false
      description: e-NCF exacto del e-CF que emitiste.
      schema: { type: string, pattern: '^[Ee][0-9]{12}$' }
    ReceivedCommercialApprovalsBuyerRnc:
      name: buyerRnc
      in: query
      required: false
      description: RNC o cédula de tu cliente (RNCComprador del ACECF).
      schema: { type: string, pattern: '^([0-9]{9}|[0-9]{11})$' }
    ReceivedDocumentsGroup:
      name: group
      in: query
      required: false
      description: Pestaña de la pantalla Recibidos del portal.
      schema:
        type: string
        enum: [por_aprobar, aceptados, rechazados, no_recibidos]
    ReceivedDocumentsType:
      name: documentType
      in: query
      required: false
      description: Tipo de e-CF, por ejemplo E31.
      schema: { type: string, pattern: '^[Ee][0-9]{2}$' }
    ReceivedDocumentsEncf:
      name: encf
      in: query
      required: false
      description: e-NCF exacto.
      schema: { type: string, pattern: '^[Ee][0-9]{12}$' }
    ReceivedDocumentsIssuerRnc:
      name: issuerRnc
      in: query
      required: false
      description: RNC o cédula del proveedor.
      schema: { type: string, pattern: '^([0-9]{9}|[0-9]{11})$' }
    ReceivedDocumentsFrom:
      name: from
      in: query
      required: false
      description: >-
        Recibidos desde este momento (incluido). `AAAA-MM-DD` es la medianoche de
        República Dominicana; también acepta fecha y hora ISO 8601.
      schema: { type: string, examples: ['2026-10-01'] }
    ReceivedDocumentsTo:
      name: to
      in: query
      required: false
      description: >-
        Recibidos antes de este momento (no incluido). `AAAA-MM-DD` es la medianoche de
        República Dominicana; también acepta fecha y hora ISO 8601.
      schema: { type: string, examples: ['2026-11-01'] }
    ReceivedDocumentsLimit:
      name: limit
      in: query
      required: false
      description: Documentos por página. Un valor mayor que 100 se trata como 100.
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
    ReceivedDocumentsCursor:
      name: cursor
      in: query
      required: false
      description: El `nextCursor` de la página anterior.
      schema: { type: string, maxLength: 512 }
    Environment:
      name: environment
      in: path
      required: true
      schema:
        type: string
        enum: [TesteCF, CerteCF, eCF]
      description: Ambiente DGII al que pertenece la operacion.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 128
        pattern: '^[A-Za-z0-9._:-]+$'
      description: |
        Identificador estable requerido para enviar una ANECF. Reutilice el mismo
        valor solo con los mismos rangos; la emision normal de e-CF usa el propio eNCF.
    PreferWait:
      name: Prefer
      in: header
      required: false
      schema:
        type: string
        example: wait=10
      description: |
        RFC 7240. `wait=N` (N de 1 a 15 segundos) espera la respuesta de la DGII
        (TrackID del e-CF o respuesta del resumen RFCE) para responder `201` con
        los datos de impresion. Un N mayor cuenta como 15; un valor invalido se
        ignora y la emision responde como sin la cabecera. El plazo cuenta desde
        que llega la solicitud.
  responses:
    BadRequest:
      description: Solicitud invalida.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Credenciales ausentes o invalidas.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Recurso no encontrado.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: Scope insuficiente u operación no permitida para la credencial.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: El estado actual del recurso no admite la operación; `error.code` indica el motivo.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    UnprocessableEntity:
      description: Payload bien formado pero rechazado por reglas de negocio o fiscales.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    PaymentRequired:
      description: >-
        PAYMENT_REQUIRED: la emisión en producción (eCF) necesita un plan pagado.
        Solo la responde eCF; TesteCF, CerteCF y `validate=true` nunca se
        bloquean por cobro. `details.reason`: `free_tier_exhausted` (cuenta
        directa sin plan pagado que ya usó sus 50 e-CF gratis del mes),
        `payment_required` (Partner sin plan pagado; los Partners no tienen
        tramo gratis) o `payment_failed` (pago vencido y terminó la gracia de 7
        días; el propietario cambia la tarjeta en Empresa → Gestionar plan y
        el pago pendiente se cobra al guardarla). `details.billingUrl` es la
        página del plan en el portal. Además
        trae `accountType` (direct o partner), `plan`, `limit`, `used`
        (accepted + reserved), `accepted` (aceptados por la DGII en el periodo),
        `reserved` (en camino a la DGII), `graceUntil`, `periodStart`,
        `periodEnd` y `periodBasis` (`calendar_month`: mes calendario de RD, el
        del plan Gratis y los planes activados a mano). No se resuelve
        reintentando: el documento no se registra y el eNCF queda libre.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            ok: false
            error:
              code: PAYMENT_REQUIRED
              message: 'Usaste los 50 e-CF gratis del mes en producción (eCF). Para seguir emitiendo elige un plan en https://zarelafact.com/empresa#plan. TesteCF y CerteCF siguen gratis.'
              details:
                reason: free_tier_exhausted
                billingUrl: https://zarelafact.com/empresa#plan
                accountType: direct
                plan: gratis
                limit: 50
                used: 50
                accepted: 48
                reserved: 2
                graceUntil: null
                periodStart: '2026-10-01T04:00:00.000Z'
                periodEnd: '2026-11-01T04:00:00.000Z'
                periodBasis: calendar_month
    Locked:
      description: PRODUCTION_NOT_ACTIVE; la Cuenta fiscal todavía no está autorizada para producción.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    SubmitSigned:
      description: >-
        Con `Prefer: wait=N`: la DGII ya respondio (TrackID del e-CF o respuesta
        Aceptado/Aceptado Condicional del resumen RFCE) o el documento quedo en
        contingencia. Misma forma que el `202`, con `effects.signed` en true,
        `effects.dgiiSubmitted` en true si la DGII lo recibio, y en `result` el
        estado actual (`status`, `dgiiStatus`, `trackId`), `securityCode`,
        `qrUrl`, `printing`, `printingStatus`, `dgiiMessages` y `lastError`,
        como la consulta. Un e-CF con TrackID todavia no es aceptado: el estado
        final llega mientras `asyncStatusRequired` sea true.
      headers:
        Preference-Applied:
          $ref: '#/components/headers/PreferenceApplied'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/SubmitResponse'
    TooManyRequests:
      description: >-
        RATE_LIMIT_EXCEEDED (ventana fija por IP, credencial, Partner, Cuenta fiscal
        o permiso) incluye `Retry-After`, cabeceras `X-RateLimit-*` y
        `details.retryAfterSeconds`/`details.limit`; espera ese tiempo antes de
        reintentar. En emisión, PLAN_LIMIT_EXCEEDED indica que un plan pagado
        agotó su cupo del mes: no trae `Retry-After` y no se resuelve
        reintentando. Consumen cupo los e-CF aceptados por la DGII (accepted,
        accepted_conditional) según su fecha de aceptación dentro del periodo
        del plan: el ciclo de la suscripción para un plan pagado en línea (se
        renueva el mismo día cada mes) o el mes calendario de República
        Dominicana para un plan activado a mano. Los que van en camino a la DGII
        lo reservan mientras se resuelven. Rechazados, cancelados y preflight no
        lo consumen. En un Partner el cupo suma todas sus Cuentas fiscales.
        `details`: plan, limit, used, accepted, reserved, billingUrl,
        periodStart, periodEnd y periodBasis (subscription o calendar_month).
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  headers:
    RetryAfter:
      description: Segundos que el cliente debe esperar antes de reintentar (mínimo 1).
      schema: { type: integer, minimum: 1 }
    PreferenceApplied:
      description: Preferencia aplicada (RFC 7240), por ejemplo `wait=10`; con un N mayor que 15, `wait=15`.
      schema: { type: string, example: wait=10 }
    RateLimitLimit:
      description: Máximo de solicitudes de la ventana que se agotó.
      schema: { type: integer }
    RateLimitRemaining:
      description: Solicitudes restantes en la ventana.
      schema: { type: integer, minimum: 0 }
    RateLimitReset:
      description: Instante Unix (segundos) en que se reinicia la ventana.
      schema: { type: integer }
  schemas:
    HealthResponse:
      type: object
      required: [ok, service, status, checkedAt]
      properties:
        ok:
          type: boolean
        service:
          type: string
          const: psfe-api
        status:
          type: string
          enum: [alive]
        checkedAt:
          type: string
          format: date-time
    ReadinessResponse:
      type: object
      required: [ok, service, status, checks]
      properties:
        ok:
          type: boolean
        service:
          type: string
        status:
          type: string
          enum: [ready, not_ready]
        checks:
          type: array
          items:
            type: object
            required: [status, name, message]
            properties:
              status:
                type: string
                enum: [pass, warn, fail]
              name:
                type: string
              message:
                type: string
              details:
                type: object
                additionalProperties: true
    EcfSubmitRequest:
      type: object
      additionalProperties: true
      description: |
        JSON DGII/XSD con raiz `ECF`, o contrato simplificado soportado por
        compatibilidad. Para emitir debe incluir el eNCF asignado por el POS;
        solo el preflight `validate=true` puede omitirlo.

        `FechaVencimientoSecuencia` (JSON DGII, `DD-MM-YYYY`) o
        `sequenceExpirationDate` (contrato simplificado) es obligatoria para
        E31, E33, E41, E43, E44, E45, E46 y E47 y no aplica a E32/E34.

        En el contrato simplificado solo se admiten las propiedades listadas aqui
        (mas `idempotencyKey`); cualquier otra responde `422` con
        `details.code = UNKNOWN_FIELD`. Un campo que el XSD del tipo no tiene
        (por ejemplo `paymentTerms` en E34/E43 o descuentos en E43/E47) tambien
        responde `422`.

        El contrato simplificado no asume datos fiscales; si faltan responde `422`
        con el campo: `issueDate` (todos los tipos), `paymentType` (todos los tipos
        tienen TipoPago), `incomeType` (E31, E32, E33, E34, E44, E45 y E46), por
        linea `itbisRate` o `indicadorFacturacion` (salvo E43, E44, E46 y E47, cuyo
        indicador fija la DGII) e `itemKind` (salvo E47, siempre servicio),
        `issuer.address` (del perfil de la cuenta o del payload; nunca "N/A") y
        `reference.reason` en E33/E34.

        Los nodos opcionales del XSD (`purchaseOrderNumber`, `purchaseOrderDate`,
        `internalInvoiceNumber`, `internalOrderNumber`, `sellerCode`, `buyer.contact`,
        `buyer.email`, `buyer.internalCode`, `delivery`, `transport` y
        `lines[].description`) se validan con el largo y el patron del XSD del tipo;
        si el XSD del tipo no tiene el nodo responde `422` en el campo.
      properties:
        type:
          type: string
          description: Tipo e-CF del contrato simplificado (`E31`..`E47`).
        encf:
          type: string
          pattern: '^E\d{12}$'
          description: eNCF asignado por el POS/ERP (E + tipo de 2 digitos + secuencia de 10).
        issueDate:
          type: string
          description: Fecha de emision (fecha o fecha-hora ISO, o `DD-MM-AAAA`). Obligatoria en el contrato simplificado.
        issuer:
          type: object
          description: >-
            Emisor. Lo que falte se completa con el perfil de la cuenta; el RNC debe ser el
            de la cuenta. La direccion es obligatoria: si el perfil no la tiene, `address`
            debe venir aqui (si no, `422` en `issuer.address`).
          properties:
            rnc: { type: string }
            legalName: { type: string, maxLength: 150 }
            commercialName: { type: string, maxLength: 150 }
            branch: { type: string, maxLength: 20, description: 'Sucursal. Solo va en el e-CF si se envia; no hay valor por omision.' }
            address: { type: string, maxLength: 100 }
            email: { type: string, maxLength: 80 }
            phone:
              oneOf:
                - { type: string, pattern: '^\d{3}-\d{3}-\d{4}$' }
                - { type: array, maxItems: 3, items: { type: string, pattern: '^\d{3}-\d{3}-\d{4}$' } }
              description: >-
                Telefonos del emisor (`TablaTelefonoEmisor`, hasta 3, formato
                `809-555-1234`). Otro formato responde `422` (`details.code =
                ISSUER_PHONE_INVALID`). El PDF no los imprime.
            province: { type: string, description: Codigo de provincia de la Tabla III DGII. }
            municipality: { type: string, description: Codigo de municipio de la Tabla III DGII. }
        buyer:
          type: object
          description: |
            Comprador. `rnc` admite guiones y espacios y se envia solo con digitos
            (9 u 11); solo se valida la estructura. Con `foreignId` se omite el RNC;
            `foreignId` no existe en E31/E41/E45. E43 no lleva comprador. Una E32 de
            RD$250,000 o mas, y las notas que la modifican, requieren `rnc` o
            `foreignId`.
          properties:
            rnc: { type: string }
            foreignId: { type: string, maxLength: 20 }
            name: { type: string, maxLength: 150 }
            address: { type: string, maxLength: 100 }
            contact:
              type: string
              maxLength: 80
              description: ContactoComprador. No existe en E43/E47.
            email:
              type: string
              maxLength: 80
              description: CorreoComprador (CorreoValidationType del XSD; sin `_`). No existe en E43/E47.
            internalCode:
              type: string
              maxLength: 20
              description: CodigoInternoComprador. No existe en E43/E47.
        paymentType:
          type: string
          enum: [cash, card, credit, free]
          description: |
            `cash` contado (TipoPago 1); `card` contado con tarjeta (TipoPago 1 y
            FormaPago 3); `credit` credito (TipoPago 2); `free` entrega gratuita
            (TipoPago 3). Obligatorio en el contrato simplificado.
        dueDate:
          type: string
          description: FechaLimitePago. Solo con `credit`, no anterior a `issueDate`; no existe en E43.
        paymentTerms:
          type: string
          maxLength: 15
          description: TerminoPago (letras, numeros o espacios). No existe en E34/E43.
        incomeType:
          type: string
          enum: ['01', '02', '03', '04', '05', '06']
          description: TipoIngresos. Obligatorio en E31, E32, E33, E34, E44, E45 y E46; no existe en E41/E43/E47.
        deferredSend:
          type: boolean
          description: |
            `true` solo si la DGII autorizo el envio diferido (Decreto 587-24 art.
            39): emite `IndicadorEnvioDiferido` 1 y el PDF lleva la leyenda. Si la
            cuenta tiene la autorizacion registrada se asume `true`; `false` lo
            quita. Sin la autorizacion registrada responde `422` (`details.code =
            DEFERRED_SEND_NOT_AUTHORIZED`), tambien con `IndicadorEnvioDiferido` 1
            en JSON DGII. No existe en E41/E43/E47.
        currency:
          type: string
          enum: [DOP, BRL, CAD, CHF, CHY, XDR, DKK, EUR, GBP, JPY, NOK, SCP, SEK, USD, VEF, HTG, MXN, COP]
          description: |
            Moneda de los montos (Tabla II DGII). Distinta de DOP: los montos se
            convierten a pesos con `exchangeRate` y el e-CF lleva `OtraMoneda` y
            `OtraMonedaDetalle`.
        exchangeRate:
          type: number
          exclusiveMinimum: 0
          maximum: 999.9999
          description: Pesos por unidad de `currency`, hasta 4 decimales. Obligatorio con `currency` distinta de DOP.
        zeroTaxMode:
          type: string
          enum: [exempt, taxedZero]
          description: Con `itbisRate` 0, exento (indicador 4) o gravado a 0 % (indicador 3).
        documentTaxTotal:
          type: number
          description: ITBIS total que declara el documento. No sustituye `itbisRate` ni `indicadorFacturacion` en las lineas.
        globalDiscountAmount:
          type: number
          description: Descuento global en monto. No se combina con `globalDiscountPercent`; no existe en E43/E47.
        globalDiscountPercent:
          type: number
          minimum: 0
          maximum: 100
          description: Descuento global en porcentaje (2 decimales). Con varias tasas se declara en `%`.
        globalSurchargeAmount:
          type: number
          minimum: 0
          description: |
            Recargo global en monto (DescuentosORecargos con `TipoAjuste` R): se
            suma a la base de cada tasa. No se combina con
            `globalSurchargePercent`; no existe en E43/E47. Con
            `pricesIncludeTax` responde `422`
            (`GLOBAL_AMOUNT_WITH_PRICES_INCLUDE_TAX`): usa el porcentaje.
        globalSurchargePercent:
          type: number
          minimum: 0
          maximum: 100
          description: Recargo global en porcentaje de la suma de las lineas (2 decimales). Con varias tasas se declara en `%`.
        pricesIncludeTax:
          type: boolean
          description: |
            `true` si los precios y montos de las lineas incluyen el ITBIS: emite
            `IndicadorMontoGravado` 1 y calcula como la DGII: base de cada tasa =
            suma de sus `MontoItem` / (1 + tasa), redondeo medio-arriba a 2
            decimales; ITBIS = base x tasa; `MontoTotal` = bases + exento + ITBIS
            (puede diferir un centavo de la suma de los precios). Descuento y
            recargo globales solo en porcentaje. Sin el campo, o `false`, los
            precios no incluyen el ITBIS (`IndicadorMontoGravado` 0). Solo existe en
            E31, E32, E33, E34, E41 y E45.
        purchaseOrderNumber:
          type: string
          maxLength: 20
          description: Comprador/NumeroOrdenCompra. No existe en E41/E43/E47.
        purchaseOrderDate:
          type: string
          pattern: '^(\d{1,2}-\d{1,2}-\d{4}|\d{4}-\d{2}-\d{2})$'
          description: Comprador/FechaOrdenCompra (`DD-MM-YYYY` o `YYYY-MM-DD`, fecha real). No existe en E41/E43/E47.
        internalInvoiceNumber:
          type: string
          maxLength: 20
          description: Emisor/NumeroFacturaInterna.
        internalOrderNumber:
          type: string
          pattern: '^\d{1,20}$'
          description: Emisor/NumeroPedidoInterno (solo digitos).
        sellerCode:
          type: string
          maxLength: 60
          description: Emisor/CodigoVendedor. No existe en E41/E43/E47.
        delivery:
          type: object
          additionalProperties: false
          description: Datos de entrega del comprador. No existen en E41/E43/E47.
          properties:
            date:
              type: string
              pattern: '^(\d{1,2}-\d{1,2}-\d{4}|\d{4}-\d{2}-\d{2})$'
              description: Comprador/FechaEntrega (`DD-MM-YYYY` o `YYYY-MM-DD`, fecha real).
            contact: { type: string, maxLength: 100, description: Comprador/ContactoEntrega. }
            address: { type: string, maxLength: 100, description: Comprador/DireccionEntrega. }
        transport:
          type: object
          additionalProperties: false
          description: Seccion `Transporte` (transporte local). No existe en E41/E43; en E47 solo tiene PaisDestino.
          properties:
            driver: { type: string, maxLength: 20, description: Conductor. }
            document: { type: string, pattern: '^\d{1,20}$', description: DocumentoTransporte (solo digitos). }
            vehicleNumber: { type: string, maxLength: 10, description: Ficha. }
            plate: { type: string, maxLength: 7, description: Placa. }
            route: { type: string, maxLength: 20, description: RutaTransporte. }
            zone: { type: string, maxLength: 20, description: ZonaTransporte. }
            deliveryNoteNumber: { type: string, maxLength: 20, description: NumeroAlbaran. }
        lines:
          type: array
          maxItems: 10000
          description: Lineas de detalle. Maximo segun el XSD del tipo (1000; 10000 en E33/E34).
          items:
            type: object
            required: [name, quantity, unitPrice]
            properties:
              name: { type: string, maxLength: 80 }
              description:
                type: string
                maxLength: 1000
                description: DescripcionItem. El PDF imprime hasta 200 caracteres; el XML la lleva completa.
              quantity: { type: number, exclusiveMinimum: 0, description: Hasta 2 decimales. }
              unitPrice: { type: number, minimum: 0, description: Hasta 4 decimales. Con `pricesIncludeTax` incluye el ITBIS. }
              itbisRate: { type: number, enum: [0, 16, 18], description: 'Obligatorio, o `indicadorFacturacion`, salvo en E43, E44, E46 y E47.' }
              indicadorFacturacion: { type: integer, enum: [1, 2, 3, 4], description: Debe coincidir con `itbisRate` si van los dos. }
              unitMeasure: { type: integer, minimum: 1, maximum: 62, description: Codigo de la Tabla IV DGII. }
              itemKind: { type: string, enum: [good, service], description: Obligatorio salvo en E47 (siempre servicio). }
              discountAmount: { type: number, minimum: 0 }
              discountPercent: { type: number, minimum: 0, maximum: 100 }
              retention:
                type: object
                description: Solo E41/E47. En E47 `isrAmount` es obligatorio en cada linea.
                properties:
                  agentIndicator: { type: integer, enum: [1, 2] }
                  itbisAmount: { type: number, minimum: 0 }
                  isrAmount: { type: number, minimum: 0 }
        reference:
          type: object
          description: >-
            Solo notas E33/E34: `encf`, `date` y `reason` son obligatorios; en E34 tambien
            `modificationCode` (1, 2 o 3). En E33 solo 3 (por omision 3). En otro tipo
            responde 422. `modificationCode` 4 (reemplazo de un comprobante de papel serie B
            emitido en contingencia) responde 422 con `details.code`
            `CONTINGENCY_PAPER_REPLACEMENT_UNSUPPORTED` en cualquier tipo: si la DGII no
            responde, el e-CF se emite igual y queda en contingencia electronica.
          properties:
            encf:
              type: string
              pattern: '^([Bb]\d{10}|[Ee]\d{12}|[AaPp]\d{18})$'
              description: NCFModificado, serie B de 11 posiciones, eNCF de 13 o serie A o P de 19.
            date: { type: string }
            modificationCode: { type: integer, enum: [1, 2, 3] }
            reason: { type: string, maxLength: 90 }
        sequenceExpirationDate:
          type: string
          pattern: '^(\d{1,2}-\d{1,2}-\d{4}|\d{4}-\d{2}-\d{2})$'
          description: |
            Contrato simplificado: fecha de vencimiento del rango de eNCF autorizado
            por DGII (`DD-MM-YYYY` o `YYYY-MM-DD`, fecha real). Obligatoria para
            E31, E33, E41, E43, E44, E45, E46 y E47. No existe en E32/E34: si se
            envia, se omite con un aviso (`SEQUENCE_EXPIRATION_DATE_NOT_APPLICABLE`). ZarelaFact no
            usa valor por omision: si falta o es invalida responde `422`. Tambien `422` si es
            anterior a `issueDate` (`SEQUENCE_EXPIRED_AT_ISSUE_DATE`) o distinta del vencimiento
            del rango registrado que contiene el eNCF (`SEQUENCE_EXPIRATION_RANGE_MISMATCH`).
          examples: ['31-12-2028']
      allOf:
        - if:
            required: [type]
            properties:
              type:
                enum: [E31, E33, E41, E43, E44, E45, E46, E47]
          then:
            required: [sequenceExpirationDate]
        - if:
            required: [type]
          then:
            required: [issueDate, paymentType, lines]
        - if:
            required: [type]
            properties:
              type:
                enum: [E31, E32, E33, E34, E44, E45, E46]
          then:
            required: [incomeType]
        - if:
            required: [type]
            properties:
              type:
                not:
                  enum: [E43, E44, E46, E47]
          then:
            properties:
              lines:
                items:
                  anyOf:
                    - required: [itbisRate]
                    - required: [indicadorFacturacion]
        - if:
            required: [type]
            properties:
              type:
                not:
                  const: E47
          then:
            properties:
              lines:
                items:
                  required: [itemKind]
        - if:
            required: [type]
            properties:
              type:
                enum: [E33, E34]
          then:
            required: [reference]
            properties:
              reference:
                required: [encf, date, reason]
        - if:
            required: [type]
            properties:
              type:
                const: E34
          then:
            properties:
              reference:
                required: [modificationCode]
    SubmitResponse:
      type: object
      required: [ok, correlationId]
      properties:
        ok:
          type: boolean
        correlationId:
          type: string
        mode:
          type: string
          enum: [preflight, queued, idempotent, submit_validation]
        errors:
          type: array
          description: Errores de validacion (`field`, `message`, `severity`, `details`); presente cuando `ok=false`.
          items:
            type: object
            additionalProperties: true
            properties:
              field: { type: string }
              message: { type: string }
              severity: { type: string }
        warnings:
          type: array
          items:
            type: object
            additionalProperties: true
        result:
          type: object
          additionalProperties: true
          properties:
            documentId:
              type: string
            encf:
              type: string
            status:
              type: string
            asyncStatusRequired:
              type: boolean
            securityCode:
              type: [string, 'null']
              description: Null en el `202`; en el `201` de `Prefer` con `wait=N`, el código de seguridad.
            qrUrl:
              type: [string, 'null']
              description: Null en el `202`; en el `201` de `Prefer` con `wait=N`, la URL del QR.
            printing:
              $ref: '#/components/schemas/DocumentPrinting'
            printingStatus:
              $ref: '#/components/schemas/PrintingStatus'
            dgiiMessages:
              $ref: '#/components/schemas/DgiiMessages'
            lastError:
              $ref: '#/components/schemas/DocumentLastError'
            replacesDocumentId:
              type: string
              description: >-
                Solo al reenviar con el mismo eNCF la version corregida de un e-CF que la
                DGII rechazo con `secuenciaUtilizada: false` (eCF y TesteCF): el
                `documentId` del rechazado, que se conserva intacto como evidencia. Va con
                el aviso `ENCF_REISSUED_AFTER_REJECTION`.
        preflight:
          type: object
          additionalProperties: true
    StatusBatchRequest:
      type: object
      properties:
        documentIds:
          type: array
          maxItems: 100
          items:
            type: string
        encfs:
          type: array
          maxItems: 100
          items:
            type: string
      anyOf:
        - required: [documentIds]
        - required: [encfs]
    StatusBatchResponse:
      type: object
      required: [ok, environment, correlationId, count, results]
      properties:
        ok:
          type: boolean
        environment:
          type: string
        correlationId:
          type: string
        count:
          type: integer
        results:
          type: array
          items:
            type: object
            additionalProperties: true
            properties:
              documentId: { type: [string, 'null'] }
              encf: { type: [string, 'null'] }
              status:
                type: string
                description: >-
                  Estado en ZarelaFact (`queued_for_signing`, `signed`,
                  `rfce_pending_dispatch`, `sent`, `pending_dgii_status`, `accepted`,
                  `accepted_conditional`, `rejected`, `contingency`, `transmission_failed`,
                  `cancelled`) o `not_found`. `transmission_failed`: el envío a la DGII falló
                  de forma definitiva (o no hay certificado activo) y no se reintenta solo;
                  no es final.
              dgiiStatus: { type: [string, 'null'] }
              trackId: { type: [string, 'null'] }
              lastCheckedAt: { type: [string, 'null'] }
              printing:
                $ref: '#/components/schemas/DocumentPrinting'
              printingStatus:
                $ref: '#/components/schemas/PrintingStatus'
              dgiiMessages:
                $ref: '#/components/schemas/DgiiMessages'
              lastError:
                $ref: '#/components/schemas/DocumentLastError'
              receiverDelivery:
                $ref: '#/components/schemas/ReceiverDelivery'
              errors:
                type: array
                description: >-
                  `NOT_FOUND` si el documento no existe en el ambiente; si no, el mismo
                  error de `lastError` (`code` y `message`). Vacío cuando no hay error.
                items:
                  type: object
                  required: [code]
                  properties:
                    code: { type: string }
                    message: { type: [string, 'null'] }
    ReceiverDelivery:
      type: [object, 'null']
      description: >-
        Entrega del e-CF al receptor electrónico (B2B), después de que la DGII lo acepta.
        Null si el documento no tuvo entrega (todavía o nunca). `state`: `submitting`
        (enviándose), `retry_scheduled` (falló por una causa pasajera; se reintenta solo en
        `nextAttemptAt`, hasta 5 intentos), `sent` (el receptor lo recibió), `failed`
        (falló de forma definitiva o se agotaron los intentos), `uncertain` (el receptor no
        respondió; se concilia con `POST /{environment}/documentos-ecf/{documentId}/entrega-receptor/reconciliar`),
        `dispatch_failed` (conciliado como no entregado), `not_electronic` (el comprador no
        es receptor electrónico: se le entrega la representación impresa) o `not_applicable`.
      required: [state, attempts, lastAttemptAt, nextAttemptAt, sentAt, reason, lastError]
      properties:
        state:
          type: string
          enum: [submitting, retry_scheduled, sent, failed, uncertain, dispatch_failed, not_electronic, not_applicable]
        attempts: { type: integer, minimum: 0 }
        lastAttemptAt: { type: [string, 'null'], format: date-time }
        nextAttemptAt: { type: [string, 'null'], format: date-time }
        sentAt: { type: [string, 'null'], format: date-time }
        reason:
          type: [string, 'null']
          description: Por qué no aplica (solo en `not_electronic` y `not_applicable`).
        lastError:
          type: [object, 'null']
          description: El último fallo (en `retry_scheduled`, `failed`, `uncertain` y `dispatch_failed`).
          properties:
            code: { type: [string, 'null'] }
            message: { type: [string, 'null'] }
            statusCode: { type: [integer, 'null'] }
            codigoMotivoNoRecibido:
              type: [integer, string, 'null']
              description: Motivo del ARECF «no recibido» del receptor, si lo hubo.
    DocumentLastError:
      type: [object, 'null']
      description: >-
        Por qué el documento no avanza: el último fallo de envío a la DGII o, en
        contingencia, la caída de la DGII (`DGII_UNAVAILABLE`). Null sin error, con estado
        final (`accepted`, `accepted_conditional`, `rejected`) o `cancelled`. Se borra
        cuando un envío posterior sale bien. Lo traen la consulta por lote y el webhook
        `document.state_changed`. En `transmission_failed` (el envío falló de forma
        definitiva o no hay certificado activo) trae `retrying: false`.
      required: [code]
      properties:
        code:
          type: string
          description: >-
            Código estable, por ejemplo `DGII_CERTIFICATE_NOT_DELEGATED` (el certificado
            no está delegado para el RNC emisor; los envíos de la cuenta quedan en pausa y
            se reintentan cada 30 min), `DGII_RECEPTION_REJECTED` (la DGII respondió que
            no recibió el documento, con su motivo en `dgiiMessages`),
            `DGII_RFCE_DUPLICATE_UNRESOLVED`, `XSD_VALIDATION_FAILED`,
            `CERTIFICATE_NOT_CONFIGURED` o `CERTIFICATE_EXPIRED` (sin certificado activo o
            vigente: `transmission_failed`) o `DGII_UNAVAILABLE` (contingencia; una
            autenticación DGII caída, `DGII_AUTH_UNAVAILABLE`, cuenta como caída).
        message: { type: [string, 'null'] }
        dgiiMessages:
          type: array
          items: { type: string }
        hint:
          type: string
          description: >-
            Qué hacer, cuando aplica. Con `DGII_CERTIFICATE_NOT_DELEGATED`: el
            administrador del contribuyente debe delegar el rol «Firmante Autorizado» al
            titular del certificado en la Oficina Virtual de la DGII («Delegación de
            e-CF» → «Delegación de Roles»).
        retrying:
          type: boolean
          description: >-
            Si ZarelaFact lo sigue intentando solo. `false` solo en `transmission_failed`:
            la DGII no tiene el documento y hace falta corregir la causa y reenviarlo (el
            mismo XML firmado, sin firmar otra vez) o anular su eNCF con una ANECF.
        at:
          type: [string, 'null']
          format: date-time
    ContingencyResponse:
      type: object
      required: [ok, environment, mode, count, documents]
      properties:
        ok:
          type: boolean
        environment:
          type: string
        mode:
          type: string
          const: contingency
        dgiiService:
          type: object
          description: >-
            Si la DGII responde en este ambiente. `degraded` desde que un envío o una
            consulta entró en contingencia (DGII sin respuesta, timeout, 5xx, autenticación
            DGII caída o circuit breaker abierto) hasta la siguiente respuesta de la DGII.
            Cada cambio también llega como webhook `dgii.service_degraded` o
            `dgii.service_restored`.
          required: [state]
          properties:
            state:
              type: string
              enum: [available, degraded]
            since:
              type: [string, 'null']
              format: date-time
            code:
              type: [string, 'null']
              description: Código de la caída, por ejemplo `DGII_TIMEOUT` o `DGII_CIRCUIT_OPEN`.
            dgiiEnvironment:
              type: [string, 'null']
        count:
          type: integer
        documents:
          type: array
          items:
            type: object
            additionalProperties: true
    CreateSequenceRangeRequest:
      type: object
      required: [ecfType, start, end]
      properties:
        ecfType:
          type: string
          example: E31
        start:
          type: integer
        end:
          type: integer
        expiresAt:
          type: string
          description: >-
            Vencimiento del rango autorizado por la DGII: fecha-hora ISO, o solo la
            fecha (`AAAA-MM-DD` o `DD-MM-AAAA`), que vence al final de ese dia en
            hora de RD (23:59:59 GMT-4). Si el rango tiene vencimiento, la emision de
            un eNCF del rango exige que `FechaVencimientoSecuencia` sea ese dia.
          examples: ['2028-12-31', '2028-12-31T23:59:59-04:00']
    SequenceRangeResponse:
      type: object
      required: [ok, range]
      properties:
        ok:
          type: boolean
        range:
          type: object
          additionalProperties: true
    SequenceRangesResponse:
      type: object
      required: [ok, ranges]
      properties:
        ok:
          type: boolean
        ranges:
          type: array
          items:
            type: object
            additionalProperties: true
    AnnulmentRequest:
      type: object
      required: [cancellations]
      properties:
        rncEmisor:
          type: string
        annulmentDate:
          type: string
          format: date-time
        submit:
          type: boolean
          default: true
        cancellations:
          type: array
          minItems: 1
          items:
            type: object
            required: [ecfType, sequences]
            properties:
              ecfType:
                type: string
                example: E31
              sequences:
                type: array
                minItems: 1
                items:
                  type: object
                  required: [from]
                  properties:
                    from:
                      type: string
                      example: E310000000150
                    to:
                      type: string
                      example: E310000000160
    CommercialApprovalRequest:
      type: object
      required: [decision]
      properties:
        decision:
          type: string
          enum: [accept, reject]
        rejectionReason:
          type: string
        approvalDate:
          type: string
          description: >-
            Fecha y hora de la decisión (FechaHoraAprobacionComercial). Sin zona es hora de
            República Dominicana; con `Z` u offset se convierte a hora de RD. Debe estar
            entre la medianoche de la `FechaEmision` del e-CF recibido y la hora actual de
            RD más 5 minutos; fuera de ese rango responde `422 COMMERCIAL_APPROVAL_DATE_INVALID`.
            Si no se envía, es la hora actual de RD.
          examples: ['2026-10-20T15:30:00']
        targetUrl:
          type: string
          format: uri
          description: >-
            URL de aprobación comercial del emisor. Solo se usa si el emisor no está en el
            directorio DGII del ambiente (`DIRECTORY_VENDOR_NOT_FOUND`) o en CerteCF; si
            está, debe ser su URL registrada. No evita la consulta al directorio, aunque
            `autoResolveDirectory` sea `false`.
        dispatch:
          oneOf:
            - type: boolean
            - type: string
              enum: [build-only]
        autoResolveDirectory:
          type: boolean
    ReceivedDocumentView:
      type: object
      description: >-
        Lo mismo que muestra la pantalla Recibidos del portal. Proveedor, tipo, monto y
        fecha de emisión salen del XML recibido.
      required: [kind, documentType, senderName, senderRnc, amount, issueDate, acuse, approval, group]
      properties:
        kind:
          type: string
          enum: [recepcion]
        documentType:
          type: [string, 'null']
          examples: [E31]
        senderName:
          type: [string, 'null']
          description: RazonSocialEmisor.
        senderRnc:
          type: [string, 'null']
        amount:
          type: [string, 'null']
          description: MontoTotal, como viene en el XML.
        issueDate:
          type: [string, 'null']
          format: date
          description: FechaEmision del e-CF.
        acuse:
          type: object
          required: [state]
          properties:
            state:
              type: string
              enum: [recibido, no_recibido]
            motivo:
              type: [integer, 'null']
              description: CodigoMotivoNoRecibido (1 especificación, 2 firma, 3 duplicado, 4 RNC comprador).
            motivoLabel:
              type: [string, 'null']
        approval:
          type: object
          required: [state]
          properties:
            state:
              type: string
              enum: [pendiente, aceptado, rechazado, no_aplica]
            deadline:
              type: [string, 'null']
              format: date
              description: Solo en `pendiente`; igual a `commercialApprovalDeadline`.
            warning:
              type: string
              description: El envío de tu ACECF no se confirmó.
            by:
              type: string
              enum: [ti]
            acecfSent:
              type: boolean
            reason:
              type: [string, 'null']
        group:
          type: [string, 'null']
          enum: [por_aprobar, aceptados, rechazados, no_recibidos, null]
        dgii:
          type: [object, 'null']
          description: >-
            Validez del e-CF en la DGII (Consulta Estado). `null` si el acuse fue «no
            recibido». `no_aplica` en CerteCF (no tiene Consulta Estado).
          required: [state]
          properties:
            state:
              type: string
              enum: [por_consultar, en_proceso, aceptado, aceptado_condicional, rechazado, no_encontrado, no_aplica, no_verificado]
            checkedAt:
              type: [string, 'null']
              format: date-time
            dgiiStatus:
              type: [string, 'null']
            retrying:
              type: boolean
              description: Si ZarelaFact volverá a consultarlo.
            reason:
              type: string
    ReceivedDocumentDgiiValidity:
      type: [object, 'null']
      description: >-
        Validez del e-CF recibido en la DGII según Consulta Estado, consultada por
        ZarelaFact con el token DGII de tu cuenta poco después de recibirlo. `null`
        mientras no se ha consultado. «No encontrado» y una DGII caída se reintentan con
        espera creciente (hasta 6 consultas en ~31 h); después queda final.
      required: [state, final]
      properties:
        state:
          type: string
          enum: [pending, accepted, accepted_conditional, rejected, not_found, not_applicable, unverified]
          description: >-
            `not_applicable`: el ambiente no tiene Consulta Estado (CerteCF).
            `unverified`: se agotaron las consultas sin respuesta concluyente.
        final:
          type: boolean
        dgiiStatus:
          type: [string, 'null']
          description: El `estado` que respondió la DGII.
        checkedAt:
          type: [string, 'null']
          format: date-time
        attempts:
          type: integer
        reason:
          type: string
    ReceivedDocument:
      type: object
      required:
        - inboundDocumentId
        - encf
        - issuerRnc
        - receiverRnc
        - status
        - commercialApproval
        - receivedAt
        - group
        - commercialApprovalDeadline
        - view
      properties:
        inboundDocumentId:
          type: string
          format: uuid
        encf:
          type: [string, 'null']
        issuerRnc:
          type: [string, 'null']
        receiverRnc:
          type: [string, 'null']
        status:
          type: string
          enum: [received, duplicate, rejected]
          description: '`duplicate` o `rejected`: el acuse (ARECF) dijo «no recibido».'
        commercialApproval:
          type: [object, 'null']
          additionalProperties: true
          description: Tu ACECF (estado, decisión, XML y entrega) si ya respondiste.
        dgiiValidity:
          $ref: '#/components/schemas/ReceivedDocumentDgiiValidity'
        receivedAt:
          type: string
          format: date-time
        group:
          type: [string, 'null']
          enum: [por_aprobar, aceptados, rechazados, no_recibidos, null]
        commercialApprovalDeadline:
          type: [string, 'null']
          format: date
          description: >-
            Fecha límite para responder la aprobación comercial: el día 15 del mes
            siguiente a la FechaEmision del e-CF (D587-24 art. 17 párr. V y Formato 606,
            NG 07-18 art. 8). Solo en `por_aprobar`; `null` en los demás grupos o si el
            XML no trae FechaEmision.
          examples: ['2026-11-15']
        view:
          $ref: '#/components/schemas/ReceivedDocumentView'
    ReceivedDocumentsResponse:
      type: object
      required: [ok, environment, items, nextCursor]
      properties:
        ok:
          type: boolean
        environment:
          type: string
        items:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/ReceivedDocument'
        nextCursor:
          type: [string, 'null']
          description: Cursor de la página siguiente; `null` en la última.
        correlationId:
          type: string
    ReceivedDocumentResponse:
      allOf:
        - $ref: '#/components/schemas/ReceivedDocument'
        - type: object
          required: [ok, environment]
          properties:
            ok:
              type: boolean
            environment:
              type: string
            correlationId:
              type: string
    IssuedDocument:
      type: object
      required:
        - documentId
        - encf
        - documentType
        - issueDate
        - buyerRnc
        - buyerName
        - total
        - status
        - dgiiStatus
        - trackId
        - createdAt
        - sentAt
        - acceptedAt
        - rejectedAt
        - printing
        - dgiiMessages
      properties:
        documentId:
          type: string
          format: uuid
        encf:
          type: [string, 'null']
        documentType:
          type: [string, 'null']
          examples: [E31]
        issueDate:
          type: [string, 'null']
          format: date
          description: FechaEmision del e-CF (`AAAA-MM-DD`).
        buyerRnc:
          type: [string, 'null']
          description: >-
            RNCComprador del e-CF. `null` si el comprador no tiene RNC (consumidor
            final, E43) o se identificó con `IdentificadorExtranjero` (E47 o
            comprador extranjero): ese identificador no es un RNC y no va aquí.
        buyerName:
          type: [string, 'null']
        total:
          type: [string, 'null']
          description: MontoTotal del e-CF.
        status:
          type: string
          description: Estado del documento, como en la consulta por lote.
        dgiiStatus:
          type: [string, 'null']
        trackId:
          type: [string, 'null']
        createdAt:
          type: string
          format: date-time
          description: Cuándo lo recibió ZarelaFact.
        sentAt:
          type: [string, 'null']
          format: date-time
        acceptedAt:
          type: [string, 'null']
          format: date-time
        rejectedAt:
          type: [string, 'null']
          format: date-time
        printing:
          $ref: '#/components/schemas/DocumentPrinting'
        printingStatus:
          $ref: '#/components/schemas/PrintingStatus'
        dgiiMessages:
          $ref: '#/components/schemas/DgiiMessages'
    IssuedDocumentsResponse:
      type: object
      required: [ok, environment, items, nextCursor]
      properties:
        ok:
          type: boolean
        environment:
          type: string
        items:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/IssuedDocument'
        nextCursor:
          type: [string, 'null']
          description: Cursor de la página siguiente; `null` en la última.
        correlationId:
          type: string
    ReceivedCommercialApproval:
      type: object
      required:
        - inboundDocumentId
        - encf
        - documentId
        - issuerRnc
        - buyerRnc
        - status
        - decision
        - rejectionReason
        - approvalDateTime
        - issueDate
        - amount
        - notReceived
        - receivedAt
      properties:
        inboundDocumentId:
          type: string
          format: uuid
        encf:
          type: [string, 'null']
          description: e-NCF del e-CF que emitiste.
        documentId:
          type: [string, 'null']
          format: uuid
          description: Tu e-CF emitido con ese e-NCF en el mismo ambiente; `null` si no está entre tus emitidos.
        issuerRnc:
          type: [string, 'null']
          description: RNCEmisor del ACECF (tu RNC).
        buyerRnc:
          type: [string, 'null']
          description: RNCComprador del ACECF (tu cliente, quien aprueba o rechaza).
        status:
          type: string
          enum: [received, duplicate, rejected]
          description: '`duplicate` o `rejected`: el receptor no la recibió (ver `notReceived`).'
        decision:
          type: [string, 'null']
          enum: [accepted, rejected, null]
          description: Estado del ACECF (1 aceptado, 2 rechazado); `null` si no se recibió.
        rejectionReason:
          type: [string, 'null']
          description: DetalleMotivoRechazo, solo con `decision` `rejected`.
        approvalDateTime:
          type: [string, 'null']
          description: FechaHoraAprobacionComercial tal como viene en el ACECF (DD-MM-AAAA HH:mm:ss, hora de RD).
          examples: ['25-10-2026 11:15:00']
        issueDate:
          type: [string, 'null']
          format: date
          description: FechaEmision del e-CF según el ACECF.
        amount:
          type: [string, 'null']
          description: MontoTotal del e-CF según el ACECF.
        correspondence:
          type: [object, 'null']
          description: >-
            Si el ACECF corresponde con el e-CF que emitiste, marcado al recibirlo: `matched`
            (mismo RNC emisor, RNC comprador y monto), `mismatch` (`mismatches` dice qué campo
            difiere: valor recibido y el de tu e-CF) o `ecf_not_found` (no emitiste ese e-NCF en
            el ambiente). `null` si no se recibió o es anterior a esta marca. Llega igual en el
            webhook `document.commercial_approval_received` (`data.correspondence`).
          required: [status, documentId, mismatches]
          properties:
            status:
              type: string
              enum: [matched, mismatch, ecf_not_found]
            documentId:
              type: [string, 'null']
            mismatches:
              type: array
              items:
                type: object
                required: [field, received, expected]
                properties:
                  field: { type: string, enum: [RNCEmisor, RNCComprador, MontoTotal] }
                  received: { type: [string, 'null'] }
                  expected: { type: [string, 'null'] }
        notReceived:
          type: [object, 'null']
          description: Motivo por el que el receptor no la recibió (1 especificación, 2 firma, 3 duplicado, 4 RNC comprador).
          properties:
            motivo:
              type: [integer, 'null']
            motivoLabel:
              type: [string, 'null']
        receivedAt:
          type: string
          format: date-time
    ReceivedCommercialApprovalsResponse:
      type: object
      required: [ok, environment, items, nextCursor]
      properties:
        ok:
          type: boolean
        environment:
          type: string
        items:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/ReceivedCommercialApproval'
        nextCursor:
          type: [string, 'null']
          description: Cursor de la página siguiente; `null` en la última.
        correlationId:
          type: string
    ReceivedCommercialApprovalResponse:
      allOf:
        - $ref: '#/components/schemas/ReceivedCommercialApproval'
        - type: object
          required: [ok, environment]
          properties:
            ok:
              type: boolean
            environment:
              type: string
            correlationId:
              type: string
    PartnerReceivedDocumentsResponse:
      type: object
      required: [fiscalAccountId, items, nextCursor]
      properties:
        fiscalAccountId:
          type: string
          format: uuid
        items:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/ReceivedDocument'
        nextCursor:
          type: [string, 'null']
          description: Cursor de la página siguiente; `null` en la última.
        correlationId:
          type: string
    CommercialApprovalStatusResponse:
      type: object
      required: [ok, inboundId, encf, issuerRnc, receiverRnc]
      properties:
        ok:
          type: boolean
        inboundId:
          type: string
        encf:
          type: string
        issuerRnc:
          type: string
        receiverRnc:
          type: string
        commercialApproval:
          type:
            - object
            - 'null'
          additionalProperties: true
    CommercialApprovalSubmitResponse:
      type: object
      required: [ok, inboundId, encf, commercialApproval]
      properties:
        ok:
          type: boolean
        inboundId:
          type: string
        encf:
          type: string
        commercialApproval:
          type: object
          additionalProperties: true
    FiscalReconciliationRequest:
      type: object
      additionalProperties: false
      required: [outcome, evidence]
      properties:
        outcome:
          type: string
          description: Resultado confirmado externamente; el conjunto válido depende del recurso.
          enum: [accepted, rejected, sent, dispatch_failed]
        evidence:
          $ref: '#/components/schemas/FiscalReconciliationEvidence'
    FiscalReconciliationEvidence:
      type: object
      additionalProperties: false
      required: [source, reference, observedAt]
      properties:
        source:
          type: string
          maxLength: 80
          example: dgii-status-query
        reference:
          type: string
          maxLength: 256
          example: track-or-ticket-reference
        observedAt:
          type: string
          format: date-time
        arecfXml:
          type: string
          description: ARECF firmado obligatorio cuando una entrega al receptor se reconcilia como `sent`; se valida contra ARECF v1.0 y la identidad del documento.
        details:
          type: object
          additionalProperties: true
    ReconciliationResponse:
      type: object
      required: [ok, idempotent]
      properties:
        ok:
          type: boolean
          const: true
        idempotent:
          type: boolean
        annulment:
          type: object
          additionalProperties: true
        commercialApproval:
          type: object
          additionalProperties: true
    CertificationSetRequest:
      type: object
      description: |
        Set a enviar. Las pruebas de datos usan el Excel que la DGII le entregó al
        contribuyente: `workbookBase64` (tal cual), `rows` o `documents`. La simulación
        usa `dgiiModel: true`. No combine los dos.
      properties:
        dgiiModel:
          type: boolean
          description: >-
            `true` envía la simulación: el set modelo de ZarelaFact (25 e-CF de todos los
            tipos y 4 resúmenes de facturas de consumo) a nombre del RNC de la cuenta, con
            un bloque de e-NCF nuevos (`summary.mode: simulation`). Solo después de que la
            DGII acepte las aprobaciones comerciales; antes responde 409
            CERTIFICATION_OWN_SET_REQUIRED, porque la DGII compara las pruebas de datos con
            el set que le entregó al contribuyente. Requiere un certificado activo.
        documents:
          type: array
          items:
            type: object
            additionalProperties: true
        rows:
          type: array
          items:
            type: object
            additionalProperties: true
        workbookBase64:
          type: string
          contentEncoding: base64
        b2bSimulation:
          type: boolean
          description: >-
            Simula la aprobación comercial (ACECF) al terminar los lotes. Por omisión es
            `true` en las pruebas de datos y `false` con `dgiiModel`.
      anyOf:
        - required: [dgiiModel]
        - required: [documents]
        - required: [rows]
        - required: [workbookBase64]
    CertificationRunResponse:
      type: object
      required: [ok, environment, mode, run]
      properties:
        ok:
          type: boolean
          const: true
        environment:
          type: string
        mode:
          type: string
          const: certification_run_started
        run:
          type: object
          required: [runId, jobId, documentIds, batches]
          additionalProperties: true
        correlationId:
          type: string
    CertificationStatusResponse:
      type: object
      required: [ok, environment, status]
      properties:
        ok:
          type: boolean
          const: true
        environment:
          type: string
        status:
          type: object
          required: [runId, jobStatus, runStatus]
          additionalProperties: true
        correlationId:
          type: string
    CertificationPartSummary:
      type: object
      required: [documentCount, accepted, rejected, inFlight, completed]
      properties:
        documentCount:
          type: integer
          description: Comprobantes activos de la parte (sin los reemplazados ni los cancelados).
        accepted: { type: integer }
        rejected: { type: integer }
        inFlight: { type: integer }
        ecf:
          description: e-CF de la parte (sin los resúmenes), como el contador «e-CF» de CerteCF.
          type: object
          properties:
            total: { type: integer }
            accepted: { type: integer }
        summaries:
          description: Resúmenes de facturas de consumo (RFCE), como el contador «RFCE» de CerteCF.
          type: object
          properties:
            total: { type: integer }
            accepted: { type: integer }
        completed:
          type: boolean
          description: true si un run de esa parte tiene todos sus comprobantes aceptados.
    CertificationParts:
      type: object
      required: [data, simulation]
      properties:
        data: { $ref: '#/components/schemas/CertificationPartSummary' }
        simulation: { $ref: '#/components/schemas/CertificationPartSummary' }
    CertificationRestart:
      type: object
      required: [part, supersededDocumentCount, cancelledRunIds]
      properties:
        part: { type: string, enum: [data, simulation] }
        supersededDocumentCount:
          type: integer
          description: Comprobantes que quedaron reemplazados.
        cancelledRunIds:
          type: array
          items: { type: string, format: uuid }
    CertificationRestartResponse:
      type: object
      required: [ok, environment, restart, parts]
      properties:
        ok:
          type: boolean
          const: true
        environment:
          type: string
        restart: { $ref: '#/components/schemas/CertificationRestart' }
        parts: { $ref: '#/components/schemas/CertificationParts' }
        correlationId:
          type: string
    CertificationResend:
      type: object
      required: [documentId, encf, status, jobType, jobId]
      properties:
        documentId: { type: string, format: uuid }
        encf: { type: string }
        status:
          type: string
          description: Estado del comprobante al pedir el reenvío.
        jobType:
          type: string
          enum: [ecf.send, ecf.status.poll]
          description: '`ecf.status.poll` si la DGII ya le dio TrackID.'
        jobId: { type: [string, 'null'], format: uuid }
    CertificationResendResponse:
      type: object
      required: [ok, environment, resend]
      properties:
        ok:
          type: boolean
          const: true
        environment:
          type: string
        resend: { $ref: '#/components/schemas/CertificationResend' }
        correlationId:
          type: string
    CertificationEvidenceResponse:
      type: object
      required: [ok, runId, fileName, contentType, encoding, byteLength, content]
      properties:
        ok:
          type: boolean
          const: true
        runId:
          type: string
        fileName:
          type: string
        contentType:
          type: string
          const: application/zip
        encoding:
          type: string
          const: base64
        byteLength:
          type: integer
        fileCount:
          type: integer
        sha256:
          type: string
        content:
          type: string
          contentEncoding: base64
    PartnerCertificationEvidenceResponse:
      type: object
      required: [runId, fileName, contentType, encoding, byteLength, fileCount, sha256, content]
      properties:
        runId: { type: string }
        fileName: { type: string }
        contentType: { type: string, const: application/zip }
        encoding: { type: string, const: base64 }
        byteLength: { type: integer, minimum: 0 }
        fileCount: { type: integer, minimum: 0 }
        sha256: { type: string }
        content: { type: string, contentEncoding: base64 }
        correlationId: { type: string }
    PartnerDocumentStatus:
      type: object
      required: [documentId, encf, documentType, status, dgiiStatus, trackId, correlationId, createdAt, updatedAt]
      properties:
        documentId: { type: string, format: uuid }
        environment:
          type: string
          enum: [ecf, testecf]
          description: Ambiente del documento. `testecf` es el sandbox de la Cuenta fiscal.
        encf: { type: string }
        documentType: { type: string }
        status: { type: string }
        dgiiStatus: { type: [string, 'null'] }
        trackId: { type: [string, 'null'] }
        correlationId: { type: [string, 'null'] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        lastError:
          $ref: '#/components/schemas/DocumentLastError'
        printing:
          $ref: '#/components/schemas/DocumentPrinting'
        printingStatus:
          $ref: '#/components/schemas/PrintingStatus'
        dgiiMessages:
          $ref: '#/components/schemas/DgiiMessages'
        receiverDelivery:
          $ref: '#/components/schemas/ReceiverDelivery'
    PrintingStatus:
      type: string
      enum: [awaiting_signature, awaiting_dgii, available, rejected]
      description: >-
        En qué punto está la representación impresa. `awaiting_signature`: sin firmar.
        `awaiting_dgii`: firmado, esperando la respuesta de la DGII (también en
        `transmission_failed`, hasta que un reenvío llegue a la DGII). `available`: `printing`
        trae los datos. `rejected`: la DGII rechazó el resumen RFCE (o el e-CF sin TrackID)
        y la representación impresa no se entrega.
    DocumentPrinting:
      type: [object, 'null']
      description: >-
        Datos para imprimir la representación impresa. Es null hasta que la DGII responde
        (Informe Técnico e-CF v1.0, secciones 8 y 9.1): el TrackID del e-CF o, en una
        factura de consumo menor a RD$250,000, la respuesta Aceptado o Aceptado Condicional
        del resumen RFCE. En contingencia o envío diferido autorizado aparece desde la firma,
        para imprimir con su leyenda.
      required: [securityCode, signatureDate, qrUrl]
      properties:
        securityCode:
          type: string
          description: Código de seguridad de 6 caracteres derivado de la firma.
        signatureDate:
          type: [string, 'null']
          description: Fecha y hora de la firma (FechaHoraFirma, DD-MM-YYYY HH:mm:ss).
        qrUrl:
          type: [string, 'null']
          format: uri
          description: URL de verificación de la DGII que se imprime como código QR, sin modificarla.
        pdfUrl:
          type: [string, 'null']
          format: uri
          description: >-
            Solo en los webhooks `document.state_changed`: la descarga del PDF por API
            (`GET /{ambiente}/documentos-ecf/{documentId}/pdf` con X-API-KEY, o la ruta
            `.../documents/{documentId}/pdf` de la API Partner con X-PARTNER-KEY). Null en
            CerteCF por la API Partner.
    DgiiMessages:
      type: array
      description: Mensajes de la respuesta final de la DGII (motivos de rechazo u observaciones). Vacío mientras no hay respuesta final.
      items: { type: string }
    PartnerProvisionRequest:
      type: object
      required: [rnc, legalName]
      additionalProperties: false
      properties:
        rnc:
          type: string
          pattern: '^[0-9]{9,11}$'
        legalName:
          type: string
          maxLength: 300
        commercialName:
          type: string
          maxLength: 300
        fiscalEmail:
          type: string
          format: email
          maxLength: 254
        phone:
          type: string
          maxLength: 50
        address:
          type: string
          maxLength: 500
        provinceCode:
          type: string
          maxLength: 20
        municipalityCode:
          type: string
          maxLength: 20
        ecfTypes:
          type: array
          maxItems: 50
          items: { type: string, maxLength: 20 }
    PartnerProfilePatch:
      type: object
      minProperties: 1
      additionalProperties: false
      properties:
        legalName: { type: string, maxLength: 300 }
        commercialName: { type: string, maxLength: 300 }
        fiscalEmail: { type: string, format: email, maxLength: 254 }
        phone: { type: string, maxLength: 50 }
        address: { type: string, maxLength: 500 }
        provinceCode: { type: string, maxLength: 20 }
        municipalityCode: { type: string, maxLength: 20 }
        ecfTypes: { type: array, maxItems: 50, items: { type: string, maxLength: 20 } }
        printedRepresentationEmail:
          type: object
          minProperties: 1
          additionalProperties: false
          description: >-
            Activa (`true`) o desactiva (`false`) por ambiente el envío de la representación
            impresa al comprador por correo. Está desactivado por defecto. Al activarlo aplica
            a los e-CF emitidos desde ese momento; el correo sale cuando la DGII acepta el e-CF
            (Aceptado o Aceptado Condicional) y el XML trae `CorreoComprador`. Solo en el perfil:
            el alta de la Cuenta fiscal no lo acepta.
          properties:
            production: { type: boolean }
            testecf: { type: boolean }
    PartnerAccountResponse:
      type: object
      required: [fiscalAccountId, externalTenantId, status, environments, certification]
      properties:
        fiscalAccountId: { type: string, format: uuid }
        externalTenantId: { type: string }
        rnc: { type: string }
        legalName: { type: string }
        commercialName: { type: string }
        status: { type: string }
        ownership:
          type: object
          description: >-
            Titularidad del vínculo. Se verifica al instalar un certificado válido del RNC.
            Mientras no esté verificada, otro Partner puede reclamar el RNC después de claimExpiresAt.
          required: [verified, verifiedAt, claimExpiresAt]
          properties:
            verified: { type: boolean }
            verifiedAt: { type: [string, 'null'], format: date-time }
            claimExpiresAt: { type: [string, 'null'], format: date-time }
        environments:
          type: object
          properties:
            certecf: { type: string, enum: [active, inactive] }
            ecf: { type: string, enum: [active, inactive] }
        printedRepresentationEmail:
          type: object
          description: Envío de la representación impresa al comprador por correo, por ambiente.
          properties:
            production: { $ref: '#/components/schemas/PrintedRepresentationEmailSetting' }
            testecf: { $ref: '#/components/schemas/PrintedRepresentationEmailSetting' }
        certification:
          anyOf:
            - $ref: '#/components/schemas/PartnerCertificationSummary'
            - $ref: '#/components/schemas/PartnerCertificationCase'
        correlationId: { type: string }
    PrintedRepresentationEmailSetting:
      type: object
      required: [enabled, enabledAt]
      properties:
        enabled: { type: boolean }
        enabledAt:
          type: [string, 'null']
          format: date-time
          description: Desde cuándo está activo; se envían los e-CF emitidos a partir de ese momento.

    PartnerCertificationSummary:
      type: object
      description: Estado inicial antes de crear el Caso durable.
      required: [state, checklist, nextAction]
      properties:
        state: { type: string, enum: [profile_ready, certificate_ready] }
        nextAction: { type: string, enum: [upload_certificate, create_certification_case] }
        checklist:
          type: object
          required: [certificateUploaded, runnerCompleted, dgiiApproved]
          properties:
            certificateUploaded: { type: boolean }
            runnerCompleted: { type: boolean }
            dgiiApproved: { type: boolean }
    PartnerUploadSession:
      type: object
      required: [uploadId, uploadUrl, uploadToken, expiresAt, maxBytes, browserOrigin, allowedExtensions]
      properties:
        uploadId: { type: string, format: uuid }
        uploadUrl: { type: string, format: uri }
        uploadToken: { type: string }
        expiresAt: { type: string, format: date-time }
        maxBytes: { type: integer }
        browserOrigin:
          type: [string, 'null']
          format: uri
          description: Origen de navegador autorizado para esta sesión; null solo en compatibilidad legada.
        allowedExtensions:
          type: array
          items: { type: string }
        correlationId: { type: string }
    PartnerCertificateResponse:
      type: object
      required: [certificate]
      properties:
        certificate:
          oneOf:
            - type: 'null'
            - type: object
              required: [status]
              properties:
                id:
                  type: string
                  description: Presente en `GET .../certificate`; la respuesta de carga no lo incluye.
                fingerprint: { type: string }
                rnc:
                  type: string
                  description: RNC de la Cuenta fiscal; presente en la respuesta de `PUT /certificate-upload-sessions/{uploadId}`.
                subject: { type: [string, 'null'] }
                issuer: { type: [string, 'null'] }
                serialNumber: { type: [string, 'null'] }
                validFrom: { type: string, format: date-time }
                expiresAt: { type: string, format: date-time }
                status: { type: string }
        correlationId: { type: string }
    PartnerDgiiRegistrationEnvironment:
      type: object
      required: [dgiiPortalUrl, receptionUrl, commercialApprovalUrl, authenticationUrl]
      properties:
        dgiiPortalUrl: { type: string, format: uri }
        receptionUrl: { type: string, format: uri }
        commercialApprovalUrl: { type: string, format: uri }
        authenticationUrl:
          type: string
          description: Siempre vacía; el campo se deja en blanco en DGII.
    PartnerDgiiRegistration:
      type: object
      required: [fiscalAccountId, externalTenantId, taxpayer, software, provider, environments]
      properties:
        fiscalAccountId: { type: string, format: uuid }
        externalTenantId: { type: string }
        taxpayer:
          type: object
          required: [rnc, legalName, commercialName]
          properties:
            rnc: { type: string }
            legalName: { type: string }
            commercialName: { type: [string, 'null'] }
        software:
          type: object
          required: [type, name, version]
          properties:
            type: { type: string, enum: [EXTERNO] }
            name: { type: string }
            version: { type: string }
        provider:
          type: object
          required: [rnc, rncDisplay, legalName, commercialName, email]
          properties:
            rnc: { type: string, pattern: '^[0-9]{9}$' }
            rncDisplay: { type: string }
            legalName: { type: string }
            commercialName: { type: string }
            email: { type: string, format: email }
        environments:
          type: object
          required: [certecf, ecf]
          properties:
            certecf:
              $ref: '#/components/schemas/PartnerDgiiRegistrationEnvironment'
            ecf:
              $ref: '#/components/schemas/PartnerDgiiRegistrationEnvironment'
        correlationId: { type: string }
    PartnerCertificationXmlSignature:
      type: object
      required: [documentType, rootTag, fileName, signedXmlBase64, certificate, signedAt]
      properties:
        documentType: { type: string, enum: [postulation, sworn_declaration] }
        rootTag: { type: string }
        fileName: { type: string }
        signedXmlBase64: { type: string, contentEncoding: base64 }
        certificate:
          type: object
          required: [id, fingerprint]
          properties:
            id: { type: [string, 'null'] }
            fingerprint: { type: [string, 'null'] }
        signedAt: { type: string, format: date-time }
        correlationId: { type: string }
    CommunicationTestTraffic:
      type: object
      required: [received, rejected, lastAt, rejections]
      properties:
        received: { type: integer, description: Documentos recibidos (acuse con Estado 0 o aprobación con 200). }
        rejected: { type: integer, description: Documentos no recibidos (firma o formato inválidos). }
        lastAt: { type: [string, 'null'], format: date-time }
        rejections:
          type: array
          description: Hasta 10 de los no recibidos, del más reciente al más antiguo.
          items:
            type: object
            properties:
              encf: { type: [string, 'null'] }
              issuerRnc: { type: [string, 'null'] }
              code: { type: [string, 'null'], example: INBOUND_SIGNATURE_INVALID }
              receivedAt: { type: [string, 'null'], format: date-time }
    CommercialApprovalTests:
      type: object
      description: >-
        Un envío de las aprobaciones comerciales de prueba (paso 3 de CerteCF): una por cada fila
        del Excel de la DGII, enviadas de una en una.
      required: [jobId, status, summary, approvals]
      properties:
        jobId: { type: string, format: uuid }
        status: { type: string, enum: [running, completed, failed, cancelled] }
        buyerRnc: { type: [string, 'null'] }
        startedAt: { type: [string, 'null'], format: date-time }
        completedAt: { type: [string, 'null'], format: date-time }
        summary:
          type: object
          required: [total, accepted, rejected, uncertain, errors, pending]
          properties:
            total: { type: integer }
            accepted: { type: integer }
            rejected: { type: integer }
            uncertain: { type: integer, description: Enviadas sin respuesta de la DGII. }
            errors: { type: integer, description: La DGII no las procesó (autenticación o límite de solicitudes). }
            pending: { type: integer }
        approvals:
          type: array
          items:
            type: object
            required: [encf, status]
            properties:
              encf: { type: string, example: E310000000002 }
              rncEmisor: { type: string, example: '131880681' }
              fechaEmision: { type: string, example: 01-04-2020 }
              montoTotal: { type: string, example: '4674.35' }
              estado: { type: integer, enum: [1, 2], description: 1 aprobada, 2 rechazada. }
              status: { type: string, enum: [pending, sending, accepted, rejected, uncertain, error] }
              fechaHoraAprobacionComercial: { type: [string, 'null'], example: 03-10-2026 00:21:30 }
              sentAt: { type: [string, 'null'], format: date-time }
              messages: { type: array, items: { type: string } }
        error:
          oneOf:
            - type: 'null'
            - type: object
              properties:
                code: { type: string }
                message: { type: string }
    PartnerCertificationCase:
      type: object
      required: [caseId, fiscalAccountId, state, checklist, nextAction]
      properties:
        caseId: { type: string, format: uuid }
        fiscalAccountId: { type: string, format: uuid }
        officialSetRef: { type: string }
        state: { type: string }
        internalState: { type: string }
        version: { type: integer }
        progress: { type: integer, minimum: 0, maximum: 100 }
        checklist:
          type: object
          required: [certificateUploaded, runnerCompleted, dgiiApproved]
          properties:
            certificateUploaded: { type: boolean }
            runnerCompleted: { type: boolean }
            dgiiApproved: { type: boolean }
            dgiiPostulationSubmitted: { type: boolean }
            printedRepresentationsUploaded: { type: boolean }
            productionUrlsSubmitted: { type: boolean }
            swornDeclarationSubmitted: { type: boolean }
            ofvRolesConfigured: { type: boolean }
            commercialApprovalsSent:
              type: boolean
              description: El último envío de las aprobaciones comerciales de prueba terminó con todas aceptadas.
            simulationPassed:
              type: boolean
              description: El último run es la simulación y la DGII la aceptó.
        latestRun:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/PartnerRunSummary'
        blockingReasons:
          type: array
          items: { type: string }
        nextAction:
          type: string
          description: >-
            El siguiente paso, en el orden del asistente: upload_certificate,
            submit_dgii_postulation, wait_for_dgii_application, upload_certification_test_set
            (el Caso usa `dgii-model`: sube el Excel de la DGII y cambia el Caso a ese set),
            run_certification_tests,
            wait_for_dgii_results, restart_failed_part, send_commercial_approvals,
            run_simulation_tests,
            upload_printed_representations,
            submit_production_urls, submit_sworn_declaration, configure_ofv_roles,
            wait_for_dgii_approval o follow_case_instructions.
        partnerActions:
          type: array
          items: { type: string }
        attestations:
          type: array
          items: { type: object, additionalProperties: true }
        downloads:
          description: >-
            Cuando la DGII aceptó el último run: las rutas para descargar el XML íntegro (e-CF) de
            las Facturas de Consumo menores a RD$250,000 (lo que se sube en «Facturas de consumo <
            250Mil»), una copia de sus resúmenes RFCE, los PDF impresos y la evidencia. Null
            mientras el run no pase.
          oneOf:
            - type: 'null'
            - type: object
              required: [runId, evidence]
              properties:
                runId: { type: string, format: uuid }
                consumerSummaries:
                  type: [string, 'null']
                  description: Copia de los resúmenes RFCE enviados de las Facturas de Consumo menores a RD$250,000 (no se suben en el portal). Null si el set no tiene.
                consumerInvoices:
                  type: [string, 'null']
                  description: XML íntegro (e-CF) de esas mismas facturas, lo que se sube en «Facturas de consumo < 250Mil». Null si el set no tiene.
                consumerInvoiceFiles:
                  type: [array, 'null']
                  description: >-
                    Una entrada por Factura de Consumo menor a RD$250,000 aceptada, con la ruta de su
                    XML suelto (`consumerInvoices` con `?encf=`), para mostrar un botón por factura. Solo
                    en el detalle de la Cuenta fiscal y del caso; no viene en los listados. Null si el set
                    no tiene.
                  items:
                    type: object
                    required: [encf, url]
                    properties:
                      encf: { type: string, example: E320000000012 }
                      url: { type: string }
                printedPdfs:
                  type: [string, 'null']
                  description: >-
                    PDF impresos de los comprobantes del run, lo que la DGII pide en el paso 5. Solo
                    cuando el último run es la simulación; null para las pruebas de datos.
                evidence: { type: string }
        commercialApprovals:
          description: >-
            El último envío de las aprobaciones comerciales de prueba (paso 3 de CerteCF). Solo en
            el detalle de la Cuenta fiscal y del Caso; null si todavía no hay ninguno.
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/CommercialApprovalTests'
        communicationTests:
          description: >-
            Pruebas de comunicación (pasos 7 a 11 de CerteCF): lo que la DGII envió de verdad al
            receptor de CerteCF de la Cuenta fiscal. `ecf` son los e-CF (paso 9) y `approvals` las
            aprobaciones comerciales (paso 11). El resultado de cada prueba lo da la DGII en su
            portal. Solo en el detalle de la Cuenta fiscal y del Caso.
          type: [object, 'null']
          properties:
            ecf: { $ref: '#/components/schemas/CommunicationTestTraffic' }
            approvals: { $ref: '#/components/schemas/CommunicationTestTraffic' }
        parts:
          $ref: '#/components/schemas/CertificationParts'
          description: >-
            Avance de las pruebas de datos y de la simulación. Viene al consultar la Cuenta
            fiscal o el Caso y al reiniciar una parte.
        restart:
          $ref: '#/components/schemas/CertificationRestart'
          description: Solo al reiniciar una parte.
        closed:
          type: boolean
          description: true si el Caso se cerró para empezar con un set nuevo.
        closedAt: { type: [string, 'null'], format: date-time }
        replayed:
          type: boolean
          description: Solo en el cierre; true si el Caso ya estaba cerrado.
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        correlationId: { type: string }
    PartnerCertificationTestSet:
      type: object
      required: [testSetRef, sha256, format, documentCount]
      properties:
        testSetRef:
          type: string
          pattern: '^set-[0-9a-f]{16}$'
          description: Úsalo como `officialSetRef` al crear el Caso.
        sha256: { type: string, pattern: '^[0-9a-f]{64}$' }
        format: { type: string, enum: [json, xlsx] }
        fileName: { type: [string, 'null'] }
        documentCount: { type: integer, minimum: 1 }
        types:
          type: array
          items: { type: string }
        encfs:
          type: array
          items: { type: string }
        createdAt: { type: string, format: date-time }
        replayed: { type: boolean }
        correlationId: { type: string }
    PartnerPrintedPdfsResponse:
      type: object
      required: [runId, fileName, contentType, encoding, byteLength, fileCount, exceedsDgiiLimit, content]
      properties:
        runId: { type: string, format: uuid }
        fileName: { type: string, example: representaciones-impresas-131123456.zip }
        contentType: { type: string, const: application/zip }
        encoding: { type: string, const: base64 }
        byteLength: { type: integer }
        fileCount: { type: integer, minimum: 1 }
        missing:
          type: integer
          description: Comprobantes del run sin PDF todavía.
        exceedsDgiiLimit:
          type: boolean
          description: true si el ZIP pasa de 10 MB, el máximo del portal de la DGII.
        selection: { type: string, enum: [all, dgii] }
        slots:
          type: array
          description: Con selection=dgii, cada casilla del paso 5 con el e-NCF y el archivo que le toca (null si falta).
          items:
            type: object
            properties:
              slot: { type: string, example: E31 }
              label: { type: string, example: Tipo 31 }
              encf: { type: [string, 'null'] }
              fileName: { type: [string, 'null'], example: 01 - Tipo 31 - E310009002001.pdf }
        content: { type: string, contentEncoding: base64 }
        correlationId: { type: string }
    PartnerRfceSummaryXmlResponse:
      type: object
      required: [runId, fileName, contentType, encoding, fileCount, files, content]
      properties:
        runId: { type: string, format: uuid }
        encf:
          type: string
          description: Solo con `encf`, la factura descargada.
        fileName: { type: string, example: resumenes-rfce-131123456.zip }
        contentType: { type: string, enum: [application/zip, application/xml] }
        encoding: { type: string, const: base64 }
        byteLength: { type: integer }
        fileCount: { type: integer, minimum: 1 }
        files:
          type: array
          description: Nombre de cada RFCE dentro del ZIP (RNC emisor + e-NCF).
          items: { type: string, example: 131123456E320000000011.xml }
        content: { type: string, contentEncoding: base64 }
        correlationId: { type: string }
    PartnerCertificationResend:
      type: object
      required: [caseId, documentId, encf, status, jobType, jobId]
      properties:
        caseId: { type: string, format: uuid }
        documentId: { type: string, format: uuid }
        encf: { type: string }
        status:
          type: string
          description: Estado del comprobante al pedir el reenvío.
        jobType:
          type: string
          enum: [ecf.send, ecf.status.poll]
          description: '`ecf.status.poll` si la DGII ya le dio TrackID.'
        jobId: { type: [string, 'null'], format: uuid }
        correlationId: { type: string }
    PartnerRunStarted:
      type: object
      required: [caseId, runId, status, documentCount]
      properties:
        caseId: { type: string, format: uuid }
        runId: { type: string, format: uuid }
        status: { type: string, enum: [queued, running, succeeded, failed, cancelled] }
        documentCount: { type: integer }
        replayed: { type: boolean }
        correlationId: { type: string }
    PartnerRunSummary:
      type: object
      required: [runId, status, documentCount]
      properties:
        runId: { type: string, format: uuid }
        status: { type: string, enum: [queued, running, succeeded, failed, cancelled] }
        documentCount: { type: integer }
        sourceRef: { type: string }
        startedAt: { type: [string, 'null'], format: date-time }
        completedAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }
        mode:
          type: string
          enum: [data, simulation]
          description: En el caso, si el último run fue de pruebas de datos o de simulación.
    PartnerRunList:
      type: object
      required: [caseId, runs]
      properties:
        caseId: { type: string, format: uuid }
        runs:
          type: array
          items:
            $ref: '#/components/schemas/PartnerRunSummary'
        correlationId: { type: string }
    PartnerCertificationRunStatus:
      type: object
      required:
        - runId
        - jobId
        - jobStatus
        - runStatus
        - batchIndex
        - batches
        - documentIds
        - currentBatchSummary
        - overallSummary
        - errors
        - evidenceArtifactId
        - b2bSimulationEnabled
      properties:
        runId: { type: string, format: uuid }
        jobId: { type: string, format: uuid }
        jobStatus: { type: string }
        runStatus: { type: string }
        batchIndex: { type: integer, minimum: 0 }
        batches: { type: array, items: { type: object, additionalProperties: true } }
        documentIds: { type: array, items: { type: string, format: uuid } }
        currentBatch:
          oneOf:
            - type: 'null'
            - type: object
              additionalProperties: true
        currentBatchSummary: { type: object, additionalProperties: true }
        overallSummary: { type: object, additionalProperties: true }
        startedAt: { type: string, format: date-time }
        completedAt: { type: string, format: date-time }
        errors: { type: array, items: {} }
        summary: { type: object, additionalProperties: true }
        evidenceArtifactId:
          oneOf:
            - type: 'null'
            - type: string
        b2bSimulationEnabled: { type: boolean }
        b2bSimulation:
          oneOf:
            - type: 'null'
            - type: object
              additionalProperties: true
        cases:
          type: array
          description: Un elemento por comprobante del set, en el orden en que se envían a la DGII.
          items:
            $ref: '#/components/schemas/PartnerCertificationRunCase'
        correlationId: { type: string }
    PartnerCertificationRunCase:
      type: object
      required: [documentId, type, encf, status, dgiiMessages]
      properties:
        documentId: { type: string, format: uuid }
        caseId:
          description: Columna CasoPrueba del Excel de la DGII (RNC emisor seguido del e-NCF). Null en sets JSON.
          oneOf:
            - type: 'null'
            - type: string
        type: { type: string, example: E31 }
        encf: { type: string, example: E310000000001 }
        batchId:
          oneOf:
            - type: 'null'
            - type: string
        status: { type: string, example: accepted }
        dgiiStatus:
          oneOf:
            - type: 'null'
            - type: string
        trackId:
          oneOf:
            - type: 'null'
            - type: string
        dgiiMessages:
          type: array
          description: Mensajes de la DGII, por ejemplo el motivo de un rechazo.
          items: { type: string }
        superseded:
          type: boolean
          description: true si el comprobante se reemplazó al reiniciar esa parte de las pruebas.
        resendable:
          type: boolean
          description: >-
            true si el comprobante se trabó por un problema de envío y se puede reenviar con
            `POST .../documents/{documentId}/resend`.
    PartnerProductionState:
      type: object
      required: [fiscalAccountId, state, stateVersion]
      properties:
        requestId:
          type: string
          format: uuid
          description: Solo en `POST .../production-activation-requests`.
        fiscalAccountId: { type: string, format: uuid }
        state: { type: string, enum: [inactive, activation_requested, under_review, active, suspended, disabled] }
        stateVersion: { type: integer, minimum: 0 }
        requestedAt:
          type: string
          format: date-time
          description: Solo en `POST .../production-activation-requests`.
        latestRequest:
          description: Solo en `GET .../production`; última solicitud o null.
          oneOf:
            - type: 'null'
            - type: object
              additionalProperties: true
              properties:
                requestId: { type: string, format: uuid }
                status: { type: string, enum: [pending, approved, rejected, cancelled] }
                requestedAt: { type: string, format: date-time }
                decidedAt: { type: [string, 'null'], format: date-time }
        correlationId: { type: string }
    PartnerWebhookDeliveryResponse:
      type: object
      required: [id, endpointId, fiscalAccountId, externalTenantId, eventId, eventType, stateVersion, status, attempts, availableAt, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        endpointId: { type: string, format: uuid }
        fiscalAccountId: { type: string, format: uuid }
        externalTenantId: { type: string }
        eventId: { type: string, format: uuid }
        eventType: { type: string }
        stateVersion: { type: integer }
        status: { type: string, enum: [queued, running, succeeded, failed] }
        attempts: { type: integer, minimum: 0 }
        availableAt: { type: string, format: date-time }
        responseStatus: { type: [integer, 'null'] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        correlationId: { type: string }
    PartnerWebhookEndpointRequest:
      type: object
      required: [url]
      additionalProperties: false
      properties:
        name: { type: string, maxLength: 200 }
        url: { type: string, format: uri, maxLength: 2048 }
        eventTypes: { type: array, minItems: 1, items: { type: string, minLength: 1 } }
    PartnerWebhookEndpointResponse:
      type: object
      required: [id, url]
      properties:
        id: { type: string, format: uuid }
        partnerId: { type: string, format: uuid }
        status: { type: string, enum: [active, paused, disabled] }
        replayed: { type: boolean }
        name: { type: [string, 'null'] }
        url: { type: string, format: uri }
        eventTypes: { type: array, items: { type: string } }
        previousSecretExpiresAt:
          type: [string, 'null']
          format: date-time
          description: Fin de la ventana en la que las entregas llevan también la firma del secreto anterior.
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        signingSecret:
          type: string
          description: Solo se devuelve al crear o rotar el endpoint.
        correlationId: { type: string }
    PartnerWebhookTestRequest:
      type: object
      required: [fiscalAccountId]
      additionalProperties: false
      properties:
        fiscalAccountId: { type: string, format: uuid }
        environment:
          type: string
          enum: [certecf, ecf]
          default: certecf
          description: Ambiente que aparecerá en el payload de prueba; no emite documentos.
    PartnerWebhookTestResponse:
      type: object
      required: [deliveryId, eventId, eventType, fiscalAccountId, environment, status]
      properties:
        deliveryId: { type: string, format: uuid }
        eventId: { type: string, format: uuid }
        eventType: { type: string, const: webhook.test }
        fiscalAccountId: { type: string, format: uuid }
        environment: { type: string, enum: [certecf, ecf] }
        status: { type: string, const: queued }
        correlationId: { type: string }
    PartnerWebhookEndpointsResponse:
      type: object
      required: [endpoints]
      properties:
        endpoints:
          type: array
          items:
            $ref: '#/components/schemas/PartnerWebhookEndpointResponse'
        correlationId: { type: string }
    GenericOkResponse:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
      additionalProperties: true
    ErrorResponse:
      type: object
      required: [ok, error]
      properties:
        ok:
          type: boolean
          const: false
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
            message:
              type: string
            details:
              type: object
              additionalProperties: true
        correlationId:
          type: string
