openapi: 3.0.3
info:
  title: ZUEI API
  description: |
    API da ZUEI — áudios de zoeira personalizados com IA.

    Aplicação Next.js (App Router). Autenticação: sessão Supabase via cookie
    de navegador (`sb-*`) OU cookie de convidado `zuei_guest` (criado pelo
    servidor na primeira chamada). Criar/gerar/pagar não exigem login; as
    rotas filtram por dono = usuário logado ou cookie de convidado. Os webhooks (`/api/suno/callback`, `/api/pix/webhook`)
    são públicos e chamados por provedores externos.

    Base de produção: https://zuei.vercel.app
  version: 1.0.0
  contact:
    name: Giosimar / ZUEI
servers:
  - url: https://zuei.vercel.app
    description: Produção
  - url: http://localhost:3000
    description: Local (desenvolvimento)

tags:
  - name: Criações
    description: Criações de zoeira (questionário → geração → áudio)
  - name: Geração de Áudio
    description: Submissão ao Suno e callback de áudio pronto
  - name: Pagamento PIX
    description: Checkout e status do PIX (EMV)
  - name: Webhooks
    description: Callbacks de provedores externos (Suno, PIX)
  - name: Auth
    description: Sessão

paths:
  /api/creations:
    post:
      tags: [Criações]
      summary: Cria um registro de criação (draft)
      description: |
        Cria a criação a partir do questionário. Não exige login: visitantes são
        identificados pelo cookie `zuei_guest` e têm limite diário
        (`GUEST_DAILY_LIMIT`, padrão 3; `IP_DAILY_LIMIT` por IP, padrão 10) —
        excedido responde 429 `{ error, code: guest_limit | ip_limit }`.
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [questionnaireData]
              properties:
                questionnaireData:
                  $ref: "#/components/schemas/QuestionnaireData"
      responses:
        "201":
          description: Criação criada
          content:
            application/json:
              schema:
                type: object
                required: [creation]
                properties:
                  creation:
                    $ref: "#/components/schemas/Creation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
    get:
      tags: [Criações]
      summary: Lista criações do usuário ou busca pública por slug
      description: |
        Sem `slug`: retorna as criações do usuário autenticado, ordenadas por
        data (mais recentes primeiro). Com `slug`: pública — retorna a criação
        com `status=completed` pelo slug (página de compartilhamento).
      security:
        - {}
        - sessionCookie: []
      parameters:
        - name: slug
          in: query
          description: Slug público da criação (compartilhamento)
          required: false
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [creations]
                    properties:
                      creations:
                        type: array
                        items:
                          $ref: "#/components/schemas/Creation"
                  - type: object
                    required: [creation]
                    properties:
                      creation:
                        $ref: "#/components/schemas/Creation"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Slug não encontrado ou criação não concluída
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/generate:
    post:
      tags: [Geração de Áudio]
      summary: Submete a geração da zoeira no Suno
      description: |
        Autenticado. A OpenAI (`OPENAI_API_KEY`) escreve letra curta, `style` e
        `title`; a geração é submetida ao Suno em `customMode=true` com
        `duration` (`SUNO_TARGET_SECONDS`, só em `SUNO_MODEL=V5_5`) e
        `callBackUrl`. Sem OpenAI cai em `customMode=false` (letra pelo Suno,
        duração livre). Grava `sunoTaskId`, `sunoMode`, `sunoStyle` e a letra
        (`script`) na criação (status `generating`). Acompanhe em `GET /api/generate/status` — o
        callback em `/api/suno/callback` também grava o áudio quando chega.
        Pode ser chamado de novo para uma criação `failed` (retry).
        Erros do Suno (ex.: 429 sem créditos) marcam a criação como `failed`
        e retornam 502 com a mensagem. Com `SUNO_MOCK=true` devolve o áudio de
        demonstração (`mock: true`).
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [creationId]
              properties:
                creationId:
                  type: string
                  format: uuid
                  example: 228f0f5e-a9a2-4a3b-9a1c-3d0f9e8c7b6a
      responses:
        "200":
          description: Submetido ou concluído
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [submitted, taskId]
                    properties:
                      submitted:
                        type: boolean
                        const: true
                      taskId:
                        type: string
                        description: ID da tarefa no Suno
                      stage:
                        $ref: "#/components/schemas/GenerationStage"
                  - type: object
                    required: [creation]
                    properties:
                      creation:
                        $ref: "#/components/schemas/Creation"
                      stage:
                        $ref: "#/components/schemas/GenerationStage"
                      mock:
                        type: boolean
                        description: true quando caiu no áudio de demonstração
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Criação não encontrada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "502":
          description: O Suno recusou o pedido (criação marcada como `failed`)
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error:
                    type: string
                    example: "Créditos do Suno esgotados. Recarregue a conta em sunoapi.org."
                  sunoCode:
                    type: integer
                    description: "Código de erro do Suno (400, 401, 413, 429, 430, 455, 500)"
                    example: 429
                  stage:
                    type: string
                    const: failed

  /api/generate/status:
    get:
      tags: [Geração de Áudio]
      summary: Consulta o progresso da geração (polling)
      description: |
        Autenticado. Consulta o Suno em `GET /api/v1/generate/record-info?taskId=`
        e sincroniza a criação no banco: grava `audio_url`/`cover_image_url` e
        marca `completed` quando alguma faixa tem `audio_url`; marca `failed`
        nos status `CREATE_TASK_FAILED`, `GENERATE_AUDIO_FAILED`,
        `CALLBACK_EXCEPTION` e `SENSITIVE_WORD_ERROR`. O front chama a cada 4s.
      security:
        - sessionCookie: []
      parameters:
        - name: creationId
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Estado atual
          content:
            application/json:
              schema:
                type: object
                required: [creation, stage]
                properties:
                  creation:
                    $ref: "#/components/schemas/Creation"
                  stage:
                    $ref: "#/components/schemas/GenerationStage"
                  message:
                    type: string
                    description: Texto amigável do estágio ou motivo da falha
                  transient:
                    type: boolean
                    description: true quando a consulta ao Suno falhou temporariamente (mantém `generating`)
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Criação não encontrada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/suno/callback:
    get:
      tags: [Webhooks]
      summary: Health check do callback do Suno
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    const: ok
    post:
      tags: [Webhooks]
      summary: Callback do Suno com o áudio pronto
      description: |
        Chamado pelo Suno quando a geração termina. Casado com a criação pelo
        `task_id` (gravado no `questionnaire_data.sunoTaskId`). Atualiza
        `audio_url` e marca `status=completed`. Sempre responde 200 para o
        provedor não reenviar.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SunoCallback"
      responses:
        "200":
          description: Recebido (ignorado, concluído ou não encontrado)
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [received, ignored, not_found]

  /api/checkout:
    post:
      tags: [Pagamento PIX]
      summary: Gera cobrança PIX da zoeira (R$ 9,90)
      description: |
        Autenticado. Verifica a criação do usuário, gera o EMV via
        `/api/pix/generate` e cria um registro de pedido (`orders`).
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [creationId]
              properties:
                creationId:
                  type: string
                  format: uuid
                  example: 228f0f5e-a9a2-4a3b-9a1c-3d0f9e8c7b6a
      responses:
        "200":
          description: Dados do PIX
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PixCodeResponse"
        "400":
          description: Falta creationId ou a criação já foi paga
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Criação não encontrada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: "Falha ao gerar PIX (ex.: PIX_KEY não configurada)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/pix/generate:
    post:
      tags: [Pagamento PIX]
      summary: Gera código PIX copia e cola (EMV)
      description: |
        Gera o BR Code a partir da `PIX_KEY` do ambiente. Não exige sessão
        (chamado internamente pelo `/api/checkout`). Valor em centavos.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [creationId, amount]
              properties:
                creationId:
                  type: string
                  format: uuid
                amount:
                  type: integer
                  description: Valor em centavos (R$ 9,90 = 990)
                  example: 990
                description:
                  type: string
                  example: ZUEI - Zoeira para João
      responses:
        "200":
          description: Código PIX gerado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PixCodeResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          description: PIX_KEY não configurada ou falha na geração
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/pix/status:
    get:
      tags: [Pagamento PIX]
      summary: Consulta o status de uma cobrança PIX
      parameters:
        - name: txid
          in: query
          required: true
          description: TXID do pedido (retornado no checkout)
          schema:
            type: string
            example: 228f0f5ea9a24a3b9a1c3d0f9e8c7b6a
      responses:
        "200":
          description: Status do pedido
          content:
            application/json:
              schema:
                type: object
                required: [status, amount]
                properties:
                  status:
                    type: string
                    enum: [pending, paid, failed, refunded]
                  amount:
                    type: integer
                    description: Valor em centavos
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          description: Pedido não encontrado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/pix/webhook:
    post:
      tags: [Webhooks]
      summary: Callback de confirmação do PIX
      description: |
        Chamado pelo PSP/banco (ou manualmente via curl) quando o PIX é pago.
        Protegido por `PIX_WEBHOOK_SECRET`, aceito em `?token=`, no header
        `x-webhook-secret` ou em `Authorization: Bearer`. Casa o pedido pelo
        `txid` (`orders.pix_txid`, derivado do id da criação) e, quando pago,
        marca `orders.status=paid` e `creations.is_paid=true`. Idempotente.

        Aceita o formato próprio (`txid`/`status`/`amount`), o formato
        BACEN/Efí (`pix: [{ txid, valor, endToEndId }]`) e payloads genéricos
        (`txId`/`transactionId`/`reference` + `status` approved/confirmed/CONCLUIDA).
      parameters:
        - name: token
          in: query
          required: false
          schema:
            type: string
          description: "PIX_WEBHOOK_SECRET (alternativa ao header)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required: [txid, status]
                  properties:
                    txid:
                      type: string
                      description: TXID do pedido
                    status:
                      type: string
                      enum: [pending, paid, approved, confirmed, failed, rejected, expired]
                    amount:
                      type: number
                      description: "Valor pago em reais (9.90) ou centavos (990)"
                - type: object
                  required: [pix]
                  properties:
                    pix:
                      type: array
                      items:
                        type: object
                        properties:
                          txid:
                            type: string
                          valor:
                            type: string
                            example: "9.90"
                          endToEndId:
                            type: string
      responses:
        "200":
          description: Recebido
          content:
            application/json:
              schema:
                type: object
                properties:
                  received:
                    type: boolean
                    const: true
                  status:
                    type: string
                    enum: [paid, failed, pending, unknown]
                  already:
                    type: boolean
                    description: true quando o pedido já estava pago
                  creationId:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          description: Valor recebido menor que o do pedido
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Pedido não encontrado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/pix/swapo:
    get:
      tags: [Webhooks]
      summary: Health check do webhook da Swapo
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    const: ok
                  configured:
                    type: boolean
                    description: true quando SWAPO_WEBHOOK_SECRET está definido
    post:
      tags: [Webhooks]
      summary: Webhook de saída da Swapo (deposit.pix.confirmed)
      description: |
        Recebe os eventos da Swapo no padrão Standard Webhooks. Verifica a
        assinatura HMAC-SHA256 (`webhook-id`.`webhook-timestamp`.corpo cru) com
        `SWAPO_WEBHOOK_SECRET`, rejeita timestamp fora de ±300s, deduplica por
        `webhook-id` em `pix_deposits` e **responde 200 imediatamente**; a
        conciliação roda depois da resposta. O evento não traz txid, então o
        pedido é casado por valor + janela de tempo (`SWAPO_MATCH_WINDOW_HOURS`).
        Entregas inválidas também recebem 200 (`accepted: false`) e são descartadas.
      parameters:
        - name: webhook-id
          in: header
          required: true
          schema:
            type: string
        - name: webhook-timestamp
          in: header
          required: true
          schema:
            type: string
          description: Segundos desde a época Unix
        - name: webhook-signature
          in: header
          required: true
          schema:
            type: string
          description: "v1,<base64(HMAC-SHA256)>"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, type, version, createdAt, data]
              properties:
                id:
                  type: string
                type:
                  type: string
                  example: deposit.pix.confirmed
                version:
                  type: integer
                  example: 1
                createdAt:
                  type: string
                  format: date-time
                data:
                  type: object
                  properties:
                    transactionId:
                      type: string
                    status:
                      type: string
                      example: COMPLETED
                    amount:
                      type: string
                      example: "9.90"
                    currency:
                      type: string
                      example: BRL
                    method:
                      type: string
                      example: PIX
                    endToEndId:
                      type: string
                    pixIdIP:
                      type: string
                      nullable: true
                    pixKey:
                      type: string
                      nullable: true
                    occurredAt:
                      type: string
                      format: date-time
                    payer:
                      type: object
                      nullable: true
      responses:
        "200":
          description: Sempre 200. `accepted=false` indica entrega descartada (motivo em `reason`).
          content:
            application/json:
              schema:
                type: object
                required: [received, accepted]
                properties:
                  received:
                    type: boolean
                    const: true
                  accepted:
                    type: boolean
                  reason:
                    type: string
                    enum: [secret_not_configured, missing_headers, invalid_signature, stale_timestamp, invalid_json, invalid_envelope, database_not_configured]
                  id:
                    type: string
                  type:
                    type: string

  /api/auth/signout:
    get:
      tags: [Auth]
      summary: Encerra a sessão e redireciona para a home
      responses:
        "302":
          description: Redirect para a página inicial

components:
  securitySchemes:
    sessionCookie:
      type: apiKey
      in: cookie
      name: sb-auth-token
      description: Cookie de sessão do Supabase (definido pelo app após login).

  schemas:
    GenerationStage:
      type: string
      description: |
        Estágio da geração, derivado do `status` do Suno:
        PENDING→queued, TEXT_SUCCESS→lyrics, FIRST_SUCCESS→audio, SUCCESS→done,
        demais→failed. `idle` = criação ainda não submetida.
      enum: [idle, queued, lyrics, audio, done, failed]
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          example: Creation not found

    QuestionnaireData:
      type: object
      required: [victimName, scenario, details, style, tone, isAnonymous]
      properties:
        victimName:
          type: string
          example: João
        victimNickname:
          type: string
          example: Jão
        scenario:
          type: string
          enum:
            - fake_news
            - audio_whatsapp
            - parabens_ironico
            - convite_falso
            - desculpa_esfarrapada
            - elogio_bizarro
            - audio_chefe
            - meme_personalizado
        details:
          type: string
          description: Detalhes da vítima para personalizar a zoeira
          example: Ele odeia perder no futebol e vive falando que é o Pelé do bairro.
        style:
          type: string
          enum: [funk, trap, pagode, sertanejo, rock, eletronica, acustico, rap]
        tone:
          type: string
          enum: [leve, media, pesada, absurda]
        isAnonymous:
          type: boolean
        additionalNotes:
          type: string

    Creation:
      type: object
      properties:
        id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        slug:
          type: string
          example: zoeira-joao
        title:
          type: string
          example: Zoeira para João
        victim_name:
          type: string
        victim_nickname:
          type: string
          nullable: true
        scenario:
          type: string
        style:
          type: string
        tone:
          type: string
        is_anonymous:
          type: boolean
        status:
          type: string
          enum: [draft, generating, completed, failed]
        audio_url:
          type: string
          format: uri
          nullable: true
        preview_audio_url:
          type: string
          format: uri
          nullable: true
        cover_image_url:
          type: string
          format: uri
          nullable: true
        script:
          type: string
          nullable: true
        questionnaire_data:
          $ref: "#/components/schemas/QuestionnaireData"
        is_paid:
          type: boolean
        amount_paid:
          type: integer
        created_at:
          type: string
          format: date-time

    SunoCallback:
      type: object
      description: Payload enviado pelo Suno no callBackUrl
      properties:
        code:
          type: integer
          description: 200 = sucesso; outros = falha
          example: 200
        msg:
          type: string
          example: All generated successfully.
        data:
          type: object
          required: [callbackType, task_id]
          properties:
            callbackType:
              type: string
              enum: [text, first, complete, error]
            task_id:
              type: string
              description: ID da tarefa no Suno (mesmo do submit)
            data:
              type: array
              description: Faixas geradas (preenchido no tipo complete)
              items:
                $ref: "#/components/schemas/SunoTrack"

    SunoTrack:
      type: object
      properties:
        id:
          type: string
        audio_url:
          type: string
          format: uri
        stream_audio_url:
          type: string
          format: uri
          nullable: true
        image_url:
          type: string
          format: uri
          nullable: true
        prompt:
          type: string
          nullable: true
        model_name:
          type: string
        title:
          type: string
          nullable: true
        duration:
          type: number
        createTime:
          type: string
          format: date-time

    PixCodeResponse:
      type: object
      required: [pixCode, pixQrCode, txid, amount]
      properties:
        pixCode:
          type: string
          description: Código PIX copia e cola (EMV/BR Code)
          example: "00020126580014BR.GOV.BCB.PIX..."
        pixQrCode:
          type: string
          description: Mesma string EMV, usada para montar o QR Code
        txid:
          type: string
          description: TXID do pedido
        amount:
          type: number
          description: "Valor em reais (ex.: 9.9)"
          example: 9.9

  responses:
    BadRequest:
      description: Requisição inválida (campos faltando)
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Sessão ausente ou expirada
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    InternalError:
      description: Erro interno do servidor
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"