diff --git a/src/index.ts b/src/index.ts index ffc6707..1a01659 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,11 @@ 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'; +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 new file mode 100644 index 0000000..c04df47 --- /dev/null +++ b/src/masks/maskBankAccount.ts @@ -0,0 +1,23 @@ +import VMasker from 'vanilla-masker'; +import { + BANK_ACCOUNT_MASKS, + DEFAULT_BANK_ACCOUNT_MASK, + ALPHANUMERIC_BANK_CODES, +} from './masks'; +import { stripAlphanumeric, stripNumeric } from './strip'; +import type { BankCompensationCode } from './types'; + +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_BANK_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/maskComplete.ts b/src/masks/maskComplete.ts index 037d31a..7024b6e 100644 --- a/src/masks/maskComplete.ts +++ b/src/masks/maskComplete.ts @@ -1,6 +1,13 @@ -import { MASKS } from './masks'; +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 } from './types'; +import type { MaskType, BankCompensationCode } from './types'; const ALPHANUMERIC_MASKS: ReadonlyArray = ['cnpj', 'cpf_cnpj']; @@ -9,24 +16,52 @@ 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': { + 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 interface MaskCompleteOptions { + compensationCode?: string; +} + +export default function maskComplete( + value: string, + type: MaskType, + 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; } @@ -36,11 +71,18 @@ 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); - 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/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/maskValue.ts b/src/masks/maskValue.ts index 343b73a..cc4e357 100644 --- a/src/masks/maskValue.ts +++ b/src/masks/maskValue.ts @@ -1,15 +1,60 @@ 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, + BANK_BRANCH_MASKS, + BANK_ACCOUNT_MASKS, + ALPHANUMERIC_BANK_CODES, +} from './masks'; import { stripAlphanumeric, stripNumeric } from './strip'; -import type { MaskType, CurrencyMaskOptions } from './types'; +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 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; @@ -19,12 +64,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 bankBranchPattern(compensationCode); + case 'bank_account': return bankAccountPattern(compensationCode); default: return ''; } }; @@ -32,7 +80,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 ''; @@ -46,8 +94,8 @@ export default function maskValue( return VMasker.toMoney(stringValue, { ...PERCENTAGE_MASK_DEFAULTS, ...(options || {}) }); } - const stripped = stripFor(stringValue, type); - const pattern = patternFor(type, stripped); + 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 ec7572d..62e0a93 100644 --- a/src/masks/masks.ts +++ b/src/masks/masks.ts @@ -5,11 +5,12 @@ 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', BAR_CODE_UTILITIES: '999999999999 999999999999 999999999999 999999999999', - DARF: '99.999.999-9', + DARF: '99 9 99 999999-99', NUMBER: '999999999999', } as const; @@ -29,7 +30,15 @@ export const PERCENTAGE_MASK_DEFAULTS: CurrencyMaskOptions = { zeroCents: false, }; -export const BANK_ACCOUNT_MASKS: Record = { +export const BANK_BRANCH_MASKS: Partial> = { + 341: '99999-9', +}; + +export const DEFAULT_BANK_BRANCH_MASK = '9999'; + +export const ALPHANUMERIC_BANK_CODES: ReadonlyArray = ['1']; + +export const BANK_ACCOUNT_MASKS: Partial> = { 1: '99999999-S', 33: '99999999-9', 41: '999999999-9', @@ -40,3 +49,5 @@ export const BANK_ACCOUNT_MASKS: Record = { 399: '999999-9', 745: '9999999-9', }; + +export const DEFAULT_BANK_ACCOUNT_MASK = '999999999999-9'; diff --git a/src/masks/types.ts b/src/masks/types.ts index 0f006ac..328eac7 100644 --- a/src/masks/types.ts +++ b/src/masks/types.ts @@ -10,7 +10,10 @@ export type MaskType = | 'darf' | 'number' | 'currency' - | 'percentage'; + | 'percentage' + | 'phone_idd' + | 'bank_branch' + | 'bank_account'; export interface CurrencyMaskOptions { precision?: number; 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..6dd260f --- /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 (1)', () => { + expect(maskBankBranch('1234', '1')).toBe('1234'); + }); + + test('agência Santander (33)', () => { + 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/maskComplete.spec.ts b/tests/masks/maskComplete.spec.ts index 6a4e8bc..fe2dafe 100644 --- a/tests/masks/maskComplete.spec.ts +++ b/tests/masks/maskComplete.spec.ts @@ -42,4 +42,60 @@ describe('maskComplete', () => { test('zipcode completo', () => { 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); + }); + + test('bank_branch incompleto', () => { + 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); + }); + + test('bank_account incompleto', () => { + expect(maskComplete('12345', 'bank_account')).toBe(false); + }); + + 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); + }); + + 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/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'); }); }); diff --git a/tests/masks/maskValue.spec.ts b/tests/masks/maskValue.spec.ts index fcb7130..0efc7e5 100644 --- a/tests/masks/maskValue.spec.ts +++ b/tests/masks/maskValue.spec.ts @@ -66,6 +66,66 @@ 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('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'); + }); + 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', () => { + 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'); + }); + 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'); + }); + 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', () => { test('null retorna empty string', () => { expect(maskValue(null, 'cpf')).toBe('');