openapi: 3.1.0
info:
  title: API de socios v1 · Sistema Facturación Perú
  version: 1.0.0
  summary: Da de alta empresas, crea sus claves de la API v1, consulta tu saldo y recibe avisos.
  description: |
    Para programadores y empresas de software con muchos clientes propios. Tú das de alta a tus clientes como empresas
    de Sistema Facturación Perú, las pagas con bolsas prepago de comprobantes y emites con cada una usando su **API v1**
    (https://sistemafacturacionperu.com/desarrolladores/referencia/).

    - Base: `https://api.sistemafacturacionperu.com/socios/v1` (nunca en el subdominio de una empresa).
    - Autenticación: OAuth 2 «client_credentials». Pide un token con `POST /token` (tu `client_id` y `client_secret`) y
      úsalo en `Authorization: Bearer sfp_pat_…`. Dura 1 hora. Nunca en la URL.
    - Éxito: `{ "success": true, "data": …, "meta"?: … }`. Error: `{ "success": false, "code": "…", "message": "…", "errors"?: {…} }`
      (el mismo formato de la API v1).
    - Todo `POST` y `PATCH` acepta `Idempotency-Key`: el reintento con el mismo cuerpo devuelve la misma respuesta con
      `Idempotent-Replayed: true`; la misma clave con otro cuerpo responde 409 `idempotencia_distinta`.
    - Límites: 120 llamadas por minuto por token (`X-RateLimit-Limit`, `X-RateLimit-Remaining`; al pasarte, 429 con
      `Retry-After`) y 10 intentos de token por minuto por IP y por `client_id`.
    - Una empresa que no es tuya responde 404 (nunca se revela que existe).

    Bolsas (con IGV, 6 meses de validez, se consume primero la que vence antes): 3.300 = S/ 180; 5.500 = S/ 285;
    10.000 = S/ 480. Descuenta 1 cada comprobante, resumen diario, baja y guía que sale por **nuestro PSE**; lo que la
    empresa envía con su propio usuario SOL no descuenta. Cuota: S/ 9 al mes con IGV por cada empresa que emitió.

    Guía completa: https://sistemafacturacionperu.com/desarrolladores/socios/
  contact:
    name: Sistema Facturación Perú
    url: https://sistemafacturacionperu.com/desarrolladores
servers:
  - url: https://api.sistemafacturacionperu.com/socios/v1
    description: Producción
security:
  - bearer: []
tags:
  - name: Autenticación
  - name: Empresas
    description: Tus clientes como empresas de Sistema Facturación Perú.
  - name: Claves
    description: Claves de la API v1 (sfp_live_) de cada empresa, para emitir.
  - name: Saldo
    description: Bolsas, consumos, recargas y cuotas.
  - name: Webhooks

paths:
  /token:
    post:
      tags: [Autenticación]
      summary: Pedir un token de acceso (client_credentials)
      description: |
        Manda tu `client_id` y `client_secret` en `Authorization: Basic base64(client_id:client_secret)` o en el cuerpo
        (JSON o formulario). El token dura 1 hora: pide otro cuando venza (401 `token_vencido`).
      operationId: token
      security: []
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PedidoToken' }
            example: { grant_type: client_credentials, client_id: sfp_partner_k2j4h6g8f0d2s4a6q8w0e2r4, client_secret: 'sfp_secret_…' }
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/PedidoToken' }
      responses:
        '200':
          description: El token
          headers:
            Cache-Control: { schema: { type: string, example: no-store } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RespuestaToken' }
              example:
                success: true
                access_token: sfp_pat_Q1w2E3r4T5y6U7i8O9p0A1s2D3f4G5h6J7k8L9z0X1c2V3b4
                token_type: Bearer
                expires_in: 3600
                expires_at: '2026-11-03T15:04:05-05:00'
                data: { access_token: 'sfp_pat_…', token_type: Bearer, expires_in: 3600, expires_at: '2026-11-03T15:04:05-05:00' }
        '400':
          description: '`grant_no_soportado` (solo client_credentials) o `secreto_en_url`'
          content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
        '401':
          description: '`credenciales_invalidas` (el mismo mensaje si el client_id no existe, el secreto es malo o la credencial se revocó)'
          content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { success: false, code: credenciales_invalidas, message: 'El client_id o el client_secret no son válidos (o la credencial fue revocada).' } } }
        '403':
          description: '`ip_no_permitida` (la credencial tiene IP permitidas) o `socio_suspendido`'
          content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
        '429':
          description: '`demasiados_intentos` (10 por minuto por IP y por client_id), con Retry-After'
          content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }

  /empresas:
    get:
      tags: [Empresas]
      summary: Listar tus empresas
      description: El estado es el último conocido; `GET /empresas/{id}` lo vuelve a leer.
      operationId: listarEmpresas
      parameters:
        - { name: estado, in: query, schema: { $ref: '#/components/schemas/EstadoEmpresa' } }
        - { name: ruc, in: query, schema: { type: string, pattern: '^\d{11}$' } }
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/PerPage' }
      responses:
        '200':
          description: Página de empresas
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { type: array, items: { $ref: '#/components/schemas/Empresa' } }
                  meta: { $ref: '#/components/schemas/Meta' }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '422': { $ref: '#/components/responses/Validacion' }
        '429': { $ref: '#/components/responses/DemasiadasSolicitudes' }
    post:
      tags: [Empresas]
      summary: Dar de alta una empresa
      description: |
        Responde 201 al instante con `estado: creando`; la empresa queda creada en 1 a 3 minutos (consulta
        `GET /empresas/{id}` o espera el webhook `empresa.lista`). Entra con el plan «Socio»: emitir y consultar + API.

        - Sin `administrador.contrasena`, el administrador recibe el correo de bienvenida con el enlace para crearla.
        - Con `certificado` y `sol` la empresa pasa a producción y envía directo a SUNAT con su SOL (no descuenta de tu
          bolsa). Con `envio: pse` (o sin SOL) envía por nuestro PSE, que activamos nosotros, y descuenta de tu bolsa.
        - Como toda empresa nueva, emite cuando su Ficha RUC queda verificada (`pendientes: ficha_ruc`): el
          administrador la sube en Configuración › Empresa › Régimen tributario.
      operationId: crearEmpresa
      parameters:
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AltaEmpresa' }
            example:
              ruc: '20601030013'
              razon_social: FERRETERIA EL TORNILLO S.A.C.
              nombre_comercial: El Tornillo
              subdominio: eltornillo
              regimen_tributario: rmt
              direccion: { direccion: 'Av. Grau 123, Lima', ubigeo: '150101' }
              administrador: { nombre: Ana Quispe, correo: ana@tornillo.pe, celular: '987654321' }
              certificado: { pfx_base64: 'MIIK…', clave: 'clave-del-pfx' }
              sol: { usuario: FACTURAS1, clave: 'clave-sol' }
      responses:
        '201':
          description: El alta en marcha (o ya lista)
          content:
            application/json:
              schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Empresa' } } }
              example:
                success: true
                data:
                  id: 15
                  ruc: '20601030013'
                  razon_social: FERRETERIA EL TORNILLO S.A.C.
                  subdominio: eltornillo
                  estado: creando
                  pendientes: []
                  envio: null
                  suspension: null
                  web: https://eltornillo.sistemafacturacionperu.com
                  api_v1: https://eltornillo.sistemafacturacionperu.com/api/v1
                  lista_desde: null
                  creada: '2026-11-03T10:15:00-05:00'
        '400': { $ref: '#/components/responses/PeticionInvalida' }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '409':
          description: '`ruc_ya_registrado` (el RUC ya tiene cuenta en el sistema: no se dice de quién), `idempotencia_distinta` o `idempotencia_en_proceso`'
          content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { success: false, code: ruc_ya_registrado, message: 'Ese RUC ya tiene una cuenta en el sistema. Si es tu cliente, pídele que nos escriba para pasarla a tu cuenta de socio.' } } }
        '422':
          description: '`validacion` (RUC con dígito verificador malo, subdominio tomado, ubigeo…) o `certificado_invalido` (el .pfx no abre con su clave)'
          content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { success: false, code: validacion, message: 'Revisa los datos enviados.', errors: { ruc: ['El RUC no es válido: revisa los 11 dígitos (el último es el dígito verificador).'] } } } }
        '429': { $ref: '#/components/responses/DemasiadasSolicitudes' }

  /empresas/{id}:
    parameters:
      - { $ref: '#/components/parameters/IdEmpresa' }
    get:
      tags: [Empresas]
      summary: Ver una empresa (vuelve a leer su estado)
      operationId: verEmpresa
      responses:
        '200': { $ref: '#/components/responses/UnaEmpresa' }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { $ref: '#/components/responses/NoEncontrado' }
    patch:
      tags: [Empresas]
      summary: Cambiar contacto, nombre comercial, dirección, certificado, usuario SOL o forma de envío
      description: El RUC, la razón social y el correo del administrador no se cambian por aquí (422).
      operationId: cambiarEmpresa
      parameters:
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CambioEmpresa' }
            example: { contacto: { nombre: Rosa, correo: rosa@cliente.pe }, certificado: { pfx_base64: 'MIIK…', clave: 'clave' }, sol: { usuario: FACTURAS1, clave: 'clave-sol' } }
      responses:
        '200': { $ref: '#/components/responses/UnaEmpresa' }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { $ref: '#/components/responses/NoEncontrado' }
        '409': { $ref: '#/components/responses/EmpresaNoLista' }
        '422': { $ref: '#/components/responses/Validacion' }

  /empresas/{id}/uso:
    get:
      tags: [Empresas]
      summary: Comprobantes del mes por tipo y lo que descontó de tu bolsa
      operationId: usoEmpresa
      parameters:
        - { $ref: '#/components/parameters/IdEmpresa' }
        - { name: mes, in: query, description: 'AAAA-MM (por defecto, el actual)', schema: { type: string, pattern: '^\d{4}-(0[1-9]|1[0-2])$' } }
      responses:
        '200':
          description: El uso del mes
          content:
            application/json:
              schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Uso' } } }
              example:
                success: true
                data:
                  mes: '2026-11'
                  emitidos: { facturas: 120, boletas: 840, notas_credito: 6, notas_debito: 0, guias: 30, retenciones: 0, percepciones: 0, liquidaciones_compra: 0, resumenes: 22, bajas: 2 }
                  descontado_de_la_bolsa: { total: 1014, por_concepto: { comprobante: 960, resumen: 22, baja: 2, guia: 30 } }
                  activa: true
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { $ref: '#/components/responses/NoEncontrado' }
        '409': { $ref: '#/components/responses/EmpresaNoLista' }
        '422': { $ref: '#/components/responses/Validacion' }

  /empresas/{id}/suspender:
    post:
      tags: [Empresas]
      summary: Pausar la emisión por API de una empresa
      description: |
        Decisión tuya (por ejemplo, tu cliente no te pagó). Su API v1 responde 423 `suspendida_por_socio` al emitir;
        consultar sigue funcionando y su web no cambia. Avisa con `empresa.suspendida`.
      operationId: suspenderEmpresa
      parameters:
        - { $ref: '#/components/parameters/IdEmpresa' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        content:
          application/json:
            schema: { type: object, properties: { motivo: { type: string, maxLength: 255 } } }
            example: { motivo: Cliente con pagos pendientes }
      responses:
        '200': { $ref: '#/components/responses/UnaEmpresa' }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { $ref: '#/components/responses/NoEncontrado' }
        '409': { $ref: '#/components/responses/EmpresaNoLista' }

  /empresas/{id}/reactivar:
    post:
      tags: [Empresas]
      summary: Volver a permitir la emisión por API
      operationId: reactivarEmpresa
      parameters:
        - { $ref: '#/components/parameters/IdEmpresa' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      responses:
        '200': { $ref: '#/components/responses/UnaEmpresa' }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { $ref: '#/components/responses/NoEncontrado' }
        '409': { $ref: '#/components/responses/EmpresaNoLista' }

  /empresas/{id}/claves:
    parameters:
      - { $ref: '#/components/parameters/IdEmpresa' }
    get:
      tags: [Claves]
      summary: Listar las claves v1 de la empresa (sin la clave)
      operationId: listarClaves
      responses:
        '200':
          description: Las claves
          content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/Clave' } } } } } }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { $ref: '#/components/responses/NoEncontrado' }
        '409': { $ref: '#/components/responses/EmpresaNoLista' }
    post:
      tags: [Claves]
      summary: Crear una clave v1 (sfp_live_) para emitir con esa empresa
      description: La clave se muestra UNA sola vez. Sin `permisos`, trae los de emitir y consultar.
      operationId: crearClave
      parameters:
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [nombre]
              properties:
                nombre: { type: string, maxLength: 100 }
                permisos:
                  type: array
                  items: { type: string, enum: [comprobantes.emitir, comprobantes.leer, comprobantes.anular, guias.emitir, guias.leer, cobros.registrar, catalogo.leer, catalogo.escribir, stock.leer, consultas, webhooks.gestionar] }
                establecimiento: { type: string, description: 'Código del local (0000). Sin él, el del administrador.' }
                vence: { type: string, format: date }
            example: { nombre: 'Mi sistema · producción', establecimiento: '0000' }
      responses:
        '201':
          description: La clave (se muestra una vez)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data:
                    allOf:
                      - { $ref: '#/components/schemas/Clave' }
                      - type: object
                        properties:
                          clave: { type: string, example: 'sfp_live_Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90' }
                          api_v1: { type: string, format: uri }
                          aviso: { type: string }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { $ref: '#/components/responses/NoEncontrado' }
        '409': { $ref: '#/components/responses/EmpresaNoLista' }
        '422': { $ref: '#/components/responses/Validacion' }

  /empresas/{id}/claves/{keyId}:
    delete:
      tags: [Claves]
      summary: Revocar una clave
      operationId: revocarClave
      parameters:
        - { $ref: '#/components/parameters/IdEmpresa' }
        - { name: keyId, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: Revocada
          content: { application/json: { example: { success: true, data: { id: 3, estado: revocada } } } }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { $ref: '#/components/responses/NoEncontrado' }

  /saldo:
    get:
      tags: [Saldo]
      summary: Tu saldo, tus bolsas, vencimientos y cuotas pendientes
      operationId: saldo
      responses:
        '200':
          description: El saldo
          content:
            application/json:
              schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Saldo' } } }
              example:
                success: true
                data:
                  saldo: 4120
                  en_bolsas_vigentes: 4120
                  por_cobrar_en_la_siguiente_recarga: 0
                  porcentaje_usado: 52
                  proximo_vencimiento: { fecha: '2027-03-01', unidades: 820 }
                  bolsas: [{ id: 4, comprobantes: 5500, usados: 2200, quedan: 3300, precio: 285, comprada: '2026-11-01', vence: '2027-05-01', estado: vigente }]
                  cuotas_pendientes: [{ id: 2, mes: '2026-10', empresas_activas: 14, monto_por_empresa: 9, monto: 126, moneda: PEN, incluye_igv: true, estado: pendiente, vence: '2026-11-11', vencida: false, pagado: null }]
                  precios: [{ comprobantes: 3300, precio: 180, moneda: PEN, incluye_igv: true }, { comprobantes: 5500, precio: 285, moneda: PEN, incluye_igv: true }, { comprobantes: 10000, precio: 480, moneda: PEN, incluye_igv: true }]
                  validez_meses: 6
        '401': { $ref: '#/components/responses/NoAutenticado' }

  /movimientos:
    get:
      tags: [Saldo]
      summary: Consumos y recargas, del más nuevo al más viejo
      operationId: movimientos
      parameters:
        - { name: tipo, in: query, schema: { type: string, enum: [consumo, recarga, ajuste] } }
        - { name: empresa_id, in: query, schema: { type: integer } }
        - { name: desde, in: query, schema: { type: string, format: date } }
        - { name: hasta, in: query, schema: { type: string, format: date } }
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/PerPage' }
      responses:
        '200':
          description: Página de movimientos
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { type: array, items: { $ref: '#/components/schemas/Movimiento' } }
                  meta: { $ref: '#/components/schemas/Meta' }
              example:
                success: true
                data: [{ id: 901, tipo: consumo, unidades: -1, concepto: comprobante, tipo_documento: '03', referencia: B001-842, empresa_id: 15, bolsa_id: 4, fecha: '2026-11-03T10:20:11-05:00' }]
                meta: { page: 1, per_page: 20, total: 1, last_page: 1 }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '422': { $ref: '#/components/responses/Validacion' }

  /webhooks:
    get:
      tags: [Webhooks]
      summary: Listar tus webhooks (y los eventos disponibles en meta)
      operationId: listarWebhooks
      responses:
        '200':
          description: Los webhooks
          content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/Webhook' } }, meta: { type: object, properties: { eventos_disponibles: { type: object } } } } } } }
        '401': { $ref: '#/components/responses/NoAutenticado' }
    post:
      tags: [Webhooks]
      summary: Registrar un webhook
      description: Solo https a un dominio público. El secreto `whsec_…` se muestra una vez; con él verificas `X-SFP-Signature`.
      operationId: crearWebhook
      parameters:
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, eventos]
              properties:
                url: { type: string, format: uri }
                eventos: { type: array, items: { $ref: '#/components/schemas/EventoSocio' } }
                descripcion: { type: string, maxLength: 150 }
            example: { url: 'https://tusistema.pe/webhooks/sfp', eventos: [empresa.lista, saldo.bajo, saldo.agotado, cuota.vencida] }
      responses:
        '201':
          description: El webhook con su secreto (una vez)
          content: { application/json: { example: { success: true, data: { id: 2, url: 'https://tusistema.pe/webhooks/sfp', eventos: [empresa.lista, saldo.bajo], descripcion: null, activo: true, ultima_entrega: null, creado: '2026-11-03 10:00:00', secreto: 'whsec_…', aviso: 'Guarda el secreto ahora: no se vuelve a mostrar.' } } } }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '422': { $ref: '#/components/responses/Validacion' }

  /webhooks/{id}:
    delete:
      tags: [Webhooks]
      summary: Borrar un webhook (y su historial)
      operationId: borrarWebhook
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: Borrado
          content: { application/json: { example: { success: true, data: { id: 2, borrado: true } } } }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { $ref: '#/components/responses/NoEncontrado' }

  /webhooks/{id}/entregas:
    get:
      tags: [Webhooks]
      summary: Entregas de un webhook
      operationId: entregasWebhook
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/PerPage' }
      responses:
        '200':
          description: Página de entregas
          content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/Entrega' } }, meta: { $ref: '#/components/schemas/Meta' } } } } }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { $ref: '#/components/responses/NoEncontrado' }

  /webhooks/entregas/{id}/reenviar:
    post:
      tags: [Webhooks]
      summary: Reenviar una entrega ya
      operationId: reenviarEntrega
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      responses:
        '202':
          description: En cola
          content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Entrega' } } } } }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '404': { $ref: '#/components/responses/NoEncontrado' }

webhooks:
  evento:
    post:
      summary: Aviso a tu URL
      description: |
        Cabeceras: `X-SFP-Signature: t=<unix>,v1=<hex HMAC-SHA256(secreto, "t.cuerpo")>`, `X-SFP-Delivery` (uuid) y
        `X-SFP-Event`. Responde 2xx en menos de 10 s; si no, reintentamos a 1 m, 5 m, 30 m, 2 h, 6 h y 24 h.
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Aviso' }
            example:
              id: 2f6c1d9e-6a0e-4f0e-9d7e-2b3c4d5e6f70
              evento: saldo.bajo
              creado: '2026-11-03T10:00:00-05:00'
              socio_id: 3
              data: { saldo: 660, porcentaje_usado: 80, umbral: 80 }
      responses:
        '200': { description: Recibido }

components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: 'Token `sfp_pat_<48>` de POST /token (1 hora).'
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: 'Hasta 255 caracteres visibles (un UUID). Se recuerda 24 h.'
      schema: { type: string, maxLength: 255 }
    IdEmpresa:
      name: id
      in: path
      required: true
      description: El id de la empresa en esta API (el de GET /empresas)
      schema: { type: integer }
    Page: { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
    PerPage: { name: per_page, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
  responses:
    UnaEmpresa:
      description: La empresa
      content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Empresa' } } } } }
    PeticionInvalida:
      description: '`json_invalido` o `idempotencia_invalida`'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NoAutenticado:
      description: '`token_invalido`, `token_vencido` o `token_en_url`; 403 `socio_suspendido` si la cuenta de socio está suspendida'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { success: false, code: token_vencido, message: 'El token de acceso venció (dura 60 minutos). Pide otro con POST /socios/v1/token.' } } }
    NoEncontrado:
      description: '`no_encontrado` (no existe o no es tuya)'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { success: false, code: no_encontrado, message: 'La empresa no existe o no la puedes ver.' } } }
    EmpresaNoLista:
      description: '`empresa_creandose` (con Retry-After) o `empresa_con_error`'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Validacion:
      description: '`validacion` con los campos en `errors`'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    DemasiadasSolicitudes:
      description: '`demasiadas_solicitudes`, con Retry-After'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
  schemas:
    Error:
      type: object
      required: [success, code, message]
      properties:
        success: { const: false }
        code: { type: string }
        message: { type: string }
        errors: { type: object, additionalProperties: { type: array, items: { type: string } } }
    Meta:
      type: object
      properties:
        page: { type: integer }
        per_page: { type: integer }
        total: { type: integer }
        last_page: { type: integer }
    PedidoToken:
      type: object
      properties:
        grant_type: { type: string, enum: [client_credentials] }
        client_id: { type: string }
        client_secret: { type: string }
    RespuestaToken:
      type: object
      properties:
        success: { const: true }
        access_token: { type: string }
        token_type: { const: Bearer }
        expires_in: { type: integer, description: Segundos }
        expires_at: { type: string, format: date-time }
        data: { type: object }
    EstadoEmpresa:
      type: string
      enum: [creando, por_verificar, lista, suspendida, error]
      description: |
        creando: el alta corre (1 a 3 minutos). por_verificar: falta algo para emitir (mira `pendientes`).
        lista: ya emite. suspendida: la pausaste tú o la bloqueó la plataforma (mira `suspension`). error: el alta falló (escríbenos).
    Empresa:
      type: object
      properties:
        id: { type: integer }
        ruc: { type: string }
        razon_social: { type: string }
        subdominio: { type: string }
        estado: { $ref: '#/components/schemas/EstadoEmpresa' }
        pendientes:
          type: array
          items:
            type: object
            properties:
              codigo: { type: string, enum: [ficha_ruc, produccion, certificado, usuario_sol, credenciales_pse] }
              detalle: { type: string }
        envio: { type: [string, 'null'], enum: [sol_propio, pse, demo, null], description: 'sol_propio no descuenta de tu bolsa; pse sí' }
        suspension:
          type: [object, 'null']
          properties:
            por: { type: string, enum: [socio, agencia] }
            motivo: { type: [string, 'null'] }
            desde: { type: [string, 'null'], format: date-time }
        web: { type: [string, 'null'], format: uri }
        api_v1: { type: [string, 'null'], format: uri }
        lista_desde: { type: [string, 'null'], format: date-time }
        creada: { type: string, format: date-time }
    Certificado:
      type: object
      required: [pfx_base64, clave]
      properties:
        pfx_base64: { type: string, description: 'El archivo .pfx en base64 (hasta 1 MB), con su clave privada' }
        clave: { type: string }
    Sol:
      type: object
      required: [usuario, clave]
      properties:
        usuario: { type: string, description: 'Usuario SOL secundario (sin el RUC delante; también se acepta con él)' }
        clave: { type: string }
    Direccion:
      type: object
      required: [direccion, ubigeo]
      properties:
        direccion: { type: string, maxLength: 200 }
        ubigeo: { type: string, pattern: '^\d{6}$', description: 'INEI (150101 = Lima, Lima, Lima)' }
    AltaEmpresa:
      type: object
      required: [ruc, razon_social, administrador]
      properties:
        ruc: { type: string, pattern: '^(10|15|16|17|20)\d{9}$' }
        razon_social: { type: string, maxLength: 255 }
        nombre_comercial: { type: string, maxLength: 255 }
        subdominio: { type: string, pattern: '^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$', description: 'Si no va, se arma del nombre' }
        regimen_tributario: { type: string, enum: [general, rmt, rer, nrus, sin_negocio] }
        direccion: { $ref: '#/components/schemas/Direccion' }
        administrador:
          type: object
          required: [nombre, correo]
          properties:
            nombre: { type: string, maxLength: 120 }
            correo: { type: string, format: email }
            celular: { type: string }
            contrasena: { type: string, minLength: 8, maxLength: 64 }
        certificado: { $ref: '#/components/schemas/Certificado' }
        sol: { $ref: '#/components/schemas/Sol' }
        envio: { type: string, enum: [sol_propio, pse] }
    CambioEmpresa:
      type: object
      properties:
        nombre_comercial: { type: string }
        direccion: { $ref: '#/components/schemas/Direccion' }
        administrador: { type: object, properties: { nombre: { type: string } } }
        contacto:
          type: object
          properties:
            nombre: { type: string }
            correo: { type: string, format: email }
            celular: { type: string }
        certificado: { $ref: '#/components/schemas/Certificado' }
        sol: { $ref: '#/components/schemas/Sol' }
        envio: { type: string, enum: [sol_propio, pse] }
    Uso:
      type: object
      properties:
        mes: { type: string }
        emitidos: { type: object, additionalProperties: { type: integer } }
        descontado_de_la_bolsa:
          type: object
          properties:
            total: { type: integer }
            por_concepto: { type: object, properties: { comprobante: { type: integer }, resumen: { type: integer }, baja: { type: integer }, guia: { type: integer } } }
        activa: { type: boolean, description: 'Emitió algo este mes (cuenta para la cuota)' }
    Clave:
      type: object
      properties:
        id: { type: integer }
        nombre: { type: string }
        prefijo: { type: string }
        permisos: { type: array, items: { type: string } }
        establecimiento_id: { type: [integer, 'null'] }
        vence: { type: [string, 'null'], format: date }
        ultimo_uso: { type: [string, 'null'], format: date-time }
        estado: { type: string, enum: [activa, vencida, revocada] }
        creada: { type: string, format: date-time }
    Bolsa:
      type: object
      properties:
        id: { type: integer }
        comprobantes: { type: integer }
        usados: { type: integer }
        quedan: { type: integer }
        precio: { type: number }
        comprada: { type: string, format: date }
        vence: { type: string, format: date }
        estado: { type: string, enum: [vigente, agotada, vencida] }
    Cuota:
      type: object
      properties:
        id: { type: integer }
        mes: { type: string }
        empresas_activas: { type: integer }
        monto_por_empresa: { type: number }
        monto: { type: number }
        moneda: { const: PEN }
        incluye_igv: { const: true }
        estado: { type: string, enum: [pendiente, pagado, anulado] }
        vence: { type: string, format: date }
        vencida: { type: boolean }
        pagado: { type: [string, 'null'], format: date }
    Saldo:
      type: object
      properties:
        saldo: { type: integer, description: 'Comprobantes disponibles (puede ser negativo: resúmenes y bajas enviados sin saldo)' }
        en_bolsas_vigentes: { type: integer }
        por_cobrar_en_la_siguiente_recarga: { type: integer }
        porcentaje_usado: { type: integer }
        proximo_vencimiento: { type: [object, 'null'], properties: { fecha: { type: string, format: date }, unidades: { type: integer } } }
        bolsas: { type: array, items: { $ref: '#/components/schemas/Bolsa' } }
        cuotas_pendientes: { type: array, items: { $ref: '#/components/schemas/Cuota' } }
        precios: { type: array, items: { type: object, properties: { comprobantes: { type: integer }, precio: { type: number }, moneda: { const: PEN }, incluye_igv: { const: true } } } }
        validez_meses: { type: integer }
    Movimiento:
      type: object
      properties:
        id: { type: integer }
        tipo: { type: string, enum: [consumo, recarga, ajuste] }
        unidades: { type: integer }
        concepto: { type: [string, 'null'], enum: [comprobante, resumen, baja, guia, null] }
        tipo_documento: { type: [string, 'null'] }
        referencia: { type: [string, 'null'] }
        empresa_id: { type: [integer, 'null'] }
        bolsa_id: { type: [integer, 'null'] }
        fecha: { type: string, format: date-time }
    EventoSocio:
      type: string
      enum: [empresa.lista, empresa.suspendida, empresa.reactivada, empresa.error, saldo.bajo, saldo.agotado, cuota.pendiente, cuota.por_vencer, cuota.vencida]
    Webhook:
      type: object
      properties:
        id: { type: integer }
        url: { type: string, format: uri }
        eventos: { type: array, items: { $ref: '#/components/schemas/EventoSocio' } }
        descripcion: { type: [string, 'null'] }
        activo: { type: boolean }
        ultima_entrega: { type: [object, 'null'] }
        creado: { type: string }
    Entrega:
      type: object
      properties:
        id: { type: integer }
        uuid: { type: string }
        webhook_id: { type: integer }
        evento: { $ref: '#/components/schemas/EventoSocio' }
        estado: { type: string, enum: [pendiente, reintentando, entregado, fallido] }
        intentos: { type: integer }
        codigo_respuesta: { type: [integer, 'null'] }
        error: { type: [string, 'null'] }
        proximo_intento: { type: [string, 'null'] }
        entregado: { type: [string, 'null'] }
        creado: { type: string }
    Aviso:
      type: object
      properties:
        id: { type: string, format: uuid }
        evento: { $ref: '#/components/schemas/EventoSocio' }
        creado: { type: string, format: date-time }
        socio_id: { type: integer }
        data: { type: object }
