openapi: 3.0.3
info:
  title: Praça API pública
  version: 1.0.0
  description: |
    API pública do portal **Praça** (praça.id): catálogo de ferramentas, Consultas Brasil, execução versionada,
    jobs assíncronos e FIPE.

    ## Envelope de resposta (consultas, execute v1, FIPE)

    Endpoints de **consulta**, **execução** e **FIPE** retornam o contrato estável:

    ```json
    {
      "ok": true,
      "data": { },
      "error": null,
      "meta": {
        "serviceKey": "query.cep",
        "source": "ViaCEP",
        "fetched_at": "2026-07-30T22:00:00.000Z",
        "cached": false,
        "latency_ms": 142
      }
    }
    ```

    Em falha de validação ou negócio, `ok` é `false`, `data` é `null` e `error` traz `code`, `message` e opcionalmente `fields`.

    ## Catálogo e jobs

    - **Catálogo** (`/api/portal-tools/categories`, `/tools`, `/search`): retorna JSON direto (array ou objeto), sem envelope.
    - **Jobs**: retorna recurso do job (`id`, `status`, `result_meta`, etc.).

    ## Autenticação

    Rotas públicas **não exigem** login. O execute v1 aceita header opcional `X-Api-Key` quando o serviço está liberado comercialmente (`api_enabled=true`).

    ## Rate limits

    Limites anônimos por IP+User-Agent (hash) e buckets por serviço. Respostas `429` quando excedido.

    ## Termos e SLA

    Uso comercial sujeito a `/api-termos` e `/api-sla` no site Praça.

  contact:
    name: Praça / TKCode
    url: https://tkcode.com.br/
  license:
    name: Uso conforme termos publicados no portal
    url: https://praca.id/api-termos

servers:
  - url: http://localhost:3001
    description: Desenvolvimento local (API Nest)
  - url: '{apiBase}'
    description: Produção (substitua pelo domínio da API do cliente)
    variables:
      apiBase:
        default: https://api.praca.id
        description: Origem da API (ex. `NEXT_PUBLIC_API_URL`)

tags:
  - name: Catálogo
    description: Listagem pública de categorias e ferramentas publicadas
  - name: Eventos
    description: Métricas agregadas sem PII
  - name: Consultas Brasil
    description: Proxy cacheado (CEP, IBGE, DDD, feriados) — envelope ToolResult
  - name: API v1 — Execute
    description: Execução versionada de engines (`portal_tool_services.api_enabled`)
  - name: Jobs
    description: Fila assíncrona (echo, uploads, conversões)
  - name: FIPE
    description: Catálogo FIPE por modelo, preço atual e histórico mensal persistente

paths:
  /api/portal-tools/categories:
    get:
      tags: [Catálogo]
      summary: Listar categorias ativas
      operationId: listCategories
      responses:
        '200':
          description: Lista de categorias
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PortalCategory'
              example:
                - id: 550e8400-e29b-41d4-a716-446655440000
                  slug: calculadoras
                  name: Calculadoras
                  description: Calculadoras financeiras e utilitárias
                  icon: calculator
                  sort_order: 10
                  tools_count: 12

  /api/portal-tools/tools:
    get:
      tags: [Catálogo]
      summary: Listar ferramentas publicadas
      operationId: listTools
      parameters:
        - $ref: '#/components/parameters/category'
        - name: featured
          in: query
          schema: { type: boolean }
        - name: popular
          in: query
          schema: { type: boolean }
        - name: new
          in: query
          schema: { type: boolean }
        - name: recommended
          in: query
          schema: { type: boolean }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        '200':
          description: Lista de ferramentas
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PortalToolSummary'

  /api/portal-tools/tools/{slug}:
    get:
      tags: [Catálogo]
      summary: Detalhe de ferramenta por slug
      operationId: getToolBySlug
      parameters:
        - name: slug
          in: path
          required: true
          schema: { type: string, example: consulta-cep }
      responses:
        '200':
          description: Ferramenta publicada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortalToolDetail'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/portal-tools/search:
    get:
      tags: [Catálogo]
      summary: Buscar ferramentas publicadas
      operationId: searchTools
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string, minLength: 1, maxLength: 200, example: cep }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
      responses:
        '200':
          description: Resultados da busca
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PortalToolSummary'

  /api/portal-tools/ads:
    get:
      tags: [Catálogo]
      summary: Posições de anúncio ativas
      operationId: listAds
      responses:
        '200':
          description: Slots com snippet HTML sanitizado
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  additionalProperties: true

  /api/portal-tools/events:
    post:
      tags: [Eventos]
      summary: Registrar evento agregado (sem PII)
      operationId: recordEvent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PortalToolEvent'
            examples:
              view:
                summary: Visualização de ferramenta
                value:
                  event_type: view
                  tool_id: 550e8400-e29b-41d4-a716-446655440001
              execution:
                summary: Execução no cliente
                value:
                  event_type: execution
                  tool_slug: calculadora-porcentagem
      responses:
        '201':
          description: Evento aceito (UPSERT diário)
          content:
            application/json:
              schema:
                type: object
                properties:
                  accepted: { type: boolean, example: true }
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/portal-tools/queries/cep:
    get:
      tags: [Consultas Brasil]
      summary: Consulta CEP (ViaCEP)
      operationId: queryCep
      parameters:
        - name: cep
          in: query
          required: true
          schema: { type: string, example: '01310100' }
      responses:
        '200':
          description: Envelope ToolResult
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'
              examples:
                sucesso:
                  $ref: '#/components/examples/CepSuccess'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/portal-tools/queries/estados:
    get:
      tags: [Consultas Brasil]
      summary: Lista estados (IBGE)
      operationId: queryEstados
      responses:
        '200':
          description: Envelope com array em data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

  /api/portal-tools/queries/municipios:
    get:
      tags: [Consultas Brasil]
      summary: Municípios por UF (IBGE)
      operationId: queryMunicipios
      parameters:
        - name: uf
          in: query
          required: true
          schema: { type: string, example: SP }
        - name: nome
          in: query
          schema: { type: string, example: Campinas }
      responses:
        '200':
          description: Envelope com array em data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

  /api/portal-tools/queries/codigo-ibge:
    get:
      tags: [Consultas Brasil]
      summary: Código IBGE por código ou nome
      operationId: queryCodigoIbge
      parameters:
        - name: codigo
          in: query
          schema: { type: string }
        - name: nome
          in: query
          schema: { type: string }
        - name: uf
          in: query
          schema: { type: string, example: SP }
      responses:
        '200':
          description: Envelope ToolResult
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

  /api/portal-tools/queries/ddd:
    get:
      tags: [Consultas Brasil]
      summary: Consulta DDD (BrasilAPI)
      operationId: queryDdd
      parameters:
        - name: ddd
          in: query
          required: true
          schema: { type: string, example: '11' }
      responses:
        '200':
          description: Envelope ToolResult
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

  /api/portal-tools/queries/feriados:
    get:
      tags: [Consultas Brasil]
      summary: Feriados nacionais por ano (BrasilAPI)
      operationId: queryFeriados
      parameters:
        - name: ano
          in: query
          required: true
          schema: { type: string, example: '2026' }
      responses:
        '200':
          description: Envelope com array em data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

  /api/portal-tools/v1/services/{serviceKey}/execute:
    post:
      tags: [API v1 — Execute]
      summary: Executar serviço por service_key
      description: |
        Requer `portal_tool_services.api_enabled = true`. Sem `X-Api-Key`, funciona como preview interno
        (limites anônimos). Com `X-Api-Key`, valida hash e allowlist comercial.
      operationId: executeService
      parameters:
        - $ref: '#/components/parameters/serviceKey'
        - $ref: '#/components/parameters/xApiKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExecuteServiceBody'
            examples:
              calcPorcentagem:
                summary: Calculadora de porcentagem
                value:
                  input:
                    valor: 200
                    percentual: 15
              queryCep:
                summary: Consulta CEP via engine
                value:
                  input:
                    cep: '01310100'
      responses:
        '200':
          description: ToolResult da engine
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'
        '403':
          description: Serviço com api_enabled=false
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HttpError'
        '401':
          description: API key inválida ou sem escopo
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HttpError'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/portal-tools/jobs:
    post:
      tags: [Jobs]
      summary: Criar job (MVP job.echo e similares sem upload)
      operationId: createJob
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateJobBody'
            example:
              service_key: job.echo
              payload_meta:
                text: Olá, Praça!
      responses:
        '201':
          description: Job enfileirado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortalJob'
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/portal-tools/jobs/upload:
    post:
      tags: [Jobs]
      summary: Upload para job.file_stub (multipart)
      operationId: uploadJobFile
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '201':
          description: Job de arquivo criado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortalJob'

  /api/portal-tools/jobs/{id}:
    get:
      tags: [Jobs]
      summary: Status do job (UUID = capability URL)
      operationId: getJob
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Estado atual do job
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortalJob'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/fipe/tipos:
    get:
      tags: [FIPE]
      summary: Listar categorias de veículos
      operationId: fipeTipos
      responses:
        '200':
          description: Carros, motos e caminhões no envelope FIPE
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

  /api/fipe/referencias:
    get:
      tags: [FIPE]
      summary: Listar referências mensais disponíveis
      operationId: fipeReferencias
      parameters:
        - name: refresh
          in: query
          schema: { type: boolean, default: false }
      responses:
        '200':
          description: Referências com código opaco para tabela_referencia
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

  /api/fipe/marcas/{tipo}:
    get:
      tags: [FIPE]
      summary: Listar marcas por categoria
      operationId: fipeMarcas
      parameters:
        - name: tipo
          in: path
          required: true
          schema: { type: string, enum: [carros, motos, caminhoes] }
        - name: tabela_referencia
          in: query
          description: Código opaco devolvido por /api/fipe/referencias
          schema: { type: string }
      responses:
        '200':
          description: Marcas normalizadas e persistidas
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

  /api/fipe/modelos/{tipo}/{marcaCodigo}:
    get:
      tags: [FIPE]
      summary: Listar modelos de uma marca
      operationId: fipeModelos
      parameters:
        - name: tipo
          in: path
          required: true
          schema: { type: string, enum: [carros, motos, caminhoes] }
        - name: marcaCodigo
          in: path
          required: true
          schema: { type: string }
        - name: q
          in: query
          schema: { type: string, maxLength: 120 }
      responses:
        '200':
          description: Modelos normalizados e persistidos
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

  /api/fipe/anos/{modeloCodigo}:
    get:
      tags: [FIPE]
      summary: Listar anos e combustíveis de um modelo
      operationId: fipeModeloAnos
      parameters:
        - name: modeloCodigo
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Versões com código FIPE e variant_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

  /api/fipe/consulta/{codigoFipe}/{anoModelo}:
    get:
      tags: [FIPE]
      summary: Consultar preço e persistir histórico da versão
      operationId: fipeConsulta
      parameters:
        - name: codigoFipe
          in: path
          required: true
          schema: { type: string, pattern: '^[0-9]{6}-[0-9]$' }
        - name: anoModelo
          in: path
          required: true
          description: Ano com quatro dígitos; use 0 para zero-km
          schema: { oneOf: [{ type: integer }, { type: string, enum: [zero] }] }
        - name: combustivel
          in: query
          schema: { type: string }
        - name: tabela_referencia
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Preço da referência e identificador da variante canônica
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/fipe/historico/{codigoFipe}/{anoModelo}:
    get:
      tags: [FIPE]
      summary: Obter série histórica mensal da versão
      operationId: fipeHistoricoCanonico
      parameters:
        - name: codigoFipe
          in: path
          required: true
          schema: { type: string }
        - name: anoModelo
          in: path
          required: true
          schema: { type: string }
        - name: combustivel
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, minimum: 2, maximum: 360, default: 120 }
      responses:
        '200':
          description: Série crescente e resumo de variação
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

  /api/fipe/populares:
    get:
      tags: [FIPE]
      summary: Listar variantes mais consultadas
      operationId: fipePopulares
      parameters:
        - name: tipo
          in: query
          schema: { type: string, enum: [carros, motos, caminhoes] }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 10 }
        - name: days
          in: query
          schema: { type: integer, minimum: 1, maximum: 365, default: 30 }
      responses:
        '200':
          description: Ranking agregado sem PII
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

  /api/fipe/placa/{placa}:
    get:
      tags: [FIPE]
      deprecated: true
      summary: Consulta legada por placa
      operationId: fipeByPlaca
      parameters:
        - name: placa
          in: path
          required: true
          schema: { type: string, example: ABC1D23 }
      responses:
        '200':
          description: Envelope FIPE com disclaimer em meta
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/fipe/veiculos/{id}:
    get:
      tags: [FIPE]
      summary: Detalhe do veículo FIPE
      operationId: fipeVeiculo
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Envelope ToolResult
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

  /api/fipe/veiculos/{id}/historico:
    get:
      tags: [FIPE]
      summary: Histórico de preços FIPE
      operationId: fipeHistorico
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Envelope com série em data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

  /api/fipe/stats:
    get:
      tags: [FIPE]
      summary: Estatísticas públicas FIPE
      operationId: fipeStats
      responses:
        '200':
          description: Totais e última atualização
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolResultEnvelope'

components:
  parameters:
    category:
      name: category
      in: query
      description: Slug da categoria
      schema: { type: string, example: consultas }
    serviceKey:
      name: serviceKey
      in: path
      required: true
      description: Chave estável em portal_tool_services (ex. calc.porcentagem, query.cep)
      schema: { type: string, example: query.cep }
    xApiKey:
      name: X-Api-Key
      in: header
      required: false
      description: Chave comercial opcional (SHA-256 hash no servidor)
      schema: { type: string }

  responses:
    BadRequest:
      description: Validação ou parâmetro inválido
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/HttpError'
    NotFound:
      description: Recurso não encontrado
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/HttpError'
    TooManyRequests:
      description: Rate limit ou cota anônima excedida
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/HttpError'

  examples:
    CepSuccess:
      value:
        ok: true
        data:
          cep: '01310100'
          logradouro: Avenida Paulista
          bairro: Bela Vista
          localidade: São Paulo
          uf: SP
          ibge: '3550308'
          ddd: '11'
        error: null
        meta:
          serviceKey: query.cep
          componentKey: query.cep
          version: '1'
          category: query
          source: ViaCEP
          fetched_at: '2026-07-30T22:00:00.000Z'
          cached: false
          latency_ms: 142

  schemas:
    HttpError:
      type: object
      properties:
        statusCode: { type: integer, example: 400 }
        message:
          oneOf:
            - { type: string }
            - { type: array, items: { type: string } }
        error: { type: string, example: Bad Request }

    ToolError:
      type: object
      nullable: true
      properties:
        code: { type: string, example: INVALID_CEP }
        message: { type: string }
        fields:
          type: object
          additionalProperties: { type: string }

    ToolMeta:
      type: object
      additionalProperties: true
      properties:
        serviceKey: { type: string }
        componentKey: { type: string }
        version: { type: string }
        category: { type: string, enum: [query, calc, text, val, gen, conv] }
        source: { type: string }
        source_url: { type: string, format: uri }
        fetched_at: { type: string, format: date-time, nullable: true }
        cached: { type: boolean }
        latency_ms: { type: integer }
        disclaimer: { type: string }

    ToolResultEnvelope:
      type: object
      required: [ok, data, error, meta]
      properties:
        ok: { type: boolean }
        data:
          nullable: true
          description: Payload de sucesso (forma depende do serviço)
        error:
          $ref: '#/components/schemas/ToolError'
        meta:
          $ref: '#/components/schemas/ToolMeta'

    PortalCategory:
      type: object
      properties:
        id: { type: string, format: uuid }
        slug: { type: string }
        name: { type: string }
        description: { type: string }
        icon: { type: string, nullable: true }
        sort_order: { type: integer }
        tools_count: { type: integer }

    PortalToolSummary:
      type: object
      properties:
        id: { type: string, format: uuid }
        slug: { type: string }
        name: { type: string }
        short_description: { type: string }
        component_key: { type: string }
        category_slug: { type: string }
        is_featured: { type: boolean }
        is_popular: { type: boolean }
        is_new: { type: boolean }
        is_recommended: { type: boolean }

    PortalToolDetail:
      allOf:
        - $ref: '#/components/schemas/PortalToolSummary'
        - type: object
          properties:
            seo_title: { type: string, nullable: true }
            seo_description: { type: string, nullable: true }
            tags:
              type: array
              items: { type: string }
            services:
              type: array
              items:
                type: object
                properties:
                  service_key: { type: string }
                  api_enabled: { type: boolean }

    PortalToolEvent:
      type: object
      required: [event_type]
      properties:
        event_type:
          type: string
          enum:
            - view
            - execution
            - execute
            - search_click
            - api_call
            - rate_limit_hit
            - ad_view
            - ad_click
        tool_id:
          type: string
          format: uuid
          description: Obrigatório se tool_slug ausente
        tool_slug:
          type: string
          maxLength: 200
          description: Alternativa a tool_id

    ExecuteServiceBody:
      type: object
      properties:
        input:
          type: object
          additionalProperties: true
          description: Validado pela engine do service_key

    CreateJobBody:
      type: object
      required: [service_key]
      properties:
        service_key:
          type: string
          enum:
            - job.echo
            - job.file_stub
            - job.text_to_pdf
            - job.images_to_pdf
            - job.image_compress
            - job.batch
            - nexa.ocr.extract
          description: Chaves com upload usam POST /jobs/upload ou /jobs/batch
        payload_meta:
          type: object
          additionalProperties: true
          description: 'Para job.echo: { "text": "..." }'

    PortalJob:
      type: object
      properties:
        id: { type: string, format: uuid }
        service_key: { type: string }
        status:
          type: string
          enum: [queued, running, done, failed, expired]
        payload_meta: { type: object, additionalProperties: true }
        result_ref: { type: string, nullable: true }
        result_meta: { type: object, additionalProperties: true }
        progress_pct: { type: integer }
        progress_current: { type: integer }
        progress_total: { type: integer }
        progress_message: { type: string, nullable: true }
        error_code: { type: string, nullable: true }
        error_message: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        started_at: { type: string, format: date-time, nullable: true }
        finished_at: { type: string, format: date-time, nullable: true }
        expires_at: { type: string, format: date-time }

  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-Api-Key
      description: Opcional no execute v1; obrigatório em integrações comerciais liberadas no admin

security: []
