{
  "openapi": "3.1.0",
  "info": {
    "title": "API de Contadeo",
    "version": "2026-08",
    "description": "Facturación electrónica SRI (Ecuador): emisión asíncrona de los 6 tipos de comprobante (202 → polling → descarga RIDE/XML), catálogos, organización y operación.\n\nGuía narrativa y onboarding: [contadeo.com/desarrolladores](https://contadeo.com/desarrolladores). Política de compatibilidad: cambios aditivos en cualquier momento (tolera campos desconocidos); cambios incompatibles con mínimo 90 días de aviso. Toda respuesta lleva el header `X-API-Version`.",
    "contact": {
      "url": "https://contadeo.com/desarrolladores"
    }
  },
  "servers": [
    {
      "url": "https://contadeo.com/api",
      "description": "Producción"
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "tags": [
    {
      "name": "auth",
      "description": "Login, refresh y contraseña"
    },
    {
      "name": "cuenta",
      "description": "Registro y cuenta (tenant)"
    },
    {
      "name": "equipo",
      "description": "Usuarios de la cuenta"
    },
    {
      "name": "organizacion",
      "description": "Emisores, establecimientos, puntos y secuenciales"
    },
    {
      "name": "certificados",
      "description": "Firmas electrónicas .p12"
    },
    {
      "name": "solicitudes-firma",
      "description": "Solicitud de firma electrónica ante NewBest (CorpNewBest Cía. Ltda.) en modo híbrido: el titular se registra en el asistente hospedado de NewBest y Contadeo tramita el resto por API. Solo existe con NEWBEST_HABILITADO=true; apagado, todas estas rutas responden 404."
    },
    {
      "name": "catalogos",
      "description": "Productos, clientes y tablas del SRI"
    },
    {
      "name": "comprobantes",
      "description": "Emisión y consulta de comprobantes"
    },
    {
      "name": "operacion",
      "description": "Reportes, colas, salud y operador"
    },
    {
      "name": "pagos",
      "description": "Pago en línea de planes con Payphone. Sin las envs de la pasarela, el checkout responde 503 y la activación sigue siendo manual (ver PRICING.md)."
    },
    {
      "name": "oauth",
      "description": "Autorización OAuth 2.1 del MCP remoto: registro dinámico de clientes (RFC 7591), consentimiento y emisión de tokens. La metadata de descubrimiento (RFC 8414/9728) se sirve en la RAÍZ del dominio (https://contadeo.com/.well-known/...), no bajo /api."
    },
    {
      "name": "mcp",
      "description": "Servidor MCP remoto (transporte HTTP streamable). Conéctalo desde un cliente MCP: `claude mcp add --transport http contadeo https://contadeo.com/api/mcp` — el cliente abre el login/consentimiento OAuth. También acepta una API key cdo_ como Bearer."
    },
    {
      "name": "asesor",
      "description": "Asesor tributario: calendario, semáforo RIMPE y obligaciones (informativo, con disclaimer)"
    },
    {
      "name": "partners",
      "description": "Programa Partners (contadores): alta, panel de comisiones, datos bancarios"
    },
    {
      "name": "tickets",
      "description": "Soporte: creación y seguimiento de tickets de soporte desde el portal del tenant"
    },
    {
      "name": "despachos",
      "description": "Plan Despacho: gestión de despachos contables y asociación de empresas (Fase 2 — sin cobro)"
    },
    {
      "name": "alertas",
      "description": "Centro de alertas/recordatorios: preferencias de digest diario de vencimientos tributarios y semáforo RIMPE (v1: solo correo)"
    },
    {
      "name": "compras",
      "description": "Módulo de compras: ingesta de comprobantes electrónicos recibidos (XML o manual), consulta y totales por período para el F104"
    },
    {
      "name": "nomina",
      "description": "Módulo de nómina (v1): empleados, rol de pagos mensual y provisiones del empleador. Cálculo puro con catálogo laboral vigente (SBU e IESS). Retención IR diferida a v2."
    },
    {
      "name": "conciliacion",
      "description": "Conciliación bancaria: extractos y movimientos"
    },
    {
      "name": "contabilidad",
      "description": "Contabilidad NIIF v1: plan de cuentas, asientos de partida doble (borrador→contabilizado→anulado), libro mayor y balance de comprobación"
    },
    {
      "name": "privacidad",
      "description": "Derechos del titular sobre sus datos (LOPDP): supresión granular de un comprador con los plazos de la resolución SPDP-SPD-2025-0030-R"
    },
    {
      "name": "contacto",
      "description": "Formulario de contacto de la web pública (startups e integradores): deja un mensaje en el buzón del operador, sin sesión"
    }
  ],
  "paths": {
    "/auth/login": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Iniciar sesión",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "password"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "password": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Par de tokens, o un desafío MFA si el usuario tiene el segundo factor activo",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Tokens"
                    },
                    {
                      "$ref": "#/components/schemas/DesafioMfa"
                    }
                  ],
                  "description": "Si la respuesta trae `mfaRequerido: true`, la contraseña era correcta pero falta el segundo factor: canjea `desafioToken` en POST /auth/mfa/verificar."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          }
        },
        "description": "La empresa NO se elige aquí: se entra a la del último acceso del usuario (el slug es un identificador interno y no se acepta). Con varias empresas, cámbiate con POST /auth/cambiar-cuenta (tenantId de GET /auth/mis-cuentas)."
      }
    },
    "/auth/refresh": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Rotar tokens con un refresh token",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "refreshToken"
                ],
                "properties": {
                  "refreshToken": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Par de tokens nuevo",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tokens"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          }
        }
      }
    },
    "/auth/forgot-password": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Solicitar enlace de recuperación de contraseña",
        "description": "Anti-enumeración: responde 204 exista o no la cuenta. Limitado a 5 solicitudes por minuto por IP.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Solicitud recibida (no revela si el correo existe)"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/auth/reset-password": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Fijar una nueva contraseña con el token del enlace",
        "description": "Verifica el token de recuperación (un solo uso, 30 min) y actualiza la contraseña, revocando los refresh tokens previos. Limitado a 10 solicitudes por minuto por IP.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token",
                  "nueva"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "nueva": {
                    "type": "string",
                    "minLength": 8
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Contraseña actualizada"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/auth/password": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Cambiar la contraseña propia (revoca los refresh tokens previos)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "actual",
                  "nueva"
                ],
                "properties": {
                  "actual": {
                    "type": "string"
                  },
                  "nueva": {
                    "type": "string",
                    "minLength": 8
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Contraseña cambiada"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          }
        }
      }
    },
    "/tenants/register": {
      "post": {
        "tags": [
          "cuenta"
        ],
        "summary": "Crear cuenta (self-serve, plan free)",
        "security": [],
        "description": "Rate-limit: 5 altas por IP cada 10 min (429). Exige `aceptaPrivacidad: true` (LOPDP) y `declaraTitularidadRuc: true` (declaración de titularidad del RUC). Si el operador definió `REGISTRATION_TOKEN`, el alta exige `registrationToken` igual (403).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ruc",
                  "email",
                  "password",
                  "aceptaPrivacidad",
                  "aceptaTerminos",
                  "declaraTitularidadRuc"
                ],
                "properties": {
                  "nombre": {
                    "type": "string",
                    "description": "Nombre de respaldo de la cuenta, solo para cuando el catastro del SRI no responde. La razón social del RUC siempre manda sobre él."
                  },
                  "nombreUsuario": {
                    "type": "string",
                    "description": "Nombre y apellidos de quien crea la cuenta (users.nombre)."
                  },
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "password": {
                    "type": "string",
                    "minLength": 8
                  },
                  "aceptaPrivacidad": {
                    "type": "boolean",
                    "description": "Aceptación del Aviso de Privacidad (LOPDP); se registra la fecha"
                  },
                  "aceptaTerminos": {
                    "type": "boolean",
                    "description": "Aceptación de los Términos y Condiciones; se registra la fecha"
                  },
                  "registrationToken": {
                    "type": "string"
                  },
                  "ruc": {
                    "type": "string",
                    "pattern": "^\\d{13}$",
                    "description": "Obligatorio (400 si falta o su dígito verificador no valida). De él salen el nombre de la cuenta (razón social), el régimen y la dirección, precargados del catastro público del SRI (best-effort: su fallo nunca rompe el registro). El identificador interno (slug) lo genera el sistema."
                  },
                  "declaraTitularidadRuc": {
                    "type": "boolean",
                    "description": "Declaración expresa de titularidad del RUC: quien registra declara ser su titular, su representante legal o administrador, o contar con autorización para emitir a su nombre. Obligatoria (400 sin ella); se guardan fecha, usuario, IP y versión del texto como evidencia."
                  },
                  "codigoPartner": {
                    "type": "string",
                    "description": "Código de referido de un partner (opcional, best-effort)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cuenta creada; par de tokens del owner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tokens",
                  "properties": {
                    "precargaSri": {
                      "type": "object",
                      "description": "Resultado de la precarga del emisor desde el catastro del SRI.",
                      "properties": {
                        "realizada": {
                          "type": "boolean"
                        },
                        "emisorId": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "razonSocial": {
                          "type": "string"
                        },
                        "regimen": {
                          "type": "string",
                          "enum": [
                            "GENERAL",
                            "RIMPE_EMPRENDEDOR",
                            "RIMPE_NEGOCIO_POPULAR"
                          ]
                        },
                        "direccion": {
                          "type": "string"
                        },
                        "motivo": {
                          "type": "string"
                        },
                        "advertencias": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "camposPorCompletar": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "429": {
            "description": "Rate-limit de registro",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/tenants/nueva-empresa": {
      "post": {
        "tags": [
          "cuenta"
        ],
        "summary": "Crea otra empresa (RUC) del usuario autenticado",
        "description": "Una empresa = un RUC = una cuenta; solo se pide el RUC y el nombre sale de la razón social del catastro del SRI. La nueva empresa nace patrocinada por la cuenta actual: mientras no contrate plan propio emite con el plan del patrocinador y consume del cupo compartido. Devuelve tokens de la empresa nueva para aterrizar en ella. Roles owner/admin; 403 con API key o token OAuth.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ruc",
                  "declaraTitularidadRuc"
                ],
                "properties": {
                  "ruc": {
                    "type": "string",
                    "pattern": "^\\d{13}$",
                    "description": "Único dato de la empresa: el nombre sale de la razón social del catastro del SRI"
                  },
                  "declaraTitularidadRuc": {
                    "type": "boolean",
                    "description": "Declaración expresa de titularidad del RUC (obligatoria; 400 sin ella)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Empresa creada",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tenantId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "nombre": {
                      "type": "string"
                    },
                    "accessToken": {
                      "type": "string"
                    },
                    "refreshToken": {
                      "type": "string"
                    },
                    "precargaSri": {
                      "type": "object",
                      "description": "Resultado de la precarga del emisor desde el catastro del SRI"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "RUC inválido o sin la declaración de titularidad"
          },
          "403": {
            "description": "API key, token OAuth, o tope de empresas alcanzado"
          },
          "429": {
            "description": "Demasiadas altas seguidas (3/min)"
          }
        }
      }
    },
    "/tenants/current": {
      "get": {
        "tags": [
          "cuenta"
        ],
        "summary": "Cuenta del token",
        "responses": {
          "200": {
            "description": "Cuenta",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tenant"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "cuenta"
        ],
        "summary": "Editar nombre, ambiente (1=Pruebas, 2=Producción) o perfil de emisión",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "nombre": {
                    "type": "string"
                  },
                  "ambienteActivo": {
                    "type": "integer",
                    "enum": [
                      1,
                      2
                    ]
                  },
                  "perfilEmision": {
                    "type": "string",
                    "enum": [
                      "comercio",
                      "profesional"
                    ],
                    "description": "Con que pantalla factura la empresa: 'comercio' (catalogo, buscador, escaner, stock, propina) o 'profesional' (conceptos de texto libre). Solo afecta al dashboard; no viaja a ningun comprobante."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cuenta actualizada",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tenant"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          }
        }
      }
    },
    "/tenants/current/uso-plan": {
      "get": {
        "tags": [
          "cuenta"
        ],
        "summary": "Plan efectivo y consumo del mes",
        "responses": {
          "200": {
            "description": "Uso del plan",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsoPlan"
                }
              }
            }
          }
        }
      }
    },
    "/equipo": {
      "get": {
        "tags": [
          "equipo"
        ],
        "summary": "Listar miembros",
        "responses": {
          "200": {
            "description": "Miembros",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Miembro"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "equipo"
        ],
        "summary": "Alta de miembro (owner/admin; respeta el límite del plan)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "roles"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "nombre": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string"
                  },
                  "roles": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/Rol"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Miembro creado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Miembro"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          }
        }
      }
    },
    "/equipo/{membershipId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/membershipId"
        }
      ],
      "patch": {
        "tags": [
          "equipo"
        ],
        "summary": "Cambiar roles",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "roles"
                ],
                "properties": {
                  "roles": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/Rol"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Miembro actualizado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Miembro"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          }
        }
      },
      "delete": {
        "tags": [
          "equipo"
        ],
        "summary": "Quitar membership (owner; debe quedar ≥1 owner)",
        "responses": {
          "204": {
            "description": "Eliminado"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          }
        }
      }
    },
    "/emisores/catastro/{ruc}": {
      "parameters": [
        {
          "name": "ruc",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^\\d{13}$"
          }
        }
      ],
      "get": {
        "tags": [
          "organizacion"
        ],
        "summary": "Consultar el catastro público del SRI por RUC (prellenado del emisor)",
        "description": "Devuelve razón social, dirección, régimen mapeado y —cuando el catastro lo declara— `obligadoContabilidad`, más advertencias (fantasma, transacciones inexistentes, estado distinto de ACTIVO). No persiste nada. 400 si el dígito verificador del RUC no valida; si el catastro no responde o el RUC no consta, responde 200 con `encontrado: false` y `motivo`.",
        "responses": {
          "200": {
            "description": "Resultado de la consulta",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "encontrado",
                    "advertencias"
                  ],
                  "properties": {
                    "encontrado": {
                      "type": "boolean"
                    },
                    "razonSocial": {
                      "type": "string"
                    },
                    "direccion": {
                      "type": "string"
                    },
                    "regimen": {
                      "type": "string",
                      "enum": [
                        "GENERAL",
                        "RIMPE_EMPRENDEDOR",
                        "RIMPE_NEGOCIO_POPULAR"
                      ]
                    },
                    "obligadoContabilidad": {
                      "type": "boolean",
                      "description": "Ausente si el catastro no lo informa para ese RUC."
                    },
                    "advertencias": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "motivo": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/emisores": {
      "get": {
        "tags": [
          "organizacion"
        ],
        "summary": "Listar emisores",
        "responses": {
          "200": {
            "description": "Emisores",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Emisor"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "organizacion"
        ],
        "summary": "Crear emisor (RUC validado con dígito verificador)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmisorInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Emisor creado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Emisor"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/emisores/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "organizacion"
        ],
        "summary": "Detalle del emisor",
        "responses": {
          "200": {
            "description": "Emisor",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Emisor"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      },
      "patch": {
        "tags": [
          "organizacion"
        ],
        "summary": "Editar emisor",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmisorPatchInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Emisor actualizado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Emisor"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "organizacion"
        ],
        "summary": "Eliminar emisor (409 si ya emitió comprobantes)",
        "responses": {
          "204": {
            "description": "Eliminado"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/emisores/{emisorId}/establecimientos": {
      "parameters": [
        {
          "$ref": "#/components/parameters/emisorId"
        }
      ],
      "get": {
        "tags": [
          "organizacion"
        ],
        "summary": "Listar establecimientos",
        "responses": {
          "200": {
            "description": "Establecimientos",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Establecimiento"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "organizacion"
        ],
        "summary": "Crear establecimiento (código de 3 dígitos)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "codigo",
                  "direccion"
                ],
                "properties": {
                  "codigo": {
                    "type": "string",
                    "pattern": "^\\d{3}$"
                  },
                  "direccion": {
                    "type": "string"
                  },
                  "nombre": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Establecimiento creado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Establecimiento"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/establecimientos/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "patch": {
        "tags": [
          "organizacion"
        ],
        "summary": "Editar establecimiento",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "direccion": {
                    "type": "string"
                  },
                  "nombre": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Actualizado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Establecimiento"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "organizacion"
        ],
        "summary": "Eliminar establecimiento",
        "responses": {
          "204": {
            "description": "Eliminado"
          }
        }
      }
    },
    "/establecimientos/{establecimientoId}/puntos-emision": {
      "parameters": [
        {
          "name": "establecimientoId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "tags": [
          "organizacion"
        ],
        "summary": "Listar puntos de emisión",
        "responses": {
          "200": {
            "description": "Puntos",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PuntoEmision"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "organizacion"
        ],
        "summary": "Crear punto de emisión (código de 3 dígitos)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "codigo"
                ],
                "properties": {
                  "codigo": {
                    "type": "string",
                    "pattern": "^\\d{3}$"
                  },
                  "descripcion": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Punto creado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PuntoEmision"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/puntos-emision/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "patch": {
        "tags": [
          "organizacion"
        ],
        "summary": "Editar punto de emisión",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "descripcion": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Actualizado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PuntoEmision"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "organizacion"
        ],
        "summary": "Eliminar punto de emisión",
        "responses": {
          "204": {
            "description": "Eliminado"
          }
        }
      }
    },
    "/emisores/{emisorId}/secuenciales": {
      "parameters": [
        {
          "$ref": "#/components/parameters/emisorId"
        }
      ],
      "delete": {
        "tags": [
          "organizacion"
        ],
        "summary": "Eliminar el contador de una serie (deshacer un fijar equivocado)",
        "description": "Borra la fila del contador de la serie indicada. Solo si la serie no tiene comprobantes emitidos: con historial responde 409 (borrar el contador reiniciaría la numeración en 1 y colisionaría con lo ya emitido). 404 si la serie no tiene contador.",
        "parameters": [
          {
            "name": "tipo",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "establecimiento",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "puntoEmision",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ambiente",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "enum": [
                1,
                2
              ]
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Contador eliminado"
          },
          "409": {
            "description": "La serie ya tiene comprobantes emitidos"
          }
        }
      },
      "get": {
        "tags": [
          "organizacion"
        ],
        "summary": "Contadores de secuenciales por serie",
        "responses": {
          "200": {
            "description": "Series",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Secuencial"
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "organizacion"
        ],
        "summary": "Fijar el último número usado de una serie",
        "description": "Migración desde otro sistema o error 45 del SRI («secuencial registrado»): el siguiente comprobante será `ultimoNumero + 1`. No se permite bajar del secuencial más alto ya emitido por la cuenta (400).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FijarSecuencial"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Serie actualizada",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Secuencial"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          }
        }
      },
      "put": {
        "tags": [
          "organizacion"
        ],
        "summary": "Alias deprecado del PATCH",
        "deprecated": true,
        "description": "Se mantiene hasta el 2027-06-30 (headers `Deprecation`/`Sunset`). Usa PATCH: el body es parcial, no un reemplazo del recurso.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FijarSecuencial"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Serie actualizada",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Secuencial"
                }
              }
            }
          }
        }
      }
    },
    "/certificados": {
      "get": {
        "tags": [
          "certificados"
        ],
        "summary": "Listar certificados (sin material secreto)",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Certificados",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Certificado"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "certificados"
        ],
        "summary": "Subir .p12 (se cifra con envelope AES-256-GCM)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId",
                  "p12Base64",
                  "passphrase",
                  "declaraTitularidadFirma"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "p12Base64": {
                    "type": "string",
                    "description": "Contenido del .p12 en base64"
                  },
                  "passphrase": {
                    "type": "string"
                  },
                  "declaraTitularidadFirma": {
                    "type": "boolean",
                    "description": "Declaración expresa de titularidad de la firma: quien la sube declara ser su titular o contar con autorización de este para firmar con ella. Obligatoria (400 sin ella); se guardan fecha, usuario, IP y versión del texto como evidencia."
                  },
                  "alias": {
                    "type": "string"
                  },
                  "ponerEnUso": {
                    "type": "boolean",
                    "default": true,
                    "description": "Omitido o true: la firma entra en uso y jubila a la anterior. false: queda EN ESPERA (estado inactivo, la actual sigue firmando) para probarla contra el SRI con POST /certificados/{id}/probar antes de que reemplace a la actual."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Certificado registrado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "subject": {
                      "type": "string"
                    },
                    "notAfter": {
                      "type": "string"
                    },
                    "estado": {
                      "type": "string",
                      "enum": [
                        "activo",
                        "inactivo"
                      ],
                      "description": "activo = en uso; inactivo = en espera (ponerEnUso: false)."
                    },
                    "advertencia": {
                      "type": "string",
                      "description": "Presente si el RUC del emisor no aparece en el titular del certificado: probable .p12 equivocado. Se guarda igualmente; el SRI es el control duro."
                    },
                    "solicitudFirmaCerrada": {
                      "type": "boolean",
                      "description": "Presente (y true) solo cuando este .p12 cerró una solicitud de firma que estaba EN_REVISION ante NewBest, en la misma transacción que el alta del certificado. Solo ocurre si el issuer del certificado es NewBest; con otra certificadora la solicitud sigue abierta. Ausente en el resto de subidas."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          }
        }
      }
    },
    "/certificados/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "delete": {
        "tags": [
          "certificados"
        ],
        "summary": "Eliminar certificado (borrado físico del .p12 cifrado)",
        "responses": {
          "204": {
            "description": "Eliminado"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      },
      "patch": {
        "tags": [
          "certificados"
        ],
        "summary": "Activar o revocar una firma",
        "description": "`activar` vuelve a firmar con este certificado y jubila al que estuviera activo (una firma en uso por emisor); `revocar` lo marca como inválido —lo revocó la certificadora, se comprometió la clave o su titular dejó de estar vinculado— sin borrar el material cifrado. Revocar es terminal: no se reactiva (409); si la firma vuelve a ser válida, se sube de nuevo. La emisión solo acepta certificados en estado `activo`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "accion"
                ],
                "properties": {
                  "accion": {
                    "type": "string",
                    "enum": [
                      "activar",
                      "revocar"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Estado resultante",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "estado": {
                      "type": "string",
                      "enum": [
                        "activo",
                        "inactivo",
                        "revocado"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/certificados/{id}/probar": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "certificados"
        ],
        "summary": "Probar una firma contra el SRI (factura de PRUEBA)",
        "description": "Emite una factura de PRUEBA (ambiente 1, consumidor final, un dólar, sin validez tributaria) firmada con ESTE certificado, aunque la empresa esté en Producción y aunque la firma esté en espera (inactiva). Los secuenciales y listados de Pruebas van aparte. Si el SRI la autoriza, el worker marca la firma como verificada y la pone en uso; la que firmaba queda inactiva, recuperable con PATCH accion=activar. El resultado se sigue en GET /certificados (campos `prueba` y `verificadaPruebasAt`). Roles: owner, admin. Una firma revocada o vencida responde 409. En la empresa de la casa responde 403: su firma se administra desde la consola de operador.",
        "responses": {
          "202": {
            "description": "Factura de prueba encolada",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comprobanteId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "claveAcceso": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/solicitudes-firma/config": {
      "get": {
        "tags": [
          "solicitudes-firma"
        ],
        "summary": "Catálogos, tarifario, cuentas y consentimiento",
        "description": "Todo lo que hace falta para pintar el asistente sin duplicar catálogos ni el texto legal. Roles: owner, admin, emisor, contador o lector. Responde 404 cuando NEWBEST_HABILITADO no está encendido: es la señal de que el módulo no existe en este despliegue.",
        "responses": {
          "200": {
            "description": "Configuración del módulo",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolicitudFirmaConfig"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/solicitudes-firma": {
      "get": {
        "tags": [
          "solicitudes-firma"
        ],
        "summary": "Listar solicitudes de firma del tenant",
        "description": "La más reciente primero. Roles: owner, admin, emisor, contador o lector.",
        "parameters": [
          {
            "$ref": "#/components/parameters/emisorId"
          }
        ],
        "responses": {
          "200": {
            "description": "Solicitudes (sin documentos)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SolicitudFirma"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          }
        }
      },
      "post": {
        "tags": [
          "solicitudes-firma"
        ],
        "summary": "Abrir una solicitud en BORRADOR",
        "description": "Precarga el titular desde el emisor, el correo del usuario actual y el catastro público del SRI (best-effort). Roles: owner o admin. Responde 409 si esa empresa ya tiene una solicitud viva: solo una a la vez por emisor, porque NewBest cobraría dos trámites y su API no tiene forma de anular ninguno.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId",
                  "tipoFirma",
                  "vigencia"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "tipoFirma": {
                    "type": "integer",
                    "enum": [
                      1,
                      2,
                      3
                    ],
                    "description": "1 persona natural, 2 persona natural con RUC, 3 representante legal de empresa. El 4 (miembro de empresa) existe en NewBest pero está fuera de v1."
                  },
                  "vigencia": {
                    "type": "integer",
                    "description": "id_firma_tiempo del catálogo; debe tener precio vigente para ese tipo (ver GET /solicitudes-firma/config)."
                  },
                  "esRenovacion": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Solicitud creada con el titular precargado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolicitudFirma"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/solicitudes-firma/{id}": {
      "get": {
        "tags": [
          "solicitudes-firma"
        ],
        "summary": "Detalle de una solicitud con sus documentos",
        "description": "Incluye `documentos` y la señal `atascada`. Roles: owner, admin, emisor, contador o lector.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id de la solicitud.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Solicitud con documentos",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolicitudFirma"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      },
      "patch": {
        "tags": [
          "solicitudes-firma"
        ],
        "summary": "Guardar el ID de persona y/o los datos del titular",
        "description": "Solo en BORRADOR, ERROR_DATOS o ERROR_ENVIO (409 en el resto: lo que ya se envió a NewBest no se puede cambiar por este lado, su API no tiene PUT). El titular se valida entero: cédula y RUC pasan por el validador fiscal del SRI y el celular se normaliza a 09XXXXXXXX. Roles: owner o admin.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id de la solicitud.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "newbestIdPersona": {
                    "type": "string",
                    "pattern": "^[0-9]{1,20}$",
                    "description": "Número que el asistente de NewBest muestra al titular al terminar su registro."
                  },
                  "titular": {
                    "$ref": "#/components/schemas/TitularSolicitud"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Solicitud actualizada",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolicitudFirma"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/solicitudes-firma/{id}/documentos": {
      "post": {
        "tags": [
          "solicitudes-firma"
        ],
        "summary": "Subir (o reemplazar) un documento de la solicitud",
        "description": "Solo los documentos de origen `contadeo` del tipo de firma: las fotos de cédula y la selfie las entrega el titular en el asistente de NewBest y no pasan por aquí. Un documento por tipo: volver a subir el mismo tipo reemplaza al anterior y borra su objeto. La extensión sale del content-type, nunca del nombre del fichero. Solo mientras la solicitud sea editable. Roles: owner o admin.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id de la solicitud.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tipoArchivo",
                  "nombre",
                  "contentType",
                  "base64"
                ],
                "properties": {
                  "tipoArchivo": {
                    "type": "integer",
                    "description": "Id del tipo de archivo en el catálogo de NewBest; debe ser uno de los de origen `contadeo` del tipo de firma de esta solicitud."
                  },
                  "nombre": {
                    "type": "string",
                    "maxLength": 255,
                    "example": "comprobante.pdf"
                  },
                  "contentType": {
                    "type": "string",
                    "enum": [
                      "application/pdf",
                      "image/jpeg",
                      "image/png"
                    ],
                    "description": "Además tiene que estar entre los formatos que admite ese tipo de archivo."
                  },
                  "base64": {
                    "type": "string",
                    "format": "byte",
                    "description": "Contenido del archivo en base64. Máximo 10 MB (~13,3 MB en base64)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Documento guardado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolicitudFirmaDocumento"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/solicitudes-firma/{id}/documentos/{docId}": {
      "delete": {
        "tags": [
          "solicitudes-firma"
        ],
        "summary": "Borrar un documento y su objeto del almacenamiento",
        "description": "Solo mientras la solicitud sea editable. Roles: owner o admin.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id de la solicitud.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "docId",
            "in": "path",
            "required": true,
            "description": "Id del documento.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Documento eliminado"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/solicitudes-firma/{id}/documentos/{docId}/url": {
      "get": {
        "tags": [
          "solicitudes-firma"
        ],
        "summary": "URL prefirmada para revisar un documento",
        "description": "Roles: owner, admin, emisor, contador o lector.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id de la solicitud.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "docId",
            "in": "path",
            "required": true,
            "description": "Id del documento.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "URL temporal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/solicitudes-firma/{id}/enviar": {
      "post": {
        "tags": [
          "solicitudes-firma"
        ],
        "summary": "Enviar la solicitud a NewBest",
        "description": "Congela el precio del tarifario, guarda la evidencia del consentimiento (fecha, usuario, IP y versión del texto) y encola el trabajo: el trámite lo hace el worker, no esta petición, de ahí el 202. Un 400 devuelve en `problemas[]` TODO lo que falta (documentos, ID de persona, campos del titular, consentimiento). 409 si ya se está enviando. 503 si no se pudo encolar: la solicitud queda en ERROR_ENVIO y se puede reintentar. Roles: owner o admin.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id de la solicitud.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "aceptaConsentimiento"
                ],
                "properties": {
                  "aceptaConsentimiento": {
                    "type": "boolean",
                    "description": "Aceptación del texto que devuelve GET /solicitudes-firma/config. Obligatoria: es la base de licitud LOPDP para comunicar los datos del titular a NewBest."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Solicitud encolada",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "estado": {
                      "type": "string",
                      "enum": [
                        "LISTA"
                      ]
                    },
                    "envios": {
                      "type": "integer"
                    },
                    "valorRealCentavos": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/solicitudes-firma/{id}/cancelar": {
      "post": {
        "tags": [
          "solicitudes-firma"
        ],
        "summary": "Cancelar la solicitud",
        "description": "Cierra el trámite y libera al emisor para poder abrir otro. No se puede cancelar mientras está ENVIANDO (409): el worker está a mitad de la conversación con NewBest, que no tiene idempotencia ni consulta de estado. Roles: owner o admin.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id de la solicitud.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Solicitud cancelada",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SolicitudFirma"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/productos": {
      "get": {
        "tags": [
          "catalogos"
        ],
        "summary": "Listar/buscar productos (inventario)",
        "parameters": [
          {
            "$ref": "#/components/parameters/q"
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Desplazamiento de página; activa la paginación",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "orden",
            "in": "query",
            "required": false,
            "description": "Campo de orden",
            "schema": {
              "type": "string",
              "enum": [
                "nombre",
                "precio",
                "codigo",
                "stock"
              ]
            }
          },
          {
            "name": "dir",
            "in": "query",
            "required": false,
            "description": "Dirección del orden",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            }
          },
          {
            "name": "tarifa",
            "in": "query",
            "required": false,
            "description": "Filtra por código de tarifa IVA (Tabla 18)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "categoria",
            "in": "query",
            "required": false,
            "description": "Filtra por categoría exacta",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "estado",
            "in": "query",
            "required": false,
            "description": "Filtro de existencias",
            "schema": {
              "type": "string",
              "enum": [
                "agotado",
                "bajo",
                "sin_control",
                "con_stock"
              ]
            }
          },
          {
            "name": "activo",
            "in": "query",
            "required": false,
            "description": "true (default) | false (archivados) | todos",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false",
                "todos"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Productos",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Producto"
                  }
                }
              }
            }
          }
        },
        "description": "Sin parámetros devuelve todo el catálogo activo (contrato legacy). Con `offset` pagina (límite por defecto 50, máx. 200)."
      },
      "post": {
        "tags": [
          "catalogos"
        ],
        "summary": "Crear producto (código único por cuenta)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductoInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Producto creado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Producto"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/productos/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "patch": {
        "tags": [
          "catalogos"
        ],
        "summary": "Editar producto",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductoInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Producto actualizado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Producto"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "catalogos"
        ],
        "summary": "Eliminar producto (borrado lógico)",
        "responses": {
          "204": {
            "description": "Eliminado"
          }
        }
      }
    },
    "/clientes": {
      "get": {
        "tags": [
          "catalogos"
        ],
        "summary": "Listar/buscar clientes",
        "description": "`q` busca en razón social, identificación y email.",
        "parameters": [
          {
            "$ref": "#/components/parameters/q"
          },
          {
            "$ref": "#/components/parameters/limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Clientes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Cliente"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "catalogos"
        ],
        "summary": "Crear cliente (tipo+identificación únicos por cuenta)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClienteInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cliente creado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cliente"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/clientes/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "patch": {
        "tags": [
          "catalogos"
        ],
        "summary": "Editar cliente",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClienteInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cliente actualizado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cliente"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "catalogos"
        ],
        "summary": "Eliminar cliente",
        "responses": {
          "204": {
            "description": "Eliminado"
          }
        }
      }
    },
    "/clientes/{id}/bloquear": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "catalogos"
        ],
        "summary": "Bloquear la ficha del comprador (LOPDP)",
        "description": "Resolución SPDP-SPD-2025-0030-R (arts. 12-14): la ficha sale de listados, prellenado, CSV y reenvíos, pero NO se borra: se conserva con acceso restringido mientras una obligación legal lo exija. Reversible con /desbloquear. Solo owner y admin; queda registrado en la auditoría de accesos.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "motivo"
                ],
                "properties": {
                  "motivo": {
                    "type": "string",
                    "maxLength": 300,
                    "description": "Por qué se bloquea (p. ej. la solicitud de supresión que lo origina)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cliente bloqueado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cliente"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/clientes/{id}/desbloquear": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "catalogos"
        ],
        "summary": "Levantar el bloqueo de la ficha del comprador (LOPDP)",
        "description": "El titular retiró su solicitud o se resolvió que no procedía. Solo owner y admin; queda registrado en la auditoría de accesos.",
        "responses": {
          "200": {
            "description": "Cliente desbloqueado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cliente"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/privacidad/supresiones": {
      "post": {
        "tags": [
          "privacidad"
        ],
        "summary": "Registrar la supresión de un comprador (LOPDP)",
        "description": "Resolución SPDP-SPD-2025-0030-R. El efecto es INMEDIATO y cumple la suspensión del art. 16: bloquea las fichas del comprador en el directorio (todas las de esa identificación, sea cual sea su tipo) y todos los comprobantes emitidos a ella. La eliminación del art. 23 se ejecuta al vencer `ejecutarAt` (dentro de las 72 horas desde la solicitud) y alcanza solo a lo que ninguna obligación ampara: el `clienteSnapshot` de los comprobantes persiste bloqueado por la retención tributaria de 7 años. Reenviar la solicitud NO reinicia el plazo: devuelve la que ya estaba. Solo owner y admin; queda registrado en la auditoría de accesos.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tipoIdentificacion",
                  "identificacion"
                ],
                "properties": {
                  "tipoIdentificacion": {
                    "type": "string",
                    "description": "Tabla 7 del SRI: 04 RUC, 05 cédula, 06 pasaporte, 07 consumidor final, 08 identificación del exterior, 09 placa. El 07 se admite salvo con el comodín de consumidor final (13 nueves), que no identifica a ningún titular.",
                    "enum": [
                      "04",
                      "05",
                      "06",
                      "07",
                      "08",
                      "09"
                    ]
                  },
                  "identificacion": {
                    "type": "string",
                    "maxLength": 20,
                    "description": "Identificación del comprador. El consumidor final (13 nueves) no identifica a un titular y se rechaza con 400."
                  },
                  "motivo": {
                    "type": "string",
                    "maxLength": 300,
                    "description": "Opcional: la ley no obliga al titular a motivar su solicitud."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Solicitud registrada y tratamiento suspendido",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "supresion": {
                      "$ref": "#/components/schemas/Supresion"
                    },
                    "fichaBloqueada": {
                      "type": "boolean",
                      "description": "Si había alguna ficha con esa identificación en el directorio y quedó bloqueada."
                    },
                    "comprobantesBloqueados": {
                      "type": "integer",
                      "description": "Comprobantes que esta solicitud acaba de bloquear."
                    },
                    "yaExistia": {
                      "type": "boolean",
                      "description": "true cuando ya había una solicitud pendiente para esa identificación y se devuelve esa, con su plazo original: reenviar el formulario no abre un segundo reloj ni reinicia el del art. 23."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      },
      "get": {
        "tags": [
          "privacidad"
        ],
        "summary": "Listar las supresiones del tenant (LOPDP)",
        "description": "Expediente de las solicitudes de supresión, de la más reciente a la más antigua. Las filas no se borran nunca: son la lista de exclusión permanente que impide que un comprador suprimido vuelva al directorio. Solo owner y admin; cada consulta queda en la auditoría de accesos.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Solicitudes de supresión",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Supresion"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/privacidad/supresiones/{id}/retirar": {
      "post": {
        "tags": [
          "privacidad"
        ],
        "summary": "Retirar una solicitud de supresión (LOPDP)",
        "description": "El titular se echa atrás dentro del plazo del art. 23, que existe precisamente para dejar esa puerta abierta. La solicitud pasa a `retirada` (la fila NO se borra: que llegó, que el tratamiento se suspendió en el acto y que después se retiró son tres hechos del expediente ARCO) y se levanta el bloqueo de las fichas del directorio y de los comprobantes de esa identificación. Solo desde `pendiente`: una supresión ya ejecutada da 409 porque la eliminación es irreversible y hay una constancia emitida que la acredita. Si sobre el mismo número queda otra solicitud viva bajo otro tipo de la tabla 7, la fila se marca pero no se desbloquea nada. Es la única vía para deshacer la suspensión del art. 16: `POST /clientes/{id}/desbloquear` rechaza con 409 una ficha con supresión viva detrás. Solo owner y admin; queda registrado en la auditoría de accesos.",
        "parameters": [
          {
            "$ref": "#/components/parameters/id"
          }
        ],
        "responses": {
          "200": {
            "description": "Solicitud retirada y bloqueo levantado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "supresion": {
                      "$ref": "#/components/schemas/Supresion"
                    },
                    "fichasDesbloqueadas": {
                      "type": "integer",
                      "description": "Fichas del directorio que volvieron a listados, CSV y prellenado."
                    },
                    "comprobantesDesbloqueados": {
                      "type": "integer",
                      "description": "Comprobantes que salieron de la conservación bloqueada."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/privacidad/constancias": {
      "get": {
        "tags": [
          "privacidad"
        ],
        "summary": "Listar las constancias de eliminación (LOPDP)",
        "description": "Documentos acreditativos de los arts. 20 y 24 de la resolución SPDP-SPD-2025-0030-R: uno por cada eliminación efectiva. El inventario son CONTEOS por repositorio, nunca copias, y la identificación del titular va enmascarada. Declara además lo que sobrevive amparado (la retención tributaria de 7 años en bloqueo) y lo que no se purga (las trazas append-only, datos seudonimizados con ciclo de vida propio por el art. 5). Solo owner; cada consulta queda en la auditoría de accesos. Las constancias de baja de una empresa (alcance `baja_empresa`) NO aparecen aquí: se entregan en la respuesta de `DELETE /tenants/{tenantId}`, porque después no queda empresa desde la que pedirlas. Las de archivo (`archivo_empresa`) sí: la empresa archivada sigue existiendo.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Constancias emitidas, de la más reciente a la más antigua",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ConstanciaEliminacion"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/privacidad/constancias/{id}.csv": {
      "get": {
        "tags": [
          "privacidad"
        ],
        "summary": "Descargar una constancia de eliminación (CSV)",
        "description": "El documento acreditativo tal como se archiva y se adjunta a la respuesta al titular. Incluye el hash SHA-256 del inventario para que quien lo reciba pueda recalcularlo y comprobar que nadie lo reescribió. Solo owner; queda en la auditoría de accesos.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "CSV",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/clientes/export.csv": {
      "get": {
        "tags": [
          "catalogos"
        ],
        "summary": "Exportar clientes a CSV (LOPDP: portabilidad)",
        "responses": {
          "200": {
            "description": "CSV",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/clientes/lookup/{identificacion}": {
      "get": {
        "tags": [
          "catalogos"
        ],
        "summary": "Prellenado del comprador por RUC/cédula",
        "description": "Busca primero en el directorio de clientes (devuelve también email/teléfono) y, si no está, en el catastro público del SRI. Para cédula (`tipo=05`) consulta el RUC de persona natural (cédula+`001`). `advertencias` avisa de RUC suspendido/fantasma (informativo).",
        "parameters": [
          {
            "name": "identificacion",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "1790016919001"
          },
          {
            "name": "tipo",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "04",
                "05"
              ]
            },
            "description": "04=RUC, 05=cédula"
          }
        ],
        "responses": {
          "200": {
            "description": "Datos para prellenar",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClienteLookup"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "404": {
            "description": "Sin datos (ni directorio ni SRI)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Catastro del SRI no disponible - tratar como \"sin prellenado\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/clientes/por-identificacion/{tipo}/{identificacion}": {
      "put": {
        "tags": [
          "catalogos"
        ],
        "summary": "Upsert por clave natural (\"recordar cliente\")",
        "description": "Crea o actualiza el cliente con esa identificación. Solo pisa los campos que llegan con valor: un upsert sin `email` no borra el email guardado. 409 si esa identificación está bloqueada por una solicitud de supresión (LOPDP): el bloqueo no se levanta reescribiendo la ficha.",
        "parameters": [
          {
            "name": "tipo",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/TipoIdentificacion"
            }
          },
          {
            "name": "identificacion",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "razonSocial"
                ],
                "properties": {
                  "razonSocial": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string"
                  },
                  "telefono": {
                    "type": "string"
                  },
                  "direccion": {
                    "type": "string"
                  },
                  "placa": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cliente creado/actualizado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cliente"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/clientes/upsert": {
      "post": {
        "tags": [
          "catalogos"
        ],
        "summary": "Alias interno deprecado del PUT /clientes/por-identificacion",
        "deprecated": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClienteInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cliente creado/actualizado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cliente"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/sri/ruc/{ruc}": {
      "get": {
        "tags": [
          "catalogos"
        ],
        "summary": "Validación de RUC contra el catastro del SRI",
        "description": "Veredicto SIEMPRE 200: `valido` (estructura y dígito verificador; el RUC de sociedad privada se valida solo por estructura, como hace el SRI), `encontrado` (consta en el catastro) y, si consta, el contribuyente con razón social, estado, régimen mapeado (`GENERAL | RIMPE_EMPRENDEDOR | RIMPE_NEGOCIO_POPULAR`) y crudo (`regimenSri`), tipo de contribuyente, actividad económica (descripción, no CIIU), dirección, `obligadoContabilidad`, `contribuyenteEspecial` y `agenteRetencion`, más `advertencias` (RUC suspendido, fantasma o con transacciones inexistentes). Resiliente: caché de 24 h y, si el SRI no responde, sirve la última copia (hasta 7 días, `fuente: \"cache\"` y `degradado: true` si venció su frescura); sin copia responde `degradado: true` con `motivo`. Nunca 404 ni 503. Alcanzable con API key (roles emisor, contador o lector).",
        "parameters": [
          {
            "name": "ruc",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "RUC de 13 dígitos. Para una cédula, consulta cédula + 001."
          }
        ],
        "responses": {
          "200": {
            "description": "Veredicto de validación",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ruc",
                    "valido",
                    "encontrado",
                    "consultadoAt",
                    "advertencias"
                  ],
                  "properties": {
                    "ruc": {
                      "type": "string"
                    },
                    "valido": {
                      "type": "boolean"
                    },
                    "encontrado": {
                      "type": "boolean"
                    },
                    "motivo": {
                      "type": "string"
                    },
                    "degradado": {
                      "type": "boolean"
                    },
                    "fuente": {
                      "type": "string",
                      "enum": [
                        "sri",
                        "cache"
                      ]
                    },
                    "consultadoAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "contribuyente": {
                      "type": "object",
                      "required": [
                        "razonSocial",
                        "regimen"
                      ],
                      "properties": {
                        "razonSocial": {
                          "type": "string"
                        },
                        "estado": {
                          "type": "string"
                        },
                        "regimen": {
                          "type": "string",
                          "enum": [
                            "GENERAL",
                            "RIMPE_EMPRENDEDOR",
                            "RIMPE_NEGOCIO_POPULAR"
                          ]
                        },
                        "regimenSri": {
                          "type": "string"
                        },
                        "tipoContribuyente": {
                          "type": "string"
                        },
                        "actividadEconomica": {
                          "type": "string"
                        },
                        "direccion": {
                          "type": "string"
                        },
                        "obligadoContabilidad": {
                          "type": "boolean"
                        },
                        "contribuyenteEspecial": {
                          "type": "boolean"
                        },
                        "agenteRetencion": {
                          "type": "boolean"
                        }
                      }
                    },
                    "advertencias": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sri/catalogos": {
      "get": {
        "tags": [
          "catalogos"
        ],
        "summary": "Tablas de referencia del SRI (vivas)",
        "description": "Las tablas con las que valida el backend: tarifa general de IVA **vigente**, tarifas por código, formas de pago, tipos de identificación/comprobante y regla de consumidor final. Consúmelas en vez de hardcodearlas.",
        "responses": {
          "200": {
            "description": "Catálogos",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogosSri"
                }
              }
            }
          }
        }
      }
    },
    "/sri/glosario": {
      "get": {
        "tags": [
          "catalogos"
        ],
        "summary": "Glosario tributario en lenguaje llano",
        "description": "Definiciones de los términos del SRI (RIDE, clave de acceso, RIMPE, retención, crédito tributario, punto de emisión…) escritas para quien nunca ha facturado. Existe para que un asistente de IA no los explique de memoria. `?q=` busca por término, alias coloquial o texto de la definición; sin `q` devuelve el catálogo completo. Las cifras con vigencia no viven aquí: cada término que dependa de una apunta, en `consultaCon`, a la herramienta que la consulta.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Término a buscar, p. ej. \"RIDE\" o \"me retuvieron\"."
          }
        ],
        "responses": {
          "200": {
            "description": "Términos encontrados",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "terminos": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "termino": {
                            "type": "string"
                          },
                          "alias": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "categoria": {
                            "type": "string",
                            "enum": [
                              "comprobantes",
                              "regimen",
                              "impuestos",
                              "proceso",
                              "configuracion"
                            ]
                          },
                          "definicion": {
                            "type": "string"
                          },
                          "enContadeo": {
                            "type": "string"
                          },
                          "consultaCon": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "nota": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/comprobantes/factura": {
      "post": {
        "tags": [
          "comprobantes"
        ],
        "summary": "Emitir factura (01)",
        "description": "Asíncrono: valida, reserva secuencial y encola. Hacer polling de `GET /comprobantes/{id}`. `infoFactura.fechaEmision` es opcional (`YYYY-MM-DD`, calendario de Ecuador): omitida = hoy; explícita = nunca futura y máximo 5 días atrás (a >1 día la respuesta trae `advertencias` por riesgo de error 65 del SRI). Cuerpo completo por tipo: ver /desarrolladores.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmitirFactura"
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/Emitido"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "402": {
            "$ref": "#/components/responses/CupoAgotado"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ]
      }
    },
    "/comprobantes/liquidacion": {
      "post": {
        "tags": [
          "comprobantes"
        ],
        "summary": "Emitir liquidación de compra (03)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmitirLiquidacion"
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/Emitido"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "402": {
            "$ref": "#/components/responses/CupoAgotado"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "description": "Asíncrono, igual que factura. `infoLiquidacion.fechaEmision` opcional (`YYYY-MM-DD`, hora Ecuador; nunca futura, máx. 5 días atrás)."
      }
    },
    "/comprobantes/nota-credito": {
      "post": {
        "tags": [
          "comprobantes"
        ],
        "summary": "Emitir nota de crédito (04)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmitirNotaCredito"
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/Emitido"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "402": {
            "$ref": "#/components/responses/CupoAgotado"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "description": "Asíncrono, igual que factura. `infoNotaCredito.fechaEmision` opcional (`YYYY-MM-DD`, hora Ecuador; nunca futura, máx. 5 días atrás)."
      }
    },
    "/comprobantes/nota-debito": {
      "post": {
        "tags": [
          "comprobantes"
        ],
        "summary": "Emitir nota de débito (05)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmitirNotaDebito"
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/Emitido"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "402": {
            "$ref": "#/components/responses/CupoAgotado"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "description": "Asíncrono, igual que factura. `infoNotaDebito.fechaEmision` opcional (`YYYY-MM-DD`, hora Ecuador; nunca futura, máx. 5 días atrás)."
      }
    },
    "/comprobantes/retencion": {
      "post": {
        "tags": [
          "comprobantes"
        ],
        "summary": "Emitir retención ATS (07)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmitirRetencion"
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/Emitido"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "402": {
            "$ref": "#/components/responses/CupoAgotado"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "description": "Asíncrono, igual que factura. `infoCompRetencion.fechaEmision` opcional (`YYYY-MM-DD`, hora Ecuador; nunca futura, máx. 5 días atrás)."
      }
    },
    "/comprobantes/guia-remision": {
      "post": {
        "tags": [
          "comprobantes"
        ],
        "summary": "Emitir guía de remisión (06)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmitirGuiaRemision"
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/Emitido"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "402": {
            "$ref": "#/components/responses/CupoAgotado"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ]
      }
    },
    "/comprobantes": {
      "get": {
        "tags": [
          "comprobantes"
        ],
        "summary": "Listar comprobantes (más recientes primero)",
        "parameters": [
          {
            "name": "estado",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/EstadoComprobante"
            }
          },
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "name": "cliente",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Identificación exacta del comprador (cédula/RUC del clienteSnapshot): lista solo los comprobantes emitidos a ese cliente."
          },
          {
            "name": "ambiente",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "enum": [
                1,
                2
              ]
            },
            "description": "1=Pruebas, 2=Producción. Si se omite se aplica el ambiente ACTIVO de la cuenta: la respuesta nunca mezcla ensayos con comprobantes reales."
          },
          {
            "name": "desde",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "fechaEmision >= desde (YYYY-MM-DD). El panel lista por periodo."
          },
          {
            "name": "hasta",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "fechaEmision <= hasta (YYYY-MM-DD)."
          }
        ],
        "responses": {
          "200": {
            "description": "Comprobantes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Comprobante"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/comprobantes/export.csv": {
      "get": {
        "tags": [
          "comprobantes"
        ],
        "summary": "Exportar comprobantes a CSV (máx. 10 000 filas)",
        "parameters": [
          {
            "name": "estado",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "desde",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "hasta",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "ambiente",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "enum": [
                1,
                2
              ]
            },
            "description": "1=Pruebas, 2=Producción. Si se omite se aplica el ambiente ACTIVO de la cuenta: la respuesta nunca mezcla ensayos con comprobantes reales."
          }
        ],
        "responses": {
          "200": {
            "description": "CSV",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/comprobantes/bloqueados": {
      "get": {
        "tags": [
          "comprobantes"
        ],
        "summary": "Listar los comprobantes bloqueados (vía restringida LOPDP)",
        "description": "Vía restringida del art. 13 de la resolución SPDP-SPD-2025-0030-R: lo bloqueado no sale por ningún otro listado ni descarga. Sirve para revisar qué hay bloqueado y desde cuándo corre su plazo de conservación. Solo owner; cada consulta queda en la auditoría de accesos.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Comprobantes bloqueados, del bloqueo más reciente al más antiguo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "tipoComprobante": {
                        "type": "string"
                      },
                      "numero": {
                        "type": "string"
                      },
                      "claveAcceso": {
                        "type": "string"
                      },
                      "estado": {
                        "type": "string"
                      },
                      "ambiente": {
                        "type": "integer"
                      },
                      "fechaEmision": {
                        "type": "string",
                        "format": "date"
                      },
                      "identificacion": {
                        "type": "string",
                        "nullable": true,
                        "description": "Identificación del comprador; el resto de sus datos está en el detalle."
                      },
                      "bloqueadoAt": {
                        "type": "string",
                        "format": "date-time"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/comprobantes/{id}/bloqueado": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "comprobantes"
        ],
        "summary": "Detalle de un comprobante bloqueado (vía restringida LOPDP)",
        "description": "Devuelve el comprador completo, para atender un requerimiento sobre una supresión concreta. 404 si el comprobante no está bloqueado: para lo demás está GET /comprobantes/{id}. Solo owner; cada consulta queda en la auditoría de accesos.",
        "responses": {
          "200": {
            "description": "Comprobante bloqueado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/comprobantes/{id}": {
      "get": {
        "tags": [
          "comprobantes"
        ],
        "summary": "Detalle y estado del comprobante (para polling cada ~3 s)",
        "parameters": [
          {
            "$ref": "#/components/parameters/id"
          },
          {
            "name": "eventos",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "true = incluye el timeline de eventos de la emisión"
          }
        ],
        "responses": {
          "200": {
            "description": "Estado actual; en DEVUELTA/RECHAZADO incluye los mensajes del SRI",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EstadoConsulta"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/comprobantes/{id}/ride": {
      "get": {
        "tags": [
          "comprobantes"
        ],
        "summary": "URL prefirmada del RIDE (PDF; expira en 1 h)",
        "parameters": [
          {
            "$ref": "#/components/parameters/id"
          }
        ],
        "responses": {
          "200": {
            "description": "URL de descarga",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UrlDescarga"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/comprobantes/{id}/xml": {
      "get": {
        "tags": [
          "comprobantes"
        ],
        "summary": "URL prefirmada del XML autorizado (expira en 1 h)",
        "parameters": [
          {
            "$ref": "#/components/parameters/id"
          }
        ],
        "responses": {
          "200": {
            "description": "URL de descarga",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UrlDescarga"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/comprobantes/{id}/nota-credito-base": {
      "get": {
        "tags": [
          "comprobantes"
        ],
        "summary": "Datos base para una nota de crédito que revierte la factura",
        "description": "Referencia al documento, comprador y desglose de IVA por tarifa para armar el reverso TOTAL desde el comprobanteId. Lo usa el MCP. Roles: owner/admin/emisor/contador/lector.",
        "parameters": [
          {
            "$ref": "#/components/parameters/id"
          }
        ],
        "responses": {
          "200": {
            "description": "Base para la nota de crédito",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/comprobantes/{id}/anular": {
      "post": {
        "tags": [
          "comprobantes"
        ],
        "summary": "Marcar AUTORIZADO como ANULADO (registro interno)",
        "description": "Reglas NAC-DGERCGC25-00000017: hasta el día 7 inclusive del mes siguiente; nunca a consumidor final. El trámite formal se hace en SRI en línea.",
        "parameters": [
          {
            "$ref": "#/components/parameters/id"
          }
        ],
        "responses": {
          "201": {
            "description": "Anulado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "estado": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          }
        }
      }
    },
    "/comprobantes/{id}/reenviar-email": {
      "post": {
        "tags": [
          "comprobantes"
        ],
        "summary": "Reenviar RIDE+XML por email (202)",
        "description": "403 si el comprobante está bloqueado por una solicitud de supresión (LOPDP): el reenvío es un envío nuevo de datos personales a quien pidió que dejáramos de tratarlos.",
        "parameters": [
          {
            "$ref": "#/components/parameters/id"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "para": {
                    "type": "string",
                    "format": "email",
                    "description": "Sin él, usa el email del campo adicional del XML"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Encolado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "encolado": {
                      "type": "boolean"
                    },
                    "para": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/api-keys": {
      "get": {
        "tags": [
          "cuenta"
        ],
        "summary": "Listar API keys (sin la key completa)",
        "responses": {
          "200": {
            "description": "Keys",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ApiKey"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "cuenta"
        ],
        "summary": "Crear API key (la key completa solo viaja aquí)",
        "description": "La key completa (`cdo_…`) viaja solo en esta respuesta. Una key con rol `emisor`, `contador` o `lector` sirve además como credencial del MCP en modo stdio (`CONTADEO_API_KEY`), así que la respuesta incluye `avisoIa`. La primera vez que la empresa abre el canal de IA -por esta vía o autorizando el conector remoto en `POST /oauth/consentimiento`- se manda además un correo al owner.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "nombre"
                ],
                "properties": {
                  "nombre": {
                    "type": "string"
                  },
                  "roles": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "emisor",
                        "contador",
                        "lector",
                        "webhooks"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Key creada (con `avisoIa` si abre el canal de IA)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyCreada"
                }
              }
            }
          }
        }
      }
    },
    "/api-keys/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "delete": {
        "tags": [
          "cuenta"
        ],
        "summary": "Revocar API key (instantáneo)",
        "responses": {
          "204": {
            "description": "Revocada"
          }
        }
      }
    },
    "/partner/branding": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Marca del partner en los correos al comprador (co-branding)",
        "description": "El correo del comprobante sale con la marca del partner; el pie siempre indica que el servicio lo opera Contadeo.",
        "responses": {
          "200": {
            "description": "Branding actual",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "marcaNombre": {
                      "type": "string",
                      "nullable": true,
                      "example": "Facteo Pro"
                    },
                    "marcaColor": {
                      "type": "string",
                      "nullable": true,
                      "example": "#7C3AED",
                      "description": "Hex \"#RRGGBB\" de la franja de acento del correo"
                    },
                    "tieneLogo": {
                      "type": "boolean"
                    },
                    "logoUrl": {
                      "type": "string",
                      "nullable": true,
                      "description": "URL prefirmada del logo para previsualizar (caduca); los correos usan la URL publica estable"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "partner"
        ],
        "summary": "Guarda nombre de marca y/o color de acento",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "marcaNombre": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 80,
                    "description": "null o \"\" quita el co-branding"
                  },
                  "marcaColor": {
                    "type": "string",
                    "nullable": true,
                    "pattern": "^#[0-9A-Fa-f]{6}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Branding actualizado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "marcaNombre": {
                      "type": "string",
                      "nullable": true,
                      "example": "Facteo Pro"
                    },
                    "marcaColor": {
                      "type": "string",
                      "nullable": true,
                      "example": "#7C3AED",
                      "description": "Hex \"#RRGGBB\" de la franja de acento del correo"
                    },
                    "tieneLogo": {
                      "type": "boolean"
                    },
                    "logoUrl": {
                      "type": "string",
                      "nullable": true,
                      "description": "URL prefirmada del logo para previsualizar (caduca); los correos usan la URL publica estable"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Color invalido, nombre de mas de 80 caracteres o dto vacio"
          }
        }
      }
    },
    "/partner/branding/logo": {
      "put": {
        "tags": [
          "partner"
        ],
        "summary": "Sube el logo de la marca (PNG/JPG, max 1 MB)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "logoBase64"
                ],
                "properties": {
                  "logoBase64": {
                    "type": "string",
                    "description": "Imagen en base64 SIN prefijo data-URI"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Branding con el logo nuevo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "marcaNombre": {
                      "type": "string",
                      "nullable": true,
                      "example": "Facteo Pro"
                    },
                    "marcaColor": {
                      "type": "string",
                      "nullable": true,
                      "example": "#7C3AED",
                      "description": "Hex \"#RRGGBB\" de la franja de acento del correo"
                    },
                    "tieneLogo": {
                      "type": "boolean"
                    },
                    "logoUrl": {
                      "type": "string",
                      "nullable": true,
                      "description": "URL prefirmada del logo para previsualizar (caduca); los correos usan la URL publica estable"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No es PNG/JPG, esta vacio o supera 1 MB"
          }
        }
      },
      "delete": {
        "tags": [
          "partner"
        ],
        "summary": "Quita el logo de la marca",
        "responses": {
          "204": {
            "description": "Logo eliminado"
          }
        }
      }
    },
    "/partner/branding/vista-previa": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Correo de ejemplo con el branding actual (HTML)",
        "description": "HTML del correo del comprobante sobre datos inventados, para previsualizar en el portal sin emitir nada.",
        "responses": {
          "200": {
            "description": "HTML del correo",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/logos/partner/{partnerId}": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Logo publico del partner (lo referencian los correos)",
        "description": "PUBLICO (sin sesion): el <img> de un correo no puede llevar Authorization y una URL prefirmada caduca. Cache de 1 hora.",
        "security": [],
        "parameters": [
          {
            "name": "partnerId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Imagen PNG o JPG",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "El partner no existe o no tiene logo"
          }
        }
      }
    },
    "/partner/tenants": {
      "post": {
        "tags": [
          "partner"
        ],
        "summary": "Aprovisionar un tenant de cliente (partner key, sin X-Cuenta)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "nombre"
                ],
                "properties": {
                  "nombre": {
                    "type": "string"
                  },
                  "ruc": {
                    "type": "string",
                    "description": "Si valida, precarga el emisor desde el catastro del SRI."
                  },
                  "plan": {
                    "type": "string",
                    "enum": [
                      "free",
                      "emprendedor",
                      "pyme",
                      "empresa",
                      "despacho"
                    ]
                  },
                  "ambiente": {
                    "type": "integer",
                    "enum": [
                      1,
                      2
                    ],
                    "description": "1=Pruebas (default), 2=Producción."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Tenant aprovisionado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tenantId": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string"
                    },
                    "precargaSri": {
                      "type": "object",
                      "nullable": true
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      },
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Listar los tenants que gestiona el partner",
        "responses": {
          "200": {
            "description": "Tenants gestionados",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "tenantId": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "nombre": {
                        "type": "string"
                      },
                      "slug": {
                        "type": "string"
                      },
                      "plan": {
                        "type": "string"
                      },
                      "ambienteActivo": {
                        "type": "integer",
                        "enum": [
                          1,
                          2
                        ],
                        "description": "1=Pruebas, 2=Producción"
                      },
                      "estado": {
                        "type": "string"
                      },
                      "origen": {
                        "type": "string",
                        "enum": [
                          "referral",
                          "provision"
                        ]
                      },
                      "createdAt": {
                        "type": "string",
                        "format": "date-time"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      }
    },
    "/partner/tenants/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Detalle de un tenant gestionado (emisores y firma)",
        "responses": {
          "200": {
            "description": "Detalle del tenant",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tenantId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "nombre": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string"
                    },
                    "plan": {
                      "type": "string"
                    },
                    "ambienteActivo": {
                      "type": "integer",
                      "enum": [
                        1,
                        2
                      ]
                    },
                    "estado": {
                      "type": "string"
                    },
                    "origen": {
                      "type": "string"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "emisores": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "ruc": {
                            "type": "string"
                          },
                          "razonSocial": {
                            "type": "string"
                          },
                          "tieneCertificadoActivo": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      },
      "patch": {
        "tags": [
          "partner"
        ],
        "summary": "Gestionar un tenant: plan, ambiente o estado",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plan": {
                    "type": "string"
                  },
                  "ambiente": {
                    "type": "integer",
                    "enum": [
                      1,
                      2
                    ]
                  },
                  "estado": {
                    "type": "string",
                    "enum": [
                      "activo",
                      "suspendido"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tenant actualizado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tenantId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "nombre": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string"
                    },
                    "plan": {
                      "type": "string"
                    },
                    "ambienteActivo": {
                      "type": "integer",
                      "enum": [
                        1,
                        2
                      ]
                    },
                    "estado": {
                      "type": "string"
                    },
                    "origen": {
                      "type": "string"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      }
    },
    "/partner/facturacion": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Cortes de facturación mayorista del partner",
        "responses": {
          "200": {
            "description": "Cortes por período",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "periodo": {
                        "type": "string",
                        "example": "2026-07"
                      },
                      "tenantsGestionados": {
                        "type": "integer"
                      },
                      "comprobantesAutorizados": {
                        "type": "integer"
                      },
                      "monto": {
                        "type": "string",
                        "nullable": true,
                        "description": "Total USD (IVA incluido); autocalculado por la tarifa, null si no hay tarifa"
                      },
                      "montoBaseCentavos": {
                        "type": "integer",
                        "nullable": true
                      },
                      "ivaCentavos": {
                        "type": "integer",
                        "nullable": true
                      },
                      "totalCentavos": {
                        "type": "integer",
                        "nullable": true
                      },
                      "detalleCalculo": {
                        "type": "object",
                        "nullable": true,
                        "description": "Snapshot auditable del cálculo: tarifa, desglose por tramo, mínimo aplicado y ajustes de períodos anteriores"
                      },
                      "estado": {
                        "type": "string",
                        "enum": [
                          "abierto",
                          "cerrado",
                          "facturando",
                          "facturado",
                          "pagado",
                          "error_factura"
                        ]
                      },
                      "claveAcceso": {
                        "type": "string",
                        "nullable": true,
                        "description": "Clave de acceso de la factura SRI emitida por Contadeo"
                      },
                      "facturadoAt": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "pagadoAt": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "createdAt": {
                        "type": "string",
                        "format": "date-time"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      }
    },
    "/partner/consumo/detalle": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Consumo del período por día y por tenant, autorizados y rechazados por separado",
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador.\n\nMismo universo que el corte (producción, por fecha de emisión) pero con los rechazados a la vista y los anulados contados aparte, para el tablero del portal. Sin datos de compradores.",
        "parameters": [
          {
            "name": "periodo",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "2026-07"
            },
            "description": "YYYY-MM; default el mes corriente en Guayaquil"
          }
        ],
        "responses": {
          "200": {
            "description": "Consumo detallado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "periodo": { "type": "string" },
                    "totales": { "type": "object", "properties": { "autorizados": { "type": "integer" }, "rechazados": { "type": "integer" }, "anulados": { "type": "integer" } } },
                    "porDia": { "type": "array", "items": { "type": "object", "properties": { "dia": { "type": "string", "format": "date" }, "autorizados": { "type": "integer" }, "rechazados": { "type": "integer" } } } },
                    "porTenant": { "type": "array", "items": { "type": "object", "properties": { "tenantId": { "type": "string", "format": "uuid" }, "slug": { "type": "string" }, "nombre": { "type": "string" }, "autorizados": { "type": "integer" }, "rechazados": { "type": "integer" } } } }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/partner/consumo/comprobantes": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Drill-through del consumo: comprobantes de un día, un tenant o un estado, sin datos del comprador",
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador.\n\nCada fila lleva lo mismo que el webhook (tenant, tipo, número, fechas, estado, clave de acceso) y nunca datos del comprador (anexo de datos §4). Paginado con `total`; se leen por tenant bajo RLS y se unen en memoria con un tope por tenant.",
        "parameters": [
          {
            "name": "periodo",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "2026-07"
            },
            "description": "YYYY-MM; default el mes corriente en Guayaquil"
          },
          { "name": "dia", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Un solo día (YYYY-MM-DD)." },
          { "name": "tenantId", "in": "query", "schema": { "type": "string", "format": "uuid" }, "description": "Solo ese tenant (debe ser de la cartera; si no, 404)." },
          { "name": "estado", "in": "query", "schema": { "type": "string", "enum": ["AUTORIZADO", "RECHAZADO", "ANULADO"] } },
          { "name": "limit", "in": "query", "schema": { "type": "integer" }, "description": "1 a 200 (50 por defecto)." },
          { "name": "offset", "in": "query", "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": {
            "description": "Página de comprobantes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comprobantes": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "tenantId": { "type": "string", "format": "uuid" }, "tenantNombre": { "type": "string" }, "tipoComprobante": { "type": "string" }, "establecimiento": { "type": "string" }, "puntoEmision": { "type": "string" }, "secuencial": { "type": "integer" }, "fechaEmision": { "type": "string", "format": "date" }, "fechaAutorizacion": { "type": "string", "format": "date-time", "nullable": true }, "estado": { "type": "string" }, "claveAcceso": { "type": "string" }, "numeroAutorizacion": { "type": "string", "nullable": true } } } },
                    "total": { "type": "integer" },
                    "desde": { "type": "integer" },
                    "limite": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/partner/facturacion/{periodo}": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Un corte de facturación con su desglose por tenant",
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador.\n\nLee el snapshot de cálculo del corte (unidades por tenant, tramos aplicados, mínimo) y lo devuelve con los nombres de los tenants de la cartera.",
        "parameters": [
          { "name": "periodo", "in": "path", "required": true, "schema": { "type": "string", "example": "2026-07" } }
        ],
        "responses": {
          "200": {
            "description": "Detalle del corte",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "periodo": { "type": "string" },
                    "estado": { "type": "string" },
                    "tenantsGestionados": { "type": "integer" },
                    "comprobantesAutorizados": { "type": "integer" },
                    "montoBaseCentavos": { "type": "integer", "nullable": true },
                    "ivaCentavos": { "type": "integer", "nullable": true },
                    "totalCentavos": { "type": "integer", "nullable": true },
                    "claveAcceso": { "type": "string", "nullable": true },
                    "facturadoAt": { "type": "string", "format": "date-time", "nullable": true },
                    "pagadoAt": { "type": "string", "format": "date-time", "nullable": true },
                    "unidades": { "type": "integer" },
                    "minimoAplicado": { "type": "boolean" },
                    "desglose": { "type": "array", "nullable": true, "items": { "type": "object" } },
                    "porTenant": { "type": "array", "items": { "type": "object", "properties": { "tenantId": { "type": "string", "format": "uuid" }, "nombre": { "type": "string" }, "slug": { "type": "string", "nullable": true }, "comprobantes": { "type": "integer" } } } }
                  }
                }
              }
            }
          },
          "404": { "description": "No hay corte para ese período" }
        }
      }
    },
    "/partner/consumo": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Consumo en vivo del período con desglose por tenant",
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador.\n\nComprobantes AUTORIZADOS de producción del período (default: mes corriente, zona America/Guayaquil), por fecha de emisión — mismo criterio que el corte y que reporte_ventas/F104, para reconciliar. Incluye el desglose por tenant (útil para re-facturar a cada cliente) y, si hay tarifa, la proyección de la base del corte.",
        "parameters": [
          {
            "name": "periodo",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "2026-07"
            },
            "description": "YYYY-MM; default el mes corriente en Guayaquil"
          }
        ],
        "responses": {
          "200": {
            "description": "Consumo del período",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "periodo": {
                      "type": "string",
                      "example": "2026-07"
                    },
                    "tenantsGestionados": {
                      "type": "integer"
                    },
                    "comprobantesAutorizados": {
                      "type": "integer"
                    },
                    "porTenant": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "tenantId": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "nombre": {
                            "type": "string"
                          },
                          "comprobantes": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "proyeccion": {
                      "type": "object",
                      "nullable": true,
                      "description": "null si no hay tarifa vigente o si el modo de cobro es prepago (sin corte pospago)",
                      "properties": {
                        "baseCentavosEstimado": {
                          "type": "integer",
                          "description": "Base estimada del corte (centavos USD, sin IVA) con la tarifa vigente"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "periodo con formato inválido"
          }
        }
      }
    },
    "/partner/tarifa": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Tarifa mayorista vigente del partner",
        "responses": {
          "200": {
            "description": "Tarifa vigente (null si el operador aún no la fijó)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "nullable": true,
                  "properties": {
                    "modelo": {
                      "type": "string",
                      "enum": [
                        "por_comprobante",
                        "por_tenant",
                        "fija",
                        "tramos"
                      ]
                    },
                    "precioUnitario": {
                      "type": "string",
                      "example": "0.0500",
                      "description": "USD; con modelo tramos, el del primer tramo (informativo)"
                    },
                    "moneda": {
                      "type": "string",
                      "example": "USD"
                    },
                    "modoCobro": {
                      "type": "string",
                      "enum": [
                        "pospago",
                        "prepago"
                      ]
                    },
                    "minimoMensual": {
                      "type": "string",
                      "example": "50.00",
                      "description": "Piso mensual del corte pospago (USD); 0 = sin mínimo"
                    },
                    "anulacionReembolsa": {
                      "type": "boolean"
                    },
                    "tramos": {
                      "type": "array",
                      "description": "Tramos MARGINALES: cada tramo cobra solo las unidades que caen dentro de él",
                      "items": {
                        "type": "object",
                        "properties": {
                          "desde": {
                            "type": "integer",
                            "example": 1
                          },
                          "precioUnitario": {
                            "type": "string",
                            "example": "0.0500"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      }
    },
    "/partner/webhooks": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Ver el webhook configurado (sin el secreto)",
        "responses": {
          "200": {
            "description": "Webhook o null",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "nullable": true,
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "eventos": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "activo": {
                      "type": "boolean"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updatedAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      },
      "put": {
        "tags": [
          "partner"
        ],
        "summary": "Crear o actualizar el webhook (el secreto se devuelve solo al crear)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "https; no se permiten hosts internos/privados. La URL se revalida en cada entrega y no se siguen redirecciones."
                  },
                  "eventos": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "comprobante.autorizado",
                        "comprobante.rechazado",
                        "comprobante.devuelto"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook guardado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      },
      "delete": {
        "tags": [
          "partner"
        ],
        "summary": "Eliminar el webhook",
        "responses": {
          "204": {
            "description": "Eliminado"
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      }
    },
    "/partner/webhooks/rotar-secreto": {
      "post": {
        "tags": [
          "partner"
        ],
        "summary": "Rotar el secreto de firma (se devuelve una sola vez)",
        "responses": {
          "200": {
            "description": "Nuevo secreto",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "secret": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      }
    },
    "/partner/webhooks/resumen-diario": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Entregas del webhook por día (salud de la integración)",
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador.\n\nConteo por día de Guayaquil: entregadas, fallidas (fallido y descartado) y pendientes. Nunca expone el payload.",
        "parameters": [
          { "name": "dias", "in": "query", "schema": { "type": "integer", "default": 14 }, "description": "1 a 90." }
        ],
        "responses": {
          "200": {
            "description": "Entregas por día",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "type": "object", "properties": { "dia": { "type": "string", "format": "date" }, "entregadas": { "type": "integer" }, "fallidas": { "type": "integer" }, "pendientes": { "type": "integer" } } } }
              }
            }
          }
        }
      }
    },
    "/partner/webhooks/deliveries": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Entregas recientes de webhook",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Entregas",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "tenantId": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "comprobanteId": {
                        "type": "string",
                        "format": "uuid",
                        "nullable": true
                      },
                      "evento": {
                        "type": "string"
                      },
                      "estado": {
                        "type": "string",
                        "enum": [
                          "pendiente",
                          "entregado",
                          "fallido",
                          "descartado"
                        ]
                      },
                      "intentos": {
                        "type": "integer"
                      },
                      "statusCode": {
                        "type": "integer",
                        "nullable": true
                      },
                      "ultimoError": {
                        "type": "string",
                        "nullable": true
                      },
                      "createdAt": {
                        "type": "string",
                        "format": "date-time"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      }
    },
    "/partner/webhooks/deliveries/{id}/replay": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "partner"
        ],
        "summary": "Reintentar una entrega de webhook",
        "responses": {
          "202": {
            "description": "Reencolada"
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      }
    },
    "/reportes/resumen": {
      "get": {
        "tags": [
          "operacion"
        ],
        "summary": "Resumen del módulo Reportes: KPI, series y rankings del rango",
        "description": "Todas las cifras salen del MISMO criterio que `/reportes/comprobantes`: tenant, emisor, rango, ambiente, sin comprobantes bloqueados (LOPDP) y solo tipos de venta (01, 04, 05); las retenciones (07) van aparte en `retencionesPorMes`. `subtotal` es la base imponible sin IVA con la nota de crédito en negativo; `total` suma el IVA neto. `topProductos` solo cubre facturas con líneas materializadas (emitidas desde la migración 0078).",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "desde",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "YYYY-MM-DD inclusive. Por defecto, el mes en curso."
          },
          {
            "name": "hasta",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "ambiente",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "enum": [
                1,
                2
              ]
            },
            "description": "1=Pruebas, 2=Producción. Si se omite se aplica el ambiente ACTIVO de la cuenta."
          }
        ],
        "responses": {
          "200": {
            "description": "Resumen",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResumenReportes"
                }
              }
            }
          }
        }
      }
    },
    "/reportes/comprobantes": {
      "get": {
        "tags": [
          "operacion"
        ],
        "summary": "Drill-through: los comprobantes que forman un segmento del resumen",
        "description": "Mismo WHERE base que `/reportes/resumen` más el criterio del segmento, paginado y con `total`. Devuelve identificación y razón social del comprador, por lo que la lectura queda auditada como el listado de `/comprobantes`.",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "desde",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "YYYY-MM-DD inclusive. Por defecto, el mes en curso."
          },
          {
            "name": "hasta",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "ambiente",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "enum": [
                1,
                2
              ]
            },
            "description": "1=Pruebas, 2=Producción. Si se omite se aplica el ambiente ACTIVO de la cuenta."
          },
          {
            "name": "tipo",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "codDoc de dos dígitos (01, 04, 05...). Sin tipo: los tres de venta."
          },
          {
            "name": "estado",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Estado del comprobante (AUTORIZADO, RECHAZADO, ANULADO...). Sin estado: todos."
          },
          {
            "name": "tarifa",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "b15",
                "b0",
                "exento",
                "noObjeto",
                "otras"
              ]
            },
            "description": "Comprobantes con base distinta de cero en esa tarifa."
          },
          {
            "name": "establecimiento",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Tres dígitos (001)."
          },
          {
            "name": "puntoEmision",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Tres dígitos (002)."
          },
          {
            "name": "cliente",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Identificación exacta del comprador (RUC, cédula o 9999999999999)."
          },
          {
            "name": "producto",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "codigoPrincipal de una línea; exige líneas materializadas."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Filas por página, 1 a 200 (50 por defecto)."
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Desplazamiento (0 por defecto)."
          }
        ],
        "responses": {
          "200": {
            "description": "Página de comprobantes con el total del segmento",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComprobantesReporte"
                }
              }
            }
          }
        }
      }
    },
    "/reportes/ventas": {
      "get": {
        "tags": [
          "operacion"
        ],
        "summary": "Agregados del período (default: mes en curso)",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "desde",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "hasta",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "ambiente",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "enum": [
                1,
                2
              ]
            },
            "description": "1=Pruebas, 2=Producción. Si se omite se aplica el ambiente ACTIVO de la cuenta: la respuesta nunca mezcla ensayos con comprobantes reales."
          }
        ],
        "responses": {
          "200": {
            "description": "Reporte",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReporteVentas"
                }
              }
            }
          }
        }
      }
    },
    "/admin/colas": {
      "get": {
        "tags": [
          "operacion"
        ],
        "summary": "Conteos por estado de las colas (owner/admin)",
        "responses": {
          "200": {
            "description": "Colas",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/admin/colas/{nombre}/reintentar": {
      "post": {
        "tags": [
          "operacion"
        ],
        "summary": "Reencolar los jobs fallidos de una cola",
        "parameters": [
          {
            "name": "nombre",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Reintentados",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reintentados": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/health/live": {
      "get": {
        "tags": [
          "operacion"
        ],
        "summary": "Proceso vivo",
        "security": [],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/health/ready": {
      "get": {
        "tags": [
          "operacion"
        ],
        "summary": "Base de datos accesible",
        "security": [],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/health/sri": {
      "get": {
        "tags": [
          "operacion"
        ],
        "summary": "Alcanzabilidad del WS del SRI",
        "security": [],
        "responses": {
          "200": {
            "description": "ok"
          }
        }
      }
    },
    "/auth/mi-perfil": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "Quién soy: nombre y correo de la sesión",
        "description": "Para el bloque de cuenta del panel. Las API keys no tienen usuario (403).",
        "responses": {
          "200": {
            "description": "Usuario de la sesión",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "userId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "nombre": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/auth/mis-cuentas": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "Cuentas (tenants) del usuario autenticado, con la activa marcada",
        "description": "Para el selector multiempresa. Las API keys no tienen cuentas de usuario (403).",
        "responses": {
          "200": {
            "description": "Membresías del usuario",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "tenantId": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "ruc": {
                        "type": "string",
                        "nullable": true,
                        "description": "RUC del emisor de la empresa; null si aún no lo configuró"
                      },
                      "razonSocial": {
                        "type": "string",
                        "nullable": true,
                        "description": "Razón social del emisor (el selector busca por ella); null si aún no lo configuró"
                      },
                      "nombre": {
                        "type": "string"
                      },
                      "plan": {
                        "type": "string"
                      },
                      "roles": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "ambienteActivo": {
                        "type": "integer",
                        "description": "Ambiente activo del tenant: 1=Pruebas, 2=Producción"
                      },
                      "activo": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/auth/cambiar-cuenta": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Cambiar de cuenta sin re-login: emite tokens para otro tenant del usuario",
        "description": "Valida la membership; 401 si el usuario no pertenece al tenant. API keys: 403.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tenantId"
                ],
                "properties": {
                  "tenantId": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Par de tokens para el tenant nuevo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accessToken": {
                      "type": "string"
                    },
                    "refreshToken": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/pagos/config": {
      "get": {
        "tags": [
          "pagos"
        ],
        "summary": "Configuración pública del pago en línea (habilitado y precios con IVA)",
        "responses": {
          "200": {
            "description": "Estado del módulo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "habilitado": {
                      "type": "boolean"
                    },
                    "ivaPorcentaje": {
                      "type": "number",
                      "example": 15
                    },
                    "planes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "nombre": {
                            "type": "string",
                            "example": "pyme"
                          },
                          "precioAnualUsd": {
                            "type": "number",
                            "example": 89
                          },
                          "totalCentavos": {
                            "type": "integer",
                            "example": 10235
                          },
                          "tiposPermitidos": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "example": [
                              "01",
                              "04",
                              "05"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          }
        }
      }
    },
    "/pagos/suscripcion/checkout": {
      "post": {
        "tags": [
          "pagos"
        ],
        "summary": "Iniciar una suscripción recurrente anual con dLocal Go (roles owner/admin)",
        "description": "Asegura el Subscription Plan de dLocal para el plan del catálogo (monto = precio anual + IVA 15%, cadencia anual), registra una suscripción pendiente y devuelve el subscribe_url con el external_id. El navegador se redirige a esa URL; la activación del plan ocurre en el webhook del primer cobro, no en el redirect. 503 si dLocal no está configurado.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "plan"
                ],
                "properties": {
                  "plan": {
                    "type": "string",
                    "enum": [
                      "emprendedor",
                      "pyme",
                      "empresa"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Suscripción pendiente creada; redirigir el navegador a subscribeUrl",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "suscripcionId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "subscribeUrl": {
                      "type": "string",
                      "format": "uri",
                      "description": "URL de dLocal a la que redirigir para completar la suscripción"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Plan no comprable en línea",
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "503": {
            "description": "Suscripción en línea no configurada; usar activación manual"
          }
        }
      }
    },
    "/pagos/suscripcion": {
      "get": {
        "tags": [
          "pagos"
        ],
        "summary": "Suscripción recurrente viva del tenant (roles owner/admin)",
        "description": "Devuelve la suscripción en estado pendiente/activa/morosa del tenant, o null si no hay ninguna (plan Free, o plan activado a mano por el operador sin pasarela). El dashboard la usa para ofrecer la baja solo a quien tiene un cobro recurrente.",
        "responses": {
          "200": {
            "description": "Suscripción viva, o null si no hay",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "nullable": true,
                  "properties": {
                    "plan": {
                      "type": "string",
                      "example": "pyme"
                    },
                    "estado": {
                      "type": "string",
                      "enum": [
                        "pendiente",
                        "activa",
                        "morosa"
                      ]
                    },
                    "montoCentavos": {
                      "type": "integer",
                      "description": "Total con IVA que dLocal cobra cada periodo"
                    },
                    "proximoCobroAt": {
                      "type": "string",
                      "format": "date",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/pagos/suscripcion/cancelar": {
      "post": {
        "tags": [
          "pagos"
        ],
        "summary": "Cancelar la suscripción recurrente del tenant (roles owner/admin)",
        "description": "Cancela en dLocal la suscripción viva del tenant (deja de cobrar). El plan sigue activo hasta su fecha de expiración (lo ya pagado no se pierde).",
        "responses": {
          "200": {
            "description": "Suscripción cancelada",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "estado": {
                      "type": "string",
                      "example": "cancelada"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "description": "No hay una suscripción activa que cancelar"
          }
        }
      }
    },
    "/pagos/dlocal/webhook": {
      "post": {
        "tags": [
          "pagos"
        ],
        "summary": "Webhook de dLocal Go — notificación de cobros de suscripción (público, firmado)",
        "description": "Endpoint público que recibe las notificaciones de dLocal Go. Se autentica por firma HMAC (header Authorization: V2-HMAC-SHA256) sobre el cuerpo crudo; sin sesión. Relee el cobro por la API de dLocal y activa/renueva el plan del tenant (mapeado por external_id). Responde 200 salvo firma inválida (401).",
        "responses": {
          "200": {
            "description": "Webhook recibido y procesado (o registrado para inspección)"
          },
          "401": {
            "description": "Firma HMAC inválida"
          }
        }
      }
    },
    "/pagos/historial": {
      "get": {
        "tags": [
          "pagos"
        ],
        "summary": "Historial de pagos del tenant (roles owner/admin)",
        "responses": {
          "200": {
            "description": "Últimas 50 transacciones",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "plan": {
                        "type": "string"
                      },
                      "montoCentavos": {
                        "type": "integer"
                      },
                      "estado": {
                        "type": "string",
                        "enum": [
                          "pendiente",
                          "aprobado",
                          "rechazado",
                          "expirado"
                        ]
                      },
                      "createdAt": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "confirmadoAt": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "planExpiraAtAplicado": {
                        "type": "string",
                        "format": "date",
                        "nullable": true
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/oauth/register": {
      "post": {
        "tags": [
          "oauth"
        ],
        "summary": "Registro dinámico de cliente (RFC 7591)",
        "description": "Público. Solo clientes PÚBLICOS con PKCE: `token_endpoint_auth_method` debe ser `none` (no se emite client_secret). Lo invocan automáticamente los clientes MCP (claude.ai, Claude Code) al conectar.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "redirect_uris"
                ],
                "properties": {
                  "client_name": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "redirect_uris": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "format": "uri"
                    },
                    "description": "https, esquema de app nativa, o http solo hacia localhost/127.0.0.1"
                  },
                  "token_endpoint_auth_method": {
                    "type": "string",
                    "enum": [
                      "none"
                    ]
                  },
                  "client_uri": {
                    "type": "string"
                  },
                  "logo_uri": {
                    "type": "string"
                  },
                  "software_id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cliente registrado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "client_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "client_id_issued_at": {
                      "type": "integer",
                      "description": "Epoch (segundos)"
                    },
                    "client_name": {
                      "type": "string"
                    },
                    "redirect_uris": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "token_endpoint_auth_method": {
                      "type": "string",
                      "enum": [
                        "none"
                      ]
                    },
                    "grant_types": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "response_types": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Metadata inválida",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Código de error OAuth (RFC 6749): invalid_request, invalid_grant, unsupported_grant_type, invalid_redirect_uri, invalid_client_metadata"
                    },
                    "error_description": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/oauth/token": {
      "post": {
        "tags": [
          "oauth"
        ],
        "summary": "Token endpoint (authorization_code + PKCE, refresh_token)",
        "description": "Público. Canjea un authorization code (un solo uso, expira a los 10 min) verificando PKCE S256, o rota tokens con un refresh token OAuth. Acepta `application/x-www-form-urlencoded` (estándar) y JSON. Los access tokens caducan según `expires_in`; el cliente renueva con el refresh_token.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "grant_type"
                ],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "enum": [
                      "authorization_code",
                      "refresh_token"
                    ]
                  },
                  "code": {
                    "type": "string"
                  },
                  "redirect_uri": {
                    "type": "string"
                  },
                  "client_id": {
                    "type": "string"
                  },
                  "code_verifier": {
                    "type": "string"
                  },
                  "refresh_token": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tokens emitidos",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string",
                      "description": "JWT con los scopes consentidos (roles emisor/contador/lector, nunca owner/admin)"
                    },
                    "token_type": {
                      "type": "string",
                      "enum": [
                        "Bearer"
                      ]
                    },
                    "expires_in": {
                      "type": "integer"
                    },
                    "refresh_token": {
                      "type": "string"
                    },
                    "scope": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Grant inválido (code expirado/canjeado, PKCE fallido...)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Código de error OAuth (RFC 6749): invalid_request, invalid_grant, unsupported_grant_type, invalid_redirect_uri, invalid_client_metadata"
                    },
                    "error_description": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/oauth/consentimiento": {
      "get": {
        "tags": [
          "oauth"
        ],
        "summary": "Validar solicitud de autorización (pantalla de consentimiento)",
        "description": "Público (no revela más que el nombre del cliente). Lo usa la SPA /oauth/autorizar para validar los parámetros OAuth ANTES de mostrar el login/consentimiento. Errores de client_id/redirect_uri se muestran en pantalla, nunca se redirigen (RFC 6749 §4.1.2.1).",
        "security": [],
        "parameters": [
          {
            "name": "client_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "redirect_uri",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "response_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "code"
              ]
            }
          },
          {
            "name": "code_challenge",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "code_challenge_method",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "S256"
              ]
            }
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Scopes separados por espacio: emisor contador lector (default: emisor)"
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "resource",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "RFC 8707: URL del recurso MCP"
          }
        ],
        "responses": {
          "200": {
            "description": "Solicitud válida",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cliente": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "nombre": {
                          "type": "string"
                        }
                      }
                    },
                    "scopes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "scope": {
                            "type": "string"
                          },
                          "descripcion": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "avisoIa": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/AvisoIa"
                        }
                      ],
                      "description": "Se muestra en la pantalla de consentimiento ANTES de autorizar: aprobar el conector remoto abre el mismo canal de IA que una API key."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          }
        }
      },
      "post": {
        "tags": [
          "oauth"
        ],
        "summary": "Aprobar la autorización (emite el authorization code)",
        "description": "Requiere la sesión del usuario que autoriza (JWT del dashboard; las API keys no pueden consentir). Liga el code al usuario y a su cuenta activa y devuelve la URL de redirect (`code` + `state`) hacia el cliente OAuth. Aprobar abre el canal de IA: si es la primera vez que la empresa lo abre -por aquí o dando de alta una API key con rol `emisor`, `contador` o `lector`- sale un correo al owner con el aviso del art. 5.1.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "client_id",
                  "redirect_uri",
                  "response_type",
                  "code_challenge",
                  "code_challenge_method"
                ],
                "properties": {
                  "client_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "redirect_uri": {
                    "type": "string"
                  },
                  "response_type": {
                    "type": "string",
                    "enum": [
                      "code"
                    ]
                  },
                  "code_challenge": {
                    "type": "string"
                  },
                  "code_challenge_method": {
                    "type": "string",
                    "enum": [
                      "S256"
                    ]
                  },
                  "scope": {
                    "type": "string",
                    "description": "Scopes separados por espacio: emisor contador lector (default: emisor)"
                  },
                  "state": {
                    "type": "string"
                  },
                  "resource": {
                    "type": "string",
                    "description": "RFC 8707: URL del recurso MCP"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Code emitido",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "redirect": {
                      "type": "string",
                      "description": "URL del cliente con ?code=...&state=... — la SPA manda el navegador ahí"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          }
        }
      }
    },
    "/mcp/auditoria": {
      "post": {
        "tags": [
          "mcp"
        ],
        "summary": "Sink de auditoría del servidor MCP (loopback, autenticado)",
        "description": "El wrapper conAuditoria del paquete contadeo-mcp postea aquí por loopback con el mismo Bearer del usuario para registrar la traza de cada llamada a una tool. Sin RolesGuard: cualquier usuario autenticado registra las suyas. Best-effort; responde 204.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "RegistroAuditoriaDto (tool, resultado, args redactados, duración)"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Traza registrada (sin cuerpo)"
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "tags": [
          "mcp"
        ],
        "summary": "Endpoint MCP (JSON-RPC sobre HTTP streamable)",
        "description": "Servidor MCP remoto de Contadeo (las 15 tools del paquete contadeo-mcp). Bearer: access token OAuth (flujo de consentimiento) o API key cdo_. Un 401 incluye `WWW-Authenticate` con `resource_metadata` (RFC 9728) para que el cliente inicie OAuth. No es un endpoint REST: habla JSON-RPC 2.0 según la spec MCP.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Mensaje JSON-RPC 2.0 (initialize, tools/list, tools/call...)"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta JSON-RPC",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Sin credencial: lleva WWW-Authenticate con la URL de la metadata RFC 9728",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "mcp"
        ],
        "summary": "No soportado (405)",
        "description": "Sin sesiones server-side: no hay stream SSE que reabrir.",
        "responses": {
          "405": {
            "description": "Servidor stateless: solo POST",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "mcp"
        ],
        "summary": "No soportado (405)",
        "description": "Sin sesiones server-side: no hay sesión que terminar.",
        "responses": {
          "405": {
            "description": "Servidor stateless: solo POST",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/oauth-authorization-server": {
      "get": {
        "tags": [
          "oauth"
        ],
        "summary": "Metadata del authorization server (RFC 8414)",
        "description": "NOTA: esta ruta se sirve en la RAÍZ del dominio (https://contadeo.com/.well-known/...), no bajo el server /api — los clientes OAuth la derivan del issuer/resource según el RFC.",
        "security": [],
        "responses": {
          "200": {
            "description": "Metadata",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "issuer": {
                      "type": "string"
                    },
                    "authorization_endpoint": {
                      "type": "string"
                    },
                    "token_endpoint": {
                      "type": "string"
                    },
                    "registration_endpoint": {
                      "type": "string"
                    },
                    "code_challenge_methods_supported": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "scopes_supported": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/oauth-protected-resource": {
      "get": {
        "tags": [
          "oauth"
        ],
        "summary": "Metadata del protected resource (RFC 9728)",
        "description": "NOTA: esta ruta se sirve en la RAÍZ del dominio (https://contadeo.com/.well-known/...), no bajo el server /api — los clientes OAuth la derivan del issuer/resource según el RFC.",
        "security": [],
        "responses": {
          "200": {
            "description": "Metadata",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "resource": {
                      "type": "string"
                    },
                    "authorization_servers": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "scopes_supported": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/oauth-protected-resource/api/mcp": {
      "get": {
        "tags": [
          "oauth"
        ],
        "summary": "Metadata del protected resource del MCP (RFC 9728, path-insertion)",
        "description": "NOTA: esta ruta se sirve en la RAÍZ del dominio (https://contadeo.com/.well-known/...), no bajo el server /api — los clientes OAuth la derivan del issuer/resource según el RFC.",
        "security": [],
        "responses": {
          "200": {
            "description": "Metadata",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "resource": {
                      "type": "string"
                    },
                    "authorization_servers": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "scopes_supported": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/asesor/calendario": {
      "get": {
        "tags": [
          "asesor"
        ],
        "summary": "Próximos vencimientos tributarios del emisor (9.º dígito del RUC)",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Opcional si el tenant tiene un solo emisor; con varios es obligatorio (400)."
          },
          {
            "name": "horizonteDias",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 90
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calendario",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "emisorId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "ruc": {
                      "type": "string"
                    },
                    "novenoDigito": {
                      "type": "string"
                    },
                    "regimen": {
                      "type": "string",
                      "enum": [
                        "GENERAL",
                        "RIMPE_EMPRENDEDOR",
                        "RIMPE_NEGOCIO_POPULAR"
                      ]
                    },
                    "generadoEn": {
                      "type": "string",
                      "format": "date"
                    },
                    "horizonteDias": {
                      "type": "integer"
                    },
                    "vencimientos": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "codigo": {
                            "type": "string"
                          },
                          "nombre": {
                            "type": "string"
                          },
                          "formulario": {
                            "type": "string"
                          },
                          "periodo": {
                            "type": "string",
                            "description": "\"2026-05\" (mensual), \"2026-S1\" (semestral) o \"2025\" (anual)"
                          },
                          "fechaLimite": {
                            "type": "string",
                            "format": "date"
                          },
                          "diasRestantes": {
                            "type": "integer"
                          },
                          "notas": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/asesor/semaforo-rimpe": {
      "get": {
        "tags": [
          "asesor"
        ],
        "summary": "Proyección de ingresos anuales frente a los límites del RIMPE",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Opcional si el tenant tiene un solo emisor; con varios es obligatorio (400)."
          }
        ],
        "responses": {
          "200": {
            "description": "Semáforo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "emisorId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "regimen": {
                      "type": "string"
                    },
                    "anioFiscal": {
                      "type": "integer"
                    },
                    "desde": {
                      "type": "string",
                      "format": "date"
                    },
                    "hasta": {
                      "type": "string",
                      "format": "date"
                    },
                    "ingresosAcumulados": {
                      "type": "number"
                    },
                    "fuenteIngresos": {
                      "type": "string",
                      "description": "Solo lo facturado en Contadeo: facturas + ND − NC autorizadas."
                    },
                    "diasTranscurridos": {
                      "type": "integer"
                    },
                    "proyeccionAnual": {
                      "type": "number"
                    },
                    "metodoProyeccion": {
                      "type": "string"
                    },
                    "limites": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "limiteSuperior": {
                          "type": "number"
                        },
                        "vigenteDesde": {
                          "type": "string",
                          "format": "date"
                        }
                      }
                    },
                    "porcentajeProyectado": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "umbralAviso": {
                      "type": "number"
                    },
                    "estado": {
                      "type": "string",
                      "enum": [
                        "VERDE",
                        "AMARILLO",
                        "ROJO",
                        "NO_APLICA"
                      ]
                    },
                    "mensajes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/asesor/obligaciones": {
      "get": {
        "tags": [
          "asesor"
        ],
        "summary": "Checklist de obligaciones tributarias según el perfil del emisor",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Opcional si el tenant tiene un solo emisor; con varios es obligatorio (400)."
          }
        ],
        "responses": {
          "200": {
            "description": "Obligaciones",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "emisorId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "perfil": {
                      "type": "object",
                      "properties": {
                        "regimen": {
                          "type": "string"
                        },
                        "obligadoContabilidad": {
                          "type": "boolean"
                        },
                        "contribuyenteEspecial": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "tipoPersona": {
                          "type": "string",
                          "enum": [
                            "NATURAL",
                            "SOCIEDAD"
                          ]
                        }
                      }
                    },
                    "obligaciones": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "codigo": {
                            "type": "string"
                          },
                          "nombre": {
                            "type": "string"
                          },
                          "formulario": {
                            "type": "string"
                          },
                          "periodicidad": {
                            "type": "string",
                            "enum": [
                              "MENSUAL",
                              "SEMESTRAL",
                              "ANUAL",
                              "PERMANENTE"
                            ]
                          },
                          "mesesPresentacion": {
                            "type": "array",
                            "items": {
                              "type": "integer"
                            }
                          },
                          "descripcion": {
                            "type": "string"
                          },
                          "fundamento": {
                            "type": "string"
                          },
                          "aValidar": {
                            "type": "boolean",
                            "description": "true = dato en revisión normativa: confirmar con un contador."
                          },
                          "notas": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/asesor/f104/estado": {
      "post": {
        "tags": [
          "asesor"
        ],
        "summary": "Ciclo de trabajo del F104: borrador, revisado o listo",
        "description": "Marca el estado de trabajo del F104 del período para la empresa activa. `listo` congela las cifras revisadas (snapshot con fecha y autor) y dispara el webhook `f104.listo` a los endpoints suscritos (solo al ENTRAR al estado; re-marcarlo no re-notifica). A `presentada` se llega únicamente por `POST /asesor/declaraciones` (409 si el período ya está presentado). Requiere sesión de usuario (403 con API key). El `GET /asesor/f104` cachea 5 minutos; este POST devuelve el estado fresco.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "anio",
                  "mes",
                  "estado"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "description": "Opcional: sin él se usa el emisor de trabajo de la cuenta"
                  },
                  "anio": {
                    "type": "integer"
                  },
                  "mes": {
                    "type": "integer"
                  },
                  "estado": {
                    "type": "string",
                    "enum": [
                      "borrador",
                      "revisado",
                      "listo"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Estado marcado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "emisorId",
                    "codigo",
                    "periodo",
                    "estado"
                  ],
                  "properties": {
                    "emisorId": {
                      "type": "string"
                    },
                    "codigo": {
                      "type": "string",
                      "description": "IVA_MENSUAL | IVA_SEMESTRAL_RIMPE"
                    },
                    "periodo": {
                      "type": "string",
                      "description": "Mensual YYYY-MM; semestral YYYY-S1/S2"
                    },
                    "estado": {
                      "type": "string",
                      "enum": [
                        "borrador",
                        "revisado",
                        "listo"
                      ]
                    },
                    "snapshotAt": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "vencimiento": {
                      "type": "string",
                      "format": "date",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Estado inválido, anio/mes ausentes o régimen sin F104"
          },
          "409": {
            "description": "El período ya está presentado; desmárcalo primero"
          }
        }
      }
    },
    "/asesor/cartera": {
      "get": {
        "tags": [
          "asesor"
        ],
        "summary": "Cartera del contador: todas mis cuentas con semáforo y vencimientos",
        "responses": {
          "200": {
            "description": "Cartera",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cuentas": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "tenantId": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "ruc": {
                            "type": "string",
                            "nullable": true,
                            "description": "RUC del emisor de la empresa; null si aún no lo configuró"
                          },
                          "nombre": {
                            "type": "string"
                          },
                          "plan": {
                            "type": "string",
                            "description": "Plan EFECTIVO propio (uno vencido se lee free). En regimen pool el que emite es el de cupoCompartido, no este."
                          },
                          "roles": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "comprobantesDelMes": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "ventasMes": {
                            "type": [
                              "number",
                              "null"
                            ],
                            "description": "Venta neta del mes sin IVA (01 + 05 − 04, autorizados, sin bloqueados). Misma convención que /reportes/resumen."
                          },
                          "serieMeses": {
                            "type": "array",
                            "items": {
                              "type": "number"
                            },
                            "description": "Seis meses de esa venta neta, el corriente al final (sparkline de la cartera)"
                          },
                          "semaforo": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "properties": {
                              "estado": {
                                "type": "string",
                                "enum": [
                                  "VERDE",
                                  "AMARILLO",
                                  "ROJO",
                                  "NO_APLICA"
                                ]
                              }
                            }
                          },
                          "proximosVencimientos": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "codigo": {
                                  "type": "string"
                                },
                                "nombre": {
                                  "type": "string"
                                },
                                "fechaLimite": {
                                  "type": "string",
                                  "format": "date"
                                },
                                "diasRestantes": {
                                  "type": "integer"
                                },
                                "periodo": {
                                  "type": "string",
                                  "description": "Período declarado: '2026-07' | '2026-S1' | '2026'"
                                },
                                "hecha": {
                                  "type": "boolean",
                                  "description": "Anotado como presentado (POST /asesor/declaraciones)"
                                },
                                "marcadaAt": {
                                  "type": [
                                    "string",
                                    "null"
                                  ],
                                  "format": "date-time"
                                }
                              }
                            }
                          },
                          "nota": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "archivada": {
                            "type": "boolean",
                            "description": "Apartada de la cartera de ESTE usuario"
                          },
                          "sinComprobantes": {
                            "type": "boolean",
                            "description": "Nunca emitió: única condición en que se puede borrar la empresa"
                          },
                          "regimenCupo": {
                            "type": "string",
                            "enum": [
                              "raiz",
                              "pool",
                              "independiente",
                              "ajena"
                            ],
                            "description": "De que cupo emite, visto desde MI cartera: raiz = cuenta mia que reparte; pool = la patrocino y sigue en free (gasta mi cupo); independiente = la patrocino pero contrato plan propio; ajena = no participa de mi cupo."
                          }
                        }
                      }
                    },
                    "generadoEn": {
                      "type": "string",
                      "format": "date"
                    },
                    "omitidas": {
                      "type": "integer",
                      "description": "Cuentas que quedan por pedir después de esta página"
                    },
                    "disclaimer": {
                      "type": "string"
                    },
                    "total": {
                      "type": "integer",
                      "description": "Cuentas que encajan en el filtro"
                    },
                    "desde": {
                      "type": "integer"
                    },
                    "limite": {
                      "type": "integer",
                      "description": "Tamaño de página (cuentas evaluadas por llamada)"
                    },
                    "cupoCompartido": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "Cupo que MI cuenta raiz reparte este mes; null si no patrocino a nadie. Solo Produccion: no suma con comprobantesDelMes de las filas. La raiz sale de mis membresias owner, nunca del tenant activo.",
                      "properties": {
                        "raizTenantId": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "raizNombre": {
                          "type": "string"
                        },
                        "plan": {
                          "type": "string",
                          "description": "Plan que rige el pool (free si la raiz esta suspendida o vencida)."
                        },
                        "usados": {
                          "type": "integer"
                        },
                        "limite": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "corteDuro": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "desde",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Desplazamiento para paginar (el servidor evalúa 30 por llamada)"
          },
          {
            "name": "archivadas",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            },
            "description": "Devuelve las empresas archivadas en vez de las activas"
          }
        ],
        "description": "Una página de la cartera. `desde` pagina de 30 en 30 y `archivadas=1` devuelve las apartadas. Sin caché: la vista se marca y archiva en vivo."
      }
    },
    "/asesor/cumplimiento": {
      "get": {
        "tags": [
          "asesor"
        ],
        "summary": "Cumplimiento tributario cross-empresa: alertas consolidadas de todas las cuentas del contador",
        "description": "Itera las membresías del usuario autenticado y genera una lista plana de alertas (vencimientos próximos, RIMPE rojo, comprobantes devueltos/rechazados, certificados por vencer), ordenadas por urgencia. Requiere sesión de usuario (no disponible con API key).",
        "responses": {
          "200": {
            "description": "Alertas de cumplimiento",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "generadoEn": {
                      "type": "string",
                      "format": "date"
                    },
                    "alertas": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "tenantId": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "empresa": {
                            "type": "string"
                          },
                          "tipo": {
                            "type": "string",
                            "enum": [
                              "VENCIMIENTO",
                              "RIMPE_ROJO",
                              "COMPROBANTE_PROBLEMA",
                              "CERTIFICADO_POR_VENCER"
                            ]
                          },
                          "severidad": {
                            "type": "string",
                            "enum": [
                              "VENCIDO",
                              "URGENTE",
                              "PROXIMO",
                              "INFO"
                            ]
                          },
                          "titulo": {
                            "type": "string"
                          },
                          "fecha": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date"
                          },
                          "diasRestantes": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "detalle": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "resumen": {
                      "type": "object",
                      "properties": {
                        "empresasEvaluadas": {
                          "type": "integer"
                        },
                        "vencidas": {
                          "type": "integer"
                        },
                        "estaSemana": {
                          "type": "integer"
                        },
                        "rimpeRojo": {
                          "type": "integer"
                        },
                        "comprobantesProblema": {
                          "type": "integer"
                        },
                        "certificadosPorVencer": {
                          "type": "integer"
                        }
                      }
                    },
                    "omitidas": {
                      "type": "integer"
                    },
                    "disclaimer": {
                      "type": "string"
                    },
                    "semaforos": {
                      "type": "array",
                      "description": "Semáforo RIMPE H2 por empresa (basado en ingresos acumulados del año, no en proyección).",
                      "items": {
                        "type": "object",
                        "properties": {
                          "tenantId": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "empresa": {
                            "type": "string"
                          },
                          "regimen": {
                            "type": "string",
                            "enum": [
                              "GENERAL",
                              "RIMPE_EMPRENDEDOR",
                              "RIMPE_NEGOCIO_POPULAR"
                            ]
                          },
                          "ingresosAnioCentavos": {
                            "type": "integer",
                            "description": "Ingresos brutos del año en centavos (fact+nd−nc, AUTORIZADO)."
                          },
                          "umbralCentavos": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "Umbral del régimen en centavos. null para GENERAL."
                          },
                          "porcentaje": {
                            "type": [
                              "number",
                              "null"
                            ],
                            "description": "Porcentaje de uso del umbral (1 decimal). null para GENERAL."
                          },
                          "estado": {
                            "type": "string",
                            "enum": [
                              "verde",
                              "ambar",
                              "rojo",
                              "no_aplica"
                            ],
                            "description": "Estado por acumulado: verde <80%, ambar 80-<100%, rojo ≥100%, no_aplica para GENERAL."
                          },
                          "faltanCentavos": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "Centavos que faltan para alcanzar el umbral (max(umbral-ingresos, 0)). null para GENERAL."
                          }
                        },
                        "required": [
                          "tenantId",
                          "empresa",
                          "regimen",
                          "ingresosAnioCentavos",
                          "umbralCentavos",
                          "porcentaje",
                          "estado",
                          "faltanCentavos"
                        ]
                      }
                    },
                    "obligacionesPorEmpresa": {
                      "type": "array",
                      "description": "Bandeja H1: obligaciones tributarias por empresa (horizonte 90 días). Uso: bandeja del despacho con vista Por empresa.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "tenantId": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "empresa": {
                            "type": "string"
                          },
                          "ruc": {
                            "type": "string"
                          },
                          "regimen": {
                            "type": "string",
                            "enum": [
                              "GENERAL",
                              "RIMPE_EMPRENDEDOR",
                              "RIMPE_NEGOCIO_POPULAR"
                            ]
                          },
                          "obligaciones": {
                            "type": "array",
                            "description": "Ordenadas por fechaVencimiento asc.",
                            "items": {
                              "type": "object",
                              "properties": {
                                "codigo": {
                                  "type": "string"
                                },
                                "nombre": {
                                  "type": "string"
                                },
                                "formulario": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "periodo": {
                                  "type": "string",
                                  "description": "\"2026-05\" | \"2026-S1\" | \"2026\""
                                },
                                "fechaVencimiento": {
                                  "type": "string",
                                  "format": "date"
                                },
                                "diasRestantes": {
                                  "type": "integer"
                                },
                                "estado": {
                                  "type": "string",
                                  "enum": [
                                    "VENCIDO",
                                    "URGENTE",
                                    "PROXIMO",
                                    "INFO"
                                  ],
                                  "description": "Derivado de diasRestantes: <0 VENCIDO, 0-7 URGENTE, 8-30 PROXIMO, >30 INFO."
                                },
                                "hecha": {
                                  "type": "boolean",
                                  "description": "Anotada como presentada (POST /asesor/declaraciones): ya no genera alerta"
                                }
                              },
                              "required": [
                                "codigo",
                                "nombre",
                                "formulario",
                                "periodo",
                                "fechaVencimiento",
                                "diasRestantes",
                                "estado"
                              ]
                            }
                          }
                        },
                        "required": [
                          "tenantId",
                          "empresa",
                          "ruc",
                          "regimen",
                          "obligaciones"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Requiere sesión de usuario (no disponible con API key)"
          }
        }
      }
    },
    "/asesor/mi-situacion": {
      "get": {
        "tags": [
          "asesor"
        ],
        "summary": "¿Cómo voy? — situación tributaria de la cuenta actual",
        "description": "Vista del independiente: equivalente de /asesor/cumplimiento para UNA sola cuenta. Consolida en una llamada si la cuenta está lista para emitir (emisor + firma vigente), los vencimientos del horizonte de 90 días, el semáforo RIMPE por acumulado y un `resumen` ya redactado en lenguaje llano. A diferencia de /asesor/cumplimiento, funciona también con API key: no recorre membresías, solo el tenant del contexto.",
        "responses": {
          "200": {
            "description": "Situación de la cuenta",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "generadoEn": {
                      "type": "string",
                      "format": "date"
                    },
                    "empresa": {
                      "type": "string"
                    },
                    "ruc": {
                      "type": "string",
                      "nullable": true
                    },
                    "razonSocial": {
                      "type": "string",
                      "nullable": true
                    },
                    "ambiente": {
                      "type": "object",
                      "properties": {
                        "codigo": {
                          "type": "integer",
                          "enum": [
                            1,
                            2
                          ]
                        },
                        "descripcion": {
                          "type": "string"
                        },
                        "esProduccion": {
                          "type": "boolean"
                        }
                      }
                    },
                    "listoParaEmitir": {
                      "type": "boolean",
                      "description": "false si falta emisor o firma electrónica vigente"
                    },
                    "porCompletar": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "resumen": {
                      "type": "string",
                      "description": "Estado narrado en dos o tres frases, listo para leerle al usuario"
                    },
                    "alertas": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "semaforoRimpe": {
                      "type": "object",
                      "nullable": true
                    },
                    "obligaciones": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/asesor/f104": {
      "get": {
        "tags": [
          "asesor"
        ],
        "summary": "Borrador de cifras del F104 (Declaración de IVA)",
        "description": "Calcula débito fiscal (IVA en ventas) menos crédito tributario (IVA en compras) para el período indicado. Devuelve el IVA a pagar o el crédito para el mes siguiente, la fecha de vencimiento derivada del catálogo según el régimen (mensual general, semestral RIMPE Emprendedor; null para Negocio Popular) y notas informativas. No es la declaración oficial.",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Opcional si el tenant tiene un solo emisor; con varios es obligatorio (400)."
          },
          {
            "name": "anio",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 2020,
              "maximum": 2100
            },
            "description": "Año del período a calcular (p. ej. 2026)."
          },
          {
            "name": "mes",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12
            },
            "description": "Mes del período a calcular (1=enero, 12=diciembre)."
          }
        ],
        "responses": {
          "200": {
            "description": "Borrador del F104",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "emisorId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "ruc": {
                      "type": "string"
                    },
                    "anio": {
                      "type": "integer"
                    },
                    "mes": {
                      "type": "integer"
                    },
                    "periodo": {
                      "type": "string",
                      "description": "\"YYYY-MM\" (p. ej. \"2026-06\")"
                    },
                    "desde": {
                      "type": "string",
                      "format": "date"
                    },
                    "hasta": {
                      "type": "string",
                      "format": "date"
                    },
                    "regimen": {
                      "type": "string",
                      "enum": [
                        "GENERAL",
                        "RIMPE_EMPRENDEDOR",
                        "RIMPE_NEGOCIO_POPULAR"
                      ],
                      "description": "Régimen del emisor."
                    },
                    "periodicidadIva": {
                      "type": "string",
                      "enum": [
                        "MENSUAL",
                        "SEMESTRAL"
                      ],
                      "nullable": true,
                      "description": "Periodicidad de la declaración de IVA según el catálogo de obligaciones; null si el régimen no presenta F104 (Negocio Popular)."
                    },
                    "ventas": {
                      "type": "object",
                      "description": "Totales de IVA de ventas del período (débito fiscal).",
                      "properties": {
                        "baseIva15": {
                          "type": "number"
                        },
                        "baseIva0": {
                          "type": "number"
                        },
                        "baseExento": {
                          "type": "number"
                        },
                        "baseNoObjeto": {
                          "type": "number"
                        },
                        "baseIvaOtras": {
                          "type": "number"
                        },
                        "montoIva": {
                          "type": "number"
                        }
                      }
                    },
                    "compras": {
                      "type": "object",
                      "description": "Totales de IVA de compras del período (crédito tributario).",
                      "properties": {
                        "baseIva15": {
                          "type": "number"
                        },
                        "baseIva0": {
                          "type": "number"
                        },
                        "baseExento": {
                          "type": "number"
                        },
                        "baseNoObjeto": {
                          "type": "number"
                        },
                        "baseIvaOtras": {
                          "type": "number"
                        },
                        "montoIva": {
                          "type": "number"
                        },
                        "retencionIva": {
                          "type": "number"
                        },
                        "retencionRenta": {
                          "type": "number"
                        },
                        "totalSinImpuestos": {
                          "type": "number"
                        },
                        "importeTotal": {
                          "type": "number"
                        }
                      }
                    },
                    "debitoFiscal": {
                      "type": "number",
                      "description": "IVA cobrado en ventas (montoIva de ventas)."
                    },
                    "creditoTributario": {
                      "type": "number",
                      "description": "IVA pagado en compras (montoIva de compras)."
                    },
                    "retencionIvaRecibida": {
                      "type": "number",
                      "description": "Retención de IVA que clientes practicaron al contribuyente (0 = no capturado aún)."
                    },
                    "impuestoCausado": {
                      "type": "number",
                      "description": "Impuesto causado (casillero 601): débito − crédito, antes de retenciones y saldos. Puede ser negativo (crédito por adquisiciones, 602)."
                    },
                    "ivaResultante": {
                      "type": "number",
                      "description": "Débito − Crédito − Retención. Positivo = a pagar; negativo = crédito próximo mes."
                    },
                    "ivaAPagar": {
                      "type": "number",
                      "description": "Valor a pagar al SRI (0 si ivaResultante ≤ 0)."
                    },
                    "creditoProximoMes": {
                      "type": "number",
                      "description": "Crédito que se arrastra al mes siguiente (0 si ivaResultante ≥ 0)."
                    },
                    "vencimiento": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date",
                      "description": "Fecha límite de presentación del F104, derivada del catálogo según el régimen y el 9.º dígito del RUC (mensual general, semestral RIMPE Emprendedor). null si el régimen no declara IVA (Negocio Popular)."
                    },
                    "notas": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Notas informativas que deben mostrarse siempre al usuario."
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "anio o mes fuera de rango, o emisorId ambiguo"
          }
        }
      }
    },
    "/asesor/libro-ventas": {
      "get": {
        "tags": [
          "asesor"
        ],
        "summary": "Libro de ventas (JSON) para un período",
        "description": "Devuelve las líneas de comprobantes de venta AUTORIZADOS (facturas, notas de débito, notas de crédito) para el emisor y mes indicados, junto con los totales agregados por tarifa de IVA. Los datos de desglose de IVA son exactos desde jun-2026 (activación del desglose).",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Opcional si el tenant tiene un solo emisor; con varios es obligatorio (400)."
          },
          {
            "name": "anio",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 2020,
              "maximum": 2100
            },
            "description": "Año del período (p. ej. 2026)."
          },
          {
            "name": "mes",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12
            },
            "description": "Mes del período (1=enero, 12=diciembre)."
          }
        ],
        "responses": {
          "200": {
            "description": "Libro de ventas del período",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "emisorId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "ruc": {
                      "type": "string"
                    },
                    "periodo": {
                      "type": "string",
                      "description": "\"YYYY-MM\""
                    },
                    "desde": {
                      "type": "string",
                      "format": "date"
                    },
                    "hasta": {
                      "type": "string",
                      "format": "date"
                    },
                    "lineas": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "fechaEmision": {
                            "type": "string",
                            "format": "date"
                          },
                          "tipoComprobante": {
                            "type": "string"
                          },
                          "establecimiento": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "puntoEmision": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "secuencial": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "claveAcceso": {
                            "type": "string"
                          },
                          "clienteRuc": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "clienteRazonSocial": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "baseIva15": {
                            "type": "string"
                          },
                          "baseIva0": {
                            "type": "string"
                          },
                          "baseExento": {
                            "type": "string"
                          },
                          "baseNoObjeto": {
                            "type": "string"
                          },
                          "baseIvaOtras": {
                            "type": "string"
                          },
                          "montoIva": {
                            "type": "string"
                          },
                          "importeTotal": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "estado": {
                            "type": "string"
                          },
                          "numeroAutorizacion": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "totales": {
                      "type": "object",
                      "properties": {
                        "baseIva15": {
                          "type": "string"
                        },
                        "baseIva0": {
                          "type": "string"
                        },
                        "baseExento": {
                          "type": "string"
                        },
                        "baseNoObjeto": {
                          "type": "string"
                        },
                        "baseIvaOtras": {
                          "type": "string"
                        },
                        "montoIva": {
                          "type": "string"
                        }
                      }
                    },
                    "notas": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "anio o mes fuera de rango, o emisorId ambiguo"
          }
        }
      }
    },
    "/asesor/libro-ventas.csv": {
      "get": {
        "tags": [
          "asesor"
        ],
        "summary": "Libro de ventas (CSV) para un período",
        "description": "Descarga el libro de ventas del período indicado en formato CSV (RFC 4180, BOM UTF-8). Columnas: Fecha, Tipo, Establecimiento, Pto. Emisión, Secuencial, Clave de acceso, RUC Cliente, Razón Social Cliente, bases de IVA, Monto IVA, Importe Total, Estado, N.° Autorización.",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Opcional si el tenant tiene un solo emisor."
          },
          {
            "name": "anio",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 2020,
              "maximum": 2100
            }
          },
          {
            "name": "mes",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Archivo CSV del libro de ventas",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "anio o mes fuera de rango, o emisorId ambiguo"
          }
        }
      }
    },
    "/asesor/libro-compras": {
      "get": {
        "tags": [
          "asesor"
        ],
        "summary": "Libro de compras (JSON) para un período",
        "description": "Devuelve las líneas de comprobantes recibidos para el emisor y mes indicados, junto con los totales agregados por tarifa de IVA, retenciones e importe total. Limitado a 200 líneas por período.",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Opcional si el tenant tiene un solo emisor; con varios es obligatorio (400)."
          },
          {
            "name": "anio",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 2020,
              "maximum": 2100
            },
            "description": "Año del período (p. ej. 2026)."
          },
          {
            "name": "mes",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12
            },
            "description": "Mes del período (1=enero, 12=diciembre)."
          }
        ],
        "responses": {
          "200": {
            "description": "Libro de compras del período",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "emisorId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "ruc": {
                      "type": "string"
                    },
                    "periodo": {
                      "type": "string",
                      "description": "\"YYYY-MM\""
                    },
                    "desde": {
                      "type": "string",
                      "format": "date"
                    },
                    "hasta": {
                      "type": "string",
                      "format": "date"
                    },
                    "lineas": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "fechaEmision": {
                            "type": "string",
                            "format": "date"
                          },
                          "tipoComprobante": {
                            "type": "string"
                          },
                          "proveedorRuc": {
                            "type": "string"
                          },
                          "proveedorRazonSocial": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "establecimiento": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "puntoEmision": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "secuencial": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "claveAcceso": {
                            "type": "string"
                          },
                          "numeroAutorizacion": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "baseIva15": {
                            "type": "string"
                          },
                          "baseIva0": {
                            "type": "string"
                          },
                          "baseExento": {
                            "type": "string"
                          },
                          "baseNoObjeto": {
                            "type": "string"
                          },
                          "baseIvaOtras": {
                            "type": "string"
                          },
                          "montoIva": {
                            "type": "string"
                          },
                          "retencionIva": {
                            "type": "string"
                          },
                          "retencionRenta": {
                            "type": "string"
                          },
                          "importeTotal": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "totales": {
                      "type": "object",
                      "properties": {
                        "baseIva15": {
                          "type": "string"
                        },
                        "baseIva0": {
                          "type": "string"
                        },
                        "baseExento": {
                          "type": "string"
                        },
                        "baseNoObjeto": {
                          "type": "string"
                        },
                        "baseIvaOtras": {
                          "type": "string"
                        },
                        "montoIva": {
                          "type": "string"
                        },
                        "retencionIva": {
                          "type": "string"
                        },
                        "retencionRenta": {
                          "type": "string"
                        },
                        "totalSinImpuestos": {
                          "type": "string"
                        },
                        "importeTotal": {
                          "type": "string"
                        }
                      }
                    },
                    "notas": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "anio o mes fuera de rango, o emisorId ambiguo"
          }
        }
      }
    },
    "/asesor/libro-compras.csv": {
      "get": {
        "tags": [
          "asesor"
        ],
        "summary": "Libro de compras (CSV) para un período",
        "description": "Descarga el libro de compras del período indicado en formato CSV (RFC 4180, BOM UTF-8). Columnas: Fecha, Tipo, RUC Proveedor, Razón Social Proveedor, Establecimiento, Pto. Emisión, Secuencial, Clave de acceso, N.° Autorización, bases de IVA, Monto IVA, Retención IVA, Retención Renta, Importe Total.",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Opcional si el tenant tiene un solo emisor."
          },
          {
            "name": "anio",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 2020,
              "maximum": 2100
            }
          },
          {
            "name": "mes",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Archivo CSV del libro de compras",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "anio o mes fuera de rango, o emisorId ambiguo"
          }
        }
      }
    },
    "/asesor/retenciones": {
      "get": {
        "tags": [
          "asesor"
        ],
        "summary": "Retenciones practicadas en el período (insumo ATS-compras)",
        "description": "Devuelve el detalle línea a línea de las retenciones que el emisor ha practicado a sus proveedores en el período indicado (un 07 emitido por cada docSustento), junto con los totales por tipo de tributo (Renta / IVA / ISD). Solo contiene datos desde jun-2026 (activación del GAP-RET; sin backfill de 07 anteriores). Las líneas con compraId nulo corresponden a comprobantes de sustento no cargados aún en Compras.",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Opcional si el tenant tiene un solo emisor; con varios es obligatorio (400)."
          },
          {
            "name": "anio",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 2020,
              "maximum": 2100
            },
            "description": "Año del período (p. ej. 2026)."
          },
          {
            "name": "mes",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12
            },
            "description": "Mes del período (1=enero, 12=diciembre)."
          }
        ],
        "responses": {
          "200": {
            "description": "Retenciones practicadas del período",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "emisorId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "ruc": {
                      "type": "string"
                    },
                    "periodo": {
                      "type": "string",
                      "description": "\"YYYY-MM\""
                    },
                    "desde": {
                      "type": "string",
                      "format": "date"
                    },
                    "hasta": {
                      "type": "string",
                      "format": "date"
                    },
                    "lineas": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "claveAccesoRetencion": {
                            "type": "string"
                          },
                          "proveedorRuc": {
                            "type": "string"
                          },
                          "codSustento": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "codDocSustento": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "numDocSustento": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "claveAccesoSustento": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "fechaEmisionSustento": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date"
                          },
                          "tipo": {
                            "type": "string",
                            "enum": [
                              "1",
                              "2",
                              "6"
                            ],
                            "description": "1=Renta, 2=IVA, 6=ISD"
                          },
                          "codigoRetencion": {
                            "type": "string"
                          },
                          "baseImponible": {
                            "type": "string"
                          },
                          "porcentaje": {
                            "type": "string"
                          },
                          "valor": {
                            "type": "string"
                          },
                          "compraId": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "uuid"
                          }
                        }
                      }
                    },
                    "totales": {
                      "type": "object",
                      "properties": {
                        "retencionRenta": {
                          "type": "string"
                        },
                        "retencionIva": {
                          "type": "string"
                        },
                        "retencionIsd": {
                          "type": "string"
                        }
                      }
                    },
                    "notas": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "disclaimer": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "anio o mes fuera de rango, o emisorId ambiguo"
          }
        }
      }
    },
    "/contacto": {
      "post": {
        "tags": [
          "contacto"
        ],
        "summary": "Enviar el formulario de contacto de la web",
        "security": [],
        "description": "Público, sin sesión. Rate-limit: 5 envíos por IP cada 10 min (429), en un contador propio (`rl:contacto:`) que NO comparte cupo con el alta de cuentas ni con el alta OEM. El mensaje **no se persiste**: sale por correo al buzón del operador (`CONTACTO_EMAIL`, con caída a `OPERADOR_EMAIL` y a `soporte@contadeo.com`) y la única huella en base de datos es la fila de `envios_correo`, con el remitente enmascarado.\n\nDos envíos con el mismo email y el mismo mensaje EL MISMO DÍA se deduplican: el segundo responde igual (200) pero no vuelve a salir, para que un doble clic o un reintento del navegador no dupliquen el aviso.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "nombre",
                  "email",
                  "motivo",
                  "mensaje",
                  "aceptaPrivacidad"
                ],
                "properties": {
                  "nombre": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254,
                    "description": "A quién hay que responder. No recibe copia: el correo va al buzón del operador"
                  },
                  "empresa": {
                    "type": "string",
                    "maxLength": 160
                  },
                  "motivo": {
                    "type": "string",
                    "enum": [
                      "startup",
                      "oem",
                      "otro"
                    ],
                    "description": "Gobierna el asunto del correo, que es como el operador tría la bandeja"
                  },
                  "mensaje": {
                    "type": "string",
                    "minLength": 20,
                    "maxLength": 2000
                  },
                  "aceptaPrivacidad": {
                    "type": "boolean",
                    "description": "Aceptación del Aviso de Privacidad (`/privacidad`). Debe ser `true` (400 si falta)"
                  },
                  "sitio": {
                    "type": "string",
                    "description": "Honeypot anti-spam. El formulario lo pinta oculto y una persona nunca lo rellena: si llega con contenido la respuesta es 200 y el mensaje se descarta sin enviarse. **Déjalo vacío o no lo mandes.**"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Mensaje recibido (también cuando se dedupea o cuando lo descarta el honeypot)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "recibido": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/partners/solicitar-oem": {
      "post": {
        "tags": [
          "partners"
        ],
        "summary": "Solicitar acceso OEM (self-serve, empresa)",
        "security": [],
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador.\n\nAlta autoservicio de una empresa como partner OEM. Rate-limit: 5 solicitudes por IP cada 10 min (429). Queda en estado `pendiente` hasta que el operador la aprueba y emite la primera `cdop_` key. Idempotente por RUC: si el RUC ya existe la respuesta es la misma que para uno nuevo (código + estado `pendiente`), a propósito, para no revelar qué empresas ya son partners.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "razonSocial",
                  "ruc",
                  "emailContacto",
                  "aceptaTerminos"
                ],
                "properties": {
                  "razonSocial": {
                    "type": "string",
                    "description": "Razón social de la empresa"
                  },
                  "ruc": {
                    "type": "string",
                    "description": "RUC de la empresa (13 dígitos, dígito verificador válido)"
                  },
                  "emailContacto": {
                    "type": "string",
                    "format": "email"
                  },
                  "nombreContacto": {
                    "type": "string"
                  },
                  "telefonoContacto": {
                    "type": "string"
                  },
                  "aceptaTerminos": {
                    "type": "boolean",
                    "description": "Aceptación de los Términos y Condiciones. Debe ser `true` (400 si falta). Este único campo acredita TRES textos: los Términos y Condiciones, el Aviso de Privacidad (`/privacidad`) y el Anexo de Protección de Datos del contrato OEM (`/oem-anexo-datos`), que se acepta en el mismo acto de solicitar el acceso; se registra como evidencia LOPDP con la fecha y, para el anexo, la versión del texto aceptado. El formulario de `/solicitar-oem` nombra y enlaza los tres en la casilla; si envías `true` desde tu propio formulario, muéstralos igual antes de marcarlo, porque la fila que queda guardada dice que el solicitante los aceptó. La evidencia se guarda solo en el alta: una solicitud repetida del mismo RUC no la reescribe."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Solicitud registrada (o la existente, con estado `pendiente`, si el RUC ya estaba dado de alta)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "codigo": {
                      "type": "string",
                      "description": "Código del partner (CTD-XXXXXXXX)"
                    },
                    "estado": {
                      "type": "string",
                      "example": "pendiente"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Datos inválidos (RUC, email, términos)"
          },
          "429": {
            "description": "Demasiadas solicitudes desde esta IP"
          }
        }
      }
    },
    "/partners/solicitar": {
      "post": {
        "tags": [
          "partners"
        ],
        "summary": "Solicitar alta como partner (idempotente)",
        "responses": {
          "201": {
            "description": "Alta",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "codigo": {
                      "type": "string"
                    },
                    "estado": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      }
    },
    "/partners/canjear-key": {
      "post": {
        "tags": [
          "partners"
        ],
        "summary": "Canjear la primera partner key (self-serve, empresa)",
        "security": [],
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador.\n\nLa empresa canjea el token de un solo uso que le entregó el operador tras aprobarla; la partner key `cdop_` se genera en este momento y se devuelve UNA sola vez. Rate-limit por IP. 400 si el token es inválido, ya se usó o expiró; 409 si el partner ya no está activo.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "Token de canje (cdct_...)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Partner key creada (se muestra una sola vez)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "nombre": {
                      "type": "string"
                    },
                    "prefijo": {
                      "type": "string"
                    },
                    "roles": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "aprovisionar": {
                      "type": "boolean"
                    },
                    "key": {
                      "type": "string",
                      "description": "La key cdop_ en claro; no se vuelve a mostrar"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Token inválido, usado o expirado"
          },
          "409": {
            "description": "El partner no está activo"
          },
          "429": {
            "description": "Demasiadas solicitudes desde esta IP"
          }
        }
      }
    },
    "/partners/mi-panel": {
      "get": {
        "tags": [
          "partners"
        ],
        "summary": "Panel del partner: nivel, clientes activos, comisiones y totales",
        "responses": {
          "200": {
            "description": "Panel",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "codigo": {
                      "type": "string"
                    },
                    "estado": {
                      "type": "string"
                    },
                    "nivel": {
                      "type": "string",
                      "enum": [
                        "aliado",
                        "pro",
                        "elite"
                      ]
                    },
                    "clientesActivos": {
                      "type": "integer"
                    },
                    "siguienteNivel": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "nivel": {
                          "type": "string"
                        },
                        "faltan": {
                          "type": "integer"
                        }
                      }
                    },
                    "comisiones": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "periodo": {
                            "type": "string"
                          },
                          "estado": {
                            "type": "string"
                          },
                          "total": {
                            "type": "number"
                          }
                        }
                      }
                    },
                    "totalPendiente": {
                      "type": "number"
                    },
                    "totalPagado": {
                      "type": "number"
                    },
                    "minimoPagoUsd": {
                      "type": "number"
                    },
                    "datosBancarios": {
                      "type": "object"
                    },
                    "payphone": {
                      "type": "object",
                      "description": "Identificador Payphone del partner para el split de comisiones. Pendiente de verificación si identifier está presente y splitHabilitado es false.",
                      "properties": {
                        "identifier": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Teléfono (+593XXXXXXXXX), cédula (10 dígitos) o RUC (13 dígitos). Null si no registrado."
                        },
                        "identifierType": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "4 = teléfono, 3 = cédula/RUC, null = sin identificador."
                        },
                        "splitHabilitado": {
                          "type": "boolean",
                          "description": "true = el operador verificó la cuenta Payphone y activó el split."
                        }
                      }
                    },
                    "nivelHistorico": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "enum": [
                        "aliado",
                        "pro",
                        "elite",
                        "master",
                        "embajador",
                        null
                      ]
                    },
                    "proximoHito": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "nivel": {
                          "type": "string",
                          "enum": [
                            "aliado",
                            "pro",
                            "elite",
                            "master",
                            "embajador"
                          ]
                        },
                        "faltan": {
                          "type": "integer"
                        },
                        "bono": {
                          "type": "number"
                        }
                      }
                    },
                    "bonosGanados": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "nivel": {
                            "type": "string",
                            "enum": [
                              "aliado",
                              "pro",
                              "elite",
                              "master",
                              "embajador"
                            ]
                          },
                          "monto": {
                            "type": "number"
                          },
                          "estado": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "fastStart": {
                      "type": "object",
                      "properties": {
                        "activo": {
                          "type": "boolean"
                        },
                        "ventanaDias": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      }
    },
    "/partners/datos-bancarios": {
      "patch": {
        "tags": [
          "partners"
        ],
        "summary": "Actualizar datos bancarios para el pago de comisiones",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "bancoNombre": {
                    "type": "string"
                  },
                  "bancoTipoCuenta": {
                    "type": "string"
                  },
                  "bancoNumeroCuenta": {
                    "type": "string"
                  },
                  "bancoTitular": {
                    "type": "string"
                  },
                  "bancoIdentificacion": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      }
    },
    "/partners/payphone": {
      "patch": {
        "tags": [
          "partners"
        ],
        "summary": "Registrar o borrar el identificador Payphone para cobro de comisiones vía split",
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador.\n\nEl tipo (3=cédula/RUC, 4=teléfono) se deriva en servidor del formato del identificador. Cualquier cambio resetea splitHabilitado=false; el operador debe re-verificar. Enviar identifier=null limpia el identificador y deshabilita el split.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "identifier"
                ],
                "properties": {
                  "identifier": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Teléfono (+593XXXXXXXXX), cédula (10 dígitos), RUC (13 dígitos) o null para limpiar."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Formato de identificador inválido",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "mensaje": {
                      "type": "string"
                    },
                    "problemas": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "campo": {
                            "type": "string"
                          },
                          "mensaje": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/partners/cobros": {
      "post": {
        "tags": [
          "partners"
        ],
        "summary": "Solicitar cobro de comisiones (devuelve factura prellenada)",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "medioPagoPreferido": {
                    "type": "string",
                    "enum": [
                      "banco",
                      "payphone"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cobro solicitado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cobroId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "totalBaseCentavos": {
                      "type": "integer"
                    },
                    "ivaCentavos": {
                      "type": "integer"
                    },
                    "totalCentavos": {
                      "type": "integer"
                    },
                    "moneda": {
                      "type": "string"
                    },
                    "periodos": {
                      "type": "string"
                    },
                    "prellenado": {
                      "type": "object",
                      "description": "EmitirFacturaDto prellenado para la factura de comisión."
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      },
      "get": {
        "tags": [
          "partners"
        ],
        "summary": "Historial de cobros del partner",
        "responses": {
          "200": {
            "description": "Cobros",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "estado": {
                        "type": "string"
                      },
                      "periodos": {
                        "type": "string"
                      },
                      "montoBaseCentavos": {
                        "type": "integer"
                      },
                      "ivaCentavos": {
                        "type": "integer"
                      },
                      "montoTotalCentavos": {
                        "type": "integer"
                      },
                      "claveAcceso": {
                        "type": [
                          "string",
                          "null"
                        ]
                      },
                      "pagoReferencia": {
                        "type": [
                          "string",
                          "null"
                        ]
                      },
                      "createdAt": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "facturadoAt": {
                        "type": [
                          "string",
                          "null"
                        ]
                      },
                      "pagadoAt": {
                        "type": [
                          "string",
                          "null"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      }
    },
    "/partners/cobros/{id}": {
      "patch": {
        "tags": [
          "partners"
        ],
        "summary": "Enlazar la factura emitida al cobro (solicitado → facturado)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comprobanteId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "claveAcceso": {
                    "type": "string"
                  }
                },
                "required": [
                  "comprobanteId",
                  "claveAcceso"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Enlazado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cobroId": {
                      "type": "string"
                    },
                    "estado": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      },
      "delete": {
        "tags": [
          "partners"
        ],
        "summary": "Cancelar un cobro abierto (libera las comisiones)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cobroId": {
                      "type": "string"
                    },
                    "estado": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "**Requiere el programa OEM activo** (`OEM_HABILITADO=true`; sin la variable, 404). Alta de partners por aprobación manual del operador."
      }
    },
    "/tickets": {
      "post": {
        "tags": [
          "tickets"
        ],
        "summary": "Crear ticket de soporte",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "asunto",
                  "cuerpo"
                ],
                "properties": {
                  "asunto": {
                    "type": "string",
                    "maxLength": 200,
                    "example": "Error al emitir factura"
                  },
                  "cuerpo": {
                    "type": "string",
                    "example": "Al intentar emitir la factura recibo el error 45..."
                  },
                  "prioridad": {
                    "type": "string",
                    "enum": [
                      "baja",
                      "normal",
                      "alta"
                    ],
                    "default": "normal"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Ticket creado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "asunto": {
                      "type": "string"
                    },
                    "estado": {
                      "type": "string"
                    },
                    "prioridad": {
                      "type": "string"
                    },
                    "ultimoMensajeAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "tickets"
        ],
        "summary": "Listar tickets del tenant",
        "parameters": [
          {
            "name": "estado",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "abierto",
                "en_proceso",
                "resuelto",
                "cerrado"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lista de tickets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "asunto": {
                        "type": "string"
                      },
                      "estado": {
                        "type": "string"
                      },
                      "prioridad": {
                        "type": "string"
                      },
                      "ultimoMensajeAt": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "createdAt": {
                        "type": "string",
                        "format": "date-time"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/tickets/{id}": {
      "get": {
        "tags": [
          "tickets"
        ],
        "summary": "Detalle de un ticket (cabecera + hilo de mensajes)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ticket con mensajes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "asunto": {
                      "type": "string"
                    },
                    "estado": {
                      "type": "string"
                    },
                    "prioridad": {
                      "type": "string"
                    },
                    "asignadoA": {
                      "type": "string",
                      "format": "uuid",
                      "nullable": true
                    },
                    "ultimoMensajeAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "mensajes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "autorTipo": {
                            "type": "string",
                            "enum": [
                              "tenant",
                              "operador"
                            ]
                          },
                          "cuerpo": {
                            "type": "string"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Ticket no encontrado"
          }
        }
      }
    },
    "/tickets/{id}/mensajes": {
      "post": {
        "tags": [
          "tickets"
        ],
        "summary": "Responder en el hilo del ticket (como tenant)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cuerpo"
                ],
                "properties": {
                  "cuerpo": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Mensaje insertado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "ticketId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "autorTipo": {
                      "type": "string"
                    },
                    "cuerpo": {
                      "type": "string"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Ticket no encontrado"
          }
        }
      }
    },
    "/tickets/{id}/adjuntos": {
      "post": {
        "tags": [
          "tickets"
        ],
        "summary": "Subir adjunto a un ticket (tenant)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "nombre",
                  "contentType",
                  "base64"
                ],
                "properties": {
                  "nombre": {
                    "type": "string",
                    "maxLength": 255,
                    "example": "factura.pdf"
                  },
                  "contentType": {
                    "type": "string",
                    "enum": [
                      "application/pdf",
                      "image/png",
                      "image/jpeg",
                      "image/webp",
                      "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
                    ],
                    "example": "application/pdf"
                  },
                  "base64": {
                    "type": "string",
                    "format": "byte",
                    "description": "Contenido del archivo codificado en base64. Máximo 10 MB (~13.3 MB en base64)."
                  },
                  "mensajeId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID del mensaje al que se asocia el adjunto (opcional)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Adjunto subido",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "nombre": {
                      "type": "string"
                    },
                    "contentType": {
                      "type": "string"
                    },
                    "tamanoBytes": {
                      "type": "integer"
                    },
                    "mensajeId": {
                      "type": "string",
                      "format": "uuid",
                      "nullable": true
                    },
                    "subidoPorTipo": {
                      "type": "string",
                      "enum": [
                        "tenant",
                        "operador"
                      ]
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Tipo no permitido, archivo > 10 MB o base64 inválido"
          },
          "404": {
            "description": "Ticket no encontrado o no pertenece al tenant"
          }
        }
      }
    },
    "/tickets/{id}/adjuntos/{adjuntoId}/url": {
      "get": {
        "tags": [
          "tickets"
        ],
        "summary": "Obtener URL prefirmada para descargar un adjunto (tenant)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "adjuntoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "URL prefirmada de descarga (expira en 1 hora)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Ticket o adjunto no encontrado, o no pertenece al tenant"
          }
        }
      }
    },
    "/buzon/brevo/webhook": {
      "post": {
        "tags": [
          "compras"
        ],
        "summary": "Webhook del buzón de correo (Brevo Inbound Parse)",
        "description": "Entrada PÚBLICA del buzón de recepción por correo: Brevo entrega aquí cada correo reenviado a la dirección `<token>@buzon.contadeo.com` de una cuenta. Autenticación por el parámetro `secreto` (configurado en Brevo; 401 si falta o no coincide, y sin `BUZON_WEBHOOK_SECRETO` en el despliegue el buzón está deshabilitado). Con secreto válido responde SIEMPRE 200: cada XML adjunto se ingesta al módulo Compras con `origen: email` resolviendo el emisor por el RUC RECEPTOR del comprobante; los PDF sin XML se guardan para revisión; los fallos por-correo quedan en la bandeja (`GET /buzon/mensajes`), no en el status. Dedupe por Message-ID y por clave de acceso.",
        "parameters": [
          {
            "name": "secreto",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Payload de Brevo Inbound Parse ({items: [...]}); los adjuntos llegan como DownloadToken y se descargan de Brevo"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Correos aceptados",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "recibidos": {
                      "type": "integer"
                    },
                    "aceptados": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Secreto ausente, inválido o buzón deshabilitado"
          }
        }
      }
    },
    "/buzon": {
      "get": {
        "tags": [
          "compras"
        ],
        "summary": "Dirección del buzón de correo de la cuenta",
        "description": "Devuelve la dirección `<token>@buzon.contadeo.com` (el token se crea perezosamente la primera vez), si el buzón está activo y si el webhook está habilitado en el despliegue. Roles: owner, admin, contador.",
        "responses": {
          "200": {
            "description": "Dirección del buzón",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "direccion": {
                      "type": "string"
                    },
                    "activo": {
                      "type": "boolean"
                    },
                    "habilitado": {
                      "type": "boolean"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "rotadoAt": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/buzon/rotar": {
      "post": {
        "tags": [
          "compras"
        ],
        "summary": "Rotar el token del buzón",
        "description": "Genera un token nuevo; la dirección anterior deja de funcionar AL INSTANTE (avisa a quien la tenga guardada). Roles: owner, admin.",
        "responses": {
          "201": {
            "description": "Dirección nueva",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "direccion": {
                      "type": "string"
                    },
                    "rotadoAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/buzon/mensajes": {
      "get": {
        "tags": [
          "compras"
        ],
        "summary": "Bandeja del buzón de correo",
        "description": "Los correos recibidos con su saldo por adjunto (compra creada, duplicado, PDF guardado a revisión, error con motivo). Quien reenvía un correo no ve la respuesta HTTP: esta bandeja ES la respuesta. Roles: owner, admin, emisor, contador, lector.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 30,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Mensajes de la bandeja",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "mensajes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "remitente": {
                            "type": "string"
                          },
                          "asunto": {
                            "type": "string",
                            "nullable": true
                          },
                          "estado": {
                            "type": "string",
                            "enum": [
                              "procesado",
                              "parcial",
                              "rechazado",
                              "requiere_revision"
                            ]
                          },
                          "motivo": {
                            "type": "string",
                            "nullable": true
                          },
                          "adjuntos": {
                            "type": "array",
                            "items": {
                              "type": "object"
                            }
                          },
                          "recibidoAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/compras/xml": {
      "post": {
        "tags": [
          "compras"
        ],
        "summary": "Subir un XML de comprobante recibido (base64)",
        "description": "Parsea y registra un comprobante electrónico recibido de un proveedor. Idempotente: si la clave de acceso ya existe devuelve { duplicado: true, id } sin reinsertar. Límite: 2 MB por archivo.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId",
                  "nombre",
                  "base64"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "nombre": {
                    "type": "string"
                  },
                  "base64": {
                    "type": "string",
                    "description": "Contenido del XML codificado en base64 (máx. 2 MB)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Compra registrada o ya existente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompraSubidaResultado"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/compras/lote": {
      "post": {
        "tags": [
          "compras"
        ],
        "summary": "Subir lote de hasta 50 XML (síncrono)",
        "description": "Procesa hasta 50 XML de comprobantes recibidos. Un error en un archivo no cancela el lote; los fallos se reportan en el campo errores.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId",
                  "archivos"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "archivos": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "type": "object",
                      "required": [
                        "nombre",
                        "base64"
                      ],
                      "properties": {
                        "nombre": {
                          "type": "string"
                        },
                        "base64": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultado del lote",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompraLoteResultado"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/compras/retencion-recibida": {
      "post": {
        "tags": [
          "compras"
        ],
        "summary": "Subir un 07 recibido de un cliente (retención sobre ventas)",
        "description": "Parsea y registra un comprobante de retención (codDoc 07) emitido por un cliente del tenant sobre sus ventas. Persiste las líneas en `retenciones_recibidas` y concilia contra las ventas del tenant por clave de acceso. Idempotente por clave de acceso del 07. El valor tipo IVA (codigo=2) alimenta el campo `retencionIvaRecibida` del F104. Límite: 2 MB por archivo.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId",
                  "nombre",
                  "base64"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "nombre": {
                    "type": "string"
                  },
                  "base64": {
                    "type": "string",
                    "description": "Contenido del XML codificado en base64 (máx. 2 MB)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Retención recibida registrada o ya existente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetencionRecibidaResultado"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/compras/manual": {
      "post": {
        "tags": [
          "compras"
        ],
        "summary": "Registrar compra manualmente (sin XML)",
        "description": "Crea una compra de origen 'manual'. Idempotente si se provee claveAcceso.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId",
                  "proveedorRuc",
                  "tipoComprobante",
                  "fechaEmision"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "proveedorRuc": {
                    "type": "string",
                    "minLength": 13,
                    "maxLength": 13
                  },
                  "proveedorRazonSocial": {
                    "type": "string"
                  },
                  "tipoComprobante": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 2
                  },
                  "claveAcceso": {
                    "type": "string"
                  },
                  "fechaEmision": {
                    "type": "string",
                    "format": "date"
                  },
                  "totalSinImpuestos": {
                    "type": "number"
                  },
                  "importeTotal": {
                    "type": "number"
                  },
                  "baseIva15": {
                    "type": "number"
                  },
                  "baseIva0": {
                    "type": "number"
                  },
                  "baseExento": {
                    "type": "number"
                  },
                  "baseNoObjeto": {
                    "type": "number"
                  },
                  "baseIvaOtras": {
                    "type": "number"
                  },
                  "montoIva": {
                    "type": "number"
                  },
                  "retencionIva": {
                    "type": "number"
                  },
                  "retencionRenta": {
                    "type": "number"
                  },
                  "sustentoTributario": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Compra registrada",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompraSubidaResultado"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/compras/registrar": {
      "post": {
        "tags": [
          "compras"
        ],
        "summary": "Registrar una compra calculando el IVA en el servidor",
        "description": "Alta de compra a partir de la BASE imponible y el código de tarifa de la Tabla 18, no de las columnas ya calculadas. El servidor deriva las bases con el mismo `resolverBasesIva` que usa el parser de XML, de modo que un alta conversacional y una ingesta de XML producen cifras idénticas. Pensado para clientes que no deben hacer aritmética tributaria (el MCP). Para quien ya trae las cifras calculadas sigue existiendo `/compras/manual`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId",
                  "proveedorRuc",
                  "tipoComprobante",
                  "fechaEmision",
                  "baseImponible",
                  "codigoPorcentaje"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "proveedorRuc": {
                    "type": "string",
                    "maxLength": 13
                  },
                  "proveedorRazonSocial": {
                    "type": "string"
                  },
                  "tipoComprobante": {
                    "type": "string",
                    "pattern": "^[0-9]{2}$"
                  },
                  "fechaEmision": {
                    "type": "string",
                    "format": "date"
                  },
                  "baseImponible": {
                    "type": "number",
                    "minimum": 0
                  },
                  "codigoPorcentaje": {
                    "type": "string",
                    "description": "Tabla 18 del SRI: '4'=15%, '0'=0%, '7'=exento… Consúltalos en /sri/catalogos."
                  },
                  "claveAcceso": {
                    "type": "string",
                    "maxLength": 49
                  },
                  "sustentoTributario": {
                    "type": "string",
                    "maxLength": 2
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Compra registrada (o duplicado por claveAcceso)"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          }
        }
      }
    },
    "/compras": {
      "get": {
        "tags": [
          "compras"
        ],
        "summary": "Listar compras del tenant (fechaEmision desc)",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "desde",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "hasta",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "proveedorRuc",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Búsqueda por razón social del proveedor"
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lista de compras",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CompraResumen"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/compras/totales": {
      "get": {
        "tags": [
          "compras"
        ],
        "summary": "Totales agregados por período (insumo F104)",
        "description": "Devuelve la suma de bases e impuestos del período indicado. Usa como insumo para el Formulario 104 de declaración de IVA.",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "desde",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "hasta",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Totales del período",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComprasTotalesPeriodo"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          }
        }
      }
    },
    "/compras/{id}": {
      "get": {
        "tags": [
          "compras"
        ],
        "summary": "Detalle de una compra",
        "parameters": [
          {
            "$ref": "#/components/parameters/id"
          }
        ],
        "responses": {
          "200": {
            "description": "Compra",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompraDetalle"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/compras/{id}/xml": {
      "get": {
        "tags": [
          "compras"
        ],
        "summary": "URL prefirmada del XML recibido (expira en 1 h)",
        "parameters": [
          {
            "$ref": "#/components/parameters/id"
          }
        ],
        "responses": {
          "200": {
            "description": "URL de descarga",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UrlDescarga"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/compras/pendientes": {
      "get": {
        "tags": [
          "compras"
        ],
        "summary": "Comprobantes sin clasificar, agrupados por proveedor",
        "description": "Bandeja de pendientes del casillero 564. Se agrupa POR PROVEEDOR a propósito: 40 comprobantes de 6 proveedores son 6 decisiones, no 40. Requiere el módulo `clasificacion`.",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rollup de pendientes por proveedor"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "402": {
            "description": "El plan no incluye el módulo de clasificación"
          }
        }
      }
    },
    "/compras/proveedores/{ruc}/clasificacion": {
      "post": {
        "tags": [
          "compras"
        ],
        "summary": "Decidir si un proveedor da derecho a crédito de IVA",
        "description": "Guarda la decisión sobre un proveedor y, salvo `aplicarA: solo_nuevos`, la aplica a sus comprobantes ya cargados. Sin `confirmar: true` NO escribe nada: devuelve el impacto (cuántos comprobantes, cuánto IVA y qué períodos se moverían) para que una persona lo revise antes. Reclasificar cierra la vigencia anterior y abre otra; nunca se pisa. Requiere el módulo `clasificacion`.",
        "parameters": [
          {
            "name": "ruc",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 13
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId",
                  "clasificacion"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "clasificacion": {
                    "type": "string",
                    "enum": [
                      "con_derecho",
                      "sin_derecho",
                      "proporcional"
                    ]
                  },
                  "sustentoTributario": {
                    "type": "string",
                    "maxLength": 2
                  },
                  "nota": {
                    "type": "string"
                  },
                  "actividadEconomica": {
                    "type": "string",
                    "description": "Respaldo del texto del catastro SRI por si el catastro no responde al decidir. La evidencia la captura el servidor: lo que se mande aquí solo se usa cuando el SRI no está disponible."
                  },
                  "aplicarA": {
                    "type": "string",
                    "enum": [
                      "pendientes",
                      "solo_nuevos"
                    ]
                  },
                  "confirmar": {
                    "type": "boolean",
                    "description": "false o ausente = solo previsualizar el impacto."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Impacto de la decisión (y `aplicados` si se confirmó)"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "402": {
            "description": "El plan no incluye el módulo de clasificación"
          }
        }
      }
    },
    "/compras/{id}/clasificacion": {
      "patch": {
        "tags": [
          "compras"
        ],
        "summary": "Clasificar un comprobante concreto (excepción)",
        "description": "Fija la clasificación de UN comprobante sin tocar la decisión guardada del proveedor: una excepción no debe envenenar al proveedor entero. Solo altera columnas de clasificación; los hechos fiscales (bases, IVA, clave de acceso, XML) son inmutables. Para `proporcional` hay que indicar `creditoIva`: Contadeo NO calcula el factor del 563. Requiere el módulo `clasificacion`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "clasificacion"
                ],
                "properties": {
                  "clasificacion": {
                    "type": "string",
                    "enum": [
                      "con_derecho",
                      "sin_derecho",
                      "proporcional"
                    ]
                  },
                  "creditoIva": {
                    "type": "number",
                    "description": "Solo para `proporcional`. Acotado por el IVA del comprobante."
                  },
                  "nota": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Clasificación aplicada"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "402": {
            "description": "El plan no incluye el módulo de clasificación"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/compras/{id}/clasificacion/historial": {
      "get": {
        "tags": [
          "compras"
        ],
        "summary": "Historial de clasificación de un comprobante",
        "description": "Traza append-only de por qué este comprobante entró (o no) al casillero 564: cada evento guarda el ANTES y el DESPUÉS, la regla que decidió, el motivo redactado y por qué canal se hizo. Requiere el módulo `clasificacion`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Eventos en orden cronológico"
          },
          "402": {
            "description": "El plan no incluye el módulo de clasificación"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/nomina/empleados": {
      "post": {
        "tags": [
          "nomina"
        ],
        "summary": "Crear empleado",
        "description": "Registra un empleado activo en el emisor indicado. Valida la cédula con el algoritmo del SRI (módulo 10). Único por (tenant, emisor, cédula).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId",
                  "cedula",
                  "nombres",
                  "fechaIngreso",
                  "sueldo"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "cedula": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 10
                  },
                  "nombres": {
                    "type": "string"
                  },
                  "cargo": {
                    "type": "string"
                  },
                  "fechaIngreso": {
                    "type": "string",
                    "format": "date"
                  },
                  "sueldo": {
                    "type": "number"
                  },
                  "tipoContrato": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Empleado creado"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      },
      "get": {
        "tags": [
          "nomina"
        ],
        "summary": "Listar empleados del emisor",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "estado",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "activo",
                "inactivo"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lista de empleados",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EmpleadoResumen"
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/nomina/empleados/{id}": {
      "patch": {
        "tags": [
          "nomina"
        ],
        "summary": "Actualizar empleado",
        "description": "Actualiza uno o más campos del empleado: sueldo, fechaSalida, estado, cargo, etc.",
        "parameters": [
          {
            "$ref": "#/components/parameters/id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "cedula": {
                    "type": "string"
                  },
                  "nombres": {
                    "type": "string"
                  },
                  "cargo": {
                    "type": "string"
                  },
                  "fechaIngreso": {
                    "type": "string",
                    "format": "date"
                  },
                  "fechaSalida": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date"
                  },
                  "sueldo": {
                    "type": "number"
                  },
                  "tipoContrato": {
                    "type": "string"
                  },
                  "estado": {
                    "type": "string",
                    "enum": [
                      "activo",
                      "inactivo"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Empleado actualizado"
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      },
      "get": {
        "tags": [
          "nomina"
        ],
        "summary": "Detalle de un empleado",
        "parameters": [
          {
            "$ref": "#/components/parameters/id"
          },
          {
            "name": "emisorId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Empleado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmpleadoResumen"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/nomina/rol/generar": {
      "post": {
        "tags": [
          "nomina"
        ],
        "summary": "Generar rol de pagos del período",
        "description": "Calcula y persiste el rol de pagos para todos los empleados activos del emisor en el período indicado. Idempotente: una segunda llamada recalcula y actualiza los datos existentes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId",
                  "periodo"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "periodo": {
                    "type": "string",
                    "pattern": "^\\d{4}-(0[1-9]|1[0-2])$",
                    "description": "Período en formato YYYY-MM, ej. 2026-06"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Rol generado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GenerarRolResultado"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/nomina/rol": {
      "get": {
        "tags": [
          "nomina"
        ],
        "summary": "Listar roles de pago del emisor",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "periodo",
            "in": "query",
            "schema": {
              "type": "string",
              "description": "Filtrar por período YYYY-MM"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lista de roles de pago",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/RolPagosResumen"
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/nomina/rol/totales": {
      "get": {
        "tags": [
          "nomina"
        ],
        "summary": "Totales del período (suma de columnas materializadas)",
        "description": "Devuelve la suma de ingresos, egresos, neto y provisiones de todos los empleados del emisor en el período. Útil para cierres de nómina.",
        "parameters": [
          {
            "name": "emisorId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "periodo",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Período YYYY-MM"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Totales del período",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NominaTotalesPeriodo"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/alertas/preferencias": {
      "get": {
        "tags": [
          "alertas"
        ],
        "summary": "Obtiene las preferencias de alertas del usuario autenticado",
        "description": "Lazy: si el usuario nunca ha configurado preferencias, devuelve los valores por defecto (emailActivo=false, antelacionDias=7, incluirRimpe=true) sin crear una fila. 403 con API key (no hay userId).",
        "responses": {
          "200": {
            "description": "Preferencias de alertas",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertasPreferencias"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      },
      "put": {
        "tags": [
          "alertas"
        ],
        "summary": "Actualiza las preferencias de alertas del usuario autenticado",
        "description": "Crea la fila si no existe (upsert). antelacionDias debe ser 1..30. 403 con API key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "emailActivo": {
                    "type": "boolean",
                    "description": "true = recibir digest diario por correo"
                  },
                  "antelacionDias": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 30,
                    "description": "Días de antelación para incluir un vencimiento en el digest"
                  },
                  "incluirRimpe": {
                    "type": "boolean",
                    "description": "true = incluir semáforo RIMPE ámbar/rojo en el digest"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Preferencias actualizadas",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertasPreferencias"
                }
              }
            }
          },
          "400": {
            "description": "antelacionDias fuera del rango 1..30"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/conciliacion/importar": {
      "post": {
        "tags": [
          "conciliacion"
        ],
        "summary": "Importar extracto bancario (CSV en base64)",
        "description": "Parsea un extracto bancario en CSV y registra los movimientos. Idempotente por hash de línea. Límite: 5 MB por archivo.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId",
                  "banco",
                  "nombre",
                  "base64"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "banco": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "cuenta": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "nombre": {
                    "type": "string"
                  },
                  "base64": {
                    "type": "string",
                    "description": "Contenido del CSV codificado en base64 (máx. 5 MB)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Extracto importado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportarExtractoResultado"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "403": {
            "$ref": "#/components/responses/RolInsuficiente"
          }
        }
      }
    },
    "/conciliacion/extractos": {
      "get": {
        "tags": [
          "conciliacion"
        ],
        "summary": "Listar extractos bancarios del tenant",
        "parameters": [
          {
            "in": "query",
            "name": "emisorId",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "desde",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "in": "query",
            "name": "hasta",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "in": "query",
            "name": "estado",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lista de extractos bancarios",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ExtractoBancario"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/conciliacion/extractos/{id}/movimientos": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "conciliacion"
        ],
        "summary": "Listar movimientos de un extracto bancario",
        "parameters": [
          {
            "in": "query",
            "name": "estado",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lista de movimientos bancarios",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/MovimientoBancario"
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/conciliacion/extractos/{id}/csv": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "conciliacion"
        ],
        "summary": "URL presignada del CSV original del extracto",
        "responses": {
          "200": {
            "description": "URL presignada del CSV (expira en 1 h)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/conciliacion/movimientos/{id}/sugerencias": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "conciliacion"
        ],
        "summary": "Sugerencias de match para un movimiento bancario",
        "description": "Calcula los candidatos con mejor score para conciliar el movimiento. Ingresos (monto≥0) buscan en ventas AUTORIZADAS; egresos en compras.",
        "responses": {
          "200": {
            "description": "Sugerencias de match ordenadas por score desc",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SugerenciasMatch"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/conciliacion/movimientos/{id}/conciliar": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "conciliacion"
        ],
        "summary": "Conciliar manualmente un movimiento bancario",
        "description": "Asocia el movimiento a un comprobante de venta o compra. Requiere confirmación humana (v1 nunca auto-concilia).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "matchTipo",
                  "matchId"
                ],
                "properties": {
                  "matchTipo": {
                    "type": "string",
                    "enum": [
                      "venta",
                      "compra"
                    ]
                  },
                  "matchId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "matchConfianza": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Movimiento conciliado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "estado": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/conciliacion/movimientos/{id}/ignorar": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "conciliacion"
        ],
        "summary": "Marcar movimiento como ignorado",
        "responses": {
          "200": {
            "description": "Movimiento marcado como ignorado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "estado": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/conciliacion/movimientos/{id}/desconciliar": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "conciliacion"
        ],
        "summary": "Revertir conciliación de un movimiento",
        "description": "Devuelve el movimiento a estado 'pendiente', borrando el match.",
        "responses": {
          "200": {
            "description": "Movimiento revertido a pendiente",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "estado": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/conciliacion/resumen": {
      "get": {
        "tags": [
          "conciliacion"
        ],
        "summary": "Resumen de conciliación por emisor",
        "description": "Conteos por estado, suma de ingresos/egresos y % de cuadre.",
        "parameters": [
          {
            "in": "query",
            "name": "emisorId",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "extractoId",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resumen de conciliación",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResumenConciliacion"
                }
              }
            }
          }
        }
      }
    },
    "/contabilidad/cuentas": {
      "get": {
        "tags": [
          "contabilidad"
        ],
        "summary": "Lista el plan de cuentas del emisor",
        "parameters": [
          {
            "in": "query",
            "name": "emisorId",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lista de cuentas contables"
          },
          "403": {
            "$ref": "#/components/responses/NoAutorizado"
          }
        }
      },
      "post": {
        "tags": [
          "contabilidad"
        ],
        "summary": "Crea una cuenta contable en el plan del emisor",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId",
                  "codigo",
                  "nombre",
                  "tipo",
                  "naturaleza"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "codigo": {
                    "type": "string",
                    "example": "1.1.01.01"
                  },
                  "nombre": {
                    "type": "string",
                    "example": "Caja General"
                  },
                  "tipo": {
                    "type": "string",
                    "enum": [
                      "activo",
                      "pasivo",
                      "patrimonio",
                      "ingreso",
                      "costo",
                      "gasto"
                    ]
                  },
                  "naturaleza": {
                    "type": "string",
                    "enum": [
                      "deudora",
                      "acreedora"
                    ]
                  },
                  "cuentaPadreId": {
                    "type": "string",
                    "format": "uuid",
                    "nullable": true
                  },
                  "imputable": {
                    "type": "boolean",
                    "default": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cuenta creada"
          },
          "400": {
            "$ref": "#/components/responses/Invalido"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          }
        }
      }
    },
    "/contabilidad/cuentas/inicializar": {
      "post": {
        "tags": [
          "contabilidad"
        ],
        "summary": "Siembra el plan de cuentas base Ecuador (idempotente)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Plan inicializado"
          },
          "403": {
            "$ref": "#/components/responses/NoAutorizado"
          }
        }
      }
    },
    "/contabilidad/cuentas/{id}": {
      "patch": {
        "tags": [
          "contabilidad"
        ],
        "summary": "Actualiza una cuenta contable (nombre, tipo, naturaleza, padre, activa)",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "nombre": {
                    "type": "string"
                  },
                  "tipo": {
                    "type": "string",
                    "enum": [
                      "activo",
                      "pasivo",
                      "patrimonio",
                      "ingreso",
                      "costo",
                      "gasto"
                    ]
                  },
                  "naturaleza": {
                    "type": "string",
                    "enum": [
                      "deudora",
                      "acreedora"
                    ]
                  },
                  "cuentaPadreId": {
                    "type": "string",
                    "format": "uuid",
                    "nullable": true
                  },
                  "activa": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cuenta actualizada"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/contabilidad/asientos": {
      "get": {
        "tags": [
          "contabilidad"
        ],
        "summary": "Lista asientos del emisor (filtro opcional por fecha)",
        "parameters": [
          {
            "in": "query",
            "name": "emisorId",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "desde",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "in": "query",
            "name": "hasta",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lista de asientos"
          }
        }
      },
      "post": {
        "tags": [
          "contabilidad"
        ],
        "summary": "Crea un asiento contable en estado borrador",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId",
                  "fecha",
                  "glosa",
                  "lineas"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "fecha": {
                    "type": "string",
                    "format": "date"
                  },
                  "glosa": {
                    "type": "string"
                  },
                  "lineas": {
                    "type": "array",
                    "minItems": 2,
                    "items": {
                      "type": "object",
                      "required": [
                        "cuentaId",
                        "debe",
                        "haber"
                      ],
                      "properties": {
                        "cuentaId": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "debe": {
                          "type": "number",
                          "minimum": 0
                        },
                        "haber": {
                          "type": "number",
                          "minimum": 0
                        },
                        "detalle": {
                          "type": "string"
                        },
                        "orden": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Asiento creado en estado borrador"
          },
          "400": {
            "$ref": "#/components/responses/Invalido"
          },
          "422": {
            "description": "Asiento descuadrado o cuentas no imputables"
          }
        }
      }
    },
    "/contabilidad/asientos/{id}": {
      "get": {
        "tags": [
          "contabilidad"
        ],
        "summary": "Detalle de un asiento con sus líneas",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "emisorId",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Asiento con líneas"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      },
      "patch": {
        "tags": [
          "contabilidad"
        ],
        "summary": "Edita un asiento en estado borrador (regenera líneas si se proveen)",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "fecha": {
                    "type": "string",
                    "format": "date"
                  },
                  "glosa": {
                    "type": "string"
                  },
                  "lineas": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Asiento actualizado"
          },
          "400": {
            "$ref": "#/components/responses/Invalido"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/contabilidad/asientos/{id}/contabilizar": {
      "post": {
        "tags": [
          "contabilidad"
        ],
        "summary": "Contabiliza un asiento (borrador → contabilizado)",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "emisorId",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Asiento contabilizado"
          },
          "400": {
            "$ref": "#/components/responses/Invalido"
          },
          "422": {
            "description": "Asiento descuadrado"
          }
        }
      }
    },
    "/contabilidad/asientos/{id}/anular": {
      "post": {
        "tags": [
          "contabilidad"
        ],
        "summary": "Anula un asiento contabilizado (contabilizado → anulado)",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "emisorId",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Asiento anulado"
          },
          "400": {
            "$ref": "#/components/responses/Invalido"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/contabilidad/libro-mayor": {
      "get": {
        "tags": [
          "contabilidad"
        ],
        "summary": "Libro mayor de una cuenta (movimientos y saldo corrido)",
        "parameters": [
          {
            "in": "query",
            "name": "emisorId",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "cuentaId",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "desde",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "in": "query",
            "name": "hasta",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Movimientos del libro mayor con saldo corrido"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/contabilidad/balance-comprobacion": {
      "get": {
        "tags": [
          "contabilidad"
        ],
        "summary": "Balance de comprobación del emisor en el período",
        "parameters": [
          {
            "in": "query",
            "name": "emisorId",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "desde",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "in": "query",
            "name": "hasta",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Balance de comprobación con totales cuadrados"
          }
        }
      }
    },
    "/emisores/{emisorId}/secuenciales/huecos": {
      "parameters": [
        {
          "$ref": "#/components/parameters/emisorId"
        }
      ],
      "get": {
        "tags": [
          "emisores"
        ],
        "summary": "Auditoría de huecos de secuenciales de una serie",
        "description": "Números de [1..ultimoNumero] sin comprobante, explicados con los eventos REEMITIDO (error 45 del SRI) cuando hay rastro; el resto DESCONOCIDO. Incluye además los comprobantes en estado ≠ AUTORIZADO de la serie. El SRI exige justificar saltos de secuencial.",
        "parameters": [
          {
            "name": "tipo",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "01",
                "03",
                "04",
                "05",
                "06",
                "07"
              ]
            },
            "description": "Tipo de comprobante"
          },
          {
            "name": "establecimiento",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{3}$"
            }
          },
          {
            "name": "puntoEmision",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{3}$"
            }
          },
          {
            "name": "ambiente",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "enum": [
                1,
                2
              ]
            },
            "description": "1=Pruebas, 2=Producción"
          }
        ],
        "responses": {
          "200": {
            "description": "Huecos de la serie con su explicación",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuditoriaHuecos"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/ride-muestra": {
      "get": {
        "tags": [
          "emisores"
        ],
        "summary": "Muestra del RIDE por enlace firmado (sin sesión)",
        "description": "Devuelve el mismo PDF de muestra que `GET /emisores/{id}/ride-muestra`, pero autenticado por el token firmado que emite `GET /emisores/{id}/ride-muestra/enlace` en vez de por Bearer. Existe porque previsualizar tiene que ser una NAVEGACIÓN del navegador: Safari en iPhone no pinta un PDF dentro de un iframe, y una URL que el navegador visita no lleva cabecera de autorización. El token caduca a los 10 minutos y lleva dentro el tenant, que es lo único que decide qué fila se lee. Limitado a 20 peticiones por minuto y por IP.",
        "parameters": [
          {
            "name": "t",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Token firmado del enlace"
          }
        ],
        "responses": {
          "200": {
            "description": "RIDE de muestra",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "El enlace no es válido o caducó"
          }
        }
      }
    },
    "/emisores/{id}/ride-muestra/enlace": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "emisores"
        ],
        "summary": "Enlace firmado y efímero a la muestra del RIDE",
        "description": "Devuelve una ruta relativa (`/api/ride-muestra?t=…`) que abre el PDF de muestra sin cabecera de autorización, para que el navegador pueda navegar a ella. Vale 10 minutos. No genera el PDF: solo firma el enlace.",
        "parameters": [
          {
            "name": "formato",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "oficial",
                "moderno"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Enlace firmado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/emisores/{id}/ride-muestra": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "emisores"
        ],
        "summary": "PDF de muestra del RIDE en una maqueta",
        "description": "Devuelve un RIDE de ejemplo con los datos y el logo REALES del emisor sobre una factura inventada, para poder comparar las maquetas antes de elegir una con PATCH /emisores/{id}. La muestra sale en ambiente de PRUEBAS (marca de agua) y con secuencial 000000000: no es un comprobante y no persiste nada. Limitado a 20 peticiones por minuto.",
        "parameters": [
          {
            "name": "formato",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "oficial",
                "moderno"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "RIDE de muestra",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/emisores/{id}/logo": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "put": {
        "tags": [
          "emisores"
        ],
        "summary": "Subir el logo del emisor (para el RIDE)",
        "description": "Sube un logo PNG o JPG (base64, máx. 1 MB) que se estampa en la cabecera del RIDE de los comprobantes NUEVOS. Reemplaza el logo anterior.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "logoBase64"
                ],
                "properties": {
                  "logoBase64": {
                    "type": "string",
                    "description": "Imagen PNG/JPG codificada en base64"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Logo guardado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ErrorValidacion"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      },
      "get": {
        "tags": [
          "emisores"
        ],
        "summary": "URL prefirmada del logo del emisor",
        "responses": {
          "200": {
            "description": "URL temporal del logo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      },
      "delete": {
        "tags": [
          "emisores"
        ],
        "summary": "Quitar el logo del emisor",
        "responses": {
          "204": {
            "description": "Logo eliminado"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          }
        }
      }
    },
    "/productos/por-codigo/{codigoPrincipal}": {
      "put": {
        "tags": [
          "productos"
        ],
        "summary": "Recordar producto por código (upsert suave)",
        "description": "Crea el producto si el código no existe; si ya existe NO lo modifica (el catálogo del usuario manda sobre nombre y precio) ni revive productos eliminados. Idempotente. Lo usa el MCP para guardar automáticamente los ítems facturados.",
        "parameters": [
          {
            "name": "codigoPrincipal",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 25
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "nombre",
                  "precioUnitario"
                ],
                "properties": {
                  "nombre": {
                    "type": "string",
                    "maxLength": 300
                  },
                  "precioUnitario": {
                    "type": "number",
                    "minimum": 0
                  },
                  "codigoAuxiliar": {
                    "type": "string",
                    "maxLength": 25
                  },
                  "impuestoCodigo": {
                    "type": "string",
                    "description": "Código de impuesto (default '2' = IVA)."
                  },
                  "tarifaCodigo": {
                    "type": "string",
                    "description": "Código de tarifa (default '4' = IVA 15%)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultado del recordatorio",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "creado": {
                      "type": "boolean",
                      "description": "true si el producto se creó en esta llamada."
                    },
                    "producto": {
                      "type": "object",
                      "description": "La fila del producto (ausente si el código pertenece a un producto eliminado)."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/productos/resumen": {
      "get": {
        "tags": [
          "catalogos"
        ],
        "summary": "Resumen del inventario (tiles y chips)",
        "responses": {
          "200": {
            "description": "Resumen",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResumenProductos"
                }
              }
            }
          }
        }
      }
    },
    "/productos/export.csv": {
      "get": {
        "tags": [
          "catalogos"
        ],
        "summary": "Exportar el inventario completo a CSV (incluye archivados)",
        "responses": {
          "200": {
            "description": "CSV",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/productos/importar": {
      "post": {
        "tags": [
          "catalogos"
        ],
        "summary": "Importar productos desde CSV (upsert por código; revive soft-borrados)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "csvBase64"
                ],
                "properties": {
                  "csvBase64": {
                    "type": "string",
                    "description": "CSV codificado en base64 (máx. 2000 filas de datos)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResultadoImportacionProductos"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Lista los webhooks de la cuenta",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Webhooks configurados",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Webhook"
                  }
                }
              }
            }
          }
        },
        "description": "Gestionable por owner/admin (sesión de usuario) o por una API key con el rol `webhooks` (opt-in al crear la key; el rol default `emisor` no basta)."
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Registra un webhook",
        "description": "Alternativa al polling de `GET /comprobantes/{id}`. El `secret` se devuelve solo aquí. Máximo 5 por cuenta. Gestionable por owner/admin (sesión de usuario) o por una API key con el rol `webhooks` (opt-in al crear la key; el rol default `emisor` no basta).",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "eventos": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Por defecto, todos."
                  },
                  "descripcion": {
                    "type": "string",
                    "maxLength": 120
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creado (incluye el secreto)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookConSecreto"
                }
              }
            }
          },
          "400": {
            "description": "URL inválida o tope de 5 alcanzado"
          },
          "409": {
            "description": "Ya existe un webhook con esa URL"
          }
        }
      }
    },
    "/webhooks/entregas": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Entregas recientes",
        "description": "Para diagnosticar por qué no llegó un evento. Gestionable por owner/admin (sesión de usuario) o por una API key con el rol `webhooks` (opt-in al crear la key; el rol default `emisor` no basta).",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Entregas",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WebhookEntrega"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/{id}": {
      "patch": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Activa o pausa un webhook",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "activo"
                ],
                "properties": {
                  "activo": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Actualizado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "404": {
            "description": "No encontrado"
          }
        },
        "description": "Gestionable por owner/admin (sesión de usuario) o por una API key con el rol `webhooks` (opt-in al crear la key; el rol default `emisor` no basta)."
      },
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Elimina un webhook",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Eliminado"
          },
          "404": {
            "description": "No encontrado"
          }
        },
        "description": "Gestionable por owner/admin (sesión de usuario) o por una API key con el rol `webhooks` (opt-in al crear la key; el rol default `emisor` no basta)."
      }
    },
    "/webhooks/{id}/rotar-secreto": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Rota el secreto de firma",
        "description": "El secreto anterior deja de firmar de inmediato. Gestionable por owner/admin (sesión de usuario) o por una API key con el rol `webhooks` (opt-in al crear la key; el rol default `emisor` no basta).",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Nuevo secreto",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookConSecreto"
                }
              }
            }
          },
          "404": {
            "description": "No encontrado"
          }
        }
      }
    },
    "/comprobantes/lote": {
      "post": {
        "tags": [
          "Comprobantes"
        ],
        "summary": "Emite un lote de facturas (hasta 100)",
        "description": "Semántica **parcial, no atómica**: cada factura reserva su propio secuencial y reporta su resultado; un fallo puntual no aborta el lote. Un error de CUENTA (402 cupo agotado / 403 suspendida) sí lo detiene y el resto queda `NO_INTENTADA`. Reintento seguro: repite el request con el mismo header `Idempotency-Key` y las que ya entraron no se duplican. Límite propio: 10 lotes/min por cuenta. Body hasta 2 MB.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 190
            },
            "description": "Idempotencia del lote entero: cada factura sin clave propia usa `header:indice`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "emisorId",
                  "certificadoId",
                  "establecimiento",
                  "puntoEmision",
                  "facturas"
                ],
                "properties": {
                  "emisorId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "certificadoId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "establecimiento": {
                    "type": "string",
                    "pattern": "^\\d{3}$"
                  },
                  "puntoEmision": {
                    "type": "string",
                    "pattern": "^\\d{3}$"
                  },
                  "facturas": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "items": {
                      "$ref": "#/components/schemas/LoteFactura"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Lote procesado (mira el estado POR elemento: puede haber errores parciales)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResultadoLote"
                }
              }
            }
          },
          "400": {
            "description": "Lote vacío, más de 100 facturas o header de idempotencia demasiado largo"
          },
          "429": {
            "description": "Límite de lotes por minuto superado"
          }
        }
      }
    },
    "/cortes": {
      "get": {
        "tags": [
          "Planes y pagos"
        ],
        "summary": "Cortes de excedente de la cuenta",
        "description": "Transparencia del cobro por volumen: qué se midió, con qué cupo y cuánto suma el excedente. El corte provisional del día 1 puede crecer hasta la consolidación del día 3 (autorizados tardíos de contingencia).",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Últimos 24 cortes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Corte"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/auth/mfa/verificar": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Completar el login con el segundo factor",
        "description": "Canjea el `desafioToken` de POST /auth/login por los tokens de sesión. Acepta un TOTP de 6 dígitos o un código de recuperación, que se consume al usarse. Sujeto al mismo lockout por cuenta que el login.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "desafioToken",
                  "codigo"
                ],
                "properties": {
                  "desafioToken": {
                    "type": "string"
                  },
                  "codigo": {
                    "type": "string",
                    "description": "TOTP de 6 dígitos o código de recuperación XXXX-XXXX"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Par de tokens",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tokens"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          }
        }
      }
    },
    "/mfa": {
      "get": {
        "tags": [
          "cuenta"
        ],
        "summary": "Estado del segundo factor de tu usuario",
        "responses": {
          "200": {
            "description": "Estado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EstadoMfa"
                }
              }
            }
          }
        }
      }
    },
    "/mfa/iniciar": {
      "post": {
        "tags": [
          "cuenta"
        ],
        "summary": "Iniciar el enrolamiento del segundo factor",
        "description": "Genera el secreto TOTP y devuelve el QR. NO activa el MFA: hasta confirmar un código, el login sigue siendo solo con contraseña.",
        "responses": {
          "201": {
            "description": "Secreto y QR",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "secreto": {
                      "type": "string",
                      "description": "Base32, por si el QR no se puede escanear"
                    },
                    "otpauthUri": {
                      "type": "string"
                    },
                    "qrDataUri": {
                      "type": "string",
                      "description": "PNG en data-URI"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/mfa/confirmar": {
      "post": {
        "tags": [
          "cuenta"
        ],
        "summary": "Confirmar y activar el segundo factor",
        "description": "Verifica un código del autenticador y activa el MFA. Devuelve los códigos de recuperación, que NO se vuelven a mostrar.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "codigo"
                ],
                "properties": {
                  "codigo": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Códigos de recuperación",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "codigosRecuperacion": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          }
        }
      }
    },
    "/mfa/desactivar": {
      "post": {
        "tags": [
          "cuenta"
        ],
        "summary": "Desactivar el segundo factor",
        "description": "Exige un código válido además de la sesión: si bastara la sesión, quien robe la contraseña podría quitar el MFA.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "codigo"
                ],
                "properties": {
                  "codigo": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Desactivado"
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          }
        }
      }
    },
    "/pagos/cotizar": {
      "get": {
        "tags": [
          "pagos"
        ],
        "summary": "Cotización de volumen (pública)",
        "description": "Qué cuesta emitir N comprobantes al mes en cada plan, con el excedente por tramos ya aplicado. **Pública**: no requiere token. Solo hace aritmética sobre el catálogo de planes, sin tocar datos de ninguna cuenta. La calculadora de volumen de la web consume este endpoint en vez de replicar la matemática del precio.\n\nEl plan `free` aparece como no viable en cuanto se supera su cupo: corta en seco (402), no acumula excedente. `excedenteVigente:false` indica que hoy el excedente se mide pero todavía no se factura.",
        "security": [],
        "parameters": [
          {
            "name": "comprobantes",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 1000000,
              "example": 3000
            },
            "description": "Comprobantes de producción al mes."
          }
        ],
        "responses": {
          "200": {
            "description": "Cotización por plan.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "comprobantesAlMes": {
                      "type": "integer"
                    },
                    "recomendado": {
                      "type": "string",
                      "enum": [
                        "free",
                        "emprendedor",
                        "pyme",
                        "empresa"
                      ]
                    },
                    "sugerirMayorista": {
                      "type": "boolean",
                      "description": "true desde 3.000/mes: conviene hablar de tarifa mayorista."
                    },
                    "excedenteVigente": {
                      "type": "boolean",
                      "description": "false mientras el excedente se mida pero no se cobre."
                    },
                    "planes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "plan": {
                            "type": "string"
                          },
                          "cupoMensual": {
                            "type": "integer",
                            "nullable": true
                          },
                          "precioAnualUsd": {
                            "type": "number",
                            "nullable": true
                          },
                          "excedenteMensual": {
                            "type": "integer"
                          },
                          "excedenteMensualCentavos": {
                            "type": "integer"
                          },
                          "totalAnualCentavos": {
                            "type": "integer",
                            "description": "Plan + 12 meses de excedente, sin IVA."
                          },
                          "costoPorComprobante": {
                            "type": "string",
                            "description": "Centavos por comprobante, 4 decimales."
                          },
                          "viable": {
                            "type": "boolean"
                          },
                          "motivoNoViable": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`comprobantes` ausente o fuera de rango."
          }
        }
      }
    },
    "/asesor/declaraciones": {
      "post": {
        "tags": [
          "asesor"
        ],
        "summary": "Marca (o desmarca) una declaración como presentada",
        "description": "Anota que la declaración de una empresa y un período ya se presentó. La anotación silencia ese vencimiento en `/asesor/cumplimiento` y en el digest de alertas, y ordena la cartera. Idempotente. El permiso sale de la membresía del usuario en la empresa indicada (no del `X-Cuenta` de la petición); `lector` no puede marcar.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tenantId",
                  "codigo",
                  "periodo",
                  "hecha"
                ],
                "properties": {
                  "tenantId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Empresa de la cartera"
                  },
                  "codigo": {
                    "type": "string",
                    "description": "Código del catálogo de obligaciones (IVA_MENSUAL, RETENCIONES_MENSUAL, ATS…)"
                  },
                  "periodo": {
                    "type": "string",
                    "description": "'2026-07' (mensual) | '2026-S1' (semestral) | '2026' (anual)"
                  },
                  "fechaLimite": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date",
                    "description": "Vencimiento que tenía al marcarla (traza)"
                  },
                  "hecha": {
                    "type": "boolean",
                    "description": "true marca, false desmarca"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Estado de la declaración",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tenantId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "codigo": {
                      "type": "string"
                    },
                    "periodo": {
                      "type": "string"
                    },
                    "hecha": {
                      "type": "boolean"
                    },
                    "marcadaAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Código fuera del catálogo o período mal formado"
          },
          "403": {
            "description": "Sin membresía en esa empresa, rol `lector` o API key"
          }
        }
      }
    },
    "/tenants/{tenantId}/archivar": {
      "post": {
        "tags": [
          "cuenta"
        ],
        "summary": "Saca una empresa de mi cartera (reversible)",
        "description": "Marca MI membresía como archivada: la empresa sale de mi cartera, de mi selector y de la empresa que elige el login, sin tocar sus datos ni a los demás miembros. No se puede archivar la empresa en la que estás (409).",
        "parameters": [
          {
            "name": "tenantId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Estado de archivo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tenantId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "archivada": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "El usuario no administra esa empresa"
          },
          "409": {
            "description": "Es la empresa de la sesión actual (solo al archivar)"
          }
        }
      }
    },
    "/tenants/{tenantId}/desarchivar": {
      "post": {
        "tags": [
          "cuenta"
        ],
        "summary": "Devuelve a mi cartera una empresa archivada",
        "description": "Limpia la marca de archivada de mi membresía.",
        "parameters": [
          {
            "name": "tenantId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Estado de archivo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tenantId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "archivada": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "El usuario no administra esa empresa"
          },
          "409": {
            "description": "Es la empresa de la sesión actual (solo al archivar)"
          }
        }
      }
    },
    "/tenants/{tenantId}": {
      "delete": {
        "tags": [
          "cuenta"
        ],
        "summary": "Baja definitiva de una empresa que nunca emitió",
        "description": "Borra la empresa y todo lo suyo, incluidos sus objetos en el almacenamiento. Solo cabe si NUNCA emitió comprobantes: con emisiones manda la conservación de 7 años y la respuesta es 409 pidiendo archivarla. También responde 409 si es la empresa de la sesión, la única del usuario, patrocina a otras, tiene suscripción viva o comisiones de partner. Rol owner en esa empresa; 403 con API key o token OAuth.\n\nDevuelve la CONSTANCIA de eliminación (resolución SPDP-SPD-2025-0030-R, arts. 20 y 24) y su CSV: es el único momento en que se puede entregar, porque después no queda empresa desde la que descargarla. Guarda el CSV. Si el almacenamiento no responde, la baja se aborta con 503 y no se borra nada: reintentar es seguro.",
        "parameters": [
          {
            "name": "tenantId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Empresa borrada, con su constancia de eliminación",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "constancia": {
                      "$ref": "#/components/schemas/ConstanciaEliminacion"
                    },
                    "csv": {
                      "type": "string",
                      "description": "El documento acreditativo ya renderizado, listo para guardar o adjuntar."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "No es owner de esa empresa, o API key / token OAuth"
          },
          "404": {
            "description": "El usuario no administra esa empresa"
          },
          "409": {
            "description": "Alguna guarda lo impide (ya emitió, es la actual, patrocina, cobra…)"
          },
          "503": {
            "description": "El almacenamiento no respondió; no se borró nada"
          }
        }
      }
    },
    "/tenants/current/emancipar": {
      "post": {
        "tags": [
          "cuenta"
        ],
        "summary": "Suelta el patrocinio de la empresa activa",
        "description": "Rompe el vinculo `patrocinador_tenant_id`: la empresa deja de emitir con el plan de otra cuenta, deja de sumar a su `consumo_pool` y deja de figurar en su cupo repartido. Contratar plan propio da independencia de cupo mientras el plan siga vigente; esto la da de forma definitiva (un plan propio vencido devolvia la empresa al pool). Idempotente: si ya estaba suelta responde 200 con `cambio: false`. Rol owner en la empresa activa.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "aceptaPerderCupoCompartido": {
                    "type": "boolean",
                    "description": "Obligatorio si el plan propio efectivo es free: al soltar el patrocinio la empresa pasa a su propio cupo (10/mes, tipos 01 y 04) en el acto."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Empresa emancipada (o ya lo estaba)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tenantId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "patrocinada": {
                      "type": "boolean",
                      "description": "false siempre tras la operacion"
                    },
                    "cambio": {
                      "type": "boolean",
                      "description": "false = ya estaba suelta"
                    },
                    "usoPlan": {
                      "$ref": "#/components/schemas/UsoPlan"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Su plan propio es free y falta `aceptaPerderCupoCompartido`"
          },
          "403": {
            "description": "Sin rol owner en la empresa activa, o token de OAuth/MCP"
          },
          "404": {
            "description": "Tenant no encontrado"
          }
        }
      }
    },
    "/tenants/current/baja": {
      "post": {
        "tags": [
          "cuenta"
        ],
        "summary": "Fase 1 de la baja: pide la baja y devuelve el paquete de devolución",
        "description": "La salida de la empresa QUE YA EMITIÓ, a la que `DELETE /tenants/{tenantId}` responde 409 porque sus comprobantes no se pueden borrar en 7 años. Esta fase NO bloquea ni apaga nada: marca la solicitud y entrega la DEVOLUCIÓN del art. 24 de la resolución SPDP-SPD-2025-0030-R — los CSV COMPLETOS (sin el tope de 10 000 del export normal) de clientes, productos y comprobantes, estos últimos separados por ambiente, más el manifiesto con una URL prefirmada de descarga por cada XML y cada RIDE.\n\nLas URLs caducan a las 24 horas y el manifiesto se regenera en `GET /tenants/current/baja/manifiesto`, también con la empresa ya archivada. Repetir la llamada regenera el paquete y NO reinicia el plazo del art. 24: `bajaSolicitadaAt` no se mueve. Quedan fuera del paquete los comprobantes bloqueados por una supresión de un comprador: esa puerta no se abre ni para una devolución.\n\nRol owner; queda en la auditoría de accesos.",
        "responses": {
          "200": {
            "description": "Paquete de devolución",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaqueteDevolucion"
                }
              }
            }
          },
          "403": {
            "description": "Sin rol owner, o API key / token OAuth"
          },
          "409": {
            "description": "La empresa ya está archivada"
          }
        }
      }
    },
    "/tenants/current/baja/manifiesto": {
      "get": {
        "tags": [
          "cuenta"
        ],
        "summary": "Regenerar una página del manifiesto de devolución",
        "description": "URLs prefirmadas nuevas para el XML y el RIDE de cada comprobante, en páginas de 1000. Sigue funcionando con la empresa ARCHIVADA, y esa es la mitad que distingue el archivo de un borrado: el titular conserva 7 años de obligación ante el SRI y su acceso a sus propios documentos no puede depender de haber acertado a bajarlos dentro de la ventana de 24 horas. Exige haber pedido la baja. Rol owner; queda en la auditoría de accesos.",
        "parameters": [
          {
            "name": "pagina",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Una página del manifiesto",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManifiestoDevolucion"
                }
              }
            }
          },
          "403": {
            "description": "Sin rol owner, o API key / token OAuth"
          },
          "409": {
            "description": "Esta empresa no ha pedido la baja"
          }
        }
      }
    },
    "/tenants/current/baja/confirmar": {
      "post": {
        "tags": [
          "cuenta"
        ],
        "summary": "Fase 2 de la baja: archiva la empresa (no borra nada)",
        "description": "La empresa sale de operación (`estado = 'archivado'`, `archivado_at`): no emite, no aparece en los listados que filtran por activo y sus comprobantes y fichas de clientes quedan BLOQUEADOS en masa (arts. 12-14: fuera de listados, exports, tools MCP y reenvíos). **No se borra nada**: el borrado físico queda diferido al cumplimiento de los 7 años de retención tributaria.\n\nSe admite por los dos caminos del art. 24: `confirmoDevolucion: true` o que hayan pasado 5 días desde la solicitud (409 antes del plazo y sin el flag). Ninguna empresa se archiva sola: no hay proceso que recorra las bajas pedidas, así que el plazo solo decide si además hace falta la declaración expresa, nunca sustituye a esta llamada.\n\nTambién responde 409 si no se pidió la baja, si no se generó el paquete, si la empresa patrocina a otras o si tiene una suscripción viva. Devuelve la constancia de alcance `archivo_empresa`, que declara lo conservado con su amparo y el diferimiento del borrado. Rol owner; queda en la auditoría de accesos.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirmoDevolucion": {
                    "type": "boolean",
                    "description": "Confirmación expresa de que descargaste y conservas tu paquete de devolución. Obligatoria mientras no se cumplan los 5 días del plazo de devolución."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Empresa archivada, con su constancia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "archivadoAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "comprobantesBloqueados": {
                      "type": "integer",
                      "description": "Los que se bloquearon AHORA; los de una supresión anterior conservan su fecha."
                    },
                    "clientesBloqueados": {
                      "type": "integer"
                    },
                    "constancia": {
                      "$ref": "#/components/schemas/ConstanciaEliminacion"
                    },
                    "csv": {
                      "type": "string",
                      "description": "El documento acreditativo ya renderizado."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Sin rol owner, o API key / token OAuth"
          },
          "409": {
            "description": "Falta la confirmación expresa antes del plazo, falta la devolución, patrocina a otras o tiene suscripción viva"
          }
        }
      }
    },
    "/tenants/patrocinadas/{tenantId}": {
      "delete": {
        "tags": [
          "cuenta"
        ],
        "summary": "Deja de patrocinar a una empresa de la cuenta",
        "description": "El mismo corte visto desde el patrocinador. Esa empresa cae a su plan propio EN EL ACTO (normalmente free, 10/mes) y sale del cupo repartido. Hasta ahora la unica salida era borrar el tenant patrocinado, que la conservacion de 7 anios bloquea en cuanto emitio una factura. Rol owner en la cuenta patrocinadora.",
        "parameters": [
          {
            "name": "tenantId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Patrocinio roto",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tenantId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "patrocinada": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Sin rol owner, o token de OAuth/MCP"
          },
          "404": {
            "description": "Esa empresa no esta patrocinada por esta cuenta"
          }
        }
      }
    },
    "/correo/baja": {
      "get": {
        "tags": [
          "correo"
        ],
        "summary": "Página de confirmación de baja de avisos (pública)",
        "description": "Lo que se abre al pulsar el enlace de baja del pie de un aviso. **No da de baja**: enseña una página con un botón que hace el POST. Los escáneres de enlaces y los prefetch de los clientes de correo abren las URLs sin que nadie las pulse, y un GET que ejecutara la baja daría de baja a quien no tocó nada.\n\nPública: llega sin sesión por definición; quien identifica al usuario es el token.",
        "security": [],
        "parameters": [
          {
            "name": "t",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Token firmado que identifica al usuario y la categoría. Lo genera `CorreoService.urlBajaUnClic()` y viaja en la cabecera `List-Unsubscribe` del propio correo."
          }
        ],
        "responses": {
          "200": {
            "description": "Página de confirmación, o aviso de que el enlace no es válido"
          }
        }
      },
      "post": {
        "tags": [
          "correo"
        ],
        "summary": "Baja de una categoría de avisos en un clic (pública, RFC 8058)",
        "description": "Apaga la categoría del token en `alertas_preferencias` (upsert: la fila solo existe si el usuario tocó sus ajustes).\n\nEs el destino del **un clic** de Gmail: Brevo añade `List-Unsubscribe-Post: List-Unsubscribe=One-Click` a todo lo que sale por su API, y por RFC 8058 eso obliga a que un POST a la URL de `List-Unsubscribe` dé de baja sin más interacción. Antes la cabecera apuntaba al panel, que respondía 405: se anunciaba la baja en un clic y no se honraba, que es justo lo que Gmail y Yahoo penalizan desde 2024.\n\nResponde 200 también con un token inválido: el cliente de correo enseña el resultado al usuario y un 4xx solo diría «algo falló» sin que pueda hacer nada. Lo que no ocurre nunca es dar de baja a quien no toca, y de eso se encarga la firma.\n\n`transaccional` no se puede dar de baja por aquí: no es una categoría firmable.",
        "security": [],
        "parameters": [
          {
            "name": "t",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Token firmado que identifica al usuario y la categoría. Lo genera `CorreoService.urlBajaUnClic()` y viaja en la cabecera `List-Unsubscribe` del propio correo."
          }
        ],
        "responses": {
          "200": {
            "description": "Baja aplicada, o aviso de que el enlace no es válido. Siempre 200, nunca el 201 por defecto de Nest: esto no crea nada y el cliente de correo que hace el un-clic espera un OK a secas."
          }
        }
      }
    },
    "/auth/partner/login": {
      "post": {
        "tags": [
          "partner"
        ],
        "security": [],
        "summary": "Login del portal de partners OEM (correo y contrasena)",
        "description": "Realm propio (personas del integrador). Devuelve tokens, o un desafio si la persona tiene segundo factor activo. La partner key `cdop_` sigue siendo la credencial maquina-a-maquina y no se usa aqui.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "password"
                ],
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tokens de sesion, o desafio MFA",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Tokens"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "mfaRequerido": {
                          "type": "boolean"
                        },
                        "desafioToken": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Credenciales invalidas, cuenta suspendida o partner no activo (mismo error para los tres)"
          }
        }
      }
    },
    "/auth/partner/mfa/verificar": {
      "post": {
        "tags": [
          "partner"
        ],
        "security": [],
        "summary": "Completar el login del portal con el segundo factor",
        "description": "Canjea el `desafioToken` de POST /auth/partner/login por los tokens de sesion. Acepta un TOTP de 6 digitos o un codigo de recuperacion, que se consume al usarse. Sujeto al mismo lockout por cuenta que el login.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "desafioToken",
                  "codigo"
                ],
                "properties": {
                  "desafioToken": {
                    "type": "string"
                  },
                  "codigo": {
                    "type": "string",
                    "description": "TOTP de 6 digitos o codigo de recuperacion XXXX-XXXX"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Par de tokens",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tokens"
                }
              }
            }
          },
          "401": {
            "description": "Desafio invalido, expirado, de otro realm, sesion revocada, codigo incorrecto o demasiados intentos"
          }
        }
      }
    },
    "/auth/partner/refresh": {
      "post": {
        "tags": [
          "partner"
        ],
        "security": [],
        "summary": "Rota los tokens del portal de partners",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "refreshToken"
                ],
                "properties": {
                  "refreshToken": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Par nuevo",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tokens"
                }
              }
            }
          },
          "401": {
            "description": "Refresh invalido, de otro realm, o sesion revocada"
          }
        }
      }
    },
    "/auth/partner/logout": {
      "post": {
        "tags": [
          "partner"
        ],
        "security": [],
        "summary": "Cierra la sesion e invalida TODOS los tokens vivos de esa persona",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "refreshToken"
                ],
                "properties": {
                  "refreshToken": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Sesion cerrada (idempotente)"
          }
        }
      }
    },
    "/auth/partner/forgot-password": {
      "post": {
        "tags": [
          "partner"
        ],
        "security": [],
        "summary": "Envia el enlace para recuperar la contrasena",
        "description": "Responde 204 exista o no la cuenta: no delata que correos son de partners.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Aceptado (siempre)"
          }
        }
      }
    },
    "/auth/partner/reset-password": {
      "post": {
        "tags": [
          "partner"
        ],
        "security": [],
        "summary": "Fija la contrasena con el token del correo",
        "description": "Sirve tanto para la invitacion inicial como para la recuperacion. El enlace es de un solo uso.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token",
                  "nueva"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "nueva": {
                    "type": "string",
                    "minLength": 8
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Contrasena fijada; se cierran las sesiones previas"
          },
          "400": {
            "description": "Contrasena de menos de 8 caracteres"
          },
          "401": {
            "description": "Enlace invalido, caducado, ya usado o de otro realm"
          }
        }
      }
    },
    "/partner/mi-cuenta": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Perfil de la sesion abierta en el portal",
        "responses": {
          "200": {
            "description": "Perfil",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "nullable": true
                    },
                    "email": {
                      "type": "string",
                      "nullable": true
                    },
                    "nombre": {
                      "type": "string",
                      "nullable": true
                    },
                    "rol": {
                      "type": "string",
                      "nullable": true
                    },
                    "partnerId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "razonSocial": {
                      "type": "string",
                      "nullable": true
                    },
                    "mfaActivo": {
                      "type": "boolean"
                    },
                    "esKey": {
                      "type": "boolean",
                      "description": "true si la sesion es una partner key (M2M), sin persona detras"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/partner/mi-cuenta/password": {
      "post": {
        "tags": [
          "partner"
        ],
        "summary": "Cambia la propia contrasena (cierra las demas sesiones)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "actual",
                  "nueva"
                ],
                "properties": {
                  "actual": {
                    "type": "string"
                  },
                  "nueva": {
                    "type": "string",
                    "minLength": 8
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cambiada"
          },
          "401": {
            "description": "La contrasena actual no es correcta"
          },
          "403": {
            "description": "La sesion es una partner key, no una persona"
          }
        }
      }
    },
    "/partner/usuarios": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Equipo del partner",
        "responses": {
          "200": {
            "description": "Personas con acceso al portal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "email": {
                        "type": "string"
                      },
                      "nombre": {
                        "type": "string"
                      },
                      "rol": {
                        "type": "string",
                        "enum": [
                          "admin",
                          "lector"
                        ]
                      },
                      "estado": {
                        "type": "string",
                        "enum": [
                          "activo",
                          "suspendido"
                        ]
                      },
                      "activo": {
                        "type": "boolean",
                        "description": "false = invitacion pendiente (nunca fijo su contrasena)"
                      },
                      "ultimoLoginAt": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "createdAt": {
                        "type": "string",
                        "format": "date-time"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "partner"
        ],
        "summary": "Invita a una persona al portal",
        "description": "Nace sin contrasena utilizable y recibe un enlace para fijar la suya. Requiere rol admin.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "nombre"
                ],
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "nombre": {
                    "type": "string"
                  },
                  "rol": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "lector"
                    ],
                    "default": "lector"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Invitada",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "email": {
                      "type": "string"
                    },
                    "nombre": {
                      "type": "string"
                    },
                    "rol": {
                      "type": "string",
                      "enum": [
                        "admin",
                        "lector"
                      ]
                    },
                    "estado": {
                      "type": "string",
                      "enum": [
                        "activo",
                        "suspendido"
                      ]
                    },
                    "activo": {
                      "type": "boolean",
                      "description": "false = invitacion pendiente (nunca fijo su contrasena)"
                    },
                    "ultimoLoginAt": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Ese correo ya tiene acceso"
          },
          "403": {
            "description": "Tu rol es de solo lectura"
          }
        }
      }
    },
    "/partner/usuarios/{id}": {
      "patch": {
        "tags": [
          "partner"
        ],
        "summary": "Cambia el rol o el estado de una persona",
        "description": "Suspender corta sus sesiones al instante. No se puede dejar al partner sin ningun admin activo, ni suspenderse uno mismo.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [],
                "properties": {
                  "rol": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "lector"
                    ]
                  },
                  "estado": {
                    "type": "string",
                    "enum": [
                      "activo",
                      "suspendido"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Actualizada",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "email": {
                      "type": "string"
                    },
                    "nombre": {
                      "type": "string"
                    },
                    "rol": {
                      "type": "string",
                      "enum": [
                        "admin",
                        "lector"
                      ]
                    },
                    "estado": {
                      "type": "string",
                      "enum": [
                        "activo",
                        "suspendido"
                      ]
                    },
                    "activo": {
                      "type": "boolean",
                      "description": "false = invitacion pendiente (nunca fijo su contrasena)"
                    },
                    "ultimoLoginAt": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Dejaria al partner sin administrador activo, o es tu propio acceso"
          },
          "404": {
            "description": "No existe en este partner"
          }
        }
      }
    },
    "/partner/usuarios/{id}/reinvitar": {
      "post": {
        "tags": [
          "partner"
        ],
        "summary": "Reenvia la invitacion a quien aun no fijo su contrasena",
        "description": "409 si esa persona ya la fijo: entonces debe usar 'olvide mi contrasena', que va a su propio correo.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reenviada"
          },
          "409": {
            "description": "Ya fijo su contrasena, o su acceso esta suspendido"
          }
        }
      }
    },
    "/partner/mfa": {
      "get": {
        "tags": [
          "partner"
        ],
        "summary": "Estado del segundo factor de tu cuenta del portal",
        "description": "Requiere la sesion del portal: la partner key es una credencial de maquina y no tiene segundo factor (403).",
        "responses": {
          "200": {
            "description": "Estado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EstadoMfa"
                }
              }
            }
          },
          "403": {
            "description": "Sesion de partner key, no de persona"
          }
        }
      }
    },
    "/partner/mfa/iniciar": {
      "post": {
        "tags": [
          "partner"
        ],
        "summary": "Iniciar el enrolamiento del segundo factor",
        "description": "Genera el secreto TOTP y devuelve el QR. NO activa el MFA: hasta confirmar un codigo, el login sigue siendo solo con contrasena. Tambien lo puede usar un rol `lector` sobre si mismo.",
        "responses": {
          "201": {
            "description": "Secreto y QR",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "secreto": {
                      "type": "string",
                      "description": "Base32, por si el QR no se puede escanear"
                    },
                    "otpauthUri": {
                      "type": "string"
                    },
                    "qrDataUri": {
                      "type": "string",
                      "description": "PNG en data-URI"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Sesion de partner key, no de persona"
          }
        }
      }
    },
    "/partner/mfa/confirmar": {
      "post": {
        "tags": [
          "partner"
        ],
        "summary": "Confirmar y activar el segundo factor",
        "description": "Verifica un codigo del autenticador y activa el MFA. Devuelve los codigos de recuperacion, que NO se vuelven a mostrar.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "codigo"
                ],
                "properties": {
                  "codigo": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Codigos de recuperacion",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "codigosRecuperacion": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          },
          "403": {
            "description": "Sesion de partner key, no de persona"
          }
        }
      }
    },
    "/partner/mfa/desactivar": {
      "post": {
        "tags": [
          "partner"
        ],
        "summary": "Desactivar el segundo factor",
        "description": "Exige un codigo valido ademas de la sesion: si bastara la sesion, quien robe la contrasena podria quitar el MFA. Solo actua sobre tu propia cuenta, nunca sobre la de un companero.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "codigo"
                ],
                "properties": {
                  "codigo": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Desactivado"
          },
          "401": {
            "$ref": "#/components/responses/NoAutorizado"
          },
          "403": {
            "description": "Sesion de partner key, no de persona"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key `cdo_...` (Configuración → API), partner key OEM `cdop_...` (rutas `/partner/*`, ver OEM-INTEGRACION.md) o accessToken JWT de /auth/login"
      }
    },
    "parameters": {
      "id": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "emisorId": {
        "name": "emisorId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "membershipId": {
        "name": "membershipId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "q": {
        "name": "q",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "Búsqueda por texto"
      },
      "limit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200
        },
        "description": "Máx. 200. Envíalo siempre desde integraciones."
      },
      "idempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 200
        },
        "description": "Clave de idempotencia (use un UUID por emisión): un reintento con la misma clave devuelve el comprobante original (`reutilizado: true`) en vez de crear otro — no quema secuencial ni duplica la factura. El alcance es por (emisor, tipo de comprobante): la misma clave usada para otro RUC o tipo del mismo tenant no colisiona."
      }
    },
    "responses": {
      "ErrorValidacion": {
        "description": "Cuerpo inválido o validación de negocio (previene errores SRI 62/52/65/69/39)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Invalido": {
        "description": "Cuerpo inválido o regla del módulo incumplida: UUID, fecha o enum mal formados, cuenta inactiva o no imputable, asiento sin líneas, o estado que no admite la operación",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NoAutorizado": {
        "description": "Token/key ausente, inválido o expirado",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RolInsuficiente": {
        "description": "Rol insuficiente o acceso denegado",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NoEncontrado": {
        "description": "Recurso inexistente (o de otra cuenta — aislamiento multi-tenant)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflicto": {
        "description": "Conflicto de unicidad",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CupoAgotado": {
        "description": "Cupo mensual del plan agotado (`upgrade: true`)",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Error"
                },
                {
                  "type": "object",
                  "properties": {
                    "upgrade": {
                      "type": "boolean"
                    }
                  }
                }
              ]
            }
          }
        }
      },
      "RateLimit": {
        "description": "Demasiadas peticiones (300/min global, 60/min emisión)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Emitido": {
        "description": "Encolado para firma y transmisión al SRI",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ResultadoEmision"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Shape único de error de la API",
        "required": [
          "mensaje",
          "problemas"
        ],
        "properties": {
          "statusCode": {
            "type": "integer"
          },
          "mensaje": {
            "type": "string"
          },
          "message": {
            "type": "string",
            "description": "Espejo de `mensaje` (compatibilidad)"
          },
          "problemas": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "campo": {
                  "type": "string"
                },
                "mensaje": {
                  "type": "string"
                },
                "codigo": {
                  "type": "string"
                },
                "severidad": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Tokens": {
        "type": "object",
        "properties": {
          "accessToken": {
            "type": "string"
          },
          "refreshToken": {
            "type": "string"
          }
        }
      },
      "Rol": {
        "type": "string",
        "enum": [
          "owner",
          "admin",
          "emisor",
          "contador",
          "lector"
        ]
      },
      "TipoIdentificacion": {
        "type": "string",
        "enum": [
          "04",
          "05",
          "06",
          "07",
          "08",
          "09"
        ],
        "description": "04=RUC, 05=cédula, 06=pasaporte, 07=consumidor final, 08=id. exterior, 09=placa"
      },
      "EstadoComprobante": {
        "type": "string",
        "enum": [
          "BORRADOR",
          "FIRMADO",
          "ENVIADO",
          "AUTORIZADO",
          "DEVUELTA",
          "RECHAZADO",
          "CONTINGENCIA",
          "ANULADO"
        ]
      },
      "Tenant": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string"
          },
          "nombre": {
            "type": "string"
          },
          "plan": {
            "type": "string"
          },
          "planExpiraAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "ambienteActivo": {
            "type": "integer",
            "enum": [
              1,
              2
            ]
          },
          "perfilEmision": {
            "type": "string",
            "enum": [
              "comercio",
              "profesional"
            ],
            "nullable": true,
            "description": "null = todavia sin elegir; la primera emision lo pregunta."
          }
        }
      },
      "UsoPlan": {
        "type": "object",
        "properties": {
          "regimen": {
            "type": "string",
            "enum": [
              "propio",
              "pool"
            ],
            "description": "Que gobierna hoy la emision de esta empresa."
          },
          "plan": {
            "type": "string"
          },
          "limiteComprobantes": {
            "type": [
              "integer",
              "null"
            ]
          },
          "usadosEsteMes": {
            "type": "integer"
          },
          "limiteUsuarios": {
            "type": [
              "integer",
              "null"
            ]
          },
          "usuarios": {
            "type": "integer"
          },
          "planExpiraAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "tiposPermitidos": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "usadosPruebasEsteMes": {
            "type": "integer",
            "description": "Emitidos en Pruebas este mes; no consumen cupo (tope blando aparte)."
          },
          "excedenteEsteMes": {
            "type": "integer",
            "description": "Comprobantes por encima del cupo incluido. 0 si el plan propio no se paga (free corta duro; el cobro por pool no existe)."
          },
          "excedenteCentavos": {
            "type": "integer",
            "description": "Costo del excedente con la tarifa por tramos vigente. Se mide, todavia no se cobra."
          },
          "modulosPermitidos": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Modulos del plan mas los concedidos a la cuenta (`tenants.modulos_extra`)."
          },
          "pool": {
            "type": "object",
            "description": "Presente si ESTA empresa consume del cupo compartido de su patrocinador.",
            "properties": {
              "patrocinadorTenantId": {
                "type": "string",
                "format": "uuid"
              },
              "patrocinadorNombre": {
                "type": "string"
              },
              "plan": {
                "type": "string",
                "description": "Plan que RIGE la emision de esta empresa (el del patrocinador)."
              },
              "usados": {
                "type": "integer",
                "description": "`consumo_pool` del mes: lo gastado por todo el pool."
              },
              "limite": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "corteDuro": {
                "type": "boolean",
                "description": "El plan que rige no se paga: al llegar al cupo se corta en seco."
              }
            }
          },
          "poolPropio": {
            "type": "object",
            "description": "Presente si ESTA empresa patrocina a otras Y quien pregunta es owner/admin: desglose de su cupo.",
            "properties": {
              "usados": {
                "type": "integer"
              },
              "limite": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "empresas": {
                "type": "array",
                "description": "Solo las patrocinadas que quien pregunta ADMINISTRA (tiene membership en ellas).",
                "items": {
                  "type": "object",
                  "properties": {
                    "tenantId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "nombre": {
                      "type": "string"
                    },
                    "usados": {
                      "type": "integer"
                    },
                    "independiente": {
                      "type": "boolean",
                      "description": "Ya tiene plan propio de pago: no consume el cupo compartido."
                    }
                  }
                }
              },
              "otras": {
                "type": "object",
                "description": "Agregado de lo que gasta este cupo y quien pregunta ya NO administra: el cupo es suyo y tiene que poder cuadrarlo, pero esos nombres no le pertenecen.",
                "properties": {
                  "empresas": {
                    "type": "integer"
                  },
                  "usados": {
                    "type": "integer"
                  }
                }
              }
            }
          },
          "propio": {
            "type": "object",
            "description": "Lo contratado por ESTA empresa; en regimen propio coincide con el nivel superior.",
            "properties": {
              "plan": {
                "type": "string"
              },
              "limiteComprobantes": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "usadosEsteMes": {
                "type": "integer",
                "description": "Emitidos en Produccion por esta empresa (su contador propio)."
              },
              "planExpiraAt": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "corteDuro": {
                "type": "boolean",
                "description": "Su plan no tiene precio: corta en seco, no acumula excedente."
              }
            }
          }
        },
        "description": "El plan que RIGE la emision y su consumo. En regimen pool (empresa patrocinada en free) plan/limiteComprobantes/usadosEsteMes/tiposPermitidos son los del PATROCINADOR (lo mismo que corta la emision); lo contratado por esta empresa va en `propio`. modulosPermitidos y limiteUsuarios son SIEMPRE del plan propio."
      },
      "Miembro": {
        "type": "object",
        "properties": {
          "membershipId": {
            "type": "string"
          },
          "userId": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "nombre": {
            "type": "string"
          },
          "estado": {
            "type": "string"
          },
          "roles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Rol"
            }
          },
          "creadoEn": {
            "type": "string"
          }
        }
      },
      "Emisor": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "ruc": {
            "type": "string"
          },
          "razonSocial": {
            "type": "string"
          },
          "nombreComercial": {
            "type": [
              "string",
              "null"
            ]
          },
          "dirMatriz": {
            "type": "string"
          },
          "obligadoContabilidad": {
            "type": "boolean"
          },
          "regimen": {
            "type": "string",
            "enum": [
              "GENERAL",
              "RIMPE_EMPRENDEDOR",
              "RIMPE_NEGOCIO_POPULAR"
            ]
          },
          "rideFormato": {
            "type": "string",
            "enum": [
              "oficial",
              "moderno"
            ],
            "description": "Maqueta del RIDE en PDF. Solo la factura (01) la respeta; el resto de comprobantes se imprimen siempre en 'oficial'."
          }
        }
      },
      "EmisorPatchInput": {
        "type": "object",
        "description": "Cambios sobre un emisor existente; todos los campos son opcionales. 'rideFormato' solo se acepta aquí, no en el alta.",
        "properties": {
          "ruc": {
            "type": "string",
            "pattern": "^\\d{13}$"
          },
          "razonSocial": {
            "type": "string"
          },
          "nombreComercial": {
            "type": "string"
          },
          "dirMatriz": {
            "type": "string"
          },
          "obligadoContabilidad": {
            "type": "boolean"
          },
          "regimen": {
            "type": "string"
          },
          "rideFormato": {
            "type": "string",
            "enum": [
              "oficial",
              "moderno"
            ]
          }
        }
      },
      "EmisorInput": {
        "type": "object",
        "required": [
          "ruc",
          "razonSocial",
          "dirMatriz"
        ],
        "properties": {
          "ruc": {
            "type": "string",
            "pattern": "^\\d{13}$"
          },
          "razonSocial": {
            "type": "string"
          },
          "nombreComercial": {
            "type": "string"
          },
          "dirMatriz": {
            "type": "string"
          },
          "obligadoContabilidad": {
            "type": "boolean"
          },
          "regimen": {
            "type": "string"
          }
        }
      },
      "Establecimiento": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "emisorId": {
            "type": "string"
          },
          "codigo": {
            "type": "string"
          },
          "direccion": {
            "type": "string"
          },
          "nombre": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "PuntoEmision": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "establecimientoId": {
            "type": "string"
          },
          "codigo": {
            "type": "string"
          },
          "descripcion": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Secuencial": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "emisorId": {
            "type": "string"
          },
          "tipoComprobante": {
            "type": "string"
          },
          "establecimiento": {
            "type": "string"
          },
          "puntoEmision": {
            "type": "string"
          },
          "ambiente": {
            "type": "integer",
            "enum": [
              1,
              2
            ]
          },
          "ultimoNumero": {
            "type": "integer"
          }
        }
      },
      "FijarSecuencial": {
        "type": "object",
        "required": [
          "tipoComprobante",
          "establecimiento",
          "puntoEmision",
          "ambiente",
          "ultimoNumero"
        ],
        "properties": {
          "tipoComprobante": {
            "type": "string",
            "enum": [
              "01",
              "03",
              "04",
              "05",
              "06",
              "07"
            ]
          },
          "establecimiento": {
            "type": "string",
            "pattern": "^\\d{3}$"
          },
          "puntoEmision": {
            "type": "string",
            "pattern": "^\\d{3}$"
          },
          "ambiente": {
            "type": "integer",
            "enum": [
              1,
              2
            ]
          },
          "ultimoNumero": {
            "type": "integer",
            "minimum": 0,
            "maximum": 999999998,
            "description": "Último número YA USADO; el siguiente será este + 1"
          }
        }
      },
      "Certificado": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "emisorId": {
            "type": "string"
          },
          "alias": {
            "type": [
              "string",
              "null"
            ]
          },
          "entidadCertificadora": {
            "type": [
              "string",
              "null"
            ],
            "description": "Entidad que emitió el certificado, leída del issuer del .p12 al subirlo. Null en los certificados anteriores a que se empezara a registrar."
          },
          "subject": {
            "type": [
              "string",
              "null"
            ]
          },
          "notAfter": {
            "type": [
              "string",
              "null"
            ]
          },
          "estado": {
            "type": "string",
            "enum": [
              "activo",
              "inactivo",
              "revocado"
            ],
            "description": "activo = en uso (una por emisor); inactivo = jubilada al subir otra o subida en espera, recuperable con PATCH accion=activar; revocado = inválida, terminal."
          },
          "verificadaPruebasAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Cuándo el SRI autorizó la factura de PRUEBA firmada con esta firma (POST /certificados/{id}/probar). En ese mismo acto el worker la puso en uso. Null si nunca se probó o la prueba aún no terminó."
          },
          "prueba": {
            "type": [
              "object",
              "null"
            ],
            "description": "Estado de la factura de PRUEBA con la que se comprobó la firma. Null si nunca se probó. AUTORIZADO = verificada; RECHAZADO/DEVUELTA traen el mensaje del SRI; el resto (BORRADOR, FIRMADO, ENVIADO, RECIBIDA) = en curso.",
            "properties": {
              "estado": {
                "type": "string"
              },
              "mensaje": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        }
      },
      "SolicitudFirma": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "emisorId": {
            "type": "string",
            "format": "uuid"
          },
          "estado": {
            "type": "string",
            "enum": [
              "BORRADOR",
              "LISTA",
              "ENVIANDO",
              "EN_REVISION",
              "ERROR_DATOS",
              "ERROR_ENVIO",
              "CERTIFICADO_CARGADO",
              "CANCELADA"
            ],
            "description": "BORRADOR → LISTA → ENVIANDO → EN_REVISION → CERTIFICADO_CARGADO, con las ramas ERROR_DATOS (NewBest rechazó los datos), ERROR_ENVIO (red agotada o catálogo pendiente) y CANCELADA."
          },
          "pasoActual": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "firmacliente",
              "documentos",
              "firma",
              "contador",
              null
            ],
            "description": "Último paso que intentó el worker; dice a qué pantalla volver tras un ERROR_DATOS."
          },
          "envios": {
            "type": "integer",
            "description": "Cuántas veces se pulsó enviar. Entra en el jobId para que un reenvío no colisione con el intento anterior."
          },
          "tipoFirma": {
            "type": "integer",
            "enum": [
              1,
              2,
              3
            ]
          },
          "tipoFirmaEtiqueta": {
            "type": "string"
          },
          "vigencia": {
            "type": "integer",
            "description": "id_firma_tiempo del catálogo de NewBest (1..7)."
          },
          "vigenciaNombre": {
            "type": "string"
          },
          "vigenciaDias": {
            "type": [
              "integer",
              "null"
            ]
          },
          "esRenovacion": {
            "type": "boolean"
          },
          "newbestIdPersona": {
            "type": [
              "string",
              "null"
            ],
            "description": "Id numérico que el asistente de NewBest muestra al titular al terminar su registro. Sin GET en su API no hay forma de verificar que sea suyo."
          },
          "titular": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TitularSolicitud"
              },
              {
                "type": "null"
              }
            ]
          },
          "valorOriginalCentavos": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Snapshot del tarifario congelado AL ENVIAR. Null antes de eso: mientras la solicitud es editable el precio se lee del catálogo vivo."
          },
          "descuentoPorcentaje": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Descuento comercial del distribuidor en PORCENTAJE entero (0..100), no en centavos: es lo que espera descuento_firma en la API de NewBest."
          },
          "valorRealCentavos": {
            "type": [
              "integer",
              "null"
            ]
          },
          "tarifarioVigenteDesde": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "consentimientoAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Evidencia del consentimiento LOPDP con el que el titular autorizó comunicar sus datos a NewBest."
          },
          "consentimientoVersion": {
            "type": [
              "string",
              "null"
            ]
          },
          "ultimoError": {
            "type": [
              "string",
              "null"
            ],
            "description": "Motivo del último ERROR_DATOS/ERROR_ENVIO, recortado a 500 caracteres."
          },
          "enviadoAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "enRevisionAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "cerradoAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "certificadoId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Certificado que cerró el trámite al subir el .p12 emitido por NewBest."
          },
          "purgarAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Cuándo se borrará la PII de la solicitud. Null mientras el plazo no empieza a correr."
          },
          "purgadoAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "atascada": {
            "type": "boolean",
            "description": "Lleva más de 10 minutos en LISTA o más de 30 en ENVIANDO: la cola o el worker no responden. Desde ENVIANDO atascada se puede volver a enviar."
          },
          "editable": {
            "type": "boolean",
            "description": "true en BORRADOR, ERROR_DATOS y ERROR_ENVIO."
          },
          "cancelable": {
            "type": "boolean",
            "description": "true en todo menos ENVIANDO, CERTIFICADO_CARGADO y CANCELADA."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "documentos": {
            "type": "array",
            "description": "Solo en el detalle (GET /solicitudes-firma/{id}) y en las respuestas de POST y PATCH.",
            "items": {
              "$ref": "#/components/schemas/SolicitudFirmaDocumento"
            }
          }
        }
      },
      "SolicitudFirmaDocumento": {
        "type": "object",
        "description": "Documento que Contadeo recoge y sube a NewBest. Nunca incluye la clave del almacenamiento ni la ruta interna en el bucket de NewBest.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "tipoArchivo": {
            "type": "integer",
            "description": "Id del tipo de archivo en el catálogo de NewBest (4 = comprobante de pago, 9..12 = documentos de la empresa)."
          },
          "tipoNombre": {
            "type": "string",
            "description": "Nombre literal del tipo en NewBest, p. ej. COMPROBANTE_PAGO."
          },
          "etiqueta": {
            "type": "string",
            "description": "Etiqueta humana del tipo."
          },
          "nombre": {
            "type": "string",
            "description": "Nombre del fichero que subió el usuario, ya saneado."
          },
          "contentType": {
            "type": "string",
            "enum": [
              "application/pdf",
              "image/jpeg",
              "image/png"
            ]
          },
          "extension": {
            "type": "string",
            "description": "Derivada del content-type, nunca del nombre del fichero."
          },
          "tamanoBytes": {
            "type": "integer"
          },
          "sha256": {
            "type": "string"
          },
          "subidoNewbestAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Cuándo lo aceptó NewBest; null mientras el worker no lo haya subido."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SolicitudFirmaConfig": {
        "type": "object",
        "description": "Catálogos, tarifario, cuentas de pago y texto del consentimiento. El dashboard renderiza lo que reciba: no hay copia de estos datos en el front.",
        "properties": {
          "habilitado": {
            "type": "boolean",
            "description": "Siempre true: con NEWBEST_HABILITADO apagado el módulo no existe y la ruta responde 404."
          },
          "enlaceRegistro": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "URL del asistente hospedado de NewBest donde el titular consiente, valida su cédula y sube sus fotos. Null si NEWBEST_ENLACE_REGISTRO no está configurada."
          },
          "tarifarioConfirmado": {
            "type": "boolean",
            "description": "false mientras NewBest no entregue el tarifario definitivo: los importes son referenciales y la UI debe decirlo."
          },
          "consentimiento": {
            "type": "object",
            "properties": {
              "version": {
                "type": "string"
              },
              "texto": {
                "type": "string"
              }
            },
            "description": "Texto que el usuario acepta al enviar. Se guarda su versión como evidencia LOPDP."
          },
          "tiposFirma": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "enum": [
                    1,
                    2,
                    3
                  ]
                },
                "nombre": {
                  "type": "string",
                  "description": "Literal de NewBest (PERSONA NATURAL, …)."
                },
                "etiqueta": {
                  "type": "string"
                },
                "documentos": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "tipoArchivo": {
                        "type": "integer"
                      },
                      "nombre": {
                        "type": "string"
                      },
                      "etiqueta": {
                        "type": "string"
                      },
                      "origen": {
                        "type": "string",
                        "enum": [
                          "asistente",
                          "contadeo"
                        ],
                        "description": "asistente = el titular ya lo entregó en NewBest y Contadeo no lo pide ni lo ve; contadeo = se sube por POST /solicitudes-firma/{id}/documentos."
                      },
                      "obligatorio": {
                        "type": "boolean"
                      },
                      "formatos": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "description": "Extensiones admitidas, sin punto."
                      },
                      "maxBytes": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          },
          "vigencias": {
            "type": "array",
            "description": "Combinaciones vendibles: solo las que tienen precio vigente. Se filtra por tipoFirma y esRenovacion.",
            "items": {
              "type": "object",
              "properties": {
                "tipoFirma": {
                  "type": "integer"
                },
                "esRenovacion": {
                  "type": "boolean"
                },
                "vigencia": {
                  "type": "integer"
                },
                "nombre": {
                  "type": "string"
                },
                "dias": {
                  "type": "integer"
                },
                "valorOriginalCentavos": {
                  "type": "integer"
                },
                "descuentoPorcentaje": {
                  "type": "integer"
                },
                "valorRealCentavos": {
                  "type": "integer"
                },
                "vigenteDesde": {
                  "type": "string",
                  "format": "date"
                }
              }
            }
          },
          "cuentasBancarias": {
            "type": "array",
            "description": "Cuentas de NewBest a las que el titular transfiere. Vacío mientras no las entreguen: sin cuentas el dashboard no muestra la sección de pago.",
            "items": {
              "type": "object",
              "properties": {
                "banco": {
                  "type": "string"
                },
                "tipoCuenta": {
                  "type": "string"
                },
                "numero": {
                  "type": "string"
                },
                "titular": {
                  "type": "string"
                },
                "identificacion": {
                  "type": "string"
                }
              }
            }
          },
          "provincias": {
            "type": "array",
            "description": "Árbol provincia → cantón → ciudad del catálogo de NewBest. Vacío mientras falte su anexo: el dashboard usa texto libre.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "nombre": {
                  "type": "string"
                },
                "cantones": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "integer"
                      },
                      "nombre": {
                        "type": "string"
                      },
                      "ciudades": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "nombre": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "TitularSolicitud": {
        "type": "object",
        "description": "Datos del titular de la firma tal como viajarán a NewBest. Es PII de una persona que puede no ser el usuario de la cuenta: el cron de retención lo pone a null al vencer el plazo (EN_REVISION 60 días, cerrada 30, ERROR_* 90).",
        "required": [
          "nombres",
          "primerApellido",
          "identificacion",
          "email",
          "telefono",
          "direccion"
        ],
        "properties": {
          "nombres": {
            "type": "string",
            "maxLength": 100
          },
          "primerApellido": {
            "type": "string",
            "maxLength": 100
          },
          "segundoApellido": {
            "type": "string",
            "maxLength": 100
          },
          "identificacion": {
            "type": "string",
            "description": "Cédula de 10 dígitos. En los tipos 1 y 2 es la del titular (los diez primeros de su RUC); en el tipo 3 es la del REPRESENTANTE LEGAL, no la de la empresa."
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "telefono": {
            "type": "string",
            "description": "Celular en la forma 09XXXXXXXX. Se acepta 593XXXXXXXXX y se normaliza al guardarlo."
          },
          "cargo": {
            "type": "string",
            "description": "Cargo del representante legal. Obligatorio en el tipo de firma 3."
          },
          "ruc": {
            "type": "string",
            "description": "Tipos 2 y 3. Obligatorio en el 3: es el RUC de la empresa."
          },
          "razonSocial": {
            "type": "string",
            "description": "Tipos 2 y 3. Obligatorio en el 3."
          },
          "direccion": {
            "type": "object",
            "required": [
              "calleUno",
              "ciudad",
              "canton",
              "provincia"
            ],
            "properties": {
              "calleUno": {
                "type": "string"
              },
              "calleDos": {
                "type": "string"
              },
              "numeracion": {
                "type": "string"
              },
              "sector": {
                "type": "string"
              },
              "ciudad": {
                "type": "string"
              },
              "canton": {
                "type": "string"
              },
              "provincia": {
                "type": "string"
              },
              "idProvincia": {
                "type": "integer",
                "description": "Id del catálogo de NewBest; ausente mientras no entreguen el anexo de provincias."
              },
              "idCanton": {
                "type": "integer"
              },
              "idCiudad": {
                "type": "integer"
              }
            }
          }
        }
      },
      "Producto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "codigoPrincipal": {
            "type": "string"
          },
          "codigoAuxiliar": {
            "type": [
              "string",
              "null"
            ]
          },
          "nombre": {
            "type": "string"
          },
          "precioUnitario": {
            "type": "string"
          },
          "impuestoCodigo": {
            "type": "string"
          },
          "tarifaCodigo": {
            "type": "string"
          },
          "stock": {
            "type": [
              "string",
              "null"
            ],
            "description": "NULL = sin control de existencias; puede ser negativo"
          },
          "stockMinimo": {
            "type": [
              "string",
              "null"
            ]
          },
          "unidadMedida": {
            "type": [
              "string",
              "null"
            ]
          },
          "categoria": {
            "type": [
              "string",
              "null"
            ]
          },
          "activo": {
            "type": "boolean",
            "description": "false = archivado (fuera de listados por defecto)"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ProductoInput": {
        "type": "object",
        "required": [
          "codigoPrincipal",
          "nombre",
          "precioUnitario"
        ],
        "properties": {
          "codigoPrincipal": {
            "type": "string"
          },
          "codigoAuxiliar": {
            "type": "string"
          },
          "nombre": {
            "type": "string"
          },
          "precioUnitario": {
            "type": "number"
          },
          "impuestoCodigo": {
            "type": "string",
            "default": "2"
          },
          "tarifaCodigo": {
            "type": "string",
            "default": "4",
            "description": "4=IVA 15%, 0=0%, 7=exento, 6=no objeto"
          },
          "stock": {
            "type": [
              "number",
              "null"
            ],
            "description": "null = sin control de existencias"
          },
          "stockMinimo": {
            "type": [
              "number",
              "null"
            ]
          },
          "unidadMedida": {
            "type": "string",
            "maxLength": 50
          },
          "categoria": {
            "type": "string",
            "maxLength": 100
          },
          "activo": {
            "type": "boolean"
          },
          "ajusteStock": {
            "type": "number",
            "description": "Solo PATCH: ajuste RELATIVO atómico (stock = stock + ajuste); excluyente con `stock`"
          }
        }
      },
      "Supresion": {
        "type": "object",
        "description": "Solicitud de supresión de un comprador (LOPDP). No se borra nunca: es la lista de exclusión permanente del tenant.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "tipoIdentificacion": {
            "$ref": "#/components/schemas/TipoIdentificacion"
          },
          "identificacion": {
            "type": "string"
          },
          "motivo": {
            "type": [
              "string",
              "null"
            ]
          },
          "solicitadoAt": {
            "type": "string",
            "format": "date-time"
          },
          "ejecutarAt": {
            "type": "string",
            "format": "date-time",
            "description": "Cuándo toca eliminar (art. 23): la solicitud más 3 días menos el margen de una pasada del cron, para que la eliminación caiga dentro de las 72 horas. Antes de esa fecha no se elimina nada."
          },
          "ejecutadoAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "estado": {
            "type": "string",
            "description": "`pendiente` mientras corre el plazo del art. 23, `ejecutado` cuando el cron eliminó lo no amparado, `retirada` si el titular se echó atrás dentro del plazo (la fila se queda, pero deja de excluir a nadie).",
            "enum": [
              "pendiente",
              "ejecutado",
              "retirada"
            ]
          },
          "evidencia": {
            "type": [
              "object",
              "null"
            ],
            "description": "Conteos de lo hecho al ejecutar: {fichasEliminadas, comprobantesBloqueados}. Nunca los datos suprimidos.",
            "properties": {
              "fichasEliminadas": {
                "type": "integer"
              },
              "comprobantesBloqueados": {
                "type": "integer"
              }
            }
          }
        }
      },
      "ManifiestoDevolucion": {
        "type": "object",
        "description": "Una página del manifiesto de descarga de la devolución: una fila por comprobante con las URLs prefirmadas de su XML y su RIDE. Las celdas de URL van vacías cuando no hay archivo (un comprobante sin autorizar no tiene RIDE).",
        "properties": {
          "pagina": {
            "type": "integer",
            "description": "1-based. Se recorta al rango válido."
          },
          "paginas": {
            "type": "integer"
          },
          "comprobantes": {
            "type": "integer",
            "description": "Del manifiesto completo, no de esta página."
          },
          "generadoAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiraAt": {
            "type": "string",
            "format": "date-time",
            "description": "Cuándo dejan de servir las URLs de ESTA página (24 horas)."
          },
          "expiraEnSegundos": {
            "type": "integer"
          },
          "csv": {
            "type": "string",
            "description": "fechaEmision, tipo, numero, claveAcceso, estado, ambiente, urlXml, urlRide, urlExpiraAt."
          }
        }
      },
      "PaqueteDevolucion": {
        "type": "object",
        "description": "La devolución del art. 24: todo lo que el titular se lleva antes de que su empresa se archive. Los CSV van en la respuesta porque son el objeto de la llamada, no un enlace que pueda caducar.",
        "properties": {
          "solicitadaAt": {
            "type": "string",
            "format": "date-time",
            "description": "Primera solicitud. Regenerar el paquete NO la mueve."
          },
          "generadaAt": {
            "type": "string",
            "format": "date-time"
          },
          "plazoDevolucionAt": {
            "type": "string",
            "format": "date-time",
            "description": "Fecha límite del art. 24 (5 días desde la solicitud) para poner la devolución a disposición. Es un compromiso de entrega, no una espera antes de poder confirmar."
          },
          "clientesCsv": {
            "type": "string",
            "description": "Directorio de compradores, sin lo bloqueado por una supresión."
          },
          "comprobantesCsv": {
            "type": "string",
            "description": "Comprobantes de PRODUCCIÓN, completos y sin tope: los que amparan los 7 años."
          },
          "comprobantesPruebasCsv": {
            "type": "string",
            "description": "Los de Pruebas, aparte: mezclarlos haría el CSV inservible fuera de la plataforma."
          },
          "productosCsv": {
            "type": "string"
          },
          "manifiesto": {
            "$ref": "#/components/schemas/ManifiestoDevolucion"
          },
          "siguientePaso": {
            "type": "string",
            "description": "Qué falta para completar la baja, en texto listo para enseñar."
          }
        }
      },
      "ConstanciaEliminacion": {
        "type": "object",
        "description": "Documento acreditativo de una eliminación (resolución SPDP-SPD-2025-0030-R, arts. 20 y 24). Append-only: no se modifica ni se borra nunca.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "alcance": {
            "type": "string",
            "enum": [
              "supresion",
              "baja_empresa",
              "archivo_empresa"
            ],
            "description": "`supresion`: un comprador ejerció su derecho de eliminación. `baja_empresa`: se borró la cuenta entera. `archivo_empresa`: la empresa que emitió se archivó bloqueada — es el único alcance que NO certifica un borrado, y por eso su `eliminado` va vacío y su `excepciones` declara el diferimiento del borrado físico."
          },
          "inventario": {
            "type": "object",
            "description": "Qué se eliminó, qué sobrevive amparado y qué no se purga. Conteos, nunca copias.",
            "properties": {
              "alcance": {
                "type": "string"
              },
              "titular": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Titular alcanzado; null en la baja de una empresa. La identificación va SIEMPRE enmascarada.",
                "properties": {
                  "tipoIdentificacion": {
                    "type": "string"
                  },
                  "identificacion": {
                    "type": "string"
                  }
                }
              },
              "eliminado": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "repositorio": {
                      "type": "string"
                    },
                    "registros": {
                      "type": "integer"
                    }
                  }
                }
              },
              "conservado": {
                "type": "array",
                "description": "Lo que sigue vivo y con qué amparo (la retención tributaria de 7 años en bloqueo).",
                "items": {
                  "type": "object",
                  "properties": {
                    "repositorio": {
                      "type": "string"
                    },
                    "registros": {
                      "type": "integer"
                    },
                    "motivo": {
                      "type": "string"
                    }
                  }
                }
              },
              "excepciones": {
                "type": "array",
                "description": "Lo que deliberadamente no se purga, con su porqué: las trazas append-only son datos seudonimizados con ciclo de vida propio (art. 5).",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "hash": {
            "type": "string",
            "description": "SHA-256 hex del inventario serializado en forma canónica (claves ordenadas). Recalculable sobre el JSON entregado: es lo que hace verificable el documento."
          },
          "emitidoAt": {
            "type": "string",
            "format": "date-time"
          },
          "actor": {
            "type": "string",
            "description": "Quién la provocó: `cron:supresiones` o `usuario:<uuid>`. Identificadores técnicos, nunca PII."
          }
        }
      },
      "Cliente": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "tipoIdentificacion": {
            "$ref": "#/components/schemas/TipoIdentificacion"
          },
          "identificacion": {
            "type": "string"
          },
          "razonSocial": {
            "type": "string"
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "telefono": {
            "type": [
              "string",
              "null"
            ]
          },
          "direccion": {
            "type": [
              "string",
              "null"
            ]
          },
          "placa": {
            "type": [
              "string",
              "null"
            ]
          },
          "bloqueadoAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "No nulo = ficha bloqueada por LOPDP: fuera de listados, prellenado, CSV y reenvíos (resolución SPDP-SPD-2025-0030-R)."
          },
          "bloqueadoMotivo": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ClienteInput": {
        "type": "object",
        "required": [
          "tipoIdentificacion",
          "identificacion",
          "razonSocial"
        ],
        "properties": {
          "tipoIdentificacion": {
            "$ref": "#/components/schemas/TipoIdentificacion"
          },
          "identificacion": {
            "type": "string"
          },
          "razonSocial": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "telefono": {
            "type": "string"
          },
          "direccion": {
            "type": "string"
          },
          "placa": {
            "type": "string"
          }
        }
      },
      "ClienteLookup": {
        "type": "object",
        "required": [
          "fuente",
          "razonSocial"
        ],
        "properties": {
          "fuente": {
            "type": "string",
            "enum": [
              "directorio",
              "sri"
            ]
          },
          "tipoIdentificacion": {
            "type": "string"
          },
          "identificacion": {
            "type": "string"
          },
          "razonSocial": {
            "type": "string"
          },
          "direccion": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "description": "Solo fuente=directorio (no es público en el SRI)"
          },
          "telefono": {
            "type": "string",
            "description": "Solo fuente=directorio"
          },
          "estadoSri": {
            "type": "string"
          },
          "regimen": {
            "type": "string"
          },
          "advertencias": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "CatalogosSri": {
        "type": "object",
        "properties": {
          "ivaGeneralVigente": {
            "type": "object",
            "properties": {
              "codigo": {
                "type": "string"
              },
              "porcentaje": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "descripcion": {
                "type": "string"
              }
            }
          },
          "tarifasIva": {
            "type": "object"
          },
          "formasPago": {
            "type": "object"
          },
          "formasPagoDetalle": {
            "type": "array",
            "description": "Tabla 24 vigente enriquecida: etiqueta coloquial y alias de búsqueda por entrada",
            "items": {
              "type": "object",
              "properties": {
                "codigo": {
                  "type": "string"
                },
                "descripcion": {
                  "type": "string"
                },
                "etiqueta": {
                  "type": "string"
                },
                "alias": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "desde": {
                  "type": "string",
                  "format": "date"
                },
                "hasta": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date"
                }
              }
            }
          },
          "tiposIdentificacion": {
            "type": "object"
          },
          "tiposComprobante": {
            "type": "object"
          },
          "codigosImpuesto": {
            "type": "object"
          },
          "consumidorFinal": {
            "type": "object",
            "properties": {
              "identificacion": {
                "type": "string"
              },
              "limiteUsd": {
                "type": "number"
              }
            }
          }
        }
      },
      "ResultadoEmision": {
        "type": "object",
        "properties": {
          "comprobanteId": {
            "type": "string",
            "format": "uuid"
          },
          "claveAcceso": {
            "type": "string",
            "minLength": 49,
            "maxLength": 49
          },
          "estado": {
            "$ref": "#/components/schemas/EstadoComprobante"
          },
          "reutilizado": {
            "type": "boolean",
            "description": "true si la Idempotency-Key ya tenía comprobante: se devuelve ese (con su estado real), sin re-emitir"
          },
          "advertencias": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Avisos no bloqueantes (p. ej. fecha de emisión con riesgo de error 65)"
          }
        }
      },
      "EmitirComun": {
        "type": "object",
        "required": [
          "emisorId",
          "certificadoId",
          "establecimiento",
          "puntoEmision"
        ],
        "properties": {
          "emisorId": {
            "type": "string",
            "format": "uuid"
          },
          "certificadoId": {
            "type": "string",
            "format": "uuid"
          },
          "establecimiento": {
            "type": "string",
            "pattern": "^\\d{3}$"
          },
          "puntoEmision": {
            "type": "string",
            "pattern": "^\\d{3}$"
          }
        }
      },
      "EmitirFactura": {
        "allOf": [
          {
            "$ref": "#/components/schemas/EmitirComun"
          },
          {
            "type": "object",
            "required": [
              "infoFactura",
              "detalles"
            ],
            "properties": {
              "infoFactura": {
                "$ref": "#/components/schemas/InfoFactura"
              },
              "detalles": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "$ref": "#/components/schemas/DetalleFactura"
                }
              },
              "infoAdicional": {
                "type": "array",
                "maxItems": 14,
                "items": {
                  "$ref": "#/components/schemas/CampoAdicional"
                }
              }
            }
          }
        ]
      },
      "MensajeSri": {
        "type": "object",
        "properties": {
          "identificador": {
            "type": "string"
          },
          "tipo": {
            "type": "string"
          },
          "mensaje": {
            "type": "string"
          },
          "informacionAdicional": {
            "type": "string"
          }
        }
      },
      "Comprobante": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "tipoComprobante": {
            "type": "string"
          },
          "establecimiento": {
            "type": "string"
          },
          "puntoEmision": {
            "type": "string"
          },
          "secuencial": {
            "type": "integer"
          },
          "claveAcceso": {
            "type": "string"
          },
          "estado": {
            "$ref": "#/components/schemas/EstadoComprobante"
          },
          "fechaEmision": {
            "type": "string"
          },
          "numeroAutorizacion": {
            "type": [
              "string",
              "null"
            ]
          },
          "clienteSnapshot": {
            "type": [
              "object",
              "null"
            ]
          },
          "totales": {
            "type": [
              "object",
              "null"
            ]
          },
          "mensajesSri": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/MensajeSri"
            }
          },
          "createdAt": {
            "type": "string"
          }
        }
      },
      "EstadoConsulta": {
        "type": "object",
        "properties": {
          "estado": {
            "$ref": "#/components/schemas/EstadoComprobante"
          },
          "claveAcceso": {
            "type": "string"
          },
          "numeroAutorizacion": {
            "type": [
              "string",
              "null"
            ]
          },
          "mensajesSri": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/MensajeSri"
            }
          },
          "tipoComprobante": {
            "type": "string",
            "description": "01|03|04|05|06|07"
          },
          "establecimiento": {
            "type": "string"
          },
          "puntoEmision": {
            "type": "string"
          },
          "secuencial": {
            "type": "integer"
          },
          "numero": {
            "type": "string",
            "description": "Serie completa, p. ej. 001-001-000000123"
          },
          "fechaEmision": {
            "type": "string",
            "format": "date"
          },
          "fechaAutorizacion": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ambiente": {
            "type": "integer",
            "description": "1=Pruebas, 2=Producción"
          },
          "totales": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "eventos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EventoWs"
            },
            "description": "Solo con ?eventos=true: timeline de la emisión en orden cronológico"
          }
        }
      },
      "UrlDescarga": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "ApiKey": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "nombre": {
            "type": "string"
          },
          "prefijo": {
            "type": "string"
          },
          "roles": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "createdAt": {
            "type": "string"
          },
          "lastUsedAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "revokedAt": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ApiKeyCreada": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiKey"
          },
          {
            "type": "object",
            "properties": {
              "key": {
                "type": "string",
                "description": "Única vez que viaja la key completa (cdo_...)"
              },
              "avisoIa": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AvisoIa"
                  }
                ],
                "description": "Solo cuando la key abre el canal de IA, es decir cuando sus roles incluyen emisor, contador o lector. Con una key solo de `webhooks` responden 403 los comprobantes, los clientes, los productos, los libros y el asesor -todo lo que devuelve fichas de compradores-, y lo único que queda abierto es `/tenants/current` y `/tenants/current/uso-plan`, que devuelven datos de la propia empresa: esa key no expone datos de compradores ni convierte a su dueño en desplegador, y por eso no lleva aviso. Además, la primera vez que la empresa abre el canal -por esta vía o autorizando el conector remoto- sale un correo al owner con el mismo texto, que no se repite por ninguna de las dos vías mientras viva su registro de envío (se conserva 12 meses)."
              }
            }
          }
        ]
      },
      "ResumenReportes": {
        "type": "object",
        "properties": {
          "desde": { "type": "string", "format": "date" },
          "hasta": { "type": "string", "format": "date" },
          "ambiente": { "type": "integer", "enum": [1, 2] },
          "porEstado": { "type": "object", "additionalProperties": { "type": "integer" } },
          "emitidos": { "type": "integer" },
          "totalAutorizado": { "type": "number", "description": "Base imponible neta sin IVA de los autorizados." },
          "totalConIva": { "type": "number" },
          "porTipo": { "type": "array", "items": { "type": "object", "properties": { "tipo": { "type": "string" }, "cantidad": { "type": "integer" }, "subtotal": { "type": "number" }, "total": { "type": "number" } } } },
          "porMes": { "type": "array", "items": { "type": "object", "properties": { "mes": { "type": "string" }, "cantidad": { "type": "integer" }, "subtotal": { "type": "number" }, "total": { "type": "number" } } } },
          "porDia": { "type": "array", "items": { "type": "object", "properties": { "dia": { "type": "string", "format": "date" }, "cantidad": { "type": "integer" } } } },
          "porPunto": { "type": "array", "items": { "type": "object", "properties": { "establecimiento": { "type": "string" }, "puntoEmision": { "type": "string" }, "cantidad": { "type": "integer" }, "subtotal": { "type": "number" } } } },
          "bases": { "type": "object", "properties": { "baseIva15": { "type": "number" }, "baseIva0": { "type": "number" }, "baseExento": { "type": "number" }, "baseNoObjeto": { "type": "number" }, "baseIvaOtras": { "type": "number" }, "montoIva": { "type": "number" } } },
          "topClientes": { "type": "array", "items": { "type": "object", "properties": { "identificacion": { "type": "string", "nullable": true }, "razonSocial": { "type": "string", "nullable": true }, "cantidad": { "type": "integer" }, "subtotal": { "type": "number" } } } },
          "topProductos": { "type": "array", "items": { "type": "object", "properties": { "codigoPrincipal": { "type": "string", "nullable": true }, "descripcion": { "type": "string" }, "cantidad": { "type": "number" }, "subtotal": { "type": "number" } } } },
          "retencionesPorMes": { "type": "array", "items": { "type": "object", "properties": { "mes": { "type": "string" }, "cantidad": { "type": "integer" }, "valor": { "type": "number" } } } }
        }
      },
      "ComprobanteReporte": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "tipoComprobante": { "type": "string" },
          "establecimiento": { "type": "string" },
          "puntoEmision": { "type": "string" },
          "secuencial": { "type": "integer" },
          "claveAcceso": { "type": "string" },
          "estado": { "type": "string" },
          "fechaEmision": { "type": "string", "format": "date" },
          "numeroAutorizacion": { "type": "string", "nullable": true },
          "clienteRuc": { "type": "string", "nullable": true },
          "clienteRazonSocial": { "type": "string", "nullable": true },
          "subtotal": { "type": "number", "description": "Base neta sin IVA; negativa en notas de crédito." },
          "montoIva": { "type": "number" },
          "total": { "type": "number" },
          "importeTotal": { "type": "number", "nullable": true },
          "bases": { "type": "object", "properties": { "baseIva15": { "type": "number" }, "baseIva0": { "type": "number" }, "baseExento": { "type": "number" }, "baseNoObjeto": { "type": "number" }, "baseIvaOtras": { "type": "number" } } },
          "productoCantidad": { "type": "number", "nullable": true },
          "productoSubtotal": { "type": "number", "nullable": true }
        }
      },
      "ComprobantesReporte": {
        "type": "object",
        "properties": {
          "comprobantes": { "type": "array", "items": { "$ref": "#/components/schemas/ComprobanteReporte" } },
          "total": { "type": "integer" },
          "desde": { "type": "integer" },
          "limite": { "type": "integer" }
        }
      },
      "ReporteVentas": {
        "type": "object",
        "properties": {
          "desde": {
            "type": "string"
          },
          "hasta": {
            "type": "string"
          },
          "porEstado": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "totalAutorizado": {
            "type": "number"
          },
          "porTipo": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "porMes": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "AlertasPreferencias": {
        "type": "object",
        "properties": {
          "emailActivo": {
            "type": "boolean",
            "description": "true = el usuario recibirá el digest diario por correo"
          },
          "antelacionDias": {
            "type": "integer",
            "minimum": 1,
            "maximum": 30,
            "description": "Días de antelación para incluir un vencimiento en el digest"
          },
          "incluirRimpe": {
            "type": "boolean",
            "description": "true = incluir semáforo RIMPE ámbar/rojo en el digest"
          },
          "ultimoDigestEnviadoAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Fecha del último digest enviado (YYYY-MM-DD) o null si nunca"
          }
        }
      },
      "CompraSubidaResultado": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "duplicado": {
            "type": "boolean",
            "description": "true si la clave de acceso ya estaba registrada"
          }
        }
      },
      "RetencionRecibidaResultado": {
        "type": "object",
        "description": "Resultado de subir un 07 recibido de un cliente",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "ID de la primera línea insertada (uuid de retenciones_recibidas)"
          },
          "duplicado": {
            "type": "boolean",
            "description": "true si la clave de acceso del 07 ya estaba registrada"
          },
          "lineas": {
            "type": "integer",
            "description": "Número total de líneas de retención en el 07"
          },
          "conciliadas": {
            "type": "integer",
            "description": "Líneas que se conciliaron con una venta del tenant"
          },
          "huerfanas": {
            "type": "integer",
            "description": "Líneas sin venta correspondiente en Contadeo (fiscalmente válidas)"
          }
        }
      },
      "CompraLoteResultado": {
        "type": "object",
        "properties": {
          "procesados": {
            "type": "integer",
            "description": "Archivos nuevos insertados con éxito"
          },
          "duplicados": {
            "type": "integer",
            "description": "Archivos ya existentes (idempotentes)"
          },
          "errores": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "nombre": {
                  "type": "string"
                },
                "motivo": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "CompraResumen": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "emisorId": {
            "type": "string",
            "format": "uuid"
          },
          "proveedorRuc": {
            "type": [
              "string",
              "null"
            ]
          },
          "proveedorRazonSocial": {
            "type": [
              "string",
              "null"
            ]
          },
          "tipoComprobante": {
            "type": "string"
          },
          "claveAcceso": {
            "type": "string"
          },
          "fechaEmision": {
            "type": "string",
            "format": "date"
          },
          "numeroAutorizacion": {
            "type": [
              "string",
              "null"
            ]
          },
          "importeTotal": {
            "type": [
              "string",
              "null"
            ]
          },
          "montoIva": {
            "type": "string"
          },
          "origen": {
            "type": "string",
            "enum": [
              "xml",
              "manual"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CompraDetalle": {
        "type": "object",
        "description": "Todos los campos de la tabla compras incluyendo bases imponibles y desglose"
      },
      "ComprasTotalesPeriodo": {
        "type": "object",
        "description": "Agregado de compras para el período indicado (insumo Formulario 104)",
        "properties": {
          "baseIva15": {
            "type": "string",
            "description": "Suma de base imponible IVA 15%"
          },
          "baseIva0": {
            "type": "string",
            "description": "Suma de base imponible IVA 0%"
          },
          "baseExento": {
            "type": "string",
            "description": "Suma de base exenta"
          },
          "baseNoObjeto": {
            "type": "string",
            "description": "Suma de base no objeto"
          },
          "baseIvaOtras": {
            "type": "string",
            "description": "Suma de bases a tarifas de IVA distintas a la general"
          },
          "montoIva": {
            "type": "string",
            "description": "Suma del IVA total"
          },
          "retencionIva": {
            "type": "string",
            "description": "Suma de retenciones de IVA"
          },
          "retencionRenta": {
            "type": "string",
            "description": "Suma de retenciones de Renta"
          },
          "totalSinImpuestos": {
            "type": "string"
          },
          "importeTotal": {
            "type": "string"
          }
        }
      },
      "EmpleadoResumen": {
        "type": "object",
        "description": "Datos de un empleado registrado en la nómina del emisor",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "tenantId": {
            "type": "string",
            "format": "uuid"
          },
          "emisorId": {
            "type": "string",
            "format": "uuid"
          },
          "cedula": {
            "type": "string"
          },
          "nombres": {
            "type": "string"
          },
          "cargo": {
            "type": [
              "string",
              "null"
            ]
          },
          "fechaIngreso": {
            "type": "string",
            "format": "date"
          },
          "fechaSalida": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "sueldo": {
            "type": "string",
            "description": "Sueldo mensual en USD"
          },
          "tipoContrato": {
            "type": [
              "string",
              "null"
            ]
          },
          "estado": {
            "type": "string",
            "enum": [
              "activo",
              "inactivo"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RolPagosResumen": {
        "type": "object",
        "description": "Rol de pagos mensual de un empleado (columnas materializadas + detalle jsonb)",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "tenantId": {
            "type": "string",
            "format": "uuid"
          },
          "emisorId": {
            "type": "string",
            "format": "uuid"
          },
          "empleadoId": {
            "type": "string",
            "format": "uuid"
          },
          "periodo": {
            "type": "string",
            "description": "Período YYYY-MM"
          },
          "totalIngresos": {
            "type": "string"
          },
          "totalEgresos": {
            "type": "string"
          },
          "neto": {
            "type": "string"
          },
          "aportePersonal": {
            "type": "string"
          },
          "provisionPatronal": {
            "type": "string"
          },
          "provisionFondoReserva": {
            "type": "string"
          },
          "provisionDecimoTercero": {
            "type": "string"
          },
          "provisionDecimoCuarto": {
            "type": "string"
          },
          "provisionVacaciones": {
            "type": "string"
          },
          "detalle": {
            "type": "object",
            "description": "Desglose completo del cálculo (ResultadoRolPagos)"
          },
          "estado": {
            "type": "string",
            "enum": [
              "borrador",
              "aprobado",
              "pagado"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "GenerarRolResultado": {
        "type": "object",
        "description": "Resultado de la generación del rol de pagos de un período",
        "properties": {
          "periodo": {
            "type": "string"
          },
          "empleadosProcesados": {
            "type": "integer"
          },
          "filas": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RolPagosResumen"
            }
          }
        }
      },
      "NominaTotalesPeriodo": {
        "type": "object",
        "description": "Suma de columnas materializadas para todos los empleados del emisor en el período",
        "properties": {
          "totalIngresos": {
            "type": [
              "string",
              "null"
            ]
          },
          "totalEgresos": {
            "type": [
              "string",
              "null"
            ]
          },
          "totalNeto": {
            "type": [
              "string",
              "null"
            ]
          },
          "totalAportePersonal": {
            "type": [
              "string",
              "null"
            ]
          },
          "totalProvisionPatronal": {
            "type": [
              "string",
              "null"
            ]
          },
          "totalProvisionFondoReserva": {
            "type": [
              "string",
              "null"
            ]
          },
          "totalProvisionDecimoTercero": {
            "type": [
              "string",
              "null"
            ]
          },
          "totalProvisionDecimoCuarto": {
            "type": [
              "string",
              "null"
            ]
          },
          "totalProvisionVacaciones": {
            "type": [
              "string",
              "null"
            ]
          },
          "cantidadEmpleados": {
            "type": "integer"
          }
        }
      },
      "ImportarExtractoResultado": {
        "type": "object",
        "properties": {
          "extractoId": {
            "type": "string",
            "format": "uuid"
          },
          "totalMovimientos": {
            "type": "integer"
          },
          "insertados": {
            "type": "integer"
          },
          "duplicados": {
            "type": "integer"
          },
          "fechaDesde": {
            "type": "string",
            "format": "date"
          },
          "fechaHasta": {
            "type": "string",
            "format": "date"
          }
        }
      },
      "ExtractoBancario": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "emisorId": {
            "type": "string",
            "format": "uuid"
          },
          "banco": {
            "type": "string"
          },
          "cuenta": {
            "type": [
              "string",
              "null"
            ]
          },
          "moneda": {
            "type": "string"
          },
          "fechaDesde": {
            "type": "string",
            "format": "date"
          },
          "fechaHasta": {
            "type": "string",
            "format": "date"
          },
          "nombreArchivo": {
            "type": "string"
          },
          "totalMovimientos": {
            "type": "integer"
          },
          "estado": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MovimientoBancario": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "extractoId": {
            "type": "string",
            "format": "uuid"
          },
          "fecha": {
            "type": "string",
            "format": "date"
          },
          "descripcion": {
            "type": "string"
          },
          "referencia": {
            "type": "string"
          },
          "monto": {
            "type": "string"
          },
          "saldo": {
            "type": [
              "string",
              "null"
            ]
          },
          "estado": {
            "type": "string"
          },
          "matchTipo": {
            "type": [
              "string",
              "null"
            ]
          },
          "matchId": {
            "type": [
              "string",
              "null"
            ]
          },
          "matchConfianza": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "SugerenciasMatch": {
        "type": "object",
        "properties": {
          "movimientoId": {
            "type": "string",
            "format": "uuid"
          },
          "sugerencias": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "candidatoId": {
                  "type": "string",
                  "format": "uuid"
                },
                "tipo": {
                  "type": "string",
                  "enum": [
                    "venta",
                    "compra"
                  ]
                },
                "score": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 100
                },
                "ambiguo": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      },
      "ResumenConciliacion": {
        "type": "object",
        "properties": {
          "pendientes": {
            "type": "integer"
          },
          "conciliados": {
            "type": "integer"
          },
          "ignorados": {
            "type": "integer"
          },
          "totalIngresos": {
            "type": "string"
          },
          "totalEgresos": {
            "type": "string"
          },
          "porcentajeCuadre": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          }
        }
      },
      "EventoWs": {
        "type": "object",
        "description": "Evento del ciclo de emisión (firma, recepción SRI, autorización, reintentos, contingencia)",
        "properties": {
          "fase": {
            "type": "string",
            "description": "recepcion | autorizacion | contingencia | anulacion"
          },
          "estadoResultado": {
            "type": [
              "string",
              "null"
            ]
          },
          "codigoSri": {
            "type": [
              "string",
              "null"
            ]
          },
          "mensaje": {
            "type": [
              "string",
              "null"
            ]
          },
          "intento": {
            "type": [
              "integer",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AuditoriaHuecos": {
        "type": "object",
        "properties": {
          "serie": {
            "type": "object",
            "properties": {
              "tipoComprobante": {
                "type": "string"
              },
              "establecimiento": {
                "type": "string"
              },
              "puntoEmision": {
                "type": "string"
              },
              "ambiente": {
                "type": "integer"
              }
            }
          },
          "ultimoNumero": {
            "type": "integer",
            "description": "Contador actual de la serie (0 = sin emisiones)"
          },
          "emitidos": {
            "type": "integer",
            "description": "Números de [1..ultimoNumero] con comprobante"
          },
          "truncado": {
            "type": "boolean",
            "description": "true si hay más de 500 huecos y la lista se truncó"
          },
          "huecos": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "secuencial": {
                  "type": "integer"
                },
                "motivo": {
                  "type": "string",
                  "enum": [
                    "REEMITIDO_ERROR_45",
                    "DESCONOCIDO"
                  ]
                },
                "comprobanteId": {
                  "type": "string",
                  "format": "uuid"
                },
                "mensaje": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "fecha": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "noAutorizados": {
            "type": "array",
            "description": "Comprobantes de la serie en estado ≠ AUTORIZADO (explican discontinuidad fiscal)",
            "items": {
              "type": "object",
              "properties": {
                "secuencial": {
                  "type": "integer"
                },
                "estado": {
                  "$ref": "#/components/schemas/EstadoComprobante"
                },
                "comprobanteId": {
                  "type": "string",
                  "format": "uuid"
                },
                "fechaEmision": {
                  "type": "string",
                  "format": "date"
                }
              }
            }
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "HTTPS obligatorio; se revalida contra SSRF en cada entrega."
          },
          "eventos": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "comprobante.autorizado",
                "comprobante.rechazado",
                "comprobante.devuelto"
              ]
            }
          },
          "activo": {
            "type": "boolean"
          },
          "descripcion": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookConSecreto": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Webhook"
          },
          {
            "type": "object",
            "properties": {
              "secret": {
                "type": "string",
                "description": "Secreto HMAC (`whsec_…`). Se devuelve UNA sola vez: no se puede volver a consultar."
              }
            }
          }
        ]
      },
      "WebhookEntrega": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "evento": {
            "type": "string"
          },
          "estado": {
            "type": "string",
            "enum": [
              "pendiente",
              "entregado",
              "fallido",
              "descartado"
            ]
          },
          "intentos": {
            "type": "integer"
          },
          "statusCode": {
            "type": [
              "integer",
              "null"
            ]
          },
          "ultimoError": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LoteFactura": {
        "type": "object",
        "required": [
          "infoFactura",
          "detalles"
        ],
        "properties": {
          "referencia": {
            "type": "string",
            "maxLength": 120,
            "description": "Identificador TUYO (nº de pedido, etc.); vuelve tal cual en el resultado."
          },
          "idempotencyKey": {
            "type": "string",
            "maxLength": 200,
            "description": "Idempotencia por factura. Si falta y el request lleva el header `Idempotency-Key`, se deriva `header:indice`."
          },
          "infoFactura": {
            "$ref": "#/components/schemas/InfoFactura"
          },
          "detalles": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/DetalleFactura"
            }
          },
          "infoAdicional": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "nombre": {
                  "type": "string"
                },
                "valor": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "ResultadoLoteItem": {
        "type": "object",
        "properties": {
          "indice": {
            "type": "integer"
          },
          "referencia": {
            "type": [
              "string",
              "null"
            ]
          },
          "estado": {
            "type": "string",
            "enum": [
              "BORRADOR",
              "ERROR",
              "NO_INTENTADA"
            ],
            "description": "BORRADOR = encolada (siga con webhooks o GET /comprobantes/{id}) · ERROR = esta factura falló · NO_INTENTADA = el lote se detuvo antes por un error de cuenta."
          },
          "comprobanteId": {
            "type": "string",
            "format": "uuid"
          },
          "claveAcceso": {
            "type": "string"
          },
          "reutilizado": {
            "type": "boolean",
            "description": "true si la idempotencyKey ya tenía comprobante: se devuelve ese, sin re-emitir."
          },
          "error": {
            "type": "string"
          }
        }
      },
      "ResultadoLote": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer"
          },
          "encoladas": {
            "type": "integer"
          },
          "fallidas": {
            "type": "integer"
          },
          "noIntentadas": {
            "type": "integer"
          },
          "resultados": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResultadoLoteItem"
            }
          }
        }
      },
      "Monto": {
        "type": [
          "string",
          "number"
        ],
        "description": "Monto o cantidad; se recomienda string decimal (\"100.00\") para no perder precisión."
      },
      "FormaPago": {
        "type": "string",
        "enum": [
          "01",
          "15",
          "16",
          "17",
          "18",
          "19",
          "20",
          "21"
        ],
        "description": "01=sin sistema financiero, 15=compensación, 16=t. débito, 17=dinero electrónico, 18=t. prepago, 19=t. crédito, 20=otros con sistema financiero, 21=endoso de títulos"
      },
      "CodigoImpuesto": {
        "type": "string",
        "enum": [
          "2",
          "3",
          "5"
        ],
        "description": "2=IVA, 3=ICE, 5=IRBPNR"
      },
      "CampoAdicional": {
        "type": "object",
        "description": "Campo de infoAdicional. Máximo 14 propios: el 15º lo reserva el RUC del proveedor del sistema (Res. NAC-DGERCGC26-00000027).",
        "required": [
          "nombre",
          "valor"
        ],
        "properties": {
          "nombre": {
            "type": "string",
            "maxLength": 300
          },
          "valor": {
            "type": "string",
            "maxLength": 300
          }
        }
      },
      "DetalleAdicional": {
        "type": "object",
        "required": [
          "nombre",
          "valor"
        ],
        "properties": {
          "nombre": {
            "type": "string"
          },
          "valor": {
            "type": "string"
          }
        }
      },
      "ImpuestoLinea": {
        "type": "object",
        "description": "Impuesto a nivel de línea de detalle.",
        "required": [
          "codigo",
          "codigoPorcentaje",
          "tarifa",
          "baseImponible",
          "valor"
        ],
        "properties": {
          "codigo": {
            "$ref": "#/components/schemas/CodigoImpuesto"
          },
          "codigoPorcentaje": {
            "type": "string"
          },
          "tarifa": {
            "$ref": "#/components/schemas/Monto"
          },
          "baseImponible": {
            "$ref": "#/components/schemas/Monto"
          },
          "valor": {
            "$ref": "#/components/schemas/Monto"
          }
        }
      },
      "TotalImpuesto": {
        "type": "object",
        "required": [
          "codigo",
          "codigoPorcentaje",
          "baseImponible",
          "valor"
        ],
        "properties": {
          "codigo": {
            "$ref": "#/components/schemas/CodigoImpuesto"
          },
          "codigoPorcentaje": {
            "type": "string"
          },
          "descuentoAdicional": {
            "$ref": "#/components/schemas/Monto"
          },
          "baseImponible": {
            "$ref": "#/components/schemas/Monto"
          },
          "tarifa": {
            "$ref": "#/components/schemas/Monto"
          },
          "valor": {
            "$ref": "#/components/schemas/Monto"
          },
          "valorDevolucionIva": {
            "$ref": "#/components/schemas/Monto"
          }
        }
      },
      "Pago": {
        "type": "object",
        "required": [
          "formaPago",
          "total"
        ],
        "properties": {
          "formaPago": {
            "$ref": "#/components/schemas/FormaPago"
          },
          "total": {
            "$ref": "#/components/schemas/Monto"
          },
          "plazo": {
            "$ref": "#/components/schemas/Monto"
          },
          "unidadTiempo": {
            "type": "string"
          }
        }
      },
      "InfoFactura": {
        "type": "object",
        "required": [
          "tipoIdentificacionComprador",
          "razonSocialComprador",
          "identificacionComprador",
          "totalSinImpuestos",
          "totalDescuento",
          "totalConImpuestos",
          "importeTotal"
        ],
        "properties": {
          "fechaEmision": {
            "type": "string",
            "format": "date",
            "description": "ISO YYYY-MM-DD; sin ella el servidor usa hoy en hora Ecuador (la única siempre válida ante el SRI)."
          },
          "dirEstablecimiento": {
            "type": "string"
          },
          "contribuyenteEspecial": {
            "type": "string"
          },
          "obligadoContabilidad": {
            "type": "string",
            "enum": [
              "SI",
              "NO"
            ]
          },
          "tipoIdentificacionComprador": {
            "$ref": "#/components/schemas/TipoIdentificacion"
          },
          "guiaRemision": {
            "type": "string"
          },
          "razonSocialComprador": {
            "type": "string"
          },
          "identificacionComprador": {
            "type": "string"
          },
          "direccionComprador": {
            "type": "string"
          },
          "totalSinImpuestos": {
            "$ref": "#/components/schemas/Monto"
          },
          "totalDescuento": {
            "$ref": "#/components/schemas/Monto"
          },
          "totalConImpuestos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TotalImpuesto"
            }
          },
          "propina": {
            "$ref": "#/components/schemas/Monto"
          },
          "importeTotal": {
            "$ref": "#/components/schemas/Monto"
          },
          "moneda": {
            "type": "string"
          },
          "pagos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Pago"
            }
          }
        }
      },
      "DetalleFactura": {
        "type": "object",
        "required": [
          "descripcion",
          "cantidad",
          "precioUnitario",
          "descuento",
          "precioTotalSinImpuesto",
          "impuestos"
        ],
        "properties": {
          "codigoPrincipal": {
            "type": "string"
          },
          "codigoAuxiliar": {
            "type": "string"
          },
          "descripcion": {
            "type": "string"
          },
          "unidadMedida": {
            "type": "string"
          },
          "cantidad": {
            "$ref": "#/components/schemas/Monto"
          },
          "precioUnitario": {
            "$ref": "#/components/schemas/Monto"
          },
          "descuento": {
            "$ref": "#/components/schemas/Monto"
          },
          "precioTotalSinImpuesto": {
            "$ref": "#/components/schemas/Monto"
          },
          "detallesAdicionales": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DetalleAdicional"
            }
          },
          "impuestos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ImpuestoLinea"
            }
          }
        }
      },
      "InfoLiquidacion": {
        "type": "object",
        "description": "La contraparte es el PROVEEDOR: se emite al comprar a quien no puede facturar.",
        "required": [
          "tipoIdentificacionProveedor",
          "razonSocialProveedor",
          "identificacionProveedor",
          "totalSinImpuestos",
          "totalDescuento",
          "totalConImpuestos",
          "importeTotal"
        ],
        "properties": {
          "fechaEmision": {
            "type": "string",
            "format": "date",
            "description": "Fecha ISO YYYY-MM-DD."
          },
          "dirEstablecimiento": {
            "type": "string"
          },
          "contribuyenteEspecial": {
            "type": "string"
          },
          "obligadoContabilidad": {
            "type": "string",
            "enum": [
              "SI",
              "NO"
            ]
          },
          "tipoIdentificacionProveedor": {
            "$ref": "#/components/schemas/TipoIdentificacion"
          },
          "razonSocialProveedor": {
            "type": "string"
          },
          "identificacionProveedor": {
            "type": "string"
          },
          "direccionProveedor": {
            "type": "string"
          },
          "totalSinImpuestos": {
            "$ref": "#/components/schemas/Monto"
          },
          "totalDescuento": {
            "$ref": "#/components/schemas/Monto"
          },
          "codDocReembolso": {
            "type": "string"
          },
          "totalComprobantesReembolso": {
            "$ref": "#/components/schemas/Monto"
          },
          "totalBaseImponibleReembolso": {
            "$ref": "#/components/schemas/Monto"
          },
          "totalImpuestoReembolso": {
            "$ref": "#/components/schemas/Monto"
          },
          "totalConImpuestos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TotalImpuesto"
            }
          },
          "importeTotal": {
            "$ref": "#/components/schemas/Monto"
          },
          "moneda": {
            "type": "string"
          },
          "pagos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Pago"
            }
          }
        }
      },
      "DetalleLiquidacion": {
        "type": "object",
        "required": [
          "descripcion",
          "cantidad",
          "precioUnitario",
          "descuento",
          "precioTotalSinImpuesto",
          "impuestos"
        ],
        "properties": {
          "codigoPrincipal": {
            "type": "string"
          },
          "codigoAuxiliar": {
            "type": "string"
          },
          "descripcion": {
            "type": "string"
          },
          "unidadMedida": {
            "type": "string"
          },
          "cantidad": {
            "$ref": "#/components/schemas/Monto"
          },
          "precioUnitario": {
            "$ref": "#/components/schemas/Monto"
          },
          "precioSinSubsidio": {
            "$ref": "#/components/schemas/Monto"
          },
          "descuento": {
            "$ref": "#/components/schemas/Monto"
          },
          "precioTotalSinImpuesto": {
            "$ref": "#/components/schemas/Monto"
          },
          "detallesAdicionales": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DetalleAdicional"
            }
          },
          "impuestos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ImpuestoLinea"
            }
          }
        }
      },
      "NotaCreditoTotalImpuesto": {
        "type": "object",
        "description": "A diferencia de la factura, NO lleva tarifa ni descuentoAdicional (así el XSD).",
        "required": [
          "codigo",
          "codigoPorcentaje",
          "baseImponible",
          "valor"
        ],
        "properties": {
          "codigo": {
            "$ref": "#/components/schemas/CodigoImpuesto"
          },
          "codigoPorcentaje": {
            "type": "string"
          },
          "baseImponible": {
            "$ref": "#/components/schemas/Monto"
          },
          "valor": {
            "$ref": "#/components/schemas/Monto"
          },
          "valorDevolucionIva": {
            "$ref": "#/components/schemas/Monto"
          }
        }
      },
      "DetalleNotaCredito": {
        "type": "object",
        "required": [
          "descripcion",
          "cantidad",
          "precioUnitario",
          "precioTotalSinImpuesto",
          "impuestos"
        ],
        "properties": {
          "codigoInterno": {
            "type": "string"
          },
          "codigoAdicional": {
            "type": "string"
          },
          "descripcion": {
            "type": "string"
          },
          "cantidad": {
            "$ref": "#/components/schemas/Monto"
          },
          "precioUnitario": {
            "$ref": "#/components/schemas/Monto"
          },
          "descuento": {
            "$ref": "#/components/schemas/Monto"
          },
          "precioTotalSinImpuesto": {
            "$ref": "#/components/schemas/Monto"
          },
          "detallesAdicionales": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DetalleAdicional"
            }
          },
          "impuestos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ImpuestoLinea"
            }
          }
        }
      },
      "InfoNotaCredito": {
        "type": "object",
        "required": [
          "tipoIdentificacionComprador",
          "razonSocialComprador",
          "identificacionComprador",
          "codDocModificado",
          "numDocModificado",
          "fechaEmisionDocSustento",
          "totalSinImpuestos",
          "valorModificacion",
          "totalConImpuestos",
          "motivo"
        ],
        "properties": {
          "fechaEmision": {
            "type": "string",
            "format": "date",
            "description": "Fecha ISO YYYY-MM-DD."
          },
          "dirEstablecimiento": {
            "type": "string"
          },
          "tipoIdentificacionComprador": {
            "$ref": "#/components/schemas/TipoIdentificacion"
          },
          "razonSocialComprador": {
            "type": "string"
          },
          "identificacionComprador": {
            "type": "string"
          },
          "contribuyenteEspecial": {
            "type": "string"
          },
          "obligadoContabilidad": {
            "type": "string",
            "enum": [
              "SI",
              "NO"
            ]
          },
          "rise": {
            "type": "string"
          },
          "codDocModificado": {
            "type": "string",
            "description": "codDoc del comprobante que se modifica (p. ej. \"01\")."
          },
          "numDocModificado": {
            "type": "string",
            "description": "###-###-#########"
          },
          "fechaEmisionDocSustento": {
            "type": "string",
            "format": "date",
            "description": "Fecha del documento que se modifica (ISO)."
          },
          "totalSinImpuestos": {
            "$ref": "#/components/schemas/Monto"
          },
          "valorModificacion": {
            "$ref": "#/components/schemas/Monto"
          },
          "moneda": {
            "type": "string"
          },
          "totalConImpuestos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NotaCreditoTotalImpuesto"
            }
          },
          "motivo": {
            "type": "string"
          }
        }
      },
      "NotaDebitoImpuesto": {
        "type": "object",
        "required": [
          "codigo",
          "codigoPorcentaje",
          "tarifa",
          "baseImponible",
          "valor"
        ],
        "properties": {
          "codigo": {
            "$ref": "#/components/schemas/CodigoImpuesto"
          },
          "codigoPorcentaje": {
            "type": "string"
          },
          "tarifa": {
            "$ref": "#/components/schemas/Monto"
          },
          "baseImponible": {
            "$ref": "#/components/schemas/Monto"
          },
          "valor": {
            "$ref": "#/components/schemas/Monto"
          },
          "valorDevolucionIva": {
            "$ref": "#/components/schemas/Monto"
          }
        }
      },
      "MotivoNotaDebito": {
        "type": "object",
        "required": [
          "razon",
          "valor"
        ],
        "properties": {
          "razon": {
            "type": "string"
          },
          "valor": {
            "$ref": "#/components/schemas/Monto"
          }
        }
      },
      "InfoNotaDebito": {
        "type": "object",
        "description": "Sin detalles de producto: impuestos de cabecera + lista de motivos (razón + valor).",
        "required": [
          "tipoIdentificacionComprador",
          "razonSocialComprador",
          "identificacionComprador",
          "codDocModificado",
          "numDocModificado",
          "fechaEmisionDocSustento",
          "totalSinImpuestos",
          "impuestos",
          "valorTotal"
        ],
        "properties": {
          "fechaEmision": {
            "type": "string",
            "format": "date",
            "description": "Fecha ISO YYYY-MM-DD."
          },
          "dirEstablecimiento": {
            "type": "string"
          },
          "tipoIdentificacionComprador": {
            "$ref": "#/components/schemas/TipoIdentificacion"
          },
          "razonSocialComprador": {
            "type": "string"
          },
          "identificacionComprador": {
            "type": "string"
          },
          "contribuyenteEspecial": {
            "type": "string"
          },
          "obligadoContabilidad": {
            "type": "string",
            "enum": [
              "SI",
              "NO"
            ]
          },
          "rise": {
            "type": "string"
          },
          "codDocModificado": {
            "type": "string"
          },
          "numDocModificado": {
            "type": "string"
          },
          "fechaEmisionDocSustento": {
            "type": "string",
            "format": "date",
            "description": "Fecha ISO YYYY-MM-DD."
          },
          "totalSinImpuestos": {
            "$ref": "#/components/schemas/Monto"
          },
          "impuestos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NotaDebitoImpuesto"
            }
          },
          "valorTotal": {
            "$ref": "#/components/schemas/Monto"
          },
          "pagos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Pago"
            }
          }
        }
      },
      "ImpuestoDocSustento": {
        "type": "object",
        "required": [
          "codImpuestoDocSustento",
          "codigoPorcentaje",
          "baseImponible",
          "tarifa",
          "valorImpuesto"
        ],
        "properties": {
          "codImpuestoDocSustento": {
            "type": "string",
            "enum": [
              "2",
              "3",
              "5"
            ]
          },
          "codigoPorcentaje": {
            "type": "string"
          },
          "baseImponible": {
            "$ref": "#/components/schemas/Monto"
          },
          "tarifa": {
            "$ref": "#/components/schemas/Monto"
          },
          "valorImpuesto": {
            "$ref": "#/components/schemas/Monto"
          }
        }
      },
      "RetencionLinea": {
        "type": "object",
        "required": [
          "codigo",
          "codigoRetencion",
          "baseImponible",
          "porcentajeRetener",
          "valorRetenido"
        ],
        "properties": {
          "codigo": {
            "type": "string",
            "enum": [
              "1",
              "2",
              "6"
            ],
            "description": "1=Renta, 2=IVA, 6=ISD"
          },
          "codigoRetencion": {
            "type": "string"
          },
          "baseImponible": {
            "$ref": "#/components/schemas/Monto"
          },
          "porcentajeRetener": {
            "$ref": "#/components/schemas/Monto"
          },
          "valorRetenido": {
            "$ref": "#/components/schemas/Monto"
          }
        }
      },
      "RetencionPago": {
        "type": "object",
        "required": [
          "formaPago",
          "total"
        ],
        "properties": {
          "formaPago": {
            "$ref": "#/components/schemas/FormaPago"
          },
          "total": {
            "$ref": "#/components/schemas/Monto"
          }
        }
      },
      "DocSustento": {
        "type": "object",
        "required": [
          "codSustento",
          "codDocSustento",
          "numDocSustento",
          "fechaEmisionDocSustento",
          "pagoLocExt",
          "totalSinImpuestos",
          "importeTotal",
          "impuestosDocSustento",
          "retenciones",
          "pagos"
        ],
        "properties": {
          "codSustento": {
            "type": "string"
          },
          "codDocSustento": {
            "type": "string"
          },
          "numDocSustento": {
            "type": "string",
            "description": "15 dígitos: estab(3)+ptoEmi(3)+secuencial(9)."
          },
          "fechaEmisionDocSustento": {
            "type": "string",
            "format": "date",
            "description": "Fecha ISO YYYY-MM-DD."
          },
          "fechaRegistroContable": {
            "type": "string",
            "format": "date",
            "description": "Fecha ISO YYYY-MM-DD."
          },
          "numAutDocSustento": {
            "type": "string"
          },
          "pagoLocExt": {
            "type": "string",
            "enum": [
              "01",
              "02"
            ],
            "description": "01=local, 02=exterior"
          },
          "tipoRegi": {
            "type": "string"
          },
          "paisEfecPago": {
            "type": "string"
          },
          "aplicConvDobTrib": {
            "type": "string",
            "enum": [
              "SI",
              "NO"
            ]
          },
          "pagExtSujRetNorLeg": {
            "type": "string",
            "enum": [
              "SI",
              "NO"
            ]
          },
          "pagoRegFis": {
            "type": "string",
            "enum": [
              "SI",
              "NO"
            ]
          },
          "totalComprobantesReembolso": {
            "$ref": "#/components/schemas/Monto"
          },
          "totalBaseImponibleReembolso": {
            "$ref": "#/components/schemas/Monto"
          },
          "totalImpuestoReembolso": {
            "$ref": "#/components/schemas/Monto"
          },
          "totalSinImpuestos": {
            "$ref": "#/components/schemas/Monto"
          },
          "importeTotal": {
            "$ref": "#/components/schemas/Monto"
          },
          "impuestosDocSustento": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ImpuestoDocSustento"
            }
          },
          "retenciones": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RetencionLinea"
            }
          },
          "pagos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RetencionPago"
            }
          }
        }
      },
      "InfoCompRetencion": {
        "type": "object",
        "required": [
          "tipoIdentificacionSujetoRetenido",
          "parteRel",
          "razonSocialSujetoRetenido",
          "identificacionSujetoRetenido",
          "periodoFiscal"
        ],
        "properties": {
          "fechaEmision": {
            "type": "string",
            "format": "date",
            "description": "Fecha ISO YYYY-MM-DD."
          },
          "dirEstablecimiento": {
            "type": "string"
          },
          "contribuyenteEspecial": {
            "type": "string"
          },
          "obligadoContabilidad": {
            "type": "string",
            "enum": [
              "SI",
              "NO"
            ]
          },
          "tipoIdentificacionSujetoRetenido": {
            "$ref": "#/components/schemas/TipoIdentificacion"
          },
          "tipoSujetoRetenido": {
            "type": "string",
            "enum": [
              "01",
              "02"
            ]
          },
          "parteRel": {
            "type": "string",
            "enum": [
              "SI",
              "NO"
            ]
          },
          "razonSocialSujetoRetenido": {
            "type": "string"
          },
          "identificacionSujetoRetenido": {
            "type": "string"
          },
          "periodoFiscal": {
            "type": "string",
            "description": "mm/aaaa"
          }
        }
      },
      "DetalleGuia": {
        "type": "object",
        "required": [
          "descripcion",
          "cantidad"
        ],
        "properties": {
          "codigoInterno": {
            "type": "string"
          },
          "codigoAdicional": {
            "type": "string"
          },
          "descripcion": {
            "type": "string"
          },
          "cantidad": {
            "$ref": "#/components/schemas/Monto"
          },
          "detallesAdicionales": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DetalleAdicional"
            }
          }
        }
      },
      "Destinatario": {
        "type": "object",
        "required": [
          "identificacionDestinatario",
          "razonSocialDestinatario",
          "dirDestinatario",
          "motivoTraslado",
          "detalles"
        ],
        "properties": {
          "identificacionDestinatario": {
            "type": "string"
          },
          "razonSocialDestinatario": {
            "type": "string"
          },
          "dirDestinatario": {
            "type": "string"
          },
          "motivoTraslado": {
            "type": "string"
          },
          "docAduaneroUnico": {
            "type": "string"
          },
          "codEstabDestino": {
            "type": "string"
          },
          "ruta": {
            "type": "string"
          },
          "codDocSustento": {
            "type": "string"
          },
          "numDocSustento": {
            "type": "string"
          },
          "numAutDocSustento": {
            "type": "string"
          },
          "fechaEmisionDocSustento": {
            "type": "string",
            "format": "date",
            "description": "Fecha ISO YYYY-MM-DD."
          },
          "detalles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DetalleGuia"
            }
          }
        }
      },
      "InfoGuiaRemision": {
        "type": "object",
        "required": [
          "dirPartida",
          "razonSocialTransportista",
          "tipoIdentificacionTransportista",
          "rucTransportista",
          "fechaIniTransporte",
          "fechaFinTransporte",
          "placa"
        ],
        "properties": {
          "dirEstablecimiento": {
            "type": "string"
          },
          "dirPartida": {
            "type": "string"
          },
          "razonSocialTransportista": {
            "type": "string"
          },
          "tipoIdentificacionTransportista": {
            "$ref": "#/components/schemas/TipoIdentificacion"
          },
          "rucTransportista": {
            "type": "string"
          },
          "rise": {
            "type": "string"
          },
          "obligadoContabilidad": {
            "type": "string",
            "enum": [
              "SI",
              "NO"
            ]
          },
          "contribuyenteEspecial": {
            "type": "string"
          },
          "fechaIniTransporte": {
            "type": "string",
            "format": "date",
            "description": "SIEMPRE requerida: alimenta la clave de acceso."
          },
          "fechaFinTransporte": {
            "type": "string",
            "format": "date",
            "description": "Fecha ISO YYYY-MM-DD."
          },
          "placa": {
            "type": "string"
          }
        }
      },
      "EmitirLiquidacion": {
        "allOf": [
          {
            "$ref": "#/components/schemas/EmitirComun"
          },
          {
            "type": "object",
            "required": [
              "infoLiquidacion",
              "detalles"
            ],
            "properties": {
              "infoLiquidacion": {
                "$ref": "#/components/schemas/InfoLiquidacion"
              },
              "detalles": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "$ref": "#/components/schemas/DetalleLiquidacion"
                }
              },
              "infoAdicional": {
                "type": "array",
                "maxItems": 14,
                "items": {
                  "$ref": "#/components/schemas/CampoAdicional"
                }
              }
            }
          }
        ]
      },
      "EmitirNotaCredito": {
        "allOf": [
          {
            "$ref": "#/components/schemas/EmitirComun"
          },
          {
            "type": "object",
            "required": [
              "infoNotaCredito",
              "detalles"
            ],
            "properties": {
              "infoNotaCredito": {
                "$ref": "#/components/schemas/InfoNotaCredito"
              },
              "detalles": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "$ref": "#/components/schemas/DetalleNotaCredito"
                }
              },
              "infoAdicional": {
                "type": "array",
                "maxItems": 14,
                "items": {
                  "$ref": "#/components/schemas/CampoAdicional"
                }
              }
            }
          }
        ]
      },
      "EmitirNotaDebito": {
        "allOf": [
          {
            "$ref": "#/components/schemas/EmitirComun"
          },
          {
            "type": "object",
            "required": [
              "infoNotaDebito",
              "motivos"
            ],
            "properties": {
              "infoNotaDebito": {
                "$ref": "#/components/schemas/InfoNotaDebito"
              },
              "motivos": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "$ref": "#/components/schemas/MotivoNotaDebito"
                }
              },
              "infoAdicional": {
                "type": "array",
                "maxItems": 14,
                "items": {
                  "$ref": "#/components/schemas/CampoAdicional"
                }
              }
            }
          }
        ]
      },
      "EmitirRetencion": {
        "allOf": [
          {
            "$ref": "#/components/schemas/EmitirComun"
          },
          {
            "type": "object",
            "required": [
              "infoCompRetencion",
              "docsSustento"
            ],
            "properties": {
              "infoCompRetencion": {
                "$ref": "#/components/schemas/InfoCompRetencion"
              },
              "docsSustento": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "$ref": "#/components/schemas/DocSustento"
                }
              },
              "infoAdicional": {
                "type": "array",
                "maxItems": 14,
                "items": {
                  "$ref": "#/components/schemas/CampoAdicional"
                }
              }
            }
          }
        ]
      },
      "EmitirGuiaRemision": {
        "allOf": [
          {
            "$ref": "#/components/schemas/EmitirComun"
          },
          {
            "type": "object",
            "required": [
              "infoGuiaRemision",
              "destinatarios"
            ],
            "properties": {
              "infoGuiaRemision": {
                "$ref": "#/components/schemas/InfoGuiaRemision"
              },
              "destinatarios": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "$ref": "#/components/schemas/Destinatario"
                }
              },
              "infoAdicional": {
                "type": "array",
                "maxItems": 14,
                "items": {
                  "$ref": "#/components/schemas/CampoAdicional"
                }
              }
            }
          }
        ]
      },
      "Corte": {
        "type": "object",
        "description": "Corte mensual de excedente: lo emitido por encima del cupo del plan, tarifado por tramos. Ciclo: abierto (provisional, día 1) → cerrado (consolidado, día 3) → facturado → pagado.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "periodo": {
            "type": "string",
            "example": "2026-08"
          },
          "facturables": {
            "type": "integer",
            "description": "Comprobantes AUTORIZADO+ANULADO en Producción del período."
          },
          "cupoAplicado": {
            "type": "integer"
          },
          "excedente": {
            "type": "integer"
          },
          "montoBaseCentavos": {
            "type": "integer"
          },
          "ivaCentavos": {
            "type": "integer"
          },
          "totalCentavos": {
            "type": "integer"
          },
          "estado": {
            "type": "string",
            "enum": [
              "abierto",
              "cerrado",
              "facturando",
              "facturado",
              "pagado",
              "error_factura"
            ]
          },
          "comprobanteId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Autofactura SRI del corte, cuando exista."
          },
          "pagadoAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DesafioMfa": {
        "type": "object",
        "description": "La contraseña se validó pero el usuario tiene MFA activo. `desafioToken` caduca en 5 minutos y solo sirve para POST /auth/mfa/verificar.",
        "required": [
          "mfaRequerido",
          "desafioToken"
        ],
        "properties": {
          "mfaRequerido": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "desafioToken": {
            "type": "string"
          }
        }
      },
      "EstadoMfa": {
        "type": "object",
        "properties": {
          "activo": {
            "type": "boolean"
          },
          "codigosRecuperacionRestantes": {
            "type": "integer"
          }
        }
      },
      "ResumenProductos": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer",
            "description": "Productos no borrados (incluye archivados)"
          },
          "activos": {
            "type": "integer"
          },
          "agotados": {
            "type": "integer",
            "description": "Activos con stock <= 0"
          },
          "bajoStock": {
            "type": "integer",
            "description": "Activos con 0 < stock < stockMinimo"
          },
          "categorias": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "nombre": {
                  "type": "string"
                },
                "cantidad": {
                  "type": "integer"
                }
              }
            }
          },
          "porTarifa": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          }
        }
      },
      "ResultadoImportacionProductos": {
        "type": "object",
        "properties": {
          "creados": {
            "type": "integer"
          },
          "actualizados": {
            "type": "integer"
          },
          "errores": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "fila": {
                  "type": "integer"
                },
                "mensaje": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "AvisoIa": {
        "type": "object",
        "description": "Aviso de transparencia del canal de IA (art. 5.1 de la resolución SPSP-SPD-2026-0009-R). Conectar un asistente convierte al tenant en desplegador del sistema de IA: informar a sus compradores de que un asistente trata sus datos es obligación suya, no de Contadeo, que actúa como encargado del tratamiento.",
        "properties": {
          "titulo": {
            "type": "string"
          },
          "texto": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "URL absoluta del texto modelo publicado en /aviso-ia, listo para adaptar como sección del aviso de privacidad del emisor."
          }
        }
      }
    }
  }
}