openapi: 3.1.0
info:
  title: Te Afirmo API
  version: "1.1.0"
  summary: API pública del sistema Te Afirmo
  description: |
    API oficial del sistema Te Afirmo. Expone autenticación (credenciales
    propias + Google OAuth), solicitud y gestión de garantías para
    licitaciones públicas y privadas, documentos de perfil, consultas a
    Mercado Público (Chilecompra), pagos vía Transbank WebPay Plus y
    webhooks de integración.

    ### Autenticación
    La mayoría de endpoints operativos requieren una cookie de sesión
    `teafirmo_session` emitida por `POST /api/auth/login`. Los formularios
    públicos (contacto, wizard de garantías, consultas a Mercado Público)
    no requieren autenticación.

    ### CSRF
    Los endpoints `POST` que reciben datos de formularios públicos
    requieren un token CSRF obtenido en `GET /api/csrf-token`. El token
    se envía en el body (`csrfToken`) y debe coincidir con la cookie
    `csrf-token` emitida por el mismo endpoint.

    ### Rate limiting
    Endpoints públicos aplican límites por IP y/o por correo. Superar el
    límite devuelve `429 Too Many Requests`.

    ### Captcha
    En producción los endpoints públicos exigen un token de Cloudflare
    Turnstile en el campo `cf-turnstile-response`.
  contact:
    name: Te Afirmo
    email: conversemos@teafirmo.cl
    url: https://www.teafirmo.cl
  license:
    name: Propietario
    identifier: LicenseRef-proprietary

servers:
  - url: https://www.teafirmo.cl
    description: Producción
  - url: http://localhost:4321
    description: Desarrollo local

tags:
  - name: Auth
    description: Registro, verificación, login (credenciales + Google OAuth) y recuperación.
  - name: Usuarios
    description: Completar perfil de usuario autenticado.
  - name: Garantías
    description: Wizard público de solicitud de garantías y endpoints asociados.
  - name: Documentos
    description: Gestión de documentos de perfil (usuarios autenticados).
  - name: Contacto
    description: Formularios de contacto y asesoría públicos.
  - name: Mercado Público
    description: Integraciones con Chilecompra (licitaciones, organismos, validaciones).
  - name: Pagos
    description: Integración con Transbank WebPay Plus.
  - name: Compartidos
    description: Acceso público a documentos compartidos por token.
  - name: Webhooks
    description: Receptores para integraciones externas (Cal.com, GoHighLevel).
  - name: Infra
    description: Health check y utilidades de cliente (CSRF).

components:
  securitySchemes:
    sessionCookie:
      type: apiKey
      in: cookie
      name: teafirmo_session
      description: Cookie HttpOnly emitida por `POST /api/auth/login`.

  schemas:
    Error:
      type: object
      properties:
        success: { type: boolean, const: false }
        error: { type: string }
        message: { type: string }
      required: [success]

    SuccessEnvelope:
      type: object
      properties:
        success: { type: boolean, const: true }
        data: { description: 'Payload específico del endpoint.' }
        message: { type: string }
      required: [success]

    Pagination:
      type: object
      properties:
        total: { type: integer }
        limit: { type: integer }
        offset: { type: integer }
        hasMore: { type: boolean }
      required: [total, limit, offset, hasMore]

    CsrfToken:
      type: object
      required: [success, token]
      properties:
        success: { type: boolean }
        token: { type: string, description: 'Token CSRF opaco. Enviar en body y validar contra cookie `csrf-token`.' }

    HealthReport:
      type: object
      properties:
        status:
          type: string
          enum: [healthy, degraded]
        version: { type: string }
        checks:
          type: object
          properties:
            database: { type: boolean }
            timestamp: { type: string, format: date-time }
            environment:
              type: object
              properties:
                hasTursoUrl: { type: boolean }
                hasTursoToken: { type: boolean }
                hasBackblazeKey: { type: boolean }
                hasResendKey: { type: boolean }
                hasGHLKey: { type: boolean }
                hasSentryDsn: { type: boolean }

    GuaranteeItem:
      type: object
      required: [tipoGarantia, montoGarantia, monedaGarantia, vigenciaDesde, vigenciaHasta]
      properties:
        tipoGarantia:
          type: string
          enum: [seriedad, cumplimiento, anticipo, ejecucion, responsabilidad, capital_trabajo, factoring, canje_retenciones]
        montoGarantia: { type: string, description: 'Valor numérico como string para preservar precisión.' }
        monedaGarantia:
          type: string
          enum: [pesos, UF, USD]
        vigenciaDesde: { type: string, format: date }
        vigenciaHasta: { type: string, format: date }
        glosaGarantia: {  type: [string, 'null']  }

    GuaranteeSubmissionChilecompra:
      type: object
      required: [sistemaLicitacion, nombreSolicitante, email, telefonoSolicitante, garantias]
      properties:
        sistemaLicitacion: { type: string, const: chilecompra }
        nombreSolicitante: { type: string }
        email: { type: string, format: email }
        telefonoSolicitante: { type: string }
        nombreEmpresaOferente: { type: string }
        rutEmpresaOferente: { type: string }
        direccionEmpresaOferente: { type: string }
        regionEmpresaOferente: { type: string }
        comunaEmpresaOferente: { type: string }
        idLicitacion: { type: string }
        institucion: { type: string }
        rutInstitucion: { type: string }
        razonSocial: { type: string }
        garantias:
          type: array
          items: { $ref: '#/components/schemas/GuaranteeItem' }
        contratoUrl: { type: string, format: uri }
        adicionalUrl: { type: string, format: uri }

    GuaranteeSubmissionPrivados:
      type: object
      required: [sistemaLicitacion, nombreSolicitante, email, telefonoSolicitante, garantias]
      properties:
        sistemaLicitacion: { type: string, const: privados }
        nombreSolicitante: { type: string }
        email: { type: string, format: email }
        telefonoSolicitante: { type: string }
        nombreEmpresaContratista: { type: string }
        rutEmpresaContratista: { type: string }
        direccionEmpresaContratista: { type: string }
        nombreContactoContratista: { type: string }
        emailContratista: { type: string, format: email }
        telefonoContratista: { type: string }
        regionEmpresaContratista: { type: string }
        comunaEmpresaContratista: { type: string }
        razonSocialMandante: { type: string }
        rutMandantePrivados: { type: string }
        nombreContactoMandante: { type: string }
        emailMandante: { type: string, format: email }
        telefonoMandante: { type: string }
        fechaInicioContrato: { type: string, format: date }
        fechaTerminoContrato: { type: string, format: date }
        garantias:
          type: array
          items: { $ref: '#/components/schemas/GuaranteeItem' }
        contratoUrl: { type: string, format: uri }
        adicionalUrl: { type: string, format: uri }

    Licitacion:
      type: object
      properties:
        codigoExterno: { type: string }
        nombre: { type: string }
        estado: { type: string }
        codigoEstado: {  type: [integer, 'null']  }
        organismo: { type: string }
        codigoOrganismo: { type: string }
        nombreUnidad: { type: string }
        fechaPublicacion: { type: string, format: date }
        fechaCierre: { type: string, format: date }
        montoEstimado: {  type: [number, 'null']  }
        moneda: { type: string }
        tipo: { type: string }
        descripcion: { type: string }

    Organismo:
      type: object
      properties:
        codigoOrganismo: { type: string }
        nombreOrganismo: { type: string }
        rut: {  type: [string, 'null']  }
        region: {  type: [string, 'null']  }
        comuna: {  type: [string, 'null']  }

    DocumentMetadata:
      type: object
      properties:
        id: { type: string }
        fileName: { type: string }
        fileId: { type: string, description: 'ID interno de Backblaze.' }
        url: { type: string, format: uri }
        previewUrl: { type: string, format: uri }
        category: { type: string }
        customTitle: {  type: [string, 'null']  }
        uploadedAt: { type: string, format: date-time }
        expiresAt: {  type: [string, 'null'], format: date-time  }

    ContactForm:
      type: object
      required: [primer_nombre, correo, csrfToken]
      properties:
        primer_nombre: { type: string }
        apellido: { type: string }
        rut: { type: string }
        telefono: { type: string }
        correo: { type: string, format: email }
        tipo_inversionista: { type: string }
        perfil_activo: { type: string }
        mensaje: { type: string }
        acepta_politicas: { type: boolean }
        csrfToken: { type: string, description: 'Obtenido en GET /api/csrf-token.' }
        'cf-turnstile-response': { type: string }

    PaymentCreateRequest:
      type: object
      required: [amount]
      properties:
        amount:
          type: integer
          minimum: 500
          description: Monto en pesos chilenos (CLP).
        description: { type: string }
        metadata: { type: object, additionalProperties: true }

    PaymentCreateResponse:
      type: object
      properties:
        success: { type: boolean }
        data:
          type: object
          properties:
            buyOrder: { type: string }
            sessionId: { type: string }
            token: { type: string, description: 'Token de Transbank para redirigir al usuario.' }
            url: { type: string, format: uri }
            amount: { type: integer }

security:
  - sessionCookie: []

paths:
  # ============ Infra ============
  /api/health:
    get:
      tags: [Infra]
      summary: Estado del sistema
      security: []
      responses:
        '200':
          description: Sistema saludable.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HealthReport' }
        '503':
          description: Degradado (falla DB o env críticas).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HealthReport' }

  /api/csrf-token:
    get:
      tags: [Infra]
      summary: Obtener token CSRF y cookie
      description: |
        Genera un token CSRF, lo retorna en el body y lo setea en la
        cookie `csrf-token`. El cliente debe enviar ambos valores al
        hacer POST a endpoints protegidos por CSRF.
      security: []
      responses:
        '200':
          description: Token generado.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CsrfToken' }

  # ============ Auth ============
  /api/auth/register:
    post:
      tags: [Auth]
      summary: Registrar usuario (persona natural o jurídica)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password, role, rut]
              properties:
                email: { type: string, format: email }
                password: { type: string, minLength: 8 }
                role: { type: string, enum: [natural, juridica] }
                rut: { type: string, description: 'RUT chileno válido.' }
                name: { type: string, description: 'Requerido si role=natural.' }
                razonSocial: { type: string, description: 'Requerido si role=juridica.' }
                source: { type: string }
                'cf-turnstile-response': { type: string }
      responses:
        '200':
          description: Registro iniciado. Código TOTP enviado al correo.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessEnvelope' }
        '400': { description: Validación fallida. }
        '409': { description: Email ya registrado y verificado. }

  /api/auth/verify:
    post:
      tags: [Auth]
      summary: Verificar correo con código TOTP
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, code]
              properties:
                email: { type: string, format: email }
                code: { type: string, minLength: 6, maxLength: 6 }
      responses:
        '200': { description: Cuenta verificada. }
        '401': { description: Código inválido. }
        '404': { description: Cuenta no encontrada. }

  /api/auth/resend:
    post:
      tags: [Auth]
      summary: Reenviar código TOTP
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
      responses:
        '200': { description: Código reenviado. }
        '404': { description: Cuenta no encontrada. }
        '429': { description: Rate limit. }

  /api/auth/login:
    post:
      tags: [Auth]
      summary: Iniciar sesión
      description: Valida credenciales y emite cookie `teafirmo_session`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string }
                profile: { type: string, enum: [natural, juridica, admin] }
                'cf-turnstile-response': { type: string }
      responses:
        '200':
          description: Sesión iniciada.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          email: { type: string, format: email }
                          role: { type: string, enum: [natural, juridica, admin] }
        '401': { description: Credenciales incorrectas. }
        '403': { description: Email no verificado o perfil no permitido. }
        '429': { description: Rate limit. }

  /api/auth/session:
    get:
      tags: [Auth]
      summary: Obtener sesión actual
      responses:
        '200':
          description: Estado de la sesión.
          content:
            application/json:
              schema:
                type: object
                properties:
                  authenticated: { type: boolean }
                  email: {  type: [string, 'null'], format: email  }
                  role: {  type: [string, 'null'], enum: [natural, juridica, admin]  }
    delete:
      tags: [Auth]
      summary: Cerrar sesión
      description: Invalida la cookie `teafirmo_session`.
      responses:
        '200': { description: Sesión cerrada. }

  /api/auth/google:
    get:
      tags: [Auth]
      summary: Iniciar OAuth con Google
      description: |
        Redirige al usuario al consent screen de Google. Tras aprobar, Google
        redirige a `/api/auth/google/callback` con un `code`.
      security: []
      responses:
        '302':
          description: Redirect a `accounts.google.com/o/oauth2/v2/auth`.

  /api/auth/google/callback:
    get:
      tags: [Auth]
      summary: Callback OAuth Google
      description: |
        Recibe el `code` de Google, intercambia por access token, crea o
        recupera el usuario en Te Afirmo y emite cookie de sesión.
      security: []
      parameters:
        - name: code
          in: query
          required: true
          schema: { type: string }
        - name: state
          in: query
          required: true
          schema: { type: string }
      responses:
        '302':
          description: Redirect a panel del usuario o /register con error.

  /api/auth/forgot-password:
    post:
      tags: [Auth]
      summary: Solicitar recuperación de contraseña
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                'cf-turnstile-response': { type: string }
      responses:
        '200': { description: 'Si la cuenta existe, se envió un enlace de reset por correo.' }

  /api/auth/reset-password:
    post:
      tags: [Auth]
      summary: Reestablecer contraseña con token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, password]
              properties:
                token: { type: string }
                password: { type: string, minLength: 8 }
      responses:
        '200': { description: Contraseña actualizada. }
        '400': { description: Token inválido o expirado. }

  # ============ Usuarios ============
  /api/auth/complete-profile:
    post:
      tags: [Usuarios]
      summary: Completar perfil del usuario autenticado
      description: Completa campos adicionales (RUT, nombre/razón social) tras registro.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                rut: { type: string }
                name: { type: string }
                razonSocial: { type: string }
                telefono: { type: string }
                direccion: { type: string }
                region: { type: string }
                comuna: { type: string }
      responses:
        '200': { description: Perfil actualizado. }
        '400': { description: Validación fallida. }
        '401': { description: Sin sesión. }

  # ============ Garantías ============
  /api/guarantees/upload-url:
    get:
      tags: [Garantías]
      summary: Obtener presigned URL para subir documento de garantía
      description: |
        Paso 1 del flujo de subida: el cliente solicita una presigned URL,
        hace `PUT` con el archivo y luego envía el `downloadUrl` recibido en
        el wizard.
      security: []
      parameters:
        - name: type
          in: query
          required: true
          schema: { type: string, enum: [contrato, adicional] }
        - name: fileName
          in: query
          required: true
          schema: { type: string }
        - name: contentType
          in: query
          schema: { type: string, default: application/octet-stream }
      responses:
        '200':
          description: Credenciales generadas.
          content:
            application/json:
              schema:
                type: object
                required: [success, presignedUrl, downloadUrl]
                properties:
                  success: { type: boolean }
                  presignedUrl: { type: string, format: uri }
                  downloadUrl: { type: string, format: uri }
        '403': { description: Token CSRF inválido. }
        '429': { description: Rate limit. }

  /api/guarantees/upload-proxy:
    post:
      tags: [Garantías]
      summary: Subir archivo vía proxy (fallback si presigned URL falla)
      description: |
        Alternativa al presigned URL cuando el cliente no puede hacer `PUT`
        directo a Backblaze (p. ej. CORS restrictivo). Sube el archivo al
        servidor y éste lo reenvía a Backblaze.
      security: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [type, file]
              properties:
                type: { type: string, enum: [contrato, adicional] }
                file: { type: string, format: binary }
      responses:
        '200':
          description: Subida completada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  downloadUrl: { type: string, format: uri }

  /api/guarantees/public:
    post:
      tags: [Garantías]
      summary: Enviar solicitud pública de garantías (wizard)
      description: |
        Endpoint final del wizard `/gestiona-tu-garantia`. Los documentos
        adjuntos deben haberse subido previamente con `/upload-url` o
        `/upload-proxy`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/GuaranteeSubmissionChilecompra'
                - $ref: '#/components/schemas/GuaranteeSubmissionPrivados'
              discriminator:
                propertyName: sistemaLicitacion
                mapping:
                  chilecompra: '#/components/schemas/GuaranteeSubmissionChilecompra'
                  privados: '#/components/schemas/GuaranteeSubmissionPrivados'
      responses:
        '200':
          description: Solicitud recibida.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          submissionId: { type: integer }
        '400': { description: Validación fallida. }

  # ============ Documentos ============
  /api/documents/upload:
    post:
      tags: [Documentos]
      summary: Subir documento de perfil
      description: |
        Sube un documento del perfil autenticado (persona natural o
        jurídica). Calcula fecha de expiración según la categoría.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [category, file]
              properties:
                category:
                  type: string
                  description: Clave de categoría (ej. `carpeta_tributaria`, `cedula_identidad`, `otros`).
                file:
                  type: string
                  format: binary
                customTitle:
                  type: string
                  description: Título personalizado cuando `category=otros`.
      responses:
        '200':
          description: Documento subido.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/DocumentMetadata' }
        '400': { description: 'Archivo inválido por tamaño, formato o categoría.' }
        '401': { description: Sin sesión. }

  /api/documents/profile:
    get:
      tags: [Documentos]
      summary: Obtener documentos del perfil
      parameters:
        - name: type
          in: query
          schema:
            type: string
            enum: [personal, documents, guarantees]
            default: personal
        - name: profile
          in: query
          description: 'Solo para admin: perfil a consultar (`natural` o `juridica`).'
          schema: { type: string, enum: [natural, juridica] }
      responses:
        '200':
          description: Documentos del perfil.
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  oneOf:
                    - $ref: '#/components/schemas/DocumentMetadata'
                    - type: array
                      items: { $ref: '#/components/schemas/DocumentMetadata' }
        '401': { description: Sin sesión. }

  /api/documents/delete:
    post:
      tags: [Documentos]
      summary: Eliminar documento del perfil
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [category]
              properties:
                category: { type: string }
                documentId: { type: string, description: 'ID específico si la categoría tiene múltiples archivos.' }
      responses:
        '200': { description: Documento eliminado. }
        '404': { description: Documento no encontrado. }

  /api/documents/download:
    get:
      tags: [Documentos]
      summary: Descargar documento (propio o compartido)
      parameters:
        - name: key
          in: query
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Archivo binario.
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '403': { description: Sin permiso. }
        '404': { description: Documento no encontrado. }

  /api/documents/share:
    post:
      tags: [Documentos]
      summary: Generar enlace compartido para un documento
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [documentKey]
              properties:
                documentKey: { type: string }
                expiresInHours:
                  type: integer
                  minimum: 1
                  maximum: 168
                  default: 72
      responses:
        '200':
          description: Enlace generado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  shareUrl: { type: string, format: uri }
                  expiresAt: { type: string, format: date-time }
        '404': { description: Documento no encontrado. }

  /api/documents/alerts:
    get:
      tags: [Documentos]
      summary: Listar alertas de vencimiento del usuario
      responses:
        '200':
          description: Alertas de documentos próximos a vencer / vencidos.
          content:
            application/json:
              schema:
                type: object
                properties:
                  expiring:
                    type: array
                    items:
                      type: object
                      properties:
                        category: { type: string }
                        documentName: { type: string }
                        expiryDate: { type: string, format: date-time }
                        daysUntilExpiry: { type: integer }
                        alertLevel: { type: string, enum: [critical, danger, warning] }

  # ============ Contacto ============
  /api/contact:
    post:
      tags: [Contacto]
      summary: Formulario de contacto general
      description: |
        Proceso completo: guarda en DB Turso, sincroniza con GoHighLevel
        y envía correo interno. Requiere CSRF + Turnstile (en producción).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ContactForm' }
      responses:
        '200': { description: Contacto recibido. }
        '400': { description: Validación fallida. }
        '403': { description: CSRF o Turnstile inválidos. }
        '429': { description: Rate limit. }

  /api/contact-advisor:
    post:
      tags: [Contacto]
      summary: Solicitud de asesor
      description: Variante del formulario de contacto enfocada en agendar con un asesor.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ContactForm' }
      responses:
        '200': { description: Solicitud recibida. }

  /api/contact-nico:
    post:
      tags: [Contacto]
      summary: Formulario landing "Nico Experto"
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ContactForm' }
      responses:
        '200': { description: Solicitud recibida. }

  # ============ Mercado Público ============
  /api/listar-licitaciones:
    get:
      tags: [Mercado Público]
      summary: Listar licitaciones por fecha
      description: Proxy sobre Mercado Público. `fecha` obligatoria (`ddmmaaaa`).
      security: []
      parameters:
        - name: fecha
          in: query
          required: true
          schema: { type: string, pattern: "^\\d{8}$" }
        - name: CodigoOrganismo
          in: query
          schema: { type: string }
        - name: estado
          in: query
          schema:
            type: string
            enum: [publicadas, cerradas, desiertas, adjudicadas, revocadas, suspendidas]
      responses:
        '200':
          description: Listado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  count: { type: integer }
                  data:
                    type: object
                    properties:
                      licitaciones:
                        type: array
                        items: { $ref: '#/components/schemas/Licitacion' }

  /api/listar-organismos:
    get:
      tags: [Mercado Público]
      summary: Listar organismos compradores
      description: Catálogo completo de compradores de Mercado Público.
      security: []
      responses:
        '200':
          description: Listado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      organismos:
                        type: array
                        items: { $ref: '#/components/schemas/Organismo' }

  /api/validate-licitacion:
    get:
      tags: [Mercado Público]
      summary: Validar código de licitación
      description: |
        Valida formato del código. Si hay `MERCADOPUBLICO_TICKET` configurado,
        además verifica existencia contra la API oficial.
      security: []
      parameters:
        - name: id
          in: query
          required: true
          schema: { type: string }
          example: "1051173-19-LE25"
      responses:
        '200':
          description: Resultado de la validación.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  valid: { type: boolean }
                  source: { type: string, enum: [basic, api] }
                  data:
                    type: object

  /api/validate-orden-compra:
    get:
      tags: [Mercado Público]
      summary: Validar orden de compra
      security: []
      parameters:
        - name: id
          in: query
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Resultado de la validación.

  /api/buscar-proveedor:
    get:
      tags: [Mercado Público]
      summary: Buscar proveedor en Mercado Público
      security: []
      parameters:
        - name: rut
          in: query
          schema: { type: string }
        - name: razonSocial
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Proveedor encontrado o listado de coincidencias.

  # ============ Pagos ============
  /api/payments/create:
    post:
      tags: [Pagos]
      summary: Crear transacción WebPay Plus
      description: |
        Requiere sesión. Crea una transacción en Transbank y devuelve el
        `token` + `url` para redirigir al usuario al flujo de pago.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PaymentCreateRequest' }
      responses:
        '200':
          description: Transacción creada.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PaymentCreateResponse' }
        '401': { description: Sin sesión. }

  /api/payments/confirm:
    post:
      tags: [Pagos]
      summary: Callback de Transbank tras el pago
      description: |
        Endpoint invocado por Transbank (form-data) al finalizar (exitoso
        o cancelado). Actualiza el estado de la transacción y redirige al
        usuario a `/payment/success`, `/payment/cancelled` o
        `/payment/error`.
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                token_ws: { type: string }
                TBK_TOKEN: { type: string }
                TBK_ORDEN_COMPRA: { type: string }
                TBK_ID_SESION: { type: string }
      responses:
        '302':
          description: Redirect al usuario a la página de resultado.

  # ============ Compartidos ============
  /api/share/{token}:
    get:
      tags: [Compartidos]
      summary: Acceder a documento compartido por token
      description: |
        Resuelve un token de compartir (generado por `POST /api/documents/share`)
        y devuelve el archivo o una página HTML con el visor.
      security: []
      parameters:
        - name: token
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Documento disponible.
        '404':
          description: Token inválido o expirado.

  # ============ Webhooks ============
  /api/webhooks/calcom:
    post:
      tags: [Webhooks]
      summary: Webhook Cal.com
      description: |
        Recibe eventos de Cal.com (`BOOKING_CREATED`, `BOOKING_CANCELLED`,
        etc.) y sincroniza con Te Afirmo.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                triggerEvent: { type: string }
                payload: { type: object, additionalProperties: true }
      responses:
        '200': { description: Evento procesado. }
        '401': { description: Firma inválida. }

  /api/webhooks/gohighlevel:
    post:
      tags: [Webhooks]
      summary: Webhook GoHighLevel
      description: |
        Recibe eventos de GoHighLevel (contactos, pipeline changes, etc.)
        y sincroniza con la base de datos.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200': { description: Evento procesado. }
        '401': { description: Token inválido. }
