diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 016765a..2b3dd21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,9 +6,14 @@ on: pull_request: branches: [ main ] +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest + timeout-minutes: 10 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 4043c45..43c8695 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -4,9 +4,14 @@ on: pull_request: branches: [ main ] +concurrency: + group: e2e-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - name: Checkout repository diff --git a/README.en.md b/README.en.md index dbf0443..3c64fc6 100644 --- a/README.en.md +++ b/README.en.md @@ -42,11 +42,11 @@ pnpm add validations-br ## useValidationsBR -### Parâmetros +### Parameters | Key | Type | Description | | :---- | :--------------------------------------------------: | ---------------------------: | -| type | `cnpj \| cpf \| cep \| email \| pis \| phone \| uf` | Type of data to be validated | +| type | `cnpj \| cpf \| cnh \| cep \| email \| pis \| phone \| uf` | Type of data to be validated | | value | `string` | Value that will be validated | ```js @@ -67,6 +67,13 @@ const cnpj = '09.015.844/0001-80'; const isValid = validateCNPJ(cnpj); ``` +The new **alphanumeric** CNPJ format from Receita Federal is also supported +(12 uppercase alphanumeric positions + 2 numeric check digits): + +```js +validateCNPJ('12.ABC.345/01DE-35'); // true +``` + ## Validate CPF ```js @@ -107,7 +114,7 @@ const phone = '(14) 99767-9472'; const isValid = validatePhone(phone); ``` -## Validar CNH +## Validate CNH ```js import { validateCNH } from 'validations-br'; @@ -120,11 +127,11 @@ const isValid = validateCNH(cnh); ## Validate CEP ```js -import { validateCEP } from 'validations-br'; -// const { validateCEP } = require('validations-br'); +import { validateCep } from 'validations-br'; +// const { validateCep } = require('validations-br'); const cep = '17280-000'; -const isValid = validateCEP(cep); +const isValid = validateCep(cep); ``` ## Validate UF diff --git a/README.md b/README.md index 705cac3..5082c52 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ pnpm add validations-br | Chave | Tipo | Descrição | | :---- | :--------------------------------------------------: | --------------------------------: | -| type | `cnpj \| cpf \| cep \| email \| pis \| phone \| uf` | Tipo de dados que serão validados | +| type | `cnpj \| cpf \| cnh \| cep \| email \| pis \| phone \| uf` | Tipo de dados que serão validados | | value | `string` | Valor que será validado | ```js @@ -68,6 +68,13 @@ const cnpj = '09.015.844/0001-80'; const isValid = validateCNPJ(cnpj); ``` +Também há suporte ao novo formato **alfanumérico** de CNPJ da Receita Federal +(12 posições alfanuméricas em maiúsculas + 2 dígitos verificadores numéricos): + +```js +validateCNPJ('12.ABC.345/01DE-35'); // true +``` + ## Validar CPF ```js @@ -121,11 +128,11 @@ const isValid = validatePhone(phone); ## Validar CEP ```js -import { validateCEP } from 'validations-br'; -// const { validateCEP } = require('validations-br'); +import { validateCep } from 'validations-br'; +// const { validateCep } = require('validations-br'); const cep = '17280-000'; -const isValid = validateCEP(cep); +const isValid = validateCep(cep); ``` ## Validar UF diff --git a/package.json b/package.json index c9d6a68..55b70eb 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,31 @@ "name": "validations-br", "version": "1.6.1", "packageManager": "pnpm@11.9.0", - "description": "A validator to BR informations", + "description": "Validators for Brazilian documents (CPF, CNPJ, CNH, PIS, CEP, IE, phone, UF, email)", "main": "lib/index.cjs", "module": "lib/index.mjs", - "types": "lib/index.d.ts", - "repository": "https://github.com/reactivando/validations-br.git", + "types": "lib/index.d.cts", + "exports": { + ".": { + "import": { + "types": "./lib/index.d.mts", + "default": "./lib/index.mjs" + }, + "require": { + "types": "./lib/index.d.cts", + "default": "./lib/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/reactivando/validations-br.git" + }, "homepage": "https://github.com/reactivando/validations-br#readme", + "bugs": { + "url": "https://github.com/reactivando/validations-br/issues" + }, "author": "Reactivando ", "keywords": [ "BR", @@ -17,6 +36,12 @@ "license": "MIT", "type": "module", "sideEffects": false, + "engines": { + "node": ">=18" + }, + "publishConfig": { + "provenance": true + }, "files": [ "lib/**/*" ], diff --git a/src/__tests__/usevalidationsbr.spec.ts b/src/__tests__/usevalidationsbr.spec.ts index 6381423..06f3c23 100644 --- a/src/__tests__/usevalidationsbr.spec.ts +++ b/src/__tests__/usevalidationsbr.spec.ts @@ -117,7 +117,7 @@ describe('UseValidationBr Pis', () => { }); }); -describe('UseValidationBr Pis', () => { +describe('UseValidationBr UF', () => { it('should be able return true to valid UF', () => { expect(useValidationsBR('uf', 'SP')).toBe(true); }); @@ -126,3 +126,16 @@ describe('UseValidationBr Pis', () => { expect(useValidationsBR('uf', 'AA')).toBe(false); }); }); + +describe('UseValidationBr CNH', () => { + it('should be able return true to valid CNH', () => { + expect(useValidationsBR('cnh', '134621966-24')).toBe(true); + }); + + it('should be able return false to invalid CNH', () => { + expect.assertions(2); + + expect(useValidationsBR('cnh', '134621966-25')).toBe(false); + expect(useValidationsBR('cnh', '11111111111')).toBe(false); + }); +}); diff --git a/src/__tests__/validateCNH.spec.ts b/src/__tests__/validateCNH.spec.ts index 107a399..657d037 100644 --- a/src/__tests__/validateCNH.spec.ts +++ b/src/__tests__/validateCNH.spec.ts @@ -4,6 +4,13 @@ import { validateCNH } from '../validations/validateCNH'; describe('validateCNH', () => { it('should return true for valid CNHs', () => { expect(validateCNH('13462196624')).toBe(true); + // 64644399109 exercises the exceptional check-digit branches + // (dv1 >= 10 -> dsc=2, dv2 >= 10 -> 0, dv2 < 0 -> +11); the others are + // additional valid samples. + expect(validateCNH('64644399109')).toBe(true); + expect(validateCNH('41835138958')).toBe(true); + expect(validateCNH('79886050534')).toBe(true); + expect(validateCNH('58879358643')).toBe(true); }); it('should return false for invalid CNHs', () => { diff --git a/src/__tests__/validateEmail.spec.ts b/src/__tests__/validateEmail.spec.ts index 8a0e04d..ca70204 100644 --- a/src/__tests__/validateEmail.spec.ts +++ b/src/__tests__/validateEmail.spec.ts @@ -7,6 +7,13 @@ describe('validateEmail', () => { expect(validateEmail('email@email.co.uk')).toBe(true); expect(validateEmail('email.test@email.com')).toBe(true); expect(validateEmail('email_test@email.com')).toBe(true); + expect(validateEmail('user@domain.photography')).toBe(true); + expect(validateEmail('user@domain.international')).toBe(true); + }); + + it('should return false for non-string inputs', () => { + expect(validateEmail(null as unknown as string)).toBe(false); + expect(validateEmail(undefined as unknown as string)).toBe(false); }); it('should return false for invalid emails', () => { diff --git a/src/__tests__/validateIE.spec.ts b/src/__tests__/validateIE.spec.ts index 03a603d..0081fdc 100644 --- a/src/__tests__/validateIE.spec.ts +++ b/src/__tests__/validateIE.spec.ts @@ -223,4 +223,41 @@ describe('validateIE', () => { expect(validateIE('2703591093-88', 'to')).toBe(false); }); }); + + describe('edge cases and check-digit clamp branches', () => { + // For these states a check digit of 0 can only result from the + // "remainder produced a digit >= 10, clamp to 0" branch, so these + // valid IEs exercise that otherwise-untested path. + it('should validate IEs whose check digit is clamped to 0', () => { + expect(validateIE('46665520', 'rj')).toBe(true); + expect(validateIE('7612174970', 'rs')).toBe(true); + expect(validateIE('11182758180', 'mt')).toBe(true); + expect(validateIE('1915451760', 'pr')).toBe(true); + expect(validateIE('8211755960110', 'mg')).toBe(true); + expect(validateIE('234682540', 'pe')).toBe(true); + expect(validateIE('565343050', 'al')).toBe(true); + expect(validateIE('100060030', 'go')).toBe(true); + expect(validateIE('20809775296850', 'ro')).toBe(true); + }); + + it('should validate alternative IE formats', () => { + expect(validateIE('2087728666', 'rn')).toBe(true); // 10-digit RN + expect(validateIE('2019640180', 'rn')).toBe(true); // 10-digit RN, DV clamped to 0 + expect(validateIE('577972880', 'to')).toBe(true); // 9-digit TO + }); + + it('should validate IEs that hit special-range / low-sum branches', () => { + expect(validateIE('000100102', 'am')).toBe(true); // AM weighted sum < 11 + expect(validateIE('030098594', 'ap')).toBe(true); // AP body range 3.000.001-3.017.000 + expect(validateIE('030176168', 'ap')).toBe(true); // AP body range 3.017.001-3.019.022 + expect(validateIE('101182660', 'go')).toBe(true); // GO body range 10.103.105-10.119.997 + }); + + it('should reject IEs with invalid prefixes or lengths', () => { + expect(validateIE('0918614000103', 'df')).toBe(false); // prefix not 07/08 + expect(validateIE('1234567', 'ba')).toBe(false); // wrong length + expect(validateIE('04035910938', 'to')).toBe(false); // invalid TO prefix + expect(validateIE('12345', 'ap')).toBe(false); // wrong length + }); + }); }); diff --git a/src/__tests__/validatePhone.spec.ts b/src/__tests__/validatePhone.spec.ts index 78a7613..168aa14 100644 --- a/src/__tests__/validatePhone.spec.ts +++ b/src/__tests__/validatePhone.spec.ts @@ -22,4 +22,9 @@ describe('validatePhone', () => { expect(validatePhone('11111111111')).toBe(false); expect(validatePhone('22222222')).toBe(false); }); + + it('should return false for non-string inputs instead of throwing', () => { + expect(validatePhone(null as unknown as string)).toBe(false); + expect(validatePhone(undefined as unknown as string)).toBe(false); + }); }); diff --git a/src/validations/ie/states/sp.ts b/src/validations/ie/states/sp.ts index b4c899d..070e62d 100644 --- a/src/validations/ie/states/sp.ts +++ b/src/validations/ie/states/sp.ts @@ -11,9 +11,8 @@ function calcFirstDigit(body: string): number { for (let i = 0; i < body.length; i++) { sum += Number.parseInt(body.charAt(i), 10) * weight[i]; } - const dig = sum % 11; - const digit = dig.toString(); - return Number.parseInt(digit.substring(digit.length - 1), 10); + // sum % 11 is 0-10; the SP rule uses its last digit (10 -> 0). + return (sum % 11) % 10; } /** @@ -33,9 +32,8 @@ function calcSecondDigit(body: string): number { weight = 10; } } - const dig = sum % 11; - const digit = dig.toString(); - return Number.parseInt(digit.substring(digit.length - 1), 10); + // sum % 11 is 0-10; the SP rule uses its last digit (10 -> 0). + return (sum % 11) % 10; } /** diff --git a/src/validations/utils.ts b/src/validations/utils.ts index e6e89fd..336c7fc 100644 --- a/src/validations/utils.ts +++ b/src/validations/utils.ts @@ -4,9 +4,13 @@ * characters. * @returns a boolean value. */ -export const isRepeated = (ref: string) => { - const ret = ref.replace(new RegExp(ref[0], 'g'), '').trim().length === 0; - return ret; +export const isRepeated = (ref: string): boolean => { + for (let i = 1; i < ref.length; i++) { + if (ref[i] !== ref[0]) { + return false; + } + } + return true; }; /** diff --git a/src/validations/validateCNH.ts b/src/validations/validateCNH.ts index 15e0ca5..2e7738f 100644 --- a/src/validations/validateCNH.ts +++ b/src/validations/validateCNH.ts @@ -7,7 +7,7 @@ import { isRepeated } from './utils'; * @returns The function `validateCNH` returns a boolean value, indicating whether the provided CNH is * valid or not. */ -export function validateCNH(value: string) { +export function validateCNH(value: string): boolean { const cnh = String(value).replace(/\D/g, ''); if (cnh.length !== 11 || isRepeated(cnh)) { diff --git a/src/validations/validateCNPJ.ts b/src/validations/validateCNPJ.ts index 8651238..4f2ef48 100644 --- a/src/validations/validateCNPJ.ts +++ b/src/validations/validateCNPJ.ts @@ -29,9 +29,10 @@ function charToNumber(char: string): number { * @returns The calculated check digit (0-9). */ function generateChecksum(base: string, weights: number[]): number { - const sum = base - .split('') - .reduce((acc, char, i) => acc + charToNumber(char) * weights[i], 0); + let sum = 0; + for (let i = 0; i < base.length; i++) { + sum += charToNumber(base[i]) * weights[i]; + } const remainder = sum % 11; return remainder < 2 ? 0 : 11 - remainder; diff --git a/src/validations/validateCPF.ts b/src/validations/validateCPF.ts index d7a0844..2e1062c 100644 --- a/src/validations/validateCPF.ts +++ b/src/validations/validateCPF.ts @@ -15,7 +15,7 @@ export function validateCPF(value: string): boolean { return false; } const valWithoutDvs = clearValue.substring(0, clearValue.length - 2); - if (!clearValue || isRepeated(clearValue)) { + if (isRepeated(clearValue)) { return false; } const dv1 = mod11(valWithoutDvs, 12); diff --git a/src/validations/validateEmail.ts b/src/validations/validateEmail.ts index 75dba5d..f7a7026 100644 --- a/src/validations/validateEmail.ts +++ b/src/validations/validateEmail.ts @@ -7,7 +7,7 @@ * valid, and `false` otherwise. */ const emailRegex = - /^[\w!#$%&'*+/=?`{|}~^-]+(?:\.[\w!#$%&'*+/=?`{|}~^-]+)*@(?:[A-Z0-9-]+\.)+[A-Z]{2,10}$/i; + /^[\w!#$%&'*+/=?`{|}~^-]+(?:\.[\w!#$%&'*+/=?`{|}~^-]+)*@(?:[A-Z0-9-]+\.)+[A-Z]{2,24}$/i; export function validateEmail(value: string): boolean { return emailRegex.test(value); diff --git a/src/validations/validatePIS.ts b/src/validations/validatePIS.ts index fb1b859..f5ef478 100644 --- a/src/validations/validatePIS.ts +++ b/src/validations/validatePIS.ts @@ -12,13 +12,14 @@ import { isRepeated } from './utils'; function generateChecksum(base: string | number, weight: number[]): number { const digits = String(base).replace(/[^\d]/g, ''); - return digits - .split('') - .reduce((acc, digit, i) => acc + +digit * weight[i], 0); + let sum = 0; + for (let i = 0; i < digits.length; i++) { + sum += +digits[i] * weight[i]; + } + return sum; } const weights = [3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; -const onlyDigitsRegex = /^[0-9]+$/; /** * The function `validatePIS` validates a PIS number, which is a Brazilian social identification @@ -31,11 +32,7 @@ const onlyDigitsRegex = /^[0-9]+$/; export function validatePIS(pis: string): boolean { const pisStr = String(pis).replace(/\D/g, ''); - if ( - pisStr.length !== 11 || - isRepeated(pisStr) || - !onlyDigitsRegex.test(pisStr) - ) { + if (pisStr.length !== 11 || isRepeated(pisStr)) { return false; } diff --git a/src/validations/validatePhone.ts b/src/validations/validatePhone.ts index 8eb98c9..5f71292 100644 --- a/src/validations/validatePhone.ts +++ b/src/validations/validatePhone.ts @@ -8,20 +8,20 @@ import { isRepeated } from './utils'; * is valid, and `false` otherwise. */ export function validatePhone(phone: string): boolean { - const clearPhone = phone.replace(/\D/g, ''); + const clearPhone = String(phone).replace(/\D/g, ''); if (isRepeated(clearPhone)) { return false; } if (!(clearPhone.length >= 8 && clearPhone.length <= 11)) { return false; } - if (clearPhone.length > 9 && [0, 1].indexOf(clearPhone.indexOf('0')) !== -1) { + if (clearPhone.length > 9 && [0, 1].includes(clearPhone.indexOf('0'))) { return false; } const shortNumber = clearPhone.length > 9 ? clearPhone.substring(2) : clearPhone; if (shortNumber.length === 8) { - return [2, 3, 4, 5, 6, 7, 8].indexOf(+shortNumber[0]) !== -1; + return [2, 3, 4, 5, 6, 7, 8].includes(+shortNumber[0]); } return shortNumber[0] === '9'; } diff --git a/tsconfig.json b/tsconfig.json index c7557d6..d4a95a7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,8 +2,10 @@ "compilerOptions": { "target": "es2016", "lib": ["es2016", "dom"], - "module": "commonjs", + "module": "ESNext", + "moduleResolution": "bundler", "declaration": true, + "isolatedDeclarations": true, "outDir": "./lib", "strict": true, "esModuleInterop": true diff --git a/tsdown.config.ts b/tsdown.config.ts index 6b829d5..7bf6860 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -6,4 +6,5 @@ export default defineConfig({ format: ['esm', 'cjs'], dts: true, unbundle: true, + clean: true, }); diff --git a/validations-br-1.6.1.tgz b/validations-br-1.6.1.tgz new file mode 100644 index 0000000..0c85802 Binary files /dev/null and b/validations-br-1.6.1.tgz differ diff --git a/vitest.config.ts b/vitest.config.ts index 6513ea5..339ee8c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,6 +13,13 @@ export default defineConfig({ 'src/validations/ie/index.ts', 'src/index.ts', ], + // Floor to keep CI from silently regressing; raise as gaps close. + thresholds: { + statements: 97, + branches: 90, + functions: 100, + lines: 97, + }, }, }, });