# Identidade do cliente entre marcas

Pergunta levantada em 17/08/2026: como um cliente pode ser compartilhado entre
as 32 unidades da Barbearia VIP, mas não entre marcas concorrentes do SaaS.

---

## A ambiguidade que resolvi para poder responder

A descrição original diz que o cliente aceita compartilhar dados com a unidade
"e essa mesma não consegue enxergar o cliente, seus dados, mas não o seu
histórico compartilhado".

Li como: **a unidade enxerga o cliente e os dados dele, mas não o histórico
dele em outras marcas.** É o que o exemplo confirma — Seu Elias vê as visitas
que João fez em Seu Elias, e nada do que ele fez na Barbearia VIP.

Se a intenção for outra, o desenho abaixo muda.

---

## Não são duas estruturas. É uma, com N diferente

A pergunta partia de "duas linhas no mesmo SaaS, com estruturas diferentes".
Olhando o que cada caso exige, as duas regras são **a mesma regra**:

| | Barbearia VIP | Seu Elias |
|---|---|---|
| Inquilino | 1 | 1 |
| Unidades | 32 | 1 |
| Cliente compartilhado entre as unidades | sim | sim (trivial: só há uma) |
| Histórico visível para outra marca | não | não |

**Cliente pequeno é o mesmo desenho com uma unidade só.** Não existe segunda
estrutura a construir, e isso vale mais que qualquer otimização: uma base de
código, um schema, um caminho de teste.

E o mais importante: **o RLS que a F1.3 já implementou entrega o isolamento
entre marcas de graça.** `vendas`, `agendas` e `clientes` têm `inquilino_id`, e
a política filtra tudo. Seu Elias não enxerga o histórico de João na Barbearia
VIP porque aquelas linhas têm outro `inquilino_id` — sem código, sem filtro
esquecido, sem exceção.

O compartilhamento **dentro** da rede também já funciona hoje: existe **uma
linha em `clientes` por pessoa por rede**, não uma por unidade. Curitiba e
Itajaí veem o mesmo registro do João porque é literalmente o mesmo registro.

---

## O que de fato falta: reconhecer a pessoa entre marcas

O único requisito novo é saber que o João da Barbearia VIP e o João do Seu Elias
são a mesma pessoa — para ele ter **um login só** no aplicativo e para o
consentimento ser dele, não da barbearia.

Isso pede um nível acima do inquilino:

```mermaid
erDiagram
    PESSOAS ||--o{ CONSENTIMENTOS : "concede"
    PESSOAS ||--o{ CLIENTES : "é, em cada marca"
    INQUILINOS ||--o{ CLIENTES : "possui"
    INQUILINOS ||--o{ CONSENTIMENTOS : "recebe"
    CLIENTES ||--o{ VENDAS : "gera"
    CLIENTES ||--o{ AGENDAS : "marca"

    PESSOAS {
        int id PK
        string chave "CPF ou telefone, com hash"
        string senha_app
    }
    CONSENTIMENTOS {
        int pessoa_id FK
        int inquilino_id FK
        datetime concedido_em
        datetime revogado_em
    }
    CLIENTES {
        int inquilino_id FK "RLS"
        int pessoa_id FK
        int unidade
        string nome
        string preferencias
    }
```

Três níveis, com responsabilidades separadas:

**`pessoas`** — identidade da plataforma. Mínima de propósito: o identificador
e o necessário para autenticar no aplicativo. **Sem `inquilino_id`, e nenhum
inquilino lê esta tabela.**

**`clientes`** — a visão que **cada marca** tem daquela pessoa. É a tabela que
já existe, mais uma coluna `pessoa_id`. Tem `inquilino_id` e RLS. Nome,
telefone, preferências, pontos, observações: cada marca tem os seus, e não vê os
da outra. Se João mudou de telefone e só avisou o Seu Elias, a Barbearia VIP
continua com o antigo — o que é correto.

**Histórico** — `vendas` e `agendas`, com `inquilino_id`. Isolamento automático.

---

## O risco que esse desenho cria, e que precisa ser fechado

`pessoas` é global. Se um inquilino conseguir consultá-la, ele descobre **se um
CPF existe na plataforma** — ou seja, se aquele cliente é cliente de algum
concorrente. Isso é vazamento comercial, e por enumeração é fácil de explorar.

Três regras que precisam valer desde o primeiro dia:

1. **Nenhum inquilino tem `SELECT` em `pessoas`.** O papel `bvip_app` não
   recebe permissão nessa tabela.
2. A ligação pessoa↔cliente acontece num **caminho privilegiado**, um serviço
   da plataforma, nunca numa consulta da aplicação do inquilino.
3. A resposta desse serviço é sempre a mesma para "existe" e "não existe" — quem
   pergunta descobre apenas se **a própria marca** já conhece a pessoa.

O identificador em `pessoas` deve ser guardado com resumo criptográfico, não em
claro: mesmo com acesso ao banco, não dá para listar CPFs.

---

## Consentimento

O "cliente aceita compartilhar" vira registro, não configuração: pessoa,
inquilino, quando concedeu, quando revogou. A LGPD exige que o consentimento
seja **demonstrável**, então guardar só um booleano "aceitou" não basta — precisa
de data e de trilha.

Revogação não apaga histórico de venda, que tem base legal própria (obrigação
fiscal). Ela encerra o compartilhamento e o uso para marketing.

---

## O que o **cliente** vê é outra pergunta

Vale separar, porque é onde é fácil errar contra a LGPD:

- **A marca** vê o que aconteceu na marca dela. É a regra descrita.
- **A pessoa** tem direito aos próprios dados, **em todas as marcas**. No
  aplicativo, João pode ver "minhas visitas na Barbearia VIP" e "minhas visitas
  no Seu Elias".

Restringir o que João vê do próprio histórico não protegeria ninguém — e
contraria o direito de acesso do titular.

---

## A regra das duas visitas é mudança de comportamento

A regra descrita — o cliente só migra de unidade depois de **duas visitas
seguidas** na mesma, e até lá permanece na última unidade fidelizada — **não é o
que o sistema faz hoje.**

Hoje, `VendaPagamentoService::finalizar()` executa a cada venda finalizada:

```php
$venda->getCliente()->setUltimaVisitaUnidade($venda->getCaixa()->getUnidade());
```

Sem condição. **A unidade muda na primeira visita**, não na segunda. E
`fidelizado_unidade` é outra coisa: um booleano de mão única, que liga ao atingir
o número de visitas configurado e nunca desliga (caracterizado em
`FidelizacaoTest`).

Ou seja, não existe hoje o conceito de "unidade à qual o cliente está
fidelizado" — existe "última unidade visitada", que é diferente.

**Isso é trabalho de produto, da Fase 3, não da migração.** Exige coluna nova
(`unidade_fidelizada`), contador de visitas consecutivas, e decisão sobre o que
acontece quando o cliente alterna entre três unidades. Mexer nisso durante a
Fase 1 quebraria os testes de caracterização — que é justamente o que eles
existem para fazer.

---

## O que muda na F1.3

Pouco, e é a boa notícia:

| | |
|---|---|
| `pessoas` | Tabela nova, sem `inquilino_id`, sem permissão para `bvip_app` |
| `consentimentos` | Tabela nova, pessoa × inquilino |
| `clientes.pessoa_id` | Coluna nova, nula por enquanto |
| Todo o resto | Já está pronto — o RLS entrega o isolamento entre marcas |

Preencher `pessoa_id` para os 242 mil clientes atuais é trivial: todos são do
inquilino 1, então cada um vira uma pessoa. A deduplicação entre marcas só passa
a existir quando houver a segunda marca.

## A chave de identidade: telefone, com uma ressalva importante

Informado em 17/08: **mais de 95% dos cadastros não têm CPF**, e o telefone é a
chave única de fato usada hoje.

Isso decide a questão prática — CPF não serve, não há alternativa. Mas telefone
como identidade tem um problema que só aparece em escala e depois de anos, que é
exatamente onde vocês vão estar:

**Número de telefone é reciclado.** A operadora devolve ao mercado um número
desligado, e ele passa a ser de outra pessoa. A base tem histórico desde
**junho de 2015** — onze anos. Numa base de 242 mil clientes com essa idade, é
certo que há números que trocaram de dono.

Se `pessoas.chave = telefone`, quando Maria receber o número que era do João ela
**herda a identidade dele**: histórico, pontos, consentimento — e, no desenho
entre marcas, em todas as marcas de uma vez. É o pior defeito possível, porque é
silencioso e cresce com o tempo.

Há ainda dois casos menores, mas frequentes numa barbearia: a pessoa troca de
número e vira "cliente novo", perdendo o histórico; e pai e filho compartilham o
mesmo telefone.

### A saída: telefone não é a identidade, é um contato verificado

```
pessoas                     id opaco, estável, nunca derivado de PII
   └── pessoas_contatos      telefone normalizado, verificado_em, ativo_ate
          └── clientes       a visão de cada marca (telefone em claro, para mensagens)
```

O `pessoas.id` é interno e não muda nunca. O telefone vira um **vínculo com
validade**: quando o número troca de dono, o vínculo antigo é encerrado
(`ativo_ate`) e um novo é criado para outra pessoa. O histórico do João continua
com o João.

Isso também resolve os dois casos menores: trocar de número é acrescentar um
contato à mesma pessoa; telefone compartilhado é um contato ligado a duas
pessoas, e a desambiguação fica no nome.

### A regra que elimina quase todo o risco

**A barbearia digitar um telefone não cria vínculo entre marcas. Só a própria
pessoa cria.**

| Ação | O que acontece |
|---|---|
| Recepção cadastra cliente digitando o telefone | Cria linha em `clientes`, com `pessoa_id` **nulo** |
| A pessoa entra no aplicativo e confirma o número por código | Cria ou casa a `pessoa`, vincula os `clientes`, registra consentimento |

Isso vale por três motivos ao mesmo tempo:

1. **Segurança** — casar identidade a partir de um número digitado por um
   terceiro é como o dado de uma pessoa acaba na conta de outra.
2. **LGPD** — o consentimento passa a ser dado pelo titular, não inferido.
3. **Custo** — não é preciso construir casamento entre marcas para lançar. Ele
   nasce junto com o login do aplicativo, que já existe.

### Onde o telefone fica guardado

| Onde | Forma | Para quê |
|---|---|---|
| `clientes.telefone` | **em claro**, com escopo de inquilino | WhatsApp, SMS, ligação — cada marca fala com o cliente dela |
| `pessoas_contatos.telefone_hmac` | **só o resumo** | casar identidade entre marcas |

O resumo precisa ser **HMAC com uma chave secreta guardada fora do banco**, não
um SHA-256 simples. O motivo é aritmético: celular brasileiro tem cerca de um
bilhão de combinações válidas, então uma tabela de resumos simples é gerada em
minutos e não protege nada. Com HMAC e a chave fora do banco, um vazamento da
tabela não vira lista de telefones.

### O padrão: DDD + 9 dígitos

Definido em 17/08. Guardar em **E.164** com o nono dígito aplicado:
`+55` + DDD + 9 dígitos, ou seja, `+5511987654321`. O resumo é calculado sobre a
forma normalizada — senão o mesmo número com máscaras diferentes vira duas
pessoas.

Isso importa porque a base tem cadastro desde **junho de 2015**, e o nono dígito
só terminou de ser implantado no Brasil em 2016. Há celular de 8 dígitos guardado
ali dentro, e hoje ainda existem duas colunas (`telefone` e
`telefone_sem_mascara`) com formatos diferentes.

**A armadilha:** dez dígitos podem ser celular antigo **ou telefone fixo**, e
prefixar 9 num fixo estraga o número. A distinção está no primeiro dígito do
assinante:

| Começa com | É | Nono dígito |
|---|---|---|
| 6, 7, 8, 9 | celular no formato antigo | **aplica** |
| 2, 3, 4, 5 | fixo | **não aplica** |

Aplicar o 9 a tudo que tem 8 dígitos é o erro clássico desta migração, e é
silencioso: o número continua parecendo válido e simplesmente não toca em
ninguém.

### Fixo não é identidade

Consequência direta do padrão ser DDD + 9 dígitos: **fixo tem DDD + 8 e nunca
vira DDD + 9.**

Não é limitação técnica, é da realidade — o vínculo entre pessoa e telefone só
se cria quando a própria pessoa confirma um código, e código chega por SMS ou
WhatsApp. Fixo não recebe nenhum dos dois.

Então o fixo continua guardado em `clientes` como **contato** (a recepção liga
para ele), mas não entra como **identidade**.

Duas funções, com essa separação explícita, em `postgres/funcoes-telefone.sql`:

| Função | Devolve |
|---|---|
| `normalizar_telefone()` | E.164 para celular **e** fixo — é o contato |
| `normalizar_celular()` | E.164 só para celular; `NULL` para fixo — é a identidade |

Ambas têm **26 casos de teste** que rodam com código de saída, cobrindo celular
atual, celular antigo de cada faixa, fixo de cada faixa, código do país com e sem
`+`, máscaras, e um fixo cujo assinante começa com 55 — que sem cuidado perderia
dois dígitos ao remover o código do país.

O que não for reconhecível devolve `NULL`, em vez de virar palpite.

### O que medir na réplica antes de migrar

```sql
-- Quantos ainda estão no formato antigo, e quantos são fixo
SELECT
    SUM(LENGTH(telefone_sem_mascara) = 10
        AND SUBSTRING(telefone_sem_mascara, 3, 1) IN ('6','7','8','9')) AS celular_8_digitos,
    SUM(LENGTH(telefone_sem_mascara) = 10
        AND SUBSTRING(telefone_sem_mascara, 3, 1) IN ('2','3','4','5')) AS fixo,
    SUM(LENGTH(telefone_sem_mascara) = 11)                              AS celular_9_digitos,
    SUM(telefone_sem_mascara IS NULL OR telefone_sem_mascara = '')      AS sem_telefone
  FROM clientes;
```

O número de **fixos** é o que interessa mais: são os clientes que não terão
identidade de plataforma até informarem um celular.

### O que medir na réplica de produção

A base anonimizada não responde nada disto — ela deriva o telefone do `id`.

```sql
SELECT COUNT(*)                                             AS total,
       SUM(telefone IS NULL OR telefone = '')               AS sem_telefone,
       COUNT(DISTINCT telefone_sem_mascara)                 AS numeros_distintos
  FROM clientes;

-- Quantas pessoas compartilham um mesmo número: dimensiona o caso do
-- telefone de família e o esforço de desambiguação.
SELECT quantos_clientes, COUNT(*) AS quantos_numeros
  FROM (SELECT telefone_sem_mascara, COUNT(*) AS quantos_clientes
          FROM clientes
         WHERE telefone_sem_mascara IS NOT NULL AND telefone_sem_mascara <> ''
         GROUP BY telefone_sem_mascara) x
 GROUP BY quantos_clientes
 ORDER BY quantos_clientes DESC
 LIMIT 10;
```

O segundo resultado é o que interessa: se houver muitos números com 3 ou mais
clientes, o telefone compartilhado é comum na operação e a desambiguação precisa
entrar no fluxo do aplicativo desde o começo.
