Skip to content

fix(financial): o DV da agência do favorecido passa a ser conferido por cálculo - #1008

Merged
GabrielAderaldo merged 2 commits into
devfrom
fix/dv-agencia-favorecido
Sep 8, 2026
Merged

GabrielAderaldo merged 2 commits into
devfrom
fix/dv-agencia-favorecido

Conversation

@GabrielAderaldo

Copy link
Copy Markdown
Contributor

O que motivou

O Validador Universal recusou a remessa de 08/09/2026 com sete erros. A hipótese de partida era "o Pix
funciona e o TED quebra"
— e o diff posicional entre o arquivo recusado e o aceito a derrubou em um passo:

linha 1: 53-58 [NNNNN ] -> [NNNNND]   |  66-71 [NNNNND] -> [NNNNND]
linha 2: 53-58 [NNNNN ] -> [NNNNND]   |  66-71 [NNNNND] -> [NNNNND]
linha 3: IDENTICA          ← o Segmento A do TED
linha 6: 53-58 [NNNNN ] -> [NNNNND]   |  66-71 [NNNNND] -> [NNNNND]
linha 7: 24-29 [NNNNN ] -> [NNNNND]   |  37-42 [NNNNNN] -> [NNNNNN]

A linha do TED é byte-idêntica nos dois arquivos. Um registro idêntico não distingue recusado de aceito.
Também não havia Pix: as formas eram 41 (TED) e 01 (crédito em conta).

O que quebrava era agência e conta, sempre com a mesma forma: o DV grudado dentro do campo numérico e a
posição do DV em branco. Provado por aritmética — a agência do favorecido era NNNN seguido exatamente do
dígito que o módulo 11 produz para NNNN.

Por que o TED escapou: o favorecido dele é de outro banco. O algoritmo do DV é do Bradesco, e ele não o
aplica a conta de terceiro. O recorte real é domicílio Bradesco × domicílio de terceiro, nunca a forma de
lançamento.

O que muda

Fonte primária (Manual 4008-523-0096 v16, p. 30): a seção do cálculo abre com "Agência Bradesco: Módulo 11
Universal, com pesos 2 a 7"
e exemplifica com a AGÊNCIA (9999 → 6); só depois estende o critério à conta.
O algoritmo sempre foi dos dois campos — a #734 implementou o consumidor da conta, e o nome da função ficou por
ela.

  • account-check-digit.ts — verifyAgencyCheckDigit delega ao núcleo existente, e
    agencyHasEmbeddedCheckDigit prova por aritmética o campo que traz o dígito colado no número. Sem segunda
    cópia do módulo 11.
  • payee-account.ts — readAgency recebe o banco e produz lacuna nomeada: malformed para o DV embutido,
    check-digit-mismatch para o DV declarado que não fecha. checkDigitGaps ganha o campo por parâmetro: a
    política é uma só para os dois.
  • Fixtures — quatro traziam 1234-5, e 5 nunca foi o dígito de 1234. A mesma classe de defeito que
    chegou ao banco, morando na suíte. O comentário de bank-code-extraction.test.ts:30-31 já previa esse modo de
    falha para a conta; faltava a agência entrar na régua.

O que NÃO muda, de propósito

  • Nada é decomposto automaticamente. Saber que 12343 é 1234-3 não autoriza escrever a agência derivada
    nas posições 024-028 — um dígito errado ali não falha o arquivo, paga a conta de outra pessoa. O que sai é
    lacuna, e a decisão volta ao operador.
  • DV ausente segue aceito. G009 declara o campo "Não Obrigatório – Informação Opcional", e o laudo confirma
    pelo silêncio: apontou as colunas da agência e nunca a posição do DV, em branco nas três linhas.
  • Fora do 237 nada muda — not-verifiable, como já era para a conta.

O segundo commit, e por que ele importa mais que o primeiro

O /code-review achou um defeito na própria correção, e ele era pior que o problema original.

readAgency passava ao detector a agência já com padStart(5, '0'). Com o argumento sempre em cinco caracteres,
a guarda de largura nunca disparava em produção, e uma agência legítima de quatro dígitos abcd era lida como
0abc + d — recusada sempre que DV('0abc') === d. 900 das 9000 agências de quatro dígitos, uma em cada
dez (1007, 1236, 1015…). E a correção que a lacuna sugeria era separar 1236 em 123-6, o que escreveria
00123 no destino: o gate recusava cadastro correto e ensinava a corromper o pagamento.

A suíte ficou verde sobre isso porque as duas guardas eram exercitadas de formas incompatíveis — o teste unitário
chamava o detector sem padding (caminho que produção nunca toma) e os três valores do loop não pertenciam à
classe do falso positivo. Ambos agora carregam 1236 e 1007, mais um caso que entra pela régua real.

Prova invertida: com o argumento errado de volta, os dois testes reprovam (fail 2). Sem isso eles não
provariam nada.

Escopo deliberadamente fora

Só o favorecido. A agência do cedente — apontada no mesmo laudo, colunas 053-057 dos três headers — continua
validada por forma. É a #1006, e está fora daqui porque a #819 pede para não entregar a verificação do
cedente sozinha: com o cadastro travado por histórico (edit-cedente-account.ts:64-68), o operador ficaria entre
uma remessa recusada e uma tela que não deixa corrigir, e a única saída restante seria UPDATE em produção.

O comentário que afirmava cobertura ponta a ponta foi corrigido no segundo commit — registro que afirma mais do
que o código faz é o defeito que o CLAUDE.md nomeia.

Gate

typecheck + format:check + lint + 11811 testes, 0 falhas (11810 antes; +1 do caso de produção).

Issues


Assisted-by: claude-code:claude-opus-5

…or cálculo

O Validador Universal recusou a remessa de 08/09/2026 apontando as colunas
024-028 do segmento A — a agência —, e nunca a posição do DV, que estava em
branco. O pré-voo aprovava porque conferia a agência só por FORMA: cinco
dígitos cabem em cinco posições, e era tudo o que se perguntava.

O manual 4008-523-0096 v16 (p. 30) abre a seção do cálculo com "Agência
Bradesco: Módulo 11 Universal, com pesos 2 a 7" e exemplifica com a AGÊNCIA;
só depois estende o critério à conta. O algoritmo sempre foi dos dois campos —
a #734 implementou o consumidor da conta, e o nome da função ficou por ela.

`verifyAgencyCheckDigit` delega ao mesmo núcleo em vez de copiá-lo, e
`agencyHasEmbeddedCheckDigit` prova por aritmética o campo que traz o dígito
colado no número. Nenhuma das duas decompõe: saber que `12343` é `1234-3` não
autoriza escrever a agência derivada na posição 024-028 — um dígito errado ali
não falha o arquivo, paga a conta de outra pessoa. O que elas produzem é lacuna
nomeada, e a decisão volta a quem tem a informação.

O DV AUSENTE segue aceito. G009 o declara "Campo Não Obrigatório – Informação
Opcional", e o laudo confirma pelo silêncio: nunca apontou a posição do DV.

Quatro fixtures traziam `1234-5`, e o `5` nunca foi o dígito de `1234` — a mesma
classe de defeito que chegou ao banco, morando na suíte. O comentário de
`bank-code-extraction.test.ts` já previa isto para a conta; faltava a agência
entrar na régua.

Escopo deliberado: só o favorecido. O DV do cedente é a #816, que a #819 pede
para não entregar sozinha — juntas elas encalham o operador entre uma remessa
recusada e um cadastro travado por histórico.

Refs #816
Refs #820

Assisted-by: claude-code:claude-opus-5
…hida com zeros

Achado do code-review, e o defeito era pior que o que a mudança corrigia.

`readAgency` passava `agency` — o valor já com `padStart(5, '0')` — ao
`agencyHasEmbeddedCheckDigit`. Como o argumento chegava sempre com cinco
caracteres, a guarda de largura do detector nunca disparava pelo caminho de
produção, e uma agência legítima de quatro dígitos `abcd` era lida como
`0abc` + `d`. Sempre que `DV('0abc') === d`, era recusada: 900 das 9000
agências de quatro dígitos, uma em cada dez, com `1007` e `1236` entre elas.

E a recusa era a pior possível. A correção que a lacuna sugere ao operador é
separar `1236` em `123-6` — que escreveria `00123` nas posições 024-028. Ou
seja: o gate recusava cadastro correto e ensinava a corromper o destino do
pagamento, que é exatamente o modo de falha descrito no cabeçalho deste
arquivo como a razão de nada aqui decompor por conta própria.

A suíte ficou verde sobre o defeito porque as duas guardas que o pegariam eram
exercitadas de formas incompatíveis: o teste unitário chamava o detector SEM
padding, caminho que a produção nunca toma, e os três valores do loop de
regressão (`1234`, `0920`, `12345`) não pertencem à classe do falso positivo.
Ambos agora carregam `1236` e `1007`, e há um caso que entra pela régua real
para provar que a guarda de largura está viva em produção — não só na chamada
direta. Prova invertida: com o argumento errado de volta, os dois reprovam.

Corrigido também o comentário que afirmava que "os dois defeitos que a recusa
de 08/09/2026 expôs passam a ser vistos antes do banco". Só os do favorecido
são. A agência do cedente, apontada no mesmo laudo nas colunas 053-057 dos três
headers, segue validada apenas por forma — é a #1006, fora deste diff por causa
da #819. Registro que afirma mais do que o código faz é o defeito que o
CLAUDE.md nomeia, e estava sendo introduzido aqui.

Refs #816
Refs #1006

Assisted-by: claude-code:claude-opus-5
Copilot AI lite review requested due to automatic review settings September 8, 2026 16:08

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
GabrielAderaldo merged commit 1d4cb6c into dev Sep 8, 2026
18 checks passed
GabrielAderaldo added a commit that referenced this pull request Sep 8, 2026
chore(release): integra a dev na main — DV da agência do favorecido (#1008)
@GabrielAderaldo
GabrielAderaldo deleted the fix/dv-agencia-favorecido branch October 5, 2026 19:44
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.

2 participants