Catalisa.Building Blocks
Catálogo/Dados/Customers

Customers

Produção

Cadastro de pessoas físicas brasileiras com CPF validado e isolado por organização

5
Endpoints
1
Entidades
0
Provedores
Tenant
Escopo
3003
Porta
2026-02
Desde

Você guarda o cadastro dos seus clientes pessoa física com CPF conferido, endereço, renda e conta bancária no formato que a operação de crédito brasileira usa — sem escrever a tabela, a validação e o isolamento por empresa de novo em cada produto.

Para quem é
  • Fintechs de crédito que precisam do cadastro do tomador antes de simular, decidir e contratar
  • Plataformas B2B que operam várias empresas clientes na mesma instância e não podem misturar cadastro
  • Times que integram vários building blocks e querem uma única identidade de pessoa entre eles
Substitui
  • Tabela de pessoas reescrita dentro de cada serviço, com a validação de CPF copiada e colada
  • Camada caseira de "de qual empresa é este cliente" espalhada por consultas SQL
O que não é
  • Um CRM — não tem funil, oportunidade, campanha nem histórico de interação comercial
  • Um cadastro de pessoa jurídica — o CNPJ está no enum de tipo, mas não é aceito hoje (ver §15)
  • Um serviço de KYC ou consulta a bureau — não checa CPF na Receita, não consulta score, não faz prova de vida
  • Um cadastro positivo ou base de enriquecimento de dados
O que dá para fazer

6 endpoints em 2 recursos.

Explorar a API →
01

Resumo executivo

O Customers guarda quem é o seu cliente pessoa física. Nome, CPF, contato, data de nascimento, nome da mãe, estado civil, vínculo de trabalho, renda mensal, endereço completo e conta bancária — os campos que uma operação de crédito brasileira pede no primeiro formulário e usa em todas as etapas seguintes.

Na prática ele resolve o momento em que a sua plataforma passa de um serviço para cinco. Sem um cadastro central, o motor de decisão tem uma cópia da pessoa, a esteira de contrato tem outra, o faturamento tem uma terceira, e o dia em que o cliente troca de telefone você descobre que existem três telefones diferentes e nenhum deles é o certo. Aqui a pessoa tem um identificador só, e os outros building blocks apontam para ele.

Está em produção, publicado em staging e em produção como serviço próprio (customers.bb.stg.catalisa.app), com cobertura de testes unitários e de integração. É um building block pequeno de propósito: cinco rotas, uma tabela, nenhuma integração externa. O que ele entrega não é funcionalidade, é o cadastro certo no lugar certo — leia a §15 antes de assumir que ele faz KYC, porque ele não faz.

AtributoValor
Identificadorcustomers
CategoriaDados
EscopoTenant (exige organizationId no token em todas as 5 rotas)
Porta (standalone)3003
Path alias@customers
Prefixo HTTP/customers
Schema PostgreSQLcustomers
StatusProdução
Depende dePostgreSQL, Redis (publicação de eventos), IAM
PermissõesCUSTOMERS_PEOPLE_CREATE, CUSTOMERS_PEOPLE_READ, CUSTOMERS_PEOPLE_UPDATE, CUSTOMERS_PEOPLE_DELETE

02

O problema

negócio

O cenário. Uma fintech de crédito começa com um produto e um banco de dados. O tomador é cadastrado na tela de originação e a vida segue. Dois anos depois existem uma esteira de proposta, um motor de decisão, um contrato eletrônico, uma régua de cobrança e um faturamento — e cada um deles guarda a própria versão da mesma pessoa.

O que trava hoje.

  • A pessoa existe cinco vezes. Cada serviço tem a própria tabela de cliente, com o próprio nome de coluna e a própria regra de obrigatoriedade. Responder "qual é o telefone atual do cliente" vira uma pergunta com cinco respostas.
  • A validação de CPF é copiada e colada. O algoritmo do dígito verificador está em três repositórios, em versões que divergiram. Um deles aceita 111.111.111-11, e ninguém sabe qual até um cadastro fantasma aparecer na carteira.
  • O isolamento entre empresas clientes é disciplina, não garantia. "Todo mundo lembra de filtrar por organization_id" funciona até o dia em que alguém não lembra. Em cadastro com CPF, esse esquecimento é incidente de dado pessoal, não bug de listagem.
  • Apagar cliente apaga histórico. Um DELETE físico resolve o pedido do cliente e destrói a rastreabilidade da operação de crédito que ele contratou — que precisa sobreviver por anos.
  • O formulário brasileiro não cabe em modelo genérico. Nome da mãe, faixa de renda, tipo de vínculo empregatício, agência com dígito e tipo de conta não são campos de um cadastro internacional. Ou você modela isso, ou empurra tudo para um metadata que ninguém consegue consultar.

O custo de não resolver. O custo visível é o retrabalho: cada produto novo começa reescrevendo o cadastro, a validação e o isolamento. O custo invisível é maior. Dado pessoal duplicado em cinco lugares é dado pessoal que você não consegue corrigir, exportar nem eliminar de forma completa quando o titular pedir — e a LGPD dá ao titular exatamente esses direitos (Lei 13.709/2018, art. 18). Uma base fragmentada transforma um pedido de dez minutos em um projeto.


03

Proposta de valor

negócio
AntesDepois
Cinco tabelas de pessoa, cinco verdadesUm personId, e os outros building blocks apontam para ele
Validação de CPF copiada em cada serviçoDígito verificador conferido em um lugar, na criação
Filtro por organização é disciplina de equiperequireOrganization recusa a requisição antes da regra de negócio
DELETE apaga a linha e o histórico juntoExclusão lógica: sai das consultas, permanece para auditoria
Campo brasileiro vira metadata sem consultaUF, CEP, banco, agência, tipo de conta e faixa de renda são colunas

O CPF é conferido, não só armazenado. A criação sanitiza a máscara, valida os dois dígitos verificadores e recusa sequências repetidas. Cadastro com CPF impossível não entra.

O tenant vem do token. Nenhuma rota lê organizationId do corpo ou da query. Ele sai do JWT assinado pelo IAM, e o requireOrganization devolve 403 quando o token não o carrega.

Atenção. O isolamento vale para leitura e escrita dentro da sua organização, mas a unicidade do CPF é global na plataforma: duas organizações não cadastram a mesma pessoa. É a limitação mais importante do BB e está detalhada na §15.

O modelo de dados fala português. Estado civil, tipo de vínculo, faixa de renda, UF e tipo de conta bancária são enumerações fechadas. Isso é o que permite filtrar, agregar e mandar para o motor de decisão sem tratamento no meio.

Cada mudança vira evento. Criar, atualizar e excluir publicam customers.person.created, .updated e .deleted no fluxo de eventos da plataforma. Quem quiser reagir — webhook, auditoria, sincronização — se inscreve, e o Customers não precisa saber que existe.


04

Casos de uso reais

negócio

Caso 1 — Uma financeira para de ter três telefones para o mesmo cliente Cenário ilustrativo

Contexto

Financeira de crédito pessoal com esteira própria de originação, motor de decisão e régua de cobrança, cada um construído por um time diferente ao longo de três anos.

A dor

O cliente atualizava o telefone na esteira, mas a cobrança continuava discando o número antigo, porque a cobrança tinha o próprio cadastro alimentado por uma carga noturna que quebrava em silêncio desde março. Ninguém sabia dizer qual das três bases era a boa.

A solução com o BB

As três aplicações passam a chamar GET /customers/api/v1/people/:personId em vez de manter cópia. A atualização é um PATCH num lugar só, e o evento customers.person.updated avisa quem precisa invalidar cache. O personId vira a chave estrangeira que a proposta, o contrato e a cobrança carregam.

flowchart LR
  EST["Esteira de originação"] --> P["Customers<br/>um personId, um telefone"]
  COB["Régua de cobrança"] --> P
  DEC["Motor de decisão"] --> P
  P -->|"PATCH em um lugar só"| EV["customers.person.updated"]
  EV --> INV["quem tem cache invalida"]
  INV -.->|"a carga noturna deixa de existir"| COB
O resultado

Uma pergunta, uma resposta. E a carga noturna deixa de existir — junto com a categoria inteira de incidente que ela produzia.

Caso 2 — Um correspondente não vê o cliente do correspondente vizinho Cenário ilustrativo

Contexto

Plataforma de crédito consignado que atende 40 correspondentes bancários na mesma instância. Cada correspondente é uma Organization no IAM.

A dor

Na versão anterior, a separação entre correspondentes era uma cláusula WHERE que cada desenvolvedor precisava lembrar de escrever. Um endpoint de relatório publicado às pressas esqueceu o filtro, e a lista de clientes de um correspondente apareceu para outro por dois dias.

flowchart LR
  REQ["Requisição com Bearer JWT"] --> AUTH["authMiddleware<br/>verifica a assinatura HS256"]
  AUTH --> PERM["requirePermission CUSTOMERS_PEOPLE_*"]
  PERM --> ORG{"o token carrega organizationId?"}
  ORG -->|"não"| F403["403 — a requisição para aqui,<br/>antes da regra de negócio"]
  ORG -->|"sim"| SVC["PersonService"]
  SVC --> REPO["PersonRepository<br/>organizationId na cláusula where"]
  REPO --> Q1["findById"]
  REPO --> Q2["findMany"]
  Q1 --> N404["pessoa de outro correspondente = 404"]
  Q2 --> N404
A solução com o BB

Todas as cinco rotas exigem requireOrganization e recebem o organizationId do token assinado. O repositório monta a cláusula de organização em findById, findMany e na checagem de existência — não há caminho de leitura em que o filtro seja opcional. Um token sem organização recebe 403 antes de tocar o serviço.

O resultado

O isolamento deixa de depender de quem escreveu a consulta. Adicionar um correspondente é criar uma organização no IAM, não provisionar um ambiente.

Caso 3 — O cadastro alimenta a decisão de crédito sem transformação no meio Cenário ilustrativo

Contexto

Operação de crédito pessoal que usa o Decision Platform para aprovar ou recusar proposta com base em renda, vínculo e idade.

A dor

Os dados chegavam ao motor de decisão como texto livre. "CLT", "clt", "Carteira assinada" e "Empregado" eram a mesma coisa para o analista e quatro coisas diferentes para a regra. A taxa de recusa por dado inconsistente competia com a taxa de recusa por risco.

Entrada da políticaAntes, texto livreDepois, tipo fechado
VínculoCLT, clt, Carteira assinada, Empregado — quatro strings para a mesma coisaemploymentType, enumeração de nove valores
Rendavalor com símbolo de moeda e separador variávelmonthlyIncome decimal de duas casas, mais incomeRange em seis faixas
Idadedata em formato de digitação livrebirthDate em ISO 8601
A solução com o BB

employmentType é uma enumeração fechada com nove valores, incomeRange com seis faixas, monthlyIncome é decimal com duas casas e birthDate é data. O motor de decisão consome o retorno de GET /people/:personId direto, sem camada de normalização.

O resultado

A regra de decisão passa a errar só onde deve errar: no risco. E mudar a política vira mudar a regra, não mudar o parser.

Caso 4 — O direito de eliminação da LGPD vira um processo com um dono Referência de mercado

Contexto

O art. 18 da Lei 13.709/2018 garante ao titular, mediante requisição e sem custo, a confirmação da existência de tratamento (I), o acesso aos dados (II), a correção de dados incompletos, inexatos ou desatualizados (III), a portabilidade (V) e a eliminação dos dados pessoais tratados com o consentimento do titular, exceto nas hipóteses previstas no art. 16 (VI).

A dor

Quando o mesmo dado pessoal vive em cinco serviços, atender ao pedido exige encontrar as cinco cópias, e a prova de que todas foram tratadas não existe. Há ainda o § 6º do art. 18, que obriga informar de imediato os agentes com quem houve uso compartilhado para que repitam o procedimento — impossível de cumprir sem saber quem tem cópia. E o art. 16, I autoriza a conservação para cumprimento de obrigação legal ou regulatória, o que significa que "apagar tudo" nem sempre é a resposta certa. A decisão precisa ser tomada com o dado em um lugar só.

flowchart LR
  T["Titular pede eliminação<br/>art. 18, VI"] --> D{"há obrigação legal ou regulatória<br/>de conservação? art. 16, I"}
  D -->|"sim"| L["DELETE /people/:personId — exclusão lógica<br/>sai das consultas, permanece para rastreabilidade"]
  D -->|"não"| E["Eliminação definitiva<br/>processo com dono, hoje sem endpoint — §15"]
  L --> C["§ 6º do art. 18<br/>comunicar quem recebeu o dado em uso compartilhado"]
  E --> C
  C --> W["consumidores de customers.person.*<br/>hoje sem propagação automática — §15"]
A solução

O dado pessoal do tomador mora em uma tabela, com um identificador. DELETE /people/:personId faz exclusão lógica: o registro some das consultas e permanece para a rastreabilidade que a operação de crédito exige. A eliminação definitiva é uma decisão consciente, executada por processo — e hoje não automatizada, o que está registrado na §15.

O resultado

O pedido do titular deixa de ser uma caça a cópias e passa a ser uma decisão sobre um registro. Sem promessa de conformidade automática: o Customers dá o lugar único e o registro do que mudou; a política de retenção continua sendo sua.


05

Mercado e diferenciais

negócio

Panorama

Panorama. Não existe uma categoria de mercado chamada "cadastro de pessoa física para fintech brasileira", e é por isso que quase todo mundo escreve o seu. Mas o conceito é padrão consolidado: os provedores de core banking e de BaaS tratam o cadastro de pessoa como recurso de primeira classe, separado da conta. A Pismo tem um domínio Customers próprio, com tipos pessoa e empresa e busca por documento. A Celcoin separa a proposta de cadastro de pessoa física da abertura de conta. A Zoop expõe busca de pessoa por CPF.

QuemO que trata como cadastro de pessoa
PismoDomínio Customers próprio, com tipos pessoa e empresa e busca por documento
CelcoinProposta de cadastro de pessoa física separada da abertura de conta
ZoopBusca de pessoa por CPF

E há um vocabulário oficial: a API Dados Cadastrais do Open Finance Brasil padroniza identificação de pessoa natural com cpfNumber, nome civil, nome social, data de nascimento, estado civil, nacionalidade, endereços, telefones e filiação (especificação OAS 3.0 no repositório oficial). O conjunto de campos do Customers é próximo desse vocabulário, e isso não é coincidência: é o mesmo domínio.

As três alternativas reais

As alternativas reais, então, são três, e nenhuma é concorrente direto.

flowchart TD
  N["Preciso de um cadastro de pessoa física<br/>compartilhado por vários serviços"] --> A1["1. Construir do zero"]
  N --> A2["2. Usar o cadastro do core banking ou do BaaS"]
  N --> A3["3. Usar o CRM que já está contratado"]
  A1 --> C1["envelhece em cada serviço novo"]
  A2 --> C2["vem embutido: você contrata o core, não o cadastro"]
  A3 --> C3["cobra por vendedor e não trata CPF como chave transacional"]

A primeira: construir do zero. A primeira é construir do zero: uma tabela, um validador de CPF copiado da internet e a promessa de sempre filtrar por tenant. É a opção mais escolhida e a que envelhece pior, porque cada serviço novo repete o exercício.

A segunda: o cadastro do core banking ou do BaaS. A segunda é usar o cadastro do core banking ou do BaaS. Ele é maduro e aderente ao regulatório — mas vem embutido. Você não contrata o cadastro, contrata o core, e o ciclo de evolução do seu modelo de dados passa a ser o do fornecedor. Pior: usar o cadastro do seu provedor bancário como cadastro geral da sua plataforma amarra o seu dado de cliente ao fornecedor que você talvez queira trocar.

A terceira: o CRM que já está contratado. A terceira é usar um CRM, tipicamente Salesforce ou HubSpot, porque já está contratado. É a comparação que mais aparece em reunião e a menos adequada: CRM foi desenhado para gerir relacionamento comercial, cobra por vendedor e não trata CPF como chave transacional. Cadastro de tomador de crédito não é oportunidade de venda.

Comparativo

CritérioCatalisa CustomersConstruir do zeroCadastro de core banking / BaaSSalesforce Sales Cloud
Base de cobrançaPrecificação em definiçãoEngenharia própriaContrato corporativoPor usuário/mês
CPF validado no cadastroSim, dígito verificadorVocê implementaSimNão nativo
Campos brasileiros de créditoColunas tipadasVocê modelaSimCampos customizados
Isolamento multi-tenantClaim assinado, obrigatórioSua disciplinaPor instância ou por marketplacePor org, com licenciamento
Adoção sem trocar o coreSimSimNãoSim
Cadastro independente do provedor bancárioSimSimNãoSim
Evento de mudança para o resto da stackSim, nativoVocê implementaDepende do fornecedorVia integração
Consulta a bureau, score, KYCNão (ver §15)Você contrataFrequentemente simNão
Pessoa jurídica (CNPJ)Não hoje (ver §15)Você implementaSimSim

Comparativo montado em 2026-08-16 a partir do código deste building block e da documentação pública de Pismo, Celcoin e Zoop. Nenhum dos três publica preço em página aberta para este recorte, e este documento não estima nenhum. Não conseguimos abrir documentação de produto suficiente de Dock, Matera e Sinqia — a última foi absorvida pela Evertec e o site redireciona —, então elas não entram no comparativo.

Nossos diferenciais

  1. O isolamento não é opcional em nenhum ponto do caminho. As cinco rotas exigem requireOrganization, e as consultas do repositório recebem o organizationId como parâmetro obrigatório. Isso é difícil de copiar não pela ideia, mas pela consistência: é o mesmo middleware nos 32 building blocks, então auditar o isolamento da plataforma inteira é auditar um arquivo.
  2. O modelo é brasileiro por dentro, não por tradução. Faixa de renda, tipo de vínculo, UF, CEP, código de banco e tipo de conta são enumerações e colunas. Um cadastro internacional adaptado guarda isso como texto, e texto não filtra nem alimenta motor de decisão sem uma camada de limpeza que ninguém quer manter.
  3. A pessoa é a mesma em todo o catálogo. O personId é o que o Decision Platform, o Pricing Engine, o Billing e o E-Signature carregam. Um cadastro isolado resolve o cadastro; este resolve a identidade compartilhada entre os blocos.

Quando escolher o concorrente

Quatro situações em que a resposta honesta é "não use o Customers".

Se o seu problema éPor que o concorrente ganha
KYC de verdadeSe você precisa de KYC de verdade — checagem do CPF na Receita, consulta a bureau, prova de vida, listas restritivas —, o Customers não faz nada disso e não vai fazer sozinho; contrate um provedor especializado e guarde aqui o resultado.
Cadastro de pessoa jurídicaSe você precisa de cadastro de pessoa jurídica, hoje o BB não aceita CNPJ (§15) e um core banking ou um cadastro próprio resolvem melhor.
Gestão comercialSe o seu problema é gestão comercial — funil, tarefas, histórico de contato, automação de marketing —, o problema é de CRM e o Salesforce ou o HubSpot ganham disparado.
Operação inteira dentro de um core bankingE se a sua operação já roda inteira dentro de um core banking, com originação e ledger no mesmo fornecedor, extrair só o cadastro para fora cria uma sincronização que você não tinha.

O Customers ganha quando o problema é um cadastro de pessoa física, compartilhado por vários serviços seus, com separação rígida entre empresas clientes.


06

Modelo de cobrança e ROI

negócio

Precificação em definição

Precificação em definição. O Customers não tem preço fechado. Ele é um bloco de fundação: quase nunca é contratado sozinho, e sim junto com os blocos que consomem o cadastro — Decision Platform, Pricing Engine, Billing, E-Signature. Não há valor a divulgar, e este documento não estima nenhum.

O que dispara custo

O que dispara custo.

DriverPor quê
Pessoas cadastradasÉ o volume de dado armazenado e o que o cliente entende como unidade
Chamadas de APILeitura de cadastro é a operação mais frequente numa esteira de originação

Comparação de custo

Comparação de custo. Não há comparação de preço a fazer, por dois motivos honestos.

  • Primeiro, a alternativa mais comum é construir do zero, cujo custo é de engenharia e não de licença.
  • Segundo, o cadastro de core banking e de BaaS não é vendido separado do produto bancário — Pismo, Celcoin e Zoop não publicam preço para esse recorte, e comparar o preço de um cadastro com o de uma plataforma bancária inteira produziria um número sem significado.

ROI — a conta de guardanapo

ROI. A conta de guardanapo tem duas linhas.

LinhaO que ela medeTem número defensável?
O que não se escreveSemanas de engenharia por serviço que precisaria do próprio cadastroSim — de uma a duas semanas por serviço
O incidente que não aconteceVazamento de cadastro entre empresas clientes, com dever de comunicação à ANPD e ao titularNão — e por isso não colocamos número

A primeira é o que não se escreve. Um cadastro de pessoa física com validação de CPF, enumerações brasileiras, exclusão lógica, paginação, filtros e isolamento por tenant é uma ou duas semanas de trabalho de um desenvolvedor experiente — por serviço que precisar dele. Com cinco serviços, isso são de cinco a dez semanas, mais a manutenção de cinco cópias que divergem.

A segunda é o incidente que não acontece. Vazamento de cadastro entre empresas clientes é incidente de dado pessoal com dever de comunicação à ANPD e ao titular (Lei 13.709/2018, art. 48). Não colocamos número nessa linha porque não existe número defensável — mas ela é a razão pela qual o isolamento é middleware obrigatório e não convenção de equipe.


07

Arquitetura

As camadas e o caminho da requisição

flowchart TD
  CL["Cliente HTTP<br/>Bearer JWT emitido pelo IAM"] --> APP

  APP["Hono app · basePath /customers · applyCommonMiddleware<br/>bodyLimit 1MB · CORS · security headers · rate limit global"]
  APP --> RT["/api/v1/people → peopleRouter, 5 rotas"]
  APP --> HL["/health → status estático do serviço"]

  RT --> M1["authMiddleware"]
  M1 --> M2["requirePermission CUSTOMERS_PEOPLE_*"]
  M2 --> M3["requireOrganization<br/>403 sem organizationId"]
  M3 --> M4["Zod parse do corpo"]

  M4 --> SVC["services/PersonService<br/>valida CPF: sanitiza máscara e confere o dígito verificador<br/>checa unicidade do CPF<br/>grava → publica evento → devolve"]

  SVC --> REPO["repositories/PersonRepository<br/>Prisma → PostgreSQL · schema customers<br/>organizationId em toda leitura<br/>deletedAt = exclusão lógica"]
  SVC --> EVP["EventPublisher<br/>Redis Stream iam-events"]

  REPO --> PG[("PostgreSQL<br/>customers.people")]
  EVP --> EVT["customers.person.created<br/>customers.person.updated<br/>customers.person.deleted"]
  EVT --> CONS["Consumidores do stream<br/>audit-trail · webhooks-engine"]

O caminho de uma criação, ponta a ponta

O diagrama acima mostra as camadas paradas. Abaixo, as mesmas camadas em movimento — uma criação de pessoa, da autenticação no IAM até o consumo do evento por quem escuta o fluxo.

sequenceDiagram
  participant C as Cliente
  participant I as IAM
  participant CU as Customers
  participant PG as PostgreSQL
  participant RS as Redis stream iam-events
  participant CO as audit-trail e webhooks-engine

  C->>I: POST /iam/api/v1/users/login
  I-->>C: JWT assinado com organizationId e permissões
  C->>CU: POST /customers/api/v1/people com Bearer
  CU->>CU: authMiddleware verifica a assinatura HS256
  CU->>CU: requirePermission CUSTOMERS_PEOPLE_CREATE
  CU->>CU: requireOrganization, 403 se faltar organizationId
  CU->>CU: Zod valida o corpo aninhado em data.attributes
  CU->>CU: sanitizeCpf e isValidCpf, 400 VALIDATION se reprovar
  CU->>PG: taxIdExists, o CPF já está cadastrado?
  PG-->>CU: não existe
  CU->>PG: INSERT em customers.people
  PG-->>CU: pessoa gravada, com id
  CU->>RS: XADD customers.person.created
  RS-->>CU: evento publicado
  CU-->>C: 201 com data.attributes
  RS->>CO: entrega do evento aos consumidores
  CO->>CO: linha do tempo da auditoria e webhook externo

Atenção. A publicação do evento está na mesma cadeia da escrita. Se o XADD falhar, a chamada devolve 500 INTERNAL — e não existe cadastro gravado sem evento publicado.

Decisões não óbvias

Decisões não óbvias. Cinco escolhas deste building block que não são as óbvias, cada uma com o trade-off que ela cobra.

DecisãoO que se ganhaO que se paga
Tabela larga em vez de grafo de tabelasLeitura sem JOIN e escrita atômicaUm endereço e uma conta bancária, sem lista nem histórico
CPF gravado sem máscaraBusca determinística por filter[taxId]A formatação passa a ser da interface
PATCH restrito a name e activeIntegridade do cadastro que já originou créditoEndereço e renda não mudam por API — §15
Evento na mesma cadeia da escritaNenhum cadastro sem eventoRedis vira dependência de escrita
Exclusão sempre lógicaRastreabilidade preservadaO CPF da pessoa excluída continua ocupado — §15

A pessoa é uma tabela larga, não um grafo de tabelas

A pessoa é uma tabela larga, não um grafo de tabelas. Endereço, telefone e conta bancária são colunas de people, não tabelas relacionadas. O trade-off é explícito: a pessoa tem um endereço e uma conta bancária, não uma lista. Ganha-se leitura sem JOIN e escrita atômica; perde-se histórico de endereço. Para operação de crédito brasileira, o endereço atual é o que a esteira usa, e o histórico de endereço quem guarda é o Audit Trail — através do changes de cada evento.

O CPF é guardado sem máscara

O CPF é guardado sem máscara. sanitizeCpf remove pontos e traço antes de gravar. Isso torna a busca por filter[taxId] determinística: você pode consultar com ou sem máscara, porque o filtro sanitiza a entrada do mesmo jeito. A formatação é responsabilidade da interface.

O PATCH altera dois campos, e só

O PATCH altera dois campos, e só. updatePersonSchema aceita name e active. Não é limitação de implementação: alterar CPF, data de nascimento ou nome da mãe de um cadastro que já originou crédito é o tipo de mudança que precisa de processo, não de um PATCH. Se o seu fluxo exige alterar endereço ou renda por API, isso está na §15 como lacuna reconhecida.

A publicação de evento faz parte da transação lógica da escrita

A publicação de evento faz parte da transação lógica da escrita. createPerson, updatePerson e deletePerson encadeiam a publicação com andThen: se o Redis estiver fora, a chamada devolve 500 com INTERNAL. A escolha é deliberada — cadastro gravado sem evento produz um Audit Trail com buraco e um webhook que nunca dispara, e um erro visível é melhor que uma inconsistência silenciosa. O custo é que o Redis vira dependência de escrita, não só de leitura.

A exclusão é lógica em toda parte

A exclusão é lógica em toda parte. deletedAt preenchido, linha preservada. Todas as consultas filtram deletedAt: null. É o que a rastreabilidade de crédito e a auditoria exigem — e é também o motivo de o CPF de uma pessoa excluída continuar ocupando a restrição de unicidade (ver §15).

Monolito vs. standalone

Monolito vs. standalone. O app.ts é montado no monolito em src/app.ts e responde em http://localhost:3000/customers. O main.ts sobe o mesmo app com Bun.serve na porta 3003 quando DEPLOYMENT_MODE=standalone — que é o modo usado em staging e produção. Não há diferença de comportamento entre os modos: o BB não chama nenhum outro building block por HTTP. O que muda é que, em standalone, o applyCommonMiddleware é a única fonte de limite de corpo, CORS, cabeçalhos de segurança e limite de taxa.

ModoEntradaOnde respondeObservação
Monolitosrc/app.ts monta o app.ts do BBhttp://localhost:3000/customersConvive com os demais building blocks no mesmo processo
Standalonemain.ts com Bun.servePorta 3003Modo usado em staging e produção; applyCommonMiddleware é a única camada de proteção comum

08

Conceitos e modelo de dados

Glossário

Glossário

TermoSignifica
PersonUma pessoa física cadastrada por uma organização. É a única entidade do BB. No HTTP o recurso se chama people; no Prisma, Person; na tabela, people.
taxIdO documento fiscal, sempre um par { type, value }. O tipo é BR_CPF ou BR_CNPJ; só BR_CPF funciona hoje (§15). O valor é gravado sem máscara.
OrganizaçãoO tenant. Vem do claim organizationId do JWT do IAM. Toda pessoa pertence a exatamente uma.
Exclusão lógicadeletedAt preenchido. O registro sai de todas as consultas e permanece na tabela. Não há endpoint que o traga de volta.
Faixa de renda (incomeRange)Enumeração fechada de faixas em reais. Convive com monthlyIncome, que é o valor exato — os dois campos são independentes e nenhum deriva do outro.
Ativo (active)Marcador de negócio, não de exclusão. Uma pessoa inativa continua visível e consultável; é o filtro filter[active] que a separa.

Modelo de dados

Modelo de dados — schema customers no PostgreSQL. 1 modelo.

Modelo PrismaTabelaPropósitoCampos-chave
Personcustomers.peopleCadastro de pessoa físicaorganizationId, name, taxIdType, taxIdValue (único global — ver §15), active, deletedAt

Uma entidade, duas relações: a Organization que a possui, no IAM, e as BillingAccount que podem apontar para ela, no Billing.

erDiagram
  Organization ||--o{ Person : possui
  Person ||--o{ BillingAccount : "é cobrada em"

  Person {
    uuid id PK
    uuid organizationId FK
    string name
    string taxIdType
    string taxIdValue UK
    boolean active
    string email
    string phoneCountryCode
    string phoneAreaCode
    string phoneNumber
    datetime birthDate
    string motherName
    string maritalStatus
    string employmentType
    decimal monthlyIncome
    string incomeRange
    string addressStreet
    string addressNumber
    string addressComplement
    string addressNeighborhood
    string addressCity
    string addressState
    string addressZipCode
    string addressCountry
    string bankCode
    string bankBranch
    string bankAccount
    string bankAccountType
    datetime createdAt
    datetime updatedAt
    datetime deletedAt
  }

Leia o diagrama junto com a tabela abaixo: taxIdValue é UK porque a restrição de unicidade é da coluna e vale para a plataforma inteira, não para a organização; monthlyIncome é Decimal(15, 2) no Prisma, com quinze dígitos e duas casas decimais; e deletedAt preenchido é o que tira a linha de todas as consultas sem apagá-la.

Campos por grupo:

GrupoColunasObrigatório
Identificaçãoname, taxIdType, taxIdValue, activeSim
Contatoemail, phoneCountryCode, phoneAreaCode, phoneNumberNão
PessoalbirthDate, motherName, maritalStatus, employmentTypeNão
FinanceiromonthlyIncome (decimal 15,2), incomeRangeNão
EndereçoaddressStreet, addressNumber, addressComplement, addressNeighborhood, addressCity, addressState, addressZipCode, addressCountryNão
Conta bancáriabankCode, bankBranch, bankAccount, bankAccountTypeNão
ControlecreatedAt, updatedAt, deletedAtAutomático

Índices: organizationId, taxIdValue, name, active, deletedAt.

Enumerações

Enumerações

EnumValores
TaxIdTypeBR_CPF · BR_CNPJ (declarado, não aceito — §15)
BrazilianStateAs 27 unidades federativas: AC AL AP AM BA CE DF ES GO MA MT MS MG PA PB PR PE PI RJ RN RS RO RR SC SP SE TO
BrazilianMaritalStatusSINGLE · MARRIED · DIVORCED · WIDOWED · SEPARATED · CIVIL_UNION
BrazilianEmploymentTypeCLT · PUBLIC_SERVANT · SELF_EMPLOYED · BUSINESS_OWNER · RETIRED · PENSIONER · LIBERAL_PROFESSIONAL · UNEMPLOYED · STUDENT
BrazilianIncomeRangeUP_TO_2K · FROM_2K_TO_5K · FROM_5K_TO_10K · FROM_10K_TO_20K · FROM_20K_TO_50K · ABOVE_50K
BrazilianAccountTypeCHECKING · SAVINGS · PAYMENT

Os valores são em inglês e em caixa alta. Uma versão anterior desta documentação listava SOLTEIRO, CASADO, CLT/PJ/AUTONOMO e ATE_1_SALARIO — nenhum desses existe no código. Confira sempre contra src/customers/types/index.ts.

Ciclo de vida de uma pessoa

Ciclo de vida de uma pessoa

stateDiagram-v2
  [*] --> ativa: POST /people
  ativa --> inativa: PATCH com active false
  inativa --> ativa: PATCH com active true
  ativa --> excluida: DELETE /people/:id
  inativa --> excluida: DELETE /people/:id
  excluida --> [*]

  state "ativa · active=true" as ativa
  state "inativa · active=false" as inativa
  state "excluída logicamente · deletedAt preenchido" as excluida

O estado excluída logicamente é terminal: o registro sai de TODAS as consultas, não há rota de restauração e o CPF continua ocupado (§15). Já ativa e inativa são um par reversível — active é marcador de negócio, e a pessoa inativa continua consultável.


09

Referência da API

Prefixo: /customers. Em monolito, a base é http://localhost:3000. Em staging, https://customers.bb.stg.catalisa.app.

Todas as 5 rotas exigem, sem exceção: authMiddleware (Bearer JWT do IAM), requirePermission(...) e o middleware local requireOrganization, que devolve 403 quando o token não carrega organizationId.

Pessoas — /customers/api/v1/people

MétodoRotaDescriçãoPermissão
POST/customers/api/v1/peopleCria pessoa. Responde 201CUSTOMERS_PEOPLE_CREATE
GET/customers/api/v1/peopleLista pessoas, paginado e filtrávelCUSTOMERS_PEOPLE_READ
GET/customers/api/v1/people/:personIdBusca uma pessoaCUSTOMERS_PEOPLE_READ
PATCH/customers/api/v1/people/:personIdAtualiza name e/ou activeCUSTOMERS_PEOPLE_UPDATE
DELETE/customers/api/v1/people/:personIdExclusão lógica. Responde 204 sem corpoCUSTOMERS_PEOPLE_DELETE

Saúde do serviço

MétodoRotaDescrição
GET/customers/healthStatus estático do serviço. Pública, não contabilizada nas 5 rotas

A sonda de saúde não consulta o banco: responde {"status":"ok","service":"customers","version":...} enquanto o processo estiver de pé. Para orquestrador ela serve; para detectar Postgres indisponível, não.


POST /customers/api/v1/people

Atenção ao envelope. O corpo é aninhado em data.attributes — não é um objeto plano.

Request

json
{
  "data": {
    "type": "people",
    "attributes": {
      "name": "Maria Aparecida de Souza",
      "taxId": { "type": "BR_CPF", "value": "529.982.247-25" },
      "email": "maria@exemplo.com.br",
      "phone": { "countryCode": "+55", "areaCode": "11", "number": "998877665" },
      "birthDate": "1988-04-12T00:00:00.000Z",
      "motherName": "Joana de Souza",
      "maritalStatus": "MARRIED",
      "employmentType": "CLT",
      "monthlyIncome": { "amount": 7200.00, "currency": "BRL" },
      "incomeRange": "FROM_5K_TO_10K",
      "address": {
        "street": "Rua das Acácias", "number": "1200", "complement": "Apto 71",
        "neighborhood": "Vila Mariana", "city": "São Paulo",
        "state": "SP", "zipCode": "04101-300", "country": "BR"
      },
      "bankAccount": {
        "bankCode": "341", "branch": "0987",
        "accountNumber": "123456-7", "accountType": "CHECKING"
      },
      "active": true
    }
  }
}
{
  "data": {
    "type": "people",
    "attributes": {
      "name": "Maria Aparecida de Souza",
      "taxId": { "type": "BR_CPF", "value": "529.982.247-25" },
      "email": "maria@exemplo.com.br",
      "phone": { "countryCode": "+55", "areaCode": "11", "number": "998877665" },
      "birthDate": "1988-04-12T00:00:00.000Z",
      "motherName": "Joana de Souza",
      "maritalStatus": "MARRIED",
      "employmentType": "CLT",
      "monthlyIncome": { "amount": 7200.00, "currency": "BRL" },
      "incomeRange": "FROM_5K_TO_10K",
      "address": {
        "street": "Rua das Acácias", "number": "1200", "complement": "Apto 71",
        "neighborhood": "Vila Mariana", "city": "São Paulo",
        "state": "SP", "zipCode": "04101-300", "country": "BR"
      },
      "bankAccount": {
        "bankCode": "341", "branch": "0987",
        "accountNumber": "123456-7", "accountType": "CHECKING"
      },
      "active": true
    }
  }
}
CampoTipoObrigatórioRegra
namestringSim1 a 200 caracteres
taxId.typeBR_CPF | BR_CNPJSimSó BR_CPF passa na validação (§15)
taxId.valuestringSimFormato de CPF, com ou sem máscara. Dígito verificador conferido
emailstringNãoFormato de e-mail
phone.areaCodestringNãoExatamente 2 dígitos
phone.numberstringNão8 ou 9 dígitos
phone.countryCodestringNãoPadrão +55
birthDatestringNãoISO 8601 com data e hora
motherNamestringNãoAté 200 caracteres
maritalStatusenumNãoVer §8
employmentTypeenumNãoVer §8
monthlyIncome.amountnumberNãoPositivo
monthlyIncome.currencystringNãoPadrão BRL
incomeRangeenumNãoVer §8
address.stateenumNãoUF de duas letras
address.zipCodestringNão00000-000 ou 00000000
address.countrystringNãoPadrão BR
bankAccount.bankCodestringNãoExatamente 3 dígitos
bankAccount.branchstringNão4 dígitos, com dígito opcional
bankAccount.accountNumberstringNãoAté 12 dígitos mais dígito verificador
bankAccount.accountTypeCHECKING | SAVINGS | PAYMENTNão—
activebooleanNãoPadrão true

Quando você informa address ou bankAccount, os campos internos marcados como obrigatórios no schema Zod passam a valer — street, number, neighborhood, city, state e zipCode no endereço; os quatro campos da conta bancária. É tudo ou nada por bloco.

Resposta 201

json
{
  "data": {
    "type": "people",
    "id": "6a5c3f21-9f0e-4a1c-9b76-2c1f4b8d0e33",
    "links": { "self": "/api/v1/people/6a5c3f21-9f0e-4a1c-9b76-2c1f4b8d0e33" },
    "attributes": {
      "name": "Maria Aparecida de Souza",
      "taxId": { "type": "BR_CPF", "value": "52998224725" },
      "email": "maria@exemplo.com.br",
      "active": true,
      "createdAt": "2026-08-16T13:22:41.001Z",
      "updatedAt": "2026-08-16T13:22:41.001Z"
    }
  },
  "links": { "self": "/api/v1/people/6a5c3f21-9f0e-4a1c-9b76-2c1f4b8d0e33" }
}
{
  "data": {
    "type": "people",
    "id": "6a5c3f21-9f0e-4a1c-9b76-2c1f4b8d0e33",
    "links": { "self": "/api/v1/people/6a5c3f21-9f0e-4a1c-9b76-2c1f4b8d0e33" },
    "attributes": {
      "name": "Maria Aparecida de Souza",
      "taxId": { "type": "BR_CPF", "value": "52998224725" },
      "email": "maria@exemplo.com.br",
      "active": true,
      "createdAt": "2026-08-16T13:22:41.001Z",
      "updatedAt": "2026-08-16T13:22:41.001Z"
    }
  },
  "links": { "self": "/api/v1/people/6a5c3f21-9f0e-4a1c-9b76-2c1f4b8d0e33" }
}

O taxId.value volta sem máscara, do jeito que foi gravado. O campo links.self é relativo e não inclui o prefixo /customers — some-o você mesmo ao montar URL absoluta.

Erros

StatusCódigoQuando
400—Corpo reprovado no Zod. details traz field, message e code por problema
400VALIDATIONCPF com formato aceito mas dígito verificador inválido, ou sequência repetida
401—Token ausente, inválido ou expirado
403—Falta CUSTOMERS_PEOPLE_CREATE, ou o token não carrega organizationId
409CONFLICTCPF já cadastrado. A unicidade é global na plataforma, não por organização — ver §15

GET /customers/api/v1/people

Parâmetros de consulta

ParâmetroTipoPadrãoDescrição
page[number]inteiro1Página, mínimo 1
page[size]inteiro20Itens por página, máximo 100
filter[active]true | false—Qualquer valor diferente de true é tratado como false
filter[name]texto—Busca por conteúdo, sem diferenciar maiúsculas
filter[taxId]texto—Igualdade exata. A máscara é removida antes da comparação

Ordenação fixa: createdAt decrescente. Não há parâmetro para alterá-la.

Resposta 200

json
{
  "data": [ { "type": "people", "id": "...", "attributes": { "...": "..." } } ],
  "meta": {
    "totalItems": 137, "totalPages": 7, "currentPage": 1,
    "itemsPerPage": 20, "hasNextPage": true, "hasPreviousPage": false
  },
  "links": {
    "self":  "/api/v1/people?page[number]=1&page[size]=20",
    "first": "/api/v1/people?page[number]=1&page[size]=20",
    "next":  "/api/v1/people?page[number]=2&page[size]=20",
    "last":  "/api/v1/people?page[number]=7&page[size]=20"
  }
}
{
  "data": [ { "type": "people", "id": "...", "attributes": { "...": "..." } } ],
  "meta": {
    "totalItems": 137, "totalPages": 7, "currentPage": 1,
    "itemsPerPage": 20, "hasNextPage": true, "hasPreviousPage": false
  },
  "links": {
    "self":  "/api/v1/people?page[number]=1&page[size]=20",
    "first": "/api/v1/people?page[number]=1&page[size]=20",
    "next":  "/api/v1/people?page[number]=2&page[size]=20",
    "last":  "/api/v1/people?page[number]=7&page[size]=20"
  }
}

PATCH /customers/api/v1/people/:personId

Aceita apenas name e active. Qualquer outro campo é ignorado em silêncio pelo schema.

Request — as duas formas funcionam:

json
{ "data": { "attributes": { "active": false } } }
{ "data": { "attributes": { "active": false } } }
json
{ "active": false }
{ "active": false }
CampoTipoRegra
namestring1 a 200 caracteres
activeboolean—

Responde 200 com a pessoa atualizada, ou 404 NOT_FOUND quando o personId não existe na sua organização — inclusive quando ele existe em outra. Um personId que não seja UUID válido devolve 400.


DELETE /customers/api/v1/people/:personId

Exclusão lógica. Responde 204 sem corpo. Chamar de novo no mesmo personId devolve 404, porque o registro já saiu das consultas.


10

Início rápido

Do zero ao primeiro cadastro consultável. Use as credenciais de AMBIENTES.md — nunca credencial de produção.

Os comandos abaixo não foram executados na redação deste documento. As respostas mostradas são as previstas pelo código.

1. Autenticar no IAM

bash
TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@catalisa.app",
    "password": "root123456",
    "organizationId": "b0000000-0000-0000-0000-000000000001"
  }' | jq -r .accessToken)

echo "${TOKEN:0:24}..."
TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@catalisa.app",
    "password": "root123456",
    "organizationId": "b0000000-0000-0000-0000-000000000001"
  }' | jq -r .accessToken)

echo "${TOKEN:0:24}..."

Resposta esperada — o começo do JWT, prova de que a variável foi preenchida:

texto
eyJhbGciOiJIUzI1NiIsInR5...
eyJhbGciOiJIUzI1NiIsInR5...

2. Confirmar que o token carrega a organização

bash
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq '.organizationId'
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq '.organizationId'

Resposta esperada:

json
"b0000000-0000-0000-0000-000000000001"
"b0000000-0000-0000-0000-000000000001"

Sem esse claim, todas as rotas do Customers respondem 403.

3. Criar a primeira pessoa

bash
API=https://customers.bb.stg.catalisa.app/customers/api/v1

PERSON=$(curl -s -X POST $API/people \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "data": {
      "type": "people",
      "attributes": {
        "name": "Maria Aparecida de Souza",
        "taxId": { "type": "BR_CPF", "value": "529.982.247-25" },
        "email": "maria@exemplo.com.br",
        "employmentType": "CLT",
        "incomeRange": "FROM_5K_TO_10K"
      }
    }
  }')

PERSON_ID=$(echo "$PERSON" | jq -r '.data.id')
echo "$PERSON" | jq '.data.attributes.taxId'
API=https://customers.bb.stg.catalisa.app/customers/api/v1

PERSON=$(curl -s -X POST $API/people \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "data": {
      "type": "people",
      "attributes": {
        "name": "Maria Aparecida de Souza",
        "taxId": { "type": "BR_CPF", "value": "529.982.247-25" },
        "email": "maria@exemplo.com.br",
        "employmentType": "CLT",
        "incomeRange": "FROM_5K_TO_10K"
      }
    }
  }')

PERSON_ID=$(echo "$PERSON" | jq -r '.data.id')
echo "$PERSON" | jq '.data.attributes.taxId'

Resposta esperada:

json
{ "type": "BR_CPF", "value": "52998224725" }
{ "type": "BR_CPF", "value": "52998224725" }

A máscara sumiu — é o comportamento esperado.

4. Ler de volta

bash
curl -s "$API/people/$PERSON_ID" -H "Authorization: Bearer $TOKEN" \
  | jq '.data.attributes | {name, taxId, employmentType}'
curl -s "$API/people/$PERSON_ID" -H "Authorization: Bearer $TOKEN" \
  | jq '.data.attributes | {name, taxId, employmentType}'

Resposta esperada — o mesmo cadastro, agora vindo do banco:

json
{
  "name": "Maria Aparecida de Souza",
  "taxId": { "type": "BR_CPF", "value": "52998224725" },
  "employmentType": "CLT"
}
{
  "name": "Maria Aparecida de Souza",
  "taxId": { "type": "BR_CPF", "value": "52998224725" },
  "employmentType": "CLT"
}

5. Confirmar que o isolamento é real

bash
curl -s -o /dev/null -w "%{http_code}\n" "$API/people" \
  -H "Authorization: Bearer token-invalido"
curl -s -o /dev/null -w "%{http_code}\n" "$API/people" \
  -H "Authorization: Bearer token-invalido"

Resposta esperada — só o código de status:

texto
401
401

Retorna 401. Não há caminho em que ausência de token válido resulte em cadastro.


11

Receitas

Encontrar uma pessoa pelo CPF

Objetivo. Descobrir se um CPF já está cadastrado na sua organização antes de abrir uma proposta.

1. Consultar pelo filtro de CPF

bash
curl -s -G "$API/people" \
  --data-urlencode 'filter[taxId]=529.982.247-25' \
  -H "Authorization: Bearer $TOKEN" | jq '.meta.totalItems, .data[0].id'
curl -s -G "$API/people" \
  --data-urlencode 'filter[taxId]=529.982.247-25' \
  -H "Authorization: Bearer $TOKEN" | jq '.meta.totalItems, .data[0].id'

Resposta esperada quando o CPF já existe na sua organização:

json
1
"6a5c3f21-9f0e-4a1c-9b76-2c1f4b8d0e33"
1
"6a5c3f21-9f0e-4a1c-9b76-2c1f4b8d0e33"

2. Interpretar o resultado vazio

json
0
null
0
null

totalItems igual a 0 significa apenas que a sua organização não tem esse CPF. Vá para a receita "Descobrir por que o cadastro voltou 409" antes de concluir que ele está livre.

Armadilhas.

  • O filtro sanitiza a máscara, então 529.982.247-25 e 52998224725 encontram o mesmo registro. Não normalize antes.
  • Colchetes precisam de escape na maioria dos shells. Use -G --data-urlencode, como acima, em vez de montar a query na mão.
  • totalItems: 0 não garante que o CPF esteja livre para cadastro: ele pode estar em uso por outra organização. Nesse caso o POST devolve 409 mesmo com a busca vazia — ver §15.

Cadastrar com endereço e conta bancária completos

Objetivo. Criar o cadastro pronto para crédito consignado, em que a conta de pagamento é obrigatória.

1. Enviar o cadastro completo

bash
curl -s -X POST $API/people \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "data": { "type": "people", "attributes": {
      "name": "João Carlos Pereira",
      "taxId": { "type": "BR_CPF", "value": "11144477735" },
      "birthDate": "1975-09-30T00:00:00.000Z",
      "motherName": "Terezinha Pereira",
      "maritalStatus": "MARRIED",
      "employmentType": "RETIRED",
      "monthlyIncome": { "amount": 3100.50, "currency": "BRL" },
      "incomeRange": "FROM_2K_TO_5K",
      "address": {
        "street": "Avenida Central", "number": "45",
        "neighborhood": "Centro", "city": "Belo Horizonte",
        "state": "MG", "zipCode": "30110-000"
      },
      "bankAccount": {
        "bankCode": "104", "branch": "1234",
        "accountNumber": "98765-4", "accountType": "SAVINGS"
      }
    }}
  }' | jq '.data.id'
curl -s -X POST $API/people \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "data": { "type": "people", "attributes": {
      "name": "João Carlos Pereira",
      "taxId": { "type": "BR_CPF", "value": "11144477735" },
      "birthDate": "1975-09-30T00:00:00.000Z",
      "motherName": "Terezinha Pereira",
      "maritalStatus": "MARRIED",
      "employmentType": "RETIRED",
      "monthlyIncome": { "amount": 3100.50, "currency": "BRL" },
      "incomeRange": "FROM_2K_TO_5K",
      "address": {
        "street": "Avenida Central", "number": "45",
        "neighborhood": "Centro", "city": "Belo Horizonte",
        "state": "MG", "zipCode": "30110-000"
      },
      "bankAccount": {
        "bankCode": "104", "branch": "1234",
        "accountNumber": "98765-4", "accountType": "SAVINGS"
      }
    }}
  }' | jq '.data.id'

Resposta esperada — 201, e o identificador da pessoa criada:

json
"9d3f7c10-2b8a-4e55-8f31-77b0c4a9e102"
"9d3f7c10-2b8a-4e55-8f31-77b0c4a9e102"

2. Conferir que o endereço e a conta foram gravados

bash
curl -s "$API/people/9d3f7c10-2b8a-4e55-8f31-77b0c4a9e102" \
  -H "Authorization: Bearer $TOKEN" | jq '.data.attributes | {address, bankAccount}'
curl -s "$API/people/9d3f7c10-2b8a-4e55-8f31-77b0c4a9e102" \
  -H "Authorization: Bearer $TOKEN" | jq '.data.attributes | {address, bankAccount}'

Resposta esperada:

json
{
  "address": {
    "street": "Avenida Central", "number": "45", "neighborhood": "Centro",
    "city": "Belo Horizonte", "state": "MG", "zipCode": "30110-000", "country": "BR"
  },
  "bankAccount": {
    "bankCode": "104", "branch": "1234",
    "accountNumber": "98765-4", "accountType": "SAVINGS"
  }
}
{
  "address": {
    "street": "Avenida Central", "number": "45", "neighborhood": "Centro",
    "city": "Belo Horizonte", "state": "MG", "zipCode": "30110-000", "country": "BR"
  },
  "bankAccount": {
    "bankCode": "104", "branch": "1234",
    "accountNumber": "98765-4", "accountType": "SAVINGS"
  }
}

Armadilhas.

  • address e bankAccount são blocos tudo-ou-nada. Mandar só city dentro de address reprova no Zod com erro em data.attributes.address.street.
  • birthDate exige data e hora ISO 8601. "1975-09-30" sozinho é reprovado.
  • bankCode tem exatamente 3 dígitos — "104", não "0104" nem 104 como número.
  • monthlyIncome.amount precisa ser positivo. Zero é reprovado.

Inativar sem excluir

Objetivo. Tirar a pessoa dos fluxos ativos preservando consulta e histórico.

1. Marcar como inativa

bash
curl -s -X PATCH "$API/people/$PERSON_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"active": false}' | jq '.data.attributes.active'
curl -s -X PATCH "$API/people/$PERSON_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"active": false}' | jq '.data.attributes.active'

Resposta esperada:

json
false
false

2. Conferir que ela sumiu da lista de ativos

bash
# Confere que ela sumiu da lista de ativos
curl -s -G "$API/people" --data-urlencode 'filter[active]=true' \
  -H "Authorization: Bearer $TOKEN" | jq '.meta.totalItems'
# Confere que ela sumiu da lista de ativos
curl -s -G "$API/people" --data-urlencode 'filter[active]=true' \
  -H "Authorization: Bearer $TOKEN" | jq '.meta.totalItems'

Resposta esperada — o total de ativos, um a menos que antes:

json
136
136

Armadilhas. active=false é diferente de exclusão: a pessoa continua aparecendo em GET /people sem filtro e em GET /people/:id. Se o objetivo é sumir de tudo, use DELETE — que é irreversível pela API.

Descobrir por que o cadastro voltou 409

Objetivo. Separar "CPF duplicado na minha base" de "CPF em uso por outra organização".

1. O CPF está na sua organização?

bash
# 1. O CPF está na sua organização?
curl -s -G "$API/people" --data-urlencode "filter[taxId]=$CPF" \
  -H "Authorization: Bearer $TOKEN" | jq '.meta.totalItems'
# 1. O CPF está na sua organização?
curl -s -G "$API/people" --data-urlencode "filter[taxId]=$CPF" \
  -H "Authorization: Bearer $TOKEN" | jq '.meta.totalItems'

Resposta esperada quando ele não está na sua base:

json
0
0

2. Reenviar a criação e ler o corpo do erro

bash
curl -s -X POST $API/people \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"data\":{\"type\":\"people\",\"attributes\":{\"name\":\"Teste\",\"taxId\":{\"type\":\"BR_CPF\",\"value\":\"$CPF\"}}}}" \
  | jq '{error, message, details}'
curl -s -X POST $API/people \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"data\":{\"type\":\"people\",\"attributes\":{\"name\":\"Teste\",\"taxId\":{\"type\":\"BR_CPF\",\"value\":\"$CPF\"}}}}" \
  | jq '{error, message, details}'

Resposta esperada — 409, com o corpo de erro padrão da plataforma:

json
{
  "error": "CONFLICT",
  "message": "CPF already exists",
  "details": { "field": "taxId.value" }
}
{
  "error": "CONFLICT",
  "message": "CPF already exists",
  "details": { "field": "taxId.value" }
}
flowchart TD
  P["POST /people devolve 409 CONFLICT"] --> B{"filter[taxId] na sua organização<br/>devolve totalItems maior que zero?"}
  B -->|"sim"| M["CPF duplicado na sua própria base<br/>reaproveite o cadastro que já existe"]
  B -->|"não"| O["CPF em uso por outra organização,<br/>ou por uma pessoa excluída logicamente"]
  O --> S["Sem endpoint para resolver — acione o suporte, §15"]

Se o resultado for 0 e o POST continuar devolvendo 409 CONFLICT, o CPF está cadastrado em outra organização da plataforma. A restrição de unicidade de taxIdValue é global, e a exclusão lógica não a libera: um CPF de pessoa excluída continua bloqueando novo cadastro. Não há endpoint para resolver isso — é a limitação registrada na §15, e o caminho é acionar o suporte.

Reagir a mudanças de cadastro em outro serviço

Objetivo. Manter um cache ou um índice de busca sincronizado sem carga noturna.

O Customers publica três eventos no fluxo Redis iam-events:

EventoQuandoCarga principal
customers.person.createdApós gravarpersonId, name, cpf, email, phone
customers.person.updatedApós atualizarpersonId, changes com os campos alterados
customers.person.deletedApós exclusão lógicapersonId

Todos carregam metadata.organizationId e metadata.timestamp.

flowchart LR
  W["POST · PATCH · DELETE<br/>no Customers"] --> S["Redis stream iam-events"]
  S --> AT["audit-trail<br/>linha do tempo do cadastro"]
  S --> WH["webhooks-engine<br/>entrega para fora da plataforma"]
  S --> MEU["seu consumidor<br/>cache ou índice de busca"]
  MEU -.->|"preferível ao acesso direto ao stream"| WH

Armadilhas.

  • O evento de criação carrega CPF, nome, e-mail e telefone em texto puro na carga. Quem consome trata dado pessoal — trate o consumidor com o mesmo cuidado que trataria o banco.
  • Se o Redis estiver indisponível, a escrita falha com 500. Isso é proposital (§7): não existe cadastro gravado sem evento publicado.
  • Para receber os eventos fora da plataforma, use o Webhooks Engine em vez de ler o fluxo direto.

12

Integração com outros building blocks

Building blockComo se relacionaObrigatório
IAMEmite o token que carrega organizationId e as permissões CUSTOMERS_PEOPLE_*. Sem ele, nenhuma rota respondeSim
Audit TrailConsome os eventos customers.person.* e monta a linha do tempo de cada cadastroNão
Webhooks EngineEntrega os mesmos eventos para sistemas fora da plataformaNão
Decision PlatformUsa renda, vínculo e data de nascimento do cadastro como entrada da política de créditoNão
Pricing EnginePrecifica por faixa de risco a partir do perfil da pessoaNão
ProductsDefine o produto de crédito que a pessoa vai contratar. Não há chave estrangeira entre os dois — o vínculo é feito pela sua aplicaçãoNão
BillingBillingAccount tem relação com Person no schema Prisma: a conta de cobrança pode apontar para a pessoa cadastrada aquiNão
File StorageGuarda documento do cadastro (RG, comprovante de renda, selfie) referenciando o personIdNão
E-SignatureUsa nome, CPF e e-mail da pessoa como signatário do contratoNão
flowchart TD
  IAM["IAM<br/>organizationId + permissões"] -->|"Bearer JWT"| CU["CUSTOMERS<br/>quem compra"]

  CU -->|"personId · sua app liga os dois"| PR["Products<br/>o que vende"]
  CU -->|"personId"| FS["File Storage<br/>documentos"]

  PR --> DP["Decision Platform<br/>aprova/recusa"]
  CU -->|"eventos customers.person.*"| AT["Audit Trail<br/>linha do tempo"]
  CU -->|"eventos customers.person.*"| WH["Webhooks Engine<br/>entrega externa"]

  DP --> PE["Pricing Engine<br/>taxa por risco"]
  PE --> ES["E-Signature<br/>contrato"]
  ES --> BI["Billing<br/>cobrança"]

A pessoa é cadastrada UMA vez e atravessa a esteira inteira pelo mesmo id.

Este diagrama é o argumento comercial: o cadastro não é um fim, é a primeira peça de uma esteira que já está montada. Um cadastro isolado obrigaria você a construir cada seta.


13

Configuração e operação

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
DATABASE_URLPostgreSQL. O BB usa o schema customersSim—
REDIS_URLRedis. Usado para publicar eventos e para o contador de limite de taxaSim—
JWT_SECRETSegredo HS256 do IAM, mínimo 44 caracteres. O serviço não sobe sem eleSim—
PORTPorta em modo standaloneNão3000 (a topologia usa 3003)
DEPLOYMENT_MODEmonolith ou standaloneNãomonolith
RATE_LIMIT_ENABLEDLiga o limite global de taxaNãotrue no código; os stacks de staging e produção definem false
RATE_LIMIT_GLOBAL_MAXRequisições por janela, por IPNão10000
RATE_LIMIT_GLOBAL_WINDOWJanela em milissegundosNão60000
CORS_ORIGINSOrigens permitidas, separadas por vírgula. Vazio bloqueia cross-origin em produçãoNãovazio

Não há nenhuma variável específica do Customers. Ele não tem chave de cifragem própria, não fala com provedor externo e não guarda segredo.

Dependências de infraestrutura

DependênciaPara quêSe cair
PostgreSQLSchema customers, tabela peopleToda rota devolve 500 INTERNAL
RedisPublicação de evento e limite de taxaEscrita falha com 500; leitura continua funcionando
IAMEmissão do token. A verificação da assinatura é local, sem redeNão é possível obter token novo; tokens válidos continuam funcionando até expirar

Limites

LimiteValorOnde
Tamanho do corpo1 MBapplyCommonMiddleware
Itens por página100getPaginationParams
Comprimento do nome200 caracteresschema Zod
Nome da mãe200 caracteresschema Zod
Precisão da renda15 dígitos, 2 decimaiscoluna Decimal(15,2)
Limite de taxa global10.000 req/min por IP, quando ligadoconfiguração

Catálogo de erros

StatusCódigoSignificaO que fazer
400—Corpo reprovado no ZodLeia details[]: cada item traz field, message e code
400VALIDATIONCPF com dígito verificador inválidoConfira o CPF na origem; o BB não corrige
400—personId não é UUIDConfira o identificador
401—Token ausente, inválido ou expiradoRenove pelo refresh token do IAM
403—Falta a permissão CUSTOMERS_PEOPLE_*Confira o papel e as permissões da organização no IAM
403—Token sem organizationIdAutentique informando a organização
404NOT_FOUNDPessoa inexistente, excluída, ou de outra organizaçãoOs três casos são indistinguíveis — é proposital
409CONFLICTCPF já cadastrado, em qualquer organizaçãoVer a receita de diagnóstico na §11
429—Limite de taxa por IPAplique recuo exponencial; o cabeçalho Retry-After traz o tempo
500INTERNALFalha de banco, ou Redis indisponível na escritaVerifique Postgres e Redis antes de investigar código

Observabilidade

  • GET /customers/health responde {"status":"ok","service":"customers","version":"..."}. É sonda de processo, não de banco.
  • Todo erro passa pelo errorHandler e sai no log com logger.error({ err }, 'Request error').
  • Cada evento publicado é contabilizado por recordEventPublished, com o tipo do evento e o building block de origem — dá para monitorar volume de cadastro por essa métrica.
  • O EventPublisher injeta contexto de rastreamento (traceId, spanId) na metadata do evento, então a criação de uma pessoa e o registro dela no Audit Trail ficam ligados no mesmo trace.

14

Segurança e compliance

Isolamento entre organizações

Isolamento entre organizações. O organizationId é claim assinado do JWT e chega ao serviço pelo c.get('user'). As cinco rotas aplicam o middleware local requireOrganization, que devolve 403 quando o claim está ausente. As três leituras do repositório — findById, findMany e a busca por identificador — recebem organizationId como parâmetro obrigatório e o incluem na cláusula where. Nenhuma rota lê organizationId do corpo, da query ou de cabeçalho. Uma pessoa de outra organização é indistinguível de uma pessoa inexistente: as duas devolvem 404.

Autenticação e permissões

Autenticação e permissões. Bearer JWT emitido pelo IAM, assinatura HS256 verificada localmente. Cada rota exige uma permissão nominal: CUSTOMERS_PEOPLE_CREATE, _READ, _UPDATE ou _DELETE. A organização é o teto — um papel que conceda CUSTOMERS_PEOPLE_DELETE numa organização que não contratou o Customers não concede nada.

RotaPermissão exigidaMiddleware de tenant
POST /peopleCUSTOMERS_PEOPLE_CREATErequireOrganization
GET /peopleCUSTOMERS_PEOPLE_READrequireOrganization
GET /people/:personIdCUSTOMERS_PEOPLE_READrequireOrganization
PATCH /people/:personIdCUSTOMERS_PEOPLE_UPDATErequireOrganization
DELETE /people/:personIdCUSTOMERS_PEOPLE_DELETErequireOrganization

Que dado pessoal este BB trata

Que dado pessoal este BB trata. Todos os campos de Person são dados pessoais na definição do art. 5º, I da LGPD: nome, CPF, data de nascimento, nome da mãe, e-mail, telefone, endereço, renda, vínculo empregatício e conta bancária. Não há dado pessoal sensível na definição do art. 5º, II — o BB não guarda origem racial, convicção religiosa, opinião política, filiação sindical, dado de saúde, vida sexual, genético ou biométrico.

Criptografia de campo: não há

Criptografia de campo: não há. O CPF, o nome, a renda e a conta bancária são gravados em texto no PostgreSQL. A proteção é de camada — TLS no transporte, credencial de banco em SOPS, rede fechada — e não de campo. Se o seu enquadramento exige cifragem em repouso por coluna, isso não está implementado e está registrado na §15. Cifragem de disco depende da infraestrutura em que o Postgres roda e não é garantida por este building block.

Mascaramento: parcial e só no caminho do log

Mascaramento: parcial e só no caminho do log. O Person retornado pela API traz o CPF completo, sem máscara. Não há endpoint que devolva o CPF parcialmente ocultado. O mascaramento que existe é do Audit Trail, que substitui campos com nome parecido com password, secret, token e apiKey por [REDACTED] — a lista não inclui CPF, e o CPF publicado no evento customers.person.created chega ao log de auditoria em texto. Considere isso ao definir quem tem AUDIT_LOGS_READ.

Retenção e eliminação

Retenção e eliminação. Não há política de retenção automática: o registro fica enquanto a organização existir. DELETE faz exclusão lógica — deletedAt preenchido, linha preservada, registro fora de todas as consultas. Isso se apoia no art. 16, I da LGPD, que autoriza a conservação após o término do tratamento para cumprimento de obrigação legal ou regulatória; para instituição regulada pelo BACEN, os cinco anos de guarda do art. 23 da Resolução CMN 4.893/2021 são exatamente essa obrigação. Mas exclusão lógica não é eliminação: atender a um pedido do titular sob o art. 18, VI exige expurgo definitivo, que hoje é operação manual de banco, sem endpoint e sem registro próprio. E o § 6º do art. 18 exige comunicar de imediato os agentes com quem houve uso compartilhado — o que, aqui, significa notificar quem consome os eventos customers.person.*, e não há automação para isso. Ver §15.

Direito do titularBase legalO que o Customers oferece hoje
Confirmação e acessoart. 18, I e IIGET /people/:personId e GET /people com filter[taxId]
Correçãoart. 18, IIIParcial — o PATCH só altera name e active
Portabilidadeart. 18, VO retorno da API é JSON completo do cadastro
Eliminaçãoart. 18, VIDELETE faz exclusão lógica; expurgo definitivo é manual — §15
Conservação obrigatóriaart. 16, I e art. 23 da Resolução CMN 4.893/2021A exclusão lógica preserva a linha para a guarda regulatória
Comunicar quem recebeu o dado§ 6º do art. 18Sem automação — os consumidores dos eventos precisam ser avisados por processo

Superfície de escrita

Superfície de escrita. O PATCH altera apenas name e active. CPF, data de nascimento, nome da mãe, renda, endereço e conta bancária não podem ser alterados por API depois da criação. Do ponto de vista de integridade isso é uma proteção; do ponto de vista do direito de correção do titular (art. 18, III), é uma lacuna operacional — registrada na §15.

Enquadramento regulatório

Enquadramento regulatório. O Customers é um cadastro, não um sistema regulado. Ele não é meio de pagamento, não movimenta recurso e não está sujeito por si só a exigência do BACEN. Quando ele alimenta uma operação de crédito de instituição autorizada, as obrigações de registro e de guarda recaem sobre a instituição, e a peça da plataforma que atende a rastreabilidade é o Audit Trail — que precisa estar efetivamente ligado, e cujo estado atual está descrito no README dele. Não há certificação PCI-DSS, SOC 2 ou ISO 27001 para este building block, e nenhuma deve ser afirmada em proposta.


15

Limitações conhecidas

LimitaçãoImpactoSituação
CNPJ não é aceitoTaxIdType.BR_CNPJ existe no enum, e o mapa de eventos da plataforma já prevê customers.company. Mas o schema Zod só aceita formato de CPF e o serviço chama isValidCpf para qualquer tipo. Enviar BR_CNPJ resulta em 400. Não há cadastro de pessoa jurídica neste BB, apesar de as permissões CUSTOMERS_COMPANIES_* existirem no IAMEspecificado, não implementado
A unicidade do CPF é global, não por organizaçãotaxIdValue é @unique na tabela e a checagem não filtra por organização. Duas empresas clientes da mesma plataforma não conseguem cadastrar a mesma pessoa: a segunda recebe 409. Em operação com correspondentes ou marketplace de crédito, isso é bloqueio funcional realConhecido — priorizar antes de vender para operação com base de clientes sobreposta
A exclusão lógica não libera o CPFA restrição de unicidade é da coluna, então mesmo com deletedAt preenchido o CPF continua ocupado. Excluir e recadastrar a mesma pessoa devolve 409, sem caminho pela APIConhecido
O PATCH só altera name e activeCorrigir endereço, telefone, e-mail, renda ou conta bancária exige intervenção fora da API. Isso atrapalha o atendimento ao direito de correção do titularLacuna reconhecida
Sem expurgo definitivo para LGPDDELETE é lógico. A eliminação sob o art. 18, VI é operação manual de banco, sem endpoint e sem trilha própria. Também não há propagação automática aos consumidores dos eventos, que o § 6º do art. 18 exige comunicarRoadmap
Sem cifragem de campoCPF, renda e conta bancária ficam em texto no Postgres. Outros building blocks da plataforma (Meta Account, BaaS, E-Signature) cifram credencial com AES-256-GCM; o Customers não cifra dado pessoalNão implementado
Sem histórico de endereço ou de contaO modelo guarda um endereço e uma conta bancária. Trocar sobrescreve. O que sobra é o changes do evento no Audit Trail — que só existe se o consumidor de auditoria estiver ligadoPor design, com efeito colateral
Sem validação de CEP, banco ou agência contra fonte externaOs formatos são conferidos por expressão regular. bankCode: "999" passa mesmo não existindoPor design — o BB não consulta serviço externo
Sem KYC, bureau ou lista restritivaNão consulta Receita Federal, Serasa, Boa Vista, PEP ou sanções. O CPF é validado matematicamente, não confirmado como existentePor design — contrate um provedor e guarde o resultado
Sem importação em massaCadastrar dez mil pessoas são dez mil chamadas POST, sujeitas ao limite de taxaRoadmap
filter[name] sem índice de busca textualA busca usa contains sem diferenciar maiúsculas, o que em PostgreSQL não usa o índice de name. Em base grande, a listagem filtrada por nome degradaConhecido
A sonda de saúde não testa o bancoGET /health responde ok com o Postgres fora do ar. Orquestrador não detecta o problema por elaConhecido
O CPF viaja em texto no eventocustomers.person.created publica cpf, name, email e phone na carga do evento, e o mascaramento do Audit Trail não cobre CPFConhecido

16

Perguntas frequentes

O Customers faz KYC?

Não. Ele valida o CPF matematicamente — dígito verificador e sequência repetida — e nada além disso. Não consulta a Receita Federal, não checa bureau de crédito, não verifica lista de pessoa politicamente exposta e não faz prova de vida. Contrate um provedor de KYC para isso e guarde aqui o cadastro e o resultado. Vender KYC com base neste building block é venda errada e aparece na primeira due diligence.

Posso cadastrar empresa, com CNPJ?

Hoje não. O tipo BR_CNPJ existe na enumeração e as permissões CUSTOMERS_COMPANIES_* existem no IAM, mas não há rota, tabela nem validação de CNPJ. Qualquer tentativa devolve 400. Está na §15 como especificado e não implementado.

Duas empresas clientes minhas podem ter o mesmo cliente?

Não, e essa é a limitação mais importante deste building block. O CPF é único na plataforma inteira, não por organização. Se você opera correspondentes, marketplace de crédito ou qualquer modelo em que a mesma pessoa apareça em duas carteiras, o segundo cadastro recebe 409. Leia a §15 antes de fechar contrato nesse cenário.

Por que não consigo atualizar o endereço pela API?

Porque o PATCH aceita apenas name e active. A decisão original protege a integridade de um cadastro que já originou crédito — alterar CPF ou data de nascimento por API é o tipo de mudança que costuma ser um erro. O efeito colateral é que endereço e renda também ficaram de fora, o que é uma lacuna e está registrada na §15.

O CPF é criptografado no banco?

Não. Ele é gravado em texto, sem máscara, na coluna tax_id_value. A proteção é de camada: TLS no transporte, rede fechada, credencial de banco em SOPS e permissão por rota. Se o seu enquadramento exige cifragem por coluna, isso não existe hoje.

Como atendo a um pedido de eliminação de dados da LGPD?

DELETE /people/:personId faz exclusão lógica — o registro sai de todas as consultas e permanece na tabela. Para muitos casos isso é o correto, porque o art. 16 da LGPD admite a conservação para cumprimento de obrigação legal, e operação de crédito costuma se enquadrar. Para eliminação definitiva sob o art. 18, VI, hoje é operação manual de banco, sem endpoint. Trate como processo com dono, não como chamada de API.

O que acontece se o Redis cair?

A leitura continua funcionando. A escrita falha com 500, porque a publicação do evento faz parte da cadeia de criação, atualização e exclusão. É deliberado: um cadastro gravado sem evento produz um Audit Trail com buraco e um webhook que nunca dispara. Se a sua chamada de escrita voltou 500 sem erro de validação, cheque o Redis antes de procurar bug.

Qual a diferença entre active: false e DELETE?

active: false é estado de negócio: a pessoa continua consultável e continua aparecendo na listagem sem filtro. DELETE é exclusão lógica: some de tudo, não tem rota de volta, e o CPF continua ocupado. Use o primeiro para pausar relacionamento e o segundo para encerrar cadastro.

Como sei que outra empresa cliente não vê meus cadastros?

Porque não existe caminho no código em que a organização venha da requisição. Ela vem do claim assinado do token, o middleware requireOrganization recusa token sem ela, e as três consultas do repositório recebem organizationId como parâmetro obrigatório. O teste prático é o da §10: uma pessoa de outra organização devolve 404, igualzinho a uma pessoa inexistente.

Por que o cadastro só tem um endereço?

Porque a esteira de crédito usa o endereço atual, e modelar uma lista custaria uma tabela, um JOIN em toda leitura e uma regra de "qual é o principal". A troca de endereço fica registrada no changes do evento customers.person.updated, que o Audit Trail persiste — desde que o consumidor de auditoria esteja ligado. Confira isso no README dele antes de contar com o histórico.


Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md