openapi: 3.0.3
info:
  title: CertiLink API
  description: |
    **CertiLink** es una plataforma descentralizada para la emisión, administración y validación pública de certificados académicos sobre la blockchain **Stellar** (Horizon + Soroban Testnet) y base de datos relacional **Supabase (PostgreSQL)**.

    ## Autenticación

    La API utiliza dos mecanismos de autenticación:

    1. **API Key (`X-API-KEY`)**: Cabecera obligatoria para todos los endpoints autenticados (Alumnos, Cursos, Certificados). Se obtiene al registrar una OTEC o al iniciar sesión.
    2. **Privy JWT (`Authorization: Bearer`)**: Utilizado exclusivamente por el Dashboard interno para asociar sesiones web3 con credenciales del sistema.

    ## Flujo de Integración Típico

    ```
    1. POST /api/certificados    → Emitir certificados on-chain (¡acción principal!)
    2. GET  /api/certificados/{id} → Validar certificados (público)
    3. POST /api/otecs           → Registrar la OTEC (obtener api_key)
    4. POST /api/auth/login      → Iniciar sesión (obtener api_key si no se tiene)
    5. POST /api/alumnos         → Registrar alumnos
    6. POST /api/cursos          → Registrar cursos
    ```

    ## Blockchain

    Cada certificado emitido registra su hash SHA-256 como Memo en una transacción de Stellar Horizon y opcionalmente mintea un NFT Soulbound no-transferible en Soroban.
  version: 2.1.0
  contact:
    name: Equipo CertiLink
    url: https://certi.browns.studio

servers:
  - url: https://certi.browns.studio
    description: Producción (Vercel + Supabase + Stellar Testnet)
  - url: http://localhost:3000
    description: Desarrollo local

tags:
  - name: Certificados
    description: Emisión y listado de certificados autenticados
  - name: Validación Pública
    description: Endpoints públicos para verificar certificados sin autenticación
  - name: OTECs
    description: Gestión de Organismos Técnicos de Capacitación
  - name: Alumnos
    description: Registro y consulta de alumnos por OTEC
  - name: Cursos
    description: Registro y consulta de cursos por OTEC
  - name: Autenticación
    description: Inicio de sesión y recuperación de credenciales

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY
      description: Clave de API de la OTEC. Se obtiene al registrar la OTEC o al iniciar sesión.
    PrivyBearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Token JWT de Privy utilizado exclusivamente por el Dashboard interno.

  schemas:
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: Mensaje descriptivo del error.
      required:
        - error
      example:
        error: "Faltan campos obligatorios: rut, nombre, email, password"

    SuccessWrapper:
      type: object
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
        data:
          type: object

    OtecInput:
      type: object
      required:
        - rut
        - nombre
        - email
        - password
      properties:
        rut:
          type: string
          description: RUT único del organismo (formato chileno).
          example: "99.999.888-7"
        nombre:
          type: string
          description: Nombre o Razón Social de la OTEC.
          example: "OTEC Capacitación Limitada"
        email:
          type: string
          format: email
          description: Correo corporativo único.
          example: "contacto@cercaotec.cl"
        password:
          type: string
          format: password
          description: Contraseña para el portal de administración.
          example: "MiContraseñaSegura123"
        logo_url:
          type: string
          format: uri
          description: URL del logotipo de la institución (opcional).
          example: "https://cercaotec.cl/assets/logo.png"
        wallet_address:
          type: string
          description: Billetera Stellar existente. Si no se provee, se generará y fondeará una automáticamente.
          example: "GCBQ7JZZVLWCV2XPXEK4BBOPKKH677UGPHYDUQP2LQXTJZOVDNYBX3DI"

    OtecResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        rut:
          type: string
        nombre:
          type: string
        email:
          type: string
          format: email
        wallet_address:
          type: string
        logo_url:
          type: string
          nullable: true
        api_key:
          type: string
          format: uuid
          description: Clave de API para autenticar peticiones subsiguientes.
        generated_secret_key:
          type: string
          nullable: true
          description: Clave secreta Stellar generada. Solo se retorna una vez al registrar la OTEC si no se proveyó wallet_address.

    OtecPublic:
      type: object
      properties:
        id:
          type: string
          format: uuid
        nombre:
          type: string
        email:
          type: string
          format: email
        rut:
          type: string
        wallet_address:
          type: string
        logo_url:
          type: string
          nullable: true

    LoginInput:
      type: object
      required:
        - email
        - password
      properties:
        email:
          type: string
          format: email
          example: "contacto@cercaotec.cl"
        password:
          type: string
          format: password
          example: "MiContraseñaSegura123"

    LoginResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        rut:
          type: string
        nombre:
          type: string
        email:
          type: string
          format: email
        wallet_address:
          type: string
        logo_url:
          type: string
          nullable: true
        api_key:
          type: string
          format: uuid

    AlumnoInput:
      type: object
      required:
        - rut
        - nombre
        - apellido
        - email
      properties:
        rut:
          type: string
          description: RUT del alumno (formato chileno).
          example: "18.765.432-1"
        nombre:
          type: string
          description: Nombre de pila del alumno.
          example: "Juanito"
        apellido:
          type: string
          description: Apellidos del alumno.
          example: "Pérez Prueba"
        email:
          type: string
          format: email
          description: Correo electrónico del alumno.
          example: "juanito@alumnoprueba.cl"
        telefono:
          type: string
          description: Teléfono de contacto (opcional).
          example: "+56911112222"
        wallet_address:
          type: string
          description: Wallet Stellar del alumno. Si se omite, se generará y fondeará una automáticamente.

    AlumnoResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        otec_id:
          type: string
          format: uuid
        rut:
          type: string
        nombre:
          type: string
        apellido:
          type: string
        email:
          type: string
          format: email
        telefono:
          type: string
          nullable: true
        wallet_address:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    CursoInput:
      type: object
      required:
        - codigo
        - nombre
        - horas
        - linea_academica
      properties:
        codigo:
          type: string
          description: Código único del curso dentro de la OTEC.
          example: "WEB3-STELLAR"
        nombre:
          type: string
          description: Nombre completo del curso.
          example: "Desarrollo Web3 con Stellar"
        horas:
          type: integer
          minimum: 1
          description: Duración en horas cronológicas.
          example: 30
        linea_academica:
          type: string
          description: Línea académica o categoría.
          example: "Tecnología"
        descripcion:
          type: string
          description: Contenido o resumen del curso (opcional).
          example: "Curso práctico de desarrollo sobre la red Stellar."
        link_programa:
          type: string
          format: uri
          description: URL pública al temario del curso (opcional).
          example: "https://cercaotec.cl/programa-web3"

    CursoResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        otec_id:
          type: string
          format: uuid
        codigo:
          type: string
        nombre:
          type: string
        horas:
          type: integer
        linea_academica:
          type: string
        descripcion:
          type: string
          nullable: true
        link_programa:
          type: string
          nullable: true
        activo:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    CertificadoEmisionInput:
      type: object
      required:
        - alumno_id
        - curso_id
        - fecha_fin
        - nota
      properties:
        alumno_id:
          type: string
          format: uuid
          description: ID del alumno registrado en la OTEC.
        curso_id:
          type: string
          format: uuid
          description: ID del curso registrado en la OTEC.
        fecha_fin:
          type: string
          format: date
          description: Fecha de término del curso (ISO 8601).
          example: "2026-06-30"
        nota:
          type: integer
          minimum: 0
          maximum: 100
          description: Nota del alumno (valor entero entre 0 y 100).
          example: 98
        file_hash:
          type: string
          description: Hash SHA-256 del diploma (PNG). Si se omite, se autogenerará.
          example: "946394e17ff53ec005d43581eab9aba43b2c077b402f5d2f077baa16140f6d6d"
        otec_secret_key:
          type: string
          description: Clave secreta Stellar de la OTEC para firmar la transacción. Si se omite, se usa la variable de entorno o una cuenta temporal.
        metadata:
          type: object
          description: Metadatos libres adicionales. Se recomienda incluir `image_url`.
          example:
            image_url: "https://cercaotec.cl/assets/img/mock-certificate.png"
            relator: "Profesor Juan Pérez"
            modalidad: "Presencial"

    CertificadoEmisionResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        hash_sha256:
          type: string
          description: Hash SHA-256 único del certificado.
        tx_hash:
          type: string
          description: Hash de la transacción en Stellar Horizon.
        soroban_tx_hash:
          type: string
          nullable: true
          description: Hash de la transacción de minteo NFT en Soroban.
        validar_url:
          type: string
          format: uri
          description: URL pública para validar el certificado.
        ledger:
          type: integer
          description: Número de ledger de Stellar donde se registró la transacción.

    CertificadoValidacionResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        codigo_certificado:
          type: string
          description: Código legible del certificado (formato CERT-YYYY-XXXX).
          example: "CERT-2026-83113FF0"
        otec:
          type: object
          properties:
            id:
              type: string
              format: uuid
            nombre:
              type: string
            rut:
              type: string
            wallet_address:
              type: string
        alumno:
          type: object
          properties:
            id:
              type: string
              format: uuid
            nombre_completo:
              type: string
            rut:
              type: string
            email:
              type: string
              format: email
        curso:
          type: object
          properties:
            id:
              type: string
              format: uuid
            nombre:
              type: string
            horas:
              type: integer
            linea_academica:
              type: string
            link_programa:
              type: string
              nullable: true
            descripcion:
              type: string
              nullable: true
        blockchain:
          type: object
          properties:
            red:
              type: string
              example: "Stellar Testnet"
            tx_hash:
              type: string
            soroban_tx_hash:
              type: string
              nullable: true
            hash_sha256:
              type: string
            explorer_url:
              type: string
              format: uri
        fecha_emision:
          type: string
          format: date-time
        fecha_fin:
          type: string
          format: date-time
        nota:
          type: integer
        estado:
          type: string
          enum:
            - emitido
            - revocado
        metadata:
          type: object
          nullable: true

    VerificacionHashResponse:
      type: object
      properties:
        success:
          type: boolean
        id:
          type: string
          format: uuid
        estado:
          type: string
          enum:
            - emitido
            - revocado

    KeyByEmailResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          properties:
            id:
              type: string
              format: uuid
            nombre:
              type: string
            rut:
              type: string
            api_key:
              type: string
              format: uuid
            wallet_address:
              type: string
            logo_url:
              type: string
              nullable: true

    # Alias de schemas referenciados por los nuevos endpoints (Fase 2-4)
    Otec:
      $ref: "#/components/schemas/OtecResponse"
    Alumno:
      $ref: "#/components/schemas/AlumnoResponse"
    Curso:
      $ref: "#/components/schemas/CursoResponse"
    Certificado:
      $ref: "#/components/schemas/CertificadoValidacionResponse"
    Pagination:
      type: object
      properties:
        page: { type: integer, example: 1 }
        limit: { type: integer, example: 20 }
        total: { type: integer, example: 134 }
        totalPages: { type: integer, example: 7 }
        hasNext: { type: boolean, example: true }
        hasPrev: { type: boolean, example: false }

  parameters:
    IdPath:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
      description: Identificador UUID del recurso.

paths:
  /api/certificados:
    get:
      tags:
        - Certificados
      summary: Listar certificados de la OTEC
      description: |
        Retorna todos los certificados emitidos por la OTEC autenticada, incluyendo datos del alumno y curso asociados.

        **Ejemplo curl:**
        ```bash
        curl https://certi.browns.studio/api/certificados \
          -H "X-API-KEY: tu-api-key"
        ```
      operationId: listarCertificados
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Lista de certificados obtenida exitosamente.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: array
                    items:
                      type: object
                      description: Certificado con datos de alumno y curso incluidos.
        "401":
          description: API Key faltante o inválida.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Error interno del servidor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    post:
      tags:
        - Certificados
      summary: Emitir un certificado on-chain
      description: |
        Genera e inmutabiliza un certificado académico en la blockchain Stellar:

        1. **Calcula** un hash SHA-256 único del certificado (o usa el `file_hash` provisto).
        2. **Registra** el hash como Memo en una transacción de Stellar Horizon (pago de 0 XLM auto-referenciado).
        3. **Mintea** un NFT Soulbound no-transferible en Soroban a la wallet del alumno (asíncrono, no bloqueante).
        4. **Envía** un email de notificación al alumno (asíncrono, no bloqueante).
        5. **Persiste** el registro completo en la base de datos.

        ⚠️ **Reglas de Negocio**:
        - La `nota` debe estar entre 0 y 100.
        - La `fecha_fin` no puede ser anterior a la fecha de emisión.
        - No se permiten certificados duplicados (mismo hash SHA-256).
        - El alumno y el curso deben pertenecer a la misma OTEC autenticada.

        **Ejemplo curl:**
        ```bash
        curl -X POST https://certi.browns.studio/api/certificados \
          -H "Content-Type: application/json" \
          -H "X-API-KEY: tu-api-key" \
          -d '{
            "alumno_id": "5da88fa3-5f29-4619-b304-911ab8f26a0f",
            "curso_id": "435732ae-88ed-4dca-b07e-7347669a008d",
            "fecha_fin": "2026-06-30",
            "nota": 98,
            "metadata": {
              "relator": "Profesor Juan Pérez",
              "modalidad": "Presencial"
            }
          }'
        ```
      operationId: emitirCertificado
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CertificadoEmisionInput"
            example:
              alumno_id: "5da88fa3-5f29-4619-b304-911ab8f26a0f"
              curso_id: "435732ae-88ed-4dca-b07e-7347669a008d"
              fecha_fin: "2026-06-30"
              nota: 98
              metadata:
                image_url: "https://cercaotec.cl/assets/img/mock-certificate.png"
                relator: "Profesor Juan Pérez"
                modalidad: "Presencial"
      responses:
        "201":
          description: Certificado emitido y registrado en Stellar on-chain con éxito.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Certificado emitido y registrado en Stellar on-chain con éxito."
                  data:
                    $ref: "#/components/schemas/CertificadoEmisionResponse"
        "400":
          description: Campos faltantes, nota fuera de rango o fecha inválida.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: API Key faltante o inválida.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Alumno o curso no encontrado o no pertenece a la OTEC.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Certificado duplicado (hash SHA-256 ya existe).
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      hash_sha256:
                        type: string
                      tx_hash:
                        type: string
        "500":
          description: Error interno del servidor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: Error de red al conectar con Stellar Horizon.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/certificados/{id}:
    get:
      tags:
        - Validación Pública
      summary: Validar un certificado por ID
      description: |
        Endpoint **público** (sin autenticación) para consultar y validar un certificado emitido.

        Acepta el ID completo (UUID de 36 caracteres) o un prefijo parcial del ID. También acepta el formato legible `CERT-YYYY-XXXX`.

        Retorna toda la información del certificado incluyendo datos de la OTEC, alumno, curso y links al explorador de Stellar.
      operationId: validarCertificado
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: UUID del certificado o código en formato CERT-YYYY-XXXX.
          example: "83113ff0-3864-4de2-a7a3-8a6568713db5"
      responses:
        "200":
          description: Certificado encontrado y validado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CertificadoValidacionResponse"
        "400":
          description: ID del certificado no provisto.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Certificado no encontrado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "Certificado no encontrado."
        "500":
          description: Error interno del servidor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    patch:
      tags:
        - Certificados
      summary: Revocar un certificado emitido
      description: |
        Endpoint autenticado para revocar un certificado académico previamente emitido por la OTEC.

        Modifica el estado del certificado a `"revocado"` en Supabase y ejecuta una transacción `Manage Data` on-chain en Stellar Testnet marcando la credencial como inhabilitada de forma permanente.
      operationId: revocarCertificado
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: UUID completo del certificado a revocar.
          example: "83113ff0-3864-4de2-a7a3-8a6568713db5"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                otec_secret_key:
                  type: string
                  description: Clave secreta Stellar de la OTEC para firmar la revocación. Si no se envía, se utiliza la clave configurada por defecto en el backend.
                  example: "SDPZDRDGJMRRY23ZDH3YFAPI7EWOD7MKYYPCGWYV57RBOCC43NISGWFM"
      responses:
        "200":
          description: Certificado revocado exitosamente.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Certificado revocado exitosamente en la base de datos y la blockchain."
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      estado:
                        type: string
                        example: "revocado"
                      stellar_tx_hash:
                        type: string
                        example: "18bf7cf7dbbdb6975f3cfb7667753c226afbb18563255daf80628d253ce8db93"
                      ledger:
                        type: integer
                        example: 3336820
        "400":
          description: ID faltante.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: API Key faltante o inválida.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: La OTEC no es dueña del certificado solicitado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "No tienes permiso para modificar este certificado."
        "404":
          description: Certificado no encontrado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Error interno del servidor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: Error al enviar la transacción de revocación a Stellar.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/certificados/verificar-hash:
    get:
      tags:
        - Validación Pública
      summary: Verificar certificado por hash SHA-256
      description: |
        Endpoint **público** para verificar la validez de un certificado a partir de su hash SHA-256 (64 caracteres hexadecimales).

        Útil para integraciones embebidas de portales externos que necesiten verificar diplomas.
      operationId: verificarHash
      parameters:
        - name: hash
          in: query
          required: true
          schema:
            type: string
            minLength: 64
            maxLength: 64
          description: Hash hexadecimal SHA-256 del certificado (64 caracteres).
          example: "946394e17ff53ec005d43581eab9aba43b2c077b402f5d2f077baa16140f6d6d"
      responses:
        "200":
          description: Certificado encontrado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerificacionHashResponse"
              example:
                success: true
                id: "83113ff0-3864-4de2-a7a3-8a6568713db5"
                estado: "emitido"
        "400":
          description: Parámetro hash no provisto.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "El parámetro 'hash' es obligatorio."
        "404":
          description: Certificado no encontrado para el hash provisto.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: "Certificado no encontrado para el hash provisto."
        "500":
          description: Error interno del servidor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/certificados/all:
    get:
      tags: [Certificados]
      summary: Listado global de certificados (Modo Construcción)
      description: |
        Retorna todos los certificados del sistema con OTEC, alumno y curso embebidos.
        Soporta filtros opcionales (`estado`, `otec_id`) y paginación (`page`, `limit`).
        **MODO_CONSTRUCCION**: público sin API Key. Cuando se activen los niveles de privacidad,
        volverá a requerir autenticación y filtrará por OTEC (salvo SuperAdmin).
      parameters:
        - in: query
          name: estado
          schema: { type: string, enum: [emitido, revocado] }
          description: Filtra por estado del certificado.
        - in: query
          name: otec_id
          schema: { type: string, format: uuid }
          description: Filtra por OTEC emisora.
        - in: query
          name: page
          schema: { type: integer, minimum: 1, default: 1 }
          description: Número de página (paginación opcional).
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
          description: Cantidad por página. Si se omite junto con `page`, se devuelven todos.
      responses:
        "200":
          description: Listado de certificados.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Certificado" }
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "500":
          description: Error interno del servidor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/otecs:
    get:
      tags:
        - OTECs
      summary: Listar OTECs registradas
      description: Retorna la lista pública de todas las OTECs registradas en la plataforma. No expone claves sensibles.
      operationId: listarOtecs
      responses:
        "200":
          description: Lista de OTECs obtenida exitosamente.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/OtecPublic"
        "500":
          description: Error interno del servidor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    post:
      tags:
        - OTECs
      summary: Registrar una nueva OTEC
      description: |
        Registra una nueva institución (OTEC) en la plataforma. Auto-genera una clave de API (`api_key`) y, si no se provee `wallet_address`, crea y fondea una billetera en Stellar Testnet automáticamente.

        ⚠️ **Importante**: La `generated_secret_key` solo se retorna en esta respuesta. El cliente debe guardarla de forma segura.

        **Ejemplo curl:**
        ```bash
        curl -X POST https://certi.browns.studio/api/otecs \
          -H "Content-Type: application/json" \
          -d '{
            "rut": "99.999.888-7",
            "nombre": "OTEC Capacitación Limitada",
            "email": "contacto@cercaotec.cl",
            "password": "MiContraseñaSegura123"
          }'
        ```
      operationId: registrarOtec
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OtecInput"
            example:
              rut: "99.999.888-7"
              nombre: "OTEC Capacitación Limitada"
              email: "contacto@cercaotec.cl"
              password: "MiContraseñaSegura123"
      responses:
        "201":
          description: OTEC registrada con éxito.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "OTEC registrada con éxito."
                  data:
                    $ref: "#/components/schemas/OtecResponse"
        "400":
          description: Faltan campos obligatorios o formato inválido.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Ya existe una OTEC con este RUT o email.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "Ya existe una OTEC registrada con este RUT o email."
        "500":
          description: Error interno del servidor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/otecs/{id}:
    get:
      tags: [OTECs]
      summary: Detalle de OTEC (Modo Construcción)
      description: Retorna información de una OTEC. La `api_key` y `_count` solo se retornan al dueño o SuperAdmin.
      parameters:
        - $ref: "#/components/parameters/IdPath"
      responses:
        "200":
          description: OTEC encontrada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data: { $ref: "#/components/schemas/Otec" }
        "404": { description: OTEC no encontrada. }
    patch:
      tags: [OTECs]
      summary: Actualizar OTEC
      description: Modifica datos de la OTEC. Solo SuperAdmin puede cambiar `estado`.
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/IdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                nombre: { type: string }
                direccion: { type: string, nullable: true }
                telefono: { type: string, nullable: true }
                logo_url: { type: string, nullable: true }
                estado: { type: string, enum: [activo, inactivo], description: "Solo SuperAdmin" }
      responses:
        "200": { description: OTEC actualizada. }
        "401": { description: Falta API Key. }
        "403": { description: No tienes permiso. }
        "404": { description: OTEC no encontrada. }

  /api/otecs/rotate-key:
    post:
      tags: [OTECs, Autenticación]
      summary: Rotar API Key
      description: Genera una nueva API Key invalidando la anterior. El SuperAdmin puede rotar la key de cualquier OTEC pasando `otec_id`.
      security: [{ ApiKeyAuth: [] }]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                otec_id: { type: string, format: uuid, description: "Solo SuperAdmin. OTEC objetivo." }
      responses:
        "200":
          description: API Key rotada con éxito.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  message: { type: string }
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      nombre: { type: string }
                      email: { type: string }
                      api_key: { type: string }
        "401": { description: Falta API Key. }

  /api/otecs/key-by-email:
    get:
      tags:
        - Autenticación
      summary: Obtener credenciales por Privy JWT (Dashboard)
      description: |
        Endpoint **protegido por Privy JWT** utilizado internamente por el panel de administración (Dashboard).

        Asocia la sesión de Privy autenticada en el frontend con las llaves de API locales del sistema. Verifica que el email del token JWT coincida con el email solicitado.
      operationId: obtenerKeyPorEmail
      security:
        - PrivyBearerAuth: []
      parameters:
        - name: email
          in: query
          required: true
          schema:
            type: string
            format: email
          description: Correo electrónico de la OTEC a buscar.
          example: "contacto@cercaotec.cl"
      responses:
        "200":
          description: Credenciales obtenidas exitosamente.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KeyByEmailResponse"
        "400":
          description: Parámetro email no provisto.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Token de autorización faltante o inválido.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: El email del token no coincide con el solicitado.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "Acceso no autorizado. El correo de la sesión no coincide con el solicitado."
        "404":
          description: OTEC no registrada con este correo.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Error interno del servidor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/alumnos:
    get:
      tags:
        - Alumnos
      summary: Listar alumnos de la OTEC
      description: Retorna la lista de todos los alumnos registrados por la OTEC autenticada. Opcionalmente se puede filtrar por RUT.
      operationId: listarAlumnos
      security:
        - ApiKeyAuth: []
      parameters:
        - name: rut
          in: query
          required: false
          schema:
            type: string
          description: Filtrar por RUT del alumno (opcional).
          example: "18.765.432-1"
      responses:
        "200":
          description: Lista de alumnos obtenida exitosamente.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/AlumnoResponse"
        "401":
          description: API Key faltante o inválida.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Error interno del servidor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    post:
      tags:
        - Alumnos
      summary: Registrar un alumno
      description: |
        Crea o recupera un alumno asociado a la OTEC autenticada. Si el alumno ya existe (mismo RUT dentro de la OTEC), retorna los datos existentes sin crear duplicados.

        Si no se provee `wallet_address`, la API genera y fondea una billetera Stellar Testnet automáticamente para el alumno.

        **Ejemplo curl:**
        ```bash
        curl -X POST https://certi.browns.studio/api/alumnos \
          -H "Content-Type: application/json" \
          -H "X-API-KEY: tu-api-key" \
          -d '{
            "rut": "18.765.432-1",
            "nombre": "Juanito",
            "apellido": "Pérez Prueba",
            "email": "juanito@alumnoprueba.cl",
            "telefono": "+56911112222"
          }'
        ```
      operationId: registrarAlumno
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AlumnoInput"
            example:
              rut: "18.765.432-1"
              nombre: "Juanito"
              apellido: "Pérez Prueba"
              email: "juanito@alumnoprueba.cl"
              telefono: "+56911112222"
      responses:
        "201":
          description: Alumno registrado con éxito.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Alumno registrado con éxito."
                  data:
                    $ref: "#/components/schemas/AlumnoResponse"
        "200":
          description: El alumno ya existía previamente (retorna datos existentes).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Alumno ya registrado previamente."
                  data:
                    $ref: "#/components/schemas/AlumnoResponse"
        "400":
          description: Faltan campos obligatorios o formato inválido.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: API Key faltante o inválida.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Duplicidad de RUT dentro de la misma OTEC.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Error interno del servidor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/alumnos/{id}:
    get:
      tags: [Alumnos]
      summary: Detalle de alumno (Modo Construcción)
      description: "Retorna la información completa de un alumno, con su OTEC y conteo de certificados. **MODO_CONSTRUCCION**: público."
      parameters:
        - $ref: "#/components/parameters/IdPath"
      responses:
        "200":
          description: Alumno encontrado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data: { $ref: "#/components/schemas/Alumno" }
        "404":
          description: Alumno no encontrado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
    patch:
      tags: [Alumnos]
      summary: Actualizar alumno
      description: Modifica campos editables. Solo la OTEC dueña (o SuperAdmin) puede editar.
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/IdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                nombre: { type: string }
                apellido: { type: string }
                email: { type: string, format: email }
                telefono: { type: string }
                wallet_address: { type: string }
      responses:
        "200":
          description: Alumno actualizado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  message: { type: string }
                  data: { $ref: "#/components/schemas/Alumno" }
        "401": { description: Falta API Key. }
        "403": { description: No pertenece a la OTEC. }
        "404": { description: Alumno no encontrado. }

  /api/cursos:
    get:
      tags:
        - Cursos
      summary: Listar cursos de la OTEC
      description: Retorna la lista de todos los cursos registrados por la OTEC autenticada. Opcionalmente se puede filtrar por código de curso.
      operationId: listarCursos
      security:
        - ApiKeyAuth: []
      parameters:
        - name: codigo
          in: query
          required: false
          schema:
            type: string
          description: Filtrar por código del curso (opcional).
          example: "WEB3-STELLAR"
      responses:
        "200":
          description: Lista de cursos obtenida exitosamente.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/CursoResponse"
        "401":
          description: API Key faltante o inválida.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Error interno del servidor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    post:
      tags:
        - Cursos
      summary: Registrar un curso
      description: |
        Crea un curso asociado a la OTEC autenticada. El código de curso debe ser único dentro de la misma OTEC. Si el curso ya existe (mismo código), retorna los datos existentes.

        **Ejemplo curl:**
        ```bash
        curl -X POST https://certi.browns.studio/api/cursos \
          -H "Content-Type: application/json" \
          -H "X-API-KEY: tu-api-key" \
          -d '{
            "codigo": "WEB3-STELLAR",
            "nombre": "Desarrollo Web3 con Stellar",
            "horas": 30,
            "linea_academica": "Tecnología"
          }'
        ```
      operationId: registrarCurso
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CursoInput"
            example:
              codigo: "WEB3-STELLAR"
              nombre: "Desarrollo Web3 con Stellar"
              horas: 30
              linea_academica: "Tecnología"
              descripcion: "Curso práctico de desarrollo sobre la red Stellar."
              link_programa: "https://cercaotec.cl/programa-web3"
      responses:
        "201":
          description: Curso registrado con éxito.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Curso registrado con éxito."
                  data:
                    $ref: "#/components/schemas/CursoResponse"
        "200":
          description: El curso ya existía previamente (retorna datos existentes).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Curso ya registrado previamente."
                  data:
                    $ref: "#/components/schemas/CursoResponse"
        "400":
          description: Faltan campos obligatorios o formato inválido.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: API Key faltante o inválida.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Duplicidad de código dentro de la misma OTEC.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Error interno del servidor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/cursos/{id}:
    get:
      tags: [Cursos]
      summary: Detalle de curso (Modo Construcción)
      description: "Retorna la información completa de un curso, con su OTEC y conteo de certificados. **MODO_CONSTRUCCION**: público."
      parameters:
        - $ref: "#/components/parameters/IdPath"
      responses:
        "200":
          description: Curso encontrado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data: { $ref: "#/components/schemas/Curso" }
        "404": { description: Curso no encontrado. }
    patch:
      tags: [Cursos]
      summary: Actualizar curso
      description: Modifica campos editables (incluido toggle `activo`). Solo la OTEC dueña (o SuperAdmin) puede editar.
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/IdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                nombre: { type: string }
                horas: { type: integer, minimum: 1 }
                linea_academica: { type: string }
                descripcion: { type: string, nullable: true }
                link_programa: { type: string, nullable: true }
                activo: { type: boolean }
      responses:
        "200": { description: Curso actualizado. }
        "401": { description: Falta API Key. }
        "403": { description: No pertenece a la OTEC. }
        "404": { description: Curso no encontrado. }
    delete:
      tags: [Cursos]
      summary: Baja lógica de curso
      description: "Marca `activo: false`. No elimina físicamente. Los certificados ya emitidos quedan intactos y validables."
      security: [{ ApiKeyAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/IdPath"
      responses:
        "200":
          description: Curso desactivado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  message: { type: string }
                  data: { $ref: "#/components/schemas/Curso" }
        "401": { description: Falta API Key. }
        "403": { description: No pertenece a la OTEC. }
        "404": { description: Curso no encontrado. }

  /api/auth/login:
    post:
      tags:
        - Autenticación
      summary: Iniciar sesión de OTEC
      description: |
        Autentica a una OTEC con email y contraseña. Retorna los datos de la sesión incluyendo la `api_key` para autenticar las peticiones subsiguientes.

        **Ejemplo curl:**
        ```bash
        curl -X POST https://certi.browns.studio/api/auth/login \
          -H "Content-Type: application/json" \
          -d '{
            "email": "contacto@cercaotec.cl",
            "password": "MiContraseñaSegura123"
          }'
        ```
      operationId: loginOtec
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginInput"
      responses:
        "200":
          description: Sesión iniciada con éxito.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Sesión iniciada con éxito."
                  data:
                    $ref: "#/components/schemas/LoginResponse"
        "400":
          description: Faltan campos obligatorios.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Credenciales incorrectas o cuenta inactiva.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "Credenciales incorrectas o la cuenta no existe."
        "500":
          description: Error interno del servidor.
          content:
            application/json:
              schema:

  /api/auth/logout:
    post:
      tags: [Autenticación]
      summary: Cierre de sesión
      description: |
        Endpoint de logout. En la arquitectura actual (API Key en `localStorage` del cliente),
        el cierre real lo hace el cliente eliminando `localStorage`. Este endpoint existe como
        punto consistente del protocolo y para futuras sesiones server-side (JWT httpOnly cookie).
      responses:
        "200":
          description: Sesión cerrada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  message: { type: string }

  /api/auth/superadmin-login:
    post:
      tags: [Autenticación]
      summary: Inicio de sesión SuperAdmin
      description: |
        Endpoint dedicado para iniciar sesión como SuperAdmin y obtener la API Key real desde la BD.
        Si `SUPERADMIN_PASSWORD` está definida en `.env`, se valida el `password` del body.
        Si no (modo construcción), se acepta cualquier payload.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                password: { type: string, description: "Requerido si SUPERADMIN_PASSWORD está definida en env." }
      responses:
        "200":
          description: Sesión SuperAdmin iniciada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  message: { type: string }
                  data: { $ref: "#/components/schemas/Otec" }
        "401": { description: Credenciales inválidas. }
        "404": { description: No existe la cuenta SuperAdmin en la BD. }
