openapi: 3.1.0
info:
  title: API de Integração Nearone
  version: "1.0.0"
  description: |
    API para sistemas de terceiros (ERP, RH, plataformas de gestão) integrarem
    à Nearone usando **chaves de API** com escopos — em vez de JWT de usuário.

    - **Autenticação:** `Authorization: Bearer no_live_…` (ou `no_test_…`).
    - **Tenant:** a organização é derivada da chave, nunca do corpo da requisição.
      Sistemas externos não cruzam tenants.
    - **Idempotência:** writes (`POST`/`PUT`) aceitam o header `Idempotency-Key`.
    - **Auditoria:** todo uso é registrado de forma append-only.

    A superfície atual cobre **Empresas**. Novos recursos serão adicionados.
  contact:
    name: Nearone
    url: https://nearone.com.br
servers:
  - url: https://api.nearone.com.br
    description: Produção
  - url: https://api-staging.nearone.com.br
    description: Staging (use chaves `test`)

tags:
  - name: Empresas
    description: |
      Inserção e consulta de empresas vinculadas à organização da chave.
      Endpoints sob `/api/integrations/v1`. Exigem escopo `companies:read`
      ou `companies:write`.
  - name: Participantes
    description: |
      Inserção e consulta de **participantes** (colaboradores) — as pessoas que
      respondem às pesquisas psicossociais NR-1. Endpoints sob
      `/api/integrations/v1`. Exigem escopo `participants:read` ou
      `participants:write`. A organização vem da chave; o CPF é a chave natural
      para upsert. Criar um participante dispara o onboarding NR-1.
  - name: API Keys
    description: |
      Gestão das chaves de API. Endpoints sob `/api/v1/integrations/api-keys`.
      **Autenticados por JWT de usuário** com papel `org_admin` — não por chave
      de API. É o fluxo que o painel da aplicação usa.

security:
  - ApiKeyAuth: []

paths:
  # ──────────────────────────── Empresas ────────────────────────────
  /api/integrations/v1/companies:
    post:
      operationId: createCompany
      tags: [Empresas]
      summary: Criar empresa
      description: |
        Cria uma empresa na organização da chave. O `organization_id` é sempre
        derivado da chave — qualquer valor enviado no corpo é ignorado.

        Envie um header `Idempotency-Key` para tornar a criação segura a
        retentativas: a primeira requisição executa e armazena a resposta;
        retentativas com **a mesma chave e o mesmo corpo** replicam a resposta
        original, enquanto a mesma chave com corpo diferente retorna `400`.
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CompanyCreate"
            examples:
              minimo:
                summary: Campos mínimos obrigatórios
                value:
                  corporate_name: "Acme Indústria LTDA"
                  trade_name: "Acme"
                  cnpj: "11.222.333/0001-81"
              completo:
                summary: Empresa com dados de NR-1
                value:
                  corporate_name: "Acme Indústria LTDA"
                  trade_name: "Acme"
                  cnpj: "11222333000181"
                  email: "contato@acme.com.br"
                  phone: "+55 41 99999-0000"
                  address_city: "Curitiba"
                  address_state: "PR"
                  address_zip_code: "80000-000"
                  cnae: "2599-3/99"
                  risk_level: 3
                  number_of_employees: 120
                  company_industry: "Metalurgia"
                  metadata:
                    erp_id: "ACME-001"
      responses:
        "201":
          description: Empresa criada.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Company"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/IdempotencyInFlight" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      operationId: listCompanies
      tags: [Empresas]
      summary: Listar empresas
      description: Lista paginada das empresas da organização da chave.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: is_active
          in: query
          description: Filtra por status ativo/inativo. Omitido = todas.
          required: false
          schema: { type: boolean }
        - name: limit
          in: query
          description: Máximo de itens por página.
          required: false
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - name: offset
          in: query
          description: Quantos itens pular (paginação).
          required: false
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        "200":
          description: Lista de empresas.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Company" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /api/integrations/v1/companies/by-cnpj/{cnpj}:
    put:
      operationId: upsertCompanyByCnpj
      tags: [Empresas]
      summary: Upsert empresa por CNPJ
      description: |
        Cria a empresa se o CNPJ ainda não existir na organização; caso
        contrário, atualiza a existente. Ideal para sincronização contínua a
        partir de um sistema-fonte (ERP/RH) onde o CNPJ é a chave natural.

        Retorna `201` quando cria e `200` quando atualiza.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: cnpj
          in: path
          required: true
          description: CNPJ com ou sem formatação (`11222333000181` ou `11.222.333/0001-81`).
          schema: { type: string }
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CompanyUpdate"
            examples:
              sincronizacao:
                summary: Sincronização de cabeçalho + headcount
                value:
                  corporate_name: "Acme Indústria LTDA"
                  trade_name: "Acme Brasil"
                  number_of_employees: 134
      responses:
        "200":
          description: Empresa existente atualizada.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Company" }
        "201":
          description: Empresa criada (CNPJ ainda não existia).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Company" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /api/integrations/v1/companies/{company_id}:
    get:
      operationId: getCompany
      tags: [Empresas]
      summary: Obter empresa por ID
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/CompanyId"
      responses:
        "200":
          description: Empresa encontrada.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Company" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    put:
      operationId: updateCompany
      tags: [Empresas]
      summary: Atualizar empresa por ID
      description: |
        Atualização parcial: envie apenas os campos a alterar. Campos omitidos
        permanecem inalterados.
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/CompanyId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CompanyUpdate" }
            examples:
              desativar:
                summary: Desativar empresa
                value:
                  is_active: false
      responses:
        "200":
          description: Empresa atualizada.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Company" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ─────────────────────────── Participantes ───────────────────────────
  /api/integrations/v1/participants:
    post:
      operationId: createParticipant
      tags: [Participantes]
      summary: Criar participante
      description: |
        Cria um participante (colaborador) na organização da chave. `cpf` e
        `full_name` são obrigatórios. CPFs duplicados na organização são
        rejeitados (use o upsert por CPF para sincronização contínua).

        Aceita `Idempotency-Key` para writes seguros a retentativas. A criação
        dispara o onboarding NR-1 do participante (assíncrono).
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ParticipantCreate"
            examples:
              minimo:
                summary: Campos mínimos obrigatórios
                value:
                  full_name: "Maria Souza"
                  cpf: "390.533.447-05"
              completo:
                summary: Participante com vínculo e lotação
                value:
                  full_name: "Maria Souza"
                  cpf: "39053344705"
                  admission_date: "2026-02-01"
                  company_id: "665f1b2c3d4e5f6a7b8c9d0e"
                  ghe_id: "665a0000000000000000fed1"
                  contact:
                    corporate_email: "maria.souza@acme.com.br"
                    mobile: "+55 41 98888-0000"
                  initial_position:
                    title: "Analista de RH"
                    department_id: "665a000000000000000d0001"
                    team_id: "665a000000000000000d0002"
      responses:
        "201":
          description: Participante criado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Participant" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/IdempotencyInFlight" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      operationId: listParticipants
      tags: [Participantes]
      summary: Listar participantes
      description: Lista paginada dos participantes da organização da chave.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: status
          in: query
          required: false
          description: "Filtra por status (ex.: `active`, `inactive`, `terminated`)."
          schema: { type: string }
        - name: search
          in: query
          required: false
          description: Busca por nome, CPF ou e-mail.
          schema: { type: string }
        - name: company_id
          in: query
          required: false
          description: Filtra por empresa vinculada.
          schema: { type: string }
        - name: ghe_id
          in: query
          required: false
          description: Filtra por Grupo Homogêneo de Exposição (GHE).
          schema: { type: string }
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - name: offset
          in: query
          required: false
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        "200":
          description: Lista de participantes.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Participant" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /api/integrations/v1/participants/by-cpf/{cpf}:
    put:
      operationId: upsertParticipantByCpf
      tags: [Participantes]
      summary: Upsert participante por CPF
      description: |
        Cria o participante se o CPF ainda não existir na organização; caso
        contrário, atualiza o existente. Ideal para sincronização contínua a
        partir de um RH/folha onde o CPF é a chave natural. O CPF do path
        sobrepõe qualquer `cpf` no corpo.

        Retorna `201` quando cria e `200` quando atualiza.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: cpf
          in: path
          required: true
          description: CPF com ou sem formatação (`39053344705` ou `390.533.447-05`).
          schema: { type: string }
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ParticipantCreate" }
            examples:
              sincronizacao:
                summary: Sincronização de cadastro
                value:
                  full_name: "Maria Souza"
                  admission_date: "2026-02-01"
                  ghe_id: "665a0000000000000000fed1"
      responses:
        "200":
          description: Participante existente atualizado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Participant" }
        "201":
          description: Participante criado (CPF ainda não existia).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Participant" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /api/integrations/v1/participants/{participant_id}:
    get:
      operationId: getParticipant
      tags: [Participantes]
      summary: Obter participante por ID
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ParticipantId"
      responses:
        "200":
          description: Participante encontrado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Participant" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    put:
      operationId: updateParticipant
      tags: [Participantes]
      summary: Atualizar participante por ID
      description: |
        Atualização parcial: envie apenas os campos a alterar. Campos omitidos
        permanecem inalterados.
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: "#/components/parameters/ParticipantId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ParticipantUpdate" }
            examples:
              desligar:
                summary: Registrar desligamento
                value:
                  status: "terminated"
                  termination_date: "2026-05-30"
      responses:
        "200":
          description: Participante atualizado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Participant" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ──────────────────────────── API Keys ────────────────────────────
  /api/v1/integrations/api-keys:
    post:
      operationId: createApiKey
      tags: [API Keys]
      summary: Criar API key
      description: |
        Cria uma chave na organização atual. **O segredo completo (`api_key`) é
        retornado apenas nesta resposta** — armazene-o com segurança. Depois,
        apenas a forma mascarada fica disponível.
      security:
        - BearerJWT: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ApiKeyCreate" }
            examples:
              leitura_escrita:
                summary: Chave de produção com leitura + escrita
                value:
                  name: "Integração ERP"
                  description: "Sincronização de empresas a partir do ERP"
                  scopes: ["companies:read", "companies:write"]
                  environment: "live"
                  rate_limit_per_minute: 120
      responses:
        "201":
          description: Chave criada (segredo exibido uma única vez).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiKeyCreated" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    get:
      operationId: listApiKeys
      tags: [API Keys]
      summary: Listar API keys
      description: Lista as chaves da organização (sempre mascaradas).
      security:
        - BearerJWT: []
      responses:
        "200":
          description: Lista de chaves.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ApiKey" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /api/v1/integrations/api-keys/{key_id}:
    get:
      operationId: getApiKey
      tags: [API Keys]
      summary: Detalhar API key
      security:
        - BearerJWT: []
      parameters: [{ $ref: "#/components/parameters/KeyId" }]
      responses:
        "200":
          description: Detalhe da chave.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiKey" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateApiKey
      tags: [API Keys]
      summary: Atualizar API key
      description: |
        Edita nome, descrição, escopos, allowlist de IP, expiração e limite de
        taxa. Use `clear_expiry`/`clear_rate_limit` para remover esses limites.
      security:
        - BearerJWT: []
      parameters: [{ $ref: "#/components/parameters/KeyId" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ApiKeyUpdate" }
      responses:
        "200":
          description: Chave atualizada.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiKey" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/integrations/api-keys/{key_id}/rotate:
    post:
      operationId: rotateApiKey
      tags: [API Keys]
      summary: Rotacionar segredo
      description: |
        Emite um novo segredo para a chave. O segredo anterior para de funcionar
        imediatamente. O novo segredo é retornado uma única vez.
      security:
        - BearerJWT: []
      parameters: [{ $ref: "#/components/parameters/KeyId" }]
      responses:
        "200":
          description: Segredo rotacionado (novo segredo exibido uma única vez).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiKeyCreated" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/integrations/api-keys/{key_id}/revoke:
    post:
      operationId: revokeApiKey
      tags: [API Keys]
      summary: Revogar API key
      description: Revoga a chave. Efeito imediato — o status é checado a cada requisição.
      security:
        - BearerJWT: []
      parameters: [{ $ref: "#/components/parameters/KeyId" }]
      responses:
        "200":
          description: Chave revogada.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiKey" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/integrations/api-keys/{key_id}/usage:
    get:
      operationId: getApiKeyUsage
      tags: [API Keys]
      summary: Uso e auditoria da chave
      description: |
        Telemetria de uso (contagem, último uso, último IP) e os eventos de
        auditoria mais recentes da chave.
      security:
        - BearerJWT: []
      parameters:
        - $ref: "#/components/parameters/KeyId"
        - name: limit
          in: query
          required: false
          description: Máximo de eventos retornados.
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
      responses:
        "200":
          description: Telemetria + eventos de auditoria.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiKeyUsage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: |
        Chave de API como bearer token: `Authorization: Bearer no_live_…`.
        O prefixo (`no_live_` / `no_test_`) define o ambiente.
    BearerJWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT de usuário com papel `org_admin` (somente gestão de chaves).

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Chave de idempotência (≤ 255 caracteres). Torna writes seguros a
        retentativas. Recomenda-se um UUID v4 por operação lógica.
      schema: { type: string, maxLength: 255 }
      example: "7c2f9a1e-4b6d-4c2a-9f3e-1a2b3c4d5e6f"
    CompanyId:
      name: company_id
      in: path
      required: true
      description: ID da empresa (ObjectId de 24 caracteres hex).
      schema: { type: string }
    ParticipantId:
      name: participant_id
      in: path
      required: true
      description: ID do participante (ObjectId de 24 caracteres hex).
      schema: { type: string }
    KeyId:
      name: key_id
      in: path
      required: true
      description: ID da chave de API.
      schema: { type: string }

  schemas:
    CompanyCreate:
      type: object
      required: [corporate_name, trade_name, cnpj]
      properties:
        corporate_name:
          type: string
          minLength: 2
          maxLength: 200
          description: Razão social.
        trade_name:
          type: string
          minLength: 2
          maxLength: 200
          description: Nome fantasia.
        cnpj:
          type: string
          description: |
            CNPJ com ou sem formatação. Validado pelos dígitos verificadores e
            **normalizado** para `##.###.###/####-##` no armazenamento.
        is_active:
          type: boolean
          default: true
          description: Se a empresa está ativa.
        email: { type: string, maxLength: 254, nullable: true }
        phone: { type: string, maxLength: 20, nullable: true }
        website: { type: string, maxLength: 200, nullable: true }
        address_street: { type: string, maxLength: 200, nullable: true }
        address_number: { type: string, maxLength: 20, nullable: true }
        address_complement: { type: string, maxLength: 100, nullable: true }
        address_neighborhood: { type: string, maxLength: 100, nullable: true }
        address_city: { type: string, maxLength: 100, nullable: true }
        address_state:
          type: string
          maxLength: 2
          nullable: true
          description: "UF (ex.: `SP`)."
        address_zip_code:
          type: string
          maxLength: 9
          nullable: true
          description: CEP.
        description: { type: string, nullable: true, description: "Descrição livre." }
        municipal_registration: { type: string, maxLength: 50, nullable: true, description: "Inscrição municipal." }
        state_registration: { type: string, maxLength: 50, nullable: true, description: "Inscrição estadual." }
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: "Metadados livres (ex.: IDs do sistema de origem)."
        cnae:
          type: string
          maxLength: 20
          nullable: true
          description: Código CNAE principal.
        risk_level:
          type: integer
          minimum: 1
          maximum: 4
          nullable: true
          description: Grau de risco NR-4 (1 a 4).
        number_of_employees:
          type: integer
          minimum: 0
          nullable: true
          description: Número de funcionários.
        company_industry:
          type: string
          maxLength: 100
          nullable: true
          description: Setor/indústria.

    CompanyUpdate:
      type: object
      description: |
        Atualização parcial — todos os campos são opcionais. Apenas os campos
        presentes são alterados.
      properties:
        corporate_name: { type: string, minLength: 2, maxLength: 200 }
        trade_name: { type: string, minLength: 2, maxLength: 200 }
        cnpj: { type: string, description: "Validado e normalizado como na criação." }
        is_active: { type: boolean }
        email: { type: string, maxLength: 254 }
        phone: { type: string, maxLength: 20 }
        website: { type: string, maxLength: 200 }
        address_street: { type: string, maxLength: 200 }
        address_number: { type: string, maxLength: 20 }
        address_complement: { type: string, maxLength: 100 }
        address_neighborhood: { type: string, maxLength: 100 }
        address_city: { type: string, maxLength: 100 }
        address_state: { type: string, maxLength: 2 }
        address_zip_code: { type: string, maxLength: 9 }
        description: { type: string }
        municipal_registration: { type: string, maxLength: 50 }
        state_registration: { type: string, maxLength: 50 }
        metadata: { type: object, additionalProperties: true }
        cnae: { type: string, maxLength: 20 }
        risk_level: { type: integer, minimum: 1, maximum: 4 }
        number_of_employees: { type: integer, minimum: 0 }
        company_industry: { type: string, maxLength: 100 }

    Company:
      allOf:
        - $ref: "#/components/schemas/CompanyCreate"
        - type: object
          properties:
            id:
              type: string
              description: ID da empresa (ObjectId em string).
            organization_id:
              type: string
              description: Organização dona (derivada da chave).
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }
            deleted_at:
              type: string
              format: date-time
              nullable: true
              description: Timestamp de soft-delete (nulo se ativa).
      example:
        id: "665f1b2c3d4e5f6a7b8c9d0e"
        organization_id: "665a0000000000000000abcd"
        corporate_name: "Acme Indústria LTDA"
        trade_name: "Acme"
        cnpj: "11.222.333/0001-81"
        is_active: true
        cnae: "2599-3/99"
        risk_level: 3
        number_of_employees: 120
        created_at: "2026-06-01T12:00:00Z"
        updated_at: "2026-06-01T12:00:00Z"
        deleted_at: null

    ParticipantContact:
      type: object
      description: Dados de contato. E-mail é necessário para o onboarding NR-1.
      properties:
        personal_email: { type: string, nullable: true }
        corporate_email: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        mobile: { type: string, nullable: true }
        whatsapp: { type: string, nullable: true }

    ParticipantAddress:
      type: object
      properties:
        zip_code: { type: string, nullable: true, description: CEP. }
        street: { type: string, nullable: true }
        number: { type: string, nullable: true }
        complement: { type: string, nullable: true }
        neighborhood: { type: string, nullable: true }
        city: { type: string, nullable: true }
        state: { type: string, maxLength: 2, nullable: true, description: "UF (ex.: `PR`)." }
        country: { type: string, default: Brasil }

    ParticipantInitialPosition:
      type: object
      description: Lotação inicial — cria a posição do participante ao cadastrar.
      properties:
        title: { type: string, description: "Cargo (ex.: `Analista de RH`)." }
        department_id: { type: string, nullable: true }
        team_id: { type: string, nullable: true }

    ParticipantCreate:
      type: object
      required: [full_name, cpf]
      description: |
        Subconjunto prático do cadastro de colaborador. O endpoint aceita os
        demais campos do RH (documentos, dados bancários, CLT, dependentes etc.)
        — veja o schema completo em `EmployeeCreate` da API interna.
      properties:
        full_name:
          type: string
          minLength: 2
          maxLength: 200
          description: Nome completo.
        social_name: { type: string, maxLength: 200, nullable: true, description: Nome social. }
        cpf:
          type: string
          minLength: 11
          maxLength: 14
          description: CPF (somente números ou formatado). Único por organização.
        gender:
          type: string
          nullable: true
          description: "Gênero (enum eSocial/IBGE)."
        birth_date: { type: string, format: date, nullable: true }
        marital_status: { type: string, nullable: true }
        education_level: { type: string, nullable: true, description: Escolaridade (enum). }
        ethnicity_race: { type: string, nullable: true, description: Raça/cor (IBGE/eSocial). }
        disability_type: { type: string, nullable: true, description: Tipo de deficiência (PCD). }
        admission_date: { type: string, format: date, nullable: true }
        is_temporary: { type: boolean, default: false }
        observations: { type: string, nullable: true }
        contact: { $ref: "#/components/schemas/ParticipantContact" }
        address: { $ref: "#/components/schemas/ParticipantAddress" }
        company_id:
          type: string
          nullable: true
          description: ID da empresa (company) à qual o participante é vinculado.
        ghe_id:
          type: string
          nullable: true
          description: ID do Grupo Homogêneo de Exposição (GHE) do PGR/NR-1.
        person_id: { type: string, nullable: true, description: Link com People Analytics. }
        user_id: { type: string, nullable: true, description: Link com usuário da plataforma. }
        initial_position: { $ref: "#/components/schemas/ParticipantInitialPosition" }

    ParticipantUpdate:
      type: object
      description: Atualização parcial — todos os campos são opcionais.
      properties:
        full_name: { type: string, minLength: 2, maxLength: 200 }
        social_name: { type: string, maxLength: 200 }
        cpf: { type: string, minLength: 11, maxLength: 14 }
        gender: { type: string }
        birth_date: { type: string, format: date }
        marital_status: { type: string }
        education_level: { type: string }
        ethnicity_race: { type: string }
        disability_type: { type: string }
        admission_date: { type: string, format: date }
        termination_date: { type: string, format: date }
        is_temporary: { type: boolean }
        status:
          type: string
          description: "Status do participante (ex.: `active`, `inactive`, `terminated`)."
        observations: { type: string }
        contact: { $ref: "#/components/schemas/ParticipantContact" }
        address: { $ref: "#/components/schemas/ParticipantAddress" }
        company_id: { type: string }
        ghe_id: { type: string }
        person_id: { type: string }
        user_id: { type: string }

    Participant:
      allOf:
        - $ref: "#/components/schemas/ParticipantCreate"
        - type: object
          properties:
            id: { type: string, description: ID do participante (ObjectId em string). }
            organization_id: { type: string, description: Organização dona (derivada da chave). }
            status: { type: string }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }
            deleted_at: { type: string, format: date-time, nullable: true }
      example:
        id: "665f1b2c3d4e5f6a7b8c9d0e"
        organization_id: "665a0000000000000000abcd"
        full_name: "Maria Souza"
        cpf: "390.533.447-05"
        status: "active"
        admission_date: "2026-02-01"
        ghe_id: "665a0000000000000000fed1"
        contact:
          corporate_email: "maria.souza@acme.com.br"
        created_at: "2026-06-03T12:00:00Z"
        updated_at: "2026-06-03T12:00:00Z"
        deleted_at: null

    ApiKeyEnvironment:
      type: string
      enum: [live, test]
    ApiKeyStatus:
      type: string
      enum: [active, revoked, expired]

    ApiKeyCreate:
      type: object
      required: [name, scopes]
      properties:
        name: { type: string, minLength: 2, maxLength: 120 }
        description: { type: string, maxLength: 500, nullable: true }
        scopes:
          type: array
          minItems: 1
          items: { type: string }
          description: "Ex.: `[\"companies:read\", \"companies:write\"]`. Aceita curinga por recurso (`companies:*`)."
        environment:
          allOf: [{ $ref: "#/components/schemas/ApiKeyEnvironment" }]
          default: live
        expires_at: { type: string, format: date-time, nullable: true }
        ip_allowlist:
          type: array
          items: { type: string }
          description: IPs/CIDRs autorizados. Vazio = qualquer IP.
        rate_limit_per_minute:
          type: integer
          minimum: 1
          maximum: 100000
          nullable: true
          description: Override do limite de taxa por minuto.

    ApiKeyUpdate:
      type: object
      properties:
        name: { type: string, minLength: 2, maxLength: 120 }
        description: { type: string, maxLength: 500 }
        scopes:
          type: array
          minItems: 1
          items: { type: string }
        expires_at: { type: string, format: date-time }
        clear_expiry: { type: boolean, default: false, description: "Remove a expiração." }
        ip_allowlist:
          type: array
          items: { type: string }
        rate_limit_per_minute: { type: integer, minimum: 1, maximum: 100000 }
        clear_rate_limit: { type: boolean, default: false, description: "Remove o override de rate limit." }

    ApiKey:
      type: object
      description: Visão mascarada de uma chave. Nunca inclui o segredo.
      properties:
        id: { type: string }
        organization_id: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        environment: { $ref: "#/components/schemas/ApiKeyEnvironment" }
        key_prefix: { type: string, description: "Prefixo público (ex.: `no_live_abc123`)." }
        last_four: { type: string }
        masked_key: { type: string, description: "Ex.: `no_live_abc123••••••••WXYZ`." }
        scopes:
          type: array
          items: { type: string }
        status: { $ref: "#/components/schemas/ApiKeyStatus" }
        expires_at: { type: string, format: date-time, nullable: true }
        ip_allowlist:
          type: array
          items: { type: string }
        rate_limit_per_minute: { type: integer, nullable: true }
        last_used_at: { type: string, format: date-time, nullable: true }
        last_used_ip: { type: string, nullable: true }
        usage_count: { type: integer }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        revoked_at: { type: string, format: date-time, nullable: true }

    ApiKeyCreated:
      allOf:
        - $ref: "#/components/schemas/ApiKey"
        - type: object
          required: [api_key]
          properties:
            api_key:
              type: string
              description: "Segredo completo — exibido **uma única vez**. Guarde agora."

    ApiKeyUsage:
      type: object
      properties:
        key_id: { type: string }
        usage_count: { type: integer }
        last_used_at: { type: string, format: date-time, nullable: true }
        last_used_ip: { type: string, nullable: true }
        events:
          type: array
          items:
            type: object
            properties:
              _id: { type: string }
              timestamp: { type: string, format: date-time }
              action: { type: string, description: "Ex.: `company.created`, `api_key.rotated`." }
              target_type: { type: string }
              target_id: { type: string, nullable: true }
              status_code: { type: integer, nullable: true }
              ip_address: { type: string, nullable: true }

    Error:
      type: object
      properties:
        detail:
          description: |
            Mensagem de erro. Em alguns casos (escopo insuficiente) é um objeto
            `{ error, message, required_scope }`.
          oneOf:
            - type: string
            - type: object
      example:
        detail: "API key inválida"

  responses:
    BadRequest:
      description: Requisição inválida (validação ou conflito de idempotência com payload diferente).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Credencial ausente, inválida, revogada ou expirada.
      headers:
        WWW-Authenticate:
          schema: { type: string }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: IP fora da allowlist ou escopo insuficiente (`insufficient_scope`).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            detail:
              error: insufficient_scope
              message: "A API key não possui o escopo 'companies:write'"
              required_scope: "companies:write"
    NotFound:
      description: Recurso não encontrado na organização da chave.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    IdempotencyInFlight:
      description: Uma requisição com esta `Idempotency-Key` ainda está em processamento.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Limite de taxa por chave excedido.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
