From 99bb27cde0cf3f72d070d38f2c90a4ce790fc175 Mon Sep 17 00:00:00 2001 From: FabioRolin Date: Wed, 17 Jun 2026 11:34:20 -0300 Subject: [PATCH 1/3] =?UTF-8?q?OR-145:=20corrige=20m=C3=A1scaras=20de=20ag?= =?UTF-8?q?=C3=AAncia=20e=20conta=20(Ita=C3=BA,=20Caixa,=20BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A migração do OR-145 (#35) criou BANK_BRANCH_MASKS e definiu por engano a agência do Itaú (341) como '99999-9' — a máscara de CONTA do Itaú — em vez de 4 dígitos. A agência do Itaú tem 4 dígitos sem DV; o backend valida com /^\d{4,4}$/, então valores como "09229-0" eram rejeitados (erro 1503). Correções: - Agência: remove o override do 341; todos os bancos usam o default '9999' (4 dígitos). Quando o banco exige DV na agência (BB, Banrisul, Bradesco, Arbi), o backend calcula o DV a partir dos 4 dígitos. - Conta Caixa (104): 12 -> 9 dígitos, alinhado ao backend (/^\d{4,9}-\d/) e à orientação de não digitar a operação (013/023/001). - Conta BB (1): DV alfanumérico -> numérico (e ALPHANUMERIC_BANK_CODES = []), alinhado ao backend (/-\d/) e ao aviso de UI "substitua X por 0". Testes atualizados para o comportamento correto. Bump 1.2.2 -> 1.2.3. Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 2 +- src/masks/masks.ts | 16 ++++++++++------ tests/masks/maskBankAccount.spec.ts | 8 ++++++-- tests/masks/maskBankBranch.spec.ts | 6 +++--- tests/masks/maskComplete.spec.ts | 10 +++++----- tests/masks/maskHintBankAccount.spec.ts | 2 +- tests/masks/maskValue.spec.ts | 10 +++++----- 7 files changed, 31 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 30c5449..5ae7f17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@useblu/utils", - "version": "1.2.2", + "version": "1.2.3", "private": false, "license": "MIT", "main": "lib/index.js", diff --git a/src/masks/masks.ts b/src/masks/masks.ts index 62e0a93..c7d055f 100644 --- a/src/masks/masks.ts +++ b/src/masks/masks.ts @@ -30,19 +30,23 @@ export const PERCENTAGE_MASK_DEFAULTS: CurrencyMaskOptions = { zeroCents: false, }; -export const BANK_BRANCH_MASKS: Partial> = { - 341: '99999-9', -}; +// Toda agência bancária é informada com 4 dígitos no frontend. Quando o banco +// exige dígito verificador na agência (BB, Banrisul, Bradesco, Arbi), o DV é +// calculado/validado pelo backend a partir dos 4 dígitos — não pelo input. +// Por isso não há override por banco aqui: todos usam DEFAULT_BANK_BRANCH_MASK. +export const BANK_BRANCH_MASKS: Partial> = {}; export const DEFAULT_BANK_BRANCH_MASK = '9999'; -export const ALPHANUMERIC_BANK_CODES: ReadonlyArray = ['1']; +// Nenhum banco usa DV alfanumérico em conta: o backend valida DV apenas +// numérico (/-\d/) e a UI orienta o usuário a trocar "X" por "0" (ex.: BB). +export const ALPHANUMERIC_BANK_CODES: ReadonlyArray = []; export const BANK_ACCOUNT_MASKS: Partial> = { - 1: '99999999-S', + 1: '99999999-9', 33: '99999999-9', 41: '999999999-9', - 104: '999999999999-9', + 104: '999999999-9', 213: '9999999-9', 237: '9999999-9', 341: '99999-9', diff --git a/tests/masks/maskBankAccount.spec.ts b/tests/masks/maskBankAccount.spec.ts index 476b737..6740d63 100644 --- a/tests/masks/maskBankAccount.spec.ts +++ b/tests/masks/maskBankAccount.spec.ts @@ -7,6 +7,10 @@ describe('maskBankAccount', () => { test('formata conta com dígito', () => { expect(maskBankAccount('123456789', '1')).toBe('12345678-9'); }); + + test('DV é numérico: caractere não numérico é removido', () => { + expect(maskBankAccount('12345678X', '1')).toBe('12345678'); + }); }); describe('Santander (33)', () => { @@ -28,8 +32,8 @@ describe('maskBankAccount', () => { }); describe('CEF (104)', () => { - test('formata conta longa com dígito', () => { - expect(maskBankAccount('1234567890123', '104')).toBe('123456789012-3'); + test('formata conta com dígito (9 dígitos, sem operação)', () => { + expect(maskBankAccount('1234567890123', '104')).toBe('123456789-0'); }); }); diff --git a/tests/masks/maskBankBranch.spec.ts b/tests/masks/maskBankBranch.spec.ts index 6dd260f..3d99eff 100644 --- a/tests/masks/maskBankBranch.spec.ts +++ b/tests/masks/maskBankBranch.spec.ts @@ -19,11 +19,11 @@ describe('maskBankBranch', () => { expect(maskBankBranch('1234', '33')).toBe('1234'); }); - test('agência Itaú (341) com dígito verificador', () => { - expect(maskBankBranch('123456', '341')).toBe('12345-6'); + test('agência Itaú (341) tem 4 dígitos sem DV', () => { + expect(maskBankBranch('123456', '341')).toBe('1234'); }); - test('agência Itaú (341) parcial', () => { + test('agência Itaú (341) com 4 dígitos', () => { expect(maskBankBranch('1234', '341')).toBe('1234'); }); diff --git a/tests/masks/maskComplete.spec.ts b/tests/masks/maskComplete.spec.ts index fe2dafe..b30b453 100644 --- a/tests/masks/maskComplete.spec.ts +++ b/tests/masks/maskComplete.spec.ts @@ -59,12 +59,12 @@ describe('maskComplete', () => { expect(maskComplete('12', 'bank_branch')).toBe(false); }); - test('bank_branch completo com compensationCode Itaú (341)', () => { - expect(maskComplete('123456', 'bank_branch', '341')).toBe(true); + test('bank_branch completo com compensationCode Itaú (341) são 4 dígitos', () => { + expect(maskComplete('1234', 'bank_branch', '341')).toBe(true); }); test('bank_branch incompleto com compensationCode Itaú (341)', () => { - expect(maskComplete('1234', 'bank_branch', '341')).toBe(false); + expect(maskComplete('12', 'bank_branch', '341')).toBe(false); }); test('bank_account completo (noop 12+1 dígitos)', () => { @@ -83,8 +83,8 @@ describe('maskComplete', () => { expect(maskComplete('123456789', 'bank_account', { compensationCode: '1' })).toBe(true); }); - test('bank_account completo com DV alfanumérico BB (1)', () => { - expect(maskComplete('12345678X', 'bank_account', { compensationCode: '1' })).toBe(true); + test('bank_account BB (1) com DV não numérico não conta como completo', () => { + expect(maskComplete('12345678X', 'bank_account', { compensationCode: '1' })).toBe(false); }); test('bank_account incompleto com compensationCode BB (1)', () => { diff --git a/tests/masks/maskHintBankAccount.spec.ts b/tests/masks/maskHintBankAccount.spec.ts index 2f657d2..375af11 100644 --- a/tests/masks/maskHintBankAccount.spec.ts +++ b/tests/masks/maskHintBankAccount.spec.ts @@ -16,7 +16,7 @@ describe('maskHintBankAccount', () => { }); test('CEF (104)', () => { - expect(maskHintBankAccount('104')).toBe('000000000000-0'); + expect(maskHintBankAccount('104')).toBe('000000000-0'); }); test('código desconhecido retorna placeholder default (noop)', () => { diff --git a/tests/masks/maskValue.spec.ts b/tests/masks/maskValue.spec.ts index 0efc7e5..5808b84 100644 --- a/tests/masks/maskValue.spec.ts +++ b/tests/masks/maskValue.spec.ts @@ -91,8 +91,8 @@ describe('maskValue', () => { test('strip de caracteres não numéricos', () => { expect(maskValue('12-34', 'bank_branch')).toBe('1234'); }); - test('com compensationCode Itaú (341) usa padrão 99999-9', () => { - expect(maskValue('123456', 'bank_branch', { compensationCode: '341' })).toBe('12345-6'); + test('com compensationCode Itaú (341) usa 4 dígitos (sem DV)', () => { + expect(maskValue('123456', 'bank_branch', { compensationCode: '341' })).toBe('1234'); }); test('com compensationCode desconhecido usa padrão noop', () => { expect(maskValue('1234', 'bank_branch', { compensationCode: '999' })).toBe('1234'); @@ -109,11 +109,11 @@ describe('maskValue', () => { test('strip de separadores antes de aplicar', () => { expect(maskValue('123456789012-3', 'bank_account')).toBe('123456789012-3'); }); - test('com compensationCode BB (1) usa padrão 99999999-S', () => { + test('com compensationCode BB (1) usa padrão 99999999-9', () => { expect(maskValue('123456789', 'bank_account', { compensationCode: '1' })).toBe('12345678-9'); }); - test('com compensationCode BB (1) preserva DV alfanumérico', () => { - expect(maskValue('12345678X', 'bank_account', { compensationCode: '1' })).toBe('12345678-X'); + test('com compensationCode BB (1) usa DV numérico (X é removido)', () => { + expect(maskValue('12345678X', 'bank_account', { compensationCode: '1' })).toBe('12345678'); }); test('com compensationCode Bradesco (237) usa padrão 9999999-9', () => { expect(maskValue('12345678', 'bank_account', { compensationCode: '237' })).toBe('1234567-8'); From 09aca32b11b035915152981556db21db2c9633f3 Mon Sep 17 00:00:00 2001 From: FabioRolin Date: Wed, 17 Jun 2026 11:40:52 -0300 Subject: [PATCH 2/3] =?UTF-8?q?OR-145:=20alinha=20conta=20do=20Arbi=20(213?= =?UTF-8?q?)=20ao=20legado=20(5=20d=C3=ADgitos)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Conta do 213 estava como '9999999-9' (7 dígitos); o legado do mfe-payment-transfer (PR #63) usava '99999-9' (5 dígitos). Alinhado ao legado para paridade exata da migração. Backend do Arbi aceita 4-9 dígitos, então 5 está dentro do limite. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/masks/masks.ts | 2 +- tests/masks/maskBankAccount.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/masks/masks.ts b/src/masks/masks.ts index c7d055f..361ed2a 100644 --- a/src/masks/masks.ts +++ b/src/masks/masks.ts @@ -47,7 +47,7 @@ export const BANK_ACCOUNT_MASKS: Partial> = 33: '99999999-9', 41: '999999999-9', 104: '999999999-9', - 213: '9999999-9', + 213: '99999-9', 237: '9999999-9', 341: '99999-9', 399: '999999-9', diff --git a/tests/masks/maskBankAccount.spec.ts b/tests/masks/maskBankAccount.spec.ts index 6740d63..364585c 100644 --- a/tests/masks/maskBankAccount.spec.ts +++ b/tests/masks/maskBankAccount.spec.ts @@ -55,9 +55,9 @@ describe('maskBankAccount', () => { }); }); - describe('Banco Original (213)', () => { + describe('Arbi (213)', () => { test('formata conta com dígito', () => { - expect(maskBankAccount('12345678', '213')).toBe('1234567-8'); + expect(maskBankAccount('12345678', '213')).toBe('12345-6'); }); }); From 349f0ed43f89b9890aa00753c36662cd4673589f Mon Sep 17 00:00:00 2001 From: FabioRolin Date: Wed, 17 Jun 2026 11:50:57 -0300 Subject: [PATCH 3/3] =?UTF-8?q?OR-145:=20remove=20BANK=5FBRANCH=5FMASKS=20?= =?UTF-8?q?e=20ALPHANUMERIC=5FBANK=5FCODES=20(c=C3=B3digo=20morto)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Após as correções, ambos os mapas ficaram sempre vazios e os lookups viraram no-op. São constantes internas (não exportadas no index), então a remoção não é breaking change. Remover o BANK_BRANCH_MASKS também elimina o footgun que causou esta própria regressão (entrada errada para o 341). - masks.ts: remove os dois e mantém DEFAULT_BANK_BRANCH_MASK. - maskBankBranch: usa sempre DEFAULT_BANK_BRANCH_MASK (param compensationCode mantido por compatibilidade de API). - maskBankAccount: strip sempre numérico. - maskValue / maskComplete: bank_branch usa o default; remove o ramo alfanumérico de conta (ALPHANUMERIC_MASKS do CNPJ permanece). Comportamento e paridade com o legado inalterados. 147/147 testes passam. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/masks/maskBankAccount.ts | 13 +++---------- src/masks/maskBankBranch.ts | 11 ++++++----- src/masks/maskComplete.ts | 16 ++-------------- src/masks/maskValue.ts | 22 +++------------------- src/masks/masks.ts | 14 ++++---------- 5 files changed, 18 insertions(+), 58 deletions(-) diff --git a/src/masks/maskBankAccount.ts b/src/masks/maskBankAccount.ts index c04df47..f416583 100644 --- a/src/masks/maskBankAccount.ts +++ b/src/masks/maskBankAccount.ts @@ -1,10 +1,6 @@ import VMasker from 'vanilla-masker'; -import { - BANK_ACCOUNT_MASKS, - DEFAULT_BANK_ACCOUNT_MASK, - ALPHANUMERIC_BANK_CODES, -} from './masks'; -import { stripAlphanumeric, stripNumeric } from './strip'; +import { BANK_ACCOUNT_MASKS, DEFAULT_BANK_ACCOUNT_MASK } from './masks'; +import { stripNumeric } from './strip'; import type { BankCompensationCode } from './types'; export default function maskBankAccount( @@ -15,9 +11,6 @@ export default function maskBankAccount( const code = compensationCode as BankCompensationCode; const pattern = BANK_ACCOUNT_MASKS[code] ?? DEFAULT_BANK_ACCOUNT_MASK; - const stripped = ALPHANUMERIC_BANK_CODES.includes(code) - ? stripAlphanumeric(String(value)) - : stripNumeric(String(value)); - return VMasker.toPattern(stripped, pattern); + return VMasker.toPattern(stripNumeric(String(value)), pattern); } diff --git a/src/masks/maskBankBranch.ts b/src/masks/maskBankBranch.ts index beb23fc..1d4d95f 100644 --- a/src/masks/maskBankBranch.ts +++ b/src/masks/maskBankBranch.ts @@ -1,17 +1,18 @@ import VMasker from 'vanilla-masker'; -import { BANK_BRANCH_MASKS, DEFAULT_BANK_BRANCH_MASK } from './masks'; +import { DEFAULT_BANK_BRANCH_MASK } from './masks'; import { stripNumeric } from './strip'; -import type { BankCompensationCode } from './types'; export default function maskBankBranch( value: string | null | undefined, + // Mantido por compatibilidade de API e simetria com maskBankAccount: a + // agência usa sempre 4 dígitos, independente do banco (o DV, quando há, é + // calculado pelo backend). + // eslint-disable-next-line @typescript-eslint/no-unused-vars compensationCode?: string, ): string { if (value === null || value === undefined || value === '') return ''; const stripped = stripNumeric(String(value)); - const pattern = BANK_BRANCH_MASKS[compensationCode as BankCompensationCode] - ?? DEFAULT_BANK_BRANCH_MASK; - return VMasker.toPattern(stripped, pattern); + return VMasker.toPattern(stripped, DEFAULT_BANK_BRANCH_MASK); } diff --git a/src/masks/maskComplete.ts b/src/masks/maskComplete.ts index 7024b6e..b67941d 100644 --- a/src/masks/maskComplete.ts +++ b/src/masks/maskComplete.ts @@ -2,9 +2,7 @@ import { MASKS, DEFAULT_BANK_BRANCH_MASK, DEFAULT_BANK_ACCOUNT_MASK, - BANK_BRANCH_MASKS, BANK_ACCOUNT_MASKS, - ALPHANUMERIC_BANK_CODES, } from './masks'; import { stripAlphanumeric, stripNumeric, PATTERN_PLACEHOLDER_REGEX } from './strip'; import type { MaskType, BankCompensationCode } from './types'; @@ -31,13 +29,7 @@ const patternFor = ( case 'barCodeUtilities': return MASKS.BAR_CODE_UTILITIES; case 'darf': return MASKS.DARF; case 'number': return MASKS.NUMBER; - case 'bank_branch': { - if (compensationCode) { - const mapped = BANK_BRANCH_MASKS[compensationCode as BankCompensationCode]; - if (mapped) return mapped; - } - return DEFAULT_BANK_BRANCH_MASK; - } + case 'bank_branch': return DEFAULT_BANK_BRANCH_MASK; case 'bank_account': { if (compensationCode) { const mapped = BANK_ACCOUNT_MASKS[compensationCode as BankCompensationCode]; @@ -76,11 +68,7 @@ export default function maskComplete( const expected = placeholdersInPattern(pattern); - const isAlphanumeric = ALPHANUMERIC_MASKS.includes(type) - || ( - (type === 'bank_account' || type === 'bank_branch') - && ALPHANUMERIC_BANK_CODES.includes(compensationCode as BankCompensationCode) - ); + const isAlphanumeric = ALPHANUMERIC_MASKS.includes(type); const stripped = isAlphanumeric ? stripAlphanumeric(value) diff --git a/src/masks/maskValue.ts b/src/masks/maskValue.ts index cc4e357..d73b96f 100644 --- a/src/masks/maskValue.ts +++ b/src/masks/maskValue.ts @@ -5,9 +5,7 @@ import { PERCENTAGE_MASK_DEFAULTS, DEFAULT_BANK_BRANCH_MASK, DEFAULT_BANK_ACCOUNT_MASK, - BANK_BRANCH_MASKS, BANK_ACCOUNT_MASKS, - ALPHANUMERIC_BANK_CODES, } from './masks'; import { stripAlphanumeric, stripNumeric } from './strip'; import type { MaskType, CurrencyMaskOptions, BankCompensationCode } from './types'; @@ -23,25 +21,11 @@ export interface MaskValueOptions extends CurrencyMaskOptions { const ALPHANUMERIC_MASKS: ReadonlyArray = ['cnpj', 'cpf_cnpj']; -const stripFor = (value: string, type: MaskType, compensationCode?: string): string => { +const stripFor = (value: string, type: MaskType): string => { if (ALPHANUMERIC_MASKS.includes(type)) return stripAlphanumeric(value); - if ( - (type === 'bank_account' || type === 'bank_branch') - && ALPHANUMERIC_BANK_CODES.includes(compensationCode as BankCompensationCode) - ) { - return stripAlphanumeric(value); - } return stripNumeric(value); }; -const bankBranchPattern = (code?: string): string => { - if (code) { - const mapped = BANK_BRANCH_MASKS[code as BankCompensationCode]; - if (mapped) return mapped; - } - return DEFAULT_BANK_BRANCH_MASK; -}; - const bankAccountPattern = (code?: string): string => { if (code) { const mapped = BANK_ACCOUNT_MASKS[code as BankCompensationCode]; @@ -71,7 +55,7 @@ const patternFor = ( case 'barCodeUtilities': return MASKS.BAR_CODE_UTILITIES; case 'darf': return MASKS.DARF; case 'number': return MASKS.NUMBER; - case 'bank_branch': return bankBranchPattern(compensationCode); + case 'bank_branch': return DEFAULT_BANK_BRANCH_MASK; case 'bank_account': return bankAccountPattern(compensationCode); default: return ''; } @@ -94,7 +78,7 @@ export default function maskValue( return VMasker.toMoney(stringValue, { ...PERCENTAGE_MASK_DEFAULTS, ...(options || {}) }); } - const stripped = stripFor(stringValue, type, options?.compensationCode); + const stripped = stripFor(stringValue, type); const pattern = patternFor(type, stripped, options?.compensationCode); if (!pattern) return stringValue; diff --git a/src/masks/masks.ts b/src/masks/masks.ts index 361ed2a..c80b228 100644 --- a/src/masks/masks.ts +++ b/src/masks/masks.ts @@ -30,18 +30,12 @@ export const PERCENTAGE_MASK_DEFAULTS: CurrencyMaskOptions = { zeroCents: false, }; -// Toda agência bancária é informada com 4 dígitos no frontend. Quando o banco -// exige dígito verificador na agência (BB, Banrisul, Bradesco, Arbi), o DV é -// calculado/validado pelo backend a partir dos 4 dígitos — não pelo input. -// Por isso não há override por banco aqui: todos usam DEFAULT_BANK_BRANCH_MASK. -export const BANK_BRANCH_MASKS: Partial> = {}; - +// Toda agência bancária é informada com 4 dígitos no frontend; quando há DV +// na agência (BB, Banrisul, Bradesco, Arbi) ele é calculado/validado pelo +// backend a partir desses 4 dígitos. Por isso a agência não tem máscara por +// banco — todos usam este default. export const DEFAULT_BANK_BRANCH_MASK = '9999'; -// Nenhum banco usa DV alfanumérico em conta: o backend valida DV apenas -// numérico (/-\d/) e a UI orienta o usuário a trocar "X" por "0" (ex.: BB). -export const ALPHANUMERIC_BANK_CODES: ReadonlyArray = []; - export const BANK_ACCOUNT_MASKS: Partial> = { 1: '99999999-9', 33: '99999999-9',