openapi: 3.1.0
info:
  title: Contracheque Online — API de Integração
  version: 1.0.0
  summary: 'Envio de folha e gestão da carteira do escritório por sistemas externos.'
  description: |
    A API expõe **as mesmas operações da interface web**, com as mesmas regras e
    permissões. Um sistema integrado age sempre em nome de um **membro do escritório**
    e pode fazer exatamente o que esse membro faria na tela — nada além disso.

    ### Antes de começar (uma vez, no navegador)
    Criar a conta do escritório ([criar conta](/?cadastro), com CAPTCHA e aceite dos termos),
    confirmar o e-mail do administrador e gerar a primeira credencial são feitos por uma
    pessoa, no navegador. Todo o resto — cadastrar empresas, enviar e consultar folha,
    gerir trabalhadores — roda por esta API. O passo a passo completo está na
    [página da documentação](/docs/api/#guia).

    ### Como autenticar
    1. O administrador do escritório cria, no Contracheque Online, um membro dedicado à
       integração (menu **Equipe do escritório**) com o papel necessário —
       normalmente `OPERADOR_FOLHA`.
    2. Em **Integrações**, gera uma credencial para esse membro. O `client_secret`
       aparece **uma única vez**.
    3. O sistema troca a credencial por um token em `POST /auth/token` e envia
       `Authorization: Bearer <access_token>` em todas as chamadas.
    4. Quando o token expirar (`expires_in`, em segundos), peça outro. Não há refresh
       token.

    ### Escolhendo a entidade
    As rotas de folha, trabalhadores e documentos aceitam `entidade_id` — na query
    (GET/DELETE) ou no corpo (POST/PUT) — para operar em **qualquer entidade da
    carteira** com o mesmo token. Sem o campo, a rota usa a **entidade ativa** do token.
    O acesso à entidade informada é conferido a cada chamada: sem acesso, `403
    SEM_ACESSO_ENTIDADE`; identificador malformado, `400 ENTIDADE_INVALIDA`.
    `POST /auth/contexto` continua disponível para trocar a entidade ativa.

    ### Perfis: o que cada um pode fazer e em quais empresas
    O acesso de um membro tem **duas dimensões independentes**:

    1. **Papel** (o que pode fazer) — `CONSULTA` < `OPERADOR_FOLHA` < `ADMINISTRADOR`.
       Cada rota informa o papel mínimo exigido na entidade. Sem o papel, a resposta é
       `403` com `code: PAPEL_INSUFICIENTE`.
    2. **Acesso à carteira** (em quais empresas) — por padrão o membro alcança **todas**
       as entidades da carteira do escritório. O administrador pode restringi-lo a
       **algumas** (`PUT /escritorio/membros/{usuarioId}/entidades`). Entidades que
       entrarem depois na carteira **não** entram automaticamente para quem está
       restrito. **Administradores do escritório sempre veem a carteira inteira.**

    Uma **credencial de API** age em nome de um membro e pode ainda ter um **escopo
    próprio** de entidades (`entidade_ids` na criação ou
    `PUT /escritorio/credenciais-api/{id}/entidades`). O token alcança a **interseção**
    entre o acesso do membro e o escopo da credencial — nunca mais do que o membro.
    Tirar uma entidade do membro tira também das credenciais dele.

    | quem | empresas alcançadas | papel em cada uma |
    |---|---|---|
    | administrador do escritório | carteira inteira | `ADMINISTRADOR` (limitado pelo vínculo da carteira) |
    | membro sem recorte | carteira inteira | o papel do membro |
    | membro com recorte | só as selecionadas | o papel do membro |
    | credencial sem escopo | as do membro dono | o papel do membro dono |
    | credencial com escopo | membro ∩ escopo | o papel do membro dono |

    Tudo o que lista empresas — `GET /entidades/minhas`, `GET /escritorio/dashboard`,
    `GET /escritorio/entidades`, a entidade ativa devolvida em `POST /auth/token` — já
    vem filtrado por essas regras. Mudanças de recorte ou escopo valem para tokens já
    emitidos em até 5 minutos.

    ### Aviso de envio de folha
    Cada folha importada com sucesso gera um e-mail para os **administradores do
    escritório** que tem a entidade na carteira e para os **administradores com vínculo
    direto** na entidade, com a competência, o arquivo e quem enviou (indicando quando o
    envio veio de uma integração). Não há nada a fazer do lado da integração.

    ### Erros
    Fora de `/auth/token` (que segue a RFC 6749), os erros têm o formato
    `{ "error": true, "message": "...", "code": "..." }`. O `code` é estável e
    pode ser usado pelo seu sistema; o `message` é para exibição. Erros de validação
    trazem ainda `issues`, com o detalhe por campo.

    | code | status | significado |
    |---|---|---|
    | `TOKEN_AUSENTE` | 401 | sem `Authorization: Bearer` |
    | `TOKEN_INVALIDO` | 403 | token inválido ou expirado — peça outro |
    | `ENTIDADE_NAO_SELECIONADA` | 400 | sem `entidade_id` e sem entidade ativa |
    | `ENTIDADE_INVALIDA` | 400 | `entidade_id` não é um UUID |
    | `SEM_ACESSO_ENTIDADE` | 403 | o membro não acessa a entidade — fora da carteira, do recorte do membro ou do escopo da credencial |
    | `ENTIDADE_NAO_APROVADA` | 403 | entidade aguardando o suporte |
    | `PAPEL_INSUFICIENTE` | 403 | papel abaixo do exigido pela rota |
    | `EMAIL_NAO_VERIFICADO` | 403 | escritório no limite de entidades e membro sem e-mail confirmado — `POST /escritorio/reenviar-verificacao` |
    | `ARQUIVO_AUSENTE` | 400 | upload sem o campo `arquivo` |
    | `ARQUIVO_JA_IMPORTADO` | 409 | o mesmo arquivo já entrou nessa competência |
    | `SERVIDOR_OCUPADO` | 503 | fila de importação cheia — reenvie em instantes |
    | `ENTIDADE_FORA_DA_CARTEIRA` | 400 | recorte de membro com entidade que não está ativa na carteira |
    | `ENTIDADE_FORA_DO_MEMBRO` | 400 | escopo de credencial com entidade que o membro dono não alcança |

    ### Layout do XML
    O arquivo de folha segue o [XSD oficial](/docs/api/contracheque.xsd)
    ([XML de exemplo](/docs/api/CC_202608_11222333000181_00001.xml)). Competência em
    `AAAAMM`, datas em `DD/MM/AAAA`, valores em `1.518,00` ou `1518.00`.

    ### Limites
    - Upload de folha: arquivos de até **10 MB**; as importações são processadas
      **uma por vez** no servidor. Se a fila estiver cheia, a resposta é `503` —
      aguarde alguns segundos e reenvie.
    - `POST /auth/token`: 10 tentativas **falhas** a cada 10 minutos por IP e
      `client_id`.
  contact:
    name: Simples Informática
    url: https://simplesinformatica.com.br/contatos/
servers:
  - url: https://app.simplesinformatica.com.br/cco/api
    description: 'Produção'

security:
  - bearerAuth: []

tags:
  - name: Autenticação
    description: 'Token de acesso e troca de entidade ativa.'
  - name: Escritório
    description: 'Dados da firma, equipe e carteira de entidades.'
  - name: Integrações
    description: 'Credenciais de API. Exige ADMINISTRADOR do escritório.'
  - name: Entidades
    description: 'Empresas e órgãos da carteira.'
  - name: Solicitações de acesso
    description: 'Pedidos de acesso a entidades de outros escritórios.'
  - name: Folha de pagamento
    description: 'Envio e consulta de contracheques da entidade ativa.'
  - name: Servidores
    description: 'Senhas de acesso e dados dos trabalhadores da entidade ativa.'
  - name: Documentos
    description: 'Dados prontos para montar contracheques e listas de senhas em PDF.'
  - name: Conta
    description: 'Dados do membro autenticado e aceite de termos.'

paths:
  /auth/token:
    post:
      operationId: postAuthToken
      tags: [Autenticação]
      summary: 'Obter token (login machine-to-machine)'
      description: |
        OAuth 2.0 **Client Credentials**. Aceita as credenciais no corpo
        (JSON ou `application/x-www-form-urlencoded`) ou no cabeçalho HTTP Basic.
        O token devolvido é idêntico ao da interface web.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PedidoToken' }
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/PedidoToken' }
      responses:
        '200':
          description: 'Token emitido.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Token' }
        '400':
          description: '`unsupported_grant_type` ou `invalid_request`.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErroOAuth' }
        '401':
          description: '`invalid_client` — credencial inválida, revogada, ou membro/escritório inativo.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErroOAuth' }
        '429':
          description: '`slow_down` — muitas tentativas falhas.'

  /auth/check:
    get:
      operationId: getAuthCheck
      tags: [Autenticação]
      summary: 'Validar o token atual'
      responses:
        '200':
          description: 'Token válido.'
          content:
            application/json:
              example: { authenticated: true, is_suporte: false }
        '401': { $ref: '#/components/responses/NaoAutenticado' }
        '403': { $ref: '#/components/responses/TokenInvalido' }

  /auth/contexto:
    post:
      operationId: postAuthContexto
      tags: [Autenticação]
      summary: 'Trocar a entidade ativa'
      description: |
        Devolve um **novo token** com a entidade escolhida. Use-o nas chamadas seguintes.
        Com token de integração, a troca vale só para o token devolvido: a entidade
        padrão do membro na tela não muda. Na maioria dos casos é mais simples informar
        `entidade_id` em cada chamada do que trocar de contexto.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [entidade_id]
              properties:
                entidade_id: { type: string, format: uuid }
      responses:
        '200':
          description: 'Contexto trocado.'
          content:
            application/json:
              example:
                success: true
                token: eyJhbGciOiJSUzI1NiIs...
                entidade: { id: 0190..., nome: PREFEITURA MUNICIPAL DE EXEMPLO, cnpj: '06553556000147', tipo: prefeitura, status_aprovacao: APROVADA }
                papel: OPERADOR_FOLHA
        '403': { $ref: '#/components/responses/SemAcessoEntidade' }
        '404': { $ref: '#/components/responses/NaoEncontrado' }

  /escritorio/me:
    get:
      operationId: getEscritorioMe
      tags: [Escritório]
      summary: 'Dados do escritório'
      responses:
        '200': { description: 'Dados do escritório do membro autenticado.' }
    put:
      operationId: putEscritorioMe
      tags: [Escritório]
      summary: 'Atualizar dados do escritório'
      description: 'Exige ADMINISTRADOR do escritório.'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                nome: { type: string, minLength: 3, maxLength: 150 }
                email_contato: { type: string, format: email }
                telefone: { type: string, maxLength: 20 }
      responses:
        '200': { description: 'Atualizado.' }
        '403': { $ref: '#/components/responses/PapelInsuficiente' }

  /escritorio/reenviar-verificacao:
    post:
      operationId: postReenviarVerificacao
      tags: [Escritório]
      summary: 'Reenviar o e-mail de confirmação do membro'
      description: |
        Envia ao e-mail do membro autenticado o link de confirmação. Membro sem e-mail
        confirmado não cadastra entidades depois que o escritório atinge o limite
        (`EMAIL_NAO_VERIFICADO`). O link é aberto por uma pessoa, na caixa de e-mail.
      responses:
        '200': { description: 'E-mail enviado, ou e-mail já confirmado.' }
        '400': { description: 'O membro não tem e-mail cadastrado.' }

  /escritorio/membros:
    get:
      operationId: getEscritorioMembros
      tags: [Escritório]
      summary: 'Listar membros'
      responses:
        '200':
          description: |
            Lista de membros do escritório. `carteira_restrita` indica se o membro está
            restrito a `entidades_permitidas` (ignorado para administradores).
          content:
            application/json:
              example:
                - { id: 0190..., nome: Ana, nick: ana, papel_escritorio: OPERADOR_FOLHA, ativo: true, carteira_restrita: true, entidades_permitidas: [0190...] }
    post:
      operationId: postEscritorioMembros
      tags: [Escritório]
      summary: 'Adicionar membro'
      description: |
        Exige ADMINISTRADOR do escritório. O membro nasce com acesso a toda a carteira,
        limitado pelo `papel` informado. Para restringi-lo a algumas entidades, use em
        seguida `PUT /escritorio/membros/{usuarioId}/entidades`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [nome, nick, email, senha, papel]
              properties:
                nome: { type: string, minLength: 3, maxLength: 200 }
                nick: { type: string, minLength: 3, maxLength: 50, pattern: '^[a-zA-Z0-9._-]+$' }
                email: { type: string, format: email }
                senha: { type: string, minLength: 8, maxLength: 100 }
                papel: { $ref: '#/components/schemas/Papel' }
      responses:
        '201': { description: 'Membro criado.' }
        '409': { description: 'Login ou e-mail já em uso.' }

  /escritorio/membros/{usuarioId}:
    parameters:
      - { name: usuarioId, in: path, required: true, schema: { type: string, format: uuid } }
    put:
      operationId: putEscritorioMembrosUsuarioId
      tags: [Escritório]
      summary: 'Alterar papel do membro'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [papel]
              properties:
                papel: { $ref: '#/components/schemas/Papel' }
      responses:
        '200': { description: 'Papel alterado.' }
        '409': { description: '`ULTIMO_ADMINISTRADOR` — promova outro antes.' }
    delete:
      operationId: deleteEscritorioMembrosUsuarioId
      tags: [Escritório]
      summary: 'Remover (desativar) membro'
      responses:
        '200': { description: 'Membro desativado.' }
        '409': { description: '`ULTIMO_ADMINISTRADOR`.' }

  /escritorio/membros/{usuarioId}/entidades:
    parameters:
      - { name: usuarioId, in: path, required: true, schema: { type: string, format: uuid } }
    put:
      operationId: putEscritorioMembrosUsuarioIdEntidades
      tags: [Escritório]
      summary: 'Definir em quais entidades da carteira o membro atua'
      description: |
        Exige ADMINISTRADOR do escritório. `restrita: false` devolve a carteira inteira
        ao membro; `restrita: true` limita o acesso herdado às `entidade_ids` (todas
        precisam estar ativas na carteira). A lista **substitui** a anterior e fica
        guardada mesmo com `restrita: false`, para religar o recorte sem remarcar.

        Administradores ignoram o recorte: a lista é salva e passa a valer se o papel
        mudar. Vínculos diretos concedidos pelo suporte não são afetados.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [restrita]
              properties:
                restrita: { type: boolean }
                entidade_ids: { type: array, items: { type: string, format: uuid }, maxItems: 2000, default: [] }
            example: { restrita: true, entidade_ids: [0190...] }
      responses:
        '200': { description: 'Acesso atualizado. `message` avisa quando o membro é administrador.' }
        '400': { description: '`ENTIDADE_FORA_DA_CARTEIRA` — `entidades` lista as recusadas.' }
        '404': { $ref: '#/components/responses/NaoEncontrado' }

  /escritorio/entidades:
    get:
      operationId: getEscritorioEntidades
      tags: [Escritório]
      summary: 'Carteira do escritório'
      description: |
        O administrador vê a carteira inteira (inclusive vínculos inativos). Demais
        membros — e qualquer token de credencial de API — veem só as entidades que
        alcançam.
      responses:
        '200': { description: 'Entidades vinculadas ao escritório, recortadas pelo acesso de quem chama.' }

  /escritorio/entidades/{entidadeId}:
    delete:
      operationId: deleteEscritorioEntidadesEntidadeId
      tags: [Escritório]
      summary: 'Desvincular entidade da carteira'
      description: 'Exige ADMINISTRADOR do escritório.'
      parameters:
        - { name: entidadeId, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: 'Desvinculada.' }

  /escritorio/dashboard:
    get:
      operationId: getEscritorioDashboard
      tags: [Escritório]
      summary: 'Indicadores da carteira'
      parameters:
        - { name: meses, in: query, schema: { type: integer, default: 12, maximum: 36 } }
      responses:
        '200': { description: 'Totais e série mensal de toda a carteira.' }

  /escritorio/dashboard/entidade/{id}:
    get:
      operationId: getEscritorioDashboardEntidadeId
      tags: [Escritório]
      summary: 'Indicadores de uma entidade'
      description: 'Exige CONSULTA na entidade.'
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: meses, in: query, schema: { type: integer, default: 24, maximum: 60 } }
      responses:
        '200': { description: 'Série mensal da entidade.' }
        '403': { $ref: '#/components/responses/SemAcessoEntidade' }

  /escritorio/credenciais-api:
    get:
      operationId: getEscritorioCredenciaisApi
      tags: [Integrações]
      summary: 'Listar credenciais'
      responses:
        '200':
          description: 'Credenciais do escritório. O segredo nunca é devolvido.'
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/CredencialApi' }
    post:
      operationId: postEscritorioCredenciaisApi
      tags: [Integrações]
      summary: 'Criar credencial'
      description: |
        O `client_secret` é devolvido **somente nesta resposta**. Limite de 10 ativas.

        `entidade_ids` (opcional) restringe a credencial a algumas entidades do membro.
        Ausente ou `null`, a credencial alcança tudo o que o membro alcança.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [nome, usuario_id]
              properties:
                nome: { type: string, minLength: 3, maxLength: 100, example: ERP Folha }
                usuario_id: { type: string, format: uuid, description: Membro ativo do escritório. }
                entidade_ids:
                  type: [array, 'null']
                  items: { type: string, format: uuid }
                  minItems: 1
                  maxItems: 2000
                  description: 'Escopo da credencial. Todas precisam estar no alcance do membro.'
      responses:
        '201':
          description: 'Credencial criada.'
          content:
            application/json:
              example:
                success: true
                credencial: { id: 0190..., nome: ERP Folha, client_id: cco_3f9a1c2b7d4e5f6a7b8c9d0e, secret_prefixo: cco_sk_Xy12a, ativo: true }
                client_secret: cco_sk_Xy12aBcD...
                aviso: 'Guarde o client_secret agora: ele não será exibido novamente.'
        '400': { description: '`ENTIDADE_FORA_DO_MEMBRO` — `entidades` lista as recusadas.' }
        '404': { description: 'Membro ativo não encontrado no escritório.' }
        '409': { description: '`LIMITE_CREDENCIAIS`.' }

  /escritorio/credenciais-api/{id}/entidades:
    put:
      operationId: putEscritorioCredenciaisApiIdEntidades
      tags: [Integrações]
      summary: 'Alterar o escopo de entidades da credencial'
      description: |
        Troca o escopo sem trocar o segredo. `entidade_ids: null` libera todas as
        entidades do membro dono. Vale para tokens já emitidos em até 5 minutos.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [entidade_ids]
              properties:
                entidade_ids:
                  type: [array, 'null']
                  items: { type: string, format: uuid }
                  minItems: 1
            example: { entidade_ids: [0190..., 0190...] }
      responses:
        '200': { description: 'Escopo atualizado.' }
        '400': { description: '`ENTIDADE_FORA_DO_MEMBRO`.' }
        '404': { description: 'Credencial ativa não encontrada.' }

  /escritorio/credenciais-api/{id}:
    delete:
      operationId: deleteEscritorioCredenciaisApiId
      tags: [Integrações]
      summary: 'Revogar credencial'
      description: 'Novos tokens deixam de ser emitidos na hora; tokens já emitidos valem até expirar.'
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: 'Revogada.' }
        '404': { $ref: '#/components/responses/NaoEncontrado' }

  /entidades/minhas:
    get:
      operationId: getEntidadesMinhas
      tags: [Entidades]
      summary: 'Entidades acessíveis (paginado)'
      parameters:
        - { name: search, in: query, schema: { type: string, maxLength: 120 } }
        - { name: tipo, in: query, description: 'Lista separada por vírgula.', schema: { type: string, example: 'prefeitura,fundo' } }
        - name: status
          in: query
          schema: { type: string, enum: [publicado, aguardando_envio, nenhum_envio, aguardando_aprovacao] }
        - $ref: '#/components/parameters/Page'
        - { name: limit, in: query, schema: { type: integer, default: 24, maximum: 100 } }
      responses:
        '200':
          description: 'Página de entidades.'
          content:
            application/json:
              example: { data: [], total: 0, page: 1, limit: 24 }

  /entidades/minhas/resumo:
    get:
      operationId: getEntidadesMinhasResumo
      tags: [Entidades]
      summary: 'Contadores da carteira'
      responses:
        '200': { description: 'Contadores por situação.' }

  /entidades/consulta-cnpj:
    get:
      operationId: getEntidadesConsultaCnpj
      tags: [Entidades]
      summary: 'Verificar se um CNPJ já está cadastrado'
      parameters:
        - { name: cnpj, in: query, required: true, schema: { type: string, example: '06553556000147' } }
      responses:
        '200':
          description: |
            `existe: false` traz os dados da Receita para cadastro. `existe: true` informa
            se você já tem acesso (`acesso: concedido`) ou pode solicitá-lo.

  /entidades:
    post:
      operationId: postEntidades
      tags: [Entidades]
      summary: 'Cadastrar entidade'
      description: |
        A entidade entra na carteira do escritório. **Órgãos públicos** nascem
        pendentes e só recebem folha após a conferência do suporte (`202`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cnpj, nome, tipo]
              properties:
                cnpj: { type: string }
                nome: { type: string, minLength: 3, maxLength: 150 }
                tipo: { $ref: '#/components/schemas/TipoEntidade' }
                municipio: { type: [string, 'null'], maxLength: 100 }
                uf: { type: [string, 'null'], minLength: 2, maxLength: 2 }
                endereco: { type: [string, 'null'], maxLength: 200 }
      responses:
        '201': { description: 'Cadastrada e disponível.' }
        '202': { description: 'Cadastrada, aguardando aprovação do suporte.' }
        '409': { description: 'CNPJ já cadastrado.' }

  /entidades/{id}:
    get:
      operationId: getEntidadesId
      tags: [Entidades]
      summary: 'Detalhe da entidade'
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: 'Dados da entidade.' }
        '403': { $ref: '#/components/responses/SemAcessoEntidade' }

  /entidades/{id}/membros:
    get:
      operationId: getEntidadesIdMembros
      tags: [Entidades]
      summary: 'Quem acessa a entidade'
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: 'Membros com acesso.' }

  /entidades/{id}/solicitacoes-acesso:
    post:
      operationId: postEntidadesIdSolicitacoesAcesso
      tags: [Solicitações de acesso]
      summary: 'Pedir acesso a uma entidade de outro escritório'
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                papel_solicitado: { $ref: '#/components/schemas/Papel' }
                justificativa: { type: string, maxLength: 1000 }
      responses:
        '201': { description: 'Solicitação registrada.' }
        '409': { description: 'Já existe solicitação pendente ou você já tem acesso.' }

  /solicitacoes-acesso/recebidas:
    get:
      operationId: getSolicitacoesAcessoRecebidas
      tags: [Solicitações de acesso]
      summary: 'Solicitações para entidades que você administra'
      parameters:
        - { name: status, in: query, schema: { type: string, example: PENDENTE } }
        - { name: entidade_id, in: query, schema: { type: string, format: uuid } }
      responses:
        '200': { description: '`{ data, total }`' }

  /solicitacoes-acesso/enviadas:
    get:
      operationId: getSolicitacoesAcessoEnviadas
      tags: [Solicitações de acesso]
      summary: 'Solicitações que você fez'
      parameters:
        - { name: status, in: query, schema: { type: string } }
      responses:
        '200': { description: '`{ data, total }`' }

  /solicitacoes-acesso/{id}/aprovar:
    post:
      operationId: postSolicitacoesAcessoIdAprovar
      tags: [Solicitações de acesso]
      summary: 'Aprovar solicitação'
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                papel: { $ref: '#/components/schemas/Papel' }
      responses:
        '200': { description: 'Aprovada.' }
        '403': { description: 'Sem permissão, ou papel acima do seu.' }
        '409': { description: 'Já decidida.' }

  /solicitacoes-acesso/{id}/recusar:
    post:
      operationId: postSolicitacoesAcessoIdRecusar
      tags: [Solicitações de acesso]
      summary: 'Recusar solicitação'
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                motivo: { type: string, maxLength: 1000 }
      responses:
        '200': { description: 'Recusada.' }

  /solicitacoes-acesso/{id}/cancelar:
    post:
      operationId: postSolicitacoesAcessoIdCancelar
      tags: [Solicitações de acesso]
      summary: 'Cancelar a própria solicitação'
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: 'Cancelada.' }

  /cc/analisar:
    post:
      operationId: postCcAnalisar
      tags: [Folha de pagamento]
      summary: 'Analisar arquivo antes de enviar (não grava nada)'
      description: |
        Lê o CNPJ e a competência do arquivo e diz o que fazer com ele, no campo `acao`:

        | acao | significado |
        |---|---|
        | `importar` | pode enviar para a entidade ativa |
        | `trocar_contexto` | é de outra entidade da carteira — envie com `entidade_id` = `entidade.id` |
        | `criar` | empresa não cadastrada — use `/cc/criar-entidade-do-arquivo` |
        | `solicitar_acesso` | pertence a outro escritório |
        | `distribuir` | XML com várias empresas, rateado por CNPJ |
        | `bloqueado` | não pode ser importado; veja `message` |
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema: { $ref: '#/components/schemas/ArquivoFolha' }
      responses:
        '200':
          description: 'Resultado da análise.'
          content:
            application/json:
              example:
                origem: xml
                cnpj: '06553556000147'
                cnpj_formatado: 06.553.556/0001-47
                razao_social: PREFEITURA MUNICIPAL DE EXEMPLO
                competencia: '202608'
                trabalhadores: 412
                multiplas_empresas: false
                acao: importar
                entidade: { id: 0190..., nome: PREFEITURA MUNICIPAL DE EXEMPLO, tipo: prefeitura }
                papel: OPERADOR_FOLHA
                message: Pronto para importar em PREFEITURA MUNICIPAL DE EXEMPLO.
        '400': { description: 'Arquivo ausente ou ilegível (`acao: bloqueado`).' }
        '503': { $ref: '#/components/responses/FilaCheia' }

  /cc/criar-entidade-do-arquivo:
    post:
      operationId: postCcCriarEntidadeDoArquivo
      tags: [Folha de pagamento]
      summary: 'Cadastrar a empresa a partir do arquivo'
      description: 'Use quando `/cc/analisar` responder `acao: criar`. Depois, envie o arquivo em `/cc/upload`.'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              allOf:
                - $ref: '#/components/schemas/ArquivoFolha'
                - type: object
                  properties:
                    nome: { type: string }
                    tipo: { $ref: '#/components/schemas/TipoEntidade' }
                    municipio: { type: string }
                    uf: { type: string }
      responses:
        '201': { description: 'Cadastrada; já pode importar.' }
        '202': { description: 'Cadastrada; aguarda aprovação do suporte.' }
        '409': { description: 'CNPJ já cadastrado.' }

  /cc/upload:
    post:
      operationId: postCcUpload
      tags: [Folha de pagamento]
      summary: 'Enviar folha (contracheques)'
      description: |
        Exige OPERADOR_FOLHA na entidade de destino. Aceita XML padrão, planilha
        Alterdata/Fortes (`.xlsx`/`.xls`) e PDF. O CNPJ do arquivo precisa ser o da
        entidade de destino.

        Reenviar o **mesmo arquivo** para a mesma competência retorna `409`
        (`ARQUIVO_JA_IMPORTADO`): para substituir, exclua a competência antes
        em `/cc/delete-exercicio`.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              allOf:
                - $ref: '#/components/schemas/ArquivoFolha'
                - type: object
                  properties:
                    entidade_id:
                      type: string
                      format: uuid
                      description: 'Destino. Omitido, usa a entidade ativa.'
      responses:
        '200':
          description: 'Importado. `ignorados` conta linhas já existentes com os mesmos valores; `origem` e `resumo` só vêm em planilha.'
          content:
            application/json:
              example:
                message: { status: OK, inseridos: 412, ignorados: 0, blocos: 1 }
                origem: alterdata
        '400': { description: 'Arquivo inválido, CNPJ de outra entidade ou formato recusado (`code` explica).' }
        '403': { $ref: '#/components/responses/SemAcessoEntidade' }
        '409': { description: '`ARQUIVO_JA_IMPORTADO`.' }
        '503': { $ref: '#/components/responses/FilaCheia' }

  /cc/listar-meses:
    get:
      operationId: getCcListarMeses
      tags: [Folha de pagamento]
      summary: 'Competências publicadas'
      description: 'Exige CONSULTA. Entidade ativa.'
      parameters:
        - $ref: '#/components/parameters/EntidadeId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Search'
      responses:
        '200':
          description: 'Competências com total de contracheques e mensagens.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  total: { type: integer, description: 'Total de competências (para paginar).' }
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        comp_mes: { type: string, example: '08' }
                        comp_ano: { type: string, example: '2026' }
                        comp_desc: { type: string, example: AGOSTO/2026 }
                        total: { type: string, description: 'Contracheques na competência (numérico, em texto).', example: '412' }
                        mensagem: { type: [string, 'null'] }
                        mensagem_id: { type: [string, 'null'], format: uuid }

  /cc/listar-servidores-mes:
    get:
      operationId: getCcListarServidoresMes
      tags: [Folha de pagamento]
      summary: 'Trabalhadores de uma competência'
      description: 'Exige CONSULTA. Entidade ativa.'
      parameters:
        - $ref: '#/components/parameters/EntidadeId'
        - $ref: '#/components/parameters/Mes'
        - $ref: '#/components/parameters/Ano'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Search'
      responses:
        '200':
          description: 'Página de trabalhadores.'
          content:
            application/json:
              example: { data: [{ id: 0190..., nome: MARIA DA SILVA, cpf: '12345678909', email: maria@exemplo.com, telefone: null, matricula: '1024' }], total: 1 }

  /cc/consultar-servidor-cc:
    post:
      operationId: postCcConsultarServidorCc
      tags: [Folha de pagamento]
      summary: 'Contracheque de um trabalhador'
      description: 'Exige CONSULTA. Devolve o contracheque com as verbas.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mes, ano, matricula, cpf]
              properties:
                entidade_id: { $ref: '#/components/schemas/EntidadeIdCorpo' }
                mes: { type: [integer, string], minimum: 1, maximum: 12, example: 8 }
                ano: { type: [integer, string], example: 2026 }
                matricula: { type: string }
                cpf: { type: string }
      responses:
        '200': { description: 'Contracheque(s) com `verbas`.' }

  /cc/delete-exercicio:
    post:
      operationId: postCcDeleteExercicio
      tags: [Folha de pagamento]
      summary: 'Excluir uma competência'
      description: 'Exige OPERADOR_FOLHA. Remove todos os contracheques da competência na entidade ativa.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mes, ano]
              properties:
                entidade_id: { $ref: '#/components/schemas/EntidadeIdCorpo' }
                mes: { type: [string, integer], example: 8 }
                ano: { type: [string, integer], example: 2026 }
      responses:
        '200': { description: 'Excluída.' }

  /cc/listar-mensagens:
    get:
      operationId: getCcListarMensagens
      tags: [Folha de pagamento]
      summary: 'Mensagens das competências'
      description: 'Exige CONSULTA.'
      parameters:
        - $ref: '#/components/parameters/EntidadeId'
      responses:
        '200':
          description: 'Mensagens exibidas nos contracheques.'
          content:
            application/json:
              example: [{ id: 0190..., mensagem: 'Feliz aniversário da cidade!', competencia: 08/2026 }]

  /cc/gravar-mensagem:
    post:
      operationId: postCcGravarMensagem
      tags: [Folha de pagamento]
      summary: 'Gravar mensagem na competência'
      description: 'Exige OPERADOR_FOLHA. O texto aparece no contracheque dos trabalhadores.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message, mes, ano]
              properties:
                entidade_id: { $ref: '#/components/schemas/EntidadeIdCorpo' }
                message: { type: string }
                mes: { type: [string, integer], example: 8 }
                ano: { type: [string, integer], example: 2026 }
      responses:
        '200': { description: 'Gravada.' }

  /cc/apagar-mensagem/{id}:
    delete:
      operationId: deleteCcApagarMensagemId
      tags: [Folha de pagamento]
      summary: 'Apagar mensagem'
      description: 'Exige OPERADOR_FOLHA.'
      parameters:
        - $ref: '#/components/parameters/EntidadeId'
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: 'Apagada.' }
        '404': { description: '`MENSAGEM_NAO_ENCONTRADA` — o id não pertence à entidade.' }

  /cc/vinculos:
    get:
      operationId: getCcVinculos
      tags: [Servidores]
      summary: 'Dados de vínculo do trabalhador'
      description: 'Exige CONSULTA.'
      parameters:
        - $ref: '#/components/parameters/EntidadeId'
        - { name: servidor_id, in: query, required: true, schema: { type: string, format: uuid } }
        - { name: matricula, in: query, required: true, schema: { type: string } }
      responses:
        '200':
          description: 'Dados de vínculo do contracheque mais recente da matrícula.'
          content:
            application/json:
              example:
                servidor_matricula: '1024'
                servidor_funcao: ASSISTENTE ADMINISTRATIVO
                servidor_funcao_nivel: II
                lotacao_departamento: SECRETARIA DE ADMINISTRAÇÃO
                lotacao_setor: RH
                tipo_admissao: CLT
                data_admissao: 01/03/2020
                banco_agencia: '1234'
                banco_conta: 56789-0
                codigo_verificacao: A1B2C3D4
                comp_mes: '08'
                comp_ano: '2026'
        '404': { $ref: '#/components/responses/NaoEncontrado' }

  /cc/atualizar-vinculos:
    put:
      operationId: putCcAtualizarVinculos
      tags: [Servidores]
      summary: 'Corrigir dados de vínculo'
      description: 'Exige OPERADOR_FOLHA. Informe ao menos um campo além de `servidor_id` e `matricula`.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [servidor_id, matricula]
              properties:
                entidade_id: { $ref: '#/components/schemas/EntidadeIdCorpo' }
                servidor_id: { type: string, format: uuid }
                matricula: { type: string }
                servidor_funcao: { type: string }
                servidor_funcao_nivel: { type: string }
                lotacao_departamento: { type: string }
                lotacao_setor: { type: string }
                tipo_admissao: { type: string }
                data_admissao: { type: string, example: 01/03/2020, description: DD/MM/AAAA }
                banco_agencia: { type: string }
                banco_conta: { type: string }
      responses:
        '200': { description: 'Atualizado.' }

  /cc/alterar-email:
    post:
      operationId: postCcAlterarEmail
      tags: [Servidores]
      summary: 'Atualizar contato do trabalhador'
      description: 'Exige OPERADOR_FOLHA.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [matricula, cpf, email]
              properties:
                entidade_id: { $ref: '#/components/schemas/EntidadeIdCorpo' }
                matricula: { type: string }
                cpf: { type: string }
                email: { type: string }
                telefone: { type: string }
      responses:
        '200': { description: 'Atualizado.' }

  /empresas/senhas-servidores:
    get:
      operationId: getEmpresasSenhasServidores
      tags: [Servidores]
      summary: 'Senhas de acesso dos trabalhadores'
      description: 'Exige OPERADOR_FOLHA. Entidade ativa.'
      parameters:
        - $ref: '#/components/parameters/EntidadeId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Search'
        - { name: sem_email, in: query, description: 'Só quem não tem e-mail cadastrado.', schema: { type: boolean } }
      responses:
        '200': { description: '`{ data, total }`' }

  /empresas/enviar-senha-email/{id_servidor}:
    post:
      operationId: postEmpresasEnviarSenhaEmailIdServidor
      tags: [Servidores]
      summary: 'Enviar a senha ao trabalhador por e-mail'
      description: 'Exige OPERADOR_FOLHA.'
      parameters:
        - $ref: '#/components/parameters/EntidadeId'
        - { name: id_servidor, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: 'E-mail enviado.' }

  /empresas/alterar-senha-servidor/{id_servidor}:
    post:
      operationId: postEmpresasAlterarSenhaServidorIdServidor
      tags: [Servidores]
      summary: 'Definir nova senha do trabalhador'
      description: 'Exige OPERADOR_FOLHA.'
      parameters:
        - { name: id_servidor, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [nova_senha]
              properties:
                entidade_id: { $ref: '#/components/schemas/EntidadeIdCorpo' }
                nova_senha: { type: string }
      responses:
        '200': { description: 'Senha alterada.' }

  /empresas/dados-prefeitura:
    get:
      operationId: getEmpresasDadosPrefeitura
      tags: [Entidades]
      summary: 'Dados cadastrais da entidade ativa'
      parameters:
        - $ref: '#/components/parameters/EntidadeId'
      responses:
        '200': { description: 'Nome, CNPJ, endereço e logomarca.' }

  /pdf/contracheque-x:
    post:
      operationId: postPdfContrachequeX
      tags: [Documentos]
      summary: 'Dados de um contracheque para gerar o PDF'
      description: 'Exige CONSULTA. Devolve empresa, trabalhador, verbas e código de verificação.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mes, ano, matricula, cpf]
              properties:
                entidade_id: { $ref: '#/components/schemas/EntidadeIdCorpo' }
                mes: { type: [integer, string], example: 8 }
                ano: { type: [integer, string], example: 2026 }
                matricula: { type: string }
                cpf: { type: string }
      responses:
        '200': { description: 'Dados do documento.' }

  /pdf/lista-senhas:
    get:
      operationId: getPdfListaSenhas
      tags: [Documentos]
      summary: 'Dados da lista de senhas'
      description: 'Exige OPERADOR_FOLHA.'
      parameters:
        - $ref: '#/components/parameters/EntidadeId'
      responses:
        '200': { description: '`{ empresa, servidores }`' }

  /pdf/canhoto-senhas:
    get:
      operationId: getPdfCanhotoSenhas
      tags: [Documentos]
      summary: 'Dados dos canhotos de senha'
      description: 'Exige OPERADOR_FOLHA.'
      parameters:
        - $ref: '#/components/parameters/EntidadeId'
      responses:
        '200': { description: '`{ empresa, servidores }`' }

  /leitor-pdf/converter-contracheque:
    post:
      operationId: postLeitorPdfConverterContracheque
      tags: [Documentos]
      summary: 'Converter contracheque em PDF para XML'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [pdf]
              properties:
                pdf: { type: string, format: binary }
      responses:
        '200': { description: '`{ arquivos }` — XMLs gerados, prontos para `/cc/upload`.' }

  /user/dados-usuario:
    get:
      operationId: getUserDadosUsuario
      tags: [Conta]
      summary: 'Dados do membro autenticado'
      responses:
        '200': { description: 'Nome, login e e-mail.' }

  /legal/pendencias:
    get:
      operationId: getLegalPendencias
      tags: [Conta]
      summary: 'Termos pendentes de aceite'
      responses:
        '200':
          description: 'Documentos vigentes ainda não aceitos.'
          content:
            application/json:
              example: { data: [], em_dia: true }

  /legal/aceitar:
    post:
      operationId: postLegalAceitar
      tags: [Conta]
      summary: 'Aceitar termos'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [aceites]
              properties:
                aceites:
                  type: array
                  items:
                    type: object
                    required: [tipo, versao]
                    properties:
                      tipo: { type: string, enum: [TERMOS_USO, POLITICA_PRIVACIDADE] }
                      versao: { type: string }
      responses:
        '200': { description: 'Aceite registrado.' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: 'Token obtido em `POST /auth/token`.'

  parameters:
    EntidadeId:
      name: entidade_id
      in: query
      required: false
      description: 'Entidade da carteira em que operar. Omitido, usa a entidade ativa do token.'
      schema: { type: string, format: uuid }
    Page: { name: page, in: query, schema: { type: integer, default: 1, minimum: 1 } }
    Limit: { name: limit, in: query, schema: { type: integer, default: 12, maximum: 100 } }
    Search: { name: search, in: query, schema: { type: string } }
    Mes: { name: mes, in: query, required: true, schema: { type: integer, minimum: 1, maximum: 12 } }
    Ano: { name: ano, in: query, required: true, schema: { type: integer, example: 2026 } }

  responses:
    NaoAutenticado:
      description: 'Token ausente.'
    TokenInvalido:
      description: 'Token inválido ou expirado — peça outro em `/auth/token`.'
    SemAcessoEntidade:
      description: '`SEM_ACESSO_ENTIDADE` ou `ENTIDADE_NAO_APROVADA`.'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Erro' }
    PapelInsuficiente:
      description: '`PAPEL_INSUFICIENTE`.'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Erro' }
    NaoEncontrado:
      description: 'Não encontrado.'
    FilaCheia:
      description: 'Fila de importação cheia. Aguarde alguns segundos e reenvie.'

  schemas:
    EntidadeIdCorpo:
      type: string
      format: uuid
      description: 'Entidade da carteira em que operar. Omitido, usa a entidade ativa do token.'
    Papel:
      type: string
      enum: [CONSULTA, OPERADOR_FOLHA, ADMINISTRADOR]
    TipoEntidade:
      type: string
      enum: [prefeitura, fundo, camara, autarquia, empresa_privada]
    Erro:
      type: object
      properties:
        error: { type: boolean, example: true }
        message: { type: string }
        code: { type: string }
    ErroOAuth:
      type: object
      properties:
        error: { type: string, example: invalid_client }
        error_description: { type: string }
    PedidoToken:
      type: object
      required: [grant_type, client_id, client_secret]
      properties:
        grant_type: { type: string, enum: [client_credentials] }
        client_id: { type: string, example: cco_3f9a1c2b7d4e5f6a7b8c9d0e }
        client_secret: { type: string, example: cco_sk_... }
    Token:
      type: object
      properties:
        access_token: { type: string }
        token_type: { type: string, example: Bearer }
        expires_in: { type: integer, example: 14400 }
        entidade_ativa:
          type: [object, 'null']
          properties:
            id: { type: string, format: uuid }
            nome: { type: string }
            cnpj: { type: string }
            tipo: { $ref: '#/components/schemas/TipoEntidade' }
            papel: { $ref: '#/components/schemas/Papel' }
        escritorio:
          type: [object, 'null']
          properties:
            id: { type: string, format: uuid }
            nome: { type: string }
            papel: { $ref: '#/components/schemas/Papel' }
    CredencialApi:
      type: object
      properties:
        id: { type: string, format: uuid }
        nome: { type: string }
        client_id: { type: string }
        secret_prefixo: { type: string }
        ativo: { type: boolean }
        data_criacao: { type: string, format: date-time }
        ultimo_uso: { type: [string, 'null'], format: date-time }
        data_revogacao: { type: [string, 'null'], format: date-time }
        entidades_restritas: { type: boolean, description: 'false = alcança todas as entidades do membro.' }
        entidades:
          type: array
          description: 'Escopo, quando `entidades_restritas`.'
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              nome: { type: string }
        usuario:
          type: object
          properties:
            id: { type: string, format: uuid }
            nome: { type: string }
            nick: { type: string }
            papel_escritorio: { $ref: '#/components/schemas/Papel' }
    ArquivoFolha:
      type: object
      required: [arquivo]
      properties:
        arquivo: { type: string, format: binary, description: 'Até 10 MB. XML no layout do [XSD oficial](/docs/api/contracheque.xsd) — veja o [exemplo](/docs/api/CC_202608_11222333000181_00001.xml).' }
        origem:
          type: string
          enum: [xml, planilha, pdf]
          description: 'Formato declarado. Recomendado — o erro fica específico quando não confere.'
