From 8c7bfb406721dd6f4d09ef4c50d4135b410c25db Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 19:05:48 +0000 Subject: [PATCH 1/5] =?UTF-8?q?OR-145:=20corrige=20gaps=20de=20m=C3=A1scar?= =?UTF-8?q?as=20DARF=20e=20banco=20identificados=20na=20migra=C3=A7=C3=A3o?= =?UTF-8?q?=20do=20mfe-bill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Corrige padrão DARF de '99.999.999-9' para '99 9 99 999999-99' (formato original dos MFEs) - Adiciona maskBankBranch() para máscara de agência (padrão 9999, Itaú 99999-9) - Adiciona maskBankAccount() para máscara de conta por código de compensação - Alinha fallback noop do maskHintBankAccount com padrão original (999999999999-9) - Exporta novas funções via index.ts Relates to Pagnet/mfe-core#336 Co-authored-by: FabioRolin Co-Authored-By: fabio.oliveira@useblu.com.br --- src/index.ts | 2 + src/masks/maskBankAccount.ts | 21 ++++++ src/masks/maskBankBranch.ts | 17 +++++ src/masks/maskHintBankAccount.ts | 6 +- src/masks/masks.ts | 10 ++- tests/masks/maskBankAccount.spec.ts | 87 +++++++++++++++++++++++++ tests/masks/maskBankBranch.spec.ts | 49 ++++++++++++++ tests/masks/maskHintBankAccount.spec.ts | 4 +- 8 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 src/masks/maskBankAccount.ts create mode 100644 src/masks/maskBankBranch.ts create mode 100644 tests/masks/maskBankAccount.spec.ts create mode 100644 tests/masks/maskBankBranch.spec.ts diff --git a/src/index.ts b/src/index.ts index ffc6707..395c3f5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,8 @@ export { default as maskCpfOrCnpj } from './masks/maskCpfOrCnpj'; export { default as maskComplete } from './masks/maskComplete'; export { default as setupMultipleMask } from './masks/setupMultipleMask'; export { default as maskHintBankAccount } from './masks/maskHintBankAccount'; +export { default as maskBankBranch } from './masks/maskBankBranch'; +export { default as maskBankAccount } from './masks/maskBankAccount'; export type { MaskType, CurrencyMaskOptions, BankCompensationCode } from './masks/types'; // breakpoints diff --git a/src/masks/maskBankAccount.ts b/src/masks/maskBankAccount.ts new file mode 100644 index 0000000..4ac9a49 --- /dev/null +++ b/src/masks/maskBankAccount.ts @@ -0,0 +1,21 @@ +import VMasker from 'vanilla-masker'; +import { BANK_ACCOUNT_MASKS, DEFAULT_BANK_ACCOUNT_MASK } from './masks'; +import { stripAlphanumeric, stripNumeric } from './strip'; +import type { BankCompensationCode } from './types'; + +const ALPHANUMERIC_CODES: ReadonlyArray = ['1']; + +export default function maskBankAccount( + value: string | null | undefined, + compensationCode?: string, +): string { + if (value === null || value === undefined || value === '') return ''; + + const code = compensationCode as BankCompensationCode; + const pattern = BANK_ACCOUNT_MASKS[code] ?? DEFAULT_BANK_ACCOUNT_MASK; + const stripped = ALPHANUMERIC_CODES.includes(code) + ? stripAlphanumeric(String(value)) + : stripNumeric(String(value)); + + return VMasker.toPattern(stripped, pattern); +} diff --git a/src/masks/maskBankBranch.ts b/src/masks/maskBankBranch.ts new file mode 100644 index 0000000..beb23fc --- /dev/null +++ b/src/masks/maskBankBranch.ts @@ -0,0 +1,17 @@ +import VMasker from 'vanilla-masker'; +import { BANK_BRANCH_MASKS, DEFAULT_BANK_BRANCH_MASK } from './masks'; +import { stripNumeric } from './strip'; +import type { BankCompensationCode } from './types'; + +export default function maskBankBranch( + value: string | null | undefined, + 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); +} diff --git a/src/masks/maskHintBankAccount.ts b/src/masks/maskHintBankAccount.ts index ad81087..3a93afc 100644 --- a/src/masks/maskHintBankAccount.ts +++ b/src/masks/maskHintBankAccount.ts @@ -1,10 +1,10 @@ -import { BANK_ACCOUNT_MASKS } from './masks'; +import { BANK_ACCOUNT_MASKS, DEFAULT_BANK_ACCOUNT_MASK } from './masks'; import type { BankCompensationCode } from './types'; -const DEFAULT_HINT = '0000000000-0'; - const toHint = (pattern: string): string => pattern.replace(/[9S]/g, '0'); +const DEFAULT_HINT = toHint(DEFAULT_BANK_ACCOUNT_MASK); + export default function maskHintBankAccount(compensationCode: string): string { const pattern = BANK_ACCOUNT_MASKS[compensationCode as BankCompensationCode]; diff --git a/src/masks/masks.ts b/src/masks/masks.ts index ec7572d..9e8c11d 100644 --- a/src/masks/masks.ts +++ b/src/masks/masks.ts @@ -9,7 +9,7 @@ export const MASKS = { DATE: '99/99/9999', BAR_CODE: '99999.99999 99999.999999 99999.999999 9 99999999999999', BAR_CODE_UTILITIES: '999999999999 999999999999 999999999999 999999999999', - DARF: '99.999.999-9', + DARF: '99 9 99 999999-99', NUMBER: '999999999999', } as const; @@ -29,6 +29,12 @@ export const PERCENTAGE_MASK_DEFAULTS: CurrencyMaskOptions = { zeroCents: false, }; +export const BANK_BRANCH_MASKS: Partial> = { + 341: '99999-9', +}; + +export const DEFAULT_BANK_BRANCH_MASK = '9999'; + export const BANK_ACCOUNT_MASKS: Record = { 1: '99999999-S', 33: '99999999-9', @@ -40,3 +46,5 @@ export const BANK_ACCOUNT_MASKS: Record = { 399: '999999-9', 745: '9999999-9', }; + +export const DEFAULT_BANK_ACCOUNT_MASK = '999999999999-9'; diff --git a/tests/masks/maskBankAccount.spec.ts b/tests/masks/maskBankAccount.spec.ts new file mode 100644 index 0000000..476b737 --- /dev/null +++ b/tests/masks/maskBankAccount.spec.ts @@ -0,0 +1,87 @@ +import 'jest'; + +import { maskBankAccount } from '../../src'; + +describe('maskBankAccount', () => { + describe('Banco do Brasil (1)', () => { + test('formata conta com dígito', () => { + expect(maskBankAccount('123456789', '1')).toBe('12345678-9'); + }); + }); + + describe('Santander (33)', () => { + test('formata conta com dígito', () => { + expect(maskBankAccount('123456789', '33')).toBe('12345678-9'); + }); + }); + + describe('Bradesco (237)', () => { + test('formata conta com dígito', () => { + expect(maskBankAccount('12345678', '237')).toBe('1234567-8'); + }); + }); + + describe('Itaú (341)', () => { + test('formata conta com dígito', () => { + expect(maskBankAccount('123456', '341')).toBe('12345-6'); + }); + }); + + describe('CEF (104)', () => { + test('formata conta longa com dígito', () => { + expect(maskBankAccount('1234567890123', '104')).toBe('123456789012-3'); + }); + }); + + describe('Banrisul (41)', () => { + test('formata conta com dígito', () => { + expect(maskBankAccount('1234567890', '41')).toBe('123456789-0'); + }); + }); + + describe('HSBC (399)', () => { + test('formata conta com dígito', () => { + expect(maskBankAccount('1234567', '399')).toBe('123456-7'); + }); + }); + + describe('Citibank (745)', () => { + test('formata conta com dígito', () => { + expect(maskBankAccount('12345678', '745')).toBe('1234567-8'); + }); + }); + + describe('Banco Original (213)', () => { + test('formata conta com dígito', () => { + expect(maskBankAccount('12345678', '213')).toBe('1234567-8'); + }); + }); + + describe('noop (banco desconhecido)', () => { + test('usa máscara default (999999999999-9)', () => { + expect(maskBankAccount('1234567890123', '999')).toBe('123456789012-3'); + }); + + test('sem código de compensação usa default', () => { + expect(maskBankAccount('1234567890123')).toBe('123456789012-3'); + }); + }); + + describe('edge cases', () => { + test('null retorna empty string', () => { + expect(maskBankAccount(null, '1')).toBe(''); + }); + + test('undefined retorna empty string', () => { + expect(maskBankAccount(undefined, '1')).toBe(''); + }); + + test('empty string retorna empty string', () => { + expect(maskBankAccount('', '1')).toBe(''); + }); + + test('strip de separadores antes de aplicar', () => { + expect(maskBankAccount('1234-5678-9', '33')).toBe('12345678-9'); + }); + }); +}); diff --git a/tests/masks/maskBankBranch.spec.ts b/tests/masks/maskBankBranch.spec.ts new file mode 100644 index 0000000..def33c4 --- /dev/null +++ b/tests/masks/maskBankBranch.spec.ts @@ -0,0 +1,49 @@ +import 'jest'; + +import { maskBankBranch } from '../../src'; + +describe('maskBankBranch', () => { + test('agência padrão (4 dígitos)', () => { + expect(maskBankBranch('1234')).toBe('1234'); + }); + + test('agência com código de compensação genérico', () => { + expect(maskBankBranch('1234', '1')).toBe('1234'); + }); + + test('agência Banco do Brasil (001)', () => { + expect(maskBankBranch('1234', '1')).toBe('1234'); + }); + + test('agência Santander (033)', () => { + 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) parcial', () => { + expect(maskBankBranch('1234', '341')).toBe('1234'); + }); + + test('strip de caracteres não numéricos', () => { + expect(maskBankBranch('12-34')).toBe('1234'); + }); + + test('banco desconhecido usa padrão noop (9999)', () => { + expect(maskBankBranch('1234', '999')).toBe('1234'); + }); + + test('null retorna empty string', () => { + expect(maskBankBranch(null)).toBe(''); + }); + + test('undefined retorna empty string', () => { + expect(maskBankBranch(undefined)).toBe(''); + }); + + test('empty string retorna empty string', () => { + expect(maskBankBranch('')).toBe(''); + }); +}); diff --git a/tests/masks/maskHintBankAccount.spec.ts b/tests/masks/maskHintBankAccount.spec.ts index 4fc857a..2f657d2 100644 --- a/tests/masks/maskHintBankAccount.spec.ts +++ b/tests/masks/maskHintBankAccount.spec.ts @@ -19,7 +19,7 @@ describe('maskHintBankAccount', () => { expect(maskHintBankAccount('104')).toBe('000000000000-0'); }); - test('código desconhecido retorna placeholder default', () => { - expect(maskHintBankAccount('999')).toBe('0000000000-0'); + test('código desconhecido retorna placeholder default (noop)', () => { + expect(maskHintBankAccount('999')).toBe('000000000000-0'); }); }); From c984c54cbc61ec0a0e6bab8214d74f3b225d1bc5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 19:23:08 +0000 Subject: [PATCH 2/5] OR-145: estende maskValue/maskComplete com suporte a bank_branch e bank_account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona 'bank_branch' e 'bank_account' ao tipo MaskType - maskValue('1234', 'bank_branch') → '1234' (padrão noop) - maskValue('1234567890123', 'bank_account') → '123456789012-3' (padrão noop) - maskComplete suporta validação de completude para os novos tipos - Testes para maskValue e maskComplete cobrindo DARF, bank_branch e bank_account Co-authored-by: FabioRolin Co-Authored-By: fabio.oliveira@useblu.com.br --- src/masks/maskComplete.ts | 4 +++- src/masks/maskValue.ts | 4 +++- src/masks/types.ts | 4 +++- tests/masks/maskComplete.spec.ts | 24 ++++++++++++++++++++++++ tests/masks/maskValue.spec.ts | 27 +++++++++++++++++++++++++++ 5 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/masks/maskComplete.ts b/src/masks/maskComplete.ts index 037d31a..2b5f332 100644 --- a/src/masks/maskComplete.ts +++ b/src/masks/maskComplete.ts @@ -1,4 +1,4 @@ -import { MASKS } from './masks'; +import { MASKS, DEFAULT_BANK_BRANCH_MASK, DEFAULT_BANK_ACCOUNT_MASK } from './masks'; import { stripAlphanumeric, stripNumeric, PATTERN_PLACEHOLDER_REGEX } from './strip'; import type { MaskType } from './types'; @@ -20,6 +20,8 @@ const patternFor = (type: MaskType): string | null => { case 'barCodeUtilities': return MASKS.BAR_CODE_UTILITIES; case 'darf': return MASKS.DARF; case 'number': return MASKS.NUMBER; + case 'bank_branch': return DEFAULT_BANK_BRANCH_MASK; + case 'bank_account': return DEFAULT_BANK_ACCOUNT_MASK; default: return null; } }; diff --git a/src/masks/maskValue.ts b/src/masks/maskValue.ts index 343b73a..a1327e4 100644 --- a/src/masks/maskValue.ts +++ b/src/masks/maskValue.ts @@ -1,5 +1,5 @@ import VMasker from 'vanilla-masker'; -import { MASKS, CURRENCY_MASK_DEFAULTS, PERCENTAGE_MASK_DEFAULTS } from './masks'; +import { MASKS, CURRENCY_MASK_DEFAULTS, PERCENTAGE_MASK_DEFAULTS, DEFAULT_BANK_BRANCH_MASK, DEFAULT_BANK_ACCOUNT_MASK } from './masks'; import { stripAlphanumeric, stripNumeric } from './strip'; import type { MaskType, CurrencyMaskOptions } from './types'; @@ -25,6 +25,8 @@ const patternFor = (type: MaskType, stripped: string): string => { case 'barCodeUtilities': return MASKS.BAR_CODE_UTILITIES; case 'darf': return MASKS.DARF; case 'number': return MASKS.NUMBER; + case 'bank_branch': return DEFAULT_BANK_BRANCH_MASK; + case 'bank_account': return DEFAULT_BANK_ACCOUNT_MASK; default: return ''; } }; diff --git a/src/masks/types.ts b/src/masks/types.ts index 0f006ac..50d9522 100644 --- a/src/masks/types.ts +++ b/src/masks/types.ts @@ -10,7 +10,9 @@ export type MaskType = | 'darf' | 'number' | 'currency' - | 'percentage'; + | 'percentage' + | 'bank_branch' + | 'bank_account'; export interface CurrencyMaskOptions { precision?: number; diff --git a/tests/masks/maskComplete.spec.ts b/tests/masks/maskComplete.spec.ts index 6a4e8bc..caa42f0 100644 --- a/tests/masks/maskComplete.spec.ts +++ b/tests/masks/maskComplete.spec.ts @@ -42,4 +42,28 @@ describe('maskComplete', () => { test('zipcode completo', () => { expect(maskComplete('01310-100', 'zipcode')).toBe(true); }); + + test('bank_branch completo (4 dígitos)', () => { + expect(maskComplete('1234', 'bank_branch')).toBe(true); + }); + + test('bank_branch incompleto', () => { + expect(maskComplete('12', 'bank_branch')).toBe(false); + }); + + test('bank_account completo (noop 12+1 dígitos)', () => { + expect(maskComplete('1234567890123', 'bank_account')).toBe(true); + }); + + test('bank_account incompleto', () => { + expect(maskComplete('12345', 'bank_account')).toBe(false); + }); + + test('darf completo (13 dígitos)', () => { + expect(maskComplete('1234567890123', 'darf')).toBe(true); + }); + + test('darf incompleto', () => { + expect(maskComplete('1234', 'darf')).toBe(false); + }); }); diff --git a/tests/masks/maskValue.spec.ts b/tests/masks/maskValue.spec.ts index fcb7130..2b001b6 100644 --- a/tests/masks/maskValue.spec.ts +++ b/tests/masks/maskValue.spec.ts @@ -66,6 +66,33 @@ describe('maskValue', () => { }); }); + describe('darf', () => { + test('formata número de referência DARF', () => { + expect(maskValue('1234567890123', 'darf')).toBe('12 3 45 678901-23'); + }); + test('strip de separadores antes de aplicar', () => { + expect(maskValue('12 3 45 678901-23', 'darf')).toBe('12 3 45 678901-23'); + }); + }); + + describe('bank_branch', () => { + test('formata agência padrão (4 dígitos)', () => { + expect(maskValue('1234', 'bank_branch')).toBe('1234'); + }); + test('strip de caracteres não numéricos', () => { + expect(maskValue('12-34', 'bank_branch')).toBe('1234'); + }); + }); + + describe('bank_account', () => { + test('formata conta padrão noop (12 dígitos + DV)', () => { + expect(maskValue('1234567890123', 'bank_account')).toBe('123456789012-3'); + }); + test('strip de separadores antes de aplicar', () => { + expect(maskValue('123456789012-3', 'bank_account')).toBe('123456789012-3'); + }); + }); + describe('edge cases', () => { test('null retorna empty string', () => { expect(maskValue(null, 'cpf')).toBe(''); From 8de5c20bc64df5ce84085193ad40fb4473f91a8c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 19:24:48 +0000 Subject: [PATCH 3/5] OR-145: corrige lint object-curly-newline nos imports Co-authored-by: FabioRolin Co-Authored-By: fabio.oliveira@useblu.com.br --- src/masks/maskComplete.ts | 6 +++++- src/masks/maskValue.ts | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/masks/maskComplete.ts b/src/masks/maskComplete.ts index 2b5f332..324d9ac 100644 --- a/src/masks/maskComplete.ts +++ b/src/masks/maskComplete.ts @@ -1,4 +1,8 @@ -import { MASKS, DEFAULT_BANK_BRANCH_MASK, DEFAULT_BANK_ACCOUNT_MASK } from './masks'; +import { + MASKS, + DEFAULT_BANK_BRANCH_MASK, + DEFAULT_BANK_ACCOUNT_MASK, +} from './masks'; import { stripAlphanumeric, stripNumeric, PATTERN_PLACEHOLDER_REGEX } from './strip'; import type { MaskType } from './types'; diff --git a/src/masks/maskValue.ts b/src/masks/maskValue.ts index a1327e4..096addb 100644 --- a/src/masks/maskValue.ts +++ b/src/masks/maskValue.ts @@ -1,5 +1,11 @@ import VMasker from 'vanilla-masker'; -import { MASKS, CURRENCY_MASK_DEFAULTS, PERCENTAGE_MASK_DEFAULTS, DEFAULT_BANK_BRANCH_MASK, DEFAULT_BANK_ACCOUNT_MASK } from './masks'; +import { + MASKS, + CURRENCY_MASK_DEFAULTS, + PERCENTAGE_MASK_DEFAULTS, + DEFAULT_BANK_BRANCH_MASK, + DEFAULT_BANK_ACCOUNT_MASK, +} from './masks'; import { stripAlphanumeric, stripNumeric } from './strip'; import type { MaskType, CurrencyMaskOptions } from './types'; From 16ddcdb4a6b12340db1ca189193aea8a41b5f118 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 19:50:06 +0000 Subject: [PATCH 4/5] =?UTF-8?q?OR-145:=20adiciona=20phone=5Fidd=20e=20comp?= =?UTF-8?q?ensationCode=20din=C3=A2mico=20no=20maskValue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona 'phone_idd' ao MaskType com padrão '+99 (99) 99999-9999' (mfe-pix) - Estende maskValue com options.compensationCode para resolver máscaras bancárias dinâmicas por banco (ex: maskValue('123456', 'bank_branch', { compensationCode: '341' })) - Estende maskComplete com compensationCode para validação de completude - Exporta MaskValueOptions interface para consumo externo - 143 testes passando (15 novos: phone_idd, bank dinâmico com compensationCode) Co-authored-by: FabioRolin Co-Authored-By: fabio.oliveira@useblu.com.br --- src/index.ts | 1 + src/masks/maskComplete.ts | 34 +++++++++++++++++++++++----- src/masks/maskValue.ts | 39 +++++++++++++++++++++++++++----- src/masks/masks.ts | 1 + src/masks/types.ts | 1 + tests/masks/maskComplete.spec.ts | 24 ++++++++++++++++++++ tests/masks/maskValue.spec.ts | 30 ++++++++++++++++++++++++ 7 files changed, 118 insertions(+), 12 deletions(-) diff --git a/src/index.ts b/src/index.ts index 395c3f5..76bc5e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,6 +36,7 @@ export { default as maskHintBankAccount } from './masks/maskHintBankAccount'; export { default as maskBankBranch } from './masks/maskBankBranch'; export { default as maskBankAccount } from './masks/maskBankAccount'; export type { MaskType, CurrencyMaskOptions, BankCompensationCode } from './masks/types'; +export type { MaskValueOptions } from './masks/maskValue'; // breakpoints diff --git a/src/masks/maskComplete.ts b/src/masks/maskComplete.ts index 324d9ac..4a4c62c 100644 --- a/src/masks/maskComplete.ts +++ b/src/masks/maskComplete.ts @@ -2,9 +2,11 @@ import { MASKS, DEFAULT_BANK_BRANCH_MASK, DEFAULT_BANK_ACCOUNT_MASK, + BANK_BRANCH_MASKS, + BANK_ACCOUNT_MASKS, } from './masks'; import { stripAlphanumeric, stripNumeric, PATTERN_PLACEHOLDER_REGEX } from './strip'; -import type { MaskType } from './types'; +import type { MaskType, BankCompensationCode } from './types'; const ALPHANUMERIC_MASKS: ReadonlyArray = ['cnpj', 'cpf_cnpj']; @@ -13,24 +15,44 @@ const placeholdersInPattern = (pattern: string): number => { return matches ? matches.length : 0; }; -const patternFor = (type: MaskType): string | null => { +const patternFor = ( + type: MaskType, + compensationCode?: string, +): string | null => { switch (type) { case 'cpf': return MASKS.CPF; case 'cnpj': return MASKS.CNPJ; case 'phone': return MASKS.PHONE; + case 'phone_idd': return MASKS.PHONE_IDD; case 'zipcode': return MASKS.ZIPCODE; case 'date': return MASKS.DATE; case 'barCode': return MASKS.BAR_CODE; case 'barCodeUtilities': return MASKS.BAR_CODE_UTILITIES; case 'darf': return MASKS.DARF; case 'number': return MASKS.NUMBER; - case 'bank_branch': return DEFAULT_BANK_BRANCH_MASK; - case 'bank_account': return DEFAULT_BANK_ACCOUNT_MASK; + case 'bank_branch': { + if (compensationCode) { + const mapped = BANK_BRANCH_MASKS[compensationCode as BankCompensationCode]; + if (mapped) return mapped; + } + return DEFAULT_BANK_BRANCH_MASK; + } + case 'bank_account': { + if (compensationCode) { + const mapped = BANK_ACCOUNT_MASKS[compensationCode as BankCompensationCode]; + if (mapped) return mapped; + } + return DEFAULT_BANK_ACCOUNT_MASK; + } default: return null; } }; -export default function maskComplete(value: string, type: MaskType): boolean { +export default function maskComplete( + value: string, + type: MaskType, + compensationCode?: string, +): boolean { if (!value) return false; if (type === 'currency' || type === 'percentage') { @@ -42,7 +64,7 @@ export default function maskComplete(value: string, type: MaskType): boolean { return stripped.length === 11 || stripped.length === 14; } - const pattern = patternFor(type); + const pattern = patternFor(type, compensationCode); if (pattern === null) return false; const expected = placeholdersInPattern(pattern); diff --git a/src/masks/maskValue.ts b/src/masks/maskValue.ts index 096addb..947c19c 100644 --- a/src/masks/maskValue.ts +++ b/src/masks/maskValue.ts @@ -5,9 +5,15 @@ import { PERCENTAGE_MASK_DEFAULTS, DEFAULT_BANK_BRANCH_MASK, DEFAULT_BANK_ACCOUNT_MASK, + BANK_BRANCH_MASKS, + BANK_ACCOUNT_MASKS, } from './masks'; import { stripAlphanumeric, stripNumeric } from './strip'; -import type { MaskType, CurrencyMaskOptions } from './types'; +import type { MaskType, CurrencyMaskOptions, BankCompensationCode } from './types'; + +export interface MaskValueOptions extends CurrencyMaskOptions { + compensationCode?: string; +} const ALPHANUMERIC_MASKS: ReadonlyArray = ['cnpj', 'cpf_cnpj']; @@ -15,7 +21,27 @@ const stripFor = (value: string, type: MaskType): string => ( ALPHANUMERIC_MASKS.includes(type) ? stripAlphanumeric(value) : stripNumeric(value) ); -const patternFor = (type: MaskType, stripped: string): string => { +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]; + if (mapped) return mapped; + } + return DEFAULT_BANK_ACCOUNT_MASK; +}; + +const patternFor = ( + type: MaskType, + stripped: string, + compensationCode?: string, +): string => { if (type === 'cpf_cnpj') { if (/[A-Z]/.test(stripped)) return MASKS.CNPJ; return stripped.length <= 11 ? MASKS.CPF : MASKS.CNPJ; @@ -25,14 +51,15 @@ const patternFor = (type: MaskType, stripped: string): string => { case 'cpf': return MASKS.CPF; case 'cnpj': return MASKS.CNPJ; case 'phone': return MASKS.PHONE; + case 'phone_idd': return MASKS.PHONE_IDD; case 'zipcode': return MASKS.ZIPCODE; case 'date': return MASKS.DATE; case 'barCode': return MASKS.BAR_CODE; case 'barCodeUtilities': return MASKS.BAR_CODE_UTILITIES; case 'darf': return MASKS.DARF; case 'number': return MASKS.NUMBER; - case 'bank_branch': return DEFAULT_BANK_BRANCH_MASK; - case 'bank_account': return DEFAULT_BANK_ACCOUNT_MASK; + case 'bank_branch': return bankBranchPattern(compensationCode); + case 'bank_account': return bankAccountPattern(compensationCode); default: return ''; } }; @@ -40,7 +67,7 @@ const patternFor = (type: MaskType, stripped: string): string => { export default function maskValue( value: string | null | undefined, type: MaskType, - options?: CurrencyMaskOptions, + options?: MaskValueOptions, ): string { if (value === null || value === undefined || value === '') return ''; @@ -55,7 +82,7 @@ export default function maskValue( } const stripped = stripFor(stringValue, type); - const pattern = patternFor(type, stripped); + const pattern = patternFor(type, stripped, options?.compensationCode); if (!pattern) return stringValue; diff --git a/src/masks/masks.ts b/src/masks/masks.ts index 9e8c11d..c80ff06 100644 --- a/src/masks/masks.ts +++ b/src/masks/masks.ts @@ -5,6 +5,7 @@ export const MASKS = { CNPJ: 'SS.SSS.SSS/SSSS-99', CPF_CNPJ: '999.999.999-99', PHONE: '(99) 99999-9999', + PHONE_IDD: '+99 (99) 99999-9999', ZIPCODE: '99999-999', DATE: '99/99/9999', BAR_CODE: '99999.99999 99999.999999 99999.999999 9 99999999999999', diff --git a/src/masks/types.ts b/src/masks/types.ts index 50d9522..328eac7 100644 --- a/src/masks/types.ts +++ b/src/masks/types.ts @@ -11,6 +11,7 @@ export type MaskType = | 'number' | 'currency' | 'percentage' + | 'phone_idd' | 'bank_branch' | 'bank_account'; diff --git a/tests/masks/maskComplete.spec.ts b/tests/masks/maskComplete.spec.ts index caa42f0..7e8cbc2 100644 --- a/tests/masks/maskComplete.spec.ts +++ b/tests/masks/maskComplete.spec.ts @@ -43,6 +43,14 @@ describe('maskComplete', () => { expect(maskComplete('01310-100', 'zipcode')).toBe(true); }); + test('phone_idd completo', () => { + expect(maskComplete('+55 (11) 99999-8888', 'phone_idd')).toBe(true); + }); + + test('phone_idd incompleto', () => { + expect(maskComplete('5511', 'phone_idd')).toBe(false); + }); + test('bank_branch completo (4 dígitos)', () => { expect(maskComplete('1234', 'bank_branch')).toBe(true); }); @@ -51,6 +59,14 @@ 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 incompleto com compensationCode Itaú (341)', () => { + expect(maskComplete('1234', 'bank_branch', '341')).toBe(false); + }); + test('bank_account completo (noop 12+1 dígitos)', () => { expect(maskComplete('1234567890123', 'bank_account')).toBe(true); }); @@ -59,6 +75,14 @@ describe('maskComplete', () => { expect(maskComplete('12345', 'bank_account')).toBe(false); }); + test('bank_account completo com compensationCode BB (1)', () => { + expect(maskComplete('123456789', 'bank_account', '1')).toBe(true); + }); + + test('bank_account incompleto com compensationCode BB (1)', () => { + expect(maskComplete('12345', 'bank_account', '1')).toBe(false); + }); + test('darf completo (13 dígitos)', () => { expect(maskComplete('1234567890123', 'darf')).toBe(true); }); diff --git a/tests/masks/maskValue.spec.ts b/tests/masks/maskValue.spec.ts index 2b001b6..28b3e15 100644 --- a/tests/masks/maskValue.spec.ts +++ b/tests/masks/maskValue.spec.ts @@ -75,6 +75,15 @@ describe('maskValue', () => { }); }); + describe('phone_idd', () => { + test('formata telefone com código de país', () => { + expect(maskValue('5511999998888', 'phone_idd')).toBe('+55 (11) 99999-8888'); + }); + test('strip de separadores antes de aplicar', () => { + expect(maskValue('+55 (11) 99999-8888', 'phone_idd')).toBe('+55 (11) 99999-8888'); + }); + }); + describe('bank_branch', () => { test('formata agência padrão (4 dígitos)', () => { expect(maskValue('1234', 'bank_branch')).toBe('1234'); @@ -82,6 +91,15 @@ 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 desconhecido usa padrão noop', () => { + expect(maskValue('1234', 'bank_branch', { compensationCode: '999' })).toBe('1234'); + }); + test('com compensationCode BB (1) usa padrão noop', () => { + expect(maskValue('1234', 'bank_branch', { compensationCode: '1' })).toBe('1234'); + }); }); describe('bank_account', () => { @@ -91,6 +109,18 @@ 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', () => { + expect(maskValue('123456789', 'bank_account', { compensationCode: '1' })).toBe('12345678-9'); + }); + test('com compensationCode Bradesco (237) usa padrão 9999999-9', () => { + expect(maskValue('12345678', 'bank_account', { compensationCode: '237' })).toBe('1234567-8'); + }); + test('com compensationCode Itaú (341) usa padrão 99999-9', () => { + expect(maskValue('123456', 'bank_account', { compensationCode: '341' })).toBe('12345-6'); + }); + test('com compensationCode desconhecido usa padrão noop', () => { + expect(maskValue('1234567890123', 'bank_account', { compensationCode: '999' })).toBe('123456789012-3'); + }); }); describe('edge cases', () => { From f491568a011cf1c9552975596b2adf775575389f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 18:15:39 +0000 Subject: [PATCH 5/5] =?UTF-8?q?OR-145:=20corrige=20strip=20alfanum=C3=A9ri?= =?UTF-8?q?co=20em=20maskValue/maskComplete=20para=20BB,=20padroniza=20API?= =?UTF-8?q?=20e=20tipagem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix: stripFor em maskValue e maskComplete agora considera compensationCode para bancos com DV alfanumérico (BB código 1), alinhando com maskBankAccount - Fix: BANK_ACCOUNT_MASKS alterado para Partial (reflete fallback real) - Refactor: extrai ALPHANUMERIC_BANK_CODES para masks.ts (fonte única) - Refactor: maskComplete aceita options object além de string posicional - Export: MaskCompleteOptions interface - Docs: JSDoc em MaskValueOptions sobre compensationCode - Tests: DV alfanumérico via maskValue e maskComplete, options object syntax - Cosmético: títulos de testes alinhados com códigos reais (001→1, 033→33) Co-authored-by: FabioRolin Co-Authored-By: fabio.oliveira@useblu.com.br --- src/index.ts | 1 + src/masks/maskBankAccount.ts | 10 ++++++---- src/masks/maskComplete.ts | 18 ++++++++++++++++-- src/masks/maskValue.ts | 21 +++++++++++++++++---- src/masks/masks.ts | 4 +++- tests/masks/maskBankBranch.spec.ts | 4 ++-- tests/masks/maskComplete.spec.ts | 10 +++++++++- tests/masks/maskValue.spec.ts | 3 +++ 8 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index 76bc5e5..1a01659 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,6 +37,7 @@ export { default as maskBankBranch } from './masks/maskBankBranch'; export { default as maskBankAccount } from './masks/maskBankAccount'; export type { MaskType, CurrencyMaskOptions, BankCompensationCode } from './masks/types'; export type { MaskValueOptions } from './masks/maskValue'; +export type { MaskCompleteOptions } from './masks/maskComplete'; // breakpoints diff --git a/src/masks/maskBankAccount.ts b/src/masks/maskBankAccount.ts index 4ac9a49..c04df47 100644 --- a/src/masks/maskBankAccount.ts +++ b/src/masks/maskBankAccount.ts @@ -1,10 +1,12 @@ import VMasker from 'vanilla-masker'; -import { BANK_ACCOUNT_MASKS, DEFAULT_BANK_ACCOUNT_MASK } from './masks'; +import { + BANK_ACCOUNT_MASKS, + DEFAULT_BANK_ACCOUNT_MASK, + ALPHANUMERIC_BANK_CODES, +} from './masks'; import { stripAlphanumeric, stripNumeric } from './strip'; import type { BankCompensationCode } from './types'; -const ALPHANUMERIC_CODES: ReadonlyArray = ['1']; - export default function maskBankAccount( value: string | null | undefined, compensationCode?: string, @@ -13,7 +15,7 @@ export default function maskBankAccount( const code = compensationCode as BankCompensationCode; const pattern = BANK_ACCOUNT_MASKS[code] ?? DEFAULT_BANK_ACCOUNT_MASK; - const stripped = ALPHANUMERIC_CODES.includes(code) + const stripped = ALPHANUMERIC_BANK_CODES.includes(code) ? stripAlphanumeric(String(value)) : stripNumeric(String(value)); diff --git a/src/masks/maskComplete.ts b/src/masks/maskComplete.ts index 4a4c62c..7024b6e 100644 --- a/src/masks/maskComplete.ts +++ b/src/masks/maskComplete.ts @@ -4,6 +4,7 @@ import { 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'; @@ -48,13 +49,19 @@ const patternFor = ( } }; +export interface MaskCompleteOptions { + compensationCode?: string; +} + export default function maskComplete( value: string, type: MaskType, - compensationCode?: string, + options?: string | MaskCompleteOptions, ): boolean { if (!value) return false; + const compensationCode = typeof options === 'string' ? options : options?.compensationCode; + if (type === 'currency' || type === 'percentage') { return value.length > 0; } @@ -68,7 +75,14 @@ export default function maskComplete( if (pattern === null) return false; const expected = placeholdersInPattern(pattern); - const stripped = ALPHANUMERIC_MASKS.includes(type) + + const isAlphanumeric = ALPHANUMERIC_MASKS.includes(type) + || ( + (type === 'bank_account' || type === 'bank_branch') + && ALPHANUMERIC_BANK_CODES.includes(compensationCode as BankCompensationCode) + ); + + const stripped = isAlphanumeric ? stripAlphanumeric(value) : stripNumeric(value); diff --git a/src/masks/maskValue.ts b/src/masks/maskValue.ts index 947c19c..cc4e357 100644 --- a/src/masks/maskValue.ts +++ b/src/masks/maskValue.ts @@ -7,19 +7,32 @@ import { 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'; +/** + * Options for `maskValue`. Extends `CurrencyMaskOptions` so that currency/percentage + * formatting options and `compensationCode` (used only by `bank_branch`/`bank_account`) + * can be passed through a single options object. + */ export interface MaskValueOptions extends CurrencyMaskOptions { compensationCode?: string; } const ALPHANUMERIC_MASKS: ReadonlyArray = ['cnpj', 'cpf_cnpj']; -const stripFor = (value: string, type: MaskType): string => ( - ALPHANUMERIC_MASKS.includes(type) ? stripAlphanumeric(value) : stripNumeric(value) -); +const stripFor = (value: string, type: MaskType, compensationCode?: string): 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) { @@ -81,7 +94,7 @@ export default function maskValue( return VMasker.toMoney(stringValue, { ...PERCENTAGE_MASK_DEFAULTS, ...(options || {}) }); } - const stripped = stripFor(stringValue, type); + const stripped = stripFor(stringValue, type, options?.compensationCode); const pattern = patternFor(type, stripped, options?.compensationCode); if (!pattern) return stringValue; diff --git a/src/masks/masks.ts b/src/masks/masks.ts index c80ff06..62e0a93 100644 --- a/src/masks/masks.ts +++ b/src/masks/masks.ts @@ -36,7 +36,9 @@ export const BANK_BRANCH_MASKS: Partial> = export const DEFAULT_BANK_BRANCH_MASK = '9999'; -export const BANK_ACCOUNT_MASKS: Record = { +export const ALPHANUMERIC_BANK_CODES: ReadonlyArray = ['1']; + +export const BANK_ACCOUNT_MASKS: Partial> = { 1: '99999999-S', 33: '99999999-9', 41: '999999999-9', diff --git a/tests/masks/maskBankBranch.spec.ts b/tests/masks/maskBankBranch.spec.ts index def33c4..6dd260f 100644 --- a/tests/masks/maskBankBranch.spec.ts +++ b/tests/masks/maskBankBranch.spec.ts @@ -11,11 +11,11 @@ describe('maskBankBranch', () => { expect(maskBankBranch('1234', '1')).toBe('1234'); }); - test('agência Banco do Brasil (001)', () => { + test('agência Banco do Brasil (1)', () => { expect(maskBankBranch('1234', '1')).toBe('1234'); }); - test('agência Santander (033)', () => { + test('agência Santander (33)', () => { expect(maskBankBranch('1234', '33')).toBe('1234'); }); diff --git a/tests/masks/maskComplete.spec.ts b/tests/masks/maskComplete.spec.ts index 7e8cbc2..fe2dafe 100644 --- a/tests/masks/maskComplete.spec.ts +++ b/tests/masks/maskComplete.spec.ts @@ -75,10 +75,18 @@ describe('maskComplete', () => { expect(maskComplete('12345', 'bank_account')).toBe(false); }); - test('bank_account completo com compensationCode BB (1)', () => { + test('bank_account completo com compensationCode BB (1) — posicional', () => { expect(maskComplete('123456789', 'bank_account', '1')).toBe(true); }); + test('bank_account completo com compensationCode BB (1) — options', () => { + 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 incompleto com compensationCode BB (1)', () => { expect(maskComplete('12345', 'bank_account', '1')).toBe(false); }); diff --git a/tests/masks/maskValue.spec.ts b/tests/masks/maskValue.spec.ts index 28b3e15..0efc7e5 100644 --- a/tests/masks/maskValue.spec.ts +++ b/tests/masks/maskValue.spec.ts @@ -112,6 +112,9 @@ describe('maskValue', () => { test('com compensationCode BB (1) usa padrão 99999999-S', () => { 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 Bradesco (237) usa padrão 9999999-9', () => { expect(maskValue('12345678', 'bank_account', { compensationCode: '237' })).toBe('1234567-8'); });