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 ​