Skip to content

fix(financial): CNPJ alfanumérico deixa de virar outra inscrição em silêncio (#863) — traslado do #983 - #1010

Merged
GabrielAderaldo merged 4 commits into
devfrom
fix/inscription-alphanumeric-863-v2
Sep 9, 2026
Merged

GabrielAderaldo merged 4 commits into
devfrom
fix/inscription-alphanumeric-863-v2

Conversation

@GabrielAderaldo

Copy link
Copy Markdown
Contributor

Substitui o #983, que não é mergeável por um motivo que não se resolve resolvendo conflito.

Por que uma branch nova, e não o merge da dev no #983

A fix/inscription-alphanumeric-863 tinha 11 commits fora da dev, e só 2 eram dela. Os outros 8 eram a leva da #948, que já entrara na dev pelo PR #1004 — com SHAs diferentes:

na branch do #983 na dev (via #1004)
11029d8e CA4 72c485cc CA4
b7ef760c CA5 22b75f1d CA5
a44f7d75 CA6 22265b42 CA6

Git não reconhece os pares como o mesmo trabalho, então tratava cada arquivo como duas criações independentes — daí os dois CONFLICT (add/add), em inscription.ts e preflight-covers-emitter-refusals.test.ts, que não têm por que existir num PR de duas correções. Mergear a dev lá dentro resolveria os conflitos e deixaria os 8 commits duplicados no histórico para sempre.

Aqui estão só os 2 commits da #863, aplicados sobre a dev atual, com autoria preservada.

O que a resolução decidiu, onde os dois lados discordavam

⚠️ Achado de percurso: a mensagem do 842a31b0 descreve remoções que o próprio diff dele não contém (ele altera 2 linhas de comentário). O conteúdo já saíra antes; a mensagem descreve o efeito acumulado da branch, não o commit. Não bloqueia nada, mas quem procurar a remoção por aquele SHA não a encontra.

Gate

typecheck + format:check + lint verdes. 11.825 testes, 0 falhas, 20 skips (os esperados). Rodado por commit, pelo hook de pre-commit.

Revisão — 4 achados, todos anteriores a este traslado

Nenhum vem da resolução de conflito; são do código original da #863 e ficam para a autora decidir.

  1. payout-readiness.ts:279 — média. O carve-out do vazio (document.trim() === '' → null) delega a cobrança a readBilletPayee, que só existe na rota billet. Na TED o resultado é assimétrico: document: '---' devolve missing, mas document: null/'' continua devolvendo ready — o pré-voo aprova, o NSA é alocado, e segmentB falha com cnab-translation-failed sem apontar campo. As duas formas de "não há inscrição" deveriam dar o mesmo veredito.
  2. payout-readiness.ts:324 — baixa/média. readAlphanumericInscription roda para todas as rotas, tax-guide inclusive, embora nenhum registro de guia escreva a inscrição do favorecido. O status muda de no-issuer para incomplete, e o operador lê "cadastro incompleto" onde a verdade é "esta forma ainda não é emitida".
  3. payout-readiness.ts:253 — baixa (registro mente sobre o código). O comentário afirma que a inscrição do cedente "não chega aqui" e que uma alfanumérica derrubaria a remessa depois do allocateNsa. checkCedenteInscription já a recusa antes, em generate-remittance.ts:145.
  4. remittance-eligibility.ts:156 — baixa. O comentário diz "a pergunta é hasInscription", mas o código escreve normalizeInscription(...) === '' à mão. Com hasInscription já existindo, este é o terceiro lugar com a definição de "vazio" duplicada — a configuração que a própria [financial] CNPJ alfanumérico do FAVORECIDO é destruído por digits() — o Segmento B emite outra inscrição #863 documenta como causa.

Fora do código: 00000000000191 é descrito como "sintético" em três fixtures, mas é CNPJ real com DV válido. Os repositórios são públicos; um valor com DV deliberadamente inválido serviria igual sem afirmar algo falso no comentário.

Closes #863
Refs #983, #948, #1004

lekadecastro and others added 2 commits September 8, 2026 14:22
…ilêncio (#863)

`digits()` existe para tirar MÁSCARA — `12.345.678/0001-99` e `12345678000199` são o mesmo
CNPJ, e converter um no outro é tradução de formato. Aplicada a um documento
ALFANUMÉRICO, ela deixava de tirar pontuação e passava a destruir conteúdo:

    digits('12ABC34501DE35', 14)  ->  '00000123450135'

Catorze dígitos, sintaticamente perfeitos, e de outra pessoa. O arquivo era aceito, o
pagamento saía, e o favorecido chegava ao banco identificado por um documento que não é o
dele. Nada no caminho reclamava: o `remittance-inspector` aprova, o validador do banco
aprova, e o erro só apareceria na conciliação ou numa contestação.

A Receita emite CNPJ com letras desde 07/2026 (ADR-0044). O layout do Bradesco na v08
(jul/2025) é anterior e declara o campo `Num` — ele não acompanhou.

O LAUDO DE 05/09 AGRAVOU ISTO, sem falar dele

Ficou estabelecido que no Pix o PSP do recebedor cruza a inscrição do arquivo com o
titular da chave no DICT, recusando com `PF` quando não bate. Uma inscrição deformada
deixou de ser só um dado errado: é a recusa do pagamento, sem que nada no ERP aponte a
causa.

O DEFEITO ERA USAR O HELPER ERRADO

`digits()` continua certo para agência, conta e CEP — ali o que ela remove é máscara, e
máscara não é conteúdo. O que faltava era um campo que soubesse que inscrição é outra
coisa. Entra `inscription()`, em `positional.ts`, e os SETE pontos de escrita passam a
usá-lo: Segmento B da transferência e do Pix (019-032), o `G031` do Segmento A, o par
sacado/cedente do Segmento J-52 (021-035 e 077-091) e os dois headers do cedente.

⚠️ A carve-out do VAZIO é obrigatória, e um teste do Segmento J-52 a cobrou: sem ela,
campo em branco passaria a sair como `inscription-alphanumeric-unsupported`, mandando
escalar ao banco um caso que é simplesmente cadastro faltando. Vazio e estouro continuam
sendo o que sempre foram.

RECUSAR, E NÃO EMITIR — porque a pergunta ao banco está aberta

As três saídas foram consideradas. Emitir com as letras num campo `Num` produz arquivo que
o banco pode recusar depois do NSA queimado. Remover as letras é o defeito. Converter (o
`ASCII − 48` da RFB) não é escolha que se faça sozinho: aquela regra é para CALCULAR o
dígito verificador, não para transmitir, e usá-la aqui inventaria um formato que o banco
não pediu.

A pergunta — o Bradesco aceita CNPJ alfanumérico no CNAB 240, e em que forma? — está
registrada na #863 e segue sem resposta. Enquanto isso, recusar com nome próprio é a única
saída que não inventa layout nem paga errado.

A CONTRAPARTE NO PRÉ-VOO, FORA DO SWITCH DE ROTA

`readAlphanumericInscription` é a única régua do arquivo verificada fora do `switch`, e é
deliberado: ela não é propriedade da rota. TODA rota com emissor escreve a inscrição —
são sete pontos de escrita em três rotas. Dentro do `switch` seriam três cópias, e uma
rota nova nasceria sem ela.

O motivo é `unmappable`, não `malformed`: o CNPJ com letras está BEM FORMADO, e mandar
"corrigir o formato" mandaria o operador estragar uma inscrição correta.

⚠️ É a única lacuna do módulo cuja ação não é "vá ao cadastro" — é ESCALAR. Por isso o erro
sobe até a application com nome próprio e converge para `remittance-build-failed`, e não
para `remittance-payments-unavailable`, que diria ao operador "confira o pré-voo" quando
não há nada lá que ele possa consertar.

A FIXTURE QUE PRECISOU MUDAR, E É UM ACHADO

`PAYEE_DOCUMENT` era `'inscricao-opaca'`, com razão escrita: provar que a régua "só
pergunta se HÁ inscrição". A afirmação deixou de ser inteiramente verdadeira, e as quatro
suítes que a usavam quebraram — corretamente.

A distinção que sobrevive é fina e ficou registrada na fixture: a régua continua NÃO
validando CPF/CNPJ (não confere dígito nem comprimento — há teste fixando que `'123'`
passa). Ela só recusa o que `digits()` destruiria. Um valor sem nenhum dígito não
distinguia os dois casos: falhava pelas duas razões ao mesmo tempo.

Gate: typecheck + format:check + lint + 11692 testes, 0 falhas.

⚠️ Empilhado sobre o PR #982 (que está sobre o #981).
⚠️ NÃO fecha a #863: o CA1 admite as duas saídas, e esta é a defensiva. A issue fecha
quando o banco responder como quer receber a inscrição alfanumérica.

Refs: #863, #948 (CA11), #891, #856
Assisted-by: Claude-Code:claude-opus-5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJKkFN7sQDoBS8AvteK1kZ
…ção vazia (#863)

Três achados da revisão do #983.

1. Documento só com pontuação divergia entre as duas pontas. `'---'`, `'.'` e `'./-'`
sobrevivem ao `trim()` e normalizam para vazio: o emissor sempre os viu como campo sem
conteúdo (`numeric-field-invalid`), enquanto o pré-voo os via como inscrição PRESENTE e —
não sendo numéricos — concluía "alfanumérica", mandando ESCALAR ao banco um cadastro que só
está incompleto. É a classe de divergência que a #837 fechou, e custa mais aqui do que nas
outras: `unmappable` é a única lacuna do módulo cuja ação não é "vá ao cadastro".

`hasInscription` passa a ser a pergunta única de presença, e o caso vira `missing` — a mesma
ação do campo em branco. Só o boleto tinha régua de presença (`readBilletPayee`), então sem
esta linha o caso passaria do pré-voo direto ao emissor na TED, com o NSA já queimado.

2. O reader mantinha `cleanDocument`, cópia idêntica de `normalizeInscription` — a mesma
configuração que produziu a #863: duas decisões sobre o mesmo dado, nenhuma citando a outra,
concordando por acidente até o dia em que uma mudasse. Passa a importar a do domínio, e o
acoplamento fica declarado: `documentTypeOf` mede o COMPRIMENTO do que a normalização
devolve, então qualquer mudança nela reclassifica pessoa física e jurídica no arquivo.

3. Comentários. Dois ainda citavam `digits()` para inscrição, que deixou de ser verdade. E o
de `readAlphanumericInscription` prometia cobrir "sete pontos de escrita": a régua lê só
`candidate.payee.document`, então são QUATRO. Os outros três — header de arquivo, header de
lote e sacado do J-52 — escrevem a inscrição da própria empresa, que não chega num
`PayoutCandidate`. Risco baixo, porque a empresa já existe e sua inscrição é numérica, mas o
comentário prometia cobertura que não existe.

Gate: typecheck, lint e format:check verdes; 11695 testes, 0 falhas.

Assisted-by: Claude-Code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01YZHvfYtCefkh7AUNwxnYaP
Copilot AI lite review requested due to automatic review settings September 8, 2026 17:36

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.

… o emissor ganha a rede que faltava

Os dois achados do code-review desta branch.

**1. Dado de cadastro em fixture (anti-padrão #9).** O documento que seis arquivos de teste usavam
como "sintético" não era: tinha DV válido e correspondia à inscrição raiz de uma instituição
financeira existente. Os repositórios são públicos, e o gate mecânico não alcança este caso —
`bank-fixture-masking.test.ts` cobre convênio, e a rule do CNAB registra que "o resto depende de
julgamento".

Trocado pelo exemplo que o próprio repositório já consagra em `.claude/rules/domain.md`, sem
correspondência a inscrição emitida e já usado em testes de `contracts`, `etl` e `jobs`. Nada
dependia de o valor ser um CNPJ existente: `isCnabEmittableInscription` só exige dígitos.

⚠️ O mesmo valor sobrevive em três arquivos FORA desta branch (um em `financial`, dois em
`partners`). Não entraram aqui para não misturar módulos num PR de CNAB.

**2. O teste prometia uma rede de regressão que não existia.** O comentário afirmava que, se alguém
trocasse `inscription` de volta por `digits`, o caso falharia "apontando exatamente o que voltaria a
acontecer". Não falharia: o arquivo chama `inscription()` DIRETO e não monta registro nenhum — ele
prova que a régua existe, não que o emissor a usa. É a distinção entre USO e MENÇÃO, que é como o
defeito original sobreviveu.

E o teste de ponta a ponta não cobre a lacuna: em `generate-remittance.test.ts` (CA3) a recusa é do
PRÉ-VOO, que barra antes de o emissor ser chamado. A camada que de fato produziria o arquivo aceito
com a inscrição de outro não tinha teste executável em lugar nenhum.

Acrescentados quatro casos que montam os registros de verdade — `fileHeader`, `batchHeader`,
`segmentB` e `segmentBPix` — com documento alfanumérico, exigindo
`inscription-alphanumeric-unsupported`. Por registro, e não por varredura de texto no fonte: o que
precisa ser verdade é comportamento, e um grep acusaria também quem só menciona o nome.

**Prova invertida (medida, não suposta):** revertendo os dois call sites de `multipag-records.ts`
para `digits()`, os 8 casos que já existiam passam TODOS — inclusive o que prometia falhar — e só os
casos novos acusam. É a demonstração de que a rede não existia.

Gate verde: typecheck + format:check + lint + test (11.812 testes, 0 falhas).

Refs: #863
Assisted-by: Claude-Code:claude-opus-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants