openapi: 3.0.3
info:
  title: CertiLink API
  description: |
    ## 🚀 INICIO RÁPIDO

    Emite un certificado completo (alumno + curso + registro on-chain) en **una sola llamada** a
    `POST /api/certificados/emision-completa` con tu cabecera `X-API-KEY`.

    **Request:**
    ```json
    {
      "nombreAlumno": "Juan Pérez",
      "rutAlumno": "18.765.432-1",
      "emailAlumno": "juan@tu-escuela.cl",
      "nombreCurso": "Curso de Diseño Web",
      "codigoCurso": "DISENO-WEB-001",
      "horas": 40,
      "lineaAcademica": "Tecnología e Innovación",
      "linkPrograma": "https://tu-escuela.cl/programas/diseno-web",
      "fechaInicio": "2026-07-01",
      "fechaFin": "2026-07-15",
      "nota": 85
    }
    ```

    **Response (`201 Created`):**
    ```json
    {
      "success": true,
      "message": "Certificado emitido y registrado en Stellar on-chain con éxito.",
      "data": {
        "id": "153aba5f-2b40-4dac-8779-79cc65db73ba",
        "hash_sha256": "8642df32ef9015679e2d5e705234b24d24884d6c75c176f59245ad884420dcee",
        "tx_hash": "197f0355e7ededc8dc97af1954be54235626ebb6872c8a0eacd20cce8dfdd7eb",
        "soroban_tx_hash": null,
        "soroban_status": "processing",
        "validar_url": "https://certilink.co/validar/153aba5f-2b40-4dac-8779-79cc65db73ba",
        "ledger": 3336820,
        "fee_payer": "CertiLink"
      }
    }
    ```

    ---

    **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

    ### ⭐ Para pruebas iniciales (recomendado)

    ```
    1. POST /api/certificados/emision-completa  → Todo en una peticion: alumno + curso + cert on-chain
    2. GET  /api/certificados/{id}              → Validar certificado emitido (publico)
    ```

    ### Flujo clasico (cuando ya gestionas catálogos)

    ```
    1. POST /api/otecs           → Registrar la OTEC (obtener api_key)
    2. POST /api/auth/login      → Iniciar sesión (obtener api_key si no se tiene)
    3. POST /api/alumnos         → Registrar alumnos
    4. POST /api/cursos          → Registrar cursos
    5. POST /api/certificados    → Emitir certificados on-chain (requiere alumno_id + curso_id)
    6. GET  /api/certificados/{id} → Validar certificados (público)
    ```

    ## 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.2.0
  contact:
    name: Equipo CertiLink
    url: https://certilink.co

servers:
  - url: https://certilink.co
    description: Producción (Vercel + Supabase + Stellar Testnet)
  - url: http://localhost:3000
    description: Desarrollo local

tags:
  - name: ⭐ Emisión de Certificado
    description: >-
      Endpoint principal. Emite un certificado completo (alumno + curso + registro on-chain)
      en una sola llamada. Empieza aquí.
  - name: Sellado de Documentos Externos
    description: >-
      Para OTECs que ya tienen su propio sistema de generación de certificados.
      Descarga el documento desde una URL, calcula su huella SHA-256 y la ancla
      en Stellar Horizon. Devuelve la URL pública de validación con el documento
      embebido y la prueba blockchain.
  - name: Endpoints Avanzados
    description: >-
      Gestión granular de OTECs, alumnos, cursos, certificados, validación pública y
      autenticación. Úsalos cuando administras catálogos por separado.

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@tu-escuela.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://tu-escuela.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@tu-escuela.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://tu-escuela.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://tu-escuela.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

    EmisionCompletaAlumnoInput:
      type: object
      required: [rut, nombre, apellido, email]
      properties:
        rut: { type: string, example: "18.765.432-1" }
        nombre: { type: string, example: "Juan" }
        apellido: { type: string, example: "Perez" }
        email: { type: string, format: email, example: "juan@certilink.cl" }
        telefono: { type: string, example: "+56987654321" }
        wallet_address: { type: string, nullable: true, description: "Opcional. Si se omite se autogenera en Stellar Testnet." }

    EmisionCompletaCursoInput:
      type: object
      required: [codigo, nombre, horas, linea_academica]
      properties:
        codigo: { type: string, example: "CURSO-BLOCKCHAIN-001" }
        nombre: { type: string, example: "Diplomado Blockchain Stellar" }
        horas: { type: integer, minimum: 1, example: 45 }
        linea_academica: { type: string, example: "Tecnología Blockchain" }
        descripcion: { type: string, nullable: true }
        link_programa: { type: string, format: uri, nullable: true }

    EmisionCompletaInput:
      type: object
      required: [alumno, curso, fecha_fin, nota]
      properties:
        alumno:
          $ref: "#/components/schemas/EmisionCompletaAlumnoInput"
        curso:
          $ref: "#/components/schemas/EmisionCompletaCursoInput"
        fecha_fin:
          type: string
          format: date
          example: "2026-07-15"
        nota:
          type: integer
          minimum: 0
          maximum: 100
          example: 87
        metadata:
          type: object
          description: "Metadatos libres. Se recomienda incluir image_url. Las claves instructor/relator se descartan al emitir."
          example:
            image_url: "https://certilink.co/assets/img/mock-certificate.png"

    EmisionCompletaFlatInput:
      type: object
      description: |
        Formato PLANO (nuevo) aceptado por `emision-completa`. Alternativa al formato anidado.
        Los campos descriptivos nuevos (`descripcionLineaAcademica`, `bannerEscuela`, `linkPrograma`,
        `horas`, `lineaAcademica`, `fechaInicio`) se persisten en el `metadata` del certificado.
        `nombreAlumno` se divide en nombre + apellido (primer token = nombre).
      required: [nombreAlumno, rutAlumno, emailAlumno, nombreCurso, codigoCurso, horas, lineaAcademica, fechaFin, nota]
      properties:
        nombreAlumno: { type: string, example: "Juan Andrés Pérez González" }
        rutAlumno: { type: string, example: "18.765.432-1" }
        emailAlumno: { type: string, format: email, example: "juan@certilink.cl" }
        telefonoAlumno: { type: string, nullable: true, example: "+56987654321" }
        walletAlumno: { type: string, nullable: true, description: "Opcional. Si se omite se autogenera en Stellar Testnet." }
        nombreCurso: { type: string, example: "Diplomado Blockchain Stellar" }
        codigoCurso: { type: string, example: "CURSO-BLOCKCHAIN-001" }
        horas: { type: integer, minimum: 1, example: 45 }
        lineaAcademica: { type: string, example: "Tecnología e Innovación" }
        linkPrograma: { type: string, format: uri, nullable: true, example: "https://tu-escuela.cl/programas/stellar" }
        descripcionLineaAcademica: { type: string, nullable: true, example: "Programas de competencias digitales avanzadas en blockchain y Web3." }
        bannerEscuela: { type: string, format: uri, nullable: true, example: "https://tu-escuela.cl/assets/img/banner-escuela.png" }
        descripcionCurso: { type: string, nullable: true }
        fechaInicio: { type: string, format: date, nullable: true, example: "2026-07-01" }
        fechaFin: { type: string, format: date, example: "2026-07-15" }
        nota: { type: integer, minimum: 0, maximum: 100, example: 85 }
        metadata:
          type: object
          nullable: true
          description: "Metadatos libres adicionales. instructor/relator se descartan al emitir."

    EmisionCompletaRecuroResult:
      type: object
      properties:
        id: { type: string, format: uuid }
        creado_en_esta_peticion:
          type: boolean
          description: "true si el recurso fue creado en esta peticion. false si ya existia y se reutilizo."

    EmisionCompletaResponse:
      type: object
      properties:
        success: { type: boolean, example: true }
        message: { type: string }
        data:
          type: object
          properties:
            id: { type: string, format: uuid }
            hash_sha256: { type: string }
            tx_hash: { type: string }
            soroban_tx_hash: { type: string, nullable: true }
            validar_url: { type: string, format: uri }
            ledger: { type: integer }
            fee_payer: { type: string, example: "CertiLink" }
            alumno:
              allOf:
                - $ref: "#/components/schemas/EmisionCompletaRecuroResult"
                - type: object
                  properties:
                    rut: { type: string }
                    nombre_completo: { type: string }
                    email: { type: string }
                    wallet_address: { type: string, nullable: true }
            curso:
              allOf:
                - $ref: "#/components/schemas/EmisionCompletaRecuroResult"
                - type: object
                  properties:
                    codigo: { type: string }
                    nombre: { type: string }
                    horas: { type: integer }
                    linea_academica: { type: string }

    SellarDocumentoInput:
      type: object
      required:
        - urlCertificado
        - nombreAlumno
        - rutAlumno
        - nombreCurso
        - fechaEmision
      properties:
        urlCertificado:
          type: string
          format: uri
          description: |
            URL pública del certificado ya generado. Formatos aceptados: PDF, PNG, JPG/JPEG, WEBP.
            Debe ser accesible públicamente desde los servidores de CertiLink (sin autenticación).
            El archivo se descarga, hashea y almacena en Vercel Blob con URL propia de CertiLink.
          example: "https://escuela.cl/certs/maria.pdf"
        nombreAlumno:
          type: string
          description: Nombre completo del alumno. El primer token se usa como nombre y el resto como apellido.
          example: "María González"
        rutAlumno:
          type: string
          description: RUT del alumno (formato chileno).
          example: "12345678-9"
        nombreCurso:
          type: string
          description: Nombre del curso o programa que acredita el certificado.
          example: "Diseño Web"
        fechaEmision:
          type: string
          format: date
          description: Fecha en que fue emitido el certificado original (ISO 8601).
          example: "2026-06-30"
        emailAlumno:
          type: string
          format: email
          description: Correo electrónico del alumno (opcional). Si se omite se genera un placeholder interno.
          example: "maria@email.com"

    SellarDocumentoResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        certificado:
          type: object
          properties:
            id:
              type: string
              format: uuid
              description: UUID del certificado sellado.
            codigo:
              type: string
              description: Código legible del certificado (formato CERT-YYYY-XXXX).
              example: "CERT-2026-A3F8B21C"
            hashSHA256:
              type: string
              description: Huella SHA-256 del contenido binario del documento descargado.
              example: "a6d535b3c4ef..."
            txHash:
              type: string
              description: Hash de la transacción en Stellar Horizon donde se anclaron los datos.
              example: "76e5e19c..."
            urlValidacion:
              type: string
              format: uri
              description: URL pública de validación. Muestra el documento desde Vercel Blob + prueba blockchain.
              example: "https://certilink.co/validar/a3f8b21c-..."
            urlCertificadoOriginal:
              type: string
              format: uri
              description: URL original del documento provista por la OTEC.
              example: "https://escuela.cl/certs/maria.pdf"
            urlCertificadoAlmacenado:
              type: string
              format: uri
              description: |
                URL permanente en Vercel Blob (certilink.co). Es la que se usa en el visor
                embebido de /validar/[id]. Existe independientemente de si la URL original sigue activa.
              example: "https://xxxxxxxx.blob.vercel-storage.com/certificados/sellado-..."
            estado:
              type: string
              example: "sellado"

    # 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/emision-completa:
    post:
      tags:
        - ⭐ Emisión de Certificado
      summary: "⭐ ENDPOINT PRINCIPAL — Emite un certificado completo en una sola llamada"
      description: |
        ## ⭐ ENDPOINT PRINCIPAL — Emite un certificado completo en una sola llamada

        ### Endpoint recomendado para primeras pruebas y demos

        Crea o reutiliza tanto el **alumno** como el **curso** asociados a la OTEC autenticada
        y emite el **certificado** en la blockchain de Stellar, todo en una **única petición**.

        **¿Para qué sirve?**
        - Para probar el flujo end-to-end sin necesidad de rescatar previamente `alumno_id` y `curso_id`.
        - Ideal para integraciones que no quieren mantener catálogos separados.
        - Recibe datos humanos (RUT del alumno, código del curso) en lugar de UUIDs opacos.

        **¿Qué hace internamente?**
        1. Autentica la OTEC via `X-API-KEY`.
        2. **Upsert del alumno**: si no existe (mismo `otec_id` + `rut`) → lo crea (con wallet Stellar
           autogenerada y fondeada en Testnet); si ya existe → lo reutiliza sin sobrescribir.
        3. **Upsert del curso**: si no existe (mismo `otec_id` + `codigo`) → lo crea; si ya existe → lo reutiliza.
        4. Calcula el hash SHA-256 del certificado y verifica unicidad (409 si ya fue emitido).
        5. **Registra on-chain** en Stellar Horizon (Memo `hash` + `ManageData` + pago simbólico 0 XLM).
        6. **Persiste** el certificado en la base de datos (vinculando los IDs resueltos en pasos 2-3).
        7. **Mintea NFT Soulbound** en Soroban (asíncrono, no bloqueante).
        8. **Envía email** de notificación al alumno (asíncrono, no bloqueante).
        9. Devuelve la respuesta consolidada con todos los IDs y URLs.

        ⚠️ **No interfiere** con los endpoints `/api/alumnos`, `/api/cursos` ni `/api/certificados`
        originales. El modelo de datos en Prisma no se modifica: el endpoint solo hace upserts
        contra el schema existente.

        **Reglas de negocio** (se aplican igual que en el endpoint clásico):
        - La `nota` debe estar entre 0 y 100.
        - La `fecha_fin` no puede ser anterior a la fecha de emisión (hoy).
        - No se permiten certificados duplicados (mismo hash SHA-256). Si el certificado ya existe
          se responde `409` con el `validar_url` del certificado previo.
        - El alumno y el curso quedan asociados automáticamente a la OTEC autenticada (perímetro tenant).

      operationId: emitirCertificadoCompleto
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        description: |
          Acepta DOS formatos: el **plano** (nuevo, `EmisionCompletaFlatInput`) y el **anidado** legacy
          (`EmisionCompletaInput`). Usa el que prefieras.
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/EmisionCompletaFlatInput"
                - $ref: "#/components/schemas/EmisionCompletaInput"
            examples:
              planoNuevo:
                summary: Formato plano (nuevo, recomendado)
                value:
                  nombreAlumno: "Juan Andrés Pérez González"
                  rutAlumno: "18.765.432-1"
                  emailAlumno: "juan@certilink.cl"
                  nombreCurso: "Diplomado Blockchain Stellar"
                  codigoCurso: "CURSO-BLOCKCHAIN-001"
                  horas: 45
                  lineaAcademica: "Tecnología e Innovación"
                  linkPrograma: "https://tu-escuela.cl/programas/stellar"
                  descripcionLineaAcademica: "Programas de competencias digitales avanzadas en blockchain y Web3."
                  bannerEscuela: "https://tu-escuela.cl/assets/img/banner-escuela.png"
                  fechaInicio: "2026-07-01"
                  fechaFin: "2026-07-15"
                  nota: 85
              anidadoLegacy:
                summary: Formato anidado (legacy)
                value:
                  alumno:
                    rut: "18.765.432-1"
                    nombre: "Juan"
                    apellido: "Perez"
                    email: "juan@certilink.cl"
                    telefono: "+56987654321"
                  curso:
                    codigo: "CURSO-BLOCKCHAIN-001"
                    nombre: "Diplomado Blockchain Stellar"
                    horas: 45
                    linea_academica: "Tecnología Blockchain"
                    descripcion: "Curso intensivo sobre Stellar y Soroban."
                    link_programa: "https://certilink.co/programas/stellar"
                  fecha_fin: "2026-07-15"
                  nota: 87
                  metadata:
                    image_url: "https://certilink.co/assets/img/mock-certificate.png"
      responses:
        "201":
          description: Certificado emitido y registrado en Stellar on-chain con éxito.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmisionCompletaResponse"
        "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"
        "409":
          description: Ya existe un certificado idéntico (duplicidad de hash SHA-256).
          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 }
                      validar_url: { type: string, format: uri }
        "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:
    get:
      tags:
        - Endpoints Avanzados
      summary: Listar certificados de la OTEC
      description: |
        Requiere `X-API-KEY`. Retorna los certificados emitidos por la OTEC autenticada,
        incluyendo datos del alumno y curso asociados. La cuenta SuperAdmin ve los de todas
        las OTECs y puede acotar con `otec_id` (parámetro ignorado para OTECs normales).

        Soporta filtro por `estado` y paginación opcional (`page`, `limit`).

        **Ejemplo curl:**
        ```bash
        curl https://certilink.co/api/certificados \
          -H "X-API-KEY: tu-api-key"
        ```
      operationId: listarCertificados
      security:
        - ApiKeyAuth: []
      parameters:
        - in: query
          name: estado
          required: false
          schema: { type: string, enum: [emitido, revocado] }
          description: Filtra por estado del certificado.
        - in: query
          name: otec_id
          required: false
          schema: { type: string, format: uuid }
          description: Solo SuperAdmin. Acota el listado a una OTEC emisora.
        - in: query
          name: page
          required: false
          schema: { type: integer, minimum: 1, default: 1 }
          description: Número de página (paginación opcional).
        - in: query
          name: limit
          required: false
          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: 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:
        - Endpoints Avanzados
      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://certilink.co/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://tu-escuela.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:
        - Endpoints Avanzados
      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:
        - Endpoints Avanzados
      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:
        - Endpoints Avanzados
      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/otecs:
    get:
      tags:
        - Endpoints Avanzados
      summary: Listar OTECs (autenticado)
      description: |
        Requiere `X-API-KEY` válida. Una OTEC normal recibe **solo su propio registro**;
        la cuenta SuperAdmin recibe **todas** las OTECs de la plataforma. No expone claves sensibles.
      operationId: listarOtecs
      security:
        - ApiKeyAuth: []
      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"
        "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:
        - Endpoints Avanzados
      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://certilink.co/api/otecs \
          -H "Content-Type: application/json" \
          -d '{
            "rut": "99.999.888-7",
            "nombre": "OTEC Capacitación Limitada",
            "email": "contacto@tu-escuela.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@tu-escuela.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: [Endpoints Avanzados]
      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: [Endpoints Avanzados]
      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: [Endpoints Avanzados]
      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:
        - Endpoints Avanzados
      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@tu-escuela.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:
        - Endpoints Avanzados
      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:
        - Endpoints Avanzados
      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://certilink.co/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: [Endpoints Avanzados]
      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: [Endpoints Avanzados]
      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:
        - Endpoints Avanzados
      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:
        - Endpoints Avanzados
      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://certilink.co/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://tu-escuela.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: [Endpoints Avanzados]
      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: [Endpoints Avanzados]
      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: [Endpoints Avanzados]
      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:
        - Endpoints Avanzados
      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://certilink.co/api/auth/login \
          -H "Content-Type: application/json" \
          -d '{
            "email": "contacto@tu-escuela.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: [Endpoints Avanzados]
      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: [Endpoints Avanzados]
      summary: Inicio de sesión SuperAdmin
      description: |
        Endpoint dedicado para iniciar sesión como SuperAdmin y obtener la API Key real desde la BD.
        **Siempre** valida el `password` del body contra `SUPERADMIN_PASSWORD` (env); si el env no
        está definido, se usa un valor por defecto. No existe bypass: sin password correcto → `401`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password: { type: string, description: "Contraseña de SuperAdmin. Obligatoria." }
      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. }

  /api/certificados/sellar:
    post:
      tags:
        - Sellado de Documentos Externos
      summary: "Sellar un certificado externo ya generado en Stellar"
      description: |
        ## Sellado de Documentos Externos

        Endpoint para **OTECs que ya tienen su propio sistema de generación de certificados** y solo
        necesitan que CertiLink registre la prueba de existencia e integridad on-chain.

        ### ¿Qué hace internamente?

        1. Autentica la OTEC via `X-API-KEY`.
        2. **Descarga el documento** desde `urlCertificado` (PDF, PNG, JPG o WEBP).
        3. **Calcula el hash SHA-256** del contenido binario descargado.
        4. Verifica que el documento no haya sido sellado previamente (`409` si ya existe; el Blob no se consume).
        5. **Sube el archivo a Vercel Blob** con una URL propia de CertiLink (permanente, independiente del servidor de la OTEC).
        6. **Crea o reutiliza** el registro del alumno en la base de datos (por `otec_id` + `rutAlumno`).
        7. **Crea o reutiliza** el registro del curso (usando un slug estable de `nombreCurso`).
        8. **Registra el hash** como Memo en una transacción Stellar Horizon.
        9. **Persiste** el certificado en la base de datos con la URL de Blob en `metadata.image_url`.
        10. **Mintea NFT Soulbound** en Soroban (asíncrono, no bloqueante).
        11. Devuelve la URL de validación pública junto a ambas URLs del certificado.

        ### Página /validar/[id]

        La URL de validación retornada muestra automáticamente:
        - El documento embebido desde **Vercel Blob** (`<iframe>` para PDF, `<img>` para imágenes).
          Permanece visible aunque la OTEC borre el archivo de su servidor.
        - La prueba blockchain: hash SHA-256, tx Stellar Horizon, estado on-chain en tiempo real.

        No se requiere ninguna modificación en el frontend.

        ### Límites y formatos soportados

        - Formatos aceptados: **PDF, PNG, JPG/JPEG, WEBP**.
        - Tamaño máximo: **20 MB**.
        - Timeout de descarga: **20 segundos**.
        - La URL de `urlCertificado` debe ser **pública y accesible** desde los servidores de CertiLink.

        **Ejemplo curl:**
        ```bash
        curl -X POST https://certilink.co/api/certificados/sellar \\
          -H "Content-Type: application/json" \\
          -H "X-API-KEY: tu-api-key" \\
          -d '{
            "urlCertificado": "https://escuela.cl/certs/maria.pdf",
            "nombreAlumno": "María González",
            "rutAlumno": "12345678-9",
            "nombreCurso": "Diseño Web",
            "fechaEmision": "2026-06-30",
            "emailAlumno": "maria@email.com"
          }'
        ```
      operationId: sellarDocumentoExterno
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SellarDocumentoInput"
            example:
              urlCertificado: "https://escuela.cl/certs/maria.pdf"
              nombreAlumno: "María González"
              rutAlumno: "12345678-9"
              nombreCurso: "Diseño Web"
              fechaEmision: "2026-06-30"
              emailAlumno: "maria@email.com"
      responses:
        "201":
          description: Documento sellado y registrado en Stellar on-chain con éxito.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SellarDocumentoResponse"
              example:
                success: true
                certificado:
                  id: "a3f8b21c-5d9e-4f12-8b3c-7e2a1d0c9f4b"
                  codigo: "CERT-2026-A3F8B21C"
                  hashSHA256: "a6d535b3c4ef8d9a2f1b7e0c3d4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2"
                  txHash: "76e5e19c8d7f3a2b1c4e5d6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6"
                  urlValidacion: "https://certilink.co/validar/a3f8b21c-5d9e-4f12-8b3c-7e2a1d0c9f4b"
                  urlCertificadoOriginal: "https://escuela.cl/certs/maria.pdf"
                  urlCertificadoAlmacenado: "https://abc123.blob.vercel-storage.com/certificados/sellado-1751400000000-a1b2c3d4.pdf"
                  estado: "sellado"
        "400":
          description: |
            Campos faltantes, URL inválida, documento no accesible, timeout de descarga,
            o documento supera el límite de 50 MB.
          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: Este documento ya fue sellado previamente (hash SHA-256 duplicado).
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  data:
                    type: object
                    properties:
                      id: { type: string, format: uuid }
                      codigo: { type: string }
                      hashSHA256: { type: string }
                      txHash: { type: string }
                      urlValidacion: { type: string, format: uri }
                      urlCertificadoOriginal: { type: string, format: uri }
        "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"
