Skip to content

Clients API: Especificação para Backend

Contexto

O módulo Clientes é o espelho do módulo Team. Enquanto GET /team lista os prestadores (contraparte com papel PROVIDER) de contratos onde a empresa logada é a tomadora, GET /clients lista os clientes/tomadores (contraparte com papel BORROWER) de contratos onde a empresa logada é a prestadora.

A perspectiva do contrato vem do workflow (Workflow.companyRole = 'borrower' | 'provider') e é herdada pelo contrato. O papel de cada parte fica persistido em ContractPart.role (BORROWER | PROVIDER | NEUTRAL). Ver contracts.md para a perspectiva e o papel das partes.

Como Clientes é o espelho de Team, ele reutiliza a mesma infraestrutura (mesmo mapper TeamMember, mesmos painéis e abas): não crie endpoints/serviços duplicados, apenas troque o filtro de papel da contraparte de PROVIDER para BORROWER e a perspectiva do contrato de tomadora para prestadora.

A página de detalhe (/clients/:id) tem:

  • Sidebar lateral direita com painéis: Contrato, Documentos, Ajuste de horas, Geral, Sessões (apenas PF)
  • 5 abas: Entries, Consolidated, Compliance, Invoices, History
  • Switch Ativo/Inativo no header do painel "Geral" (controla ContractPart.isActive)

:id em todas as rotas pode ser interpretado como o companyId/userId do cliente exceto no endpoint de toggle, onde é o contractPartId.


Modelo: TeamMember (reutilizado)

GET /clients devolve a mesma estrutura TeamMember documentada em team-api.md. Nada novo é introduzido no shape; muda apenas quem é listado (a contraparte BORROWER de contratos onde a empresa é PROVIDER).

Regra do user notnull (idêntica a Team):

  • PF (ContractPart.userId != null): user = User do contrato; company = null
  • PJ (ContractPart.companyId != null): user = User responsável (OWNER da Company); company = a Company

Endpoints

GET /clients

Lista todos os clientes/tomadores da empresa prestadora com resumo de horas (PJ + PF mesclados). Espelho de GET /team.

Auth: Bearer token + x-company-id (empresa prestadora em contexto)

Filtro de papel: apenas partes com ContractPart.role = BORROWER em contratos cuja perspectiva (companyRole) é provider.

Query Params:

ParamTipoObrigatórioDescrição
searchstringNãoBusca textual
status"active" | "inactive"NãoFiltrar por status
pagenumberNãoPaginação
pageSizenumberNãoPaginação

A busca cobre simultaneamente: company.name, company.tradeName, company.document, user.name, user.email, user.cpf.

Response 200: { data: TeamMember[], meta: { total, page, pageSize, totalPages } }


GET /clients/:id

Retorna o detalhe completo de um cliente: dados cadastrais (member), contrato ativo, histórico, documentos e sessões. Mesma resposta de GET /team/:id.

Auth: Bearer token + x-company-id

Path Params:

ParamTipoDescrição
idstring (UUID)companyId (PJ) ou userId (PF) do cliente

Response 200:

json
{
  "member": TeamMember,
  "contract": TeamMemberContract | null,
  "contractsHistory": TeamMemberContractHistory[],
  "documents": TeamMemberDocument[],
  "sessions": SessionEntry[]
}

Response 404: Cliente não encontrado


PATCH /clients/:contractPartId/active

Ativa ou inativa o vínculo do cliente com a empresa. Atualiza somente ContractPart.isActive: não toca em User.status nem Company. Mesmo comportamento de PATCH /team/:contractPartId/active.

Body:

json
{ "isActive": true }

Response 200: { data: TeamMember }

Response 404: ContractPart não encontrado para a empresa atual


DELETE /clients/:id

Remove o vínculo (deleta o ContractPart) entre o cliente e a empresa. Mesmo comportamento de DELETE /team/:id.

Response 204: No Content


Compliance do cliente (reutiliza a máquina de compliance, sem duplicar)

O compliance do cliente não cria um mecanismo novo: reutiliza integralmente os fluxos de compliance (compliance-flows) e o runtime ProviderCompliance, apenas anexando um fluxo existente ao par (empresa prestadora → cliente), na direção inversa da do Team (aqui a empresa exige e o cliente cumpre): providerComplianceFlow.borrowerId = empresa, providerId = cliente.

GET /clients/:id/compliances

Lista os compliances do cliente (runtime). Reutiliza TeamRepository.findProviderCompliances(borrowerId=empresa, providerId=cliente) + ProviderComplianceMapper. Response 200: { data: ProviderCompliance[] } (mesma shape de GET /team/:id/compliances).

GET /clients/:id/compliance-flows

Lista os fluxos de compliance anexados ao cliente. Reutiliza TeamService.findComplianceFlows. Response 200: { data: ProviderComplianceFlow[] }.

POST /clients/:id/compliance-flows

Anexa um fluxo de compliance existente ao cliente e gera o período corrente. Reutiliza TeamService.attachComplianceFlow + ProviderComplianceRepository.findOrCreateForPeriod. Body: { complianceFlowId: string }. Response 201: { data: ProviderComplianceFlow }.

DELETE /clients/:id/compliance-flows/:flowId

Desanexa um fluxo de compliance do cliente. Reutiliza TeamService.removeComplianceFlow (mesma máquina do prestador). O :flowId é o providerComplianceFlowId que vem no objeto de compliance (ProviderCompliance.providerComplianceFlowId), usado para identificar o anexo. Response 204: No Content.

Na UI, o botão "Remover" no header do fluxo de compliance dispara este endpoint (com confirmação). Diferença em relação ao prestador: o cliente não tem PUT /clients/:id/compliance-flows/:flowId (edição de chefias imediatas por etapa); o fluxo do cliente não usa lideranças por etapa, então o header oferece apenas "Remover".

PATCH /clients/:id/compliances/:complianceId/steps/:stepId/review

Aprova/rejeita um documento de uma etapa. Reutiliza TeamService.reviewComplianceStep. Body: { documentId, action: "approve" | "reject", rejectionReason? }.


Documentos, cobranças e notas do cliente (mini-CRM)

Conceitos novos (não existiam no sistema), com tabelas próprias client_documents, client_charges, client_notes (escopadas por companyId + clientId). Todos os endpoints exigem Bearer + x-company-id.

Método + rotaDescriçãoBodyResponse
GET /clients/:id/documentsDocumentos gerenciados (enviado/recebido/solicitado)-{ data: ClientDocument[] }
POST /clients/:id/documentsRegistrar documento (notifica o cliente: in-app + e-mail){ name, direction, category, status?, fileId?, fileName?, fileUrl? }{ data: ClientDocument }
PATCH /clients/:id/documents/:documentId/reviewAprovar/rejeitar documento{ action: "approve"|"reject", rejectionReason? }{ data: ClientDocument }
DELETE /clients/:id/documents/:documentIdExcluir documento (soft delete; notifica o cliente: in-app + e-mail; bloqueado para documento received)-204
GET /clients/:id/chargesCobranças do cliente-{ data: ClientCharge[] }
POST /clients/:id/chargesGerar cobrança (notifica o cliente: in-app + e-mail){ reference, amount, description?, status?, dueDate? }{ data: ClientCharge }
PATCH /clients/:id/charges/:chargeIdDar baixa / atualizar cobrança (409 se já paga){ status, paidAt? }{ data: ClientCharge }
GET /clients/:id/notesNotas internas-{ data: ClientNote[] }
POST /clients/:id/notesRegistrar nota{ content }{ data: ClientNote }

direction: sent | received | requested. category: contract | identity | fiscal | receipt | other. status (documento): pending | delivered | approved | rejected. status (cobrança): sent | paid | overdue (default sent; os antigos draft e viewed foram removidos). Enums persistidos em SCREAMING_SNAKE, expostos em lowercase.

Formulário "Solicitar documento" (front): espelha o passo de documentos do workflow (nome, descrição, formatos aceitos, tamanho máximo) e não mostra categoria (persiste other por padrão) nem toggle de obrigatório (todo documento solicitado é obrigatório). O upload do arquivo (envio ou cumprimento) reusa o UploadService global (registro em files + fileId); não há coluna de formato/tamanho no client_documents (o arquivo é validado pelo UploadService). Na UI do prestador a lista de documentos usa abas Recebidos/Enviados e preview ao clicar (com Baixar no preview), no mesmo padrão do Portal do Cliente.

A tabela client_charges tem uma coluna description (TEXT NULL) opcional, além de reference, amount, status, dueDate e paidAt.

Notificação ao cliente: registrar um documento (direction != received), excluir um documento, gerar uma cobrança e publicar um lançamento de horas disparam notificação in-app (NotificationService) e e-mail (EmailService + template Handlebars custom-notification), no padrão do sistema. A resolução de destinatário cobre o cliente PF (clientId = User.id) e PJ (clientId = Company.id: e-mail da empresa + membros via UserCompany). Tipos: DOCUMENT_UPLOADED para envio de documento/lançamento, ACTION_REQUIRED para solicitação/cobrança. Documentos received (anexados pelo cliente) não podem ser excluídos pela empresa.

Deep-link no portal: essas notificações do cliente gravam activity.section para o Portal do Cliente saber a aba de destino ao clicar: documento leva a documents, cobrança leva a invoices, lançamento de horas leva a worklogs.

Baixa idempotente: PATCH /clients/:id/charges/:chargeId marcando uma cobrança já paid como paga retorna 409 Conflict ("Cobrança já está quitada").


Portal público do cliente (deslogado)

Backend do Portal do Cliente. Vive dentro do módulo clients (não há módulo client-portal). Rota base public/client/:token com @Public() + @SkipCompany() e guard de sessão próprio; :token é um JWT que carrega o accessId (registro client_accesses), nunca o id do contrato.

Método + rotaAuthDescriçãoBodyResponse
GET public/client/:tokentokenContexto do acesso; auto-dispara OTP se exigir verificação-{ contractName, companyName, clientName, maskedEmail, hasAccount, loginUrl, requiresVerification }
POST public/client/:token/codetoken(Re)enviar o código OTP (cooldown)-{ sent, cooldown }
POST public/client/:token/verifytokenVerificar o código e abrir a sessão{ code }{ sessionToken }
GET public/client/:token/contracttoken + sessãoView curada do contrato-{ contract, documents, invoices, timeline, updates, workLogs }
POST public/client/:token/documents/:documentId/fulfilltoken + sessãoCumprir documento solicitado (multipart file); reusa o UploadService globalfile{ id, name }
POST public/client/:token/work-logs/:workLogId/acknowledgetoken + sessãoDar ciência num lançamento de horas-{ data: WorkLog }

O upload usa o UploadService global (cria registro em files + fileId no documento); o documento cumprido vira direction=received, status=delivered. updates[] traz { id, title, message, actorName, section, createdAt } (a section alimenta o deep-link). hasAccount=true devolve loginUrl (login do tenant) e o front redireciona em vez de pedir OTP.

Gestão do link (lado do prestador, autenticado):

Método + rotaDescriçãoResponse
POST /clients/:id/contracts/:contractId/access-linkEmitir ou recuperar o link do portal do cliente{ data: { url } }
DELETE /clients/:id/contracts/:contractId/access-linkRevogar o link (marca revokedAt)204

Regras

  1. Clientes só lista contrapartes de contratos com perspectiva prestadora (companyRole = 'provider'); a mesma contraparte que fosse prestadora em outro contrato apareceria em Team, não aqui.
  2. Reutilizar o serviço/mapper de Team; a diferença é o filtro de papel (BORROWER) e de perspectiva.
  3. As mesmas regras de "quem aparece" da Equipe valem para Clientes (contrato vigente, dedup 1 registro por cliente, distrato/cancelamento/overdue removem). Ver business/clientes.md.
  4. Endpoints de compliance e horas do cliente seguem o padrão de Team (/monitoring/... e compliance por membro), sem duplicação.

Documentos relacionados