Skip to content

release(financial): o pré-voo do Pix ganha governança, exclusividade e a conta que fecha (#948) - #1005

Merged
lekadecastro merged 10 commits into
mainfrom
dev
Sep 7, 2026
Merged

lekadecastro merged 10 commits into
mainfrom
dev

Conversation

@lekadecastro

Copy link
Copy Markdown
Contributor

Segunda leva do dia. Leva o pré-voo do Pix — CA3, CA4, CA5 e CA6 da #948 — pela PR #1004. Tudo em fin_*.

O que entra

CA O que entrega
CA4 O Pix sai em remessa exclusiva; a seleção mista é recusada antes do allocateNsa, sem queimar sequência
CA5 unplannedCount e unplannedTotalCents sobem na resposta do pré-voo — a conta da tela fecha
CA3 Duas redes de governança: uma varre a união inteira de recusas do emissor, outra as do reader. Recusa nova não compila até ser classificada
CA6 Teste fim-a-fim do pré-voo até o objeto no armazenamento, contra o golden que o banco validou por escrito

Mais o rótulo da forma 45 na tela de confirmação — o arquivo sempre esteve certo, faltava a tradução na borda.

O que importa para o deploy

  • Nenhuma migration. Só código e teste.
  • Mudança de contrato na borda HTTP: a resposta do pré-voo ganha unplannedCount e unplannedTotalCents. Campo aditivo — não quebra o front que não os lê.
  • Erro novo no 422 da geração: remittance-pix-requires-exclusive-file, com mensagem PT-BR que nomeia a regra e diz explicitamente que nada foi consumido.
  • Nada muda para TED, transferência e boleto. A régua exige dois grupos de arquivo, e só o Pix cria o segundo. Há caso fixando que a seleção sem Pix não é tocada — essas rotas estão validadas em produção pelo cliente e a leva não as alcança.

O que ficou de fora, e por quê

A #863 (CNPJ alfanumérico) não vem nesta leva, por decisão da P.O. Ela estava misturada com a #948 na PR #983, sob um título que só citava a #863 — sete dos dez commits eram da #948. As duas foram separadas; os commits da #863 seguem na #983, e a régua de inscrição continua fora da main.

A separação exigiu um commit de adaptação: a rede da CA3 enumera CnabTranslateError inteira, então a classificação da variante da #863 saiu junto com a variante. Quando a #863 voltar, o Record para de compilar até alguém reclassificá-la.

Estado

Fora desta PR

A dependabot #973 (mysql2 3.22.3 → 3.23.1) segue apontando para a main em vez da dev.

https://claude.ai/code/session_01XNaURHGErqVMsDMAMDThyg

lekadecastro and others added 10 commits September 6, 2026 20:44
…ra de recusas do emissor (#948 CA3)

Havia duas redes por campo — `pix-key-single-source` (#948) e `inscription-single-source` (#863) —
e cada uma nasceu DEPOIS do defeito que impede. Cobrem o que já se descobriu. A CA3 pede a outra
coisa: varrer `CnabTranslateError` inteira e cobrar, de cada variante, resposta à pergunta "o
pré-voo barra isto antes do `allocateNsa`?".

O mecanismo é o `Record<CnabTranslateError, Coverage>`, não os `assert`: recusa nova na união não
COMPILA até ser classificada. Três classificações, e a distinção entre elas é a ação de quem recebe:

  · `preflight`  — o pré-voo bloqueia, e a sonda EXECUTA o pré-voo para provar. Classificar como
                   coberto e não estar é justamente o erro que um teste de tipos deixaria passar.
  · `file-scope` — a recusa é do ARQUIVO ou do cedente (convênio, nome do arquivo, veredito do
                   inspetor). Uma régua por título não tem onde vê-la.
  · `gap`        — o emissor recusa e o pré-voo aprova. Dívida declarada, com issue obrigatória:
                   há caso cobrando URL rastreável, para `gap` não virar o balde onde a próxima
                   variante cai só para o teste continuar verde.

Das 10 variantes: 4 com sonda, 5 `file-scope`, 1 `gap`.

⚠️ A rede achou uma lacuna VIVA na primeira execução, e não na frente Pix — no boleto. Favorecido
com inscrição válida e NOME em branco sai `ready` no pré-voo e `billet-party-unidentified` no
emissor, com o NSA já consumido; `'€€€'` cai junto, porque `alpha()` o transforma em brancos (#862)
e a guarda mede o que VAI PARA O ARQUIVO. A causa é estrutural: `PayeePaymentTarget` não tem campo
de nome, então o pré-voo não vê o dado que o emissor guarda. Registrada como #985 e classificada
`gap` — não corrigida aqui, porque o formato do boleto está validado em produção e o conserto
atravessa tipo compartilhado.

Nada de produção muda: o arquivo é só teste.

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

Assisted-by: Claude-Code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01YZHvfYtCefkh7AUNwxnYaP
…lidou por escrito (#948)

O bloco do arquivo de Pix ancorava no golden de 01/09, fornecido pela P.O. A régua agora é o
`TESTE_PIX_BLOQUEADO_CNAB240_REV2.REM`, submetido em 02/09 e VALIDADO POR ESCRITO em 05/09 pela
equipe Multipag Pix/VAN — "o arquivo foi validado e não necessita de ajustes em sua estrutura,
estando apto para transmissão". É a diferença entre um exemplo e um veredito, e é o mais perto de
homologação que esta rota chega: não há ambiente de teste para remessa de pagamento (ADR-0061).

⚠️ O COMENTÁRIO CONTRADIZIA A ASSERÇÃO 60 LINHAS ABAIXO. Duas linhas afirmavam que o bloco bancário
do favorecido sai PREENCHIDO no Segmento A, apoiadas no golden de 01/09; o caso `o Segmento A [...]
ZERA o bloco bancário` mede o oposto, e é ele que está certo. A prosa sobreviveu à mudança de tipo
da #945 que a tornou falsa — a mesma classe de divergência que o #983 corrigiu no emissor, e que o
CLAUDE.md chama de defeito a registrar, não de texto a escolher.

Quem arbitrou foi o banco, no sexto ponto do laudo: "é possível enviar os campos referentes ao
banco, agência e conta do favorecido preenchidos com zeros".

Conferido campo a campo contra o REV2: 14 campos, ZERO divergências. Cobre os seis pontos que o
laudo confirmou (modalidade 45, câmara 009, G100 = 04, ISPB zerado, movimento 0 + instrução 09,
bloco bancário zerado) e três que só o arquivo revela e que nenhum teste de unidade pegaria, porque
medem o que este código programou:

  · `G100` sai `04` + BRANCO, alfa à esquerda num campo de 3 — não `004` numérico;
  · a literal `PIX` ocupa 172-174 do header de arquivo, contra o que o Validador Universal pede;
  · `043` (DV agência/conta) sai em BRANCO (#754).

Só comentário muda; nenhum caso foi acrescentado ou alterado.

Gate: typecheck, lint e format:check verdes; a suíte do arquivo passa 57/57.

Assisted-by: Claude-Code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01YZHvfYtCefkh7AUNwxnYaP
…recusada antes do NSA (#948 CA4)

Decisão da P.O. de 03/09/2026. A barreira de tela já existe no web-app; faltava a régua do servidor,
e uma regra que só vive no cliente não é regra — a rota é alcançável sem passar por tela nenhuma.

A recusa nasce em `planRemittanceFiles`, que roda ANTES do `allocateNsa`. É a posição, e não a
mensagem, que torna a recusa barata: recusar depois deixaria um número da série queimado sem arquivo
do outro lado, e o NSA não volta por desenho.

⚠️ A REGRA É ESPECÍFICA DO PIX, e isso é o ponto. TED, transferência entre contas Bradesco e boleto
já foram testados, habilitados e validados EM PRODUÇÃO pelo cliente — arquivo transmitido, aceito e
pago. Para eles repartir continua sendo o comportamento certo, e nada aqui os alcança: a régua exige
DOIS grupos de arquivo, e só o Pix cria o segundo. Há caso fixando isso (`não toca a seleção sem
Pix`), porque "não alterou" se demonstra, não se presume.

O erro sobe NOMEADO pelas quatro camadas em vez de virar `translation-failed`, e o contraste com
`remittance-mixed-file-modalities` é a razão: os dois falam de mistura e param em lugares opostos.
Aquele é a defesa do MONTADOR contra quem não repartiu — chegar lá é defeito de código, e o operador
não tem o que fazer. Este é régua de SELEÇÃO — o operador refaz e segue. Um nome só para os dois
mandaria metade das pessoas procurar um defeito que não existe.

A mensagem ao operador nomeia a regra em vez de descrever a falha, e diz que nada foi consumido:
"O Pix sai em remessa exclusiva: gere os títulos de Pix numa remessa e os demais em outra. Nada foi
gerado e nenhum número de arquivo foi consumido."

Dois efeitos que valem registro:

· `planRemittanceFiles` NÃO TINHA teste direto — era exercida só através do use case. Passa a ter
  quatro, incluindo a guarda da faixa madura.

· O `switch` da partição perdeu o `default`. Com ele, a variante seguinte da união entraria calada no
  desfecho genérico; enumerado, o `switch-exhaustiveness-check` obriga quem acrescentar uma recusa a
  escolher o nome que o operador vai ler. Foi o ESLint que cobrou, e está certo.

E a rede de governança do CA3 fez o trabalho dela: acrescentar a variante à união quebrou a
COMPILAÇÃO do `Record<CnabTranslateError, Coverage>` antes de qualquer caso rodar, exigindo a
classificação. Ela entrou como `file-scope`, com a razão que a distingue das outras desse balde —
não é recusa do arquivo nem do cedente, é da SELEÇÃO, e uma régua por título não enxerga os vizinhos.

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

Assisted-by: Claude-Code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01YZHvfYtCefkh7AUNwxnYaP
…onta da tela fecha (#948 CA5)

`planBatches` já produzia `unplannedCount` e `unplannedTotalCents`; o pré-voo os DESCARTAVA,
propagando só `batches`. A tela recebia os lotes e a seleção, via que não batiam, e não tinha como
dizer quanto ficou de fora — levantava a dúvida sem oferecer a resposta.

⚠️ E PROPAGAR O CAMPO DO PLANEJADOR DIRETO ESTAVA ERRADO, que é o achado desta fatia. O nome é o
mesmo e o comentário dele prometia `lotes + não planejado = seleção`, mas a seleção que ELE vê já
passou por `plannableOf`, que retém só título `ready` com rota. O número dele conta o resíduo —
pronto que ainda assim não agrupou —, não o que o operador marcou e não vai sair. O primeiro teste
da aritmética falhou com `2 !== 4` numa seleção de quatro títulos com dois impedidos, e foi ele que
mostrou a diferença.

"Não planejado" tem DUAS origens, e a conta só fecha somando as duas: o que não chegou ao planejador
(a diferença entre a seleção e `plannable`) mais o que ele recusou. As parcelas vêm de fontes
independentes de propósito — se as duas etapas divergirem, a soma para de bater e o teste acusa, que
é o único motivo de este número existir.

O comentário do port foi corrigido junto: ele dizia "da seleção" onde só podia dizer "dos recebidos
aqui", e é a leitura que me levou ao erro. O planejador não tem como fechar a conta da seleção
sozinho — ele não sabe o que não lhe foi dado.

⚠️ Os dois campos NÃO substituem `blockedCount` e companhia, e confundi-los produz tela errada: os
contadores classificam o TÍTULO pela ação do operador (completar cadastro, retirar da seleção,
aprovar); estes medem o LOTE. O mesmo título conta nos dois, e isso não é dupla contagem — são
respostas a "o que preciso fazer?" e "a soma dos lotes explica minha seleção?".

Publicados no schema, então entram no contrato OpenAPI. `0` é resposta e vai sempre: campo que só
aparece às vezes obriga a tela a tratar dois formatos, e é aí que ela decide errado — há caso
fixando isso.

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

Assisted-by: Claude-Code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01YZHvfYtCefkh7AUNwxnYaP
… teste só (#948 CA6)

Estava preso a #923/#945 — "aguardando o banco". O laudo da equipe Multipag Pix/VAN de 05/09/2026
destravou: "o arquivo foi validado e não necessita de ajustes em sua estrutura, estando apto para
transmissão".

As duas metades já eram cobertas separadamente, e é por isso que este arquivo existe: um pré-voo que
aprova e uma geração que produz arquivo não provam que o operador consegue ir de um ao outro. A #837
nasceu dessa lacuna — as duas pontas concordavam em teste e divergiam em uso.

O caminho medido, em três etapas no mesmo caso:

  1. `previewRemittance` aprova o título (`readyCount = 1`, rota `pix`), a conta do CA5 fecha
     (`unplannedCount = 0`), e o lote que a TELA mostra é a forma `45` — a mesma que vai sair.
  2. `generateRemittance` emite UM arquivo, que é o CA4 visto do outro lado: seleção só de Pix
     produz exatamente um.
  3. O objeto chega a `saida/<fileName>` e é lido de volta do armazenamento. É aqui que o teste
     deixa de medir valor de retorno e passa a medir o que a VAN vai buscar — arquivo correto que
     não chega ao armazenamento não paga ninguém.

A estrutura é conferida contra o `TESTE_PIX_BLOQUEADO_CNAB240_REV2.REM`: 6 registros, SEM Segmento J,
literal `PIX` em 172-174, forma `45`, layout `045`, movimento `0` + instrução `09`, câmara `009`,
bloco bancário do favorecido zerado em 021-042, `043` em branco, `G100` = `04 ` (alfa à ESQUERDA, não
`004`) e ISPB zerado. O inspetor de forma aprova o objeto depositado.

⚠️ O QUE ELE NÃO PROVA, escrito no próprio arquivo para ninguém confiar de mais: os dois use cases
leem por PORTS DIFERENTES, então as duas fixtures descrevem o mesmo título sem serem o mesmo dado.
Que o cadastro real alimente as duas coerentemente é dos adapters Drizzle, e é medido na integração.
O que se prova aqui é o CAMINHO.

O planejador e o tradutor são os adapters REAIS, não fakes: um fake reimplementaria a régua de
agrupamento e de layout dentro do teste, e duas réguas divergem — a mesma razão pela qual o front não
pode replicar a partição.

Verificado que o teste MORDE: trocando a asserção da forma para `46`, ele falha com `'45' !== '46'`.
Caso novo que passa de primeira merece a prova.

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

Assisted-by: Claude-Code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01YZHvfYtCefkh7AUNwxnYaP
…ade, não dívida (#985)

A divergência continua REAL e medida — boleto com inscrição válida e nome em branco sai `ready` no
pré-voo e `billet-party-unidentified` no emissor. O que estava errado era a classificação: eu não
perguntei se o estado é ALCANÇÁVEL antes de abrir a issue, que é o passo 0 da skill.

Não é. `name` é `NOT NULL` em todas as tabelas de parceiro, `PayeeContractor.name` é `string`
não-anulável, e a criação valida `.min(1)` na borda. Sobra o nome composto só de caracteres sem
transliteração legível — emoji, ideograma —, que não sai de cadastro brasileiro. É a mesma categoria
das condições do reader sobre `dueDate` e `valueCents`.

⚠️ E NÃO FOI A VALIDAÇÃO EM PRODUÇÃO QUE FECHOU ISSO, embora a rota de boleto esteja validada no
Validador Universal e em produção pelo cliente. Aquele laudo prova que o FORMATO está certo para os
dados que passaram; ele é cego para pré-voo aprovando o que o emissor recusa, porque isso não é
defeito de forma e nunca chega a virar arquivo. Quem fecha é o `NOT NULL`. Ficou escrito no teste
porque o argumento errado, se ficar de pé, dispensa o próximo achado desta classe — e o próximo pode
ser alcançável.

A rede ganhou a classificação `schema` e a cobrança que a torna honesta: quem alega inalcançável
CITA a coluna ou a validação que sustenta a alegação, e a próxima pessoa consegue conferir se ainda
vale. Sem isso, `schema` viraria o balde que o `gap` deixou de ser.

O caso de `gap` fica de pé passando a VAZIO, e é o estado desejado, não teste esquecido — está dito
no comentário para ninguém o remover por parecer inútil.

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

Assisted-by: Claude-Code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01YZHvfYtCefkh7AUNwxnYaP
…#948)

O lote de Pix saía no pré-voo como `45` cru, sem nome legível — apareceu na primeira validação em
tela da rota. O mapa de rótulos da borda cobria `01`, `30`, `31` e `41`, e ninguém contou a quinta.

⚠️ O ARQUIVO SEMPRE ESTEVE CERTO, e a distinção é a lição desta fatia. A forma `45` sai correta nas
posições 012-013 do header de lote — é o que passou no Validador Universal e ganhou o laudo escrito
do Bradesco em 05/09/2026, conferido campo a campo contra o `TESTE_PIX_BLOQUEADO_CNAB240_REV2.REM`.
O que faltava era a tradução na BORDA, que validação de arquivo nenhuma alcança: o banco confere o
que recebe, não o que o operador lê.

E a lacuna ficou invisível por uma segunda razão, que vale registrar junto: o rótulo só apareceria
numa tela de pré-voo com título Pix APROVADO, e não há nenhum na base local — só `Transmitted` e
`Paid`. A ausência de DADO escondeu a ausência de RÓTULO, e as duas foram descobertas no mesmo
minuto, pela mesma tentativa de validar em tela.

O nome é o do manual (`batch-profile.ts:84`, G029 p. 100): "Pix Transferência". Não é invenção da
borda — é o que o operador encontra se procurar a modalidade na documentação do banco.

Para a próxima forma não repetir isto, `batch-profile.ts` passa a exportar `LAUNCH_FORMS` com as
cinco que o emissor pode escrever, e o teste novo percorre a lista exigindo rótulo de cada uma.
Acrescentar forma nova ali passa a ser parte de emiti-la, não um passo separado que se esquece.

O fallback NÃO foi substituído, e há caso fixando isso: forma desconhecida continua caindo no
próprio código, porque um traço esconderia que o emissor ganhou uma rota que a borda não acompanhou.
Foi justamente o fallback que tornou este defeito visível em vez de silencioso — o que se cobra
agora é que nenhuma forma CONHECIDA dependa dele.

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

Assisted-by: Claude-Code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01YZHvfYtCefkh7AUNwxnYaP
…sta leva (#948)

Esta branch é a #948 separada da #983, e a #863 fica de fora — decisão da P.O. A rede de
governança enumera `CnabTranslateError` inteira, então tirar a variante da #863 da união
obriga a tirar a classificação dela daqui: o `Record` não compila com chave que o tipo não
tem, que é exatamente o mecanismo funcionando.

O que saiu, e por que nada se perde:

  · a entrada `cnab-inscription-alphanumeric-unsupported` e sua sonda — a variante não
    existe mais na união desta branch;
  · a citação a `inscription-single-source` no cabeçalho, que é arquivo da #863 e não
    existe aqui. Citar teste inexistente é a classe de divergência que o CLAUDE.md manda
    registrar, não herdar;
  · o piso de sondas cai de 4 para 3, que é quantas restaram.

⚠️ O PISO NÃO É COSMÉTICO, e vale dizer por que ele desce em vez de sumir. Ele existe para
impedir o esvaziamento silencioso — alguém trocar `preflight` por `file-scope` em massa e o
teste seguir verde sem sonda nenhuma. Deixá-lo em 4 com três sondas o transformaria em
vermelho permanente; removê-lo tiraria a guarda. Desce para o número real.

Quando a #863 voltar, a variante volta à união e o `Record` para de compilar até alguém
reclassificá-la — a rede cobra sozinha, sem precisar que ninguém se lembre disto aqui.

Gate: typecheck + format:check + lint verdes; 11799 testes, 0 falhas (baseline 11785).

Refs: #948, #863
Assisted-by: claude-code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01XNaURHGErqVMsDMAMDThyg
…sustenta (#948)

Achado do code review desta branch. Ao inserir `sumOf`, eu o coloquei ENTRE o docblock de
`plannableOf` e a própria `plannableOf` — então o parágrafo que descreve "`null` para tudo
que não está `ready`" passou a documentar um somador que não faz nada disso, e a função cujo
contrato ele descreve ficou sem comentário algum.

Não é preciosismo num repositório onde o comentário É o contrato: o que torna a aritmética
do CA5 correta é exatamente `plannableOf` reter só "pronto com rota". Quem afrouxar esse
predicado muda a conta da tela sem tocar numa linha de `sumOf` — e, com o docblock no lugar
errado, sem ler nada que o avisasse.

O comentário volta para junto da função, e ganha a frase que faltava: a ligação explícita
entre o predicado e o `unplannedCount`.

Gate: typecheck + format:check + lint verdes; 11799 testes, 0 falhas.

Refs: #948
Assisted-by: claude-code:claude-opus-5
Claude-Session: https://claude.ai/code/session_01XNaURHGErqVMsDMAMDThyg
…nca-exclusividade-948

fix(financial): o pré-voo do Pix ganha governança, exclusividade de arquivo e a conta que fecha (#948 CA3–CA6)
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.

1 participant