openapi: 3.1.0
info:
  title: API Facturador emitir-cpe
  version: 1.0.0-alpha
  summary: API de facturación electrónica SUNAT (Perú) — emisión de CPE, guías de remisión y distribución.
  description: "API pública del facturador **emitir-cpe**: emisión de comprobantes de pago\nelectrónicos\
    \ peruanos (Factura 01, Boleta 03, Notas de Crédito 07 y Débito 08),\nresúmenes diarios y bajas (RC/RA/RR),\
    \ guías de remisión electrónicas\n(GRE 2022 REST, tipo 09), archivos legales (XML firmado, CDR, PDF\
    \ con QR),\ndistribución (email, portal público, webhooks firmados) y administración\nmulti-tenant\
    \ (empresas, series, clientes, catálogo, tokens de API).\n\n## Convenciones\n- **Multi-tenant**: el\
    \ contexto de empresa SIEMPRE sale del token/sesión,\n  jamás de un parámetro de request.\n- **IDs**:\
    \ ULID opacos ordenables por tiempo.\n- **Errores**: todo error es `application/problem+json` (RFC\
    \ 7807) con un\n  `code` estable de máquina, `sunat_code` cuando aplica y `request_id`\n  (ULID) para\
    \ correlación. Ver el schema `Problem`.\n- **Idempotencia**: `X-Idempotency-Key` es obligatoria en\
    \ toda emisión\n  (documentos, resúmenes, bajas y guías). TTL de 24 h: la misma clave con el\n  mismo\
    \ body devuelve la respuesta cacheada; con distinto body responde\n  `422 idempotency_key_reused`;\
    \ en curso responde `409 idempotency_in_flight`.\n- **Rate limiting**: toda respuesta incluye cabeceras\
    \ `RateLimit-Limit`,\n  `RateLimit-Remaining` y `RateLimit-Reset`; los 429 añaden `Retry-After`.\n\
    - **Paginación**: por cursor (`?after=<ulid>&limit=`); las listas devuelven\n  `meta.next_cursor`\
    \ (null en la última página). Sin OFFSET.\n- **Emisión asíncrona**: `POST /v1/documents` responde\
    \ `201` con el\n  documento firmado (`estado=signed`); el resultado SUNAT llega por webhook\n  o consulta.\
    \ El modo `?wait=true` espera el CDR con timeout.\n- **Abilities**: los endpoints anotan en `x-abilities`\
    \ las habilidades que\n  debe tener el token de API (`documents:read/write/void`, `files:read`,\n\
    \  `series:manage`, `clients:read/write`, `webhooks:manage`,\n  `summaries:manage`, `despatches:write`,\
    \ `company:manage`, `reports:read`).\n  Un token sin la ability recibe `403 missing_ability`.\n"
  contact:
    name: Soporte emitir-cpe
    url: https://docs.emitir-cpe.com
    email: soporte@emitir-cpe.com
  license:
    name: Propietaria
    identifier: LicenseRef-Proprietary
servers:
- url: https://emitir-cpe.com
  description: Producción
- url: http://localhost:8080
  description: Desarrollo local
security:
- apiToken: []
tags:
- name: auth
  description: Registro, login, sesiones, recuperación de contraseña y verificación de email.
- name: me
  description: Perfil del usuario autenticado, sus empresas y la empresa activa.
- name: companies
  description: Empresas (tenants), sucursales, miembros, invitaciones y auditoría.
- name: credentials
  description: Credenciales SUNAT (Clave SOL, GRE) y certificados digitales.
- name: series
  description: Series y correlativos por tipo de documento.
- name: clients
  description: Padrón de clientes de la empresa.
- name: catalog
  description: Catálogo de items de venta (sin inventario/stock).
- name: documents
  description: Emisión y ciclo de vida de comprobantes (01/03/07/08).
- name: files
  description: Archivos legales del comprobante (XML firmado, CDR, PDF).
- name: distribution
  description: Distribución del comprobante (email, portal público).
- name: summaries
  description: Resúmenes diarios de boletas (RC) y comunicaciones de baja (RA/RR).
- name: despatches
  description: Guías de remisión electrónicas remitente (GRE 2022 REST, tipo 09).
- name: webhooks
  description: Webhooks salientes por empresa, firmados con HMAC-SHA256.
- name: api-tokens
  description: Tokens de API con abilities granulares.
- name: exports
  description: Exportaciones asíncronas (CSV/Excel del listado, zip de archivos).
- name: meta
  description: Recursos de la propia API (contrato OpenAPI, portal público).
paths:
  /v1/auth/register:
    post:
      tags:
      - auth
      operationId: authRegister
      summary: Registrar cuenta de usuario
      description: Crea una cuenta con email y contraseña. Envía email de verificación.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - email
              - password
              - name
              properties:
                email:
                  type: string
                  format: email
                  example: maria@losandes.pe
                password:
                  type: string
                  format: password
                  minLength: 10
                  example: S0les-y-C3ntimos!
                name:
                  type: string
                  example: María Quispe
      responses:
        '201':
          description: Cuenta creada. Se envió el email de verificación.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/auth/login:
    post:
      tags:
      - auth
      operationId: authLogin
      summary: Iniciar sesión
      description: Autentica con email y contraseña y devuelve una sesión opaca server-side (token bearer
        + refresh token). Limitado por IP contra fuerza bruta.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - email
              - password
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                  format: password
      responses:
        '200':
          description: Sesión creada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthSession'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/auth/logout:
    post:
      tags:
      - auth
      operationId: authLogout
      summary: Cerrar sesión
      description: Revoca la sesión del token bearer presentado.
      responses:
        '204':
          description: Sesión revocada.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/auth/refresh:
    post:
      tags:
      - auth
      operationId: authRefresh
      summary: Refrescar sesión
      description: Intercambia un refresh token vigente por una nueva sesión (rotación de refresh token
        incluida).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - refresh_token
              properties:
                refresh_token:
                  type: string
      responses:
        '200':
          description: Nueva sesión.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthSession'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/auth/password/forgot:
    post:
      tags:
      - auth
      operationId: authPasswordForgot
      summary: Solicitar recuperación de contraseña
      description: Envía un token de un solo uso (expira) al email. Siempre responde 202 para no filtrar
        existencia de cuentas.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - email
              properties:
                email:
                  type: string
                  format: email
      responses:
        '202':
          description: Si la cuenta existe
          se envió el email de recuperación.: null
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/auth/password/reset:
    post:
      tags:
      - auth
      operationId: authPasswordReset
      summary: Restablecer contraseña
      description: Consume el token de un solo uso y fija la nueva contraseña. Revoca todas las sesiones
        activas del usuario.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - token
              - password
              properties:
                token:
                  type: string
                password:
                  type: string
                  format: password
                  minLength: 10
      responses:
        '204':
          description: Contraseña actualizada.
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/auth/verify-email:
    post:
      tags:
      - auth
      operationId: authVerifyEmail
      summary: Verificar email
      description: Consume el token de verificación enviado al registrarse.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - token
              properties:
                token:
                  type: string
      responses:
        '204':
          description: Email verificado.
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/me:
    get:
      tags:
      - me
      operationId: getMe
      summary: Perfil del usuario autenticado
      responses:
        '200':
          description: Perfil.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '401':
          $ref: '#/components/responses/Unauthorized'
    put:
      tags:
      - me
      operationId: updateMe
      summary: Actualizar perfil
      description: Nombre, zona horaria e idioma del usuario.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                timezone:
                  type: string
                  example: America/Lima
                locale:
                  type: string
                  example: es-PE
      responses:
        '200':
          description: Perfil actualizado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/me/companies:
    get:
      tags:
      - me
      operationId: listMyCompanies
      summary: Empresas del usuario
      description: Empresas a las que pertenece el usuario, con su rol en cada una (owner/admin/emisor/lector).
      responses:
        '200':
          description: Membresías del usuario.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Membership'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/me/active-company:
    post:
      tags:
      - me
      operationId: setActiveCompany
      summary: Cambiar empresa activa
      description: Fija la empresa activa de la sesión. El contexto de tenant SIEMPRE sale de la sesión/token
        — nunca de un parámetro por request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - company_id
              properties:
                company_id:
                  type: string
                  example: 01J2ZJXJ2N4Y8QW3M0V6T7R9S1
      responses:
        '200':
          description: Empresa activa actualizada.
          content:
            application/json:
              schema:
                type: object
                required:
                - active_company_id
                properties:
                  active_company_id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/companies:
    post:
      tags:
      - companies
      operationId: createCompany
      summary: Crear empresa
      description: Crea una empresa (tenant) con RUC, razón social y dirección fiscal. El usuario creador
        queda como owner. La empresa nace en modo sandbox (endpoints beta de SUNAT) hasta que se active
        producción.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CompanyInput'
      responses:
        '201':
          description: Empresa creada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Company'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/company:
    get:
      tags:
      - companies
      operationId: getCompany
      summary: Datos de la empresa activa
      responses:
        '200':
          description: Empresa activa.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Company'
        '401':
          $ref: '#/components/responses/Unauthorized'
    put:
      tags:
      - companies
      operationId: updateCompany
      summary: Actualizar datos de la empresa
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CompanyInput'
      responses:
        '200':
          description: Empresa actualizada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Company'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
    patch:
      tags:
      - companies
      operationId: patchCompany
      summary: Actualización parcial (p. ej. modo sandbox)
      description: 'Cambios puntuales de flags de la empresa. `sandbox: true` fuerza los endpoints beta
        del motor SUNAT (emisión sin riesgo).'
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sandbox:
                  type: boolean
                  description: Modo sandbox por empresa (fuerza endpoints beta de SUNAT).
                nombre_comercial:
                  type: string
      responses:
        '200':
          description: Empresa actualizada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Company'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/company/branches:
    get:
      tags:
      - companies
      operationId: listBranches
      summary: Listar sucursales (anexos SUNAT)
      parameters:
      - $ref: '#/components/parameters/After'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Sucursales de la empresa.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Branch'
                  meta:
                    $ref: '#/components/schemas/PageMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
      - companies
      operationId: createBranch
      summary: Crear sucursal
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BranchInput'
      responses:
        '201':
          description: Sucursal creada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Branch'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/company/branches/{id}:
    parameters:
    - $ref: '#/components/parameters/ResourceId'
    get:
      tags:
      - companies
      operationId: getBranch
      summary: Obtener sucursal
      responses:
        '200':
          description: Sucursal.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Branch'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - companies
      operationId: updateBranch
      summary: Actualizar sucursal
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BranchInput'
      responses:
        '200':
          description: Sucursal actualizada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Branch'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
    delete:
      tags:
      - companies
      operationId: disableBranch
      summary: Desactivar sucursal
      description: Marca la sucursal como desactivada (`disabled_at`). No hay borrado físico — series
        y documentos pueden referenciarla.
      x-abilities:
      - company:manage
      responses:
        '204':
          description: Sucursal desactivada.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/company/members:
    get:
      tags:
      - companies
      operationId: listMembers
      summary: Listar miembros de la empresa
      responses:
        '200':
          description: Miembros con su rol.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Member'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/company/members/{user_id}:
    parameters:
    - name: user_id
      in: path
      required: true
      schema:
        type: string
      description: ID del usuario miembro.
    patch:
      tags:
      - companies
      operationId: updateMember
      summary: Cambiar rol de un miembro
      description: 'Cambia el rol del miembro. Asignar `owner` transfiere el ownership: el owner actual
        pasa a `admin` (solo el owner puede hacerlo).'
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - role
              properties:
                role:
                  $ref: '#/components/schemas/Role'
      responses:
        '200':
          description: Miembro actualizado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Member'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
    delete:
      tags:
      - companies
      operationId: removeMember
      summary: Remover miembro
      description: Quita al usuario de la empresa. El owner no puede removerse a sí mismo sin transferir
        el ownership antes (409 invalid_state).
      x-abilities:
      - company:manage
      responses:
        '204':
          description: Miembro removido.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
  /v1/company/invitations:
    get:
      tags:
      - companies
      operationId: listInvitations
      summary: Listar invitaciones pendientes
      x-abilities:
      - company:manage
      responses:
        '200':
          description: Invitaciones.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Invitation'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
      - companies
      operationId: createInvitation
      summary: Invitar usuario a la empresa
      description: Envía por email una invitación con rol asignado. Expira y es reenviable.
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - email
              - role
              properties:
                email:
                  type: string
                  format: email
                  example: contador@losandes.pe
                role:
                  $ref: '#/components/schemas/Role'
      responses:
        '201':
          description: Invitación creada y enviada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invitation'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/company/invitations/{id}:
    parameters:
    - $ref: '#/components/parameters/ResourceId'
    delete:
      tags:
      - companies
      operationId: revokeInvitation
      summary: Revocar invitación
      x-abilities:
      - company:manage
      responses:
        '204':
          description: Invitación revocada.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/company/invitations/{id}/resend:
    parameters:
    - $ref: '#/components/parameters/ResourceId'
    post:
      tags:
      - companies
      operationId: resendInvitation
      summary: Reenviar invitación
      description: Reenvía el email de invitación y extiende su expiración.
      x-abilities:
      - company:manage
      responses:
        '202':
          description: Invitación reenviada.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
  /v1/company/credentials/sol:
    put:
      tags:
      - credentials
      operationId: putSolCredentials
      summary: Configurar Clave SOL (usuario secundario)
      description: Guarda las credenciales SOL de la empresa cifradas en reposo, una fila por ambiente.
        El modo beta/producción es POR PRODUCTO (SOAP, GRE independientes).
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - environment
              - usuario
              - clave
              properties:
                environment:
                  $ref: '#/components/schemas/Environment'
                usuario:
                  type: string
                  description: Usuario secundario SOL (sin el RUC; el sistema antepone el RUC de la empresa).
                  example: FACTURA1
                clave:
                  type: string
                  format: password
                  example: mi-clave-sol
      responses:
        '200':
          description: Credenciales guardadas.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CredentialStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/company/credentials/gre:
    put:
      tags:
      - credentials
      operationId: putGreCredentials
      summary: Configurar credenciales GRE (client_id/secret)
      description: Credenciales del API REST de guías de remisión 2022, separadas por ambiente beta/producción.
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - environment
              - client_id
              - client_secret
              properties:
                environment:
                  $ref: '#/components/schemas/Environment'
                client_id:
                  type: string
                  example: test-85e5b0ae-255c-4891-a595-0b98c65c9854
                client_secret:
                  type: string
                  format: password
      responses:
        '200':
          description: Credenciales guardadas.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CredentialStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/company/credentials/gre/test:
    post:
      tags:
      - credentials
      operationId: testGreCredentials
      summary: Probar conexión GRE
      description: Ejecuta un test de conexión (obtención de token OAuth) contra el ambiente indicado
        y registra `verified_at` si es exitoso.
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - environment
              properties:
                environment:
                  $ref: '#/components/schemas/Environment'
      responses:
        '200':
          description: Resultado del test.
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - environment
                properties:
                  ok:
                    type: boolean
                    example: true
                  environment:
                    $ref: '#/components/schemas/Environment'
                  latency_ms:
                    type: integer
                    example: 412
                  message:
                    type:
                    - string
                    - 'null'
                    example: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
  /v1/company/certificate:
    post:
      tags:
      - credentials
      operationId: uploadCertificate
      summary: Subir certificado digital
      description: 'Sube el certificado de firma en PEM (cert+clave) o **PFX/P12 con

        conversión automática** en el motor. Valida vigencia y correspondencia

        clave-certificado. El certificado anterior se conserva cifrado en el

        historial (rotación auditada). Alertas automáticas de vencimiento a

        30/15/7/1 días.

        '
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - contenido_base64
              properties:
                contenido_base64:
                  type: string
                  contentEncoding: base64
                  description: Contenido del archivo PEM o PFX/P12 en base64.
                password:
                  type: string
                  format: password
                  description: Contraseña del PFX/P12 (requerida si el archivo es PFX).
      responses:
        '201':
          description: Certificado validado y activado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Certificate'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '413':
          description: Archivo demasiado grande (límite 25 MB).
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/company/certificates:
    get:
      tags:
      - credentials
      operationId: listCertificates
      summary: Historial de certificados
      description: Historial completo de certificados (el activo es el vigente más reciente). El material
        criptográfico jamás se devuelve.
      x-abilities:
      - company:manage
      responses:
        '200':
          description: Certificados de la empresa.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Certificate'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /v1/company/audit-log:
    get:
      tags:
      - companies
      operationId: listAuditLog
      summary: Consultar auditoría
      description: Bitácora append-only de toda escritura administrativa de la empresa.
      x-abilities:
      - company:manage
      parameters:
      - $ref: '#/components/parameters/After'
      - $ref: '#/components/parameters/Limit'
      - name: action
        in: query
        schema:
          type: string
        description: Filtra por acción (p. ej. `company.credentials.updated`).
      - name: actor_id
        in: query
        schema:
          type: string
      - name: fecha_desde
        in: query
        schema:
          type: string
          format: date
      - name: fecha_hasta
        in: query
        schema:
          type: string
          format: date
      responses:
        '200':
          description: Entradas de auditoría (descendente por fecha).
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AuditLogEntry'
                  meta:
                    $ref: '#/components/schemas/PageMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /v1/company/usage:
    get:
      tags:
      - companies
      operationId: getUsage
      summary: Contadores de uso del periodo
      description: Medición mensual de uso de la empresa (documentos emitidos, requests de API, emails
        enviados). Visible al cliente.
      x-abilities:
      - reports:read
      parameters:
      - name: period
        in: query
        schema:
          type: string
          pattern: ^[0-9]{6}$
        description: Periodo YYYYMM. Por defecto, el mes en curso (zona America/Lima).
        example: '202607'
      responses:
        '200':
          description: Contadores del periodo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageCounters'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /v1/series:
    get:
      tags:
      - series
      operationId: listSeries
      summary: Listar series
      x-abilities:
      - series:manage
      parameters:
      - $ref: '#/components/parameters/After'
      - $ref: '#/components/parameters/Limit'
      - name: tipo_doc
        in: query
        schema:
          $ref: '#/components/schemas/TipoDocSerie'
      - name: branch_id
        in: query
        schema:
          type: string
      - name: incluir_desactivadas
        in: query
        schema:
          type: boolean
          default: false
      responses:
        '200':
          description: Series de la empresa.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Series'
                  meta:
                    $ref: '#/components/schemas/PageMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
      - series
      operationId: createSeries
      summary: Crear serie
      description: 'Crea una serie para un tipo de documento, con validación de formato

        SUNAT (F### factura, B### boleta, T### guía, etc.). `start_at` permite

        arrancar el correlativo en un número dado (migración desde otro

        sistema). El correlativo se asigna transaccionalmente en el commit de

        emisión — sin huecos.

        '
      x-abilities:
      - series:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SeriesInput'
      responses:
        '201':
          description: Serie creada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Series'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/series/{id}:
    parameters:
    - $ref: '#/components/parameters/ResourceId'
    get:
      tags:
      - series
      operationId: getSeries
      summary: Obtener serie
      x-abilities:
      - series:manage
      responses:
        '200':
          description: Serie.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Series'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags:
      - series
      operationId: patchSeries
      summary: Actualizar serie (estado, sucursal, correlativo inicial)
      description: Desactiva/reactiva la serie, cambia su sucursal (anexo) y/o corrige el correlativo
        inicial. Sin borrado físico — los documentos referencian la serie. Una serie desactivada rechaza
        nuevas emisiones con `422 series_disabled`. El correlativo inicial (`start_at`) solo puede cambiarse
        antes de la primera emisión (migración desde otro sistema); si la serie ya emitió responde `422`.
      x-abilities:
      - series:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                disabled:
                  type: boolean
                  description: '`true` desactiva la serie; `false` la reactiva.'
                branch_id:
                  type:
                  - string
                  - 'null'
                  description: Sucursal (anexo) a la que se asocia la serie; `null` la deja a nivel empresa.
                start_at:
                  type: integer
                  minimum: 1
                  description: Correlativo inicial. Solo editable si la serie aún no emitió (correlativo_actual
                    = 0).
      responses:
        '200':
          description: Serie actualizada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Series'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/clients:
    get:
      tags:
      - clients
      operationId: listClients
      summary: Listar / buscar clientes
      description: Búsqueda por documento exacto (`document=6-20601234567` o solo el número) y por nombre
        (`q=`) para autocompletar y sincronización de ERPs.
      x-abilities:
      - clients:read
      parameters:
      - $ref: '#/components/parameters/After'
      - $ref: '#/components/parameters/Limit'
      - name: document
        in: query
        schema:
          type: string
        description: Número de documento exacto, opcionalmente prefijado por el tipo (`6-20601234567`).
        example: 20601234567
      - name: q
        in: query
        schema:
          type: string
        description: Búsqueda por razón social / nombre.
        example: los andes
      - name: incluir_archivados
        in: query
        schema:
          type: boolean
          default: false
      responses:
        '200':
          description: Clientes.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Client'
                  meta:
                    $ref: '#/components/schemas/PageMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
      - clients
      operationId: createClient
      summary: Crear cliente
      description: Único por (tipo_doc, num_doc) dentro de la empresa.
      x-abilities:
      - clients:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClientInput'
      responses:
        '201':
          description: Cliente creado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Client'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/clients/{id}:
    parameters:
    - $ref: '#/components/parameters/ResourceId'
    get:
      tags:
      - clients
      operationId: getClient
      summary: Obtener cliente
      x-abilities:
      - clients:read
      responses:
        '200':
          description: Cliente.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Client'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - clients
      operationId: updateClient
      summary: Actualizar cliente
      description: Cambiar el padrón jamás altera documentos ya emitidos (el documento guarda un snapshot
        del cliente).
      x-abilities:
      - clients:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClientInput'
      responses:
        '200':
          description: Cliente actualizado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Client'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
    delete:
      tags:
      - clients
      operationId: archiveClient
      summary: Archivar cliente
      description: Archiva el cliente (`archived_at`). No hay borrado físico — documentos emitidos lo
        referencian.
      x-abilities:
      - clients:write
      responses:
        '204':
          description: Cliente archivado.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/clients/by-document:
    put:
      tags:
      - clients
      operationId: upsertClientByDocument
      summary: Upsert de cliente por documento
      description: Crea o actualiza el cliente identificado por (tipo_doc, num_doc). Pensado para integradores
        que sincronizan su maestro.
      x-abilities:
      - clients:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClientInput'
      responses:
        '200':
          description: Cliente actualizado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Client'
        '201':
          description: Cliente creado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Client'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/clients/batch:
    post:
      tags:
      - clients
      operationId: batchUpsertClients
      summary: Batch-upsert de clientes
      description: Upsert masivo por (tipo_doc, num_doc). Máximo 500 clientes por request. Responde el
        resultado por fila — las filas válidas se aplican aunque otras fallen.
      x-abilities:
      - clients:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - clients
              properties:
                clients:
                  type: array
                  maxItems: 500
                  items:
                    $ref: '#/components/schemas/ClientInput'
      responses:
        '200':
          description: Resultado del batch.
          content:
            application/json:
              schema:
                type: object
                required:
                - created
                - updated
                - errors
                properties:
                  created:
                    type: integer
                    example: 12
                  updated:
                    type: integer
                    example: 87
                  errors:
                    type: array
                    items:
                      type: object
                      required:
                      - index
                      - message
                      properties:
                        index:
                          type: integer
                          example: 41
                        message:
                          type: string
                          example: 'num_doc: RUC inválido (dígito verificador)'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/catalogs:
    get:
      tags:
      - catalog
      operationId: listCatalogs
      summary: Índice de catálogos SUNAT estáticos
      description: Lista los catálogos SUNAT disponibles (fuente única de verdad del backend). `closed=true`
        = la UI solo debe permitir códigos de la lista; `false` = lista de los más usados, se acepta también
        otro código válido.
      responses:
        '200':
          description: Catálogos disponibles.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      required:
                      - id
                      - nombre
                      - closed
                      properties:
                        id:
                          type: string
                          example: detraccion
                        nombre:
                          type: string
                        closed:
                          type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/catalogs/{id}:
    get:
      tags:
      - catalog
      operationId: getCatalog
      summary: Un catálogo SUNAT estático (código→descripción)
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Slug del catálogo (afectacion-igv, motivo-nota-credito, motivo-nota-debito, medio-pago,
          tipo-operacion, unidad-medida, detraccion, cargo, descuento, regimen-percepcion, tipo-doc-identidad).
      responses:
        '200':
          description: Catálogo.
          content:
            application/json:
              schema:
                type: object
                required:
                - id
                - nombre
                - closed
                - items
                properties:
                  id:
                    type: string
                  nombre:
                    type: string
                  closed:
                    type: boolean
                  items:
                    type: array
                    items:
                      type: object
                      required:
                      - code
                      - label
                      properties:
                        code:
                          type: string
                        label:
                          type: string
                  simbolos:
                    type: object
                    description: 'Solo unidad-medida: símbolo comercial editable por código (NIU→"UND").'
                    additionalProperties:
                      type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/catalogs/product-codes:
    get:
      tags:
      - catalog
      operationId: searchProductCodes
      summary: Buscar código de producto SUNAT (Catálogo 25 / UNSPSC)
      description: Busca en el Catálogo 25 — Código de Producto SUNAT (UNSPSC v26_0801). Con `q` numérico
        filtra por prefijo de código; en otro caso por descripción. Para el buscador del formulario de
        productos. Obligatorio para bienes/servicios afectos desde el 2026-08-01.
      parameters:
      - name: q
        in: query
        required: false
        schema:
          type: string
        description: Texto o prefijo de código a buscar.
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          default: 20
          maximum: 50
        description: Máximo de resultados (1..50).
      responses:
        '200':
          description: Coincidencias del catálogo.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      required:
                      - code
                      - descripcion
                      properties:
                        code:
                          type: string
                          example: '43211503'
                        descripcion:
                          type: string
                          example: Computadores notebook
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/catalog-items:
    get:
      tags:
      - catalog
      operationId: listCatalogItems
      summary: Listar items del catálogo
      x-abilities:
      - documents:read
      parameters:
      - $ref: '#/components/parameters/After'
      - $ref: '#/components/parameters/Limit'
      - name: q
        in: query
        schema:
          type: string
        description: Búsqueda por código o descripción.
      - name: incluir_archivados
        in: query
        schema:
          type: boolean
          default: false
      responses:
        '200':
          description: Items del catálogo.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CatalogItem'
                  meta:
                    $ref: '#/components/schemas/PageMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
      - catalog
      operationId: createCatalogItem
      summary: Crear item de catálogo
      description: Comodidad de emisión — SIN inventario ni stock (exclusión de producto). Único por código
        dentro de la empresa.
      x-abilities:
      - documents:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CatalogItemInput'
      responses:
        '201':
          description: Item creado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CatalogItem'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/catalog-items/{id}:
    parameters:
    - $ref: '#/components/parameters/ResourceId'
    get:
      tags:
      - catalog
      operationId: getCatalogItem
      summary: Obtener item de catálogo
      x-abilities:
      - documents:read
      responses:
        '200':
          description: Item.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CatalogItem'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - catalog
      operationId: updateCatalogItem
      summary: Actualizar item de catálogo
      description: Los documentos emitidos guardan snapshot de los items — cambiar el catálogo jamás altera
        documentos.
      x-abilities:
      - documents:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CatalogItemInput'
      responses:
        '200':
          description: Item actualizado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CatalogItem'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
    delete:
      tags:
      - catalog
      operationId: archiveCatalogItem
      summary: Archivar item de catálogo
      x-abilities:
      - documents:write
      responses:
        '204':
          description: Item archivado.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/documents:
    post:
      tags:
      - documents
      operationId: createDocumentF2
      summary: Emitir comprobante — extensión F2 (20/40/04 y plantillas)
      description: "`POST /v1/documents` acepta desde F2 tres tipos adicionales en\n`tipo_doc` (obligatorio\
        \ explícito para 20/40):\n\n- **`20` Comprobante de Retención**: serie `R###` + bloque\n  `retencion`\
        \ (régimen catálogo 23; tasa 3% derivada, montos por\n  detalle calculados si se omiten). El proveedor\
        \ debe tener RUC.\n  No admite `?draft=true`. Viaja por sendBill contra el endpoint\n  SOAP de\
        \ \"otros CPE\".\n- **`40` Comprobante de Percepción**: serie alfanumérica de 4 +\n  bloque `percepcion_cpe`\
        \ (régimen catálogo 22: 01=2%, 02=1%,\n  03=0.5%). Mismas reglas que 20.\n- **`04` Liquidación\
        \ de Compra**: usa los campos de venta (items,\n  totales, cálculo asistido) con serie `E###`,\
        \ cliente persona\n  natural con DNI y bloque opcional `liquidacion` (vencimiento,\n  retención\
        \ IR 1.5%/4%). Admite borradores.\n\n`?template_id=` (F2): usa una plantilla de `/v1/document-templates`\n\
        como base; el body se aplica encima (merge superficial por campo de\nprimer nivel; `items` vacío\
        \ no pisa los de la plantilla).\n\nEl contador de uso de documentos emitidos se incrementa en\
        \ la\nMISMA transacción de emisión (exacto bajo concurrencia).\n"
      x-abilities:
      - documents:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      - name: template_id
        in: query
        schema:
          type: string
        description: Plantilla base (document_templates) sobre la que se aplica el body.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentInputF2'
            examples:
              retencion:
                summary: Comprobante de Retención R001 (régimen 01, 3%)
                value:
                  tipo_doc: '20'
                  serie: R001
                  cliente:
                    tipo_doc: '6'
                    num_doc: '20601234567'
                    razon_social: PROVEEDOR ANDINO S.A.C.
                  retencion:
                    regimen: '01'
                    detalles:
                    - tipo_doc: '01'
                      numero: F001-1
                      fecha_emision: '2026-07-01'
                      importe_total: 210.0
              percepcion:
                summary: Comprobante de Percepción P001 (régimen 01, 2%)
                value:
                  tipo_doc: '40'
                  serie: P001
                  cliente:
                    tipo_doc: '6'
                    num_doc: '20601234567'
                    razon_social: CLIENTE PERCIBIDO S.A.C.
                  percepcion_cpe:
                    regimen: '01'
                    detalles:
                    - numero: F001-2
                      fecha_emision: '2026-07-01'
                      importe_total: 200.0
              liquidacion:
                summary: Liquidación de compra E001 con retención IR 1.5%
                value:
                  tipo_doc: '04'
                  serie: E001
                  cliente:
                    tipo_doc: '1'
                    num_doc: '42345678'
                    razon_social: JUAN QUISPE MAMANI
                  items:
                  - descripcion: Papa nativa
                    unidad: KGM
                    cantidad: 500
                    valor_unitario: 2.2
                    tip_afe_igv: '10'
                  liquidacion:
                    retencion_ir:
                      tasa: 1.5
      responses:
        '201':
          description: Documento firmado y encolado (`estado=signed`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Document'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
    get:
      tags:
      - documents
      operationId: listDocuments
      summary: Listar documentos (filtros ricos)
      description: Listado con cursor pagination, estable bajo inserciones concurrentes. Todos los filtros
        son combinables.
      x-abilities:
      - documents:read
      parameters:
      - $ref: '#/components/parameters/After'
      - $ref: '#/components/parameters/Limit'
      - name: fecha_desde
        in: query
        schema:
          type: string
          format: date
        description: Fecha de emisión desde (inclusive).
      - name: fecha_hasta
        in: query
        schema:
          type: string
          format: date
        description: Fecha de emisión hasta (inclusive).
      - name: tipo_doc
        in: query
        schema:
          $ref: '#/components/schemas/TipoDocVenta'
      - name: estado
        in: query
        schema:
          $ref: '#/components/schemas/EstadoDocumento'
        description: Estado del ciclo de vida. `contingency` lista el panel de pendientes por caída SUNAT.
      - name: serie
        in: query
        schema:
          type: string
        example: F001
      - name: numero
        in: query
        schema:
          type: string
        description: Número completo (`F001-123`).
      - name: client_id
        in: query
        schema:
          type: string
      - name: client_num_doc
        in: query
        schema:
          type: string
        example: '20601234567'
      - name: moneda
        in: query
        schema:
          type: string
          example: PEN
      - name: total_min
        in: query
        schema:
          type: number
        description: Importe total mínimo (inclusive).
      - name: total_max
        in: query
        schema:
          type: number
        description: Importe total máximo (inclusive).
      - name: external_reference
        in: query
        schema:
          type: string
        description: Referencia externa exacta del integrador.
      responses:
        '200':
          description: Documentos (descendente por fecha de emisión e id).
          headers:
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimit-Limit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimit-Remaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimit-Reset'
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Document'
                  meta:
                    $ref: '#/components/schemas/PageMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/documents:preview:
    post:
      tags:
      - documents
      operationId: previewDocument
      summary: Previsualizar cálculo de totales
      description: 'Ejecuta el **cálculo asistido** sin emitir: recibe el mismo body que

        `POST /v1/documents`, calcula/valida líneas y totales (IGV 18%, ISC,

        ICBPER, IVAP, redondeo) y devuelve el resultado con advertencias. No

        persiste nada, no consume correlativo ni requiere idempotencia.

        '
      x-abilities:
      - documents:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentInput'
      responses:
        '200':
          description: Totales calculados/validados.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentPreview'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/documents/{id}:
    parameters:
    - $ref: '#/components/parameters/ResourceId'
    get:
      tags:
      - documents
      operationId: getDocument
      summary: Obtener documento
      description: Lectura consistente — un GET inmediatamente después del POST siempre ve el documento.
      x-abilities:
      - documents:read
      responses:
        '200':
          description: Documento.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Document'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - documents
      operationId: updateDraftDocument
      summary: Editar borrador
      description: Solo documentos en `estado=draft` son editables. Cualquier otro estado responde `409
        invalid_state` (los documentos emitidos son inmutables/legales).
      x-abilities:
      - documents:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentInput'
      responses:
        '200':
          description: Borrador actualizado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Document'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/documents/{id}/void:
    post:
      tags:
      - documents
      operationId: voidDocument
      summary: Dar de baja (RA/RR)
      description: 'Solicita la baja del documento ante SUNAT con motivo: facturas y notas

        van por Comunicación de Baja (RA); boletas ya informadas en resumen van

        por Reversión (RR). El sistema crea/agrupa el summary correspondiente,

        lo envía y hace polling del ticket hasta estado final. Solo documentos

        `accepted` / `accepted_with_obs` son dables de baja (`409

        invalid_state` en caso contrario — p. ej. void de un draft).

        '
      x-abilities:
      - documents:void
      parameters:
      - $ref: '#/components/parameters/ResourceId'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - motivo
              properties:
                motivo:
                  type: string
                  maxLength: 100
                  example: Error en datos del cliente
      responses:
        '202':
          description: Baja encolada. Devuelve el summary (RA/RR) que la procesa.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Summary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/documents/{id}/retry:
    post:
      tags:
      - documents
      operationId: retryDocument
      summary: Reintentar envío (solo fallos técnicos)
      description: Re-encola un documento en `estado=error` (reintentos técnicos agotados). Guarded —
        un documento rechazado por SUNAT (error de negocio) no es reintentable y responde `409 invalid_state`.
      x-abilities:
      - documents:write
      parameters:
      - $ref: '#/components/parameters/ResourceId'
      responses:
        '202':
          description: Documento re-encolado (`estado=signed`, esperando envío).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Document'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/documents/{id}/events:
    get:
      tags:
      - documents
      operationId: listDocumentEvents
      summary: Timeline del documento
      description: Bitácora append-only del documento — creación, firma, envíos, CDR, emails, webhooks,
        baja.
      x-abilities:
      - documents:read
      parameters:
      - $ref: '#/components/parameters/ResourceId'
      - $ref: '#/components/parameters/After'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Eventos en orden cronológico.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DocumentEvent'
                  meta:
                    $ref: '#/components/schemas/PageMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/documents/{id}/files/{kind}:
    get:
      tags:
      - files
      operationId: getDocumentFile
      summary: Descargar archivo del documento
      description: 'Descarga el XML firmado (copia legal inmutable), el CDR (zip) o la

        representación impresa PDF con QR. El PDF se genera perezosamente y se

        cachea en storage; `?format=` elige la plantilla (A4, A5, ticket 80mm o

        50mm). Retención legal de 5 años. La cabecera `X-Content-Sha256`

        trae el hash del archivo.

        '
      x-abilities:
      - files:read
      parameters:
      - $ref: '#/components/parameters/ResourceId'
      - name: kind
        in: path
        required: true
        schema:
          type: string
          enum:
          - xml
          - cdr
          - pdf
      - name: format
        in: query
        schema:
          type: string
          enum:
          - a4
          - a5
          - 80mm
          - 50mm
          default: a4
        description: Formato de la representación impresa. Solo aplica a `kind=pdf`.
      responses:
        '200':
          description: Contenido del archivo.
          headers:
            X-Content-Sha256:
              description: SHA-256 hex del archivo.
              schema:
                type: string
          content:
            application/xml:
              schema:
                type: string
                description: XML UBL 2.1 firmado.
            application/zip:
              schema:
                type: string
                format: binary
                description: CDR comprimido tal como lo devolvió SUNAT.
            application/pdf:
              schema:
                type: string
                format: binary
                description: Representación impresa con QR.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Documento inexistente, o archivo aún no disponible (p. ej. CDR antes de la respuesta
            SUNAT).
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/documents/{id}/send-email:
    post:
      tags:
      - distribution
      operationId: sendDocumentEmail
      summary: Enviar comprobante por email
      description: Envía al cliente el comprobante con XML+PDF+CDR adjuntos, vía SMTP del tenant o SMTP
        de la plataforma. El envío es asíncrono y queda en el log de envíos (timeline del documento).
        Si el SMTP falla, el documento NO se afecta.
      x-abilities:
      - files:read
      parameters:
      - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                to:
                  type: array
                  items:
                    type: string
                    format: email
                  description: Destinatarios. Por defecto, el email del snapshot del cliente.
                  example:
                  - facturacion@losandes.pe
                cc:
                  type: array
                  items:
                    type: string
                    format: email
                message:
                  type: string
                  description: Mensaje adicional para el cuerpo del email.
      responses:
        '202':
          description: Email encolado.
          content:
            application/json:
              schema:
                type: object
                required:
                - status
                - to
                properties:
                  status:
                    type: string
                    enum:
                    - queued
                    example: queued
                  to:
                    type: array
                    items:
                      type: string
                      format: email
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/documents/{id}/portal-link:
    post:
      tags:
      - distribution
      operationId: createPortalLink
      summary: Crear enlace de portal público
      description: Genera un enlace firmado (expira, con contadores de acceso) para que el cliente final
        descargue XML/PDF/CDR sin login, vía `GET /p/{token}`.
      x-abilities:
      - files:read
      parameters:
      - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                expires_in_hours:
                  type: integer
                  minimum: 1
                  maximum: 8760
                  default: 720
                  description: Vigencia del enlace en horas (default 30 días).
                max_accesses:
                  type:
                  - integer
                  - 'null'
                  minimum: 1
                  description: Máximo de accesos permitidos (null = ilimitado).
      responses:
        '201':
          description: Enlace creado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortalLink'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
  /v1/summaries:
    post:
      tags:
      - summaries
      operationId: createSummary
      summary: Crear resumen diario / baja manual
      description: 'Crea manualmente un Resumen Diario de boletas (RC), una Comunicación de

        Baja (RA) o una Reversión (RR). El resumen diario también se genera

        automáticamente por el worker nocturno (configurable por empresa). El

        envío es asíncrono: SUNAT devuelve un ticket que el sistema sondea con

        backoff hasta estado final.

        '
      x-abilities:
      - summaries:manage
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SummaryInput'
            examples:
              resumen_diario:
                summary: Resumen diario de boletas de ayer
                value:
                  tipo: RC
                  fecha_referencia: '2026-07-17'
                  documentos:
                  - document_id: 01J2ZKA4T6V8X0Z2B4D6F8H0J2
                    accion: add
                  - document_id: 01J2ZKB5U7W9Y1A3C5E7G9I1K3
                    accion: add
      responses:
        '202':
          description: Resumen creado y encolado para envío.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Summary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
    get:
      tags:
      - summaries
      operationId: listSummaries
      summary: Listar resúmenes y bajas
      x-abilities:
      - summaries:manage
      parameters:
      - $ref: '#/components/parameters/After'
      - $ref: '#/components/parameters/Limit'
      - name: tipo
        in: query
        schema:
          type: string
          enum:
          - RC
          - RA
          - RR
      - name: estado
        in: query
        schema:
          $ref: '#/components/schemas/EstadoSummary'
      - name: fecha_desde
        in: query
        schema:
          type: string
          format: date
        description: Fecha de referencia desde (inclusive).
      - name: fecha_hasta
        in: query
        schema:
          type: string
          format: date
        description: Fecha de referencia hasta (inclusive).
      responses:
        '200':
          description: Resúmenes.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Summary'
                  meta:
                    $ref: '#/components/schemas/PageMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /v1/summaries/{id}:
    parameters:
    - $ref: '#/components/parameters/ResourceId'
    get:
      tags:
      - summaries
      operationId: getSummary
      summary: Obtener resumen / baja
      x-abilities:
      - summaries:manage
      responses:
        '200':
          description: Resumen con su estado SUNAT y documentos incluidos.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Summary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/despatches:
    post:
      tags:
      - despatches
      operationId: createDespatch
      summary: Emitir guía de remisión remitente (09)
      description: 'Emite una Guía de Remisión Electrónica remitente formato 2022 (API REST

        con ticket): transporte público o privado, vehículos principal y

        secundarios, choferes, indicadores de traslado, peso, contenedores y

        puertos. El envío es asíncrono — el sistema hace polling del ticket GRE

        y persiste el CDR. Idempotencia obligatoria.

        '
      x-abilities:
      - despatches:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DespatchInput'
            examples:
              transporte_publico:
                summary: GRE con transportista (modalidad 01)
                value:
                  serie: T001
                  fecha_emision: '2026-07-18'
                  destinatario:
                    tipo_doc: '6'
                    num_doc: '20601234567'
                    razon_social: COMERCIAL LOS ANDES S.A.C.
                  envio:
                    cod_traslado: '01'
                    mod_traslado: '01'
                    fecha_traslado: '2026-07-19'
                    peso_total: 425.5
                    und_peso_total: KGM
                    num_bultos: 10
                    transportista:
                      tipo_doc: '6'
                      num_doc: '20512345678'
                      razon_social: TRANSPORTES RÁPIDOS DEL SUR S.A.
                      nro_mtc: 0001-MTC/15.01
                    partida:
                      ubigeo: '150101'
                      direccion: Jr. Comercio 456, Cercado de Lima
                    llegada:
                      ubigeo: '040101'
                      direccion: Av. Ejército 789, Arequipa
                  items:
                  - codigo: P001
                    descripcion: Cemento Portland tipo I x 42.5 kg
                    unidad: NIU
                    cantidad: 10
                  external_reference: GR-2026-000731
      responses:
        '202':
          description: Guía firmada y encolada (`estado=signed`); el resultado SUNAT llega por webhook
            o consulta.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Despatch'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
    get:
      tags:
      - despatches
      operationId: listDespatches
      summary: Listar guías de remisión
      x-abilities:
      - documents:read
      parameters:
      - $ref: '#/components/parameters/After'
      - $ref: '#/components/parameters/Limit'
      - name: fecha_desde
        in: query
        schema:
          type: string
          format: date
      - name: fecha_hasta
        in: query
        schema:
          type: string
          format: date
      - name: estado
        in: query
        schema:
          $ref: '#/components/schemas/EstadoDocumento'
      - name: serie
        in: query
        schema:
          type: string
          example: T001
      - name: external_reference
        in: query
        schema:
          type: string
      responses:
        '200':
          description: Guías de remisión.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Despatch'
                  meta:
                    $ref: '#/components/schemas/PageMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /v1/despatches/{id}:
    parameters:
    - $ref: '#/components/parameters/ResourceId'
    get:
      tags:
      - despatches
      operationId: getDespatch
      summary: Obtener guía de remisión
      x-abilities:
      - documents:read
      responses:
        '200':
          description: Guía con su estado SUNAT (ticket GRE, CDR).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Despatch'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/webhooks:
    get:
      tags:
      - webhooks
      operationId: listWebhooks
      summary: Listar webhooks
      x-abilities:
      - webhooks:manage
      responses:
        '200':
          description: Endpoints de webhook de la empresa.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookEndpoint'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
      - webhooks
      operationId: createWebhook
      summary: Crear webhook
      description: 'Registra un endpoint para eventos versionados (`document.accepted.v1`,

        `document.rejected.v1`, `document.voided.v1`, `summary.completed.v1`,

        `despatch.accepted.v1`, `certificate.expiring.v1`, …). Cada entrega va

        firmada con HMAC-SHA256 (`X-Webhook-Signature`) y timestamp anti-replay

        (`X-Webhook-Timestamp`); reintentos con backoff `1m,5m,30m,2h,6h,24h`

        hasta `dead`. El `secret` solo se devuelve UNA vez, en esta respuesta.

        '
      x-abilities:
      - webhooks:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEndpointInput'
      responses:
        '201':
          description: Webhook creado. Incluye el `secret` (única vez que se muestra).
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/WebhookEndpoint'
                - type: object
                  required:
                  - secret
                  properties:
                    secret:
                      type: string
                      description: Secreto HMAC-SHA256. Guárdelo — no vuelve a mostrarse.
                      example: whsec_9f2c7d1e8a4b6c3f5d0e2a7b
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/webhooks/{id}:
    parameters:
    - $ref: '#/components/parameters/ResourceId'
    get:
      tags:
      - webhooks
      operationId: getWebhook
      summary: Obtener webhook
      x-abilities:
      - webhooks:manage
      responses:
        '200':
          description: Webhook.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - webhooks
      operationId: updateWebhook
      summary: Actualizar webhook
      description: Cambiar URL, eventos suscritos o estado activo. Reactivar un endpoint `dead` reinicia
        su racha de fallos.
      x-abilities:
      - webhooks:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEndpointInput'
      responses:
        '200':
          description: Webhook actualizado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
    delete:
      tags:
      - webhooks
      operationId: deleteWebhook
      summary: Eliminar webhook
      x-abilities:
      - webhooks:manage
      responses:
        '204':
          description: Webhook eliminado.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/webhooks/{id}/test:
    post:
      tags:
      - webhooks
      operationId: testWebhook
      summary: Enviar evento de prueba
      description: Dispara un evento `webhook.test.v1` firmado al endpoint y devuelve el resultado del
        intento en línea.
      x-abilities:
      - webhooks:manage
      parameters:
      - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Resultado del intento de prueba.
          content:
            application/json:
              schema:
                type: object
                required:
                - delivered
                properties:
                  delivered:
                    type: boolean
                    example: true
                  response_code:
                    type:
                    - integer
                    - 'null'
                    example: 200
                  latency_ms:
                    type: integer
                    example: 187
                  error:
                    type:
                    - string
                    - 'null'
                    example: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/webhooks/{id}/deliveries:
    get:
      tags:
      - webhooks
      operationId: listWebhookDeliveries
      summary: Log de entregas del webhook
      description: Historial de intentos de entrega (payload, código de respuesta, reintentos programados)
        para depuración del integrador.
      x-abilities:
      - webhooks:manage
      parameters:
      - $ref: '#/components/parameters/ResourceId'
      - $ref: '#/components/parameters/After'
      - $ref: '#/components/parameters/Limit'
      - name: status
        in: query
        schema:
          type: string
          enum:
          - pending
          - delivered
          - failed
          - dead
      - name: event_type
        in: query
        schema:
          type: string
          example: document.accepted.v1
      responses:
        '200':
          description: Entregas (descendente por fecha).
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookDelivery'
                  meta:
                    $ref: '#/components/schemas/PageMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/api-tokens:
    get:
      tags:
      - api-tokens
      operationId: listApiTokens
      summary: Listar tokens de API
      x-abilities:
      - company:manage
      responses:
        '200':
          description: Tokens de la empresa (solo prefijo visible, jamás el token completo).
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApiToken'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
      - api-tokens
      operationId: createApiToken
      summary: Crear token de API
      description: Crea un token con abilities granulares y expiración opcional. En DB solo se guarda
        el hash; el token completo se devuelve UNA sola vez en esta respuesta.
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiTokenInput'
      responses:
        '201':
          description: Token creado. Incluye el token en claro (única vez).
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/ApiToken'
                - type: object
                  required:
                  - token
                  properties:
                    token:
                      type: string
                      description: Token completo. Guárdelo — no vuelve a mostrarse.
                      example: ecp_live_9f2c7d1e8a4b6c3f5d0e2a7b9c1d4e6f
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/api-tokens/{id}:
    parameters:
    - $ref: '#/components/parameters/ResourceId'
    get:
      tags:
      - api-tokens
      operationId: getApiToken
      summary: Obtener token de API
      x-abilities:
      - company:manage
      responses:
        '200':
          description: Token (metadatos; jamás el valor).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiToken'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags:
      - api-tokens
      operationId: revokeApiToken
      summary: Revocar token de API
      description: Revocación inmediata (`revoked_at`). Los requests posteriores con ese token reciben
        `401 unauthenticated`.
      x-abilities:
      - company:manage
      responses:
        '204':
          description: Token revocado.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/exports/documents:
    post:
      tags:
      - exports
      operationId: exportDocuments
      summary: Exportar listado de documentos (CSV/Excel)
      description: Export asíncrono del listado filtrado (mismos filtros que `GET /v1/documents`). Nunca
        ata la conexión HTTP a un trabajo largo — se consulta con `GET /v1/exports/{id}`.
      x-abilities:
      - reports:read
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - formato
              properties:
                formato:
                  type: string
                  enum:
                  - csv
                  - xlsx
                filtros:
                  type: object
                  description: Mismos filtros del listado (`fecha_desde`, `fecha_hasta`, `tipo_doc`, `estado`,
                    `serie`, `client_num_doc`, `moneda`, `total_min`, `total_max`, `external_reference`).
                  additionalProperties: true
                  example:
                    fecha_desde: '2026-07-01'
                    fecha_hasta: '2026-07-31'
                    tipo_doc: '01'
                    estado: accepted
      responses:
        '202':
          description: Export encolado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Export'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/exports/files:
    post:
      tags:
      - exports
      operationId: exportFiles
      summary: Exportar archivos por rango (zip)
      description: Genera un zip con XML/CDR/PDF de los documentos del rango (retención legal 5 años).
        Asíncrono — se consulta con `GET /v1/exports/{id}`.
      x-abilities:
      - files:read
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - fecha_desde
              - fecha_hasta
              properties:
                fecha_desde:
                  type: string
                  format: date
                  example: '2026-01-01'
                fecha_hasta:
                  type: string
                  format: date
                  example: '2026-06-30'
                tipos_doc:
                  type: array
                  items:
                    $ref: '#/components/schemas/TipoDocVenta'
                  description: Limitar a ciertos tipos de documento.
                kinds:
                  type: array
                  items:
                    type: string
                    enum:
                    - xml
                    - cdr
                    - pdf
                  description: Tipos de archivo a incluir (default todos los disponibles).
      responses:
        '202':
          description: Export encolado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Export'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/exports/{id}:
    parameters:
    - $ref: '#/components/parameters/ResourceId'
    get:
      tags:
      - exports
      operationId: getExport
      summary: Consultar export (polling)
      description: Estado del export; cuando `status=done`, `download_url` entrega el archivo (URL temporal,
        el export expira y se limpia automáticamente).
      responses:
        '200':
          description: Export.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Export'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/openapi.yaml:
    get:
      tags:
      - meta
      operationId: getOpenApiSpec
      summary: Contrato OpenAPI de esta API
      description: Devuelve este documento OpenAPI 3.1 (fuente de la Postman collection y los SDKs).
      security: []
      responses:
        '200':
          description: Especificación OpenAPI.
          content:
            application/yaml:
              schema:
                type: string
  /p/{token}:
    get:
      tags:
      - meta
      operationId: getPortalPage
      summary: Portal público del documento
      description: 'Página pública (sin login) del comprobante, accesible con el enlace

        firmado creado por `POST /v1/documents/{id}/portal-link`. Permite ver el

        documento y descargar XML/PDF/CDR mientras el enlace no expire ni

        agote sus accesos. Limitado por IP contra abuso.

        '
      security: []
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
        description: Token opaco del enlace firmado.
      - name: file
        in: query
        required: false
        schema:
          type: string
          enum:
          - xml
          - cdr
          - pdf
        description: Descargar un archivo del documento en lugar de la vista.
      responses:
        '200':
          description: 'Vista del documento. Por defecto `text/html` (página autocontenida); con `Accept:
            application/json` devuelve los datos del comprobante para renderizarlos con marca propia.'
          content:
            text/html:
              schema:
                type: string
            application/json:
              schema:
                type: object
                required:
                - document_id
                - numero
                - tipo_doc
                - fecha_emision
                - moneda
                - importe_total
                - estado
                - emisor_razon
                - emisor_ruc
                - cliente_razon
                - cliente_doc
                - files
                properties:
                  document_id:
                    type: string
                  numero:
                    type: string
                    example: F001-123
                  tipo_doc:
                    type: string
                    example: '01'
                  fecha_emision:
                    type: string
                    example: '2026-07-19'
                  moneda:
                    type: string
                    example: PEN
                  importe_total:
                    type: string
                    example: '236.00'
                  estado:
                    type: string
                    example: accepted
                  emisor_razon:
                    type: string
                  emisor_ruc:
                    type: string
                    example: '20123456789'
                  cliente_razon:
                    type: string
                  cliente_doc:
                    type: string
                  files:
                    type: array
                    items:
                      type: string
                      enum:
                      - xml
                      - cdr
                      - pdf
        '404':
          description: Enlace inexistente, expirado, revocado o con accesos agotados.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/documents/{id}/duplicate:
    parameters:
    - $ref: '#/components/parameters/PathID'
    post:
      tags:
      - documents
      operationId: duplicateDocument
      summary: Duplicar documento como input editable
      description: 'Devuelve el `DocumentInput` canónico del documento (su payload de

        emisión) listo para editar y volver a emitir: sin `fecha_emision`

        (el duplicado se emite "hoy") ni `external_reference`. **No emite

        nada** — el cliente decide cuándo mandar el input a

        `POST /v1/documents` con su propia `X-Idempotency-Key`.

        '
      x-abilities:
      - documents:write
      responses:
        '200':
          description: Input listo para emitir.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentInputF2'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/documents/{id}/status/stream:
    parameters:
    - $ref: '#/components/parameters/PathID'
    get:
      tags:
      - documents
      operationId: streamDocumentStatus
      summary: Estado del documento en vivo (SSE)
      description: "Stream `text/event-stream` (Server-Sent Events) del estado del\ndocumento para UI\
        \ e integradores:\n\n- Evento `estado` con el snapshot (`DocumentStatusEvent`) al abrir\n  y en\
        \ cada cambio de estado.\n- Heartbeat comentario (`: ping`) cada 15 segundos.\n- Cierre automático\
        \ al llegar a estado final (`accepted`,\n  `accepted_with_obs`, `rejected`, `error`, `voided`)\
        \ o a los\n  5 minutos (el cliente puede reconectar).\n"
      x-abilities:
      - documents:read
      responses:
        '200':
          description: Stream SSE de eventos `estado`.
          content:
            text/event-stream:
              schema:
                type: string
                description: 'Frames SSE: `event: estado` + `data: DocumentStatusEvent` JSON.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/document-templates:
    get:
      tags:
      - documents
      operationId: listDocumentTemplates
      summary: Listar plantillas de emisión
      x-abilities:
      - documents:read
      responses:
        '200':
          description: Plantillas de la empresa (orden por nombre).
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/DocumentTemplate'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags:
      - documents
      operationId: createDocumentTemplate
      summary: Crear plantilla de emisión
      description: Nombre único por empresa; el payload es un `DocumentInput` parcial o completo.
      x-abilities:
      - documents:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentTemplateInput'
      responses:
        '201':
          description: Plantilla creada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentTemplate'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/document-templates/{id}:
    parameters:
    - $ref: '#/components/parameters/PathID'
    get:
      tags:
      - documents
      operationId: getDocumentTemplate
      summary: Obtener plantilla
      x-abilities:
      - documents:read
      responses:
        '200':
          description: Plantilla.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentTemplate'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags:
      - documents
      operationId: updateDocumentTemplate
      summary: Reemplazar plantilla
      x-abilities:
      - documents:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentTemplateInput'
      responses:
        '200':
          description: Plantilla actualizada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentTemplate'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
    delete:
      tags:
      - documents
      operationId: deleteDocumentTemplate
      summary: Eliminar plantilla
      description: Si documentos programados (F3) la referencian, responde `409 invalid_state`.
      x-abilities:
      - documents:write
      responses:
        '204':
          description: Plantilla eliminada.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
  /v1/summaries/{id}/revert:
    parameters:
    - $ref: '#/components/parameters/PathID'
    post:
      tags:
      - summaries
      operationId: revertSummary
      summary: Revertir proceso RA/RC aceptado (comunicación RR)
      description: 'Crea una **Comunicación de Reversión RR** del proceso `{id}` (una

        baja RA o un resumen RC con acción de baja YA ACEPTADOS por SUNAT).

        El RR referencia los mismos documentos dados de baja por el

        proceso original; cuando SUNAT lo acepta (ticket + polling), esos

        documentos vuelven de `voided` a `accepted` — la ÚNICA transición

        de salida de `voided` en la máquina de estados.


        Asíncrono (`202`): responde el summary `RR-YYYYMMDD-NNN` en

        `pending`; el resultado llega por polling del ticket. Un solo RR

        activo por proceso (`409 duplicate` si ya existe).

        '
      x-abilities:
      - summaries:manage
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                motivo:
                  type: string
                  maxLength: 100
                  description: Motivo de la reversión (viaja en el XML RR).
      responses:
        '202':
          description: Reversión RR creada y encolada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Summary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/summaries/{id}/files/{kind}:
    parameters:
    - $ref: '#/components/parameters/PathID'
    - name: kind
      in: path
      required: true
      schema:
        type: string
        enum:
        - xml
        - cdr
    get:
      tags:
      - summaries
      operationId: getSummaryFile
      summary: Descargar XML firmado o CDR del proceso
      description: Copia legal del proceso colectivo (RC/RA/RR). `404` mientras el worker no lo haya generado.
      x-abilities:
      - files:read
      responses:
        '200':
          description: Contenido del archivo (`X-Content-Sha256` con el hash).
          content:
            application/xml:
              schema:
                type: string
            application/zip:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/cpe-lookup:
    post:
      tags:
      - documents
      operationId: lookupCPE
      summary: Consultar validez de un comprobante (síncrono)
      description: 'Consulta `getStatusCdr` del billConsultService de SUNAT con la

        Clave SOL de la empresa. El comprobante puede ser propio o de

        terceros (`ruc` del emisor consultado; default: el RUC de la

        empresa). La consulta queda registrada en el historial de consultas

        (`status=done`, o `failed` si SUNAT no respondió).

        '
      x-abilities:
      - documents:read
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CpeLookupInput'
      responses:
        '200':
          description: Consulta resuelta.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CpeLookup'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/cpe-lookup/batch:
    post:
      tags:
      - documents
      operationId: batchLookupCPE
      summary: Consultar validez masiva (hasta 100, asíncrona)
      description: 'Registra hasta 100 consultas en `pending` que se resuelven

        de forma asíncrona. Sondee cada consulta con

        `GET /v1/cpe-lookup/{id}` hasta `done`/`failed`.

        '
      x-abilities:
      - documents:read
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - items
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    $ref: '#/components/schemas/CpeLookupInput'
      responses:
        '202':
          description: Consultas registradas (pending).
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CpeLookup'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/cpe-lookup/{id}:
    parameters:
    - $ref: '#/components/parameters/PathID'
    get:
      tags:
      - documents
      operationId: getCpeLookup
      summary: Obtener resultado de una consulta CPE
      x-abilities:
      - documents:read
      responses:
        '200':
          description: Consulta (pending, done o failed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CpeLookup'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/plans:
    get:
      summary: Catálogo de planes activos (límites y precios)
      tags:
      - billing
      responses:
        '200':
          description: Planes activos
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Plan'
  /v1/company/subscription:
    get:
      summary: Suscripción de la empresa — plan vigente, límites efectivos, uso del periodo y vencimientos
      description: 'Complementa GET /v1/company/usage (contadores crudos): aquí el uso va acompañado del
        límite efectivo del plan (overrides de la licencia incluidos) y su porcentaje. plan=null si la
        empresa no tiene licencia asignada.

        '
      tags:
      - billing
      responses:
        '200':
          description: Suscripción vigente
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Subscription'
  /v1/billing/checkout:
    post:
      summary: Inicia el pago de un plan (x-abilities company:manage)
      tags:
      - billing
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - plan_id
              properties:
                plan_id:
                  type: string
                provider:
                  type: string
                  default: manual
                  description: Pasarela a usar
      responses:
        '201':
          description: Intent de pago creado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutIntent'
        '422':
          description: plan_id inválido o pasarela no disponible (validation_failed)
  /v1/scheduled-documents:
    get:
      summary: Lista los documentos programados (x-abilities documents:read)
      tags:
      - scheduled-documents
      responses:
        '200':
          description: Programados de la empresa (recientes primero)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ScheduledDocument'
    post:
      summary: Crea un documento programado (x-abilities documents:write)
      tags:
      - scheduled-documents
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScheduledDocumentInput'
      responses:
        '201':
          description: Programado creado (next_run_at calculado del cron, hora America/Lima)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduledDocument'
        '422':
          description: cron inválido o plantilla inexistente (validation_failed)
  /v1/scheduled-documents/{id}:
    get:
      summary: Detalle de un programado (x-abilities documents:read)
      tags:
      - scheduled-documents
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Programado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduledDocument'
        '404':
          description: No existe (o es de otro tenant)
    put:
      summary: Reemplaza template_id/cron/active y recalcula next_run_at (x-abilities documents:write)
      tags:
      - scheduled-documents
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScheduledDocumentInput'
      responses:
        '200':
          description: Programado actualizado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduledDocument'
    delete:
      summary: Elimina el programado (x-abilities documents:write)
      tags:
      - scheduled-documents
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '204':
          description: Eliminado
  /v1/clients/{id}/portal-link:
    post:
      summary: Crea el enlace del portal del cliente final (x-abilities files:read)
      description: 'Enlace firmado /pc/{token} que lista TODOS los documentos aceptados de ese cliente
        en la empresa. El token es capability: opaco, con expiración (default 720 h, máx 8760) y máximo
        de accesos opcional; solo se persiste su sha256.

        '
      tags:
      - portal
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                expires_in_hours:
                  type: integer
                  default: 720
                  maximum: 8760
                max_accesses:
                  type: integer
                  minimum: 1
                  nullable: true
      responses:
        '201':
          description: Enlace creado (la URL completa con token solo viaja aquí)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientPortalLink'
        '404':
          description: Cliente inexistente o archivado
  /pc/{token}:
    get:
      summary: Portal público del cliente final (HTML, sin login)
      description: 'Lista los documentos aceptados del cliente del enlace. Rate limit por IP, cabeceras
        anti-indexación (X-Robots-Tag, no-store). Token inválido/expirado/revocado/agotado → 404 problem+json
        uniforme.

        '
      tags:
      - portal
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: 'Listado del cliente. Por defecto `text/html`; con `Accept: application/json` devuelve
            los datos para renderizarlos con marca propia.'
          content:
            text/html:
              schema:
                type: string
            application/json:
              schema:
                $ref: '#/components/schemas/PortalClientList'
        '404':
          description: not_found uniforme
  /pc/{token}/d/{doc}:
    get:
      summary: Descarga de un archivo del portal del cliente final
      tags:
      - portal
      parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
      - name: doc
        in: path
        required: true
        schema:
          type: string
        description: id del documento listado
      - name: file
        in: query
        required: true
        schema:
          type: string
          enum:
          - xml
          - cdr
          - pdf
      responses:
        '200':
          description: Archivo (attachment)
        '404':
          description: not_found uniforme (documento de otro cliente incluido)
  /v1/notifications:
    get:
      summary: Centro de notificaciones del usuario (no leídas primero)
      tags:
      - notifications
      parameters:
      - name: type
        in: query
        schema:
          type: string
        description: Filtra por tipo de evento
      - name: read
        in: query
        schema:
          type: string
          enum:
          - 'true'
          - 'false'
      - name: archived
        in: query
        schema:
          type: string
          enum:
          - 'true'
        description: Incluye archivadas
      - name: after
        in: query
        schema:
          type: string
        description: Cursor opaco (meta.next_cursor)
      - name: limit
        in: query
        schema:
          type: integer
          maximum: 100
          default: 25
      responses:
        '200':
          description: Listado (no leídas primero, luego más recientes)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Notification'
                  meta:
                    type: object
                    properties:
                      next_cursor:
                        type: string
  /v1/notifications:read-all:
    patch:
      summary: Marca leídas todas las notificaciones visibles no leídas
      tags:
      - notifications
      responses:
        '200':
          description: Cuántas marcó
          content:
            application/json:
              schema:
                type: object
                properties:
                  marked:
                    type: integer
  /v1/notifications/{id}:
    patch:
      summary: Marca leída/no-leída y archiva/desarchiva
      description: 'En una notificación de empresa (company_wide) la marca de leída es compartida entre
        los miembros de la empresa.

        '
      tags:
      - notifications
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                read:
                  type: boolean
                archived:
                  type: boolean
      responses:
        '200':
          description: Notificación actualizada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Notification'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/me/notification-preferences:
    get:
      summary: Matriz efectiva de preferencias por tipo (empresa activa)
      tags:
      - notifications
      responses:
        '200':
          description: Una entrada por tipo del catálogo
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/NotificationPreference'
    put:
      summary: Upsert de preferencias (las no enviadas se conservan)
      tags:
      - notifications
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - preferences
              properties:
                preferences:
                  type: array
                  items:
                    type: object
                    required:
                    - type
                    - channels
                    properties:
                      type:
                        $ref: '#/components/schemas/NotificationType'
                      channels:
                        type: array
                        items:
                          type: string
                          enum:
                          - inapp
                          - email
      responses:
        '200':
          description: Matriz efectiva resultante
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/NotificationPreference'
  /v1/auth/2fa/setup:
    post:
      summary: Genera el secreto TOTP (RFC 6238) del usuario
      description: 'Requiere sesión. El secreto y el otpauth:// URI viajan UNA sola vez; se guarda cifrado
        en reposo. Repetible mientras el 2FA no esté activo; con 2FA activo responde 409 invalid_state.

        '
      tags:
      - auth
      responses:
        '200':
          description: Secreto para enrolar el autenticador
          content:
            application/json:
              schema:
                type: object
                properties:
                  secret:
                    type: string
                    description: Base32 sin padding
                  otpauth_uri:
                    type: string
  /v1/auth/2fa/verify:
    post:
      summary: Activa el 2FA con el primer código TOTP válido
      tags:
      - auth
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - code
              properties:
                code:
                  type: string
                  description: Código TOTP de 6 dígitos
      responses:
        '200':
          description: 2FA activo; los recovery codes viajan UNA sola vez
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled:
                    type: boolean
                  recovery_codes:
                    type: array
                    items:
                      type: string
                    description: 10 códigos de un solo uso ("xxxxx-xxxxx")
  /v1/auth/2fa/disable:
    post:
      summary: Deshabilita el 2FA (código TOTP vigente o recovery code)
      tags:
      - auth
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - code
              properties:
                code:
                  type: string
      responses:
        '204':
          description: 2FA deshabilitado
  /v1/auth/2fa/challenge:
    post:
      summary: Completa el login 2FA (público, rate limit por IP)
      description: 'Canjea el challenge_token devuelto por POST /v1/auth/login (usuarios con 2FA activo;
        expira en 5 minutos) más un código TOTP o un recovery code por la sesión completa. El challenge
        solo se consume cuando el código es válido.

        '
      tags:
      - auth
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - challenge_token
              - code
              properties:
                challenge_token:
                  type: string
                code:
                  type: string
      responses:
        '200':
          description: Sesión creada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthSession'
        '401':
          description: challenge o código inválido
  /v1/identity/ruc/{num}:
    get:
      summary: Lookup de RUC contra el proveedor configurado (con caché 30 días)
      tags:
      - identity
      x-abilities:
      - clients:read
      parameters:
      - name: num
        in: path
        required: true
        schema:
          type: string
          pattern: ^[0-9]{11}$
      responses:
        '200':
          description: Identidad normalizada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentityResult'
        '503':
          description: dependency_unavailable — sin proveedor configurado o proveedor caído
  /v1/identity/dni/{num}:
    get:
      summary: Lookup de DNI contra el proveedor configurado (con caché 365 días)
      tags:
      - identity
      x-abilities:
      - clients:read
      parameters:
      - name: num
        in: path
        required: true
        schema:
          type: string
          pattern: ^[0-9]{8}$
      responses:
        '200':
          description: Identidad normalizada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentityResult'
        '503':
          description: dependency_unavailable
  /v1/company/identity-settings:
    get:
      summary: Configuración del proveedor de identidad (sin token)
      tags:
      - identity
      x-abilities:
      - company:manage
      responses:
        '200':
          description: Configuración vigente
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentitySettings'
    put:
      summary: Configura el proveedor de identidad de la empresa
      tags:
      - identity
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - url_template
              properties:
                url_template:
                  type: string
                  description: URL con {tipo} y {numero}, p.ej. https://api.prov.pe/{tipo}/{numero}
                auth_header:
                  type: string
                  description: Header de auth (default "Authorization")
                token:
                  type: string
                  nullable: true
                  description: Valor del header; se guarda cifrado en reposo. "" borra el token; omitido
                    lo conserva.
      responses:
        '200':
          description: Configuración resultante
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentitySettings'
  /v1/imports:
    post:
      summary: Sube un archivo CSV/XLSX y crea el batch validado (preview)
      description: 'multipart/form-data con `file` (máx 10 MiB) y `kind` (documents|clients|catalog_items).
        El layout de columnas está en la guía de importaciones de la documentación. El batch queda ready
        (o failed si ninguna fila es válida) con totals {rows, ok, errors} y los errores por fila (truncados
        a 100; archivo completo en totals.errors_file).

        '
      tags:
      - imports
      x-abilities:
      - documents:write
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
              - file
              - kind
              properties:
                file:
                  type: string
                  format: binary
                kind:
                  type: string
                  enum:
                  - documents
                  - clients
                  - catalog_items
      responses:
        '201':
          description: Batch creado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportBatch'
  /v1/imports/{id}/run:
    post:
      summary: Ejecuta el batch (job import.run asíncrono)
      description: 'ready→running + encolado transaccional. Idempotente por fila: documents usa external_reference
        (el de la fila o "imp_<batch>_<fila>"); clients/catalog_items son upserts. Al terminar el batch
        pasa a done y se emite la notificación import.completed.

        '
      tags:
      - imports
      x-abilities:
      - documents:write
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '202':
          description: Batch en ejecución
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportBatch'
        '409':
          description: invalid_state — el batch no está ready
  /v1/imports/{id}:
    get:
      summary: Estado, progreso y errores del batch
      tags:
      - imports
      x-abilities:
      - documents:read
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Batch
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportBatch'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/company/unit-symbols:
    get:
      summary: Símbolo comercial por unidad de medida (efectivo)
      description: Lista las unidades (cat. 03) con su símbolo comercial efectivo para la empresa (default
        del catálogo mas overrides). El código SUNAT es la fuente de verdad del XML; el símbolo es lo
        que se muestra (UI y PDF).
      tags:
      - company
      x-abilities:
      - company:manage
      responses:
        '200':
          description: Unidades con su símbolo.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      required:
                      - code
                      - label
                      - default_simbolo
                      - simbolo
                      - overridden
                      properties:
                        code:
                          type: string
                          example: NIU
                        label:
                          type: string
                          example: Unidad
                        default_simbolo:
                          type: string
                          example: UND
                        simbolo:
                          type: string
                          example: UND
                        overridden:
                          type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    put:
      summary: Editar símbolos comerciales de unidades
      description: Guarda overrides (código a símbolo). Un símbolo vacío o igual al default restablece
        la unidad. Devuelve la lista efectiva.
      tags:
      - company
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - symbols
              properties:
                symbols:
                  type: object
                  additionalProperties:
                    type: string
                  example:
                    NIU: UNID
                    ZZ: SERVICIO
      responses:
        '200':
          description: Símbolos actualizados (lista efectiva).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/company/pdf-settings:
    get:
      summary: Personalización de la representación impresa
      tags:
      - company
      x-abilities:
      - company:manage
      responses:
        '200':
          description: Configuración vigente (defaults donde no hay fila)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PDFSettings'
    put:
      summary: Actualiza la personalización (campos omitidos se conservan)
      description: 'footer_text, telefono, email y web se reflejan de inmediato en la representación impresa;
        primary_color y show_unit_price quedan persistidos para las plantillas que los soporten. String
        vacío restablece el valor por defecto.

        '
      tags:
      - company
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                footer_text:
                  type: string
                  maxLength: 500
                primary_color:
                  type: string
                  pattern: ^#[0-9a-fA-F]{6}$
                show_unit_price:
                  type: boolean
                telefono:
                  type: string
                email:
                  type: string
                web:
                  type: string
      responses:
        '200':
          description: Configuración resultante
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PDFSettings'
  /v1/reports/dashboard:
    get:
      tags:
      - Reportes
      summary: Dashboard del periodo
      description: Resumen de emisión del periodo (por estado, por tipo, top clientes, serie diaria).
        Requiere reports:read.
      operationId: reportsDashboard
      x-abilities:
      - reports:read
      parameters:
      - name: desde
        in: query
        schema:
          type: string
          format: date
        description: YYYY-MM-DD; default inicio de mes
      - name: hasta
        in: query
        schema:
          type: string
          format: date
        description: YYYY-MM-DD; default hoy
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportDashboard'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/reports/products:
    get:
      tags:
      - Reportes
      summary: Ventas por producto
      description: Agrega los ítems de comprobantes aceptados del periodo por código de producto. Requiere
        reports:read.
      operationId: reportsProducts
      x-abilities:
      - reports:read
      parameters:
      - name: desde
        in: query
        schema:
          type: string
          format: date
      - name: hasta
        in: query
        schema:
          type: string
          format: date
      - name: after
        in: query
        schema:
          type: string
      - name: limit
        in: query
        schema:
          type: integer
          default: 50
          maximum: 200
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportProductsPage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /v1/reports/receivables:
    get:
      tags:
      - Reportes
      summary: Cuentas por cobrar
      description: Comprobantes aceptados al crédito del periodo con sus cuotas y vencimientos. Requiere
        reports:read.
      operationId: reportsReceivables
      x-abilities:
      - reports:read
      parameters:
      - name: desde
        in: query
        schema:
          type: string
          format: date
      - name: hasta
        in: query
        schema:
          type: string
          format: date
      - name: after
        in: query
        schema:
          type: string
      - name: limit
        in: query
        schema:
          type: integer
          default: 50
          maximum: 200
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportReceivablesPage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /v1/reports/sales-register:
    get:
      tags:
      - Reportes
      summary: Registro de ventas (formato SUNAT)
      description: Filas del registro de ventas del periodo (comprobantes aceptados 01/03/07/08/04) con
        sus columnas fiscales. Requiere reports:read.
      operationId: reportsSalesRegister
      x-abilities:
      - reports:read
      parameters:
      - name: desde
        in: query
        schema:
          type: string
          format: date
      - name: hasta
        in: query
        schema:
          type: string
          format: date
      - name: after
        in: query
        schema:
          type: string
      - name: limit
        in: query
        schema:
          type: integer
          default: 50
          maximum: 200
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportSalesRegisterPage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /v1/company/credentials/sire:
    put:
      tags:
      - credentials
      operationId: putSireCredentials
      summary: Configurar credenciales SIRE (client_id/secret)
      description: 'Credenciales OAuth2 del API SIRE (generadas en el menú SOL con

        alcance "MIGE RCE y RVIE - SIRE"), separadas por ambiente. El

        user/pass OAuth2 se toma de la Clave SOL del MISMO ambiente, que

        debe estar configurada antes. Se cifran en reposo; toda

        rotación resetea `verified_at`.

        '
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - environment
              - client_id
              - client_secret
              properties:
                environment:
                  $ref: '#/components/schemas/Environment'
                client_id:
                  type: string
                  example: test-85e5b0ae-255c-4891-a595-0b98c65c9854
                client_secret:
                  type: string
                  format: password
      responses:
        '200':
          description: Credenciales guardadas.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CredentialStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/company/credentials/sire/test:
    post:
      tags:
      - credentials
      operationId: testSireCredentials
      summary: Probar conexión SIRE
      description: Ejecuta un test de conexión real (token OAuth2 con scope api-sire) contra SUNAT y registra
        `verified_at` si es exitoso.
      x-abilities:
      - company:manage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - environment
              properties:
                environment:
                  $ref: '#/components/schemas/Environment'
      responses:
        '200':
          description: Resultado del test.
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                - environment
                properties:
                  ok:
                    type: boolean
                    example: true
                  environment:
                    $ref: '#/components/schemas/Environment'
                  latency_ms:
                    type: integer
                    example: 412
                  message:
                    type:
                    - string
                    - 'null'
                    example: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
  /v1/sire/{libro}/periods/{p}/download-proposal:
    parameters:
    - $ref: '#/components/parameters/SireLibro'
    - $ref: '#/components/parameters/SirePeriodo'
    post:
      tags:
      - sire
      operationId: sireDownloadProposal
      summary: Descargar propuesta (RVIE 5.18 / RCE 5.34)
      description: 'Pide a SUNAT exportar la propuesta del periodo. SUNAT responde con

        un ticket; el sistema lo sondea automáticamente y persiste el zip

        descargado como archivo del proceso (`sire_files`).

        '
      x-abilities:
      - sire:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                cod_tipo_archivo:
                  type: integer
                  enum:
                  - 0
                  - 1
                  default: 0
                  description: Formato del archivo a generar (0 = txt, 1 = csv).
      responses:
        '202':
          description: Proceso SIRE creado y enviado (polling automático).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SireProcess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
  /v1/sire/{libro}/periods/{p}/accept-proposal:
    parameters:
    - $ref: '#/components/parameters/SireLibro'
    - $ref: '#/components/parameters/SirePeriodo'
    post:
      tags:
      - sire
      operationId: sireAcceptProposal
      summary: Aceptar propuesta (RVIE 5.8 / RCE 5.2)
      description: Acepta la propuesta vigente del periodo y la fija como preliminar. Operación con ticket
        (polling automático).
      x-abilities:
      - sire:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202':
          description: Proceso SIRE creado y enviado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SireProcess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
  /v1/sire/{libro}/periods/{p}/replace-proposal:
    parameters:
    - $ref: '#/components/parameters/SireLibro'
    - $ref: '#/components/parameters/SirePeriodo'
    post:
      tags:
      - sire
      operationId: sireReplaceProposal
      summary: Reemplazar propuesta (carga TUS, RVIE 5.3 / RCE 5.3)
      description: 'Sube el TXT propio (comprimido en ZIP según RS 112-2021 Anexo N° 1)

        que reemplaza la propuesta SUNAT del periodo. El archivo viaja en

        base64 dentro del JSON (límite general de 2 MiB del API); el

        sistema lo entrega a SUNAT por el protocolo TUS.IO y sondea el

        resultado por `nomArchivoImportacion`.

        '
      x-abilities:
      - sire:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SireArchivoCarga'
      responses:
        '202':
          description: Carga registrada (polling automático).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SireProcess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
  /v1/sire/{libro}/periods/{p}/preliminary/register:
    parameters:
    - $ref: '#/components/parameters/SireLibro'
    - $ref: '#/components/parameters/SirePeriodo'
    post:
      tags:
      - sire
      operationId: sireRegisterPreliminary
      summary: Registrar preliminar (RVIE 5.9 / RCE 5.4)
      description: Registra el preliminar del periodo (ticket, polling automático).
      x-abilities:
      - sire:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202':
          description: Proceso SIRE creado y enviado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SireProcess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
  /v1/sire/{libro}/periods/{p}/preliminary/export:
    parameters:
    - $ref: '#/components/parameters/SireLibro'
    - $ref: '#/components/parameters/SirePeriodo'
    post:
      tags:
      - sire
      operationId: sireExportPreliminary
      summary: Exportar preliminar (RVIE 5.22 / RCE 5.40)
      description: Exporta el preliminar del periodo como archivo (ticket, polling automático; el zip
        queda en los archivos del proceso).
      x-abilities:
      - sire:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                cod_tipo_archivo:
                  type: integer
                  enum:
                  - 0
                  - 1
                  default: 0
                  description: Formato del archivo a generar (0 = txt, 1 = csv).
      responses:
        '202':
          description: Proceso SIRE creado y enviado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SireProcess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
  /v1/sire/rce/periods/{p}/include-exclude:
    parameters:
    - $ref: '#/components/parameters/SirePeriodo'
    post:
      tags:
      - sire
      operationId: sireIncludeExclude
      summary: Incluir/excluir comprobantes de la propuesta RCE (5.8)
      description: Sube por TUS el archivo de incluir-excluir comprobantes de la propuesta RCE (codProceso
        55). Solo libro rce.
      x-abilities:
      - sire:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SireArchivoCarga'
      responses:
        '202':
          description: Carga registrada (polling automático).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SireProcess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
  /v1/sire/rce/periods/{p}/non-domiciled:
    parameters:
    - $ref: '#/components/parameters/SirePeriodo'
    post:
      tags:
      - sire
      operationId: sireNonDomiciled
      summary: Cargar compras no domiciliados RCE (5.5)
      description: Sube por TUS el registro de compras de no domiciliados del periodo (codProceso 56).
        Solo libro rce.
      x-abilities:
      - sire:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SireArchivoCarga'
      responses:
        '202':
          description: Carga registrada (polling automático).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SireProcess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
  /v1/sire/{libro}/periods/{p}/adjustments/load:
    parameters:
    - $ref: '#/components/parameters/SireLibro'
    - $ref: '#/components/parameters/SirePeriodo'
    post:
      tags:
      - sire
      operationId: sireAdjustmentsLoad
      summary: Cargar ajustes posteriores AJU/AJP (RVIE 5.6-5.7 / RCE 5.18-5.27)
      description: 'Sube por TUS el archivo de ajustes posteriores del periodo.

        `periodos_anteriores=true` selecciona la variante AJP (ajustes de

        periodos anteriores a la vigencia del SIRE); `no_domiciliados=true`

        (solo rce) la variante de no domiciliados.

        '
      x-abilities:
      - sire:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SireArchivoCarga'
      responses:
        '202':
          description: Carga registrada (polling automático).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SireProcess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
  /v1/sire/rvie/periods/{p}/adjustments/export:
    parameters:
    - $ref: '#/components/parameters/SirePeriodo'
    post:
      tags:
      - sire
      operationId: sireAdjustmentsExport
      summary: Descargar ajustes posteriores RVIE (5.29/5.30)
      description: 'Exporta los ajustes posteriores del periodo (o los de periodos

        anteriores con `periodos_anteriores=true`) como ticket + archivo.

        `cod_proceso` es el código del Anexo I del manual SIRE; si se

        omite se envía 23.

        '
      x-abilities:
      - sire:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                cod_tipo_archivo:
                  type: integer
                  enum:
                  - 0
                  - 1
                  default: 0
                cod_proceso:
                  type: string
                  example: '23'
                periodos_anteriores:
                  type: boolean
                  default: false
      responses:
        '202':
          description: Proceso SIRE creado y enviado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SireProcess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
  /v1/sire/rce/periods/{p}/adjustments/send:
    parameters:
    - $ref: '#/components/parameters/SirePeriodo'
    post:
      tags:
      - sire
      operationId: sireAdjustmentsSend
      summary: Enviar ajustes posteriores RCE (5.19/5.22/5.25/5.28)
      description: 'Formaliza (envía) los ajustes posteriores cargados del periodo.

        Operación síncrona: el proceso queda `done` en la respuesta. Las

        variantes se seleccionan con `periodos_anteriores` y

        `no_domiciliados`; algunas exigen `num_ajuste_posterior` y

        `num_ticket`.

        '
      x-abilities:
      - sire:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                periodos_anteriores:
                  type: boolean
                  default: false
                no_domiciliados:
                  type: boolean
                  default: false
                num_ajuste_posterior:
                  type: string
                num_ticket:
                  type: string
                ajuste:
                  $ref: '#/components/schemas/SireAjuste'
      responses:
        '202':
          description: Ajustes enviados (proceso done).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SireProcess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
  /v1/sire/rce/periods/{p}/adjustments/delete-receipts:
    parameters:
    - $ref: '#/components/parameters/SirePeriodo'
    post:
      tags:
      - sire
      operationId: sireAdjustmentsDeleteReceipts
      summary: Eliminar comprobantes de los ajustes RCE (5.20/5.23/5.26/5.29)
      description: 'Elimina comprobantes de la colección de ajustes posteriores del

        periodo. Operación síncrona (proceso `done`). El detalle de

        comprobantes (`ajuste`) es obligatorio; variantes por

        `periodos_anteriores` / `no_domiciliados`.

        '
      x-abilities:
      - sire:write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - ajuste
              properties:
                periodos_anteriores:
                  type: boolean
                  default: false
                no_domiciliados:
                  type: boolean
                  default: false
                num_ajuste_posterior:
                  type: string
                ajuste:
                  $ref: '#/components/schemas/SireAjuste'
      responses:
        '202':
          description: Comprobantes eliminados (proceso done).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SireProcess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
  /v1/sire/processes:
    get:
      tags:
      - sire
      operationId: listSireProcesses
      summary: Listar procesos SIRE
      description: Todo proceso SIRE (propuesta, preliminar, cargas, ajustes) queda registrado con su
        estado, ticket y archivos descargados.
      x-abilities:
      - sire:read
      parameters:
      - name: libro
        in: query
        schema:
          type: string
          enum:
          - rvie
          - rce
      - name: periodo
        in: query
        schema:
          type: string
          pattern: ^[0-9]{6}$
          example: '202607'
      - name: estado
        in: query
        schema:
          type: string
          enum:
          - pending
          - sent
          - in_process
          - done
          - failed
      - $ref: '#/components/parameters/After'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Página de procesos.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SireProcess'
                  meta:
                    $ref: '#/components/schemas/PageMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
  /v1/sire/processes/{id}:
    parameters:
    - $ref: '#/components/parameters/ResourceId'
    get:
      tags:
      - sire
      operationId: getSireProcess
      summary: Detalle de un proceso SIRE
      description: Incluye los archivos descargados con sus URLs de descarga autenticadas.
      x-abilities:
      - sire:read
      responses:
        '200':
          description: Proceso con archivos.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SireProcess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/sire/processes/{id}/files/{fileId}:
    parameters:
    - $ref: '#/components/parameters/ResourceId'
    - name: fileId
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - sire
      operationId: downloadSireFile
      summary: Descargar un archivo del proceso SIRE
      description: Entrega el zip generado por SUNAT (propuesta, reporte del preliminar, resultado de
        carga) tal cual se persistió.
      x-abilities:
      - sire:read
      responses:
        '200':
          description: Contenido binario del archivo.
          content:
            application/zip:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/sire/{libro}/periods:
    parameters:
    - $ref: '#/components/parameters/SireLibro'
    get:
      tags:
      - sire
      operationId: listSirePeriods
      summary: Periodos habilitados del libro (RVIE 5.2 / RCE 5.33)
      description: Proxy cacheado (10 minutos) de la consulta de periodos de SUNAT.
      x-abilities:
      - sire:read
      responses:
        '200':
          description: Periodos habilitados.
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SirePeriodo'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '503':
          $ref: '#/components/responses/DependencyUnavailable'
  /v1/sire/rvie/periods/{p}/reconciliation:
    parameters:
    - $ref: '#/components/parameters/SirePeriodo'
    get:
      tags:
      - sire
      operationId: getSireReconciliation
      summary: Conciliación RVIE vs documentos emitidos
      description: 'Cruza la propuesta RVIE descargada más reciente del periodo contra

        los comprobantes emitidos y aceptados en el sistema (tipos

        01/03/07/08): faltantes en SUNAT, faltantes en el sistema y

        diferencias de importe total. Requiere haber ejecutado

        download-proposal para el periodo (409 invalid_state si no).

        '
      x-abilities:
      - sire:read
      responses:
        '200':
          description: Informe de conciliación.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SireReconciliation'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
components:
  securitySchemes:
    apiToken:
      type: http
      scheme: bearer
      bearerFormat: opaque
      description: 'Token bearer: un token de API de empresa (`ecp_live_…` / `ecp_test_…`,

        creado en `/v1/api-tokens`) o una sesión de usuario (creada en

        `/v1/auth/login`). El contexto de empresa sale SIEMPRE del token/sesión.

        '
  parameters:
    ResourceId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: ID (ULID) del recurso.
      example: 01J2ZK8Q3WPRK9C7VE0M2T5H4X
    After:
      name: after
      in: query
      schema:
        type: string
      description: Cursor de paginación — el `meta.next_cursor` de la página anterior (ULID orden-tiempo).
      example: 01J2ZK8Q3WPRK9C7VE0M2T5H4X
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
      description: Cantidad máxima de elementos por página.
    IdempotencyKey:
      name: X-Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 8
        maxLength: 128
      description: 'Clave de idempotencia obligatoria en toda emisión, elegida por el

        integrador (p. ej. un UUID). TTL 24 h. La misma clave con el mismo body

        devuelve la respuesta cacheada; con distinto body → `422

        idempotency_key_reused`; con la ejecución original en curso → `409

        idempotency_in_flight` con `Retry-After`.

        '
      example: 4f9a1c2e-7b3d-4e5f-9a0b-1c2d3e4f5a6b
    PathID:
      name: id
      in: path
      required: true
      description: Identificador ULID del recurso.
      schema:
        type: string
    SireLibro:
      name: libro
      in: path
      required: true
      description: Libro electrónico SIRE.
      schema:
        type: string
        enum:
        - rvie
        - rce
    SirePeriodo:
      name: p
      in: path
      required: true
      description: Periodo tributario YYYYMM.
      schema:
        type: string
        pattern: ^[0-9]{6}$
        example: '202607'
  headers:
    RateLimit-Limit:
      description: Límite de requests por minuto del token (según plan).
      schema:
        type: integer
      example: 300
    RateLimit-Remaining:
      description: Requests restantes en la ventana actual.
      schema:
        type: integer
      example: 297
    RateLimit-Reset:
      description: Segundos hasta que la ventana se reinicia.
      schema:
        type: integer
      example: 42
    Retry-After:
      description: Segundos sugeridos antes de reintentar.
      schema:
        type: integer
      example: 30
  responses:
    Unauthorized:
      description: No autenticado — token ausente, revocado o expirado (`code=unauthenticated`).
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://docs.emitir-cpe.com/errors/unauthenticated
            title: No autenticado
            status: 401
            code: unauthenticated
            detail: El token fue revocado o expiró.
            request_id: 01J2ZKCB7X9Z1B3D5F7H9J1L3N
    Forbidden:
      description: Sin permiso — al token le falta la ability requerida (`missing_ability`), la empresa
        está suspendida (`company_suspended`) o la operación exige sandbox (`sandbox_only`).
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://docs.emitir-cpe.com/errors/missing-ability
            title: Permiso insuficiente
            status: 403
            code: missing_ability
            detail: 'El token no tiene la ability requerida: documents:write.'
            request_id: 01J2ZKCB7X9Z1B3D5F7H9J1L3N
    NotFound:
      description: Recurso inexistente. Un recurso de OTRO tenant también responde 404 (nunca 403, para
        no filtrar existencia).
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://docs.emitir-cpe.com/errors/not-found
            title: Recurso no encontrado
            status: 404
            code: not_found
            detail: No existe el recurso solicitado.
            request_id: 01J2ZKCB7X9Z1B3D5F7H9J1L3N
    Conflict:
      description: Conflicto — idempotencia en curso (`idempotency_in_flight`, con `Retry-After`), duplicado
        (`duplicate`) o estado inválido para la operación (`invalid_state`, p. ej. void de un draft).
      headers:
        Retry-After:
          $ref: '#/components/headers/Retry-After'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://docs.emitir-cpe.com/errors/invalid-state
            title: Estado inválido para la operación
            status: 409
            code: invalid_state
            detail: Solo documentos aceptados pueden darse de baja (estado actual del documento distinto
              de accepted).
            request_id: 01J2ZKCB7X9Z1B3D5F7H9J1L3N
    ValidationFailed:
      description: Error de negocio — validación (`validation_failed`, con `errors[]`), rechazo SUNAT
        (`sunat_rejected`, con `sunat_code`), clave de idempotencia reusada con otro body (`idempotency_key_reused`)
        o serie desactivada (`series_disabled`).
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            validacion:
              summary: Validación de campos
              value:
                type: https://docs.emitir-cpe.com/errors/validation-failed
                title: La solicitud no pasó las validaciones
                status: 422
                code: validation_failed
                detail: 2 campos inválidos.
                errors:
                - field: items[0].tip_afe_igv
                  message: valor fuera del catálogo 07
                - field: cliente.num_doc
                  message: RUC inválido (dígito verificador)
                request_id: 01J2ZKCB7X9Z1B3D5F7H9J1L3N
            rechazo_sunat:
              summary: Rechazo SUNAT (modo ?wait=true)
              value:
                type: https://docs.emitir-cpe.com/errors/document-rejected
                title: Documento rechazado por SUNAT
                status: 422
                code: sunat_rejected
                detail: '2324: El comprobante fue informado previamente'
                sunat_code: '2324'
                request_id: 01J2ZKCB7X9Z1B3D5F7H9J1L3N
    RateLimited:
      description: Límite alcanzado — por token (`rate_limited`) o por concurrencia del tenant (`tenant_concurrency`).
        Reintente después de `Retry-After`.
      headers:
        Retry-After:
          $ref: '#/components/headers/Retry-After'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimit-Limit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimit-Remaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimit-Reset'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://docs.emitir-cpe.com/errors/rate-limited
            title: Límite de requests alcanzado
            status: 429
            code: rate_limited
            detail: Se alcanzó el límite de 300 requests por minuto del token.
            request_id: 01J2ZKCB7X9Z1B3D5F7H9J1L3N
    DependencyUnavailable:
      description: Dependencia no disponible (`dependency_unavailable`) — DB/colas saturadas o servicio
        externo caído. Reintente después de `Retry-After`.
      headers:
        Retry-After:
          $ref: '#/components/headers/Retry-After'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://docs.emitir-cpe.com/errors/dependency-unavailable
            title: Dependencia no disponible
            status: 503
            code: dependency_unavailable
            detail: Servicio temporalmente saturado; reintente en unos segundos.
            request_id: 01J2ZKCB7X9Z1B3D5F7H9J1L3N
  schemas:
    Problem:
      type: object
      description: Error RFC 7807 (`application/problem+json`). `code` es la interfaz estable de máquina
        (jamás se renombra); los mensajes van en español neutro. 5xx jamás filtran internals.
      required:
      - title
      - status
      - code
      - request_id
      properties:
        type:
          type: string
          format: uri
          description: URI de la página del error en la doc de integradores.
          example: https://docs.emitir-cpe.com/errors/document-rejected
        title:
          type: string
          description: Descripción corta legible del tipo de error.
          example: Documento rechazado por SUNAT
        status:
          type: integer
          description: Código HTTP.
          example: 422
        code:
          type: string
          description: Código estable de máquina del catálogo publicado (`malformed_request`, `unauthenticated`,
            `missing_ability`, `company_suspended`, `sandbox_only`, `not_found`, `idempotency_in_flight`,
            `duplicate`, `invalid_state`, `validation_failed`, `sunat_rejected`, `idempotency_key_reused`,
            `series_disabled`, `rate_limited`, `tenant_concurrency`, `internal`, `dependency_unavailable`).
          example: sunat_rejected
        detail:
          type: string
          description: Detalle legible de esta ocurrencia.
          example: '2324: El comprobante fue informado previamente'
        sunat_code:
          type:
          - string
          - 'null'
          description: Código SUNAT del CDR/fault, cuando aplica.
          example: '2324'
        errors:
          type: array
          description: Errores de validación campo a campo (presente con `code=validation_failed`).
          items:
            $ref: '#/components/schemas/FieldError'
        request_id:
          type: string
          description: ULID de correlación presente en toda respuesta y todo log.
          example: 01J2ZKCB7X9Z1B3D5F7H9J1L3N
    FieldError:
      type: object
      required:
      - field
      - message
      properties:
        field:
          type: string
          description: Ruta del campo inválido en el body.
          example: details[2].igv
        message:
          type: string
          example: el IGV declarado no coincide con el calculado (esperado 45.76)
    PageMeta:
      type: object
      description: Metadatos de paginación por cursor.
      required:
      - next_cursor
      properties:
        next_cursor:
          type:
          - string
          - 'null'
          description: Cursor para la siguiente página (`?after=`); `null` si no hay más resultados.
          example: 01J2ZK8Q3WPRK9C7VE0M2T5H4X
    Address:
      type: object
      description: Dirección peruana (catálogos SUNAT de ubigeo).
      properties:
        direccion:
          type: string
          example: Av. Arequipa 1234, Lince
        ubigeo:
          type: string
          pattern: ^[0-9]{6}$
          example: '150116'
        distrito:
          type: string
          example: Lince
        provincia:
          type: string
          example: Lima
        departamento:
          type: string
          example: Lima
        codigo_pais:
          type: string
          default: PE
          example: PE
    Environment:
      type: string
      description: Ambiente SUNAT del producto (beta/producción, independiente por producto).
      enum:
      - beta
      - production
    Role:
      type: string
      description: Rol del usuario en la empresa.
      enum:
      - owner
      - admin
      - emisor
      - lector
    TipoDocVenta:
      type: string
      description: 'Tipo de comprobante (catálogo SUNAT 01): 01 Factura, 03 Boleta, 04 Liquidación de
        compra, 07 Nota de Crédito, 08 Nota de Débito, 20 Comprobante de Retención, 40 Comprobante de
        Percepción.'
      enum:
      - '01'
      - '03'
      - '04'
      - '07'
      - 08
      - '20'
      - '40'
    TipoDocSerie:
      type: string
      description: 'Tipo de documento de la serie (catálogo SUNAT 01): incluye guía (09), liquidación
        (04), retención (20) y percepción (40).'
      enum:
      - '01'
      - '03'
      - '04'
      - '07'
      - 08
      - 09
      - '20'
      - '40'
    TipoDocIdentidad:
      type: string
      description: 'Tipo de documento de identidad (catálogo SUNAT 06): 0 sin documento, 1 DNI, 4 carnet
        de extranjería, 6 RUC, 7 pasaporte.'
      enum:
      - '0'
      - '1'
      - '4'
      - '6'
      - '7'
    EstadoDocumento:
      type: string
      description: 'Estado del ciclo de vida del documento (máquina de estados única):

        `draft` borrador editable → `pending` correlativo asignado, firmando →

        `signed` XML firmado, en cola → `submitted` enviado, esperando CDR →

        `accepted` / `accepted_with_obs` / `rejected` estados finales SUNAT;

        `error` fallo técnico agotado (reintentable); `contingency` retenido por

        caída SUNAT (reenvío automático); `voided` baja aceptada (RA/RR).

        '
      enum:
      - draft
      - pending
      - signed
      - submitted
      - accepted
      - accepted_with_obs
      - rejected
      - error
      - contingency
      - voided
    EstadoSummary:
      type: string
      description: Estado del proceso colectivo (RC/RA/RR), incluye `in_process` mientras SUNAT procesa
        el ticket.
      enum:
      - pending
      - signed
      - submitted
      - in_process
      - accepted
      - rejected
      - error
    User:
      type: object
      required:
      - id
      - email
      - name
      - locale
      - timezone
      - created_at
      properties:
        id:
          type: string
          example: 01J2ZJWD5M8P2R6T0V4X8Z1B3C
        email:
          type: string
          format: email
          example: maria@losandes.pe
        email_verified_at:
          type:
          - string
          - 'null'
          format: date-time
        name:
          type: string
          example: María Quispe
        locale:
          type: string
          example: es-PE
        timezone:
          type: string
          example: America/Lima
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AuthSession:
      type: object
      required:
      - token
      - refresh_token
      - expires_at
      - user
      properties:
        token:
          type: string
          description: Token bearer opaco de la sesión.
          example: ecp_sess_3f8a1b2c4d5e6f7a8b9c0d1e2f3a4b5c
        refresh_token:
          type: string
          description: Token de un solo uso para renovar la sesión.
        expires_at:
          type: string
          format: date-time
        user:
          $ref: '#/components/schemas/User'
        active_company_id:
          type:
          - string
          - 'null'
          description: Empresa activa de la sesión (null si el usuario aún no tiene empresas).
    Membership:
      type: object
      required:
      - company
      - role
      properties:
        company:
          $ref: '#/components/schemas/Company'
        role:
          $ref: '#/components/schemas/Role'
    Company:
      type: object
      required:
      - id
      - ruc
      - razon_social
      - sandbox
      - status
      - created_at
      properties:
        id:
          type: string
          example: 01J2ZJXJ2N4Y8QW3M0V6T7R9S1
        ruc:
          type: string
          pattern: ^[0-9]{11}$
          example: '20123456789'
        razon_social:
          type: string
          example: INVERSIONES PACHACÚTEC S.A.C.
        nombre_comercial:
          type:
          - string
          - 'null'
          example: Pachacútec
        address:
          $ref: '#/components/schemas/Address'
        logo_url:
          type:
          - string
          - 'null'
          description: URL del logo subido.
        sandbox:
          type: boolean
          description: Si es `true`, toda emisión va contra los endpoints beta de SUNAT (sin riesgo).
          example: true
        status:
          type: string
          enum:
          - active
          - suspended
          - closed
          example: active
        metadata:
          type: object
          additionalProperties: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CompanyInput:
      type: object
      required:
      - ruc
      - razon_social
      properties:
        ruc:
          type: string
          pattern: ^[0-9]{11}$
          example: '20123456789'
        razon_social:
          type: string
          example: INVERSIONES PACHACÚTEC S.A.C.
        nombre_comercial:
          type: string
          example: Pachacútec
        address:
          $ref: '#/components/schemas/Address'
        logo_base64:
          type: string
          contentEncoding: base64
          description: Logo PNG/JPG en base64 (máx 1 MB).
        metadata:
          type: object
          additionalProperties: true
    Branch:
      type: object
      required:
      - id
      - codigo
      - nombre
      properties:
        id:
          type: string
        codigo:
          type: string
          description: Código de anexo SUNAT.
          example: '0001'
        nombre:
          type: string
          example: Almacén Ate
        address:
          $ref: '#/components/schemas/Address'
        disabled_at:
          type:
          - string
          - 'null'
          format: date-time
    BranchInput:
      type: object
      required:
      - codigo
      - nombre
      properties:
        codigo:
          type: string
          example: '0001'
        nombre:
          type: string
          example: Almacén Ate
        address:
          $ref: '#/components/schemas/Address'
    Member:
      type: object
      required:
      - user_id
      - email
      - name
      - role
      - created_at
      properties:
        user_id:
          type: string
        email:
          type: string
          format: email
        name:
          type: string
        role:
          $ref: '#/components/schemas/Role'
        created_at:
          type: string
          format: date-time
          description: Fecha en que se unió a la empresa.
    Invitation:
      type: object
      required:
      - id
      - email
      - role
      - expires_at
      - created_at
      properties:
        id:
          type: string
        email:
          type: string
          format: email
          example: contador@losandes.pe
        role:
          $ref: '#/components/schemas/Role'
        invited_by:
          type: string
          description: ID del usuario que invitó.
        expires_at:
          type: string
          format: date-time
        accepted_at:
          type:
          - string
          - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
    CredentialStatus:
      type: object
      description: Estado de una credencial SUNAT guardada. Los secretos jamás se devuelven.
      required:
      - product
      - environment
      - updated_at
      properties:
        product:
          type: string
          enum:
          - sol
          - gre
          example: sol
        environment:
          $ref: '#/components/schemas/Environment'
        verified_at:
          type:
          - string
          - 'null'
          format: date-time
          description: Último test de conexión exitoso (null si nunca se probó).
        updated_at:
          type: string
          format: date-time
    Certificate:
      type: object
      description: Certificado digital de firma (metadatos; el material criptográfico se guarda cifrado
        y jamás se devuelve).
      required:
      - id
      - subject
      - serial
      - not_before
      - not_after
      - active
      - created_at
      properties:
        id:
          type: string
        subject:
          type: string
          example: CN=INVERSIONES PACHACÚTEC S.A.C., OU=20123456789
        serial:
          type: string
          example: 4A3F9C21B8D0E6F2
        not_before:
          type: string
          format: date-time
        not_after:
          type: string
          format: date-time
          description: Vencimiento — dispara alertas automáticas a 30/15/7/1 días.
        active:
          type: boolean
        created_at:
          type: string
          format: date-time
    AuditLogEntry:
      type: object
      required:
      - id
      - actor_type
      - action
      - created_at
      properties:
        id:
          type: string
        actor_type:
          type: string
          enum:
          - user
          - api_token
          - system
          - superadmin
        actor_id:
          type:
          - string
          - 'null'
        action:
          type: string
          example: company.credentials.updated
        target:
          type:
          - string
          - 'null'
          example: documents/01J2ZK8Q3WPRK9C7VE0M2T5H4X
        detail:
          type: object
          additionalProperties: true
        ip:
          type:
          - string
          - 'null'
          example: 200.48.225.10
        created_at:
          type: string
          format: date-time
    UsageCounters:
      type: object
      description: Contadores mensuales de uso de la empresa.
      required:
      - period
      - metrics
      properties:
        period:
          type: string
          pattern: ^[0-9]{6}$
          example: '202607'
        metrics:
          type: object
          description: Métrica → valor acumulado del periodo.
          additionalProperties:
            type: integer
          example:
            documents_issued: 1245
            api_requests: 48210
            emails_sent: 1180
    Series:
      type: object
      required:
      - id
      - tipo_doc
      - serie
      - correlativo_actual
      - start_at
      - created_at
      properties:
        id:
          type: string
          example: 01J2ZJZQ9S2U4W6Y8A0C2E4G6I
        branch_id:
          type:
          - string
          - 'null'
        tipo_doc:
          $ref: '#/components/schemas/TipoDocSerie'
        serie:
          type: string
          description: Serie con formato SUNAT (F### factura, B### boleta, T### guía, FC##/BC## notas
            según serie del afectado).
          example: F001
        correlativo_actual:
          type: integer
          description: Último correlativo asignado (0 si aún no emite).
          example: 123
        start_at:
          type: integer
          description: Correlativo inicial (migración desde otro sistema).
          example: 1
        contingency:
          type: boolean
          default: false
        disabled_at:
          type:
          - string
          - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
    SeriesInput:
      type: object
      required:
      - tipo_doc
      - serie
      properties:
        tipo_doc:
          $ref: '#/components/schemas/TipoDocSerie'
        serie:
          type: string
          example: F001
        branch_id:
          type:
          - string
          - 'null'
        start_at:
          type: integer
          minimum: 1
          default: 1
          description: El primer documento emitido tomará este correlativo.
    Client:
      type: object
      required:
      - id
      - tipo_doc
      - num_doc
      - razon_social
      - created_at
      properties:
        id:
          type: string
          example: 01J2ZJY7R8T3W1K5N9B2D4F6H8
        tipo_doc:
          $ref: '#/components/schemas/TipoDocIdentidad'
        num_doc:
          type: string
          example: '20601234567'
        razon_social:
          type: string
          example: COMERCIAL LOS ANDES S.A.C.
        email:
          type:
          - string
          - 'null'
          format: email
          example: facturacion@losandes.pe
        phone:
          type:
          - string
          - 'null'
          example: +51 987 654 321
        address:
          $ref: '#/components/schemas/Address'
        metadata:
          type: object
          additionalProperties: true
        archived_at:
          type:
          - string
          - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ClientInput:
      type: object
      required:
      - tipo_doc
      - num_doc
      - razon_social
      properties:
        tipo_doc:
          $ref: '#/components/schemas/TipoDocIdentidad'
        num_doc:
          type: string
          example: '20601234567'
        razon_social:
          type: string
          example: COMERCIAL LOS ANDES S.A.C.
        email:
          type: string
          format: email
        phone:
          type: string
        address:
          $ref: '#/components/schemas/Address'
        metadata:
          type: object
          additionalProperties: true
    CatalogItem:
      type: object
      required:
      - id
      - codigo
      - descripcion
      - unidad
      - moneda
      - tip_afe_igv
      properties:
        id:
          type: string
        codigo:
          type: string
          example: P001
        descripcion:
          type: string
          example: Cemento Portland tipo I x 42.5 kg
        unidad:
          type: string
          description: Unidad de medida (catálogo SUNAT 03).
          example: NIU
        precio_unitario:
          type:
          - number
          - 'null'
          description: Precio unitario referencial CON IGV.
          example: 30.0
        moneda:
          type: string
          example: PEN
        tip_afe_igv:
          type: string
          description: Afectación IGV por defecto (catálogo SUNAT 07).
          example: '10'
        isc:
          type:
          - object
          - 'null'
          description: Configuración ISC si aplica.
          properties:
            sistema:
              type: string
              description: Sistema de cálculo ISC (catálogo 08).
              example: '01'
            porcentaje:
              type: number
              example: 17
        icbper:
          type: boolean
          description: Si el item paga ICBPER (bolsas plásticas).
          default: false
        cod_prod_sunat:
          type:
          - string
          - 'null'
          description: Código de producto SUNAT (catálogo 25).
          example: '23101505'
        metadata:
          type: object
          additionalProperties: true
        archived_at:
          type:
          - string
          - 'null'
          format: date-time
    CatalogItemInput:
      type: object
      required:
      - codigo
      - descripcion
      properties:
        codigo:
          type: string
          example: P001
        descripcion:
          type: string
          example: Cemento Portland tipo I x 42.5 kg
        unidad:
          type: string
          default: NIU
        precio_unitario:
          type: number
          example: 30.0
        moneda:
          type: string
          default: PEN
        tip_afe_igv:
          type: string
          default: '10'
        isc:
          type: object
          properties:
            sistema:
              type: string
              example: '01'
            porcentaje:
              type: number
              example: 17
        icbper:
          type: boolean
          default: false
        cod_prod_sunat:
          type: string
        metadata:
          type: object
          additionalProperties: true
    Cuota:
      type: object
      description: Cuota de pago al crédito.
      required:
      - monto
      - fecha
      properties:
        monto:
          type: number
          example: 200.0
        fecha:
          type: string
          format: date
          example: '2026-08-18'
    Detraccion:
      type: object
      description: Detracción (SPOT) — Resolución 183-2004/SUNAT.
      required:
      - codigo_bien_servicio
      - cuenta_banco_nacion
      - porcentaje
      properties:
        codigo_bien_servicio:
          type: string
          description: Código de bien/servicio sujeto (catálogo 54).
          example: '022'
        cuenta_banco_nacion:
          type: string
          example: 00-045-678912
        porcentaje:
          type: number
          example: 12
        monto:
          type:
          - number
          - 'null'
          description: Monto detraído (se calcula si se omite).
          example: 48.0
        medio_pago:
          type: string
          description: Medio de pago (catálogo 59).
          default: '001'
    Percepcion:
      type: object
      description: Percepción del IGV.
      required:
      - codigo_regimen
      properties:
        codigo_regimen:
          type: string
          description: Régimen de percepción (catálogo 53).
          example: '51'
        porcentaje:
          type: number
          example: 2
        monto:
          type:
          - number
          - 'null'
          example: 8.0
        monto_total:
          type:
          - number
          - 'null'
          description: Total incluida la percepción.
          example: 408.0
    Anticipo:
      type: object
      description: Anticipo aplicado (comprobante previo).
      required:
      - tipo_doc
      - numero
      - total
      properties:
        tipo_doc:
          $ref: '#/components/schemas/TipoDocVenta'
        numero:
          type: string
          example: F001-100
        total:
          type: number
          example: 118.0
        fecha_emision:
          type: string
          format: date
    CargoDescuento:
      type: object
      description: Cargo o descuento (catálogo 53).
      required:
      - codigo
      - monto
      properties:
        codigo:
          type: string
          description: Código del catálogo 53 (p. ej. "02" descuento global, "50" otros cargos).
          example: '02'
        factor:
          type:
          - number
          - 'null'
          description: Factor sobre la base (0.10 = 10%).
          example: 0.1
        monto:
          type: number
          example: 33.9
        base:
          type:
          - number
          - 'null'
          example: 338.98
    Leyenda:
      type: object
      required:
      - code
      - value
      properties:
        code:
          type: string
          description: Código de leyenda (catálogo 52; "1000" importe en letras, "1002" gratuita, "2006"
            detracción…).
          example: '1000'
        value:
          type: string
          example: CUATROCIENTOS Y 00/100 SOLES
    DocumentoRelacionado:
      type: object
      description: Documento relacionado (catálogo 12) o guía relacionada.
      required:
      - tipo_doc
      - numero
      properties:
        tipo_doc:
          type: string
          example: 09
        numero:
          type: string
          example: T001-455
    Vendedor:
      type: object
      description: Vendedor asociado a la operación.
      properties:
        tipo_doc:
          $ref: '#/components/schemas/TipoDocIdentidad'
        num_doc:
          type: string
          example: '42345678'
        nombre:
          type: string
          example: José Huamán
    DocumentItemInput:
      type: object
      description: 'Línea de detalle de emisión. Con **cálculo asistido** basta

        `descripcion` (o `catalog_item_id`), `cantidad` y `valor_unitario` (o

        `precio_unitario` con IGV) — el sistema calcula IGV/ISC/ICBPER, valor de

        venta y totales de línea. Si envía los montos, el sistema los valida

        (CrossValidate).

        '
      required:
      - cantidad
      properties:
        catalog_item_id:
          type:
          - string
          - 'null'
          description: Item del catálogo del que copiar código, descripción, unidad, precio y afectación
            (los campos enviados tienen prioridad).
        codigo:
          type:
          - string
          - 'null'
          example: P001
        descripcion:
          type: string
          example: Cemento Portland tipo I x 42.5 kg
        unidad:
          type: string
          default: NIU
        cantidad:
          type: number
          example: 10
        valor_unitario:
          type:
          - number
          - 'null'
          description: Valor unitario SIN IGV.
          example: 25.42
        precio_unitario:
          type:
          - number
          - 'null'
          description: Precio unitario CON IGV (alternativa a `valor_unitario`).
          example: 30.0
        tip_afe_igv:
          type: string
          description: Afectación IGV (catálogo 07; "10" gravada, "20" exonerada, "30" inafecta, "40"
            exportación, gratuitas 1x/2x/3x).
          default: '10'
        igv:
          type:
          - number
          - 'null'
          description: IGV de la línea (calculado si se omite).
        isc:
          type:
          - object
          - 'null'
          properties:
            sistema:
              type: string
              example: '01'
            porcentaje:
              type:
              - number
              - 'null'
              example: 17
            monto:
              type:
              - number
              - 'null'
        icbper:
          type:
          - boolean
          - 'null'
          description: Aplica ICBPER (impuesto a bolsas plásticas) por unidad.
        descuentos:
          type: array
          items:
            $ref: '#/components/schemas/CargoDescuento'
        cargos:
          type: array
          items:
            $ref: '#/components/schemas/CargoDescuento'
        atributos:
          type: array
          description: Atributos adicionales de línea (catálogo 55).
          items:
            type: object
            properties:
              code:
                type: string
                example: '7000'
              name:
                type: string
              value:
                type: string
              fecha:
                type: string
                format: date
        valor_venta:
          type:
          - number
          - 'null'
          description: Valor de venta de la línea (calculado si se omite).
        total_impuestos:
          type:
          - number
          - 'null'
    DocumentItem:
      type: object
      description: Línea de detalle emitida (snapshot inmutable).
      required:
      - line
      - descripcion
      - unidad
      - cantidad
      - valor_unitario
      - tip_afe_igv
      - valor_venta
      properties:
        line:
          type: integer
          example: 1
        catalog_item_id:
          type:
          - string
          - 'null'
        codigo:
          type:
          - string
          - 'null'
          example: P001
        descripcion:
          type: string
          example: Cemento Portland tipo I x 42.5 kg
        unidad:
          type: string
          example: NIU
        cantidad:
          type: number
          example: 10
        valor_unitario:
          type: number
          example: 25.42
        precio_unitario:
          type:
          - number
          - 'null'
          example: 30.0
        tip_afe_igv:
          type: string
          example: '10'
        igv:
          type:
          - number
          - 'null'
          example: 45.76
        isc:
          type:
          - number
          - 'null'
        icbper:
          type:
          - number
          - 'null'
        descuentos:
          type:
          - array
          - 'null'
          items:
            $ref: '#/components/schemas/CargoDescuento'
        cargos:
          type:
          - array
          - 'null'
          items:
            $ref: '#/components/schemas/CargoDescuento'
        atributos:
          type:
          - array
          - 'null'
          items:
            type: object
            additionalProperties: true
        valor_venta:
          type: number
          example: 254.2
        total_impuestos:
          type:
          - number
          - 'null'
          example: 45.76
    DocumentFile:
      type: object
      description: Archivo del documento (XML firmado, CDR, PDFs cacheados).
      required:
      - kind
      - url
      - sha256
      - size_bytes
      properties:
        kind:
          type: string
          enum:
          - xml
          - cdr
          - pdf_a4
          - pdf_a5
          - pdf_80mm
          - pdf_50mm
        url:
          type: string
          description: Ruta de descarga (`/v1/documents/{id}/files/…`).
          example: /v1/documents/01J2ZK8Q3WPRK9C7VE0M2T5H4X/files/xml
        sha256:
          type: string
          example: 9f2c7d1e8a4b6c3f5d0e2a7b9c1d4e6f8a0b2c4d6e8f0a1b3c5d7e9f1a3b5c7d
        size_bytes:
          type: integer
          example: 18342
    DocumentInput:
      type: object
      description: 'Emisión de comprobante. La serie se indica por `series_id` **o** por

        (`tipo_doc`, `serie`). El cliente, por `client_id` (del padrón) **o**

        inline en `cliente` (se upserta al padrón y se snapshotea). Los totales

        son opcionales: si se omiten, el sistema los calcula desde las líneas

        (**cálculo asistido**); si se envían, los valida (CrossValidate) según

        `modo_totales`.

        '
      required:
      - items
      properties:
        series_id:
          type:
          - string
          - 'null'
          description: ID de la serie (alternativa a tipo_doc+serie).
        tipo_doc:
          $ref: '#/components/schemas/TipoDocVenta'
        serie:
          type:
          - string
          - 'null'
          example: F001
        fecha_emision:
          type:
          - string
          - 'null'
          format: date
          description: Default hoy (America/Lima).
        moneda:
          type: string
          default: PEN
          description: Código ISO 4217 (PEN
          USD…).: null
        tipo_cambio:
          type:
          - number
          - 'null'
          example: 3.752
        client_id:
          type:
          - string
          - 'null'
          description: Cliente del padrón (alternativa a `cliente`).
        cliente:
          oneOf:
          - $ref: '#/components/schemas/ClientInput'
          - type: 'null'
          description: Cliente inline (se upserta al padrón por tipo_doc+num_doc).
        affected_doc_id:
          type:
          - string
          - 'null'
          description: NC/ND — ID del documento afectado emitido en el sistema.
        affected_numero:
          type:
          - string
          - 'null'
          description: NC/ND — número del afectado (`F001-100`) si NO vive en el sistema.
          example: F001-100
        cod_motivo:
          type:
          - string
          - 'null'
          description: NC/ND — motivo (catálogo 09 para NC, 10 para ND).
          example: '01'
        des_motivo:
          type:
          - string
          - 'null'
          example: Anulación de la operación
        forma_pago:
          type: string
          enum:
          - contado
          - credito
          default: contado
        cuotas:
          type: array
          description: Cuotas del crédito (requeridas si `forma_pago=credito`).
          items:
            $ref: '#/components/schemas/Cuota'
        items:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/DocumentItemInput'
        modo_totales:
          type: string
          enum:
          - calcular
          - validar
          description: '`calcular`: el sistema calcula todo total omitido. `validar`: exige los totales
            y los cruza contra las líneas (default `validar` cuando se envían totales, `calcular` cuando
            no).'
        op_gravadas:
          type:
          - number
          - 'null'
        op_exoneradas:
          type:
          - number
          - 'null'
        op_inafectas:
          type:
          - number
          - 'null'
        op_exportacion:
          type:
          - number
          - 'null'
        op_gratuitas:
          type:
          - number
          - 'null'
        igv:
          type:
          - number
          - 'null'
        ivap:
          type:
          - number
          - 'null'
        isc:
          type:
          - number
          - 'null'
        icbper:
          type:
          - number
          - 'null'
        otros_tributos:
          type:
          - number
          - 'null'
        otros_cargos:
          type:
          - number
          - 'null'
        descuentos_globales:
          type:
          - number
          - 'null'
        anticipos_total:
          type:
          - number
          - 'null'
          description: Total de anticipos deducidos.
        redondeo:
          type:
          - number
          - 'null'
        total_impuestos:
          type:
          - number
          - 'null'
        importe_total:
          type:
          - number
          - 'null'
        descuentos:
          type: array
          description: Descuentos globales detallados.
          items:
            $ref: '#/components/schemas/CargoDescuento'
        cargos:
          type: array
          description: Cargos globales detallados.
          items:
            $ref: '#/components/schemas/CargoDescuento'
        detraccion:
          oneOf:
          - $ref: '#/components/schemas/Detraccion'
          - type: 'null'
        percepcion:
          oneOf:
          - $ref: '#/components/schemas/Percepcion'
          - type: 'null'
        anticipos:
          type: array
          items:
            $ref: '#/components/schemas/Anticipo'
        guias:
          type: array
          description: Guías de remisión relacionadas.
          items:
            $ref: '#/components/schemas/DocumentoRelacionado'
        documentos_relacionados:
          type: array
          items:
            $ref: '#/components/schemas/DocumentoRelacionado'
        direccion_entrega:
          oneOf:
          - $ref: '#/components/schemas/Address'
          - type: 'null'
        vendedor:
          oneOf:
          - $ref: '#/components/schemas/Vendedor'
          - type: 'null'
        leyendas:
          type: array
          description: Leyendas (catálogo 52). El importe en letras (1000) se genera automáticamente si
            se omite.
          items:
            $ref: '#/components/schemas/Leyenda'
        external_reference:
          type:
          - string
          - 'null'
          description: Referencia del integrador — indexada, filtrable, unicidad opcional por empresa.
          example: ORD-2026-0001542
        metadata:
          type: object
          additionalProperties: true
          description: Datos arbitrarios del integrador (jamás usados para lógica core).
    Document:
      type: object
      description: Comprobante emitido (inmutable salvo en `draft`). Incluye snapshot del cliente, todos
        los totales peruanos, estado SUNAT y archivos.
      required:
      - id
      - tipo_doc
      - serie
      - numero
      - fecha_emision
      - moneda
      - client_tipo_doc
      - client_num_doc
      - client_razon_social
      - estado
      - source
      - created_at
      - updated_at
      properties:
        id:
          type: string
          example: 01J2ZK8Q3WPRK9C7VE0M2T5H4X
        tipo_doc:
          $ref: '#/components/schemas/TipoDocVenta'
        serie:
          type: string
          example: F001
        correlativo:
          type:
          - integer
          - 'null'
          description: Null mientras es borrador.
          example: 123
        numero:
          type: string
          description: Serie-correlativo.
          example: F001-123
        fecha_emision:
          type: string
          format: date
          example: '2026-07-18'
        moneda:
          type: string
          example: PEN
        tipo_cambio:
          type:
          - number
          - 'null'
        branch_id:
          type:
          - string
          - 'null'
        client_id:
          type:
          - string
          - 'null'
        client_tipo_doc:
          $ref: '#/components/schemas/TipoDocIdentidad'
        client_num_doc:
          type: string
          example: '20601234567'
        client_razon_social:
          type: string
          example: COMERCIAL LOS ANDES S.A.C.
        client_snapshot:
          type: object
          additionalProperties: true
          description: Dirección/email del cliente al momento de emitir.
        op_gravadas:
          type:
          - number
          - 'null'
          example: 338.98
        op_exoneradas:
          type:
          - number
          - 'null'
        op_inafectas:
          type:
          - number
          - 'null'
        op_exportacion:
          type:
          - number
          - 'null'
        op_gratuitas:
          type:
          - number
          - 'null'
        igv:
          type:
          - number
          - 'null'
          example: 61.02
        ivap:
          type:
          - number
          - 'null'
        isc:
          type:
          - number
          - 'null'
        icbper:
          type:
          - number
          - 'null'
        otros_tributos:
          type:
          - number
          - 'null'
        otros_cargos:
          type:
          - number
          - 'null'
        descuentos_globales:
          type:
          - number
          - 'null'
        anticipos:
          type:
          - number
          - 'null'
          description: Total de anticipos deducidos.
        redondeo:
          type:
          - number
          - 'null'
        total_impuestos:
          type:
          - number
          - 'null'
          example: 61.02
        importe_total:
          type:
          - number
          - 'null'
          example: 400.0
        detraccion:
          oneOf:
          - $ref: '#/components/schemas/Detraccion'
          - type: 'null'
        percepcion:
          oneOf:
          - $ref: '#/components/schemas/Percepcion'
          - type: 'null'
        forma_pago:
          type:
          - string
          - 'null'
          enum:
          - contado
          - credito
          - null
        cuotas:
          type:
          - array
          - 'null'
          items:
            $ref: '#/components/schemas/Cuota'
        affected_doc_id:
          type:
          - string
          - 'null'
        affected_numero:
          type:
          - string
          - 'null'
        cod_motivo:
          type:
          - string
          - 'null'
        des_motivo:
          type:
          - string
          - 'null'
        estado:
          $ref: '#/components/schemas/EstadoDocumento'
        sunat_ticket:
          type:
          - string
          - 'null'
        sunat_code:
          type:
          - string
          - 'null'
          description: Código del CDR/fault SUNAT.
          example: '0'
        sunat_message:
          type:
          - string
          - 'null'
          example: La Factura numero F001-123, ha sido aceptada
        observations:
          type:
          - array
          - 'null'
          description: Notas del CDR cuando fue aceptada con observaciones.
          items:
            type: object
            additionalProperties: true
        voided_by_summary_id:
          type:
          - string
          - 'null'
          description: Summary (RA/RR) que la dio de baja.
        external_reference:
          type:
          - string
          - 'null'
          example: ORD-2026-0001542
        source:
          type: string
          enum:
          - api
          - ui
          - import
          - scheduled
          - system
          example: api
        metadata:
          type: object
          additionalProperties: true
        items:
          type: array
          items:
            $ref: '#/components/schemas/DocumentItem'
        files:
          type: array
          items:
            $ref: '#/components/schemas/DocumentFile'
        issued_by:
          type:
          - string
          - 'null'
          description: ID del usuario o token que emitió.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    DocumentPreview:
      type: object
      description: Resultado del cálculo asistido — nada se persiste.
      required:
      - items
      - totales
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/DocumentItem'
        totales:
          type: object
          properties:
            op_gravadas:
              type: number
              example: 338.98
            op_exoneradas:
              type: number
            op_inafectas:
              type: number
            op_exportacion:
              type: number
            op_gratuitas:
              type: number
            igv:
              type: number
              example: 61.02
            ivap:
              type: number
            isc:
              type: number
            icbper:
              type: number
            otros_tributos:
              type: number
            otros_cargos:
              type: number
            descuentos_globales:
              type: number
            anticipos:
              type: number
            redondeo:
              type: number
            total_impuestos:
              type: number
              example: 61.02
            importe_total:
              type: number
              example: 400.0
        leyendas:
          type: array
          description: Leyendas generadas (incluye importe en letras).
          items:
            $ref: '#/components/schemas/Leyenda'
        warnings:
          type: array
          description: Diferencias detectadas al validar totales enviados.
          items:
            $ref: '#/components/schemas/FieldError'
    DocumentEvent:
      type: object
      description: Evento de la bitácora append-only del documento.
      required:
      - id
      - type
      - created_at
      properties:
        id:
          type: string
        type:
          type: string
          description: Tipo de evento (`created`, `signed`, `submitted`, `cdr.accepted`, `cdr.rejected`,
            `email.sent`, `webhook.delivered`, `void.requested`, …).
          example: cdr.accepted
        detail:
          type: object
          additionalProperties: true
          example:
            sunat_code: '0'
            latency_ms: 1240
        actor:
          type:
          - string
          - 'null'
          description: Usuario/token/sistema que originó el evento.
        created_at:
          type: string
          format: date-time
    SummaryInput:
      type: object
      description: Creación manual de un proceso colectivo. `RC` resumen diario de boletas; `RA` comunicación
        de baja; `RR` reversión (baja de boletas ya resumidas).
      required:
      - tipo
      - fecha_referencia
      - documentos
      properties:
        tipo:
          type: string
          enum:
          - RC
          - RA
          - RR
        fecha_referencia:
          type: string
          format: date
          description: Fecha de emisión de las boletas (RC) o de los documentos a dar de baja (RA/RR).
          example: '2026-07-17'
        documentos:
          type: array
          minItems: 1
          items:
            type: object
            required:
            - document_id
            properties:
              document_id:
                type: string
              accion:
                type: string
                enum:
                - add
                - modify
                - void
                default: add
                description: Acción del ítem en el resumen (catálogo RC).
              motivo_baja:
                type:
                - string
                - 'null'
                example: Error en datos del cliente
    Summary:
      type: object
      description: Proceso colectivo RC/RA/RR con su ciclo de ticket SUNAT.
      required:
      - id
      - tipo
      - identificador
      - fecha_referencia
      - correlativo
      - estado
      - created_at
      - updated_at
      properties:
        id:
          type: string
          example: 01J2ZKD8W0Y2A4C6E8G0I2K4M6
        tipo:
          type: string
          enum:
          - RC
          - RA
          - RR
          example: RA
        identificador:
          type: string
          example: RA-20260718-001
        fecha_referencia:
          type: string
          format: date
          example: '2026-07-17'
        correlativo:
          type: integer
          example: 1
        estado:
          $ref: '#/components/schemas/EstadoSummary'
        sunat_ticket:
          type:
          - string
          - 'null'
          example: '1594384630963'
        sunat_code:
          type:
          - string
          - 'null'
          example: '0'
        sunat_message:
          type:
          - string
          - 'null'
        documentos:
          type: array
          items:
            type: object
            required:
            - document_id
            - accion
            properties:
              document_id:
                type: string
              numero:
                type: string
                example: B001-4512
              accion:
                type: string
                enum:
                - add
                - modify
                - void
              motivo_baja:
                type:
                - string
                - 'null'
        files:
          type: array
          description: XML firmado y CDR del proceso.
          items:
            $ref: '#/components/schemas/DocumentFile'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    DespatchPunto:
      type: object
      description: Punto de partida o llegada del traslado.
      required:
      - ubigeo
      - direccion
      properties:
        ubigeo:
          type: string
          pattern: ^[0-9]{6}$
          example: '150101'
        direccion:
          type: string
          example: Jr. Comercio 456, Cercado de Lima
        ruc_relacionado:
          type:
          - string
          - 'null'
          description: RUC del establecimiento cuando pertenece a un tercero.
        cod_local:
          type:
          - string
          - 'null'
          description: Código de establecimiento anexo SUNAT (requerido en traslados entre establecimientos).
          example: '0001'
    Transportista:
      type: object
      description: Transportista (modalidad 01 — transporte público).
      required:
      - tipo_doc
      - num_doc
      - razon_social
      properties:
        tipo_doc:
          $ref: '#/components/schemas/TipoDocIdentidad'
        num_doc:
          type: string
          example: '20512345678'
        razon_social:
          type: string
          example: TRANSPORTES RÁPIDOS DEL SUR S.A.
        nro_mtc:
          type:
          - string
          - 'null'
          example: 0001-MTC/15.01
    Vehiculo:
      type: object
      description: Vehículo del traslado (modalidad 02 — transporte privado).
      required:
      - placa
      properties:
        placa:
          type: string
          example: ABC-123
        tarjeta_circulacion:
          type:
          - string
          - 'null'
          description: TUC / tarjeta única de circulación.
        autorizacion:
          type:
          - object
          - 'null'
          description: Autorización especial del vehículo.
          properties:
            entidad:
              type: string
              description: Entidad emisora (catálogo D37).
            numero:
              type: string
        secundarios:
          type: array
          description: Vehículos secundarios del convoy.
          items:
            type: object
            required:
            - placa
            properties:
              placa:
                type: string
                example: XYZ-789
              tarjeta_circulacion:
                type:
                - string
                - 'null'
    Chofer:
      type: object
      description: Conductor (modalidad 02 — transporte privado).
      required:
      - tipo_doc
      - num_doc
      - licencia
      - nombres
      - apellidos
      properties:
        tipo:
          type: string
          enum:
          - Principal
          - Secundario
          default: Principal
        tipo_doc:
          $ref: '#/components/schemas/TipoDocIdentidad'
        num_doc:
          type: string
          example: '42345678'
        licencia:
          type: string
          example: Q42345678
        nombres:
          type: string
          example: Juan Carlos
        apellidos:
          type: string
          example: Rojas Paredes
    DespatchEnvio:
      type: object
      description: Datos del envío (formato GRE 2022).
      required:
      - cod_traslado
      - mod_traslado
      - fecha_traslado
      - peso_total
      - und_peso_total
      - partida
      - llegada
      properties:
        cod_traslado:
          type: string
          description: Motivo del traslado (catálogo 20; "01" venta, "02" compra, "04" traslado entre
            establecimientos, "08" importación, "13" otros…).
          example: '01'
        des_traslado:
          type:
          - string
          - 'null'
          description: Descripción del motivo (requerida con cod_traslado=13).
        mod_traslado:
          type: string
          enum:
          - '01'
          - '02'
          description: Modalidad (catálogo 18) — "01" transporte público, "02" privado.
        fecha_traslado:
          type: string
          format: date
          example: '2026-07-19'
        peso_total:
          type: number
          example: 425.5
        und_peso_total:
          type: string
          enum:
          - KGM
          - TNE
          description: Unidad del peso bruto total.
          example: KGM
        num_bultos:
          type:
          - integer
          - 'null'
          example: 10
        indicadores:
          type: array
          description: Indicadores GRE 2022 (`SUNAT_Envio_IndicadorTrasladoVehiculoM1L`, `SUNAT_Envio_IndicadorRetornoVehiculoEnvaseVacio`,
            `SUNAT_Envio_IndicadorRetornoVehiculoVacio`, `SUNAT_Envio_IndicadorTransbordoProgramado`,
            `SUNAT_Envio_IndicadorVehiculoConductoresTransp`…).
          items:
            type: string
          example:
          - SUNAT_Envio_IndicadorTrasladoVehiculoM1L
        transportista:
          oneOf:
          - $ref: '#/components/schemas/Transportista'
          - type: 'null'
          description: Requerido con mod_traslado=01 (salvo indicador M1L).
        vehiculos:
          type: array
          description: Vehículo principal y secundarios (mod_traslado=02).
          items:
            $ref: '#/components/schemas/Vehiculo'
        choferes:
          type: array
          items:
            $ref: '#/components/schemas/Chofer'
        partida:
          $ref: '#/components/schemas/DespatchPunto'
        llegada:
          $ref: '#/components/schemas/DespatchPunto'
        contenedores:
          type: array
          description: Números de contenedor (importación).
          items:
            type: string
          example:
          - TCLU-2949780
        puerto:
          type:
          - object
          - 'null'
          description: Puerto de embarque/desembarque (catálogo D45).
          properties:
            codigo:
              type: string
              example: PECLL
            nombre:
              type: string
              example: Puerto del Callao
        aeropuerto:
          type:
          - object
          - 'null'
          properties:
            codigo:
              type: string
              example: PELIM
            nombre:
              type: string
              example: Aeropuerto Jorge Chávez
    DespatchItem:
      type: object
      description: Bien trasladado.
      required:
      - descripcion
      - unidad
      - cantidad
      properties:
        codigo:
          type:
          - string
          - 'null'
          example: P001
        descripcion:
          type: string
          example: Cemento Portland tipo I x 42.5 kg
        unidad:
          type: string
          default: NIU
        cantidad:
          type: number
          example: 10
        cod_prod_sunat:
          type:
          - string
          - 'null'
        partida_arancelaria:
          type:
          - string
          - 'null'
          description: Requerida en algunos motivos aduaneros.
    DespatchInput:
      type: object
      description: Emisión de guía de remisión remitente (09). La serie se indica por `series_id` o por
        `serie` (T###); el destinatario, por `client_id` o inline.
      required:
      - envio
      - items
      properties:
        series_id:
          type:
          - string
          - 'null'
        serie:
          type:
          - string
          - 'null'
          example: T001
        fecha_emision:
          type:
          - string
          - 'null'
          format: date
          description: Default hoy (America/Lima).
        client_id:
          type:
          - string
          - 'null'
        destinatario:
          oneOf:
          - $ref: '#/components/schemas/ClientInput'
          - type: 'null'
          description: Destinatario inline (alternativa a `client_id`).
        envio:
          $ref: '#/components/schemas/DespatchEnvio'
        items:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/DespatchItem'
        documentos_relacionados:
          type: array
          description: Documentos relacionados (factura de la venta, DAM, etc.).
          items:
            $ref: '#/components/schemas/DocumentoRelacionado'
        observaciones:
          type:
          - string
          - 'null'
        external_reference:
          type:
          - string
          - 'null'
          example: GR-2026-000731
        metadata:
          type: object
          additionalProperties: true
    Despatch:
      type: object
      description: Guía de remisión emitida, con su ciclo ticket GRE → CDR.
      required:
      - id
      - tipo_doc
      - serie
      - correlativo
      - numero
      - fecha_emision
      - estado
      - created_at
      - updated_at
      properties:
        id:
          type: string
          example: 01J2ZKE9X1Z3B5D7F9H1J3L5N7
        tipo_doc:
          type: string
          enum:
          - 09
          description: Guía de remisión remitente.
        serie:
          type: string
          example: T001
        correlativo:
          type: integer
          example: 456
        numero:
          type: string
          example: T001-456
        fecha_emision:
          type: string
          format: date
        client_id:
          type:
          - string
          - 'null'
        client_tipo_doc:
          $ref: '#/components/schemas/TipoDocIdentidad'
        client_num_doc:
          type: string
          example: '20601234567'
        client_razon_social:
          type: string
          example: COMERCIAL LOS ANDES S.A.C.
        envio:
          $ref: '#/components/schemas/DespatchEnvio'
        items:
          type: array
          items:
            $ref: '#/components/schemas/DespatchItem'
        documentos_relacionados:
          type: array
          items:
            $ref: '#/components/schemas/DocumentoRelacionado'
        estado:
          $ref: '#/components/schemas/EstadoDocumento'
        sunat_ticket:
          type:
          - string
          - 'null'
          example: 1594384630963-abc
        sunat_code:
          type:
          - string
          - 'null'
        sunat_message:
          type:
          - string
          - 'null'
        external_reference:
          type:
          - string
          - 'null'
          example: GR-2026-000731
        metadata:
          type: object
          additionalProperties: true
        files:
          type: array
          items:
            $ref: '#/components/schemas/DocumentFile'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    WebhookEndpointInput:
      type: object
      required:
      - url
      - events
      properties:
        url:
          type: string
          format: uri
          example: https://erp.losandes.pe/hooks/emitir-cpe
        events:
          type: array
          minItems: 1
          description: Tipos de evento versionados a suscribir, o `["*"]` para todos.
          items:
            type: string
          example:
          - document.accepted.v1
          - document.rejected.v1
          - document.voided.v1
        active:
          type: boolean
          default: true
    WebhookEndpoint:
      type: object
      required:
      - id
      - url
      - events
      - active
      - created_at
      properties:
        id:
          type: string
          example: 01J2ZKF0Y2A4C6E8G0I2K4M6O8
        url:
          type: string
          format: uri
          example: https://erp.losandes.pe/hooks/emitir-cpe
        events:
          type: array
          items:
            type: string
          example:
          - document.accepted.v1
          - document.rejected.v1
        active:
          type: boolean
        failure_streak:
          type: integer
          description: Fallos consecutivos (alerta y deshabilitación tras N).
          example: 0
        created_at:
          type: string
          format: date-time
    WebhookDelivery:
      type: object
      required:
      - id
      - endpoint_id
      - event_type
      - event_id
      - status
      - attempt
      - created_at
      properties:
        id:
          type: string
        endpoint_id:
          type: string
        event_type:
          type: string
          example: document.accepted.v1
        event_id:
          type: string
          description: ID único del evento — idempotencia del receptor (el mismo evento puede reintentarse).
          example: 01J2ZKG1Z3B5D7F9H1J3L5N7P9
        payload:
          type: object
          additionalProperties: true
          example:
            event: document.accepted.v1
            document_id: 01J2ZK8Q3WPRK9C7VE0M2T5H4X
            numero: F001-123
            sunat_code: '0'
        attempt:
          type: integer
          example: 1
        status:
          type: string
          enum:
          - pending
          - delivered
          - failed
          - dead
        response_code:
          type:
          - integer
          - 'null'
          example: 200
        response_body:
          type:
          - string
          - 'null'
          description: Respuesta del receptor (truncada).
        next_retry_at:
          type:
          - string
          - 'null'
          format: date-time
        delivered_at:
          type:
          - string
          - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
    PortalLink:
      type: object
      description: Enlace firmado del portal público del documento.
      required:
      - id
      - document_id
      - url
      - created_at
      properties:
        id:
          type: string
        document_id:
          type: string
          example: 01J2ZK8Q3WPRK9C7VE0M2T5H4X
        url:
          type: string
          format: uri
          description: URL pública (`/p/{token}`). El token solo se entrega aquí.
          example: https://emitir-cpe.com/p/pl_9f2c7d1e8a4b6c3f5d0e2a7b9c1d4e6f
        expires_at:
          type:
          - string
          - 'null'
          format: date-time
        max_accesses:
          type:
          - integer
          - 'null'
          example: null
        access_count:
          type: integer
          example: 0
        revoked_at:
          type:
          - string
          - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
    ApiTokenInput:
      type: object
      required:
      - name
      - abilities
      properties:
        name:
          type: string
          example: ERP producción
        abilities:
          type: array
          minItems: 1
          description: Abilities granulares del token.
          items:
            type: string
            enum:
            - documents:read
            - documents:write
            - documents:void
            - files:read
            - series:manage
            - clients:read
            - clients:write
            - webhooks:manage
            - summaries:manage
            - despatches:write
            - company:manage
            - reports:read
          example:
          - documents:read
          - documents:write
          - files:read
        expires_at:
          type:
          - string
          - 'null'
          format: date-time
          description: Expiración opcional (null = sin expiración).
    ApiToken:
      type: object
      required:
      - id
      - name
      - token_prefix
      - abilities
      - created_at
      properties:
        id:
          type: string
          example: 01J2ZKH2A4C6E8G0I2K4M6O8Q0
        name:
          type: string
          example: ERP producción
        token_prefix:
          type: string
          description: Primeros caracteres visibles para identificar el token.
          example: ecp_live_9f2c
        abilities:
          type: array
          items:
            type: string
          example:
          - documents:read
          - documents:write
          - files:read
        expires_at:
          type:
          - string
          - 'null'
          format: date-time
        last_used_at:
          type:
          - string
          - 'null'
          format: date-time
        revoked_at:
          type:
          - string
          - 'null'
          format: date-time
        created_by:
          type:
          - string
          - 'null'
        created_at:
          type: string
          format: date-time
    Export:
      type: object
      description: Export asíncrono (CSV/Excel del listado o zip de archivos).
      required:
      - id
      - kind
      - status
      - created_at
      properties:
        id:
          type: string
          example: 01J2ZKI3B5D7F9H1J3L5N7P9R1
        kind:
          type: string
          enum:
          - documents_csv
          - documents_xlsx
          - files_zip
        status:
          type: string
          enum:
          - pending
          - running
          - done
          - failed
          example: pending
        download_url:
          type:
          - string
          - 'null'
          description: URL de descarga cuando `status=done` (temporal — el export expira).
        error:
          type:
          - string
          - 'null'
        expires_at:
          type:
          - string
          - 'null'
          format: date-time
          description: Limpieza automática del archivo generado.
        created_at:
          type: string
          format: date-time
    DocumentInputF2:
      allOf:
      - $ref: '#/components/schemas/DocumentInput'
      - type: object
        properties:
          retencion:
            $ref: '#/components/schemas/RetencionCPEInput'
          percepcion_cpe:
            $ref: '#/components/schemas/PercepcionCPEInput'
          liquidacion:
            $ref: '#/components/schemas/LiquidacionInput'
    RetencionCPEInput:
      type: object
      description: Bloque del Comprobante de Retención (tipo_doc 20). Obligatorio para 20.
      required:
      - regimen
      - detalles
      properties:
        regimen:
          type: string
          description: 'Catálogo 23: `01` = régimen general (tasa 3%).'
          enum:
          - '01'
        tasa:
          type: number
          description: Tasa % explícita (default derivada del régimen).
        observacion:
          type: string
          maxLength: 250
        detalles:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/DetalleCPE'
    PercepcionCPEInput:
      type: object
      description: 'Bloque del Comprobante de Percepción (tipo_doc 40). Obligatorio

        para 40. Distinto del campo `percepcion` de las facturas (que es

        la percepción DE una venta 01).

        '
      required:
      - regimen
      - detalles
      properties:
        regimen:
          type: string
          description: 'Catálogo 22: `01` = 2%, `02` = 1% (combustible), `03` = 0.5%.'
          enum:
          - '01'
          - '02'
          - '03'
        tasa:
          type: number
          description: Tasa % explícita (default derivada del régimen).
        observacion:
          type: string
          maxLength: 250
        detalles:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/DetalleCPE'
    DetalleCPE:
      type: object
      description: Comprobante relacionado (la factura retenida/percibida).
      required:
      - numero
      - fecha_emision
      - importe_total
      properties:
        tipo_doc:
          type: string
          default: '01'
          description: Tipo del comprobante relacionado (catálogo 01).
        numero:
          type: string
          description: SERIE-CORRELATIVO ("F001-1").
        fecha_emision:
          type: string
          format: date
        importe_total:
          type: number
          description: Importe total del comprobante relacionado.
        moneda:
          type: string
          default: (moneda del CPE)
        fecha_operacion:
          type: string
          format: date
          description: Fecha de la retención/percepción (default fecha de emisión del CPE 20/40).
        importe:
          type: number
          description: Monto retenido/percibido (default tasa% del importe_total).
        saldo:
          type: number
          description: Neto pagado (20, total − importe) o cobrado (40, total + importe); derivado si
            se omite.
    LiquidacionInput:
      type: object
      description: Extras de la Liquidación de Compra (tipo_doc 04); el resto son los campos de venta.
      properties:
        fec_vencimiento:
          type: string
          format: date
        observacion:
          type: string
        retencion_ir:
          type: object
          description: Retención del impuesto a la renta (solo operaciones ≥ S/ 700).
          required:
          - tasa
          properties:
            tasa:
              type: number
              enum:
              - 1.5
              - 4
              description: 1.5% general; 4% minería aurífera artesanal.
            base:
              type: number
              description: Base imponible (default importe total de la operación).
            monto:
              type: number
              description: Importe retenido (default tasa% de la base).
    DocumentTemplate:
      type: object
      required:
      - id
      - name
      - payload
      - created_at
      properties:
        id:
          type: string
        name:
          type: string
        payload:
          $ref: '#/components/schemas/DocumentInputF2'
        created_at:
          type: string
          format: date-time
    DocumentTemplateInput:
      type: object
      required:
      - name
      - payload
      properties:
        name:
          type: string
          maxLength: 120
          description: Único por empresa.
        payload:
          $ref: '#/components/schemas/DocumentInputF2'
    DocumentStatusEvent:
      type: object
      description: Payload del evento `estado` del stream SSE.
      required:
      - id
      - estado
      - updated_at
      properties:
        id:
          type: string
        estado:
          type: string
          enum:
          - draft
          - pending
          - signed
          - submitted
          - accepted
          - accepted_with_obs
          - rejected
          - error
          - contingency
          - voided
        sunat_code:
          type:
          - string
          - 'null'
        sunat_message:
          type:
          - string
          - 'null'
        updated_at:
          type: string
          format: date-time
    CpeLookupInput:
      type: object
      required:
      - tipo_doc
      - serie
      - numero
      properties:
        ruc:
          type: string
          pattern: ^[0-9]{11}$
          description: RUC del emisor del comprobante consultado (default el RUC de la empresa).
        tipo_doc:
          type: string
          minLength: 2
          maxLength: 2
          description: Catálogo 01.
        serie:
          type: string
          pattern: ^[A-Z0-9]{4}$
        numero:
          type: integer
          minimum: 1
    CpeLookup:
      type: object
      required:
      - id
      - status
      - params
      - created_at
      properties:
        id:
          type: string
        status:
          type: string
          enum:
          - pending
          - done
          - failed
        params:
          $ref: '#/components/schemas/CpeLookupInput'
        result:
          type:
          - object
          - 'null'
          description: 'Resultado SUNAT (`done`): `code` y `message` crudos de

            getStatusCdr más `estado` de máquina (`accepted` = 0001,

            `not_found` = 0002, `voided` = 0003, `unknown`, `error`) y

            `con_cdr`. En `failed`: `{error}` con el fallo técnico.

            '
        created_at:
          type: string
          format: date-time
    PortalClientList:
      type: object
      description: Vista pública del portal del cliente (listado por token).
      required:
      - emisor_razon
      - emisor_ruc
      - cliente_razon
      - cliente_doc
      - docs
      properties:
        emisor_razon:
          type: string
        emisor_ruc:
          type: string
          example: '20123456789'
        cliente_razon:
          type: string
        cliente_doc:
          type: string
        docs:
          type: array
          items:
            type: object
            required:
            - document_id
            - numero
            - tipo_doc
            - fecha_emision
            - moneda
            - importe_total
            - estado
            - files
            properties:
              document_id:
                type: string
              numero:
                type: string
                example: F001-123
              tipo_doc:
                type: string
                example: '01'
              fecha_emision:
                type: string
                example: '2026-07-19'
              moneda:
                type: string
                example: PEN
              importe_total:
                type: string
                example: '236.00'
              estado:
                type: string
                example: accepted
              files:
                type: array
                items:
                  type: string
                  enum:
                  - xml
                  - cdr
                  - pdf
    Plan:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        limits:
          type: object
          description: 'Claves entendidas: docs_month, users, webhooks, series, retention_years. Ausente
            o <= 0 = sin límite.'
          additionalProperties:
            type: integer
        price:
          type: object
          description: '{PEN: 49.00, period: month|year}'
          additionalProperties: true
    License:
      type: object
      properties:
        id:
          type: string
        plan_id:
          type: string
        status:
          type: string
          enum:
          - trial
          - active
          - past_due
          - suspended
          - cancelled
        trial_ends_at:
          type: string
          format: date-time
          nullable: true
        current_period_end:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    Subscription:
      type: object
      properties:
        plan:
          $ref: '#/components/schemas/Plan'
          nullable: true
        status:
          type: string
          enum:
          - trial
          - active
          - past_due
        trial_ends_at:
          type: string
          format: date-time
          nullable: true
        current_period_end:
          type: string
          format: date-time
          nullable: true
        period:
          type: string
          description: YYYYMM (America/Lima)
        limits:
          type: object
          additionalProperties:
            type: integer
          description: Límites efectivos (plan ⊕ overrides de la licencia)
        usage:
          type: array
          items:
            type: object
            properties:
              metric:
                type: string
                enum:
                - documents_issued
                - users
                - webhooks
                - series
              used:
                type: integer
              limit:
                type: integer
                description: 0 = sin límite
              pct:
                type: number
    CheckoutIntent:
      type: object
      properties:
        id:
          type: string
        provider:
          type: string
        plan_id:
          type: string
        status:
          type: string
          enum:
          - pending
          - completed
        url:
          type: string
          description: URL de pago del proveedor (vacía en manual)
        instructions:
          type: string
        created_at:
          type: string
          format: date-time
    ScheduledDocumentInput:
      type: object
      required:
      - template_id
      - cron
      properties:
        template_id:
          type: string
          description: Plantilla de document-templates
        cron:
          type: string
          description: '5 campos "minuto hora día-mes mes día-semana" en hora America/Lima. Sintaxis soportada:
            número, *, a-b, */n, a-b/n y listas con coma (día-semana 0-7, 0 y 7 = domingo). Semántica
            estándar día-mes/día-semana (si ambos restringidos, matchea cualquiera).

            '
          example: 0 8 1 * *
        active:
          type: boolean
          default: true
    ScheduledDocument:
      type: object
      properties:
        id:
          type: string
        template_id:
          type: string
        cron:
          type: string
        active:
          type: boolean
        last_run_at:
          type: string
          format: date-time
          nullable: true
        next_run_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    ClientPortalLink:
      type: object
      properties:
        id:
          type: string
        client_id:
          type: string
        url:
          type: string
          description: https://…/pc/{token} — única vez que viaja el token
        expires_at:
          type: string
          format: date-time
          nullable: true
        max_accesses:
          type: integer
          nullable: true
        access_count:
          type: integer
        revoked_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    ProblemPlanLimitReached:
      description: 'RFC 7807 con code `plan_limit_reached` (HTTP 422): el límite mensual de documentos
        del plan está agotado. No es retryable dentro del periodo; el integrador debe subir de plan o
        esperar el siguiente mes. Emitida por POST /v1/documents y demás caminos de emisión.

        '
      type: object
      properties:
        type:
          type: string
          example: https://docs.emitir-cpe.com/errors/plan_limit_reached
        title:
          type: string
          example: Límite del plan alcanzado
        status:
          type: integer
          example: 422
        code:
          type: string
          example: plan_limit_reached
        detail:
          type: string
        request_id:
          type: string
    NotificationType:
      type: string
      enum:
      - document.rejected
      - document.error
      - certificate.expiring
      - summary.failed
      - webhook.endpoint.disabled
      - sire.process.completed
      - import.completed
      - plan.limit.near
      - digest
    Notification:
      type: object
      properties:
        id:
          type: string
        type:
          $ref: '#/components/schemas/NotificationType'
        title:
          type: string
        body:
          type: string
        link:
          type: string
          nullable: true
        company_wide:
          type: boolean
          description: true = notificación para toda la empresa (no personal)
        read_at:
          type: string
          format: date-time
          nullable: true
        archived_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    NotificationPreference:
      type: object
      properties:
        type:
          $ref: '#/components/schemas/NotificationType'
        channels:
          type: array
          items:
            type: string
            enum:
            - inapp
            - email
        default:
          type: boolean
          description: true = sin fila guardada, aplican los defaults
    TwoFactorChallenge:
      type: object
      description: 'Respuesta de POST /v1/auth/login cuando el usuario tiene 2FA activo (en vez de AuthSession).
        Completar con POST /v1/auth/2fa/challenge.

        '
      properties:
        requires_2fa:
          type: boolean
          enum:
          - true
        challenge_token:
          type: string
          description: Token de un solo uso
          5 minutos: null
        expires_at:
          type: string
          format: date-time
    IdentityResult:
      type: object
      properties:
        tipo:
          type: string
          enum:
          - ruc
          - dni
        numero:
          type: string
        nombre:
          type: string
          description: Nombre (DNI) o razón social (RUC)
        razon_social:
          type: string
        estado:
          type: string
        direccion:
          type: string
        cached:
          type: boolean
          description: true = respondido desde el caché compartido
        fetched_at:
          type: string
          format: date-time
    IdentitySettings:
      type: object
      properties:
        url_template:
          type: string
        auth_header:
          type: string
        has_token:
          type: boolean
    ImportRowError:
      type: object
      properties:
        row:
          type: integer
          description: Fila del archivo (la cabecera es la 1); 0 = error del batch
        message:
          type: string
    ImportBatch:
      type: object
      properties:
        id:
          type: string
        kind:
          type: string
          enum:
          - documents
          - clients
          - catalog_items
        status:
          type: string
          enum:
          - validating
          - ready
          - running
          - done
          - failed
          - cancelled
        totals:
          type: object
          properties:
            rows:
              type: integer
              description: Filas de datos leídas
            ok:
              type: integer
              description: Filas válidas en la validación
            errors:
              type: integer
            processed:
              type: integer
            succeeded:
              type: integer
            failed:
              type: integer
            errors_file:
              type: string
              description: Clave en storage del CSV completo de errores
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ImportRowError'
          description: Truncado a 100; el archivo completo en totals.errors_file
        created_by:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    PDFSettings:
      type: object
      properties:
        footer_text:
          type: string
          description: Pie personalizado de la representación impresa
        primary_color:
          type: string
          description: '#rrggbb (persistido; aplicación en plantillas pendiente)'
        show_unit_price:
          type: boolean
          default: true
        telefono:
          type: string
        email:
          type: string
        web:
          type: string
    ReportDashboard:
      type: object
      properties:
        periodo:
          type: object
          properties:
            desde:
              type: string
            hasta:
              type: string
        totales:
          type: object
          properties:
            documentos:
              type: integer
            monto_pen:
              type: string
            aceptados:
              type: integer
            rechazados:
              type: integer
            pendientes:
              type: integer
        por_estado:
          type: object
          additionalProperties:
            type: integer
        por_tipo:
          type: array
          items:
            type: object
            properties:
              tipo_doc:
                type: string
              count:
                type: integer
              monto_pen:
                type: string
        top_clientes:
          type: array
          items:
            type: object
            properties:
              num_doc:
                type: string
              razon_social:
                type: string
              count:
                type: integer
              monto_pen:
                type: string
        serie_diaria:
          type: array
          items:
            type: object
            properties:
              fecha:
                type: string
              count:
                type: integer
              monto_pen:
                type: string
    ReportProductsPage:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ReportProducto'
        meta:
          $ref: '#/components/schemas/PageMeta'
    ReportProducto:
      type: object
      properties:
        codigo:
          type: string
        descripcion:
          type: string
        cantidad:
          type: string
        valor_venta_pen:
          type: string
        documentos:
          type: integer
    ReportReceivablesPage:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ReportReceivable'
        meta:
          $ref: '#/components/schemas/PageMeta'
    ReportReceivable:
      type: object
      properties:
        document_id:
          type: string
        numero:
          type: string
        cliente_razon_social:
          type: string
        fecha_emision:
          type: string
        moneda:
          type: string
        importe_total:
          type: string
        cuotas:
          type: array
          items:
            type: object
            properties:
              monto:
                type: string
              fecha:
                type: string
              vencida:
                type: boolean
    ReportSalesRegisterPage:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ReportSalesRow'
        meta:
          $ref: '#/components/schemas/PageMeta'
    ReportSalesRow:
      type: object
      properties:
        fecha_emision:
          type: string
        tipo_doc:
          type: string
        serie:
          type: string
        numero:
          type: string
        cliente_tipo_doc:
          type: string
        cliente_num_doc:
          type: string
        cliente_razon_social:
          type: string
        moneda:
          type: string
        op_gravadas:
          type: string
        op_exoneradas:
          type: string
        op_inafectas:
          type: string
        op_exportacion:
          type: string
        igv:
          type: string
        isc:
          type: string
        icbper:
          type: string
        otros_tributos:
          type: string
        importe_total:
          type: string
        estado:
          type: string
    SireProcess:
      type: object
      description: Proceso SIRE orquestado. El webhook `sire.process.completed.v1` y la notificación `sire.process.completed`
        avisan cuando converge.
      required:
      - id
      - libro
      - periodo
      - operation
      - estado
      - detail
      - created_at
      - updated_at
      properties:
        id:
          type: string
          example: 01J1SIREPROCESS0000000001
        libro:
          type: string
          enum:
          - rvie
          - rce
        periodo:
          type: string
          example: '202607'
        operation:
          type: string
          description: Operación del catálogo cerrado.
          enum:
          - descargar_propuesta
          - aceptar_propuesta
          - reemplazar_propuesta
          - registrar_preliminar
          - exportar_preliminar
          - incluir_excluir
          - no_domiciliados
          - cargar_ajustes
          - cargar_ajustes_previos
          - descargar_ajustes
          - enviar_ajustes
          - eliminar_ajustes
        ticket:
          type:
          - string
          - 'null'
          description: numTicket SUNAT (null en cargas TUS, que se correlacionan por nombre de archivo,
            y en operaciones síncronas).
        estado:
          type: string
          enum:
          - pending
          - sent
          - in_process
          - done
          - failed
        detail:
          type: object
          additionalProperties: true
          description: Respuesta SUNAT y contadores (cod_estado_proceso, filas_validadas, filas_error,
            nom_archivo, error…).
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        files:
          type: array
          description: Archivos descargados del fileserver SUNAT (solo en el detalle).
          items:
            $ref: '#/components/schemas/SireFile'
    SireFile:
      type: object
      description: Archivo persistido de un proceso SIRE.
      required:
      - id
      - nombre
      - url
      - sha256
      - size_bytes
      - created_at
      properties:
        id:
          type: string
        nombre:
          type: string
          example: 20100176450-CPF-202607-01.zip
        url:
          type: string
          description: Ruta de descarga autenticada.
          example: /v1/sire/processes/01J1.../files/01J2...
        sha256:
          type: string
        size_bytes:
          type: integer
          format: int64
        created_at:
          type: string
          format: date-time
    SirePeriodo:
      type: object
      description: Periodo habilitado del libro según SUNAT.
      required:
      - periodo
      - cod_estado
      - des_estado
      properties:
        periodo:
          type: string
          example: '202607'
        cod_estado:
          type: string
          example: '1'
        des_estado:
          type: string
          example: Activo
        ejercicio:
          type: string
          example: '2026'
        des_ejercicio:
          type: string
          example: Vigente
    SireArchivoCarga:
      type: object
      description: Archivo de carga TUS en base64 (TXT comprimido en ZIP según RS 112-2021; el nombre
        debe seguir la convención SUNAT).
      required:
      - filename
      - contenido_base64
      properties:
        filename:
          type: string
          example: LE2010017645020260700140400021112.zip
        contenido_base64:
          type: string
          format: byte
        periodos_anteriores:
          type: boolean
          default: false
          description: Solo adjustments/load — variante AJP (periodos anteriores a la vigencia).
        no_domiciliados:
          type: boolean
          default: false
          description: Solo adjustments/load en rce — variante no domiciliados.
    SireAjuste:
      type: object
      description: Colección de ajustes posteriores RCE y su detalle de comprobantes.
      required:
      - cod_ajuste_posterior
      - detalle
      properties:
        cod_ajuste_posterior:
          type: string
        detalle:
          type: array
          items:
            type: object
            required:
            - cod_tipo_cdp
            - num_cdp
            properties:
              cod_tipo_cdp:
                type: string
                example: '01'
              num_serie_cdp:
                type: string
                example: F001
              num_cdp:
                type: string
                example: '123'
              cod_car:
                type: string
                description: CAR SUNAT (27 dígitos).
              id:
                type: string
              num_ticket:
                type: string
    SireReconciliation:
      type: object
      description: Informe de conciliación RVIE vs documentos del sistema.
      required:
      - periodo
      - process_id
      - file_id
      - total_sunat
      - total_sistema
      - coincidentes
      - faltantes_en_sunat
      - faltantes_en_sistema
      - diferencias_monto
      properties:
        periodo:
          type: string
          example: '202607'
        process_id:
          type: string
          description: Proceso de descarga de propuesta usado.
        file_id:
          type: string
          description: Archivo de propuesta parseado.
        total_sunat:
          type: integer
        total_sistema:
          type: integer
        coincidentes:
          type: integer
        faltantes_en_sunat:
          type: array
          description: Emitidos y aceptados en el sistema que no figuran en la propuesta SUNAT.
          items:
            type: object
            required:
            - document_id
            - tipo_doc
            - numero
            - fecha_emision
            - importe_total
            - estado
            properties:
              document_id:
                type: string
              tipo_doc:
                type: string
                example: '01'
              numero:
                type: string
                example: F001-123
              fecha_emision:
                type: string
                format: date
              importe_total:
                type: number
              estado:
                type: string
        faltantes_en_sistema:
          type: array
          description: Figuran en la propuesta SUNAT pero no existen (aceptados) en el sistema.
          items:
            type: object
            required:
            - tipo_doc
            - serie
            - numero
            - total
            properties:
              car_sunat:
                type: string
              fecha_emision:
                type: string
                example: 15/07/2026
              tipo_doc:
                type: string
                example: '03'
              serie:
                type: string
                example: B001
              numero:
                type: string
                example: '45'
              cliente_doc:
                type: string
              total:
                type: number
              moneda:
                type: string
                example: PEN
        diferencias_monto:
          type: array
          description: Mismo comprobante con importe total distinto.
          items:
            type: object
            required:
            - tipo_doc
            - numero
            - document_id
            - monto_sunat
            - monto_sistema
            - diferencia
            properties:
              tipo_doc:
                type: string
              numero:
                type: string
              document_id:
                type: string
              monto_sunat:
                type: number
              monto_sistema:
                type: number
              diferencia:
                type: number
