openapi: 3.1.0
info:
  title: Zarela PSFE 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.
servers:
  - url: https://ecf.api.zarela.do
    description: Produccion
  - url: http://localhost:3000
    description: Desarrollo local
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
  /{environment}/documentos-ecf:
    parameters:
      - $ref: '#/components/parameters/Environment'
    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`, Zarela
        requiere el eNCF asignado por el POS, persiste el documento y encola
        firma/envio asincrono para DGII.
      parameters:
        - name: validate
          in: query
          required: false
          schema:
            type: boolean
          description: Preflight sin efectos fiscales cuando es `true`.
      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
                      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
      responses:
        '200':
          description: Preflight completado cuando `validate=true`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitResponse'
        '202':
          description: Documento registrado y encolado para procesamiento asincrono.
          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'
  /{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.
      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/{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: |
        Persiste evidencia WORM y fija una sola vez `sent` o
        `dispatch_failed`. 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.
      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.
      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.
      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'
  /{environment}/anulaciones-ecf:
    parameters:
      - $ref: '#/components/parameters/Environment'
      - $ref: '#/components/parameters/IdempotencyKey'
    post:
      tags: [Anulaciones]
      summary: Crea una solicitud ANECF.
      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 a DGII.
          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}/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.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommercialApprovalRequest'
      responses:
        '200':
          description: ACECF generado sin despacho.
          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'
        '502':
          description: El despacho fallo o quedo con resultado incierto; consulta el estado antes de actuar.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommercialApprovalSubmitResponse'
  /{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'
        '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'
        '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'
  /fe/recepcion/api/ecf:
    post:
      tags: [Publico DGII]
      security: []
      summary: Recepcion publica de ECF/RFCE entrante.
      requestBody:
        required: true
        content:
          application/xml:
            schema:
              type: string
      responses:
        '200':
          description: Replay idempotente de un XML ya recibido.
          content:
            application/xml:
              schema:
                type: string
        '202':
          description: XML recibido y ARECF generado.
          content:
            application/xml:
              schema:
                type: string
  /fe/aprobacioncomercial/api/ecf:
    post:
      tags: [Publico DGII]
      security: []
      summary: Recepcion publica de ACECF.
      requestBody:
        required: true
        content:
          application/xml:
            schema:
              type: string
      responses:
        '200':
          description: Replay idempotente de un XML ya recibido.
          content:
            application/xml:
              schema:
                type: string
        '202':
          description: ACECF recibido y acuse generado.
          content:
            application/xml:
              schema:
                type: string
  /fe/autenticacion/api/ValidacionCertificado:
    post:
      tags: [Publico DGII]
      security: []
      summary: Validacion publica de certificado, casing DGII.
      requestBody:
        required: true
        content:
          application/xml:
            schema:
              type: string
      responses:
        '200':
          description: Respuesta XML de validacion.
          content:
            application/xml:
              schema:
                type: string
  /fe/autenticacion/api/validacioncertificado:
    post:
      tags: [Publico DGII]
      security: []
      summary: Validacion publica de certificado, casing lowercase.
      requestBody:
        required: true
        content:
          application/xml:
            schema:
              type: string
      responses:
        '200':
          description: Respuesta XML de validacion.
          content:
            application/xml:
              schema:
                type: string
  /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.
      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'
  /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'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/profile:
    patch:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Actualiza los datos no identificadores de la cuenta fiscal.
      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'
  /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'
  /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 ERP entrega el uploadToken al navegador o cliente autorizado; nunca debe persistir el certificado ni la contraseña en su backend.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      responses:
        '201':
          description: Sesion temporal creada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerUploadSession'
  /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'
  /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.
      parameters:
        - name: uploadId
          in: path
          required: true
          schema:
            type: string
        - name: Authorization
          in: header
          required: true
          schema:
            type: string
            pattern: '^Upload .+$'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file, passphrase]
              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'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Crea o recupera el caso de certificacion CerteCF.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [officialSetRef]
              properties:
                officialSetRef:
                  type: string
                  description: Referencia de un set oficial habilitado por PSFE.
      responses:
        '201':
          description: Caso creado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificationCase'
        '200':
          description: Caso activo existente.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerCertificationCase'
  /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'
  /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'
    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
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                correlationId: { type: string }
                b2bSimulation: { type: boolean, default: true }
      responses:
        '202':
          description: Run encolado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerRunStarted'
  /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/GenericOkResponse'
  /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/CertificationEvidenceResponse'
        '409':
          description: La evidencia todavía no está disponible.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/certification-cases/{caseId}/attestations:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Registra una atestacion humana del integrador.
      description: Los pasos de validacion del certificado, runner y aprobacion DGII solo los registra PSFE internamente.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - $ref: '#/components/parameters/CertificationCaseId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, actorRef]
              properties:
                type: { type: string }
                actorRef: { type: string }
                occurredAt: { type: string, format: date-time }
                evidenceRefs: { type: array, items: { type: string } }
                notes: { type: string }
      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'
  /partner/v1/fiscal-accounts/{fiscalAccountId}/production-activation-requests:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Solicita activacion de produccion.
      description: La solicitud no aprueba por si sola; requiere decision interna o evidencia DGII verificada por PSFE.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [certificationCaseId]
              properties:
                certificationCaseId: { type: string }
                notes: { type: string }
                evidenceRefs: { type: array, items: { type: string } }
      responses:
        '201':
          description: Solicitud creada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerProductionState'
        '200':
          description: Solicitud repetida idempotentemente.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerProductionState'
  /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'
  /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` no persiste ni envia a DGII; la emision normal es asincrona.
      parameters:
        - $ref: '#/components/parameters/PartnerFiscalAccountId'
        - name: validate
          in: query
          required: false
          schema: { type: boolean, default: false }
      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'
        '202':
          description: Documento encolado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitResponse'
        '423':
          description: La cuenta todavía no está autorizada para produccion.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /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 }
      responses:
        '200':
          description: Estado del documento.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericOkResponse'
  /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'
  /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'
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Registra un endpoint webhook del ERP.
      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'
  /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'
  /partner/v1/webhook-endpoints/{endpointId}/rotate-secret:
    post:
      tags: [Partner]
      security:
        - PartnerKeyAuth: []
      summary: Rota el secreto de un webhook.
      parameters:
        - name: endpointId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                overlapSeconds: { type: integer, minimum: 0, maximum: 86400 }
      responses:
        '200':
          description: Endpoint actualizado; el nuevo secreto se muestra una sola vez.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerWebhookEndpointResponse'
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
    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.
  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.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  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.
    SubmitResponse:
      type: object
      required: [ok, correlationId]
      properties:
        ok:
          type: boolean
        correlationId:
          type: string
        mode:
          type: string
          enum: [preflight, queued, idempotent]
        result:
          type: object
          additionalProperties: true
          properties:
            documentId:
              type: string
            encf:
              type: string
            status:
              type: string
            asyncStatusRequired:
              type: boolean
        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
    ContingencyResponse:
      type: object
      required: [ok, environment, mode, count, documents]
      properties:
        ok:
          type: boolean
        environment:
          type: string
        mode:
          type: string
          const: contingency
        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
          format: date-time
    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
        targetUrl:
          type: string
          format: uri
        dispatch:
          oneOf:
            - type: boolean
            - type: string
              enum: [build-only]
        autoResolveDirectory:
          type: boolean
    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 en contrato simplificado. Use `documents`, `rows` o `workbookBase64`.
      properties:
        documents:
          type: array
          items:
            type: object
            additionalProperties: true
        rows:
          type: array
          items:
            type: object
            additionalProperties: true
        workbookBase64:
          type: string
          contentEncoding: base64
        b2bSimulation:
          type: boolean
          default: true
      anyOf:
        - 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
    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
    PartnerProvisionRequest:
      type: object
      required: [rnc, legalName]
      properties:
        rnc:
          type: string
          pattern: '^[0-9]{9,11}$'
        legalName:
          type: string
        commercialName:
          type: string
        fiscalEmail:
          type: string
          format: email
        phone:
          type: string
        address:
          type: string
        provinceCode:
          type: string
        municipalityCode:
          type: string
        ecfTypes:
          type: array
          items: { type: string }
    PartnerProfilePatch:
      type: object
      minProperties: 1
      properties:
        legalName: { type: string }
        commercialName: { type: string }
        fiscalEmail: { type: string, format: email }
        phone: { type: string }
        address: { type: string }
        provinceCode: { type: string }
        municipalityCode: { type: string }
        ecfTypes: { type: array, items: { type: string } }
    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 }
        environments:
          type: object
          properties:
            certecf: { type: string, enum: [active, inactive] }
            ecf: { type: string, enum: [active, inactive] }
        certification:
          $ref: '#/components/schemas/PartnerCertificationCase'
    PartnerUploadSession:
      type: object
      required: [uploadId, uploadUrl, uploadToken, expiresAt, maxBytes, allowedExtensions]
      properties:
        uploadId: { type: string, format: uuid }
        uploadUrl: { type: string, format: uri }
        uploadToken: { type: string }
        expiresAt: { type: string, format: date-time }
        maxBytes: { type: integer }
        allowedExtensions:
          type: array
          items: { type: string }
    PartnerCertificateResponse:
      type: object
      required: [certificate]
      properties:
        certificate:
          oneOf:
            - type: 'null'
            - type: object
              required: [status]
              properties:
                id: { type: string }
                fingerprint: { type: string }
                subject: { type: string }
                issuer: { type: string }
                serialNumber: { type: string }
                validFrom: { type: string, format: date-time }
                expiresAt: { type: string, format: date-time }
                status: { 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 }
        latestRun:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/PartnerRunSummary'
        blockingReasons:
          type: array
          items: { type: string }
        nextAction: { type: string }
        partnerActions:
          type: array
          items: { type: string }
        attestations:
          type: array
          items: { type: object, additionalProperties: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    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 }
    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, format: date-time, nullable: true }
        completedAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }
    PartnerRunList:
      type: object
      required: [caseId, runs]
      properties:
        caseId: { type: string, format: uuid }
        runs:
          type: array
          items:
            $ref: '#/components/schemas/PartnerRunSummary'
    PartnerProductionState:
      type: object
      required: [fiscalAccountId, state, stateVersion]
      properties:
        fiscalAccountId: { type: string, format: uuid }
        state: { type: string, enum: [inactive, activation_requested, under_review, active, suspended, disabled] }
        stateVersion: { type: integer, minimum: 0 }
        latestRequest: { type: object, nullable: true, additionalProperties: true }
    PartnerWebhookEndpointRequest:
      type: object
      required: [url]
      properties:
        name: { type: string }
        url: { type: string, format: uri }
        eventTypes: { type: array, items: { type: string } }
    PartnerWebhookEndpointResponse:
      type: object
      required: [id, url]
      properties:
        id: { type: string, format: uuid }
        name: { type: string, nullable: true }
        url: { type: string, format: uri }
        eventTypes: { type: array, items: { type: string } }
        signingSecret:
          type: string
          description: Solo se devuelve al crear o rotar el endpoint.
    PartnerWebhookTestRequest:
      type: object
      required: [fiscalAccountId]
      properties:
        fiscalAccountId: { type: string, format: uuid }
        environment:
          type: string
          enum: [testecf, 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: [testecf, certecf, ecf] }
        status: { type: string, const: queued }
    PartnerWebhookEndpointsResponse:
      type: object
      required: [endpoints]
      properties:
        endpoints:
          type: array
          items:
            $ref: '#/components/schemas/PartnerWebhookEndpointResponse'
    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
