Skip to content

feat(partners): fornecedor pessoa física (CPF) — SupplierDocument, PF sem Razão Social/Nome Fantasia - #1025

Merged
GabrielAderaldo merged 4 commits into
devfrom
feat/fornecedor-pessoa-fisica
Oct 1, 2026
Merged

GabrielAderaldo merged 4 commits into
devfrom
feat/fornecedor-pessoa-fisica

Conversation

@GabrielAderaldo

Copy link
Copy Markdown
Contributor

Fecha #1022.

O que muda

O cadastro de Fornecedor passa a aceitar pessoa física (CPF), paga por RPA. Na PF, razão social e nome fantasia não existem; na PJ seguem obrigatórios. O cálculo do RPA já existia no Contas a Pagar; esta entrega é só o cadastro.

  • Domínio: SupplierDocument (CPF | CNPJ) no módulo partners, e identidade PF × PJ como union discriminada (individual × company). O Cnpj do kernel não muda: Financiador e Acordo continuam recusando CPF.
  • Banco: migration 0020 renomeia cnpj → document (continua varchar(14)), cria o índice par_suppliers_document_idx e torna corporate_name/fantasy_name nullable. Dois CHECKs amarram o NULL ao tamanho do documento. Sem backfill.
  • HTTP v1 (aditivo):
    • entrada: document (11 ou 14 caracteres, sem máscara); cnpj aceito como alias deprecated por um ciclo;
    • resposta: document e personType: 'PF' | 'PJ', com o alias cnpj mantido; corporateName/fantasyName vêm null na PF;
    • erros: invalid-supplier-document, *-document-duplicate, supplier-corporate-name-not-allowed-for-pf e supplier-fantasy-name-not-allowed-for-pf.
  • Edição: trocar PF ↔ PJ é trocar o documento, então exige supplier:edit-sensitive.
  • Busca, CSV, OCR e ETL:
    • a busca encontra por CPF, com ou sem máscara;
    • o CSV troca o cabeçalho para "CPF/CNPJ";
    • a leitura de nota resolve CPF (findSupplierIdByDocument);
    • a ETL aceita CPF na coluna legada.
  • CNAB/VAN: sem mudança de código. Entram 7 testes de favorecido CPF (transferência, J-52 e Pix).
  • ADR-0070: supersede parcialmente o ADR-0031.

Um commit separado conserta o harness: os hooks não rodavam em clone com espaço no caminho.

Como foi verificado

  • Gate: typecheck, format:check, lint e test verdes, com 11902 testes e 0 falhas.
  • MySQL 8.4 real (imagem pinada do compose.yaml): suíte de integração do partners com 50/50, passando em banco limpo, na repetição sem recriar e em ordem invertida.
    • Verificado no information_schema: document varchar(14) utf8mb4_bin, o índice novo e os dois CHECKs.
    • SQL direto: PF com razão social e PJ sem nome fantasia são recusadas com 3819; PF válida é aceita.
  • Revisões: /code-review em nível high, com 7 achados corrigidos e 3 mantidos (ver o comentário de decisões), e /security-review sem achados.

Pendências fora deste PR

… sem razão social

Fecha #1022. O cadastro de Fornecedor aceita pessoa física (CPF), pagos por RPA.

- Domínio: `SupplierDocument` (CPF | CNPJ, no módulo, não no kernel) e identidade PF × PJ
  como union discriminada — na PF razão social e nome fantasia não existem no tipo. O tipo
  de pessoa é derivado do documento, nunca gravado. PF com os campos preenchidos é recusada
  (`supplier-*-not-allowed-for-pf`); branco conta como ausente.
- Persistência: migration 0020 (`cnpj` → `document`, índice `par_suppliers_document_idx`,
  `corporate_name`/`fantasy_name` nullable com CHECK amarrado ao tamanho do documento).
  Ausência é NULL só na borda; sem valor sentinela (ADR-0070).
- HTTP v1 aditivo: `document` + `personType`; `cnpj` segue como alias deprecated por um
  ciclo, na entrada e na resposta. Códigos `invalid-supplier-document` e `*-document-duplicate`.
- Busca por CPF, CSV "CPF/CNPJ", leitura de nota resolve CPF (`findSupplierIdByDocument`),
  ETL aceita CPF na coluna legada.
- Testes de favorecido CPF no CNAB (transferência, J-52, Pix); sem mudança de código lá.
- ADR-0070 supersede parcialmente o ADR-0031.

Assisted-by: Claude-Code:claude-opus-5-5
Os 20 comandos de hook do `.claude/settings.json` eram `${CLAUDE_PROJECT_DIR}/...` sem
aspas: num clone em "Área de trabalho" o `/bin/sh` partia o caminho no espaço e todo hook
falhava como "non-blocking" — Prettier pós-edição, bloqueios de Bash e o gate do Stop
deixavam de rodar em silêncio. Agora `"\"${CLAUDE_PROJECT_DIR}\"/..."`, a forma da doc.

Com os hooks de volta, apareceram mais três defeitos da mesma família:

- `pre-commit-typecheck.sh` guardava `pnpm --dir=${CORE_API_DIR}` numa string e a expandia
  sem aspas: os quatro gates do pre-commit morriam com `ENOENT ... lstat '/home/.../Área'`.
  Agora a chamada é direta, com `--dir` entre aspas.
- `stop-quality-gate.sh` reportava `pnpm: comando não encontrado` como QUATRO vermelhos.
  Shell de hook não carrega o nvm; ausência do pnpm agora é diagnóstico próprio ("gate
  não rodou"), que continua bloqueando, mas com a causa verdadeira.
- O fixture do `gate-blocker` montava `typecheck: <caminho do tsc> --noEmit` sem aspas no
  package.json temporário; o cenário GREEN falhava por ambiente, não por gate.

Assisted-by: Claude-Code:claude-opus-5-5
- `rehydrate` aplica só a regra do CHECK (PF ⟺ nomes NULL). Exigir nome não-branco na
  LEITURA derrubava o `list()` inteiro por uma PJ gravada com razão social `''` por fora
  do domínio — o CHECK aceita, e essa linha sempre reidratou.
- Borda: documento só alfanumérico. Só o tamanho deixava um CPF mascarado (14 caracteres)
  passar como CNPJ sem máscara, enquanto o CNPJ mascarado caía em 400.
- ETL: CPF ambíguo (os 11 dígitos com zeros à esquerda também formam CNPJ válido — CNPJ
  legado que perdeu os zeros) vai para quarentena, não para PF; e documento inválido volta
  a juntar o erro de razão social na mesma quarentena.
- `companyNamesOf` no domínio substitui o mesmo `switch` repetido em DTO, CSV, mapper e
  busca; tamanhos de CPF/CNPJ têm fonte única em `SupplierDocument`.
- Reverte a edição da linha de status do ADR-0031: ADR aceito não se edita; a supersessão
  fica registrada no ADR-0070. ADR-0070 registra o risco aceito do OCR resolver CPF.
- Diagnóstico de duplicatas e textos de teste apontam `par_suppliers.document`.

Assisted-by: Claude-Code:claude-opus-5-5
Copilot AI balanced review requested due to automatic review settings October 1, 2026 19:07
@GabrielAderaldo GabrielAderaldo added the ai-assisted PR de sessão de IA — commits exigem trailer Assisted-by (ADR-0054/#549) label Oct 1, 2026
@GabrielAderaldo

Copy link
Copy Markdown
Contributor Author

Decisões tomadas e o motivo de cada uma

Registro do que foi decidido nesta entrega, inclusive o que se alinhou com a P.O. A versão normativa está no ADR-0070.

1. Ausência de razão social/nome fantasia: union no domínio e NULL só nas bordas, sem valor sentinela

Decidido: na PF os dois campos não existem no tipo, porque a identidade é uma union discriminada (individual × company). Na coluna MySQL e no JSON, essa ausência é NULL/null, amarrada ao documento por CHECK.

Alternativa considerada e recusada: usar um valor sentinela (-1, 00 ou outra marca) no lugar de null. A motivação era evitar que um null "corrompido" virasse undefined ou NaN no caminho. Os motivos da recusa:

  • Em JS, null é um valor primitivo fixo, não um ponteiro para memória não inicializada. O NULL do MySQL é estado definido pelo padrão SQL, e o mysql2 o entrega como null. No JSON, null vai e volta exato; quem some na serialização é undefined.
  • Corrupção de memória ou de disco atinge qualquer valor, então um -1 corrompido também vira outra coisa.
  • Os campos são texto: o sentinela seria a string "-1". Ela passaria pelo CHECK e vazaria para busca, CSV e tela se algum consumidor esquecesse de filtrar. Seria um magic value com outro nome.
  • O objetivo real ("o código nunca fica em dúvida se o valor existe") é entregue melhor pela union: dentro do domínio não há nem null nem sentinela, e o compilador obriga cada consumidor a tratar a PF.

Para a P.O, o resultado de produto não muda: PF sem razão social e sem nome fantasia, com os campos desativados na tela.

2. O tipo de pessoa é derivado, não gravado

personType sai do documento (11 posições → PF; letra ou 14 → PJ). Um campo gravado permitiria o estado "PF com CNPJ". Derivado, esse estado não é representável.

3. SupplierDocument no módulo, não no kernel

Só o Fornecedor aceita CPF ou CNPJ. Financiador e Acordo seguem com o Cnpj do kernel e recusam CPF.

4. PF com razão social preenchida é recusada (422), não ignorada

Descartar em silêncio algo que o usuário digitou esconderia um erro de cadastro, como um CPF digitado no lugar do CNPJ. Branco ("") conta como ausente, porque a tela desativa os campos e pode enviá-los vazios.

5. Trocar PF ↔ PJ exige supplier:edit-sensitive

Trocar o tipo de pessoa necessariamente troca o documento, que já era o campo sensível. Não foi preciso regra nova.

6. Contrato HTTP v1 aditivo, com alias cnpj por um ciclo

/api/v1 é contrato congelado (ADR-0033). Os campos novos (document, personType) se somam aos antigos, e o cnpj continua aceito e devolvido até o front migrar. Assim o backend pode subir antes do front sem quebrar a tela atual.

⚠️ Mudança de status: PJ sem corporateName passa de 400 (Zod) para 422 (domínio). O shape não pode mais exigir o campo, porque quem decide é o documento.

7. Achados da revisão mantidos de propósito

  • OCR resolvendo CPF: a issue pede isso, porque o emitente de um RPA é PF. O risco aceito é que um CPF de outra parte da nota (tomador, representante) que coincida com um fornecedor PF seja pré-selecionado. É revisável na tela, não é lançamento. Se aparecer na prática, a correção é restringir o resolver ao campo do emitente.
  • Cabeçalho do CSV "CPF/CNPJ" sem alias: foi pedido na issue. Quem lê o export pela coluna cnpj precisa se ajustar.
  • Nome resolveSupplierByCnpj no financial: o port do partners foi renomeado (findSupplierIdByDocument), mas os nomes internos do financial ficam para um ajuste próprio daquele módulo.

8. ETL: CPF ambíguo vai para quarentena

Pelos pesos do módulo 11, um documento de 11 dígitos iniciado em 00 pode ser ao mesmo tempo um CPF válido e um CNPJ legado que perdeu os zeros à esquerda. Nesse caso a ETL não importa como PF, porque descartaria a razão social real. A linha vai para quarentena (ExcludedByDecision), para decisão humana.

9. Harness (commit separado)

Os hooks do Claude Code e o pre-commit não rodavam em clone com espaço no caminho ("Área de trabalho"): caminhos sem aspas. Com isso, Prettier pós-edição, bloqueios e o gate do Stop vinham sendo pulados em silêncio. O conserto está no commit fix(harness).

10. Ambiente de validação

O Docker local é o snap da Canonical, que não executa containers com no-new-privileges. Por isso o pnpm run test:integration:partners não sobe nesta máquina. A prova contra MySQL foi feita num container isolado com a mesma imagem por digest do compose.yaml. O hardening do compose não foi afrouxado.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@GabrielAderaldo

Copy link
Copy Markdown
Contributor Author

Sobre os 4 checks vermelhos de integration (storage, logo, photo e gate): eles não vêm deste PR. As imagens minio/minio e minio/mc pinadas no compose.yaml deixaram de ser baixáveis do Docker Hub (pull access denied, inclusive pelo digest), e o mesmo workflow falha todo dia na main desde 2026-09-12. Registrado em #1026.

Todo o resto passou: typecheck + format + lint + test, semgrep, commit-policy, claude-review e as integrações com MySQL, inclusive partners, financial e etl*.

…-fisica

# Conflicts:
#	.claude/hooks/stop-quality-gate.sh
#	handbook/architecture/adr/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-assisted PR de sessão de IA — commits exigem trailer Assisted-by (ADR-0054/#549)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants