From 09408b11c73ec914d194d2cc56c360bd699299a7 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:45 -0300 Subject: [PATCH 01/75] feat(cnae,ncm): add the pad option to formatCnae and formatNcm --- src/format-cnae/format-cnae.test.ts | 73 ++++++++++++++++++++++++++--- src/format-cnae/format-cnae.ts | 55 ++++++++++++++++++---- src/format-ncm/format-ncm.test.ts | 63 +++++++++++++++++++++++-- src/format-ncm/format-ncm.ts | 56 ++++++++++++++++++---- 4 files changed, 219 insertions(+), 28 deletions(-) diff --git a/src/format-cnae/format-cnae.test.ts b/src/format-cnae/format-cnae.test.ts index 4e2b5d50..e5b6f353 100644 --- a/src/format-cnae/format-cnae.test.ts +++ b/src/format-cnae/format-cnae.test.ts @@ -1,11 +1,11 @@ -import { anyGarbage, digits } from "../_internals/test/arbitraries"; +import { anyGarbage, digits, digitsUpTo } from "../_internals/test/arbitraries"; import { expectIdempotent, expectMatchesPattern, expectNeverThrows, } from "../_internals/test/properties"; import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; -import { formatCnae } from "./format-cnae"; +import { formatCnae, type FormatCnaeOptions } from "./format-cnae"; describe("formatCnae", () => { it("should format a CNAE code given as digits", () => { @@ -28,9 +28,43 @@ describe("formatCnae", () => { expect(formatCnae("")).toBe(""); }); - it("should left pad a short code with zeros up to the full CNAE length", () => { - expect(formatCnae("1")).toBe("0000-0/01"); - expect(formatCnae("501")).toBe("0000-5/01"); + it("should mask a partial value progressively by default", () => { + expect(formatCnae("6")).toBe("6"); + expect(formatCnae("62")).toBe("62"); + expect(formatCnae("620")).toBe("620"); + expect(formatCnae("6201")).toBe("6201"); + expect(formatCnae("62015")).toBe("6201-5"); + expect(formatCnae("620150")).toBe("6201-5/0"); + expect(formatCnae("6201501")).toBe("6201-5/01"); + }); + + it("should mask a partial number progressively by default", () => { + expect(formatCnae(62)).toBe("62"); + expect(formatCnae(111_301)).toBe("1113-0/1"); + }); + + it("should not add digits after the CNAE length", () => { + expect(formatCnae("62015010000")).toBe("6201-5/01"); + }); + + describe("pad option", () => { + it("should left pad a short code with zeros up to the full CNAE length", () => { + expect(formatCnae("", { pad: true })).toBe("0000-0/00"); + expect(formatCnae("1", { pad: true })).toBe("0000-0/01"); + expect(formatCnae("62", { pad: true })).toBe("0000-0/62"); + expect(formatCnae("501", { pad: true })).toBe("0000-5/01"); + expect(formatCnae("62015", { pad: true })).toBe("0062-0/15"); + expect(formatCnae("6201501", { pad: true })).toBe("6201-5/01"); + }); + + it("should left pad a number the same way as its digits", () => { + expect(formatCnae(62, { pad: true })).toBe("0000-0/62"); + expect(formatCnae(111_301, { pad: true })).toBe("0111-3/01"); + }); + + it("should mask progressively for an explicit false", () => { + expect(formatCnae("62", { pad: false })).toBe("62"); + }); }); it("should return an empty string for null and undefined", () => { @@ -40,6 +74,20 @@ describe("formatCnae", () => { expect(formatCnae()).toBe(""); }); + it("should return an empty string for a value that is not digits and mask characters", () => { + expect(formatCnae("abc6201501")).toBe(""); + }); + + it("should return an empty string for a number that is not a non-negative safe integer", () => { + expect(formatCnae(-6_201_501)).toBe(""); + expect(formatCnae(620_150.1)).toBe(""); + expect(formatCnae(2 ** 53)).toBe(""); + }); + + it("should return an empty string for a null-prototype object", () => { + expect(formatCnae(Object.create(null))).toBe(""); + }); + describe("properties", () => { const sevenDigitArbitrary = digits(7); @@ -51,6 +99,14 @@ describe("formatCnae", () => { expectMatchesPattern(formatCnae, /^\d{4}-\d\/\d{2}$/, sevenDigitArbitrary); }); + test("should format every shorter value in the NNNN-N/NN pattern when padding", () => { + expectMatchesPattern( + (value) => formatCnae(value, { pad: true }), + /^\d{4}-\d\/\d{2}$/, + digitsUpTo(7), + ); + }); + test("should be idempotent on a full 7 digit code", () => { expectIdempotent(formatCnae, sevenDigitArbitrary); }); @@ -58,8 +114,13 @@ describe("formatCnae", () => { }); describe("formatCnae types", () => { - test("should take a string or number and return a string", () => { + test("should take a string or number value and options and return a string", () => { expectTypeOf(formatCnae).parameter(0).toEqualTypeOf(); + expectTypeOf(formatCnae).parameter(1).toEqualTypeOf(); expectTypeOf(formatCnae).returns.toEqualTypeOf(); }); + + test("should type the pad option as an optional boolean", () => { + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/src/format-cnae/format-cnae.ts b/src/format-cnae/format-cnae.ts index 989348a6..11b55bb3 100644 --- a/src/format-cnae/format-cnae.ts +++ b/src/format-cnae/format-cnae.ts @@ -1,14 +1,41 @@ import { format } from "../_internals/format/format"; -import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +/** + * Shape a value may be written in while a CNAE code is being typed: digits and the mask + * characters of the `NNNN-N/NN` presentation, nothing else. + */ +const CNAE_MASK_REGEX = /^[\d\s.\-/]*$/; + +/** Options of `formatCnae`. */ +export type FormatCnaeOptions = { + /** Whether to left pad the value with zeros up to the 7 digits of a complete subclass code (default: `false`). */ + pad?: boolean; +}; + /** * Formats a CNAE (Classificação Nacional de Atividades Econômicas) subclass code. * * This is a purely structural transformation, it does not check the code against the * official table, use `isValidCnae` for that. * + * With the default `pad: false` the mask is applied progressively, as far as the value goes, + * which is what an input being typed into needs (`"62"` stays `"62"`, `"62015"` becomes + * `"6201-5"`). With `pad: true` the value is first left padded with zeros to the 7 digits of a + * complete subclass code, so it always comes back fully masked (`"62"` gives `"0000-0/62"`). + * A number is treated exactly like the string of its digits: it is only padded under + * `pad: true`, so `formatCnae(111301)` gives `"1113-0/1"` and `formatCnae(111301, { pad: true })` + * gives `"0111-3/01"`. + * + * A string is only formatted when it holds nothing but digits and the mask characters; + * anything else (`"abc6201501"`) gives `""` instead of having its digits picked out. A number + * is only formatted when it is a non-negative safe integer, since a sign, a decimal point or a + * rounded magnitude would otherwise be read as a code the caller never wrote. + * * @param {string|number} value - The CNAE code to be formatted. + * @param {FormatCnaeOptions} [options] - Optional formatting options. + * @param {boolean} [options.pad] - Whether to pad the value with leading zeros. Defaults to `false`. * @returns {string} The formatted code in the `NNNN-N/NN` pattern, or an empty string * when there is nothing to format. * @@ -16,15 +43,25 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * ```typescript * formatCnae("6201501"); // "6201-5/01" * formatCnae(6201501); // "6201-5/01" + * formatCnae("62"); // "62" (partial values are masked as far as they go) + * formatCnae("62015"); // "6201-5" + * formatCnae("62", { pad: true }); // "0000-0/62" (padded to 7 digits first) + * formatCnae("abc6201501"); // "" (not a documented form) + * formatCnae(-6201501); // "" (not a non-negative safe integer) * ``` * * @see Official: https://servicodados.ibge.gov.br/api/v2/cnae/subclasses */ -export const formatCnae = (value: string | number): string => - isNullish(value) || value === "" - ? "" - : format({ - value: sanitizeToDigits(value), - pattern: "0000-0/00", - pad: true, - }); +export const formatCnae = (value: string | number, options?: FormatCnaeOptions): string => { + if (!isLookupCode(value)) return ""; + + const code = String(value); + + if (!CNAE_MASK_REGEX.test(code)) return ""; + + return format({ + pad: options?.pad, + value: sanitizeToDigits(code), + pattern: "0000-0/00", + }); +}; diff --git a/src/format-ncm/format-ncm.test.ts b/src/format-ncm/format-ncm.test.ts index 9426e236..67e3b3b1 100644 --- a/src/format-ncm/format-ncm.test.ts +++ b/src/format-ncm/format-ncm.test.ts @@ -1,11 +1,11 @@ -import { anyGarbage, digits } from "../_internals/test/arbitraries"; +import { anyGarbage, digits, digitsUpTo } from "../_internals/test/arbitraries"; import { expectIdempotent, expectMatchesPattern, expectNeverThrows, } from "../_internals/test/properties"; import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; -import { formatNcm } from "./format-ncm"; +import { formatNcm, type FormatNcmOptions } from "./format-ncm"; describe("formatNcm", () => { it("should format an NCM code given as digits", () => { @@ -20,7 +20,7 @@ describe("formatNcm", () => { expect(formatNcm("8471.30.12")).toBe("8471.30.12"); }); - it("should format a partial value progressively", () => { + it("should mask a partial value progressively by default", () => { expect(formatNcm("8")).toBe("8"); expect(formatNcm("84")).toBe("84"); expect(formatNcm("847")).toBe("847"); @@ -30,6 +30,11 @@ describe("formatNcm", () => { expect(formatNcm("8471301")).toBe("8471.30.1"); }); + it("should mask a partial number progressively by default", () => { + expect(formatNcm(8471)).toBe("8471"); + expect(formatNcm(847_130)).toBe("8471.30"); + }); + it("should not validate whether the code exists in the official table", () => { expect(formatNcm("00000000")).toBe("0000.00.00"); }); @@ -38,6 +43,29 @@ describe("formatNcm", () => { expect(formatNcm("")).toBe(""); }); + it("should not add digits after the NCM length", () => { + expect(formatNcm("847130120000")).toBe("8471.30.12"); + }); + + describe("pad option", () => { + it("should left pad a short code with zeros up to the full NCM length", () => { + expect(formatNcm("", { pad: true })).toBe("0000.00.00"); + expect(formatNcm("1", { pad: true })).toBe("0000.00.01"); + expect(formatNcm("8471", { pad: true })).toBe("0000.84.71"); + expect(formatNcm("847130", { pad: true })).toBe("0084.71.30"); + expect(formatNcm("84713012", { pad: true })).toBe("8471.30.12"); + }); + + it("should left pad a number the same way as its digits", () => { + expect(formatNcm(8471, { pad: true })).toBe("0000.84.71"); + expect(formatNcm(84_713_012, { pad: true })).toBe("8471.30.12"); + }); + + it("should mask progressively for an explicit false", () => { + expect(formatNcm("8471", { pad: false })).toBe("8471"); + }); + }); + it("should return an empty string for null and undefined", () => { // @ts-expect-error not a string or number expect(formatNcm(null)).toBe(""); @@ -45,6 +73,20 @@ describe("formatNcm", () => { expect(formatNcm()).toBe(""); }); + it("should return an empty string for a value that is not digits and mask characters", () => { + expect(formatNcm("abc8471")).toBe(""); + }); + + it("should return an empty string for a number that is not a non-negative safe integer", () => { + expect(formatNcm(-84_713_012)).toBe(""); + expect(formatNcm(8_471_301.2)).toBe(""); + expect(formatNcm(2 ** 53)).toBe(""); + }); + + it("should return an empty string for a null-prototype object", () => { + expect(formatNcm(Object.create(null))).toBe(""); + }); + describe("properties", () => { const eightDigitArbitrary = digits(8); @@ -56,6 +98,14 @@ describe("formatNcm", () => { expectMatchesPattern(formatNcm, /^\d{4}\.\d{2}\.\d{2}$/, eightDigitArbitrary); }); + test("should format every shorter value in the NNNN.NN.NN pattern when padding", () => { + expectMatchesPattern( + (value) => formatNcm(value, { pad: true }), + /^\d{4}\.\d{2}\.\d{2}$/, + digitsUpTo(8), + ); + }); + test("should be idempotent on a full 8 digit code", () => { expectIdempotent(formatNcm, eightDigitArbitrary); }); @@ -63,8 +113,13 @@ describe("formatNcm", () => { }); describe("formatNcm types", () => { - test("should take a string or number and return a string", () => { + test("should take a string or number value and options and return a string", () => { expectTypeOf(formatNcm).parameter(0).toEqualTypeOf(); + expectTypeOf(formatNcm).parameter(1).toEqualTypeOf(); expectTypeOf(formatNcm).returns.toEqualTypeOf(); }); + + test("should type the pad option as an optional boolean", () => { + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/src/format-ncm/format-ncm.ts b/src/format-ncm/format-ncm.ts index c9076b2e..3b7c2686 100644 --- a/src/format-ncm/format-ncm.ts +++ b/src/format-ncm/format-ncm.ts @@ -1,14 +1,42 @@ import { format } from "../_internals/format/format"; -import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +/** + * Shape a value may be written in while an NCM code is being typed: digits and the mask + * characters of the `NNNN.NN.NN` presentation, nothing else. Wider than the complete-code + * shape `isValidNcm` demands, because this formatter masks progressively. + */ +const NCM_MASK_REGEX = /^[\d\s.\-/]*$/; + +/** Options of `formatNcm`. */ +export type FormatNcmOptions = { + /** Whether to left pad the value with zeros up to the 8 digits of a complete NCM code (default: `false`). */ + pad?: boolean; +}; + /** * Formats a NCM (Nomenclatura Comum do Mercosul) code. * * This is a purely structural transformation, it does not check the code against the * official table, use `isValidNcm` for that. * + * With the default `pad: false` the mask is applied progressively, as far as the value goes, + * which is what an input being typed into needs (`"8471"` stays `"8471"`, `"847130"` becomes + * `"8471.30"`). With `pad: true` the value is first left padded with zeros to the 8 digits of a + * complete code, so it always comes back fully masked (`"8471"` gives `"0000.84.71"`). + * A number is treated exactly like the string of its digits: it is only padded under + * `pad: true`, so `formatNcm(8471)` gives `"8471"` and `formatNcm(8471, { pad: true })` gives + * `"0000.84.71"`. + * + * A string is only formatted when it holds nothing but digits and the mask characters; + * anything else (`"abc8471"`) gives `""` instead of having its digits picked out. A number is + * only formatted when it is a non-negative safe integer, since a sign, a decimal point or a + * rounded magnitude would otherwise be read as a code the caller never wrote. + * * @param {string|number} value - The NCM code to be formatted. + * @param {FormatNcmOptions} [options] - Optional formatting options. + * @param {boolean} [options.pad] - Whether to pad the value with leading zeros. Defaults to `false`. * @returns {string} The formatted code in the `NNNN.NN.NN` pattern, or an empty string * when there is nothing to format. * @@ -16,15 +44,25 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * ```typescript * formatNcm("84713012"); // "8471.30.12" * formatNcm(84713012); // "8471.30.12" - * formatNcm("8471"); // "8471" (partial values are formatted progressively) + * formatNcm("8471"); // "8471" (partial values are masked as far as they go) + * formatNcm("847130"); // "8471.30" + * formatNcm("8471", { pad: true }); // "0000.84.71" (padded to 8 digits first) + * formatNcm("abc8471"); // "" (not a documented form) + * formatNcm(-84713012); // "" (not a non-negative safe integer) * ``` * * @see Official: https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json */ -export const formatNcm = (value: string | number): string => - isNullish(value) - ? "" - : format({ - value: sanitizeToDigits(value), - pattern: "0000.00.00", - }); +export const formatNcm = (value: string | number, options?: FormatNcmOptions): string => { + if (!isLookupCode(value)) return ""; + + const code = String(value); + + if (!NCM_MASK_REGEX.test(code)) return ""; + + return format({ + pad: options?.pad, + value: sanitizeToDigits(code), + pattern: "0000.00.00", + }); +}; From 4645f5477060f52d19edee117b2e087e770e0c57 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:45 -0300 Subject: [PATCH 02/75] fix(holidays): add the GO and DF state entries, end PB 26/07 in 2015 and reject hostile state codes --- .../resolve-state-holiday-date.test.ts | 22 ++ .../resolve-state-holiday-date.ts | 44 ++- src/get-holidays/constants.ts | 254 ++++++++++++++---- src/get-holidays/get-holidays.test.ts | 167 +++++++++++- src/get-holidays/get-holidays.ts | 98 ++++--- src/is-holiday/is-holiday.test.ts | 20 +- src/is-holiday/is-holiday.ts | 32 ++- 7 files changed, 524 insertions(+), 113 deletions(-) diff --git a/src/_internals/resolve-state-holiday-date/resolve-state-holiday-date.test.ts b/src/_internals/resolve-state-holiday-date/resolve-state-holiday-date.test.ts index 37e3e8c7..cdcc7540 100644 --- a/src/_internals/resolve-state-holiday-date/resolve-state-holiday-date.test.ts +++ b/src/_internals/resolve-state-holiday-date/resolve-state-holiday-date.test.ts @@ -29,6 +29,28 @@ describe("resolveStateHolidayDate", () => { ); }); + test("should move a fixed date landing Monday to Friday on to the following Sunday", () => { + const rule = { day: 11, month: 8, nextSundayWhenWeekday: true }; + + expect(resolveStateHolidayDate(2025, rule)).toEqual(new Date(2025, 7, 17)); + expect(resolveStateHolidayDate(2026, rule)).toEqual(new Date(2026, 7, 16)); + expect(resolveStateHolidayDate(2027, rule)).toEqual(new Date(2027, 7, 15)); + expect(resolveStateHolidayDate(2028, rule)).toEqual(new Date(2028, 7, 13)); + }); + + test("should leave a fixed date already falling on a Saturday or a Sunday where it is", () => { + const rule = { day: 25, month: 11, nextSundayWhenWeekday: true }; + + expect(resolveStateHolidayDate(2028, rule)).toEqual(new Date(2028, 10, 25)); + expect(resolveStateHolidayDate(2029, rule)).toEqual(new Date(2029, 10, 25)); + }); + + test("should move an Easter derived date landing Monday to Friday on to the following Sunday", () => { + expect( + resolveStateHolidayDate(2024, { easterOffset: 60, nextSundayWhenWeekday: true }), + ).toEqual(new Date(2024, 5, 2)); + }); + test("should throw when the rule defines neither an Easter offset nor both day and month", () => { const message = "State holiday entry must define either `easterOffset` or both `day` and `month`"; diff --git a/src/_internals/resolve-state-holiday-date/resolve-state-holiday-date.ts b/src/_internals/resolve-state-holiday-date/resolve-state-holiday-date.ts index c1398b3a..94534217 100644 --- a/src/_internals/resolve-state-holiday-date/resolve-state-holiday-date.ts +++ b/src/_internals/resolve-state-holiday-date/resolve-state-holiday-date.ts @@ -6,8 +6,17 @@ export type HolidayDateRule = { month?: number; /** Offset in days from Easter Sunday (Carnaval is -47, Corpus Christi is 60); Easter itself is 0. */ easterOffset?: number; + /** + * Whether the holiday is observed on the following Sunday when the date the two rules above + * resolve to falls on a weekday (Monday to Friday), as Santa Catarina's two state holidays do. + */ + nextSundayWhenWeekday?: boolean; }; +const SUNDAY = 0; +const SATURDAY = 6; +const DAYS_IN_WEEK = 7; + function calculateEaster(year: number): Date { const a = year % 19; const b = Math.floor(year / 100); @@ -35,9 +44,21 @@ function calculateHolidayFromEaster(year: number, offset: number): Date { return holidayDate; } +function moveToNextSundayWhenWeekday(date: Date): Date { + const weekday = date.getDay(); + + if (weekday === SUNDAY || weekday === SATURDAY) return date; + + const observed = new Date(date); + observed.setDate(date.getDate() + (DAYS_IN_WEEK - weekday)); + + return observed; +} + /** * Resolves the date of a holiday in a given year: a fixed `day`/`month` pair, or an offset in - * days from Easter Sunday, computed with the Meeus/Jones/Butcher algorithm. + * days from Easter Sunday, computed with the Meeus/Jones/Butcher algorithm. When the rule sets + * `nextSundayWhenWeekday`, a date landing on a weekday is moved on to the following Sunday. * * @param {number} year - The four digit year. * @param {HolidayDateRule} rule - The fixed date or the Easter offset of the holiday. @@ -49,23 +70,26 @@ function calculateHolidayFromEaster(year: number, offset: number): Date { * resolveStateHolidayDate(2024, { easterOffset: 0 }); // 2024-03-31 (Easter Sunday) * resolveStateHolidayDate(2024, { easterOffset: 60 }); // 2024-05-30 (Corpus Christi) * resolveStateHolidayDate(2024, { day: 9, month: 7 }); // 2024-07-09 + * resolveStateHolidayDate(2025, { day: 11, month: 8, nextSundayWhenWeekday: true }); // 2025-08-17 * ``` * * @see Based on: https://en.wikipedia.org/wiki/Date_of_Easter#Anonymous_Gregorian_algorithm */ export const resolveStateHolidayDate = ( year: number, - { day, month, easterOffset }: HolidayDateRule, + { day, month, easterOffset, nextSundayWhenWeekday }: HolidayDateRule, ): Date => { - if (easterOffset !== undefined) { - return calculateHolidayFromEaster(year, easterOffset); - } + let date: Date; - if (day !== undefined && month !== undefined) { - return new Date(year, month - 1, day); + if (easterOffset !== undefined) { + date = calculateHolidayFromEaster(year, easterOffset); + } else if (day !== undefined && month !== undefined) { + date = new Date(year, month - 1, day); + } else { + throw new Error( + "State holiday entry must define either `easterOffset` or both `day` and `month`", + ); } - throw new Error( - "State holiday entry must define either `easterOffset` or both `day` and `month`", - ); + return nextSundayWhenWeekday === true ? moveToNextSundayWhenWeekday(date) : date; }; diff --git a/src/get-holidays/constants.ts b/src/get-holidays/constants.ts index 345daeb2..010ff73a 100644 --- a/src/get-holidays/constants.ts +++ b/src/get-holidays/constants.ts @@ -6,6 +6,7 @@ export type StateHolidayEntry = { day?: number; month?: number; easterOffset?: number; + nextSundayWhenWeekday?: boolean; type?: HolidayType; since?: number; until?: number; @@ -24,55 +25,170 @@ export const FIXED_HOLIDAYS = { export const CONSCIENCIA_NEGRA_NATIONAL_SINCE_YEAR = 2024; +/** + * Lei 14.759/2023 names the holiday "Dia Nacional de Zumbi e da Consciência Negra"; the shorter + * form below is the one 2.3.0 emitted and is kept so the output does not change. + */ export const CONSCIENCIA_NEGRA_HOLIDAY_NAME = "Dia da Consciência Negra"; export const LEGACY_CONSCIENCIA_NEGRA_HOLIDAY_NAME = "Consciência Negra"; +/** First year Alagoas' 16 September is a feriado estadual, not a ponto facultativo (Lei AL nº 9.358/2024). */ +export const AL_EMANCIPACAO_FERIADO_SINCE_YEAR = 2024; + +/** First year Paraíba's 26 July is no longer a holiday: Lei PB nº 10.601/2015 revoked its basis on 17/12/2015. */ +export const PB_MORTE_JOAO_PESSOA_UNTIL_YEAR = 2016; + +/** First year Tocantins' 18 March is no longer a holiday: Lei TO nº 2.013/2009 repealed the feriado clause on 18/02/2009. */ +export const TO_AUTONOMIA_UNTIL_YEAR = 2009; + /** - * Feriados estaduais por lei estadual, um por UF (uma UF pode ter mais de um `@see`). + * Feriados estaduais, um `@see` por entrada. + * + * Only one of these is a feriado civil under art. 1º, II of Lei 9.093/1995, which authorizes + * "a data magna do Estado fixada em lei estadual", in the singular. The remaining entries rest + * on ordinary state laws (and, for a few states, on the state constitution) that declare further + * feriados estaduais; the library reports them because they are observed in practice, not + * because art. 1º, II covers them. * - * @see Based on: https://pt.wikipedia.org/wiki/Acre Lei AC nº 1.538/2004, Dia do Evangélico - * @see Based on: https://pt.wikipedia.org/wiki/Acre Lei AC nº 1.411/2001, Dia Internacional da Mulher - * @see Based on: https://pt.wikipedia.org/wiki/Acre Lei AC nº 14/1964, Aniversário do Acre - * @see Based on: https://pt.wikipedia.org/wiki/Acre Lei AC nº 1.526/2004, Dia da Amazônia - * @see Based on: https://pt.wikipedia.org/wiki/Acre Lei AC nº 57/1965, Assinatura do Tratado de Petrópolis - * @see Based on: https://pt.wikipedia.org/wiki/Alagoas Lei AL nº 5.508/1993, São João - * @see Based on: https://pt.wikipedia.org/wiki/Alagoas Lei AL nº 5.509/1993, São Pedro - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Decreto AL nº 68.782/2019 (ponto facultativo), Emancipação Política de Alagoas - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Lei AP nº 667/2002, Dia de São José - * @see Based on: https://pt.wikipedia.org/wiki/Amap%C3%A1 Constituição Estadual do AP, Criação do Território Federal do Amapá - * @see Based on: https://pt.wikipedia.org/wiki/Dia_Nacional_de_Zumbi_e_da_Consci%C3%AAncia_Negra Lei AP nº 1.169/2007, Dia Estadual da Consciência Negra (state holiday until it became national in 2024) - * @see Official: https://sapl.al.am.leg.br/norma/8919 Lei AM nº 25/1977, Elevação do Amazonas à categoria de Província (05/09) - * @see Official: https://sapl.al.am.leg.br/norma/2873 Lei AM nº 84/2010, Dia da Consciência Negra (state holiday until it became national in 2024) - * @see Based on: https://www.legisweb.com.br/legislacao/?id=316229 Decreto AM de 02/02/2016 (calendário oficial), Nossa Senhora da Conceição (08/12): ponto facultativo estadual; feriado apenas no Município de Manaus (Lei Municipal nº 496/1999) - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Constituição Estadual da BA, Independência da Bahia - * @see Based on: https://pt.wikipedia.org/wiki/Cear%C3%A1 Constituição Estadual do CE (Data Magna), Abolição da Escravidão no Ceará - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Lei Distrital nº 963/1995, Dia do Evangélico (DF) - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Fundação de Brasília (21/4, Lei Orgânica do DF) - * @see Based on: https://pt.wikipedia.org/wiki/Esp%C3%ADrito_Santo_(estado) Lei ES nº 11.010/2019, Nossa Senhora da Penha (padroeira do estado, 8º dia após a Páscoa) - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Lei MA nº 2.457/1964, Adesão do Maranhão à Independência - * @see Official: https://www.al.mt.gov.br/norma-juridica/urn:lex:br;mato.grosso:estadual:lei.ordinaria:2002-12-27;7879 Lei MT nº 7.879/2002, Dia da Consciência Negra (state holiday until it became national in 2024) - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Lei MS nº 10/1979, Criação do Estado de Mato Grosso do Sul - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Lei PA nº 5.999/1996, Adesão do Pará à Independência - * @see Based on: https://pt.wikipedia.org/wiki/Para%C3%ADba Lei PB nº 10.601/2015, Fundação do Estado e Dia de Nossa Senhora das Neves - * @see Based on: https://pt.wikipedia.org/wiki/Para%C3%ADba Lei PB nº 3.489/1967, art. 2º, Morte de João Pessoa - * @see Based on: https://pt.wikipedia.org/wiki/Paran%C3%A1 Lei PR nº 18.384/2014 (ponto facultativo), Emancipação Política do Paraná - * @see Official: https://legis.alepe.pe.gov.br/texto.aspx?ano=2017&complemento=0&numero=16059&tipo=&tiponorma=1&url= Lei PE nº 16.059/2017, Revolução Pernambucana (Data Magna, fixed 6 March; supersedes the movable "primeiro domingo de março" date set by Lei PE nº 13.835/2009) - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Lei PI nº 176/1937, Dia do Piauí - * @see Official: http://alerjln1.alerj.rj.gov.br/CONTLEI.NSF/c8aa0900025feef6032564ec0060dfff/1baf90ca125ff96f8325740a00776600 Lei RJ nº 5.198/2008, São Jorge - * @see Official: http://alerjln1.alerj.rj.gov.br/CONTLEI.NSF/69d90307244602bb032567e800668618/80a541c3a5a9d63183256c7d0057bf25 Lei RJ nº 4.007/2002, Dia da Consciência Negra (state holiday until it became national in 2024; ADI 4.131 pending at the STF) - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Lei RN nº 8.913/2006, Mártires de Cunhaú e Uruaçu - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Constituição Estadual do RS, Revolução Farroupilha - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Lei RO nº 3.170/2013, Criação do Estado de Rondônia - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Constituição Estadual de RR, Criação do Estado de Roraima - * @see Based on: https://pt.wikipedia.org/wiki/Santa_Catarina Lei SC nº 16.719/2015 (consolida e revoga as Leis nº 10.306/1996 e 12.906/2004), Criação da Capitania de Santa Catarina - * @see Based on: https://pt.wikipedia.org/wiki/Santa_Catarina Lei SC nº 16.719/2015, Dia de Santa Catarina de Alexandria - * @see Official: https://www.al.sp.gov.br/documentacao/estudos-e-manuais/feriado-9-julho/artigo.htm Lei SP nº 9.497/1997 (PL 710/1995), Revolução Constitucionalista - * @see Official: https://www.al.sp.gov.br/repositorio/legislacao/lei/2023/lei-17746-12.09.2023.html Lei SP nº 17.746/2023, Dia da Consciência Negra (state holiday in 2023, national since 2024) - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Constituição Estadual de SE, Emancipação Política de Sergipe - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Lei TO nº 960/1998, Autonomia do Estado do Tocantins - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Lei TO nº 627/1993, Padroeira do Estado (Nossa Senhora da Natividade) - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Lei TO nº 98/1989, Criação do Estado do Tocantins + * The statutory date is what is emitted. Three states shift the observed date and only Santa + * Catarina's shift is modelled here (`nextSundayWhenWeekday`): Acre moves feriados falling from + * Tuesday to Thursday on to the following Friday (Lei AC nº 2.126/2009, except 15/06), and the + * Goiás executive may move 26/07 and 28/10 to a nearby dia útil by decree (Lei GO nº 20.756/2020, + * art. 269, § 1º), neither of which can be resolved from a year alone. + * + * @see Official: https://legis.ac.gov.br/detalhar/1087 + * Lei AC nº 1.538/2004, Dia do Evangélico (23/01) + * @see Official: https://legis.ac.gov.br/detalhar/1828 + * Lei AC nº 1.411/2001, Dia Internacional da Mulher (08/03) + * @see Official: https://legis.ac.gov.br/detalhar/618 + * Lei AC nº 14/1964, Aniversário do Acre (15/06) + * @see Official: https://legis.ac.gov.br/detalhar/940 + * Lei AC nº 243/1968, art. 2º, Dia da Amazônia (05/09): "É considerado feriado estadual o dia 5 de + * setembro em homenagem ao DIA DA AMAZÔNIA". Lei AC nº 1.526/2004, cited here before, only adds + * the date to the calendário oficial de eventos. + * @see Official: https://legis.ac.gov.br/detalhar/688 + * Lei AC nº 57/1965, Assinatura do Tratado de Petrópolis (17/11) + * @see Official: https://sapl.al.al.leg.br/norma/3363 + * Lei AL nº 5.508/1993, São João (24/06) + * @see Official: https://sapl.al.al.leg.br/norma/3364 + * Lei AL nº 5.509/1993, São Pedro (29/06) + * @see Official: https://sapl.al.al.leg.br/norma/3117 + * Lei AL nº 9.358, de 26/08/2024, Emancipação Política de Alagoas (16/09): "DISPÕE SOBRE O FERIADO + * ESTADUAL DA EMANCIPAÇÃO POLÍTICA DO ESTADO DE ALAGOAS - DIA 16 DE SETEMBRO". Until 2023 the date + * was only the ponto facultativo of the Decreto AL nº 68.782/2019. + * @see Official: https://al.ap.leg.br/ver_texto_lei.php?iddocumento=17488 + * Lei AP nº 667/2002, art. 1º par. único, Dia de São José (19/03) + * @see Official: https://silegis.al.ap.leg.br/proposicaopdf/2CEatualizadaeconsolidadaateEC071comSumario.pdf + * Constituição Estadual do AP, art. 355, Criação do Território Federal do Amapá (13/09): "O dia 13 + * de Setembro, data magna do Amapá, é feriado em todo o território do Estado". + * @see Official: https://al.ap.leg.br/ver_texto_lei.php?iddocumento=22214 + * Lei AP nº 1.169/2007, Dia Estadual da Consciência Negra (state holiday until it became national + * in 2024) + * @see Official: https://sapl.al.am.leg.br/norma/8919 + * Lei AM nº 25/1977, Elevação do Amazonas à categoria de Província (05/09) + * @see Official: https://sapl.al.am.leg.br/norma/2873 + * Lei AM nº 84/2010, Dia da Consciência Negra (state holiday until it became national in 2024) + * @see Official: https://sapl.cmm.am.gov.br/norma/3932 + * Lei Municipal de Manaus nº 496/1999, Nossa Senhora da Conceição (08/12): "INSTITUI feriado + * religioso no Município de Manaus no dia 8 de dezembro". No state norm declaring 08/12 was + * located in the ALEAM records, so the entry is reported as an optional day, not as a feriado + * estadual. + * @see Official: https://www.legislabahia.ba.gov.br/documentos/constituicao-do-estado-da-bahia-de-05-de-outubro-de-1989 + * Constituição Estadual da BA, art. 6º § 3º, Independência da Bahia (02/07): "O Dois de Julho, + * data magna da Bahia ..., é feriado em todo o território do Estado". + * @see Official: https://belt.al.ce.gov.br/index.php/constituicao-do-ceara/emendas-a-constituicao-do-ceara/item/5643-emenda-constitucional-n-73-de-1-de-dezembro-de-2011-d-o-06-12-11 + * Constituição Estadual do CE, art. 18 par. único (EC nº 73/2011), Abolição da Escravidão no Ceará + * (25/03): the text fixes the data magna, and the feriado follows from Lei 9.093/1995, art. 1º, + * II. + * @see Official: https://www.sinj.df.gov.br/sinj/Norma/18459/Lei_72_27_12_1989.html + * Lei distrital nº 72/1989, art. 1º, I, Fundação de Brasília (21/04), and art. 1º par. único, + * Corpus Christi: "São, igualmente feriados, a Sexta-feira da Paixão e Corpus Christi, datas + * móveis". + * @see Official: https://www.sinj.df.gov.br/sinj/Norma/48922/Lei_963_1995.html + * Lei distrital nº 963/1995, Dia do Evangélico (30/11) + * @see Official: https://www3.al.es.gov.br/Arquivo/Documents/legislacao/html/LEI110102019.html + * Lei ES nº 11.010/2019, art. 1º par. único, Nossa Senhora da Penha (padroeira do estado, "sempre + * na segunda-feira, oitavo dia posterior ao domingo de Páscoa") + * @see Official: https://legisla.casacivil.go.gov.br/pesquisa_legislacao/100979/lei-20756 + * Lei GO nº 20.756/2020, art. 269, II, the three feriados estaduais of Goiás: "a) 26 de julho, + * consagrado à fundação da cidade de Goiás; b) 24 de outubro, comemorativo ao lançamento da pedra + * fundamental de Goiânia; c) 28 de outubro, consagrado ao servidor público". + * @see Official: https://arquivos.al.ma.leg.br:8443/ged/legislacao/LEI_2457 + * Lei MA nº 2.457/1964, Adesão do Maranhão à Independência (28/07) + * @see Official: https://www.al.mt.gov.br/norma-juridica/urn:lex:br;mato.grosso:estadual:lei.ordinaria:2002-12-27;7879 + * Lei MT nº 7.879/2002, Dia da Consciência Negra (state holiday until it became national in 2024) + * @see Official: https://aacpdappls.net.ms.gov.br/appls/legislacao/secoge/govato.nsf/1b758e65922af3e904256b220050342a/a489a293563f506304256e450002e9f8 + * Lei MS nº 10/1979, Criação do Estado de Mato Grosso do Sul (11/10) + * @see Official: https://bancodeleis.alepa.pa.gov.br/arquivos/lei5999_1996_93239.pdf + * Lei PA nº 5.999/1996, Adesão do Pará à Independência (15/08) + * @see Official: https://sapl.al.pb.leg.br/norma/11988 + * Lei PB nº 10.601/2015, Data Magna do Estado da Paraíba (05/08): "INSTITUI COMO FERIADO CIVIL O + * DIA 05 DE AGOSTO, DATA MAGNA DO ESTADO DA PARAÍBA". Its art. 2º also revoked art. 2º of Lei PB + * nº 3.489/1967, the basis of the 26/07 Morte de João Pessoa entry, which is therefore emitted + * only up to 2015. + * @see Official: https://www.legislacao.pr.gov.br/legislacao/pesquisarAto.do?action=exibir&codAto=134573 + * Lei PR nº 18.384/2014, Emancipação Política do Paraná (19/12), expressly "não se constituindo em + * feriado civil" + * @see Official: https://legis.alepe.pe.gov.br/texto.aspx?tiponorma=1&numero=16241&complemento=0&ano=2017&tipo=&url= + * Lei PE nº 16.241/2017, art. 49, Revolução Pernambucana (06/03): "Dia 6 de março: Data Magna do + * Estado de Pernambuco e feriado civil no âmbito do Estado de Pernambuco". Revoked the Lei PE nº + * 16.059/2017 cited here before, which had itself superseded the movable "primeiro domingo de + * março" of Lei PE nº 13.835/2009. + * @see Official: https://sapl.al.pi.leg.br/norma/5849 + * Lei PI nº 176/1937, Dia do Piauí (19/10) + * @see Official: http://alerjln1.alerj.rj.gov.br/CONTLEI.NSF/c8aa0900025feef6032564ec0060dfff/1baf90ca125ff96f8325740a00776600 + * Lei RJ nº 5.198/2008, São Jorge (23/04), upheld by STF ADI 4092 (Plenário, sessão virtual de 18 + * a 25/08/2023, trânsito em julgado 28/10/2023): "O Tribunal, por maioria, declarou a + * constitucionalidade da Lei do Estado do Rio de Janeiro n. 5.198, de 5 de março de 2008". + * @see Official: http://alerjln1.alerj.rj.gov.br/CONTLEI.NSF/69d90307244602bb032567e800668618/80a541c3a5a9d63183256c7d0057bf25 + * Lei RJ nº 4.007/2002, Dia da Consciência Negra (state holiday until it became national in 2024). + * STF ADI 4131, cited here before as pending against it, in fact challenged Lei RJ nº 5.243/2008 + * and was não conhecida on 21/09/2018 (trânsito em julgado 25/10/2018). + * @see Official: http://www.al.rn.leg.br/storage/legislacao//arq5064574f632ec.pdf + * Lei RN nº 8.913/2006, Mártires de Cunhaú e Uruaçu (03/10) + * @see Official: https://ww2.al.rs.gov.br/dal/Legisla%C3%A7%C3%A3o/Constitui%C3%A7%C3%A3oEstadual/tabid/3683/Default.aspx + * Constituição Estadual do RS, art. 6º § 1º (EC nº 11/1995, renumbered by EC nº 83/2023), + * Revolução Farroupilha (20/09): "O dia 20 de setembro é a data magna, sendo considerado feriado + * no Estado". + * @see Official: https://sapl.al.ro.leg.br/norma/4958 + * Lei RO nº 2.291, de 22/04/2010, Criação do Estado de Rondônia (04/01): "DECLARA O DIA 4 DE + * JANEIRO DATA MAGNA E FERIADO CIVIL ESTADUAL". Lei RO nº 3.170/2013, cited here before, is a + * supplementary credit law unrelated to holidays. + * @see Official: http://sapl.al.rr.leg.br/media/sapl/public/normajuridica/1991/3912/constituicao_estadual_do_estado_de_roraima.pdf + * Constituição Estadual de RR, art. 9º, Criação do Estado de Roraima (05/10): "Cinco de outubro, + * data magna de Roraima, é feriado em todo o território do Estado". + * @see Official: http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html + * Lei SC nº 18.531/2022, the in-force consolidation, whose Anexo Único carries both Santa Catarina + * holidays and the Sunday transfer: "Sempre que o dia 11 de agosto coincidir com dia útil da + * semana, o feriado e os eventos alusivos à data serão transferidos para o domingo subsequente" + * and the same clause for 25 de novembro. + * @see Official: http://leis.alesc.sc.gov.br/html/1996/10306_1996_lei.html + * Lei SC nº 10.306/1996, art. 1º, in the wording of Lei SC nº 12.906/2004: "É considerada data + * magna do Estado o dia 11 de agosto, Dia do Estado de Santa Catarina, e dia de Santa Catarina de + * Alexandria, dia 25 de novembro". + * @see Official: http://leis.alesc.sc.gov.br/html/2005/13408_2005_lei.html + * Lei SC nº 13.408/2005, which added the parágrafo único transferring both dates to the following + * Sunday. Lei SC nº 16.719/2015, cited here before, was revoked by Lei SC nº 17.335/2017, itself + * consolidated and revoked by Lei SC nº 18.531/2022. + * @see Official: https://www.al.sp.gov.br/documentacao/estudos-e-manuais/feriado-9-julho/artigo.htm + * Lei SP nº 9.497/1997 (PL 710/1995), Revolução Constitucionalista (09/07) + * @see Official: https://www.al.sp.gov.br/repositorio/legislacao/lei/2023/lei-17746-12.09.2023.html + * Lei SP nº 17.746/2023, Dia da Consciência Negra: a permanent state holiday, listed here only for + * 2023 because the national holiday of Lei 14.759/2023 takes over from 2024. + * @see Official: https://aleselegis.al.se.leg.br/Arquivo/Documents/legislacao/html/CE11989.html + * Constituição Estadual de SE, art. 269 (EC nº 20/2000), Independência de Sergipe (08/07): "Será + * feriado estadual o dia 08 de julho, data consagrada à Independência de Sergipe". + * @see Official: https://www.al.to.leg.br/arquivo/15717 + * Lei TO nº 960/1998, whose art. 1º caput only institutes the Dia da Autonomia (18/03); the + * feriado estadual sat in the parágrafo único. + * @see Official: https://www.al.to.leg.br/arquivo/15724 + * Lei TO nº 2.013, de 18/02/2009, which replaced that parágrafo único with a purely commemorative + * provision, so 18/03 is emitted only up to 2008. + * @see Official: https://www.al.to.leg.br/arquivo/6883 + * Lei TO nº 627/1993, Padroeira do Estado (Nossa Senhora da Natividade, 08/09) + * @see Official: https://www.al.to.leg.br/arquivo/6358 + * Lei TO nº 98/1989, Criação do Estado do Tocantins (05/10) */ export const STATE_HOLIDAYS: Partial> = { AC: [ @@ -85,7 +201,19 @@ export const STATE_HOLIDAYS: Partial> = { AL: [ { name: "São João", day: 24, month: 6 }, { name: "São Pedro", day: 29, month: 6 }, - { name: "Emancipação Política de Alagoas", day: 16, month: 9, type: "optional" }, + { + name: "Emancipação Política de Alagoas", + day: 16, + month: 9, + type: "optional", + until: AL_EMANCIPACAO_FERIADO_SINCE_YEAR, + }, + { + name: "Emancipação Política de Alagoas", + day: 16, + month: 9, + since: AL_EMANCIPACAO_FERIADO_SINCE_YEAR, + }, ], AP: [ { name: "Dia de São José", day: 19, month: 3 }, @@ -113,9 +241,15 @@ export const STATE_HOLIDAYS: Partial> = { CE: [{ name: "Abolição da Escravidão no Ceará", day: 25, month: 3 }], DF: [ { name: "Fundação de Brasília", day: 21, month: 4 }, + { name: "Corpus Christi", easterOffset: 60 }, { name: "Dia do Evangélico", day: 30, month: 11 }, ], ES: [{ name: "Nossa Senhora da Penha", easterOffset: 8 }], + GO: [ + { name: "Fundação da Cidade de Goiás", day: 26, month: 7 }, + { name: "Lançamento da Pedra Fundamental de Goiânia", day: 24, month: 10 }, + { name: "Dia do Servidor Público", day: 28, month: 10 }, + ], MA: [{ name: "Adesão do Maranhão à Independência", day: 28, month: 7 }], MT: [ { @@ -128,12 +262,13 @@ export const STATE_HOLIDAYS: Partial> = { MS: [{ name: "Criação do Estado de Mato Grosso do Sul", day: 11, month: 10 }], PA: [{ name: "Adesão do Pará à Independência", day: 15, month: 8 }], PB: [ + { name: "Data Magna do Estado da Paraíba", day: 5, month: 8 }, { - name: "Fundação do Estado e Dia de Nossa Senhora das Neves", - day: 5, - month: 8, + name: "Morte de João Pessoa", + day: 26, + month: 7, + until: PB_MORTE_JOAO_PESSOA_UNTIL_YEAR, }, - { name: "Morte de João Pessoa", day: 26, month: 7 }, ], PR: [{ name: "Emancipação Política do Paraná", day: 19, month: 12, type: "optional" }], PE: [{ name: "Revolução Pernambucana", day: 6, month: 3 }], @@ -152,8 +287,18 @@ export const STATE_HOLIDAYS: Partial> = { RO: [{ name: "Criação do Estado de Rondônia", day: 4, month: 1 }], RR: [{ name: "Criação do Estado de Roraima", day: 5, month: 10 }], SC: [ - { name: "Criação da Capitania de Santa Catarina", day: 11, month: 8 }, - { name: "Dia de Santa Catarina de Alexandria", day: 25, month: 11 }, + { + name: "Dia do Estado de Santa Catarina", + day: 11, + month: 8, + nextSundayWhenWeekday: true, + }, + { + name: "Dia de Santa Catarina de Alexandria", + day: 25, + month: 11, + nextSundayWhenWeekday: true, + }, ], SP: [ { name: "Revolução Constitucionalista", day: 9, month: 7 }, @@ -165,9 +310,14 @@ export const STATE_HOLIDAYS: Partial> = { until: CONSCIENCIA_NEGRA_NATIONAL_SINCE_YEAR, }, ], - SE: [{ name: "Emancipação Política de Sergipe", day: 8, month: 7 }], + SE: [{ name: "Independência de Sergipe", day: 8, month: 7 }], TO: [ - { name: "Autonomia do Estado do Tocantins", day: 18, month: 3 }, + { + name: "Autonomia do Estado do Tocantins", + day: 18, + month: 3, + until: TO_AUTONOMIA_UNTIL_YEAR, + }, { name: "Padroeira do Estado (Nossa Senhora da Natividade)", day: 8, diff --git a/src/get-holidays/get-holidays.test.ts b/src/get-holidays/get-holidays.test.ts index f125e0ce..0ecf6f10 100644 --- a/src/get-holidays/get-holidays.test.ts +++ b/src/get-holidays/get-holidays.test.ts @@ -7,6 +7,10 @@ import { isBusinessDay } from "../is-business-day/is-business-day"; import { STATE_HOLIDAYS } from "./constants"; import { getHolidays, type GetHolidaysOptions, type Holiday } from "./get-holidays"; +const PROTOTYPE_KEYS = Object.getOwnPropertyNames(Object.prototype); + +const hostileStateCodes = fc.constantFrom(...PROTOTYPE_KEYS, "SP", "xx"); + function getHolidaysFor(year: number, stateCode: StateCode | null): Holiday[] { return stateCode === null ? getHolidays(year) : getHolidays({ year, stateCode }); } @@ -340,21 +344,21 @@ describe("getHolidays", () => { ).toHaveLength(1); }); - test("should include state holidays added after the 2026 legal audit: PB's Morte de João Pessoa (Lei nº 3.489/1967, art. 2º), TO's Autonomia do Estado do Tocantins (Lei nº 960/1998), and AP's Dia Estadual da Consciência Negra (Lei nº 1.169/2007, until superseded by the 2024 national holiday)", () => { - const pbHolidays = getHolidays({ year: 2024, stateCode: "PB" }); - const toHolidays = getHolidays({ year: 2024, stateCode: "TO" }); + test("should include state holidays added after the 2026 legal audit while they were in force: PB's Morte de João Pessoa (Lei nº 3.489/1967, art. 2º), TO's Autonomia do Estado do Tocantins (Lei nº 960/1998), and AP's Dia Estadual da Consciência Negra (Lei nº 1.169/2007, until superseded by the 2024 national holiday)", () => { + const pbHolidays = getHolidays({ year: 2015, stateCode: "PB" }); + const toHolidays = getHolidays({ year: 2008, stateCode: "TO" }); const apHolidays2023 = getHolidays({ year: 2023, stateCode: "AP" }); const apHolidays2024 = getHolidays({ year: 2024, stateCode: "AP" }); expect(pbHolidays).toContainEqual({ name: "Morte de João Pessoa", - date: new Date(2024, 6, 26), + date: new Date(2015, 6, 26), type: "state", }); expect(toHolidays).toContainEqual({ name: "Autonomia do Estado do Tocantins", - date: new Date(2024, 2, 18), + date: new Date(2008, 2, 18), type: "state", }); @@ -367,6 +371,150 @@ describe("getHolidays", () => { expect(apHolidays2024.some((h) => h.name === "Dia Estadual da Consciência Negra")).toBe(false); }); + test("should stop emitting PB's Morte de João Pessoa from 2016 on, since Lei PB nº 10.601/2015 art. 2º revoked art. 2º of Lei PB nº 3.489/1967 on 17/12/2015", () => { + expect( + getHolidays({ year: 2016, stateCode: "PB" }).some((h) => h.name === "Morte de João Pessoa"), + ).toBe(false); + + expect(getHolidays({ year: 2016, stateCode: "PB" })).toContainEqual({ + name: "Data Magna do Estado da Paraíba", + date: new Date(2016, 7, 5), + type: "state", + }); + }); + + test("should stop emitting TO's Autonomia do Estado do Tocantins from 2009 on, since Lei TO nº 2.013/2009 rewrote the parágrafo único of Lei TO nº 960/1998 art. 1º, the only clause that declared the feriado, into a commemorative provision", () => { + expect( + getHolidays({ year: 2009, stateCode: "TO" }).some( + (h) => h.name === "Autonomia do Estado do Tocantins", + ), + ).toBe(false); + + expect(getHolidays({ year: 2009, stateCode: "TO" })).toContainEqual({ + name: "Criação do Estado do Tocantins", + date: new Date(2009, 9, 5), + type: "state", + }); + }); + + test("should type AL's 16 September as a feriado estadual from 2024 on (Lei AL nº 9.358/2024) and as an optional day before it (Decreto AL nº 68.782/2019)", () => { + expect(getHolidays({ year: 2023, stateCode: "AL" })).toContainEqual({ + name: "Emancipação Política de Alagoas", + date: new Date(2023, 8, 16), + type: "optional", + }); + + expect(getHolidays({ year: 2024, stateCode: "AL" })).toContainEqual({ + name: "Emancipação Política de Alagoas", + date: new Date(2024, 8, 16), + type: "state", + }); + + expect( + getHolidays({ year: 2024, stateCode: "AL" }).filter( + (h) => h.name === "Emancipação Política de Alagoas", + ), + ).toHaveLength(1); + }); + + test("should list the three Goiás state holidays of Lei GO nº 20.756/2020, art. 269, II", () => { + const holidays = getHolidays({ year: 2024, stateCode: "GO" }); + + expect(holidays).toContainEqual({ + name: "Fundação da Cidade de Goiás", + date: new Date(2024, 6, 26), + type: "state", + }); + expect(holidays).toContainEqual({ + name: "Lançamento da Pedra Fundamental de Goiânia", + date: new Date(2024, 9, 24), + type: "state", + }); + expect(holidays).toContainEqual({ + name: "Dia do Servidor Público", + date: new Date(2024, 9, 28), + type: "state", + }); + }); + + test("should replace the national optional Corpus Christi with a DF state entry, which Lei distrital nº 72/1989 art. 1º parágrafo único declares a feriado, without listing the date twice", () => { + const dfHolidays = getHolidays({ year: 2024, stateCode: "DF" }); + const nationalHolidays = getHolidays(2024); + + expect(dfHolidays.filter((h) => h.name === "Corpus Christi")).toEqual([ + { name: "Corpus Christi", date: new Date(2024, 4, 30), type: "state" }, + ]); + + expect(nationalHolidays).toContainEqual({ + name: "Corpus Christi", + date: new Date(2024, 4, 30), + type: "optional", + }); + + expect(getHolidays({ year: 2024, stateCode: "SP" })).toContainEqual({ + name: "Corpus Christi", + date: new Date(2024, 4, 30), + type: "optional", + }); + }); + + test("should list DF's Fundação de Brasília (Lei distrital nº 10.633/1989) next to the national Tiradentes, which falls on the same 21 April under a different name", () => { + const dfHolidays = getHolidays({ year: 2024, stateCode: "DF" }); + + expect(dfHolidays).toContainEqual({ + name: "Fundação de Brasília", + date: new Date(2024, 3, 21), + type: "state", + }); + expect(dfHolidays).toContainEqual({ + name: "Tiradentes", + date: new Date(2024, 3, 21), + type: "national", + }); + }); + + test("should move both Santa Catarina holidays to the following Sunday when they fall Monday to Friday, as Lei SC nº 18.531/2022 requires (11/08/2025 is a Monday, 25/11/2025 a Tuesday)", () => { + const holidays = getHolidays({ year: 2025, stateCode: "SC" }); + + expect(holidays).toContainEqual({ + name: "Dia do Estado de Santa Catarina", + date: new Date(2025, 7, 17), + type: "state", + }); + expect(holidays).toContainEqual({ + name: "Dia de Santa Catarina de Alexandria", + date: new Date(2025, 10, 30), + type: "state", + }); + }); + + test("should keep both Santa Catarina holidays on their statutory date in a year they already fall on a weekend (11/08/2024 is a Sunday, 25/11/2029 a Sunday and 25/11/2028 a Saturday)", () => { + expect(getHolidays({ year: 2024, stateCode: "SC" })).toContainEqual({ + name: "Dia do Estado de Santa Catarina", + date: new Date(2024, 7, 11), + type: "state", + }); + expect(getHolidays({ year: 2029, stateCode: "SC" })).toContainEqual({ + name: "Dia de Santa Catarina de Alexandria", + date: new Date(2029, 10, 25), + type: "state", + }); + expect(getHolidays({ year: 2028, stateCode: "SC" })).toContainEqual({ + name: "Dia de Santa Catarina de Alexandria", + date: new Date(2028, 10, 25), + type: "state", + }); + }); + + test("should treat a prototype chain key as an unknown stateCode instead of throwing", () => { + const nationalHolidays = getHolidays(2024); + + for (const stateCode of PROTOTYPE_KEYS) { + // @ts-expect-error: intentionally invalid input + expect(getHolidays({ year: 2024, stateCode })).toEqual(nationalHolidays); + } + }); + test("should compute ES's Nossa Senhora da Penha (Lei nº 11.010/2019) as a movable state holiday, 8 days after Easter Sunday, replacing the removed 'Dia do Estado do Espírito Santo' which was only a municipal ponto facultativo", () => { const holidays = getHolidays({ year: 2024, stateCode: "ES" }); @@ -436,9 +584,14 @@ describe("getHolidays", () => { ); }); - test("should never throw, regardless of the input", () => { + const anyYear = fc.oneof(yearArbitrary, fc.anything()); + const anyStateCode = fc.oneof(hostileStateCodes, stateCodeArbitrary, fc.anything()); + const hostileOptions = fc.record({ year: anyYear, stateCode: anyStateCode }); + const anyInput = fc.oneof(fc.anything(), hostileOptions); + + test("should never throw, regardless of the input, prototype chain state codes included", () => { fc.assert( - fc.property(fc.anything(), (value) => { + fc.property(anyInput, (value) => { expect(() => getHolidays(value as never)).not.toThrow(); }), ); diff --git a/src/get-holidays/get-holidays.ts b/src/get-holidays/get-holidays.ts index 6007793d..3e84dc2d 100644 --- a/src/get-holidays/get-holidays.ts +++ b/src/get-holidays/get-holidays.ts @@ -79,22 +79,33 @@ const computeHolidays = (year: number, stateCode: StateCode | undefined): Holida }, ); - // Stryker disable next-line ConditionalExpression: when stateCode is undefined, STATE_HOLIDAYS[stateCode] resolves to undefined too, so the inner `if (stateHolidays)` already no-ops either way - if (stateCode !== undefined) { - const stateHolidays = STATE_HOLIDAYS[stateCode]; - if (stateHolidays) { - for (const entry of stateHolidays) { - const { name, type, since, until } = entry; - // Stryker disable next-line ConditionalExpression: `since` is undefined for most entries, and `year < undefined` is already always false, so the explicit `since !== undefined` guard never changes the outcome - if (since !== undefined && year < since) continue; - // Stryker disable next-line ConditionalExpression: `until` is undefined for most entries, and `year >= undefined` is already always false, so the explicit `until !== undefined` guard never changes the outcome - if (until !== undefined && year >= until) continue; - - holidays.push({ - name, - date: resolveStateHolidayDate(year, entry), - type: type ?? "state", - }); + // An own entry lookup, so a prototype chain key ("toString", "__proto__", ...) is an unknown + // state code like any other, and so is `undefined` when no state code was given. + const stateEntry = Object.entries(STATE_HOLIDAYS).find(([code]) => code === stateCode); + + if (stateEntry) { + for (const entry of stateEntry[1]) { + const { name, type, since, until } = entry; + // Stryker disable next-line ConditionalExpression: `since` is undefined for most entries, and `year < undefined` is already always false, so the explicit `since !== undefined` guard never changes the outcome + if (since !== undefined && year < since) continue; + // Stryker disable next-line ConditionalExpression: `until` is undefined for most entries, and `year >= undefined` is already always false, so the explicit `until !== undefined` guard never changes the outcome + if (until !== undefined && year >= until) continue; + + const date = resolveStateHolidayDate(year, entry); + const stateHoliday: Holiday = { name, date, type: type ?? "state" }; + // Name and date together are the identity of a holiday here: a state entry only replaces + // a national one when both match, so DF's Corpus Christi replaces the national optional + // one while its Fundação de Brasília is listed next to Tiradentes, which falls on the + // same 21 April under a different name. + const stateHolidayKey = `${name}|${date.getTime()}`; + const nationalIndex = holidays.findIndex( + (holiday) => `${holiday.name}|${holiday.date.getTime()}` === stateHolidayKey, + ); + + if (nationalIndex === -1) { + holidays.push(stateHoliday); + } else { + holidays[nationalIndex] = stateHoliday; } } } @@ -117,7 +128,22 @@ const computeHolidays = (year: number, stateCode: StateCode | undefined): Holida * * If `stateCode` is provided but is not a valid/known state code, it is ignored and * only national holidays are returned (this mirrors passing no `stateCode` at all, - * and is kept for backwards compatibility). + * and is kept for backwards compatibility). The lookup is an own-property one, so a + * prototype-chain key such as `"__proto__"`, `"constructor"` or `"toString"` is an unknown + * state code like any other rather than a crash. + * + * When a state entry falls on the same date as a national one and carries the same name, the + * state entry replaces it instead of being listed twice: this is how the Distrito Federal's + * Corpus Christi, a feriado under Lei distrital nº 72/1989 art. 1º parágrafo único, comes back + * typed `"state"` for `stateCode: "DF"` while staying `"optional"` everywhere else. + * + * Only one state holiday per UF is a feriado civil under Lei 9.093/1995 art. 1º, II, which + * authorizes "a data magna do Estado fixada em lei estadual" in the singular; the other entries + * of `STATE_HOLIDAYS` rest on ordinary state laws and are reported because they are observed in + * practice. The date returned is the statutory one. Santa Catarina's two holidays are the only + * observance shift the table models (both move to the following Sunday when they fall Monday to + * Friday); Acre's Tuesday-to-Thursday shift and the Goiás decrees that may move 26/07 and 28/10 + * are not, because neither can be resolved from a year alone. * * @param {number} year - The year for which to retrieve holidays (must be between 1900 and 2099) * @returns {Holiday[]} An array of holidays sorted by date @@ -131,23 +157,33 @@ const computeHolidays = (year: number, stateCode: StateCode | undefined): Holida * const spHolidays = getHolidays({ year: 2024, stateCode: 'SP' }); * ``` * - * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l0662.htm Lei 662/1949, the base - * national holidays law (Ano novo, Dia do trabalhador, Independência do Brasil, Proclamação da - * República, Natal). - * @see Official: https://www.planalto.gov.br/ccivil_03/leis/2002/l10607.htm Lei 10.607/2002, - * added Tiradentes and Finados to the national holidays. - * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l6802.htm Lei 6.802/1980, declared - * Nossa Senhora Aparecida (12 October) a national holiday. - * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2023/lei/l14759.htm Lei - * 14.759/2023, nationalized Dia da Consciência Negra (20 November) from + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l0662.htm + * Lei 662/1949, the base national holidays law (Ano novo, Dia do trabalhador, Independência do + * Brasil, Proclamação da República, Natal). + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/2002/l10607.htm + * Lei 10.607/2002, added Tiradentes and Finados to the national holidays. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l6802.htm + * Lei 6.802/1980, declared Nossa Senhora Aparecida (12 October) a national holiday. + * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2023/lei/l14759.htm + * Lei 14.759/2023, nationalized Dia da Consciência Negra (20 November) from * `CONSCIENCIA_NEGRA_NATIONAL_SINCE_YEAR` (2024) onward. - * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l9093.htm Lei 9.093/1995, the - * framework law authorizing one state civil holiday and up to four municipal religious holidays; - * the legal basis for `STATE_HOLIDAYS`. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l9093.htm + * Lei 9.093/1995, the framework law authorizing one state civil holiday (art. 1º, II, "a data + * magna do Estado fixada em lei estadual") and up to four municipal religious holidays, "neste + * incluída a Sexta-Feira da Paixão" (art. 2º); the legal basis for the data magna entries of + * `STATE_HOLIDAYS`. + * @see Official: https://www.in.gov.br/web/dou/-/portaria-mgi-n-11.460-de-29-de-dezembro-de-2025-678388627 + * Portaria MGI nº 11.460/2025, the federal executive's annual calendar of feriados nacionais and + * pontos facultativos, reissued every December. It is the source of the typing of the four entries + * derived from Easter, which no federal law declares: "Paixão de Cristo (feriado nacional)" + * (Easter minus 2, emitted as `"Sexta-feira Santa"` typed `national`), "Carnaval (ponto + * facultativo)" (Easter minus 47) and "Corpus Christi (ponto facultativo)" (Easter plus 60), both + * typed `optional`. Sexta-feira Santa has no statutory basis of its own: Lei 9.093/1995 art. 2º + * places it among the *municipal* religious holidays, and it is typed `national` here because the + * portaria observes it nationwide. Easter itself is emitted as `"Páscoa"` typed `religious`, + * computed with the Meeus/Jones/Butcher algorithm by `resolveStateHolidayDate`. * @see Official: state holiday laws are cited individually, one `@see` per holiday, in * `src/get-holidays/constants.ts`. - * @see Based on: https://pt.wikipedia.org/wiki/Feriados_no_Brasil Used as secondary evidence for - * some state holidays where no official law text was located (see constants.ts for which). */ export function getHolidays(year: number): Holiday[]; /** diff --git a/src/is-holiday/is-holiday.test.ts b/src/is-holiday/is-holiday.test.ts index f2c53ed4..a5f9646f 100644 --- a/src/is-holiday/is-holiday.test.ts +++ b/src/is-holiday/is-holiday.test.ts @@ -11,6 +11,13 @@ function getHolidaysFor(year: number, stateCode: StateCode | null): Holiday[] { return stateCode === null ? getHolidays(year) : getHolidays({ year, stateCode }); } +const PROTOTYPE_KEYS = Object.getOwnPropertyNames(Object.prototype); + +const anyTargetDate = fc.oneof(fc.date(), fc.anything()); +const anyStateCode = fc.oneof(fc.constantFrom(...PROTOTYPE_KEYS, "SP", "xx"), fc.anything()); +const hostileOptions = fc.record({ targetDate: anyTargetDate, stateCode: anyStateCode }); +const anyInput = fc.oneof(fc.anything(), hostileOptions); + describe("isHoliday", () => { it("should return true for a national holiday built from local date components", () => { expect(isHoliday({ targetDate: new Date(2024, 0, 1) })).toBe(true); @@ -67,6 +74,15 @@ describe("isHoliday", () => { expect(isHoliday({ targetDate: new Date(2024, 5, 10), stateCode: "XX" })).toBe(false); }); + it("should treat a prototype chain key as an unknown stateCode instead of throwing", () => { + for (const stateCode of PROTOTYPE_KEYS) { + // @ts-expect-error: intentionally invalid input + expect(isHoliday({ targetDate: new Date(2024, 0, 1), stateCode })).toBe(true); + // @ts-expect-error: intentionally invalid input + expect(isHoliday({ targetDate: new Date(2024, 5, 10), stateCode })).toBe(false); + } + }); + describe("local calendar date vs UTC instant", () => { it("should read the local calendar day of a UTC-midnight instant, not its UTC day, deriving the expectation from the ambient zone (e.g. '2024-12-25' is local 2024-12-24 in America/Sao_Paulo, UTC-3) so the test is deterministic under vitest, bun and deno", () => { const utcMidnight = new Date("2024-12-25"); @@ -102,8 +118,8 @@ describe("isHoliday", () => { ); }); - test("should never throw, regardless of the input", () => { - expectNeverThrows(isHoliday, fc.anything()); + test("should never throw, regardless of the input, prototype chain state codes included", () => { + expectNeverThrows(isHoliday, anyInput); }); }); }); diff --git a/src/is-holiday/is-holiday.ts b/src/is-holiday/is-holiday.ts index b8f5b278..acf594e2 100644 --- a/src/is-holiday/is-holiday.ts +++ b/src/is-holiday/is-holiday.ts @@ -21,7 +21,13 @@ export type IsHolidayOptions = { * (`new Date(2024, 11, 25)`) or from a full ISO datetime when you mean a specific local day. * * If `stateCode` is provided but is not a valid/known state code, it is ignored and only - * national holidays are considered (same behavior as `getHolidays`). + * national holidays are considered (same behavior as `getHolidays`). The lookup is an + * own-property one, so a prototype-chain key such as `"__proto__"` or `"constructor"` is an + * unknown state code like any other. + * + * The date a state holiday is checked against is the statutory one, except for Santa Catarina's + * two holidays, which `getHolidays` moves to the following Sunday when they fall Monday to + * Friday, as Lei SC nº 18.531/2022 requires. * * @param {IsHolidayOptions} [options] - Options for the check. * @param {Date} options.targetDate - The date to check. @@ -40,16 +46,20 @@ export type IsHolidayOptions = { * The underlying national holidays are the ones `getHolidays` computes; see its JSDoc (and * `src/get-holidays/constants.ts` for state holidays) for the full set of laws behind them. * - * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l0662.htm Lei 662/1949, the base - * national holidays law. - * @see Official: https://www.planalto.gov.br/ccivil_03/leis/2002/l10607.htm Lei 10.607/2002, - * added Tiradentes and Finados. - * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l6802.htm Lei 6.802/1980, declared - * Nossa Senhora Aparecida a national holiday. - * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2023/lei/l14759.htm Lei - * 14.759/2023, nationalized Dia da Consciência Negra from 2024. - * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l9093.htm Lei 9.093/1995, the - * framework law authorizing state and municipal holidays. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l0662.htm + * Lei 662/1949, the base national holidays law. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/2002/l10607.htm + * Lei 10.607/2002, added Tiradentes and Finados. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l6802.htm + * Lei 6.802/1980, declared Nossa Senhora Aparecida a national holiday. + * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2023/lei/l14759.htm + * Lei 14.759/2023, nationalized Dia da Consciência Negra from 2024. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l9093.htm + * Lei 9.093/1995, the framework law authorizing state and municipal holidays. + * @see Official: https://www.in.gov.br/web/dou/-/portaria-mgi-n-11.460-de-29-de-dezembro-de-2025-678388627 + * Portaria MGI nº 11.460/2025, the federal executive's annual calendar of feriados nacionais and + * pontos facultativos, the only source behind the Easter-derived entries; see the `getHolidays` + * JSDoc for why Sexta-feira Santa is typed `national` without a law of its own. */ export const isHoliday = (options?: IsHolidayOptions): boolean => { if (isNullish(options) || typeof options !== "object") { From 7cb4fedc91cfe511c52191d3db4318e7811fd5c3 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:43:13 -0300 Subject: [PATCH 03/75] test(runtime): honour the vitest mockClear semantics and rethrow matcher usage errors under not --- src/_internals/test/runtime-deno.ts | 41 ++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/_internals/test/runtime-deno.ts b/src/_internals/test/runtime-deno.ts index db72f70d..72bd581b 100644 --- a/src/_internals/test/runtime-deno.ts +++ b/src/_internals/test/runtime-deno.ts @@ -13,6 +13,7 @@ type MockImplementation = (...args: unknown[]) => unknown; type MockFunction = ((...args: unknown[]) => unknown) & { mock: { calls: unknown[][] }; mockClear: () => void; + mockReset: () => void; mockRejectedValue: (value: unknown) => MockFunction; mockRejectedValueOnce: (value: unknown) => MockFunction; mockResolvedValue: (value: unknown) => MockFunction; @@ -35,8 +36,20 @@ function hasLength(value: unknown): value is { length: number } { return isRecord(value) && typeof value["length"] === "number"; } +class AssertionMismatch extends Error { + public constructor(message: string) { + super(message); + + this.name = "AssertionMismatch"; + } +} + function createAssertionError(message: string): Error { - return new Error(message); + return new AssertionMismatch(message); +} + +function createUsageError(message: string): Error { + return new TypeError(message); } function describeValue(value: unknown): string { @@ -122,8 +135,12 @@ function createMock(implementation?: MockImplementation): MockFunction { const mockFn: MockFunction = Object.assign(baseFn, { mock: { calls }, mockClear: (): void => { + calls.length = 0; + }, + mockReset: (): void => { queue.length = 0; calls.length = 0; + currentImplementation = implementation; }, mockResolvedValueOnce: (value: unknown): MockFunction => { queue.push(() => Promise.resolve(value)); @@ -306,7 +323,7 @@ const createCollectionMatchers = (actual: unknown): Matchers => ({ }, toMatch(expected: RegExp | string): void { if (typeof actual !== "string") { - throw createAssertionError("Expected value to be a string"); + throw createUsageError("Expected value to be a string"); } if (expected instanceof RegExp) { @@ -323,7 +340,7 @@ const createCollectionMatchers = (actual: unknown): Matchers => ({ }, toContainEqual(expected: unknown): void { if (!Array.isArray(actual)) { - throw createAssertionError("Expected value to be an array"); + throw createUsageError("Expected value to be an array"); } if (!actual.some((value) => deepEqual(value, expected))) { @@ -337,7 +354,7 @@ const createCollectionMatchers = (actual: unknown): Matchers => ({ }, toHaveLength(expected: number): void { if (!hasLength(actual)) { - throw createAssertionError("Expected value to have a length"); + throw createUsageError("Expected value to have a length"); } if (actual.length !== expected) { @@ -354,7 +371,7 @@ const createCollectionMatchers = (actual: unknown): Matchers => ({ const createBehaviorMatchers = (actual: unknown): Matchers => ({ toThrow(expected?: ThrowExpectation): void { if (!isCallable(actual)) { - throw createAssertionError("Expected value to be a function"); + throw createUsageError("Expected value to be a function"); } try { @@ -369,7 +386,7 @@ const createBehaviorMatchers = (actual: unknown): Matchers => ({ }, toHaveBeenCalled(): void { if (!isMockFunction(actual)) { - throw createAssertionError("Expected value to be a mock function"); + throw createUsageError("Expected value to be a mock function"); } if (actual.mock.calls.length === 0) { @@ -378,7 +395,7 @@ const createBehaviorMatchers = (actual: unknown): Matchers => ({ }, toHaveBeenCalledTimes(expected: number): void { if (!isMockFunction(actual)) { - throw createAssertionError("Expected value to be a mock function"); + throw createUsageError("Expected value to be a mock function"); } if (actual.mock.calls.length !== expected) { @@ -416,8 +433,12 @@ function createExpect(actual: unknown): ExpectResult { (...args: unknown[]): void => { try { matcher(...args); - } catch { - return; + } catch (error) { + if (error instanceof AssertionMismatch) { + return; + } + + throw error; } throw createAssertionError(`Expected value not to satisfy ${name}`); @@ -559,7 +580,7 @@ export const vi = { fn: createMock, restoreAllMocks: (): void => { for (const mockFn of registeredMocks) { - mockFn.mockClear(); + mockFn.mockReset(); } }, }; From 688359f200b7b4a7a2d68e668c1daf4d6b3424a2 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:43:13 -0300 Subject: [PATCH 04/75] feat(business-days): follow the date-fns signatures and add subBusinessDays --- .../is-supported-holiday-year.test.ts | 18 ++ .../is-supported-holiday-year.ts | 22 ++ src/_internals/test/arbitraries.ts | 31 ++- src/_internals/test/properties.ts | 17 ++ .../add-business-days.test.ts | 175 ++++++------ src/add-business-days/add-business-days.ts | 112 ++++---- .../difference-in-business-days.test.ts | 252 +++++++----------- .../difference-in-business-days.ts | 126 ++++----- src/is-business-day/is-business-day.test.ts | 54 +++- src/is-business-day/is-business-day.ts | 47 ++-- .../sub-business-days.test.ts | 149 +++++++++++ src/sub-business-days/sub-business-days.ts | 62 +++++ 12 files changed, 662 insertions(+), 403 deletions(-) create mode 100644 src/_internals/is-supported-holiday-year/is-supported-holiday-year.test.ts create mode 100644 src/_internals/is-supported-holiday-year/is-supported-holiday-year.ts create mode 100644 src/sub-business-days/sub-business-days.test.ts create mode 100644 src/sub-business-days/sub-business-days.ts diff --git a/src/_internals/is-supported-holiday-year/is-supported-holiday-year.test.ts b/src/_internals/is-supported-holiday-year/is-supported-holiday-year.test.ts new file mode 100644 index 00000000..35ae5b4b --- /dev/null +++ b/src/_internals/is-supported-holiday-year/is-supported-holiday-year.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "../test/runtime"; +import { isSupportedHolidayYear } from "./is-supported-holiday-year"; + +describe("isSupportedHolidayYear", () => { + test("should accept a year inside the range the holiday tables cover", () => { + expect(isSupportedHolidayYear(2024)).toBe(true); + }); + + test("should accept both bounds, 1900 and 2099, inclusively", () => { + expect(isSupportedHolidayYear(1900)).toBe(true); + expect(isSupportedHolidayYear(2099)).toBe(true); + }); + + test("should reject the year right below and right above the range", () => { + expect(isSupportedHolidayYear(1899)).toBe(false); + expect(isSupportedHolidayYear(2100)).toBe(false); + }); +}); diff --git a/src/_internals/is-supported-holiday-year/is-supported-holiday-year.ts b/src/_internals/is-supported-holiday-year/is-supported-holiday-year.ts new file mode 100644 index 00000000..d157d2bf --- /dev/null +++ b/src/_internals/is-supported-holiday-year/is-supported-holiday-year.ts @@ -0,0 +1,22 @@ +import { HOLIDAYS_MAX_YEAR, HOLIDAYS_MIN_YEAR } from "../constants/holidays"; + +/** + * Checks whether a year is inside the range the holiday tables cover. + * + * `getHolidays` only computes 1900 through 2099, so every date utility built on top of it + * (`isBusinessDay`, `addBusinessDays`, `subBusinessDays`, `differenceInBusinessDays`) refuses a + * year outside that range instead of silently answering as if there were no holidays in it. + * + * @param {number} year - The full year to check, as `Date#getFullYear` reports it. + * @returns {boolean} True when the bundled holiday tables cover the year. + * + * @example + * ```typescript + * isSupportedHolidayYear(2024); // true + * isSupportedHolidayYear(1900); // true (inclusive lower bound) + * isSupportedHolidayYear(2099); // true (inclusive upper bound) + * isSupportedHolidayYear(2100); // false + * ``` + */ +export const isSupportedHolidayYear = (year: number): boolean => + year >= HOLIDAYS_MIN_YEAR && year <= HOLIDAYS_MAX_YEAR; diff --git a/src/_internals/test/arbitraries.ts b/src/_internals/test/arbitraries.ts index a606b5eb..4bb3dc57 100644 --- a/src/_internals/test/arbitraries.ts +++ b/src/_internals/test/arbitraries.ts @@ -38,7 +38,11 @@ export const anyValue: fc.Arbitrary = fc.oneof( /** ASCII alphanumeric text, at most twelve characters long. */ export const asciiAlphanumericText: fc.Arbitrary = fc.stringMatching(/^[0-9A-Za-z]{0,12}$/); -/** Booleans, `null`, numbers, strings, arrays and plain objects, including nested primitives. */ +/** + * Booleans, `null`, numbers, strings, arrays and plain objects, including nested primitives, + * plus null-prototype objects: an object built with `Object.create(null)` has no `toString`, + * so it is the shape that catches a util reaching a sanitizer behind a nullish guard alone. + */ export const anyGarbage: fc.Arbitrary = fc.oneof( fc.boolean(), fc.constant(null), @@ -46,6 +50,7 @@ export const anyGarbage: fc.Arbitrary = fc.oneof( fc.string(), fc.array(anyPrimitive), fc.object({ key: fc.constantFrom("a", "b", "c") }), + fc.object({ withNullPrototype: true }), ); /** @@ -140,6 +145,30 @@ export const businessDayDates: fc.Arbitrary = fc.date({ noInvalidDate: true, }); +/** + * `Object.prototype`'s own keys: the ones a lookup must resolve as unknown rather than reach + * through the prototype chain. + */ +export const PROTOTYPE_KEYS: string[] = Object.getOwnPropertyNames(Object.prototype); + +/** A date, or anything at all: what a business day util may be handed as its date argument. */ +export const anyBusinessDayDate: fc.Arbitrary = fc.oneof(businessDayDates, fc.anything()); + +/** A number of business days, or anything at all: what a business day util may be asked to walk. */ +export const anyBusinessDayAmount: fc.Arbitrary = fc.oneof( + fc.integer({ min: -200, max: 200 }), + fc.anything(), +); + +const anyStateCode = fc.oneof(fc.constantFrom(...PROTOTYPE_KEYS, "SP", "xx"), fc.anything()); +const anyIncludeOptional = fc.oneof(fc.boolean(), fc.anything()); + +/** Business day options, or anything at all, prototype chain keys as the state code included. */ +export const anyBusinessDayOptions: fc.Arbitrary = fc.oneof( + fc.anything(), + fc.record({ stateCode: anyStateCode, includeOptional: anyIncludeOptional }), +); + /** An amount with at most two decimals, the precision currency formatting round-trips. */ export const twoDecimalAmounts: fc.Arbitrary = fc .integer({ min: -1_000_000_000, max: 1_000_000_000 }) diff --git a/src/_internals/test/properties.ts b/src/_internals/test/properties.ts index 08e38313..e4005002 100644 --- a/src/_internals/test/properties.ts +++ b/src/_internals/test/properties.ts @@ -40,6 +40,23 @@ export const expectNeverThrowsWithOptions = ( ); }; +/** + * Asserts the util never throws for any argument list the arbitrary produces. + * @param {Function} fn The util under test. + * @param {fc.Arbitrary} argumentLists The argument lists to spread into it. + * @returns {void} Nothing. + */ +export const expectNeverThrowsWithArguments = ( + fn: (...args: never[]) => unknown, + argumentLists: fc.Arbitrary, +): void => { + fc.assert( + fc.property(argumentLists, (values) => { + expect(() => fn(...(values as never[]))).not.toThrow(); + }), + ); +}; + /** * Asserts the util always returns a value of `expectedType`, whatever the arbitrary produces. * @param {UnknownInputFunction} fn The util under test. diff --git a/src/add-business-days/add-business-days.test.ts b/src/add-business-days/add-business-days.test.ts index 9bcbc3ae..b4929081 100644 --- a/src/add-business-days/add-business-days.test.ts +++ b/src/add-business-days/add-business-days.test.ts @@ -1,68 +1,69 @@ import * as fc from "fast-check"; -import { type StateCode } from "../_internals/constants/states"; -import { businessDayDates } from "../_internals/test/arbitraries"; -import { expectNeverThrows } from "../_internals/test/properties"; +import { + anyBusinessDayAmount, + anyBusinessDayDate, + anyBusinessDayOptions, + businessDayDates, + PROTOTYPE_KEYS, +} from "../_internals/test/arbitraries"; +import { expectNeverThrowsWithArguments } from "../_internals/test/properties"; import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; -import { isBusinessDay } from "../is-business-day/is-business-day"; -import { addBusinessDays, type AddBusinessDaysParams } from "./add-business-days"; +import { type BusinessDayOptions, isBusinessDay } from "../is-business-day/is-business-day"; +import { addBusinessDays } from "./add-business-days"; describe("addBusinessDays", () => { it("should match the date-fns addBusinessDays example (10 business days from 2014-09-01 lands on 2014-09-15, https://date-fns.org/docs/addBusinessDays)", () => { - const result = addBusinessDays({ date: new Date(2014, 8, 1), days: 10 }); + const result = addBusinessDays(new Date(2014, 8, 1), 10); expect(result).toEqual(new Date(2014, 8, 15)); }); it("should skip a weekend when the very next day is a business day (Tue 2024-01-02 + 1 -> Wed 2024-01-03, noon)", () => { - const result = addBusinessDays({ date: new Date(2024, 0, 2, 12), days: 1 }); + const result = addBusinessDays(new Date(2024, 0, 2, 12), 1); expect(result).toEqual(new Date(2024, 0, 3, 12)); }); it("should skip Saturday and Sunday to land on the next Monday (Fri 2024-01-05 + 1)", () => { - const result = addBusinessDays({ date: new Date(2024, 0, 5, 12), days: 1 }); + const result = addBusinessDays(new Date(2024, 0, 5, 12), 1); expect(result).toEqual(new Date(2024, 0, 8, 12)); }); describe("supported years", () => { it("should return null when the date is outside 1900-2099 (Mon 2100-01-04)", () => { - expect(addBusinessDays({ date: new Date(2100, 0, 4, 12), days: 1 })).toBeNull(); + expect(addBusinessDays(new Date(2100, 0, 4, 12), 1)).toBeNull(); }); it("should return null when the walk leaves 2099 (Thu 2099-12-31 + 1) or 1900 (Tue 1900-01-02 - 1)", () => { - expect(addBusinessDays({ date: new Date(2099, 11, 31, 12), days: 1 })).toBeNull(); - expect(addBusinessDays({ date: new Date(1900, 0, 2, 12), days: -1 })).toBeNull(); + expect(addBusinessDays(new Date(2099, 11, 31, 12), 1)).toBeNull(); + expect(addBusinessDays(new Date(1900, 0, 2, 12), -1)).toBeNull(); }); it("should return null instead of looping when the date is the maximum representable Date", () => { - expect(addBusinessDays({ date: new Date(8.64e15), days: 1 })).toBeNull(); + expect(addBusinessDays(new Date(8.64e15), 1)).toBeNull(); }); it("should accept the inclusive boundary years 1900 and 2099", () => { - expect(addBusinessDays({ date: new Date(1900, 0, 2), days: 0 })).toEqual( - new Date(1900, 0, 2), - ); - expect(addBusinessDays({ date: new Date(2099, 0, 2), days: 0 })).toEqual( - new Date(2099, 0, 2), - ); + expect(addBusinessDays(new Date(1900, 0, 2), 0)).toEqual(new Date(1900, 0, 2)); + expect(addBusinessDays(new Date(2099, 0, 2), 0)).toEqual(new Date(2099, 0, 2)); }); - it("should return null for a date outside the supported range even when days is 0", () => { - expect(addBusinessDays({ date: new Date(2150, 0, 1), days: 0 })).toBeNull(); + it("should return null for a date outside the supported range even when the amount is 0", () => { + expect(addBusinessDays(new Date(2150, 0, 1), 0)).toBeNull(); }); }); describe("national holidays and year boundaries", () => { it("should skip Ano novo across a year boundary (2024-12-31 + 1 -> 2025-01-02)", () => { - const result = addBusinessDays({ date: new Date(2024, 11, 31, 12), days: 1 }); + const result = addBusinessDays(new Date(2024, 11, 31, 12), 1); expect(result).toEqual(new Date(2025, 0, 2, 12)); }); it("should treat 2025-01-01 (Ano novo) as a holiday, not counted towards the business days", () => { - const result = addBusinessDays({ date: new Date(2024, 11, 30, 12), days: 2 }); + const result = addBusinessDays(new Date(2024, 11, 30, 12), 2); expect(result).toEqual(new Date(2025, 0, 2, 12)); }); @@ -70,13 +71,13 @@ describe("addBusinessDays", () => { describe("state holidays", () => { it("should skip a state holiday when stateCode is provided (SP, Revolução Constitucionalista 2024-07-09)", () => { - const result = addBusinessDays({ date: new Date(2024, 6, 8, 12), days: 1, stateCode: "SP" }); + const result = addBusinessDays(new Date(2024, 6, 8, 12), 1, { stateCode: "SP" }); expect(result).toEqual(new Date(2024, 6, 10, 12)); }); - it("should not skip the same date when stateCode is not provided", () => { - const result = addBusinessDays({ date: new Date(2024, 6, 8, 12), days: 1 }); + it("should not skip the same date when no options are provided", () => { + const result = addBusinessDays(new Date(2024, 6, 8, 12), 1); expect(result).toEqual(new Date(2024, 6, 9, 12)); }); @@ -84,113 +85,108 @@ describe("addBusinessDays", () => { describe("includeOptional", () => { it("should skip Carnaval 2024-02-13 by default (includeOptional defaults to true)", () => { - const result = addBusinessDays({ date: new Date(2024, 1, 12, 12), days: 1 }); + const result = addBusinessDays(new Date(2024, 1, 12, 12), 1); expect(result).toEqual(new Date(2024, 1, 14, 12)); }); it("should count Carnaval 2024-02-13 as a business day when includeOptional is false", () => { - const result = addBusinessDays({ - date: new Date(2024, 1, 12, 12), - days: 1, - includeOptional: false, - }); + const result = addBusinessDays(new Date(2024, 1, 12, 12), 1, { includeOptional: false }); expect(result).toEqual(new Date(2024, 1, 13, 12)); }); }); - describe("negative days", () => { + describe("negative amounts", () => { it("should walk backwards, skipping weekends (Fri 2024-01-05 - 1 -> Thu 2024-01-04)", () => { - const result = addBusinessDays({ date: new Date(2024, 0, 5, 12), days: -1 }); + const result = addBusinessDays(new Date(2024, 0, 5, 12), -1); expect(result).toEqual(new Date(2024, 0, 4, 12)); }); it("should walk backwards across a weekend (Mon 2024-01-08 - 1 -> Fri 2024-01-05)", () => { - const result = addBusinessDays({ date: new Date(2024, 0, 8, 12), days: -1 }); + const result = addBusinessDays(new Date(2024, 0, 8, 12), -1); expect(result).toEqual(new Date(2024, 0, 5, 12)); }); }); - describe("days: 0", () => { + describe("an amount of 0", () => { it("should return a new Date equal to a business day input, unchanged", () => { const input = new Date(2024, 0, 2, 12); - const result = addBusinessDays({ date: input, days: 0 }); + const result = addBusinessDays(input, 0); expect(result).toEqual(new Date(2024, 0, 2, 12)); expect(result).not.toBe(input); }); it("should return the same calendar day even when it is a Saturday, mirroring date-fns' addBusinessDays(date, 0) behavior of not rolling to the next business day", () => { - const result = addBusinessDays({ date: new Date(2024, 0, 6, 12), days: 0 }); + const result = addBusinessDays(new Date(2024, 0, 6, 12), 0); expect(result).toEqual(new Date(2024, 0, 6, 12)); }); it("should return the same calendar day even when it is a holiday", () => { - const result = addBusinessDays({ date: new Date(2024, 0, 1, 12), days: 0 }); + const result = addBusinessDays(new Date(2024, 0, 1, 12), 0); expect(result).toEqual(new Date(2024, 0, 1, 12)); }); }); describe("invalid input", () => { - it("should return null when params is null", () => { + it("should return null when the date is null", () => { // @ts-expect-error: intentionally invalid input - expect(addBusinessDays(null)).toBeNull(); + expect(addBusinessDays(null, 1)).toBeNull(); }); - it("should return null when params is undefined", () => { + it("should return null when called without arguments", () => { // @ts-expect-error: intentionally invalid input expect(addBusinessDays()).toBeNull(); }); - it("should return null when params is not an object", () => { - // @ts-expect-error: intentionally invalid input - expect(addBusinessDays("2024-01-02")).toBeNull(); + it("should return null when the date is an invalid Date", () => { + expect(addBusinessDays(new Date("not a date"), 1)).toBeNull(); }); - it('should return null when params is a function, even one carrying date/days properties (typeof params !== "object" must reject it, not just isNullish)', () => { - const fakeParams = Object.assign(() => null, { date: new Date(2024, 0, 2), days: 1 }); - - expect(addBusinessDays(fakeParams)).toBeNull(); + it("should return null when the date is not a Date", () => { + // @ts-expect-error: intentionally invalid input + expect(addBusinessDays("2024-01-02", 1)).toBeNull(); }); - it("should return null when date is an invalid Date", () => { - expect(addBusinessDays({ date: new Date("not a date"), days: 1 })).toBeNull(); + it("should return null when the amount is not an integer", () => { + expect(addBusinessDays(new Date(2024, 0, 2), 1.5)).toBeNull(); }); - it("should return null when date is not a Date", () => { - // @ts-expect-error: intentionally invalid input - expect(addBusinessDays({ date: "2024-01-02", days: 1 })).toBeNull(); + it("should return null when the amount is NaN", () => { + expect(addBusinessDays(new Date(2024, 0, 2), Number.NaN)).toBeNull(); }); - it("should return null when days is not an integer", () => { - expect(addBusinessDays({ date: new Date(2024, 0, 2), days: 1.5 })).toBeNull(); + it("should return null when the amount is Infinity", () => { + expect(addBusinessDays(new Date(2024, 0, 2), Number.POSITIVE_INFINITY)).toBeNull(); }); - it("should return null when days is NaN", () => { - expect(addBusinessDays({ date: new Date(2024, 0, 2), days: Number.NaN })).toBeNull(); + it("should return null when the amount is not a number", () => { + // @ts-expect-error: intentionally invalid input + expect(addBusinessDays(new Date(2024, 0, 2), "1")).toBeNull(); }); - it("should return null when days is Infinity", () => { - expect( - addBusinessDays({ date: new Date(2024, 0, 2), days: Number.POSITIVE_INFINITY }), - ).toBeNull(); + it("should return null when the stateCode is not a string", () => { + // @ts-expect-error: intentionally invalid input + expect(addBusinessDays(new Date(2024, 0, 2), 1, { stateCode: 123 })).toBeNull(); }); - it("should return null when days is not a number", () => { + it("should ignore options that are not an object", () => { // @ts-expect-error: intentionally invalid input - expect(addBusinessDays({ date: new Date(2024, 0, 2), days: "1" })).toBeNull(); + expect(addBusinessDays(new Date(2024, 6, 8, 12), 1, "SP")).toEqual(new Date(2024, 6, 9, 12)); }); - it("should return null when stateCode is not a string", () => { - expect( - // @ts-expect-error: intentionally invalid input - addBusinessDays({ date: new Date(2024, 0, 2), days: 1, stateCode: 123 }), - ).toBeNull(); + it("should treat a prototype chain key as an unknown stateCode instead of throwing", () => { + for (const stateCode of PROTOTYPE_KEYS) { + expect( + // @ts-expect-error: intentionally invalid input + addBusinessDays(new Date(2024, 0, 2, 12), 1, { stateCode }), + ).toEqual(new Date(2024, 0, 3, 12)); + } }); }); @@ -198,13 +194,13 @@ describe("addBusinessDays", () => { const input = new Date(2024, 0, 2, 12); const before = input.getTime(); - addBusinessDays({ date: input, days: 5 }); + addBusinessDays(input, 5); expect(input.getTime()).toBe(before); }); it("should preserve the time-of-day of the input", () => { - const result = addBusinessDays({ date: new Date(2024, 0, 2, 9, 30, 15, 500), days: 1 }); + const result = addBusinessDays(new Date(2024, 0, 2, 9, 30, 15, 500), 1); expect(result?.getHours()).toBe(9); expect(result?.getMinutes()).toBe(30); @@ -213,18 +209,21 @@ describe("addBusinessDays", () => { }); describe("properties", () => { - const daysArbitrary = fc.integer({ min: -200, max: 200 }); + const amounts = fc.integer({ min: -200, max: 200 }); - test("should never throw, regardless of the input", () => { - expectNeverThrows(addBusinessDays, fc.anything()); + test("should never throw, regardless of the input, prototype chain state codes included", () => { + expectNeverThrowsWithArguments( + addBusinessDays, + fc.tuple(anyBusinessDayDate, anyBusinessDayAmount, anyBusinessDayOptions), + ); }); - test("should land on a business day whenever a non-zero number of days is requested", () => { + test("should land on a business day whenever a non-zero amount is requested", () => { fc.assert( - fc.property(businessDayDates, daysArbitrary, (date, days) => { - if (days === 0) return; + fc.property(businessDayDates, amounts, (date, amount) => { + if (amount === 0) return; - const result = addBusinessDays({ date, days }); + const result = addBusinessDays(date, amount); if (result !== null) { expect(isBusinessDay(result)).toBe(true); @@ -233,16 +232,16 @@ describe("addBusinessDays", () => { ); }); - test("should move the date forward for positive days and backward for negative days", () => { + test("should move the date forward for a positive amount and backward for a negative one", () => { fc.assert( - fc.property(businessDayDates, daysArbitrary, (date, days) => { - const result = addBusinessDays({ date, days }); + fc.property(businessDayDates, amounts, (date, amount) => { + const result = addBusinessDays(date, amount); if (result === null) return; - if (days > 0) { + if (amount > 0) { expect(result.getTime()).toBeGreaterThan(date.getTime()); - } else if (days < 0) { + } else if (amount < 0) { expect(result.getTime()).toBeLessThan(date.getTime()); } else { expect(result.getTime()).toBe(date.getTime()); @@ -254,14 +253,10 @@ describe("addBusinessDays", () => { }); describe("addBusinessDays types", () => { - test("should take an AddBusinessDaysParams and return a Date or null", () => { - expectTypeOf(addBusinessDays).parameter(0).toEqualTypeOf(); - expectTypeOf().toEqualTypeOf<{ - date: Date; - days: number; - stateCode?: StateCode; - includeOptional?: boolean; - }>(); + test("should take a Date, a number and optional BusinessDayOptions, and return a Date or null", () => { + expectTypeOf(addBusinessDays).parameter(0).toEqualTypeOf(); + expectTypeOf(addBusinessDays).parameter(1).toEqualTypeOf(); + expectTypeOf(addBusinessDays).parameter(2).toEqualTypeOf(); expectTypeOf(addBusinessDays).returns.toEqualTypeOf(); }); }); diff --git a/src/add-business-days/add-business-days.ts b/src/add-business-days/add-business-days.ts index a31d2be2..bed87cad 100644 --- a/src/add-business-days/add-business-days.ts +++ b/src/add-business-days/add-business-days.ts @@ -1,102 +1,88 @@ -import { HOLIDAYS_MAX_YEAR, HOLIDAYS_MIN_YEAR } from "../_internals/constants/holidays"; -import { type StateCode } from "../_internals/constants/states"; -import { isNullish } from "../_internals/is-nullish/is-nullish"; -import { isBusinessDay } from "../is-business-day/is-business-day"; - -/** The parameters `addBusinessDays` takes: the date to count from, how many business days to add and which holidays count. */ -export type AddBusinessDaysParams = { - /** The date to count from. Never mutated: a new `Date` is returned. */ - date: Date; - /** Number of business days to add; a negative value walks backwards. Must be a finite integer. */ - days: number; - /** Two letter state code whose state holidays are also treated as non-business days (default: national holidays only). */ - stateCode?: StateCode; - /** Whether optional-type holidays (e.g. Carnaval, Corpus Christi) count as non-business days (default: `true`, matching Brazilian banking practice). */ - includeOptional?: boolean; -}; - -const isSupportedYear = (date: Date): boolean => { - const year = date.getFullYear(); - - return year >= HOLIDAYS_MIN_YEAR && year <= HOLIDAYS_MAX_YEAR; -}; +import { isSupportedHolidayYear } from "../_internals/is-supported-holiday-year/is-supported-holiday-year"; +import { type BusinessDayOptions, isBusinessDay } from "../is-business-day/is-business-day"; /** * Adds a number of Brazilian business days (dias úteis) to a date. * * A business day is a day for which `isBusinessDay` returns `true` (not a Saturday, a - * Sunday, or a Brazilian holiday), evaluated with the same `stateCode`/`includeOptional` - * options. The function walks one calendar day at a time, in the direction of `days`, - * counting only business days, so it is exact regardless of the arrangement of holidays - * around `date` (cheap in practice: `getHolidays` is memoized per year). + * Sunday, or a Brazilian holiday), evaluated with the same `options`. The function walks one + * calendar day at a time, in the direction of `amount`, counting only business days, so it is + * exact regardless of the arrangement of holidays around `date` (cheap in practice: + * `getHolidays` is memoized per year). * - * `days: 0` returns a **new `Date` equal to `date`, unchanged**, even when `date` itself + * `amount: 0` returns a **new `Date` equal to `date`, unchanged**, even when `date` itself * falls on a weekend or holiday. This mirrors the verified behavior of date-fns' * `addBusinessDays(date, 0)`, which also returns the input date as-is rather than rolling - * it to the next business day; see `@see` below. A negative `days` walks backwards, one - * business day at a time, exactly like date-fns. + * it to the next business day; see `@see` below. A negative `amount` walks backwards, one + * business day at a time, exactly like date-fns; `subBusinessDays` is the same walk spelled + * positively. * * The time-of-day (hours, minutes, seconds, milliseconds) of `date` is preserved in the * result, and `date` itself is never mutated. * - * If `stateCode` is provided but is not a valid/known state code, it is ignored and only - * national holidays are considered (same behavior as `getHolidays`/`isBusinessDay`). + * If `options.stateCode` is provided but is not a valid/known state code, it is ignored and + * only national holidays are considered (same behavior as `getHolidays`/`isBusinessDay`), so a + * prototype-chain key such as `"__proto__"` is an unknown state code like any other. An + * `options` that is not an object at all is ignored, exactly as `isBusinessDay` ignores it. * * Only years from 1900 through 2099 are supported, the range `getHolidays` computes. A `date` * outside it, or a walk that leaves it, returns `null`. * - * @param {AddBusinessDaysParams} params - The parameters for the calculation. - * @param {Date} params.date - The date to count from. - * @param {number} params.days - The number of business days to add (negative to subtract). - * @param {StateCode} [params.stateCode] - Brazilian state code whose state holidays are also considered. - * @param {boolean} [params.includeOptional] - Whether optional holidays count as non-business days (default: `true`). - * @returns {Date | null} A new `Date`, `days` business days after `date`. `null` on bad - * input: a `params` that is not an object, a `date` that is not a valid `Date` or is outside - * 1900-2099, a `days` that is not a finite integer, a `stateCode` that is not a string, or a - * walk that leaves the supported years. + * @param {Date} date - The date to count from. Never mutated: a new `Date` is returned. + * @param {number} amount - The number of business days to add; a negative value walks backwards. + * @param {BusinessDayOptions} [options] - Which holidays count as non-business days. + * @param {StateCode} [options.stateCode] - Brazilian state code whose state holidays are also considered. + * @param {boolean} [options.includeOptional] - Whether optional holidays count as non-business days (default: `true`). + * @returns {Date | null} A new `Date`, `amount` business days after `date`. `null` on bad + * input: a `date` that is not a valid `Date` or is outside 1900-2099, an `amount` that is not a + * finite integer, a `stateCode` that is not a string, or a walk that leaves the supported years. * * @example * ```typescript - * addBusinessDays({ date: new Date(2024, 0, 2, 12), days: 1 }); // Wed 2024-01-03, 12:00 (the next day is already a business day) - * addBusinessDays({ date: new Date(2024, 11, 31, 12), days: 1 }); // Thu 2025-01-02, 12:00 (Jan 1 is Ano novo, skipped) - * addBusinessDays({ date: new Date(2024, 0, 5, 12), days: -1 }); // Thu 2024-01-04, 12:00 (walks backwards) - * addBusinessDays({ date: new Date(2024, 0, 6, 12), days: 0 }); // Sat 2024-01-06, 12:00 (unchanged, even though Saturday is not a business day) - * addBusinessDays({ date: new Date("not a date"), days: 1 }); // null - * addBusinessDays({ date: new Date(2024, 0, 2), days: 1.5 }); // null (not an integer) - * addBusinessDays({ date: new Date(2099, 11, 31), days: 1 }); // null (the walk leaves the supported years) - * addBusinessDays(null); // null + * addBusinessDays(new Date(2024, 0, 2, 12), 1); // Wed 2024-01-03, 12:00 (the next day is already a business day) + * addBusinessDays(new Date(2024, 11, 31, 12), 1); // Thu 2025-01-02, 12:00 (Jan 1 is Ano novo, skipped) + * addBusinessDays(new Date(2024, 0, 5, 12), -1); // Thu 2024-01-04, 12:00 (walks backwards) + * addBusinessDays(new Date(2024, 0, 6, 12), 0); // Sat 2024-01-06, 12:00 (unchanged, even though Saturday is not a business day) + * addBusinessDays(new Date(2024, 6, 8, 12), 1, { stateCode: "SP" }); // Wed 2024-07-10, 12:00 (Jul 9 is a state holiday in SP) + * addBusinessDays(new Date("not a date"), 1); // null + * addBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) + * addBusinessDays(new Date(2099, 11, 31), 1); // null (the walk leaves the supported years) + * addBusinessDays(null, 1); // null * ``` * - * @see Based on: https://date-fns.org/docs/addBusinessDays Reference behavior for `days: 0` and - * for walking backwards on a negative `days`. The underlying holiday determination's official - * sources are cited in `isBusinessDay`/`getHolidays`. + * @see Based on: https://date-fns.org/docs/addBusinessDays Reference behavior for `amount: 0`, + * for the positional `(date, amount)` argument order and for walking backwards on a negative + * `amount`. The underlying holiday determination's official sources are cited in + * `isBusinessDay`/`getHolidays`. */ -export const addBusinessDays = (params: AddBusinessDaysParams): Date | null => { - if (isNullish(params) || typeof params !== "object") return null; - - const { date, days, stateCode, includeOptional } = params; - +export const addBusinessDays = ( + date: Date, + amount: number, + options?: BusinessDayOptions, +): Date | null => { if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null; - if (!Number.isInteger(days)) return null; + if (!Number.isInteger(amount)) return null; + + const stateCode = options?.stateCode; if (stateCode !== undefined && typeof stateCode !== "string") return null; - if (!isSupportedYear(date)) return null; + if (!isSupportedHolidayYear(date.getFullYear())) return null; const result = new Date(date); const hours = result.getHours(); - // Stryker disable next-line EqualityOperator: when days is 0, remaining is 0 below and the loop never reads step, so > vs >= here is unobservable - const step = days > 0 ? 1 : -1; - let remaining = Math.abs(days); + // Stryker disable next-line EqualityOperator: when amount is 0, remaining is 0 below and the loop never reads step, so > vs >= here is unobservable + const step = amount > 0 ? 1 : -1; + let remaining = Math.abs(amount); while (remaining > 0) { result.setDate(result.getDate() + step); - if (!isSupportedYear(result)) return null; + if (!isSupportedHolidayYear(result.getFullYear())) return null; - if (isBusinessDay(result, { stateCode, includeOptional })) { + if (isBusinessDay(result, options)) { remaining -= 1; } } diff --git a/src/difference-in-business-days/difference-in-business-days.test.ts b/src/difference-in-business-days/difference-in-business-days.test.ts index 3f3b92da..428530e5 100644 --- a/src/difference-in-business-days/difference-in-business-days.test.ts +++ b/src/difference-in-business-days/difference-in-business-days.test.ts @@ -1,100 +1,66 @@ import * as fc from "fast-check"; -import { type StateCode } from "../_internals/constants/states"; -import { businessDayDates } from "../_internals/test/arbitraries"; -import { expectNeverThrows } from "../_internals/test/properties"; +import { + anyBusinessDayDate, + anyBusinessDayOptions, + businessDayDates, + PROTOTYPE_KEYS, +} from "../_internals/test/arbitraries"; +import { expectNeverThrowsWithArguments } from "../_internals/test/properties"; import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { addBusinessDays } from "../add-business-days/add-business-days"; -import { isBusinessDay } from "../is-business-day/is-business-day"; -import { - differenceInBusinessDays, - type DifferenceInBusinessDaysParams, -} from "./difference-in-business-days"; +import { type BusinessDayOptions, isBusinessDay } from "../is-business-day/is-business-day"; +import { differenceInBusinessDays } from "./difference-in-business-days"; describe("differenceInBusinessDays", () => { - it("should return 0 for the same calendar day", () => { - const result = differenceInBusinessDays({ - from: new Date(2024, 0, 2, 9), - to: new Date(2024, 0, 2, 18), - }); + it("should match the date-fns differenceInBusinessDays example (2014-07-20 minus 2014-01-10 is 136 weekdays, https://date-fns.org/docs/differenceInBusinessDays) minus the 5 Brazilian holidays that fall on a weekday in between (Carnaval, Sexta-feira Santa, Tiradentes, Dia do trabalhador and Corpus Christi)", () => { + const result = differenceInBusinessDays(new Date(2014, 6, 20), new Date(2014, 0, 10)); - expect(result).toBe(0); + expect(result).toBe(131); }); - it("should count the from day when it is a business day and exclude the to day (Tue 2024-01-02 to Wed 2024-01-03)", () => { - const result = differenceInBusinessDays({ - from: new Date(2024, 0, 2), - to: new Date(2024, 0, 3), - }); - - expect(result).toBe(1); + it("should return 0 for the same calendar day", () => { + expect(differenceInBusinessDays(new Date(2024, 0, 2, 18), new Date(2024, 0, 2, 9))).toBe(0); }); - it("should not count the from day when it is a holiday (2024-01-01 Ano novo to 2024-01-02)", () => { - const result = differenceInBusinessDays({ - from: new Date(2024, 0, 1), - to: new Date(2024, 0, 2), - }); - - expect(result).toBe(0); + it("should count the earlier date when it is a business day and exclude the later one (Tue 2024-01-02 to Wed 2024-01-03)", () => { + expect(differenceInBusinessDays(new Date(2024, 0, 3), new Date(2024, 0, 2))).toBe(1); }); - it("should skip weekends between from and to (Fri 2024-01-05 to Mon 2024-01-08)", () => { - const result = differenceInBusinessDays({ - from: new Date(2024, 0, 5), - to: new Date(2024, 0, 8), - }); - - expect(result).toBe(1); + it("should not count the earlier date when it is a holiday (2024-01-01 Ano novo to 2024-01-02)", () => { + expect(differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 1))).toBe(0); }); - it("should ignore the time of day of both from and to", () => { - const result = differenceInBusinessDays({ - from: new Date(2024, 0, 2, 23, 59), - to: new Date(2024, 0, 3, 0, 1), - }); + it("should skip the weekend in between (Fri 2024-01-05 to Mon 2024-01-08)", () => { + expect(differenceInBusinessDays(new Date(2024, 0, 8), new Date(2024, 0, 5))).toBe(1); + }); - expect(result).toBe(1); + it("should ignore the time of day of both dates", () => { + expect(differenceInBusinessDays(new Date(2024, 0, 3, 0, 1), new Date(2024, 0, 2, 23, 59))).toBe( + 1, + ); }); describe("supported years", () => { - it("should return null when from or to is outside 1900-2099", () => { - expect( - differenceInBusinessDays({ from: new Date(2100, 0, 4), to: new Date(2100, 0, 5) }), - ).toBeNull(); - expect( - differenceInBusinessDays({ from: new Date(2099, 11, 31), to: new Date(2100, 0, 4) }), - ).toBeNull(); - expect( - differenceInBusinessDays({ from: new Date(1899, 11, 29), to: new Date(1900, 0, 2) }), - ).toBeNull(); + it("should return null when either date is outside 1900-2099", () => { + expect(differenceInBusinessDays(new Date(2100, 0, 5), new Date(2100, 0, 4))).toBeNull(); + expect(differenceInBusinessDays(new Date(2100, 0, 4), new Date(2099, 11, 31))).toBeNull(); + expect(differenceInBusinessDays(new Date(1900, 0, 2), new Date(1899, 11, 29))).toBeNull(); }); it("should accept the inclusive boundary years 1900 and 2099 (same-day range, so the result is 0 rather than null)", () => { - expect( - differenceInBusinessDays({ from: new Date(1900, 0, 2), to: new Date(1900, 0, 2) }), - ).toBe(0); - expect( - differenceInBusinessDays({ from: new Date(2099, 0, 2), to: new Date(2099, 0, 2) }), - ).toBe(0); + expect(differenceInBusinessDays(new Date(1900, 0, 2), new Date(1900, 0, 2))).toBe(0); + expect(differenceInBusinessDays(new Date(2099, 0, 2), new Date(2099, 0, 2))).toBe(0); }); }); - describe("negative results", () => { - it("should return a negative number when to is before from (Wed 2024-01-03 to Tue 2024-01-02)", () => { - const result = differenceInBusinessDays({ - from: new Date(2024, 0, 3), - to: new Date(2024, 0, 2), - }); - - expect(result).toBe(-1); + describe("sign convention", () => { + it("should return a negative number when the later date is actually before the earlier one (Tue 2024-01-02 given as laterDate, Wed 2024-01-03 as earlierDate)", () => { + expect(differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 3))).toBe(-1); }); - it("should return positive zero, not negative zero, when there are no business days walking backwards (Sun 2024-01-07 to Sat 2024-01-06)", () => { - const result = differenceInBusinessDays({ - from: new Date(2024, 0, 7), - to: new Date(2024, 0, 6), - }); + it("should return positive zero, not negative zero, when there is no business day to count walking backwards (Sat 2024-01-06 given as laterDate, Sun 2024-01-07 as earlierDate)", () => { + const result = differenceInBusinessDays(new Date(2024, 0, 6), new Date(2024, 0, 7)); expect(result).toBe(0); expect(Object.is(result, -0)).toBe(false); @@ -102,51 +68,32 @@ describe("differenceInBusinessDays", () => { }); describe("national holidays and year boundaries", () => { - it("should count business days across a year boundary, skipping Ano novo (2024-12-30 Mon to 2025-01-03 Fri)", () => { - const result = differenceInBusinessDays({ - from: new Date(2024, 11, 30), - to: new Date(2025, 0, 3), - }); - - expect(result).toBe(3); + it("should count business days across a year boundary, skipping Ano novo (Mon 2024-12-30 to Fri 2025-01-03)", () => { + expect(differenceInBusinessDays(new Date(2025, 0, 3), new Date(2024, 11, 30))).toBe(3); }); }); describe("state holidays", () => { it("should skip a state holiday when stateCode is provided (SP, Revolução Constitucionalista 2024-07-09)", () => { - const result = differenceInBusinessDays({ - from: new Date(2024, 6, 8), - to: new Date(2024, 6, 10), + const result = differenceInBusinessDays(new Date(2024, 6, 10), new Date(2024, 6, 8), { stateCode: "SP", }); expect(result).toBe(1); }); - it("should not skip that date when stateCode is not provided", () => { - const result = differenceInBusinessDays({ - from: new Date(2024, 6, 8), - to: new Date(2024, 6, 10), - }); - - expect(result).toBe(2); + it("should not skip that date when no options are provided", () => { + expect(differenceInBusinessDays(new Date(2024, 6, 10), new Date(2024, 6, 8))).toBe(2); }); }); describe("includeOptional", () => { it("should skip Carnaval 2024-02-13 by default (includeOptional defaults to true)", () => { - const result = differenceInBusinessDays({ - from: new Date(2024, 1, 12), - to: new Date(2024, 1, 14), - }); - - expect(result).toBe(1); + expect(differenceInBusinessDays(new Date(2024, 1, 14), new Date(2024, 1, 12))).toBe(1); }); it("should count Carnaval 2024-02-13 as a business day when includeOptional is false", () => { - const result = differenceInBusinessDays({ - from: new Date(2024, 1, 12), - to: new Date(2024, 1, 14), + const result = differenceInBusinessDays(new Date(2024, 1, 14), new Date(2024, 1, 12), { includeOptional: false, }); @@ -155,104 +102,97 @@ describe("differenceInBusinessDays", () => { }); describe("invalid input", () => { - it("should return null when params is null", () => { - // @ts-expect-error: intentionally invalid input - expect(differenceInBusinessDays(null)).toBeNull(); - }); - - it("should return null when params is undefined", () => { + it("should return null when called without arguments", () => { // @ts-expect-error: intentionally invalid input expect(differenceInBusinessDays()).toBeNull(); }); - it("should return null when params is not an object", () => { + it("should return null when the later date is null", () => { // @ts-expect-error: intentionally invalid input - expect(differenceInBusinessDays("2024-01-02")).toBeNull(); + expect(differenceInBusinessDays(null, new Date(2024, 0, 2))).toBeNull(); }); - it('should return null when params is a function, even one carrying from/to properties (typeof params !== "object" must reject it, not just isNullish)', () => { - const fakeParams = Object.assign(() => null, { - from: new Date(2024, 0, 2), - to: new Date(2024, 0, 3), - }); - - expect(differenceInBusinessDays(fakeParams)).toBeNull(); + it("should return null when the later date is an invalid Date", () => { + expect(differenceInBusinessDays(new Date("not a date"), new Date(2024, 0, 2))).toBeNull(); }); - it("should return null when from is an invalid Date", () => { - expect( - differenceInBusinessDays({ from: new Date("not a date"), to: new Date(2024, 0, 2) }), - ).toBeNull(); + it("should return null when the earlier date is an invalid Date", () => { + expect(differenceInBusinessDays(new Date(2024, 0, 2), new Date("not a date"))).toBeNull(); }); - it("should return null when to is an invalid Date", () => { - expect( - differenceInBusinessDays({ from: new Date(2024, 0, 2), to: new Date("not a date") }), - ).toBeNull(); + it("should return null when the later date is not a Date", () => { + // @ts-expect-error: intentionally invalid input + expect(differenceInBusinessDays("2024-01-03", new Date(2024, 0, 2))).toBeNull(); }); - it("should return null when from is not a Date", () => { - expect( - // @ts-expect-error: intentionally invalid input - differenceInBusinessDays({ from: "2024-01-02", to: new Date(2024, 0, 3) }), - ).toBeNull(); + it("should return null when the earlier date is not a Date", () => { + // @ts-expect-error: intentionally invalid input + expect(differenceInBusinessDays(new Date(2024, 0, 3), "2024-01-02")).toBeNull(); }); - it("should return null when to is not a Date", () => { - expect( + it("should return null when the stateCode is not a string", () => { + const result = differenceInBusinessDays(new Date(2024, 0, 3), new Date(2024, 0, 2), { // @ts-expect-error: intentionally invalid input - differenceInBusinessDays({ from: new Date(2024, 0, 2), to: "2024-01-03" }), - ).toBeNull(); + stateCode: 11, + }); + + expect(result).toBeNull(); }); - it("should return null when stateCode is not a string", () => { - expect( - differenceInBusinessDays({ - from: new Date(2024, 0, 2), - to: new Date(2024, 0, 3), - // @ts-expect-error: intentionally invalid input - stateCode: 11, - }), - ).toBeNull(); + it("should ignore options that are not an object", () => { + // @ts-expect-error: intentionally invalid input + expect(differenceInBusinessDays(new Date(2024, 6, 10), new Date(2024, 6, 8), "SP")).toBe(2); }); it("should ignore a stateCode that is not a known state", () => { - const result = differenceInBusinessDays({ - from: new Date(2024, 0, 2), - to: new Date(2024, 0, 3), + const result = differenceInBusinessDays(new Date(2024, 0, 3), new Date(2024, 0, 2), { // @ts-expect-error: intentionally invalid input stateCode: "XX", }); expect(result).toBe(1); }); + + it("should treat a prototype chain key as an unknown stateCode instead of throwing", () => { + for (const stateCode of PROTOTYPE_KEYS) { + expect( + differenceInBusinessDays(new Date(2024, 0, 3), new Date(2024, 0, 2), { + // @ts-expect-error: intentionally invalid input + stateCode, + }), + ).toBe(1); + } + }); }); describe("properties", () => { - const daysArbitrary = fc.integer({ min: -100, max: 100 }); + const amounts = fc.integer({ min: -100, max: 100 }); - test("should never throw, regardless of the input", () => { - expectNeverThrows(differenceInBusinessDays, fc.anything()); + test("should never throw, regardless of the input, prototype chain state codes included", () => { + expectNeverThrowsWithArguments( + differenceInBusinessDays, + fc.tuple(anyBusinessDayDate, anyBusinessDayDate, anyBusinessDayOptions), + ); }); test("should return 0 for the same calendar day", () => { fc.assert( fc.property(businessDayDates, (date) => { - expect(differenceInBusinessDays({ from: date, to: date })).toBe(0); + expect(differenceInBusinessDays(date, date)).toBe(0); }), ); }); test("should undo addBusinessDays when starting from a business day", () => { fc.assert( - fc.property(businessDayDates, daysArbitrary, (from, days) => { - if (!isBusinessDay(from)) return; + fc.property(businessDayDates, amounts, (earlierDate, amount) => { + if (!isBusinessDay(earlierDate)) return; - const to = addBusinessDays({ date: from, days }); + const laterDate = addBusinessDays(earlierDate, amount); - if (to === null) return; + if (laterDate === null) return; - expect(differenceInBusinessDays({ from, to })).toBe(days); + expect(differenceInBusinessDays(laterDate, earlierDate)).toBe(amount); }), ); }); @@ -260,16 +200,12 @@ describe("differenceInBusinessDays", () => { }); describe("differenceInBusinessDays types", () => { - test("should take a DifferenceInBusinessDaysParams and return a number or null", () => { + test("should take two Dates and optional BusinessDayOptions, and return a number or null", () => { + expectTypeOf(differenceInBusinessDays).parameter(0).toEqualTypeOf(); + expectTypeOf(differenceInBusinessDays).parameter(1).toEqualTypeOf(); expectTypeOf(differenceInBusinessDays) - .parameter(0) - .toEqualTypeOf(); - expectTypeOf().toEqualTypeOf<{ - from: Date; - to: Date; - stateCode?: StateCode; - includeOptional?: boolean; - }>(); + .parameter(2) + .toEqualTypeOf(); expectTypeOf(differenceInBusinessDays).returns.toEqualTypeOf(); }); }); diff --git a/src/difference-in-business-days/difference-in-business-days.ts b/src/difference-in-business-days/difference-in-business-days.ts index ad93147e..fd757449 100644 --- a/src/difference-in-business-days/difference-in-business-days.ts +++ b/src/difference-in-business-days/difference-in-business-days.ts @@ -1,25 +1,5 @@ -import { HOLIDAYS_MAX_YEAR, HOLIDAYS_MIN_YEAR } from "../_internals/constants/holidays"; -import { type StateCode } from "../_internals/constants/states"; -import { isNullish } from "../_internals/is-nullish/is-nullish"; -import { isBusinessDay } from "../is-business-day/is-business-day"; - -/** The parameters `differenceInBusinessDays` takes: the two dates to count between and which holidays count. */ -export type DifferenceInBusinessDaysParams = { - /** The date to count from. Counted as a business day when it is one; never mutated. */ - from: Date; - /** The date to count to. Never counted itself, regardless of whether it is a business day. */ - to: Date; - /** Two letter state code whose state holidays are also treated as non-business days (default: national holidays only). */ - stateCode?: StateCode; - /** Whether optional-type holidays (e.g. Carnaval, Corpus Christi) count as non-business days (default: `true`, matching Brazilian banking practice). */ - includeOptional?: boolean; -}; - -const isSupportedYear = (date: Date): boolean => { - const year = date.getFullYear(); - - return year >= HOLIDAYS_MIN_YEAR && year <= HOLIDAYS_MAX_YEAR; -}; +import { isSupportedHolidayYear } from "../_internals/is-supported-holiday-year/is-supported-holiday-year"; +import { type BusinessDayOptions, isBusinessDay } from "../is-business-day/is-business-day"; const toLocalDayTimestamp = (date: Date): number => Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()); @@ -28,74 +8,84 @@ const toLocalDayTimestamp = (date: Date): number => * Counts the number of Brazilian business days (dias úteis) between two dates. * * Mirrors the semantics of date-fns' `differenceInBusinessDays`, verified against its source - * (`differenceInBusinessDays.js` in the `date-fns` package): the day at `from` is counted when - * it is itself a business day, the day at `to` is never counted, and every business day - * strictly in between is counted once. Concretely, the function walks one calendar day at a - * time from `from` towards `to` (or the other way around, when `to` is before `from`), adding - * one for every day that `isBusinessDay` accepts, stopping just before reaching `to`. Only the - * calendar day of each `Date` matters, exactly like `differenceInCalendarDays`: the time of day - * is ignored. + * (`differenceInBusinessDays.js` in the `date-fns` package), argument order included: the walk + * starts at `earlierDate` and stops just before `laterDate`, so **`earlierDate` is counted when + * it is itself a business day and `laterDate` is never counted**, whatever their order, and every + * business day strictly in between is counted once. Only the calendar day of each `Date` matters, + * exactly like `differenceInCalendarDays`: the time of day is ignored. * - * A business day is a day for which `isBusinessDay` returns `true` (not a Saturday, a Sunday, - * or a Brazilian holiday), evaluated with the same `stateCode`/`includeOptional` options. + * The result is positive when `laterDate` is after `earlierDate` and negative when it is before + * it, the date-fns sign convention; two dates on the same calendar day return `0` (a positive + * zero, never `-0`). * - * `from` and `to` on the same calendar day return `0`. A `to` before `from` returns a negative - * number, mirroring date-fns. + * A business day is a day for which `isBusinessDay` returns `true` (not a Saturday, a Sunday, + * or a Brazilian holiday), evaluated with the same `options`. * - * If `stateCode` is provided but is not a valid/known state code, it is ignored and only - * national holidays are considered (same behavior as `getHolidays`/`isBusinessDay`). + * If `options.stateCode` is provided but is not a valid/known state code, it is ignored and only + * national holidays are considered (same behavior as `getHolidays`/`isBusinessDay`), so a + * prototype-chain key such as `"__proto__"` is an unknown state code like any other. An `options` + * that is not an object at all is ignored, exactly as `isBusinessDay` ignores it. * - * Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a `from` - * or `to` outside it returns `null`. + * Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a + * `laterDate` or `earlierDate` outside it returns `null`. * - * @param {DifferenceInBusinessDaysParams} params - The parameters of the calculation. - * @param {Date} params.from - The date to count from. - * @param {Date} params.to - The date to count to. - * @param {StateCode} [params.stateCode] - Brazilian state code whose state holidays are also considered. - * @param {boolean} [params.includeOptional] - Whether optional holidays count as non-business days (default: `true`). - * @returns {number|null} The number of business days between `from` and `to`, or `null` on bad - * input: a `params` that is not an object, a `from`/`to` that is not a valid `Date` or is - * outside 1900-2099, or a `stateCode` that is not a string. + * @param {Date} laterDate - The date to count to. Never counted itself, regardless of whether it is a business day. + * @param {Date} earlierDate - The date to count from. Counted as a business day when it is one; never mutated. + * @param {BusinessDayOptions} [options] - Which holidays count as non-business days. + * @param {StateCode} [options.stateCode] - Brazilian state code whose state holidays are also considered. + * @param {boolean} [options.includeOptional] - Whether optional holidays count as non-business days (default: `true`). + * @returns {number | null} The number of business days between the two dates, or `null` on bad + * input: a `laterDate`/`earlierDate` that is not a valid `Date` or is outside 1900-2099, or a + * `stateCode` that is not a string. * * @example * ```typescript - * differenceInBusinessDays({ from: new Date(2024, 0, 1), to: new Date(2024, 0, 2) }); // 0 (Jan 1 is Ano novo) - * differenceInBusinessDays({ from: new Date(2024, 0, 2), to: new Date(2024, 0, 3) }); // 1 (Jan 2 counted, a Tuesday) - * differenceInBusinessDays({ from: new Date(2024, 0, 3), to: new Date(2024, 0, 2) }); // -1 (to before from) - * differenceInBusinessDays({ from: new Date(2024, 0, 2), to: new Date(2024, 0, 2) }); // 0 (same day) - * differenceInBusinessDays({ from: new Date(2024, 6, 8), to: new Date(2024, 6, 10), stateCode: "SP" }); // 1 (Jul 9 is a state holiday in SP) - * differenceInBusinessDays({ from: new Date("not a date"), to: new Date() }); // null - * differenceInBusinessDays({ from: new Date(2100, 0, 4), to: new Date(2100, 0, 5) }); // null (outside the supported years) + * differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 1)); // 0 (Jan 1 is Ano novo, not counted) + * differenceInBusinessDays(new Date(2024, 0, 3), new Date(2024, 0, 2)); // 1 (Jan 2 counted, a Tuesday; Jan 3 is not) + * differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 3)); // -1 (the later date comes first, so the count is negative) + * differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 2)); // 0 (same day) + * differenceInBusinessDays(new Date(2024, 6, 10), new Date(2024, 6, 8), { stateCode: "SP" }); // 1 (Jul 9 is a state holiday in SP) + * differenceInBusinessDays(new Date(), new Date("not a date")); // null + * differenceInBusinessDays(new Date(2100, 0, 5), new Date(2100, 0, 4)); // null (outside the supported years) * ``` * - * @see Based on: https://date-fns.org/docs/differenceInBusinessDays Documented behavior. + * @see Based on: https://date-fns.org/docs/differenceInBusinessDays Documented behavior and the + * positional `(laterDate, earlierDate)` argument order. * @see Based on: https://unpkg.com/date-fns@4.1.0/differenceInBusinessDays.js Source used to - * verify the exact boundary treatment (`from` counted, `to` excluded) and the sign convention. - * The underlying holiday determination's official sources are cited in + * verify the exact boundary treatment (`earlierDate` counted, `laterDate` excluded) and the sign + * convention. The underlying holiday determination's official sources are cited in * `isBusinessDay`/`getHolidays`. */ -export const differenceInBusinessDays = (params: DifferenceInBusinessDaysParams): number | null => { - if (isNullish(params) || typeof params !== "object") return null; +export const differenceInBusinessDays = ( + laterDate: Date, + earlierDate: Date, + options?: BusinessDayOptions, +): number | null => { + if (!(laterDate instanceof Date) || Number.isNaN(laterDate.getTime())) return null; + if (!(earlierDate instanceof Date) || Number.isNaN(earlierDate.getTime())) return null; - const { from, to, stateCode, includeOptional } = params; + const stateCode = options?.stateCode; - if (!(from instanceof Date) || Number.isNaN(from.getTime())) return null; - if (!(to instanceof Date) || Number.isNaN(to.getTime())) return null; if (stateCode !== undefined && typeof stateCode !== "string") return null; - if (!isSupportedYear(from) || !isSupportedYear(to)) return null; + if (!isSupportedHolidayYear(laterDate.getFullYear())) return null; + if (!isSupportedHolidayYear(earlierDate.getFullYear())) return null; - const fromDay = toLocalDayTimestamp(from); - const toDay = toLocalDayTimestamp(to); + const laterDay = toLocalDayTimestamp(laterDate); + const earlierDay = toLocalDayTimestamp(earlierDate); - // Stryker disable next-line EqualityOperator: when fromDay equals toDay, the loop below never runs (movingDate already equals toDay), so < vs <= here is unobservable - const step = fromDay < toDay ? 1 : -1; - const movingDate = new Date(from.getFullYear(), from.getMonth(), from.getDate()); + // Stryker disable next-line EqualityOperator: when the two days are equal, the loop below never runs (movingDate already equals laterDay), so < vs <= here is unobservable + const step = earlierDay < laterDay ? 1 : -1; + const movingDate = new Date( + earlierDate.getFullYear(), + earlierDate.getMonth(), + earlierDate.getDate(), + ); let result = 0; - while (toLocalDayTimestamp(movingDate) !== toDay) { - if (isBusinessDay(movingDate, { stateCode, includeOptional })) result += step; + while (toLocalDayTimestamp(movingDate) !== laterDay) { + if (isBusinessDay(movingDate, options)) result += step; movingDate.setDate(movingDate.getDate() + step); } diff --git a/src/is-business-day/is-business-day.test.ts b/src/is-business-day/is-business-day.test.ts index 138174b2..f6c44e46 100644 --- a/src/is-business-day/is-business-day.test.ts +++ b/src/is-business-day/is-business-day.test.ts @@ -1,16 +1,33 @@ import * as fc from "fast-check"; import { type StateCode } from "../_internals/constants/states"; -import { holidayYears, monthDays, monthIndexes, stateCodes } from "../_internals/test/arbitraries"; +import { + businessDayDates, + holidayYears, + monthDays, + monthIndexes, + stateCodes, +} from "../_internals/test/arbitraries"; import { expectNeverThrowsWithOptions } from "../_internals/test/properties"; import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { getHolidays, type Holiday } from "../get-holidays/get-holidays"; -import { isBusinessDay, type IsBusinessDayOptions } from "./is-business-day"; +import { isBusinessDay, type BusinessDayOptions } from "./is-business-day"; + +const PROTOTYPE_KEYS = Object.getOwnPropertyNames(Object.prototype); function getHolidaysFor(year: number, stateCode: StateCode | null): Holiday[] { return stateCode === null ? getHolidays(year) : getHolidays({ year, stateCode }); } +const anyStateCode = fc.oneof(fc.constantFrom(...PROTOTYPE_KEYS, "SP", "xx"), fc.anything()); +const anyIncludeOptional = fc.oneof(fc.boolean(), fc.anything()); +const hostileOptions = fc.record({ + stateCode: anyStateCode, + includeOptional: anyIncludeOptional, +}); +const anyValueInput = fc.oneof(fc.anything(), businessDayDates); +const anyOptionsInput = fc.oneof(fc.anything(), hostileOptions); + describe("isBusinessDay", () => { it("should return true for a plain weekday that is not a holiday (noon, DST-safe)", () => { expect(isBusinessDay(new Date(2024, 0, 2, 12))).toBe(true); @@ -51,6 +68,29 @@ describe("isBusinessDay", () => { // @ts-expect-error: intentionally invalid input expect(isBusinessDay(new Date(2024, 6, 9, 12), { stateCode: "XX" })).toBe(true); }); + + it("should treat a prototype chain key as an unknown stateCode instead of throwing", () => { + for (const stateCode of PROTOTYPE_KEYS) { + // @ts-expect-error: intentionally invalid input + expect(isBusinessDay(new Date(2024, 6, 9, 12), { stateCode })).toBe(true); + // @ts-expect-error: intentionally invalid input + expect(isBusinessDay(new Date(2024, 0, 1, 12), { stateCode })).toBe(false); + } + }); + + it("should treat Monday 11/08/2025 as a business day in SC, since Lei SC nº 18.531/2022 moves the feriado to Sunday 17/08", () => { + expect(isBusinessDay(new Date(2025, 7, 11, 12), { stateCode: "SC" })).toBe(true); + expect(isBusinessDay(new Date(2025, 7, 17, 12), { stateCode: "SC" })).toBe(false); + }); + + it("should treat Corpus Christi as a non-business day in the DF even with includeOptional false, since Lei distrital nº 72/1989 declares it a feriado", () => { + expect( + isBusinessDay(new Date(2024, 4, 30, 12), { stateCode: "DF", includeOptional: false }), + ).toBe(false); + expect( + isBusinessDay(new Date(2024, 4, 30, 12), { stateCode: "SP", includeOptional: false }), + ).toBe(true); + }); }); describe("includeOptional", () => { @@ -151,8 +191,8 @@ describe("isBusinessDay", () => { ); }); - test("should never throw, regardless of the input", () => { - expectNeverThrowsWithOptions(isBusinessDay, fc.anything(), fc.anything()); + test("should never throw, regardless of the input, prototype chain state codes included", () => { + expectNeverThrowsWithOptions(isBusinessDay, anyValueInput, anyOptionsInput); }); }); }); @@ -160,9 +200,9 @@ describe("isBusinessDay", () => { describe("isBusinessDay types", () => { test("should take a Date, options, and return a boolean", () => { expectTypeOf(isBusinessDay).parameter(0).toEqualTypeOf(); - expectTypeOf(isBusinessDay).parameter(1).toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); + expectTypeOf(isBusinessDay).parameter(1).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); expectTypeOf(isBusinessDay).returns.toEqualTypeOf(); }); }); diff --git a/src/is-business-day/is-business-day.ts b/src/is-business-day/is-business-day.ts index 23ee24a5..c508182f 100644 --- a/src/is-business-day/is-business-day.ts +++ b/src/is-business-day/is-business-day.ts @@ -1,9 +1,12 @@ -import { HOLIDAYS_MAX_YEAR, HOLIDAYS_MIN_YEAR } from "../_internals/constants/holidays"; import { type StateCode } from "../_internals/constants/states"; +import { isSupportedHolidayYear } from "../_internals/is-supported-holiday-year/is-supported-holiday-year"; import { getHolidays } from "../get-holidays/get-holidays"; -/** Options of `isBusinessDay`. */ -export type IsBusinessDayOptions = { +/** + * Options shared by every business day util (`isBusinessDay`, `addBusinessDays`, + * `subBusinessDays` and `differenceInBusinessDays`): which holidays count as non-business days. + */ +export type BusinessDayOptions = { /** Two letter state code whose state holidays are also treated as non-business days (default: national holidays only). */ stateCode?: StateCode; /** Whether optional-type holidays (`Holiday.type === "optional"`, e.g. Carnaval, Corpus Christi) count as non-business days (default: `true`). */ @@ -29,12 +32,20 @@ const WEEKEND_DAYS = new Set([0, 6]); * * If `options.stateCode` is provided but is not a valid/known state code, it is ignored * and only national holidays are considered (same behavior as `getHolidays`/`isHoliday`). + * The lookup is an own-property one, so a prototype-chain key such as `"__proto__"` or + * `"constructor"` is an unknown state code like any other. + * + * Two state rules change what `includeOptional: false` answers. The Distrito Federal declares + * Corpus Christi a feriado (Lei distrital nº 72/1989, art. 1º parágrafo único), so with + * `stateCode: "DF"` it is typed `"state"` and still counts; and Santa Catarina's two holidays + * are observed on the following Sunday when they fall Monday to Friday (Lei SC nº 18.531/2022), + * so 11 August 2025, a Monday, is a business day there. * * Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date * outside it returns `false` rather than silently treating every weekday as a business day. * * @param {Date} value - The date to check. - * @param {IsBusinessDayOptions} [options] - Options for the check. + * @param {BusinessDayOptions} [options] - Which holidays count as non-business days. * @param {StateCode} [options.stateCode] - Brazilian state code whose state holidays are also considered. * @param {boolean} [options.includeOptional] - Whether optional holidays count as non-business days (default: `true`). * @returns {boolean} True when `value` is a business day, false otherwise. Bad input also @@ -57,23 +68,27 @@ const WEEKEND_DAYS = new Set([0, 6]); * The underlying holidays are the ones `getHolidays` computes; see its JSDoc (and * `src/get-holidays/constants.ts` for state holidays) for the full set of laws behind them. * - * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l0662.htm Lei 662/1949, the base - * national holidays law. - * @see Official: https://www.planalto.gov.br/ccivil_03/leis/2002/l10607.htm Lei 10.607/2002, - * added Tiradentes and Finados. - * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l6802.htm Lei 6.802/1980, declared - * Nossa Senhora Aparecida a national holiday. - * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2023/lei/l14759.htm Lei - * 14.759/2023, nationalized Dia da Consciência Negra from 2024. - * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l9093.htm Lei 9.093/1995, the - * framework law authorizing state and municipal holidays. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l0662.htm + * Lei 662/1949, the base national holidays law. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/2002/l10607.htm + * Lei 10.607/2002, added Tiradentes and Finados. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l6802.htm + * Lei 6.802/1980, declared Nossa Senhora Aparecida a national holiday. + * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2023/lei/l14759.htm + * Lei 14.759/2023, nationalized Dia da Consciência Negra from 2024. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l9093.htm + * Lei 9.093/1995, the framework law authorizing state and municipal holidays. + * @see Official: https://www.in.gov.br/web/dou/-/portaria-mgi-n-11.460-de-29-de-dezembro-de-2025-678388627 + * Portaria MGI nº 11.460/2025, the federal executive's annual calendar of feriados nacionais and + * pontos facultativos: the source of Sexta-feira Santa being observed nationally and of Carnaval + * and Corpus Christi being ponto facultativo, which is what `includeOptional` switches on. */ -export const isBusinessDay = (value: Date, options?: IsBusinessDayOptions): boolean => { +export const isBusinessDay = (value: Date, options?: BusinessDayOptions): boolean => { if (!(value instanceof Date) || Number.isNaN(value.getTime())) return false; const year = value.getFullYear(); - if (year < HOLIDAYS_MIN_YEAR || year > HOLIDAYS_MAX_YEAR) return false; + if (!isSupportedHolidayYear(year)) return false; if (WEEKEND_DAYS.has(value.getDay())) return false; diff --git a/src/sub-business-days/sub-business-days.test.ts b/src/sub-business-days/sub-business-days.test.ts new file mode 100644 index 00000000..f428e7a8 --- /dev/null +++ b/src/sub-business-days/sub-business-days.test.ts @@ -0,0 +1,149 @@ +import * as fc from "fast-check"; + +import { + anyBusinessDayAmount, + anyBusinessDayDate, + anyBusinessDayOptions, + businessDayDates, + PROTOTYPE_KEYS, +} from "../_internals/test/arbitraries"; +import { expectNeverThrowsWithArguments } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { addBusinessDays } from "../add-business-days/add-business-days"; +import { type BusinessDayOptions } from "../is-business-day/is-business-day"; +import { subBusinessDays } from "./sub-business-days"; + +const NULL_CALLS: [string, () => Date | null][] = [ + // @ts-expect-error: intentionally invalid input + ["no arguments at all", () => subBusinessDays()], + // @ts-expect-error: intentionally invalid input + ["a null date", () => subBusinessDays(null, 1)], + ["an invalid Date", () => subBusinessDays(new Date("not a date"), 1)], + // @ts-expect-error: intentionally invalid input + ["a date given as a string", () => subBusinessDays("2024-01-05", 1)], + ["an amount with a fractional part", () => subBusinessDays(new Date(2024, 0, 5), 1.5)], + ["an amount of NaN", () => subBusinessDays(new Date(2024, 0, 5), Number.NaN)], + ["an amount of -Infinity", () => subBusinessDays(new Date(2024, 0, 5), Number.NEGATIVE_INFINITY)], + // @ts-expect-error: intentionally invalid input + ["an amount given as a numeric string", () => subBusinessDays(new Date(2024, 0, 5), "1")], + // @ts-expect-error: intentionally invalid input + ["an amount given as null", () => subBusinessDays(new Date(2024, 0, 5), null)], + [ + "a stateCode that is not a string", + // @ts-expect-error: intentionally invalid input + () => subBusinessDays(new Date(2024, 0, 5), 1, { stateCode: 7 }), + ], + ["a date before the supported years", () => subBusinessDays(new Date(1899, 11, 29), 1)], + ["a walk that leaves 1900", () => subBusinessDays(new Date(1900, 0, 2, 12), 1)], + ["a walk that leaves 2099", () => subBusinessDays(new Date(2099, 11, 31, 12), -1)], +]; + +describe("subBusinessDays", () => { + it("should step back to the previous day when it is already a business day (Fri 2024-01-05 - 1 -> Thu 2024-01-04, noon)", () => { + expect(subBusinessDays(new Date(2024, 0, 5, 12), 1)).toEqual(new Date(2024, 0, 4, 12)); + }); + + it("should walk back over Saturday and Sunday (Mon 2024-01-08 - 1 -> Fri 2024-01-05)", () => { + expect(subBusinessDays(new Date(2024, 0, 8, 12), 1)).toEqual(new Date(2024, 0, 5, 12)); + }); + + it("should walk back over Ano novo and a year boundary (Thu 2025-01-02 - 1 -> Tue 2024-12-31)", () => { + expect(subBusinessDays(new Date(2025, 0, 2, 12), 1)).toEqual(new Date(2024, 11, 31, 12)); + }); + + it("should count several business days back at once (Fri 2024-01-12 - 5 -> Fri 2024-01-05)", () => { + expect(subBusinessDays(new Date(2024, 0, 12, 12), 5)).toEqual(new Date(2024, 0, 5, 12)); + }); + + it("should walk forwards for a negative amount (Fri 2024-01-05 - -1 -> Mon 2024-01-08)", () => { + expect(subBusinessDays(new Date(2024, 0, 5, 12), -1)).toEqual(new Date(2024, 0, 8, 12)); + }); + + it("should return a new Date equal to the input for an amount of 0, weekend or not", () => { + const saturday = new Date(2024, 0, 6, 12); + const result = subBusinessDays(saturday, 0); + + expect(result).toEqual(new Date(2024, 0, 6, 12)); + expect(result).not.toBe(saturday); + }); + + it("should keep the time-of-day of the input and leave the input untouched", () => { + const input = new Date(2024, 0, 5, 9, 30, 15, 500); + const result = subBusinessDays(input, 1); + + expect(result).toEqual(new Date(2024, 0, 4, 9, 30, 15, 500)); + expect(input).toEqual(new Date(2024, 0, 5, 9, 30, 15, 500)); + }); + + describe("state holidays", () => { + it("should walk back over a state holiday when stateCode is given (SP, Revolução Constitucionalista 2024-07-09)", () => { + const result = subBusinessDays(new Date(2024, 6, 10, 12), 1, { stateCode: "SP" }); + + expect(result).toEqual(new Date(2024, 6, 8, 12)); + }); + + it("should land on that same holiday without a stateCode", () => { + expect(subBusinessDays(new Date(2024, 6, 10, 12), 1)).toEqual(new Date(2024, 6, 9, 12)); + }); + + it("should treat a prototype chain key as an unknown stateCode instead of throwing", () => { + for (const stateCode of PROTOTYPE_KEYS) { + // @ts-expect-error: intentionally invalid input + expect(subBusinessDays(new Date(2024, 0, 5, 12), 1, { stateCode })).toEqual( + new Date(2024, 0, 4, 12), + ); + } + }); + + it("should ignore options that are not an object", () => { + // @ts-expect-error: intentionally invalid input + expect(subBusinessDays(new Date(2024, 6, 10, 12), 1, "SP")).toEqual(new Date(2024, 6, 9, 12)); + }); + }); + + describe("includeOptional", () => { + it("should walk back over Carnaval 2024-02-13 by default (Wed 2024-02-14 - 1 -> Mon 2024-02-12)", () => { + expect(subBusinessDays(new Date(2024, 1, 14, 12), 1)).toEqual(new Date(2024, 1, 12, 12)); + }); + + it("should stop on Carnaval 2024-02-13 when includeOptional is false", () => { + const result = subBusinessDays(new Date(2024, 1, 14, 12), 1, { includeOptional: false }); + + expect(result).toEqual(new Date(2024, 1, 13, 12)); + }); + }); + + describe("invalid input", () => { + for (const [label, call] of NULL_CALLS) { + it(`should return null for ${label}`, () => { + expect(call()).toBeNull(); + }); + } + }); + + describe("properties", () => { + test("should never throw, regardless of the input, prototype chain state codes included", () => { + expectNeverThrowsWithArguments( + subBusinessDays, + fc.tuple(anyBusinessDayDate, anyBusinessDayAmount, anyBusinessDayOptions), + ); + }); + + test("should be addBusinessDays with the opposite amount", () => { + fc.assert( + fc.property(businessDayDates, fc.integer({ min: -200, max: 200 }), (date, amount) => { + expect(subBusinessDays(date, amount)).toEqual(addBusinessDays(date, -amount)); + }), + ); + }); + }); +}); + +describe("subBusinessDays types", () => { + test("should take a Date, a number and optional BusinessDayOptions, and return a Date or null", () => { + expectTypeOf(subBusinessDays).parameter(0).toEqualTypeOf(); + expectTypeOf(subBusinessDays).parameter(1).toEqualTypeOf(); + expectTypeOf(subBusinessDays).parameter(2).toEqualTypeOf(); + expectTypeOf(subBusinessDays).returns.toEqualTypeOf(); + }); +}); diff --git a/src/sub-business-days/sub-business-days.ts b/src/sub-business-days/sub-business-days.ts new file mode 100644 index 00000000..8e692058 --- /dev/null +++ b/src/sub-business-days/sub-business-days.ts @@ -0,0 +1,62 @@ +import { addBusinessDays } from "../add-business-days/add-business-days"; +import { type BusinessDayOptions } from "../is-business-day/is-business-day"; + +/** + * Subtracts a number of Brazilian business days (dias úteis) from a date. + * + * The mirror image of `addBusinessDays`, which it delegates to: `subBusinessDays(date, amount)` + * is `addBusinessDays(date, -amount)`, down to the last detail. A business day is a day for + * which `isBusinessDay` returns `true` (not a Saturday, a Sunday, or a Brazilian holiday), + * evaluated with the same `options`, and the walk goes one calendar day at a time, counting only + * business days. + * + * `amount: 0` returns a **new `Date` equal to `date`, unchanged**, even when `date` itself falls + * on a weekend or holiday, and a negative `amount` walks *forwards*, exactly like date-fns' + * `subBusinessDays`. + * + * The time-of-day (hours, minutes, seconds, milliseconds) of `date` is preserved in the result, + * and `date` itself is never mutated. + * + * If `options.stateCode` is provided but is not a valid/known state code, it is ignored and only + * national holidays are considered (same behavior as `getHolidays`/`isBusinessDay`), so a + * prototype-chain key such as `"__proto__"` is an unknown state code like any other. An `options` + * that is not an object at all is ignored, exactly as `isBusinessDay` ignores it. + * + * Only years from 1900 through 2099 are supported, the range `getHolidays` computes. A `date` + * outside it, or a walk that leaves it, returns `null`. + * + * @param {Date} date - The date to count from. Never mutated: a new `Date` is returned. + * @param {number} amount - The number of business days to subtract; a negative value walks forwards. + * @param {BusinessDayOptions} [options] - Which holidays count as non-business days. + * @param {StateCode} [options.stateCode] - Brazilian state code whose state holidays are also considered. + * @param {boolean} [options.includeOptional] - Whether optional holidays count as non-business days (default: `true`). + * @returns {Date | null} A new `Date`, `amount` business days before `date`. `null` on bad input: + * a `date` that is not a valid `Date` or is outside 1900-2099, an `amount` that is not a finite + * integer, a `stateCode` that is not a string, or a walk that leaves the supported years. + * + * @example + * ```typescript + * subBusinessDays(new Date(2024, 0, 5, 12), 1); // Thu 2024-01-04, 12:00 (the previous day is already a business day) + * subBusinessDays(new Date(2024, 0, 8, 12), 1); // Fri 2024-01-05, 12:00 (walks back over the weekend) + * subBusinessDays(new Date(2025, 0, 2, 12), 1); // Tue 2024-12-31, 12:00 (Jan 1 is Ano novo, skipped) + * subBusinessDays(new Date(2024, 0, 5, 12), -1); // Mon 2024-01-08, 12:00 (walks forwards) + * subBusinessDays(new Date(2024, 0, 6, 12), 0); // Sat 2024-01-06, 12:00 (unchanged, even though Saturday is not a business day) + * subBusinessDays(new Date(2024, 6, 10, 12), 1, { stateCode: "SP" }); // Mon 2024-07-08, 12:00 (Jul 9 is a state holiday in SP) + * subBusinessDays(new Date("not a date"), 1); // null + * subBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) + * subBusinessDays(new Date(1900, 0, 2), 1); // null (the walk leaves the supported years) + * ``` + * + * @see Based on: https://date-fns.org/docs/subBusinessDays Reference behavior and the positional + * `(date, amount)` argument order. The underlying holiday determination's official sources are + * cited in `isBusinessDay`/`getHolidays`. + */ +export const subBusinessDays = ( + date: Date, + amount: number, + options?: BusinessDayOptions, +): Date | null => { + if (!Number.isInteger(amount)) return null; + + return addBusinessDays(date, -amount, options); +}; From 898186f81f967ba3471d83cd03df0bc124803939 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:45 -0300 Subject: [PATCH 05/75] fix(registro-profissional): read T and S as transfer suffixes after the CRC check digit --- .../constants.ts | 7 +- .../is-valid-registro-profissional.test.ts | 63 ++++++++++++++++- .../is-valid-registro-profissional.ts | 68 +++++++++++++------ 3 files changed, 112 insertions(+), 26 deletions(-) diff --git a/src/is-valid-registro-profissional/constants.ts b/src/is-valid-registro-profissional/constants.ts index 95b759b0..7af5c522 100644 --- a/src/is-valid-registro-profissional/constants.ts +++ b/src/is-valid-registro-profissional/constants.ts @@ -17,7 +17,12 @@ export const CRO_REGEX = /^(?\d{3,6})(?[A-Z]{2})$/; export const CRP_REGEX = /^(?\d{2})(?\d{4,6})$/; -export const CRC_REGEX = /^(?[A-Z]{2})(?\d{6})(?[OPT])(?\d)$/; +/** + * UF, six digits, the tipo de registro (`O` Originário or `P` Provisório), the check digit and, + * for a Registro Transferido or Secundário, the `T`/`S` suffix plus the UF of the destination CRC. + */ +export const CRC_REGEX = + /^(?[A-Z]{2})(?\d{6})(?[OP])(?\d)(?:(?[TS])(?[A-Z]{2}))?$/; /** Lowest regional code of the CFP system, CRP-01. */ export const CRP_MIN_REGION = 1; diff --git a/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts b/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts index 42226716..43a821c7 100644 --- a/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts +++ b/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts @@ -69,6 +69,22 @@ describe("isValidRegistroProfissional", () => { expect(isValidRegistroProfissional("SP-123456/X-3", { council: "CRC" })).toBe(false); }); + test('when a CRC number puts "T" in the tipo de registro slot, which the Manual de Registro restricts to O and P', () => { + expect(isValidRegistroProfissional("SP-123456/T-3", { council: "CRC" })).toBe(false); + }); + + test('when a CRC number puts "S" in the tipo de registro slot', () => { + expect(isValidRegistroProfissional("SP-123456/S-3", { council: "CRC" })).toBe(false); + }); + + test("when the destination UF of a transferred CRC number is not a real Brazilian state code", () => { + expect(isValidRegistroProfissional("SP-123456/O-3 T-ZZ", { council: "CRC" })).toBe(false); + }); + + test("when a transferred CRC number carries no destination UF at all", () => { + expect(isValidRegistroProfissional("SP-123456/O-3 T", { council: "CRC" })).toBe(false); + }); + test("when a CRP regional code is 00, below the CRP-01 of the CFP system", () => { expect(isValidRegistroProfissional("00/12345", { council: "CRP" })).toBe(false); }); @@ -127,8 +143,25 @@ describe("isValidRegistroProfissional", () => { expect(isValidRegistroProfissional("DF-000002/O-5", { council: "CRC" })).toBe(true); }); - test("for a valid CRC number of a registro transferido", () => { - expect(isValidRegistroProfissional("RJ-654321/T-9", { council: "CRC" })).toBe(true); + test('for "SP-123456/O-3 T-MG", the Manual de Registro\'s own example of a registro definitivo transferido', () => { + expect(isValidRegistroProfissional("SP-123456/O-3 T-MG", { council: "CRC" })).toBe(true); + }); + + test('for "TO-654321/P-8 T-SC", the Manual de Registro\'s own example of a registro provisório transferido', () => { + expect(isValidRegistroProfissional("TO-654321/P-8 T-SC", { council: "CRC" })).toBe(true); + }); + + test('for "PI-111222/O-5 S-AC", the Manual de Registro\'s own example of a registro secundário', () => { + expect(isValidRegistroProfissional("PI-111222/O-5 S-AC", { council: "CRC" })).toBe(true); + }); + + test("for a transferred CRC number matching options.stateCode, which is the originating UF", () => { + expect( + isValidRegistroProfissional("SP-123456/O-3 T-MG", { council: "CRC", stateCode: "SP" }), + ).toBe(true); + expect( + isValidRegistroProfissional("SP-123456/O-3 T-MG", { council: "CRC", stateCode: "MG" }), + ).toBe(false); }); }); @@ -173,7 +206,7 @@ describe("isValidRegistroProfissional", () => { fc.property( states, fc.integer({ min: 1, max: 9_999_999 }), - fc.constantFrom("O", "P", "T"), + fc.constantFrom("O", "P"), (stateCode, number, category) => { const digits = String(number); const value = `${stateCode}-${digits}/${category}-3`; @@ -186,6 +219,30 @@ describe("isValidRegistroProfissional", () => { ); }); + test('should accept the "T" and "S" suffixes only after the check digit and only with a real destination UF', () => { + fc.assert( + fc.property( + states, + states, + fc.constantFrom("O", "P"), + (stateCode, destination, category) => { + const number = `${stateCode}-123456/${category}-3`; + + for (const suffix of ["T", "S"]) { + expect( + isValidRegistroProfissional(`${number} ${suffix}-${destination}`, { + council: "CRC", + }), + ).toBe(true); + expect( + isValidRegistroProfissional(`${stateCode}-123456/${suffix}-3`, { council: "CRC" }), + ).toBe(false); + } + }, + ), + ); + }); + test("should reject a registration whose UF is not the expected one", () => { fc.assert( fc.property(states, states, numbers, (stateCode, other, number) => { diff --git a/src/is-valid-registro-profissional/is-valid-registro-profissional.ts b/src/is-valid-registro-profissional/is-valid-registro-profissional.ts index d6fbd193..85593b93 100644 --- a/src/is-valid-registro-profissional/is-valid-registro-profissional.ts +++ b/src/is-valid-registro-profissional/is-valid-registro-profissional.ts @@ -52,20 +52,29 @@ const isKnownCrpRegion = (value: string): boolean => { * system, CRP-01 to CRP-24. It is not a literal UF (some regions cover more than one state), * so `options.stateCode` is ignored for this council. * - `"CRC"` (Conselho Regional de Contabilidade): UF + 6 digits + the tipo de registro (`"O"` - * Originário, `"P"` Provisório or `"T"` Transferido) + 1 check digit whose value is not - * verified, e.g. `"SP-123456/O-3"`. The letter says nothing about the professional category: - * the Manual de Registro states that the distinction between `"O"` and `"P"` applies - * "independentemente da categoria profissional do contabilista", and `"T"` comes from the - * Resolução CFC nº 1.707/2023, art. 5º, parágrafo único, which appends it to the número do - * Registro Originário when a registration is transferred to another CRC. + * Originário or `"P"` Provisório) + 1 check digit whose value is not verified, e.g. + * `"SP-123456/O-3"`. The letter says nothing about the professional category: the Manual de + * Registro states that the distinction between `"O"` and `"P"` applies "independentemente da + * categoria profissional do contabilista". A Registro Transferido or Secundário is written by + * appending `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, as the + * Resolução CFC nº 1.707/2023, art. 5º, parágrafo único, and the Manual's own examples + * (`"SP-123456/O-3 T-MG"`, `"TO-654321/P-8 T-SC"`, `"PI-111222/O-5 S-AC"`) put it. Both UFs + * have to be real state codes; `options.stateCode` is compared against the originating one, + * the UF the número do Registro Originário belongs to. * * CREA (Conselho Regional de Engenharia e Agronomia) is not supported: since the 2016 national * unification (RNP) its registration number format could not be confirmed from an official, * publicly documented source. * - * Only the CRC and the CRP shapes rest on a published source. The OAB, the CFM and the CFO do - * not publish the format of the numbers their seccionais and regionais issue, so the digit - * ranges accepted for `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative. + * Only the CRC shape and the CRP regional codes rest on a published source: the CFP page lists + * the 24 Conselhos Regionais and nothing else, so the 4 to 6 digit body of a CRP number is as + * unsourced as the OAB, CRM and CRO ranges. The OAB, the CFM and the CFO do not publish the + * format of the numbers their seccionais and regionais issue, so the digit ranges accepted for + * `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative, and two counterexamples + * are known: the OAB/SP public search field is `maxlength="7"` and rejects only inputs of two + * characters or fewer, and the CFM's Manual de Procedimentos Administrativos documents a `300` + * prefixed CRM for foreign-trained physicians and a trailing `P` for inscrição provisória, + * neither of which the accepted shape can express. * * @param {string} value - The registration number to be validated. * @param {IsValidRegistroProfissionalOptions} options - The validation options. @@ -81,21 +90,34 @@ const isKnownCrpRegion = (value: string): boolean => { * isValidRegistroProfissional("123456-RJ", { council: "OAB", stateCode: "SP" }); // false (UF mismatch) * isValidRegistroProfissional("06/12345", { council: "CRP" }); // true * isValidRegistroProfissional("SP-123456/O-3", { council: "CRC" }); // true + * isValidRegistroProfissional("SP-123456/O-3 T-MG", { council: "CRC" }); // true (transferido) + * isValidRegistroProfissional("SP-123456/T-3", { council: "CRC" }); // false ("T" is not a tipo) * isValidRegistroProfissional("123456", { council: "OAB" }); // false (no UF) * ``` * - * @see Official: https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf Manual de - * Registro do Sistema CFC/CRCs, item 1.1: the CRC registration is the sigla of the UF, six - * sequential digits, the letter of the tipo de registro and a check digit, with "UF-000001/P-7" - * and "UF-000002/O-5" as its own worked examples. - * @see Official: https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/ Conselho - * Federal de Psicologia: the 24 Conselhos Regionais of the system, numbered CRP-01 to CRP-24. - * @see Based on: https://www.oab.org.br/ Ordem dos Advogados do Brasil (OAB), which publishes - * no format for the número de inscrição and the seccional. - * @see Based on: https://portal.cfm.org.br/ Conselho Federal de Medicina (CRM), which publishes - * no format for the registration number and the UF. - * @see Based on: https://cfo.org.br/ Conselho Federal de Odontologia (CRO), which publishes no - * format for the registration number and the UF. + * @see Official: https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf + * Manual de Registro do Sistema CFC/CRCs, item 1.1: the CRC registration is the sigla of the UF, + * six sequential digits, the letter of the tipo de registro and a check digit, with + * "UF-000001/P-7" and "UF-000002/O-5" as its own worked examples; the same item adds the "T" of + * the Registro Transferido "ao número do Registro Definitivo Originário ou Registro Provisório … + * acompanhada de um hífen e da sigla designativa da jurisdição do CRC de destino". + * @see Official: https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf + * Resolução CFC nº 1.707/2023, art. 5º parágrafo único: "No caso de Registro Transferido, ao + * número do Registro Originário será acrescentada a letra 'T', acompanhada da sigla designativa da + * jurisdição do CRC de destino." + * @see Official: https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/ + * Conselho Federal de Psicologia: the 24 Conselhos Regionais of the system, numbered CRP-01 to + * CRP-24. The page establishes the regional codes only; it publishes no length for the inscription + * number itself. + * @see Based on: https://www.oab.org.br/ + * Ordem dos Advogados do Brasil (OAB), which publishes no format for the número de inscrição and + * the seccional. + * @see Based on: https://portal.cfm.org.br/ + * Conselho Federal de Medicina (CRM), which publishes no format for the registration number and + * the UF. + * @see Based on: https://cfo.org.br/ + * Conselho Federal de Odontologia (CRO), which publishes no format for the registration number and + * the UF. */ export const isValidRegistroProfissional = ( value: string, @@ -113,10 +135,12 @@ export const isValidRegistroProfissional = ( if (!match?.groups) return false; - const { region, uf } = match.groups; + const { region, uf, transferUf } = match.groups; if (region !== undefined && !isKnownCrpRegion(region)) return false; + if (transferUf !== undefined && !isKnownStateCode(transferUf)) return false; + if (uf === undefined) return true; if (!isKnownStateCode(uf)) return false; From 3710dfa6679f6bca3f12d38362a3a9bb289bca66 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:45 -0300 Subject: [PATCH 06/75] test(ie): pin the 38 SINTEGRA worked examples and the prototype-key state codes --- src/is-valid-ie/is-valid-ie.test.ts | 49 +++++++++++++++++++++++++++++ src/is-valid-ie/is-valid-ie.ts | 11 +++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/is-valid-ie/is-valid-ie.test.ts b/src/is-valid-ie/is-valid-ie.test.ts index f0598be5..1c15ab43 100644 --- a/src/is-valid-ie/is-valid-ie.test.ts +++ b/src/is-valid-ie/is-valid-ie.test.ts @@ -758,6 +758,55 @@ describe("isValidIe", () => { }); }); + describe("SINTEGRA worked examples", () => { + const publishedExamples: [StateCode, string][] = [ + ["AC", "01.004.823/001-12"], + ["AL", "240000048"], + ["AP", "030123459"], + ["AM", "99.999.999-0"], + ["BA", "123456-63"], + ["BA", "612345-57"], + ["BA", "1000003-06"], + ["CE", "06000001-5"], + ["ES", "999999990"], + ["GO", "10.987.654-7"], + ["MA", "120000385"], + ["MG", "062.307.904/0081"], + ["MS", "280000006"], + ["MT", "0013000001-9"], + ["PA", "15999999-5"], + ["PA", "75000002-3"], + ["PB", "06000001-5"], + ["PI", "012345679"], + ["PR", "123.45678-50"], + ["RN", "20.040.040-1"], + ["RN", "20.0.040.040-0"], + ["RO", "0000000062521-3"], + ["RR", "24006628-1"], + ["RR", "24001755-6"], + ["RR", "24003429-0"], + ["RR", "24001360-3"], + ["RR", "24008266-8"], + ["RR", "24006153-6"], + ["RR", "24007356-2"], + ["RR", "24005467-4"], + ["RR", "24004145-5"], + ["RR", "24001340-7"], + ["RS", "224/3658792"], + ["SC", "251.040.852"], + ["SE", "27123456-3"], + ["SP", "110.042.490.114"], + ["SP", "P-01100424.3/002"], + ["TO", "29010227836"], + ]; + + test("should accept every worked example the SINTEGRA pages print", () => { + for (const [stateCode, ie] of publishedExamples) { + expect(isValidIe(stateCode, ie)).toBe(true); + } + }); + }); + describe("state code lookup", () => { test("should not resolve properties from the prototype chain", () => { // @ts-expect-error: intentionally invalid input diff --git a/src/is-valid-ie/is-valid-ie.ts b/src/is-valid-ie/is-valid-ie.ts index 3d9f46fe..b49c961d 100644 --- a/src/is-valid-ie/is-valid-ie.ts +++ b/src/is-valid-ie/is-valid-ie.ts @@ -518,9 +518,13 @@ const IE_VALIDATORS: Record = { * - AL: the tipo de empresa digit (third position) is not restricted to 0, 3, 5, 7 and 8. * - PE: only the current 9 digit eFisco format is accepted; the old 14 digit CACEPE format * documented on the same page is not. + * - TO: the SINTEGRA page documents only the 11 digit form, the one carrying the tipo digits in + * positions 3 and 4. The 9 digit form is also accepted, applying the same modulus 11 rule with + * weights 9 down to 2 to the first eight digits; it is 2.3.0 behavior kept for compatibility + * and no published SEFAZ-TO roteiro covers it. * - An all zero registration is accepted for every state whose published formula yields a - * check digit of 0 for it (AM, BA with 9 digits, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, SC, - * SE, SP and TO with 9 digits), unlike isValidCpf and isValidCnpj, which reject repeated + * check digit of 0 for it (AM, BA with 8 or 9 digits, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, + * SC, SE, SP and TO with 9 digits), unlike isValidCpf and isValidCnpj, which reject repeated * digits. * * @param {StateCode} stateCode - The state abbreviation (e.g., 'SP', 'RJ', 'MG') @@ -569,6 +573,9 @@ const IE_VALIDATORS: Record = { * @see Official: http://www.sintegra.gov.br/Cad_Estados/cad_SE.html * @see Official: http://www.sintegra.gov.br/Cad_Estados/cad_SP.html * @see Official: http://www.sintegra.gov.br/Cad_Estados/cad_TO.html + * Documents only the 11 digit form, with the tipo digits 01, 02, 03 and 99 in positions 3 and 4; + * the 9 digit form the validator also accepts is not covered by this page or by any other + * published SEFAZ-TO roteiro. * @see Official: https://goias.gov.br/economia/roteiro-de-critica-da-inscricao-estadual-de-goias/ * SEFAZ-GO's roteiro de crítica, the source of the Goiás prefixes and special ranges. */ From e9e8eba3d0a976fb86baa7833e2e328034edc968 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:45 -0300 Subject: [PATCH 07/75] refactor(municipality): keep collapsing whitespace runs in name lookups as 2.3.0 does --- src/get-municipality/get-municipality.test.ts | 26 +++++++++++++++++++ src/get-municipality/get-municipality.ts | 13 +++++----- .../get-state-code-by-name.test.ts | 16 ++++++++++++ .../get-state-code-by-name.ts | 17 +++++++++--- 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/get-municipality/get-municipality.test.ts b/src/get-municipality/get-municipality.test.ts index 6f969b19..67477bdb 100644 --- a/src/get-municipality/get-municipality.test.ts +++ b/src/get-municipality/get-municipality.test.ts @@ -39,6 +39,32 @@ describe("getMunicipality", () => { ); }); + it("should collapse every run of internal whitespace in the municipality name before matching", async () => { + await expect(getMunicipality({ municipalityName: "sao paulo", uf: "sp" })).resolves.toBe( + "3550308", + ); + await expect(getMunicipality({ municipalityName: "sao\tpaulo", uf: "sp" })).resolves.toBe( + "3550308", + ); + await expect(getMunicipality({ municipalityName: "sao\npaulo", uf: "sp" })).resolves.toBe( + "3550308", + ); + await expect( + getMunicipality({ municipalityName: " Angra \t dos \n Reis ", uf: "RJ" }), + ).resolves.toBe("3300100"); + }); + + it("should not match a municipality name written without the space the dataset carries", async () => { + await expect(getMunicipality({ municipalityName: "saopaulo", uf: "SP" })).resolves.toBeNull(); + }); + + it("should fold the casing to upper case, the direction that expands ß to SS", async () => { + await expect(getMunicipality({ municipalityName: "Passos", uf: "MG" })).resolves.toBe( + "3147907", + ); + await expect(getMunicipality({ municipalityName: "Paßos", uf: "MG" })).resolves.toBe("3147907"); + }); + it("should trim and uppercase a uf with surrounding whitespace and lowercase letters", async () => { await expect(getMunicipality({ municipalityName: "São Paulo", uf: " sp " })).resolves.toBe( "3550308", diff --git a/src/get-municipality/get-municipality.ts b/src/get-municipality/get-municipality.ts index ff56f74c..e052104a 100644 --- a/src/get-municipality/get-municipality.ts +++ b/src/get-municipality/get-municipality.ts @@ -23,11 +23,8 @@ export type GetMunicipalityOptions = GetMunicipalityByCodeOptions | GetMunicipal let codeIndex: Map | undefined; -// Stryker disable next-line MethodExpression: normalizeName is only ever used to compare two -// values against each other (never returned or displayed), and every name in the dataset is -// plain ASCII Latin letters once accents are stripped, so folding to upper or lower case is -// symmetric and cannot change which names are considered equal. -const normalizeName = (value: string): string => removeAccents(value).trim().toUpperCase(); +const normalizeName = (value: string): string => + removeAccents(value).replaceAll(/\s+/g, " ").trim().toUpperCase(); const getMunicipalityByCode = (code: string | number): [string, string] | null => { if (!isLookupCode(code)) return null; @@ -75,7 +72,11 @@ const getMunicipalityCodeByName = ({ * Looks a Brazilian municipality up in the offline IBGE "localidades" dataset. * * Given a `code` it resolves the municipality name and its UF; given a `municipalityName` - * and a `uf` it resolves the IBGE code. The name lookup ignores accents and casing. + * and a `uf` it resolves the IBGE code. The name lookup ignores accents and casing, and every + * run of whitespace collapses into a single space, so `"sao paulo"` matches `"São Paulo"`; a + * name written without the space does not, since only the runs that are there collapse. The + * casing is folded to upper case, the direction Unicode expands `"ß"` to `"SS"` in, so + * `"Paßos"` matches `"Passos"`. * Validation failures and unknown municipalities are reported as `null`. A `code` given as a * number must be a non-negative integer: a sign and a decimal point are not digits, so * `-3550308` and `355030.8` are rejected instead of being read as `3550308`. diff --git a/src/get-state-code-by-name/get-state-code-by-name.test.ts b/src/get-state-code-by-name/get-state-code-by-name.test.ts index 01c34c1d..f84096a7 100644 --- a/src/get-state-code-by-name/get-state-code-by-name.test.ts +++ b/src/get-state-code-by-name/get-state-code-by-name.test.ts @@ -23,6 +23,12 @@ describe("getStateCodeByName", () => { expect(getStateCodeByName(" São Paulo ")).toBe("SP"); }); + it("should collapse every run of internal whitespace", () => { + expect(getStateCodeByName("Rio de Janeiro")).toBe("RJ"); + expect(getStateCodeByName("Rio\tde\nJaneiro")).toBe("RJ"); + expect(getStateCodeByName(" sao paulo ")).toBe("SP"); + }); + it("should combine accent removal, casing and trimming together", () => { expect(getStateCodeByName(" sao PAULO ")).toBe("SP"); }); @@ -40,6 +46,16 @@ describe("getStateCodeByName", () => { expect(getStateCodeByName("Rio Grande do Sul")).toBe("RS"); }); + it("should not match a name written without the space the published one carries", () => { + expect(getStateCodeByName("sao paulo")).toBe("SP"); + expect(getStateCodeByName("saopaulo")).toBeNull(); + }); + + it("should fold the casing to lower case, which leaves ß as it is instead of expanding it to ss", () => { + expect(getStateCodeByName("Mato Grosso")).toBe("MT"); + expect(getStateCodeByName("Mato Großo")).toBeNull(); + }); + it("should return null for a name that matches no state", () => { expect(getStateCodeByName("Neverland")).toBeNull(); }); diff --git a/src/get-state-code-by-name/get-state-code-by-name.ts b/src/get-state-code-by-name/get-state-code-by-name.ts index 9c746f92..e2d72980 100644 --- a/src/get-state-code-by-name/get-state-code-by-name.ts +++ b/src/get-state-code-by-name/get-state-code-by-name.ts @@ -1,11 +1,21 @@ import { DATA, type StateCode } from "../_internals/constants/states"; import { removeAccents } from "../remove-accents/remove-accents"; +export type { StateCode } from "../_internals/constants/states"; + +const normalizeName = (value: string): string => + removeAccents(value).replaceAll(/\s+/g, " ").trim().toLowerCase(); + /** * Retrieves the two-letter code (sigla) of a Brazilian state given its full name. * * The match is accent-insensitive, case-insensitive and ignores leading/trailing whitespace, - * so `" são paulo "`, `"Sao Paulo"` and `"SÃO PAULO"` all resolve to `"SP"`. + * so `" são paulo "`, `"Sao Paulo"` and `"SÃO PAULO"` all resolve to `"SP"`. Every run of + * internal whitespace collapses into a single space too, so `"Rio de Janeiro"` resolves to + * `"RJ"`, while a name written without the space matches nothing: only the runs that are there + * collapse, so `"saopaulo"` is not `"São Paulo"`. The casing is folded to lower case, the + * direction that leaves `"ß"` alone instead of expanding it into `"SS"`, so `"Mato Großo"` is + * not `"Mato Grosso"` either. * * @param {string} name - The full name of the state. * @returns {StateCode|null} The two-letter state code, or `null` when `name` does not match @@ -18,13 +28,14 @@ import { removeAccents } from "../remove-accents/remove-accents"; * getStateCodeByName("São Paulo"); // "SP" * getStateCodeByName("sao paulo"); // "SP" * getStateCodeByName(" Rio de Janeiro "); // "RJ" + * getStateCodeByName("Rio de Janeiro"); // "RJ" * getStateCodeByName("Neverland"); // null * ``` */ export const getStateCodeByName = (name: string): StateCode | null => { - const normalized = removeAccents(name).trim().toLowerCase(); + const normalized = normalizeName(name); - const state = DATA.find((entry) => removeAccents(entry.name).toLowerCase() === normalized); + const state = DATA.find((entry) => normalizeName(entry.name) === normalized); return state ? state.code : null; }; From b6cb522fcc25d70b3bed9b6bc0df774390d87f1b Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:45 -0300 Subject: [PATCH 08/75] feat(capitalize): keep company designations, roman numerals and state codes upper case by default --- src/capitalize/capitalize.test.ts | 97 +++++++++++++++++++++-- src/capitalize/capitalize.ts | 114 ++++++++++++++++++++------- src/capitalize/constants.ts | 123 ++++++++++++++++++++++++++++++ 3 files changed, 299 insertions(+), 35 deletions(-) diff --git a/src/capitalize/capitalize.test.ts b/src/capitalize/capitalize.test.ts index 8aa6fc9d..fd03895b 100644 --- a/src/capitalize/capitalize.test.ts +++ b/src/capitalize/capitalize.test.ts @@ -1,7 +1,10 @@ import * as fc from "fast-check"; +import { DATA } from "../_internals/constants/states"; +import { expectNeverThrowsWithOptions } from "../_internals/test/properties"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { capitalize, type CapitalizeOptions } from "./capitalize"; +import { STATE_CODES } from "./constants"; describe("capitalize", () => { describe("should capitalize", () => { @@ -43,11 +46,60 @@ describe("capitalize", () => { }); test("when upper case words are provided in any case", () => { - expect(capitalize("empresa ltda")).toBe("Empresa Ltda"); expect(capitalize("empresa ltda", { upperCaseWords: ["ltda"] })).toBe("Empresa LTDA"); expect(capitalize("meu cpf e rg", { upperCaseWords: ["CPF", "Rg"] })).toBe("Meu CPF e RG"); }); + test("when the value is a Brazilian personal name", () => { + expect(capitalize("jose da silva")).toBe("Jose da Silva"); + expect(capitalize("JOSÉ DA SILVA")).toBe("José da Silva"); + expect(capitalize("de")).toBe("De"); + }); + + test("when the value carries a company designation, upper cased by default", () => { + expect(capitalize("empresa ltda")).toBe("Empresa LTDA"); + expect(capitalize("banco do brasil s.a.")).toBe("Banco do Brasil S.A."); + expect(capitalize("casa de carnes s/a")).toBe("Casa de Carnes S/A"); + expect(capitalize("consultoria s/s")).toBe("Consultoria S/S"); + expect(capitalize("padaria e confeitaria me")).toBe("Padaria e Confeitaria ME"); + expect(capitalize("meu cpf e rg")).toBe("Meu CPF e RG"); + expect(capitalize("cep 01310-100")).toBe("CEP 01310-100"); + }); + + test("when a word looks like a designation but is not one, or is a designation left out of the default list", () => { + expect(capitalize("jose de sa")).toBe("Jose de Sa"); + expect(capitalize("eu vi maria")).toBe("Eu Vi Maria"); + expect(capitalize("diga-me")).toBe("Diga-ME"); + }); + + test("when the value is a Brazilian address", () => { + expect(capitalize("mogi-guaçu")).toBe("Mogi-Guaçu"); + expect(capitalize("santana/rs")).toBe("Santana/RS"); + expect(capitalize("porto alegre/rs")).toBe("Porto Alegre/RS"); + expect(capitalize("são paulo/sp")).toBe("São Paulo/SP"); + }); + + test("when a word after a slash is not a state code, and when a state code has no slash before it", () => { + expect(capitalize("santana/br")).toBe("Santana/Br"); + expect(capitalize("santana/xingu")).toBe("Santana/Xingu"); + expect(capitalize("santana rs")).toBe("Santana Rs"); + }); + + test("when the value carries a roman numeral", () => { + expect(capitalize("joão paulo ii")).toBe("João Paulo II"); + expect(capitalize("rua xv de novembro")).toBe("Rua XV de Novembro"); + expect(capitalize("avenida papa joão xxiii")).toBe("Avenida Papa João XXIII"); + }); + + test("when a word list given in the options replaces the default one", () => { + expect(capitalize("empresa ltda", { upperCaseWords: [] })).toBe("Empresa Ltda"); + expect(capitalize("jose da silva", { lowerCaseWords: [] })).toBe("Jose Da Silva"); + expect(capitalize("banco do brasil s.a.", { upperCaseWords: ["s.a."] })).toBe( + "Banco do Brasil S.A.", + ); + expect(capitalize("santana/rs", { upperCaseWords: [] })).toBe("Santana/RS"); + }); + test("when the value contains whitespace other than a space", () => { expect(capitalize("joao\tsilva")).toBe("Joao Silva"); expect(capitalize("joao\n\nsilva")).toBe("Joao Silva"); @@ -56,7 +108,7 @@ describe("capitalize", () => { test("when the value contains hyphens or slashes", () => { expect(capitalize("MOGI-GUAÇU")).toBe("Mogi-Guaçu"); - expect(capitalize("SANTANA/RS")).toBe("Santana/Rs"); + expect(capitalize("SANTANA/RS")).toBe("Santana/RS"); expect(capitalize("SANTANA/RS", { upperCaseWords: ["rs"] })).toBe("Santana/RS"); expect(capitalize("sÃo josÉ do rio-preto")).toBe("São José do Rio-Preto"); expect(capitalize("de-facto")).toBe("De-Facto"); @@ -83,13 +135,44 @@ describe("capitalize", () => { expect(capitalize(123)).toBe(""); }); + describe("should fall back to the defaults when a word list is malformed", () => { + test("when the word list is not an array", () => { + // @ts-expect-error: intentionally invalid input + expect(capitalize("jose da silva", { lowerCaseWords: null })).toBe("Jose da Silva"); + // @ts-expect-error: intentionally invalid input + expect(capitalize("jose da silva", { upperCaseWords: null })).toBe("Jose da Silva"); + // @ts-expect-error: intentionally invalid input + expect(capitalize("jose da silva", { lowerCaseWords: "ab" })).toBe("Jose da Silva"); + // @ts-expect-error: intentionally invalid input + expect(capitalize("jose da silva", { upperCaseWords: 1 })).toBe("Jose da Silva"); + }); + + test("when the word list holds a value that is not a string", () => { + // @ts-expect-error: intentionally invalid input + expect(capitalize("jose da silva", { lowerCaseWords: [null] })).toBe("Jose Da Silva"); + // @ts-expect-error: intentionally invalid input + expect(capitalize("jose da silva", { upperCaseWords: [1] })).toBe("Jose da Silva"); + // @ts-expect-error: intentionally invalid input + expect(capitalize("jose da silva", { lowerCaseWords: [1, "da"] })).toBe("Jose da Silva"); + }); + }); + + test("should keep its state code list in sync with the one published by the IBGE", () => { + expect(STATE_CODES).toStrictEqual(DATA.map((state) => state.code)); + }); + describe("properties", () => { + const nulls = fc.constant(null); + const wordListMembers = fc.oneof(fc.string(), fc.integer(), nulls); + const wordLists = fc.oneof(nulls, fc.string(), fc.integer(), fc.array(wordListMembers)); + const optionRecord = fc.record( + { lowerCaseWords: wordLists, upperCaseWords: wordLists }, + { requiredKeys: [] }, + ); + const hostileOptions = fc.oneof(fc.anything(), optionRecord); + test("should never throw, regardless of the input", () => { - fc.assert( - fc.property(fc.anything(), (value) => { - expect(() => capitalize(value as never)).not.toThrow(); - }), - ); + expectNeverThrowsWithOptions(capitalize, fc.anything(), hostileOptions); }); test("should be idempotent on its own output", () => { diff --git a/src/capitalize/capitalize.ts b/src/capitalize/capitalize.ts index 70d238fb..ec9ed319 100644 --- a/src/capitalize/capitalize.ts +++ b/src/capitalize/capitalize.ts @@ -1,84 +1,142 @@ -import { PREPOSITIONS, SEPARATOR_REGEX, WHITESPACE_REGEX } from "./constants"; +import { + PREPOSITIONS, + SEPARATOR_REGEX, + STATE_CODES, + UPPER_CASE_WORDS, + WHITESPACE_REGEX, +} from "./constants"; /** Options of `capitalize`. */ export type CapitalizeOptions = { /** Words to keep in lower case when they are not the first word (default: the Portuguese prepositions). */ lowerCaseWords?: string[]; - /** Words to keep in upper case wherever they appear (default: `[]`). */ + /** Words to keep in upper case wherever they appear (default: the Brazilian company designations, document abbreviations and roman numerals). */ upperCaseWords?: string[]; }; +const stateCodeSet: Set = new Set(STATE_CODES); + +const toWordSet = ( + words: unknown, + fallback: readonly string[], + fold: (word: string) => string, +): Set => { + const source: readonly unknown[] = Array.isArray(words) ? words : fallback; + + return new Set(source.filter((word) => typeof word === "string").map((word) => fold(word))); +}; + /** - * Capitalizes a given string according to specific rules for lower-case and upper-case words. + * Capitalizes a given string according to the way a Brazilian name, company name or address is + * written, with no configuration needed: `"jose da silva"` becomes `"Jose da Silva"`, + * `"empresa ltda"` becomes `"Empresa LTDA"` and `"santana/rs"` becomes `"Santana/RS"`. * * Words are separated by whitespace, by `-` and by `/`, so `"MOGI-GUAÇU"` becomes - * `"Mogi-Guaçu"` and `"SANTANA/RS"` becomes `"Santana/Rs"`. Hyphens and slashes are kept - * where they are, while every run of whitespace (spaces, tabs, newlines) collapses into a - * single space and the leading and trailing whitespace is dropped. + * `"Mogi-Guaçu"`. Hyphens and slashes are kept where they are, while every run of whitespace + * (spaces, tabs, newlines) collapses into a single space and the leading and trailing whitespace + * is dropped. * - * - Words listed in `lowerCaseWords` (default: `PREPOSITIONS`) will be converted to lower case, except for the first word. - * - Words listed in `upperCaseWords` will be converted to upper case (none by default). The - * comparison ignores the case of the words given in both lists. - * - All other words will be capitalized (first letter upper case, rest lower case). + * - Words listed in `lowerCaseWords` are converted to lower case, except for the first word. The + * default list is the Portuguese prepositions, articles and conjunctions that stay in lower + * case inside a proper name ("de", "da", "do", "e", ...), so `"JOSÉ DA SILVA"` becomes + * `"José da Silva"`. + * - Words listed in `upperCaseWords` are converted to upper case wherever they appear. The + * default list is the company designations and document abbreviations that are written in upper + * case in Brazilian usage (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, + * `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) plus the roman numerals that appear in names + * and addresses (`II` through `XXIII`, except `VI`, so `"joão paulo ii"` becomes + * `"João Paulo II"` and `"rua xv de novembro"` becomes `"Rua XV de Novembro"`). `ME` matches + * the pronoun "me" too, so free text such as `"diga-me"` becomes `"Diga-ME"`: pass an + * `upperCaseWords` of your own when the input is not a name. A designation + * written around a slash, `S/A` and `S/S`, is matched across that slash even though a slash + * separates words, so `"casa de carnes s/a"` becomes `"Casa de Carnes S/A"`. + * - A two letter word that follows a `/` is converted to upper case when it is the code of a + * Brazilian state, the way a municipality and its Federative Unit are written together, so + * `"porto alegre/rs"` becomes `"Porto Alegre/RS"` while `"santana/br"` becomes `"Santana/Br"`. + * A state code that does not follow a `/` is left alone (`"santana rs"` becomes + * `"Santana Rs"`), and so is any other two letter word. + * - All other words are capitalized (first letter upper case, rest lower case). + * + * Both lists are compared ignoring the case of the words, and either one given in `options` + * replaces its default list entirely, so `capitalize("empresa ltda", { upperCaseWords: [] })` + * gives `"Empresa Ltda"`. A `lowerCaseWords`/`upperCaseWords` that is not an array falls back to + * its default, and a member of either list that is not a string is ignored, so a malformed + * option never throws. * * @param {string} value - The input string to be capitalized. * @param {CapitalizeOptions} [options] - Optional configuration for capitalization. - * @param {string[]} [options.lowerCaseWords] - Array of words to keep in lower case (default: `PREPOSITIONS`). - * @param {string[]} [options.upperCaseWords] - Array of words to keep in upper case (default: `[]`). + * @param {string[]} [options.lowerCaseWords] - Array of words to keep in lower case (default: the Portuguese prepositions). + * @param {string[]} [options.upperCaseWords] - Array of words to keep in upper case (default: the Brazilian company designations, document abbreviations and roman numerals). * @returns {string} The capitalized string according to the specified rules. * + * The default `lowerCaseWords` list is the set of prepositions and conjunctions the Manual de + * Redação da Presidência da República keeps in lower case inside a proper name, and the default + * `upperCaseWords` list is sourced in `constants.ts` from the laws that create each designation. + * + * @see Based on: https://www4.planalto.gov.br/centrodeestudos/assuntos/manual-de-redacao-da-presidencia-da-republica + * * @example * ```typescript * capitalize("JOSÉ DA SILVA"); // "José da Silva" - * capitalize("empresa ltda"); // "Empresa Ltda" - * capitalize("empresa ltda", { upperCaseWords: ["ltda"] }); // "Empresa LTDA" + * capitalize("empresa ltda"); // "Empresa LTDA" + * capitalize("banco do brasil s.a."); // "Banco do Brasil S.A." + * capitalize("casa de carnes s/a"); // "Casa de Carnes S/A" * capitalize("MOGI-GUAÇU"); // "Mogi-Guaçu" - * capitalize("SANTANA/RS"); // "Santana/Rs" - * capitalize("SANTANA/RS", { upperCaseWords: ["rs"] }); // "Santana/RS" + * capitalize("santana/rs"); // "Santana/RS" + * capitalize("rua xv de novembro"); // "Rua XV de Novembro" + * capitalize("empresa ltda", { upperCaseWords: [] }); // "Empresa Ltda" * capitalize("joao\tsilva"); // "Joao Silva" * ``` */ export const capitalize = (value: string, options?: CapitalizeOptions): string => { if (typeof value !== "string") return ""; - // Stryker disable next-line ArrayDeclaration: the default is never compared against multi-word placeholder content, so any non-empty placeholder array stays unmatched and behaviorally identical - const { lowerCaseWords = PREPOSITIONS, upperCaseWords = [] } = options ?? {}; + const { lowerCaseWords, upperCaseWords } = options ?? {}; - const lowerCaseSet = new Set(lowerCaseWords.map((word) => word.toLocaleLowerCase("pt-BR"))); + const lowerCaseSet = toWordSet(lowerCaseWords, PREPOSITIONS, (word) => + word.toLocaleLowerCase("pt-BR"), + ); - const upperCaseSet = new Set(upperCaseWords.map((word) => word.toLocaleUpperCase("pt-BR"))); + const upperCaseSet = toWordSet(upperCaseWords, UPPER_CASE_WORDS, (word) => + word.toLocaleUpperCase("pt-BR"), + ); const tokens = value.trim().split(SEPARATOR_REGEX); - let result = ""; + const output: string[] = []; let wordIndex = 0; for (const token of tokens) { if (!token) continue; if (WHITESPACE_REGEX.test(token)) { - result += " "; + output.push(" "); continue; } if (token === "-" || token === "/") { - result += token; + output.push(token); continue; } const lowerCaseWord = token.toLocaleLowerCase("pt-BR"); const upperCaseWord = token.toLocaleUpperCase("pt-BR"); + const designation = (output.slice(-2).join("") + upperCaseWord).toLocaleUpperCase("pt-BR"); - if (wordIndex > 0 && lowerCaseSet.has(lowerCaseWord)) { - result += lowerCaseWord; + if (upperCaseSet.has(designation)) { + output.splice(-2, 2, designation); + } else if (wordIndex > 0 && lowerCaseSet.has(lowerCaseWord)) { + output.push(lowerCaseWord); } else if (upperCaseSet.has(upperCaseWord)) { - result += upperCaseWord; + output.push(upperCaseWord); + } else if (output.at(-1) === "/" && stateCodeSet.has(upperCaseWord)) { + output.push(upperCaseWord); } else { - result += upperCaseWord.charAt(0) + lowerCaseWord.slice(1); + output.push(upperCaseWord.charAt(0) + lowerCaseWord.slice(1)); } wordIndex++; } - return result; + return output.join(""); }; diff --git a/src/capitalize/constants.ts b/src/capitalize/constants.ts index 081cd985..d1d9fff4 100644 --- a/src/capitalize/constants.ts +++ b/src/capitalize/constants.ts @@ -1,3 +1,15 @@ +import { type StateCode } from "../_internals/constants/states"; + +/** + * Prepositions, articles and conjunctions that stay in lower case inside a proper name, the + * default `lowerCaseWords` of `capitalize`. The Manual de Redação da Presidência da República + * writes personal and institutional names with every word capitalized except the connective + * words ("Ministério da Justiça", "José da Silva"), and the same convention is used by the IBGE + * for the names of municipalities ("Mogi das Cruzes", "Santa Bárbara d'Oeste"). + * + * @see Official: https://www4.planalto.gov.br/centrodeestudos/assuntos/manual-de-redacao-da-presidencia-da-republica + * @see Official: https://servicodados.ibge.gov.br/api/docs/localidades + */ export const PREPOSITIONS = [ "a", "com", @@ -17,6 +29,117 @@ export const PREPOSITIONS = [ "sem", ]; +/** + * Company designations and document abbreviations that are written in upper case in Brazilian + * names. "SA" without punctuation is deliberately absent: it is indistinguishable from the + * surname "Sá" typed without its accent, which would turn "Jose de Sa" into "Jose de SA". + * + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l6404consol.htm + * Lei nº 6.404/1976, art. 3º: the sociedade anônima is designated by "companhia" or "sociedade + * anônima", "expressas por extenso ou abreviadamente", the abbreviations being CIA, S.A. and S/A. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/2002/l10406compilada.htm + * Código Civil, art. 1.158: the sociedade limitada carries the final word "limitada" "ou a sua + * abreviatura" (LTDA); art. 991 defines the sociedade em conta de participação (SCP), enrolled in + * the CNPJ under that abbreviation; art. 980-A, which created the EIRELI, was revoked by the Lei + * nº 14.382/2022, so the abbreviation is kept only because registered names still carry it. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp123.htm + * Lei Complementar nº 123/2006, art. 72, revoked by the Lei Complementar nº 155/2016, added + * "Microempresa ou Empresa de Pequeno Porte, ou suas respectivas abreviações, ME ou EPP" to the + * name; art. 18-A defines the Microempreendedor Individual (MEI). + * @see Based on: https://www.gov.br/empresas-e-negocios/pt-br/drei/legislacao/instrucoes-normativas + * Instruções normativas of the DREI, which the Juntas Comerciais follow to register a nome + * empresarial and the source of the S/S spelling of the sociedade simples. + */ +const BUSINESS_ABBREVIATIONS = [ + "CEP", + "CIA", + "CNPJ", + "CPF", + "EIRELI", + "EPP", + "LTDA", + "ME", + "MEI", + "RG", + "S.A.", + "S.S.", + "S/A", + "S/S", + "SCP", + "UF", +]; + +/** + * Roman numerals that appear inside Brazilian names and addresses ("João Paulo II", "Rua XV de + * Novembro", "Avenida Papa João XXIII"). The single letter numerals (V, X, L, C, D, M) are left + * out because a single letter is already written in upper case by the default rule, and VI is + * left out because it collides with the pt-BR verb form "vi". + */ +const ROMAN_NUMERALS = [ + "II", + "III", + "IV", + "VII", + "VIII", + "IX", + "XI", + "XII", + "XIII", + "XIV", + "XV", + "XVI", + "XVII", + "XVIII", + "XIX", + "XX", + "XXI", + "XXII", + "XXIII", +]; + +/** Words that are written in upper case wherever they appear, the default `upperCaseWords`. */ +export const UPPER_CASE_WORDS = [...BUSINESS_ABBREVIATIONS, ...ROMAN_NUMERALS]; + +/** + * The two letter code of each Brazilian state, written in upper case when it follows a `/` + * ("Santana/RS"), the way a municipality and its Federative Unit are written together. The codes + * are a literal copy of the `code` of every state published by the IBGE (see + * `_internals/constants/states`, whose table is not imported here so that `capitalize` does not + * carry the whole state dataset into a consumer's bundle); `capitalize.test.ts` asserts that this + * list is exactly that one. + * + * @see Official: https://servicodados.ibge.gov.br/api/docs/localidades + */ +export const STATE_CODES: StateCode[] = [ + "AC", + "AL", + "AP", + "AM", + "BA", + "CE", + "DF", + "ES", + "GO", + "MA", + "MT", + "MS", + "MG", + "PA", + "PB", + "PR", + "PE", + "PI", + "RJ", + "RN", + "RS", + "RO", + "RR", + "SC", + "SP", + "SE", + "TO", +]; + export const SEPARATOR_REGEX = /(\s+|[-/])/; export const WHITESPACE_REGEX = /^\s+$/; From c9f5a5ec70402cb71c8f01f33fdec80eaba5bf9f Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:46 -0300 Subject: [PATCH 09/75] fix(currency): coerce a non-string value without throwing and read the cents from the decimals --- .../convert-currency-to-words.test.ts | 111 ++++++++++++------ .../convert-currency-to-words.ts | 52 ++++---- src/format-currency/format-currency.test.ts | 51 +++++++- src/format-currency/format-currency.ts | 39 ++++-- src/parse-currency/parse-currency.test.ts | 24 +++- src/parse-currency/parse-currency.ts | 9 ++ 6 files changed, 202 insertions(+), 84 deletions(-) diff --git a/src/convert-currency-to-words/convert-currency-to-words.test.ts b/src/convert-currency-to-words/convert-currency-to-words.test.ts index cbd4ec87..b12b5e79 100644 --- a/src/convert-currency-to-words/convert-currency-to-words.test.ts +++ b/src/convert-currency-to-words/convert-currency-to-words.test.ts @@ -1,14 +1,8 @@ import * as fc from "fast-check"; -import { - NUMBER_TO_WORDS_MAX_VALUE, - type WordsCase, -} from "../_internals/number-to-words/number-to-words"; +import { NUMBER_TO_WORDS_MAX_VALUE } from "../_internals/number-to-words/number-to-words"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; -import { - convertCurrencyToWords, - type ConvertCurrencyToWordsOptions, -} from "./convert-currency-to-words"; +import { convertCurrencyToWords } from "./convert-currency-to-words"; function expectAmounts(cases: readonly (readonly [number, string])[]): void { const failures = cases @@ -118,8 +112,10 @@ describe("convertCurrencyToWords", () => { expect(convertCurrencyToWords(9_007_199_254_740.99)).toContain("noventa e nove centavos"); }); - test("should still report cents exactly at the Number.MAX_SAFE_INTEGER cents boundary", () => { - expect(convertCurrencyToWords(90_071_992_547_409.9)).toContain("noventa e um centavos"); + test("should still report cents exactly at the Number.MAX_SAFE_INTEGER cents boundary, reading the 90 cents the double holds (90071992547409.9 is exactly 90071992547409.90625, the 91st cent only shows up when 9007199254740990.625 is scaled and rounded to Number.MAX_SAFE_INTEGER)", () => { + expect(convertCurrencyToWords(90_071_992_547_409.9)).toBe( + "noventa trilhões, setenta e um bilhões, novecentos e noventa e dois milhões, quinhentos e quarenta e sete mil, quatrocentos e nove reais e noventa centavos", + ); }); }); @@ -138,35 +134,78 @@ describe("convertCurrencyToWords", () => { test("should absorb the noise of an amount that is itself the sum of two floats", () => { expect(convertCurrencyToWords(0.1 + 0.2)).toBe("trinta centavos"); }); - }); - describe("case option", () => { - test("should keep the result lowercase by default", () => { - expect(convertCurrencyToWords(1000)).toBe("mil reais"); + test("should not invent a cent for a large amount whose sub cent part scales to a hair below the next integer (1000000000000.0099 * 100 is 100000000000000.98)", () => { + expect(convertCurrencyToWords(1_000_000_000_000.0099)).toBe("um trilhão de reais"); }); - test("should keep the result lowercase for 'lower'", () => { - expect(convertCurrencyToWords(1000, { case: "lower" })).toBe("mil reais"); + test("should truncate, not round, the sub cent part of a large amount", () => { + const cases: [number, string][] = [ + [1_000_000_000_000.0199, "um trilhão de reais e um centavo"], + [ + 123_456_789_012.345, + "cento e vinte e três bilhões, quatrocentos e cinquenta e seis milhões, setecentos e oitenta e nove mil e doze reais e trinta e quatro centavos", + ], + [ + 87_654_321_098.7654, + "oitenta e sete bilhões, seiscentos e cinquenta e quatro milhões, trezentos e vinte e um mil e noventa e oito reais e setenta e seis centavos", + ], + [ + 999_999_999_999.999, + "novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais e noventa e nove centavos", + ], + [ + 9_007_199_254_740.99, + "nove trilhões, sete bilhões, cento e noventa e nove milhões, duzentos e cinquenta e quatro mil, setecentos e quarenta reais e noventa e nove centavos", + ], + ]; + + expectAmounts(cases); }); - test("should capitalize only the first letter for 'sentence'", () => { - expect(convertCurrencyToWords(1000, { case: "sentence" })).toBe("Mil reais"); - expect(convertCurrencyToWords(0, { case: "sentence" })).toBe("Zero reais"); + test("should return 'zero reais' for an amount so small that it is written in exponent notation", () => { + expect(convertCurrencyToWords(1.5e-7)).toBe("zero reais"); + expect(convertCurrencyToWords(-1.5e-7)).toBe("zero reais"); + expect(convertCurrencyToWords(1e-7)).toBe("zero reais"); + expect(convertCurrencyToWords(Number.MIN_VALUE)).toBe("zero reais"); }); - test("should uppercase everything for 'upper', keeping accents", () => { - expect(convertCurrencyToWords(1000, { case: "upper" })).toBe("MIL REAIS"); - expect(convertCurrencyToWords(1523.45, { case: "upper" })).toBe( - "MIL, QUINHENTOS E VINTE E TRÊS REAIS E QUARENTA E CINCO CENTAVOS", + test("should read back the exact cents of every amount from R$ 0.00 to R$ 1 000.00, cent by cent, and of every 997th cent up to R$ 20 000.00, against the same amount built from whole reais and whole cents", () => { + const reaisWords = Array.from({ length: 20_001 }, (_, reais) => + convertCurrencyToWords(reais), ); - expect(convertCurrencyToWords(-5.5, { case: "upper" })).toBe( - "MENOS CINCO REAIS E CINQUENTA CENTAVOS", + const centavosWords = Array.from({ length: 100 }, (_, centavos) => + convertCurrencyToWords(centavos / 100), ); + const expectedWords = (reais: number, centavos: number): string => { + if (reais === 0) return centavosWords[centavos]; + if (centavos === 0) return reaisWords[reais]; + + return `${reaisWords[reais]} e ${centavosWords[centavos]}`; + }; + const failures: number[] = []; + + const check = (cents: number): void => { + const expected = expectedWords(Math.floor(cents / 100), cents % 100); + + if (convertCurrencyToWords(cents / 100) !== expected) failures.push(cents); + }; + + for (let cents = 0; cents <= 100_000; cents++) check(cents); + for (let cents = 100_997; cents <= 2_000_000; cents += 997) check(cents); + + expect(failures).toEqual([]); }); + }); - test("should ignore an invalid case value and fall back to 'lower'", () => { - // @ts-expect-error: intentionally invalid input - expect(convertCurrencyToWords(1000, { case: "invalid" })).toBe("mil reais"); + describe("letter case", () => { + test("should always keep the result lowercase", () => { + expect(convertCurrencyToWords(1000)).toBe("mil reais"); + expect(convertCurrencyToWords(0)).toBe("zero reais"); + expect(convertCurrencyToWords(-5.5)).toBe("menos cinco reais e cinquenta centavos"); + expect(convertCurrencyToWords(1523.45)).toBe( + "mil, quinhentos e vinte e três reais e quarenta e cinco centavos", + ); }); }); @@ -431,15 +470,12 @@ describe("convertCurrencyToWords", () => { ); }); - test("should uppercase the result the same way as the lower case result, for the 'upper' case option", () => { + test("should never return a character in upper case", () => { fc.assert( fc.property(safeCentsArbitrary, (cents) => { - const value = cents / 100; - const lower = convertCurrencyToWords(value); + const words = convertCurrencyToWords(cents / 100); - expect(convertCurrencyToWords(value, { case: "upper" })).toBe( - lower.toLocaleUpperCase("pt-BR"), - ); + expect(words).toBe(words.toLocaleLowerCase("pt-BR")); }), ); }); @@ -447,12 +483,9 @@ describe("convertCurrencyToWords", () => { }); describe("convertCurrencyToWords types", () => { - test("should take a number, options, and return a string", () => { + test("should take a single number and return a string", () => { expectTypeOf(convertCurrencyToWords).parameter(0).toEqualTypeOf(); - expectTypeOf(convertCurrencyToWords) - .parameter(1) - .toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); + expectTypeOf(convertCurrencyToWords).parameters.toEqualTypeOf<[value: number]>(); expectTypeOf(convertCurrencyToWords).returns.toEqualTypeOf(); }); }); diff --git a/src/convert-currency-to-words/convert-currency-to-words.ts b/src/convert-currency-to-words/convert-currency-to-words.ts index c1d0a35e..c9aeadb1 100644 --- a/src/convert-currency-to-words/convert-currency-to-words.ts +++ b/src/convert-currency-to-words/convert-currency-to-words.ts @@ -1,35 +1,35 @@ -import { applyWordsCase } from "../_internals/apply-words-case/apply-words-case"; import { NUMBER_TO_WORDS_MAX_VALUE, numberToWords, - type WordsCase, } from "../_internals/number-to-words/number-to-words"; -/** Options of `convertCurrencyToWords`. */ -export type ConvertCurrencyToWordsOptions = { - /** Letter case applied to the result: `"lower"` (unchanged), `"sentence"` (capitalizes only the first letter) or `"upper"` (uppercases everything, keeping accents). Defaults to `"lower"`; an invalid value is ignored and `"lower"` is used instead. */ - case?: WordsCase; -}; - const MILLION_SCALE_SUFFIXES = ["lhão", "lhões"]; +const ONE_CENTAVO = 0.01; + /** * Scales an amount to whole cents, truncating it, without letting the floating point noise of - * the multiplication decide the result. `absolute * 100` lands a hair off the integer it - * should be (`1.15 * 100` is `114.99999999999999`, `0.57 * 100` is `56.99999999999999`), so a - * scaled value within one double rounding error of an integer is read as that integer. - * An amount that is genuinely below the next cent sits much further away than that - * (`1.999999999 * 100` is `199.9999999`) and is truncated, as it must be. + * the multiplication decide the result. `absolute * 100` lands a hair off the integer it should + * be (`1.15 * 100` is `114.99999999999999`, `0.57 * 100` is `56.99999999999999`) and that error + * grows with the amount, up to a whole cent for the trillions (`1000000000000.0099 * 100` is + * `100000000000000.98`, a hair below an integer while the amount holds no cents at all), so the + * cents are read off the decimal notation of the amount instead of off the product. + * `String(absolute)` is the shortest decimal that reads back as `absolute`, i.e. the amount as + * it was written, and its first two fractional digits are the cents; anything after them is + * truncated, as it must be (`1.999999999` is one real and 99 cents). + * An amount below one cent has no cents to read, which also keeps `String(absolute)` in plain + * decimal notation: the exponent form only shows up below `1e-6` and from `1e21` up, and an + * amount that large is out of range for the caller. * * @param {number} absolute - The absolute amount in reais. * @returns {number} The amount truncated to whole cents. */ const toCents = (absolute: number): number => { - const scaled = absolute * 100; - const rounded = Math.round(scaled); + if (absolute < ONE_CENTAVO) return 0; + + const [wholeReais, fraction = ""] = String(absolute).split("."); - // Stryker disable next-line EqualityOperator: `<` is equivalent, the two sides are never equal. Writing scaled as m * 2 ** (k - 52) with 2 ** k <= scaled < 2 ** (k + 1) and m its 53 bit significand, both scaled and rounded are multiples of the ulp 2 ** (k - 52), so the difference is j * 2 ** (k - 52) for an integer j, while Number.EPSILON * scaled is exactly m * 2 ** (k - 104): equality asks for m === j * 2 ** 52, and m < 2 ** 53 leaves only m === 2 ** 52, i.e. scaled a power of two. A power of two of at least 1 is an integer, whose difference is 0, and one below 1 rounds to 0 or to 1 at a distance of at least 0.25, never one ulp. The only case where both sides are 0 is scaled === 0, where rounded and Math.trunc(scaled) are both 0 anyway - return Math.abs(scaled - rounded) <= Number.EPSILON * scaled ? rounded : Math.trunc(scaled); + return Number(`${wholeReais}${fraction.slice(0, 2).padEnd(2, "0")}`); }; const endsInMillionScale = (words: string): boolean => @@ -51,9 +51,9 @@ const endsInMillionScale = (words: string): boolean => * double cannot carry cents at all, so the amount is read as a whole number of reais instead of * reporting cents that the input never held. * + * The result is always lowercase; apply any other casing to it yourself. + * * @param {number} value - The monetary amount to convert, in reais (e.g. `1523.45` for R$ 1.523,45). - * @param {ConvertCurrencyToWordsOptions} [options] - Optional formatting options. - * @param {WordsCase} [options.case] - Letter case applied to the result. Defaults to `"lower"`. * @returns {string} The amount written out in Portuguese, or `""` for invalid input. * * @example @@ -64,15 +64,12 @@ const endsInMillionScale = (words: string): boolean => * convertCurrencyToWords(1000000); // "um milhão de reais" * convertCurrencyToWords(0); // "zero reais" * convertCurrencyToWords(-5.5); // "menos cinco reais e cinquenta centavos" - * convertCurrencyToWords(1000, { case: "upper" }); // "MIL REAIS" + * convertCurrencyToWords(-0.001); // "zero reais" * ``` * * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/currency.py */ -export const convertCurrencyToWords = ( - value: number, - options?: ConvertCurrencyToWordsOptions, -): string => { +export const convertCurrencyToWords = (value: number): string => { if (!Number.isFinite(value)) return ""; const absolute = Math.abs(value); @@ -97,11 +94,10 @@ export const convertCurrencyToWords = ( parts.push(reais > 0 ? `e ${centavosText}` : centavosText); } - if (reais === 0 && centavos === 0) return applyWordsCase("zero reais", options?.case); + if (reais === 0 && centavos === 0) return "zero reais"; const joined = parts.join(" "); - // Stryker disable next-line EqualityOperator: equivalent, value is never exactly 0 here (reais === 0 && centavos === 0 already returned above) - const result = value < 0 ? `menos ${joined}` : joined; - return applyWordsCase(result, options?.case); + // Stryker disable next-line EqualityOperator: equivalent, value is never exactly 0 here (reais === 0 && centavos === 0 already returned above) + return value < 0 ? `menos ${joined}` : joined; }; diff --git a/src/format-currency/format-currency.test.ts b/src/format-currency/format-currency.test.ts index a96880fc..c6f7945e 100644 --- a/src/format-currency/format-currency.test.ts +++ b/src/format-currency/format-currency.test.ts @@ -119,6 +119,35 @@ describe("formatCurrency", () => { expect(formatCurrency()).toBe(""); }); + it("should return an empty string for a value with no numeric reading", () => { + expect(formatCurrency(Object.create(null))).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCurrency(Symbol("x"))).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCurrency({})).toBe(""); + }); + + it("should coerce the other values the way 2.3.0 did", () => { + // @ts-expect-error: intentionally invalid input + expect(formatCurrency(null)).toBe("0,00"); + // @ts-expect-error: intentionally invalid input + expect(formatCurrency([])).toBe("0,00"); + // @ts-expect-error: intentionally invalid input + expect(formatCurrency(true)).toBe("1,00"); + }); + + it("should read a bigint as a whole number", () => { + // @ts-expect-error: intentionally invalid input + expect(formatCurrency(1234n)).toBe("1.234,00"); + }); + + it("should fall back to a precision of 2 when the requested one is not a finite number", () => { + // @ts-expect-error: intentionally invalid input + expect(formatCurrency(1234.5678, { precision: "3" })).toBe("1.234,57"); + // @ts-expect-error: intentionally invalid input + expect(formatCurrency(1234.5678, { precision: true })).toBe("1.234,57"); + }); + it("should read as many fraction digits as the requested precision allows, not just the default 2, when reading a string", () => { expect(formatCurrency("1234,12345", { precision: 5 })).toBe("1.234,12345"); }); @@ -129,20 +158,32 @@ describe("formatCurrency", () => { }); describe("properties", () => { - const optionsArbitrary = fc - .option(fc.record({ symbol: fc.boolean(), precision: fc.double() }, { requiredKeys: [] })) - .map((options) => options ?? undefined); + const hostileValues = fc.oneof( + anyGarbage, + fc.constant(Object.create(null)), + fc.constant(Symbol("x")), + fc.bigInt(), + ); + + const nulls = fc.constant(null); + const symbols = fc.oneof(fc.boolean(), nulls, fc.string()); + const precisions = fc.oneof(fc.double(), nulls, fc.string(), fc.boolean()); + const optionRecord = fc.record( + { symbol: symbols, precision: precisions }, + { requiredKeys: [] }, + ); + const optionsArbitrary = fc.option(optionRecord).map((options) => options ?? undefined); test("should round-trip with parseCurrency for any value with 2 decimals", () => { expectRoundTrip(formatCurrency, parseCurrency, twoDecimalAmounts); }); test("should never throw, regardless of the input", () => { - expectNeverThrowsWithOptions(formatCurrency, anyGarbage, optionsArbitrary); + expectNeverThrowsWithOptions(formatCurrency, hostileValues, optionsArbitrary); }); test("should always return a string", () => { - expectAlwaysReturnsType(formatCurrency, "string", anyGarbage); + expectAlwaysReturnsType(formatCurrency, "string", hostileValues); }); }); }); diff --git a/src/format-currency/format-currency.ts b/src/format-currency/format-currency.ts index 01fd616b..5f646b2e 100644 --- a/src/format-currency/format-currency.ts +++ b/src/format-currency/format-currency.ts @@ -34,10 +34,13 @@ const getFormatter = (symbol: boolean, precision: number): Intl.NumberFormat => return formatter; }; -const toNumber = (value: unknown, precision: number): number => - typeof value === "string" - ? parseDecimal(value, { maxFractionDigits: Math.max(DEFAULT_PRECISION, precision) }) - : Number(value); +const toNumber = (value: unknown, precision: number): number => { + if (typeof value === "string") { + return parseDecimal(value, { maxFractionDigits: Math.max(DEFAULT_PRECISION, precision) }); + } + + return Number(value); +}; /** * Formats a given value as a currency string in Brazilian Real (BRL). @@ -50,9 +53,12 @@ const toNumber = (value: unknown, precision: number): number => * `"1.234,00"`. * * A value that is not a finite number, such as `NaN`, `Infinity` or `-Infinity`, formats as - * an empty string. + * an empty string, and so does a value that cannot be coerced to a number at all, such as a + * symbol, a null-prototype object or a plain object (`Number({})` is `NaN`); every other + * value goes through `Number()` the way 2.3.0 did, so `null`, `[]` and `true` still format. * - * The precision is clamped to `0-20`, the range Node's `Intl.NumberFormat` accepts. + * The precision is clamped to `0-20`, the range Node's `Intl.NumberFormat` accepts, and a + * precision that is not a finite number falls back to 2. * * @param {string|number} value - The value to be formatted. Can be a string or a number. * @param {FormatCurrencyOptions} [options] - Optional formatting options. @@ -60,6 +66,13 @@ const toNumber = (value: unknown, precision: number): number => * @param {number} options.precision - The number of decimal places to include in the formatted string. Defaults to 2, clamped to 0-20. * @returns {string} The formatted currency string, or an empty string when the value is not finite. * + * The `R$` prefix and the comma before the centavos are the ones Lei nº 9.069/1995, art. 1º, + * §§ 1º and 2º prescribes; the `.` grouping comes from the CLDR pt-BR locale data behind + * `Intl.NumberFormat`. + * + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l9069.htm + * @see Based on: https://cldr.unicode.org/ + * * @example * ```typescript * formatCurrency(1234.56); // "1.234,56" @@ -73,11 +86,15 @@ const toNumber = (value: unknown, precision: number): number => export const formatCurrency = (value: string | number, options?: FormatCurrencyOptions): string => { const precision = clampPrecision(options?.precision); - const enhancedValue = toNumber(value, precision); + try { + const enhancedValue = toNumber(value, precision); - if (!Number.isFinite(enhancedValue)) return ""; + if (!Number.isFinite(enhancedValue)) return ""; - return getFormatter(Boolean(options?.symbol), precision) - .format(enhancedValue) - .replace("\u00A0", " "); + return getFormatter(Boolean(options?.symbol), precision) + .format(enhancedValue) + .replaceAll("\u00A0", " "); + } catch { + return ""; + } }; diff --git a/src/parse-currency/parse-currency.test.ts b/src/parse-currency/parse-currency.test.ts index 7b0bbf42..b39b2df4 100644 --- a/src/parse-currency/parse-currency.test.ts +++ b/src/parse-currency/parse-currency.test.ts @@ -110,6 +110,21 @@ describe("parseCurrency", () => { expect(parseCurrency("R$ 150", { precision: -1 })).toBe(150); expect(parseCurrency("R$ 150", { precision: 21 })).toBe(150 / 10 ** 20); }); + + test("when the precision is not a finite number", () => { + // @ts-expect-error: intentionally invalid input + expect(parseCurrency("1,001", { precision: "3" })).toBe(1001); + // @ts-expect-error: intentionally invalid input + expect(parseCurrency("R$ 1,50", { precision: null })).toBe(1.5); + }); + }); + + describe("should return 0 for a value with no numeric reading", () => { + test("when the value is a null-prototype object or a symbol", () => { + expect(parseCurrency(Object.create(null))).toBe(0); + // @ts-expect-error: intentionally invalid input + expect(parseCurrency(Symbol("x"))).toBe(0); + }); }); describe("should round-trip with formatCurrency", () => { @@ -127,9 +142,16 @@ describe("parseCurrency", () => { }); describe("properties", () => { + const hostileValues = fc.oneof( + fc.anything(), + fc.constant(Object.create(null)), + fc.constant(Symbol("x")), + fc.bigInt(), + ); + test("should never throw and always return a finite number, regardless of the input", () => { fc.assert( - fc.property(fc.anything(), (value) => { + fc.property(hostileValues, (value) => { const result = parseCurrency(value as never); expect(Number.isFinite(result)).toBe(true); diff --git a/src/parse-currency/parse-currency.ts b/src/parse-currency/parse-currency.ts index b115adac..25889303 100644 --- a/src/parse-currency/parse-currency.ts +++ b/src/parse-currency/parse-currency.ts @@ -17,11 +17,20 @@ export type ParseCurrencyOptions = { * parses to 12.34. A `-` written before the first digit is preserved, so `"-R$ 1,00"` parses * to -1. * + * The precision is clamped to `0-20`, and a precision that is not a finite number falls back + * to 2. + * * @param {string} value - The string value to be parsed (e.g., "R$ 1.234,56" or "1234,56") * @param {ParseCurrencyOptions} [options] - Optional parsing options. * @param {number} options.precision - The number of decimal places used as the minor unit scale. Fractions accept up to two digits, or `precision` digits when it is greater. Defaults to 2, clamped to 0-20. * @returns {number} The parsed number value (e.g., 1234.56) * + * The `R$` prefix and the comma before the centavos are the ones Lei nº 9.069/1995, art. 1º, + * §§ 1º and 2º prescribes; the `.` grouping comes from the CLDR pt-BR locale data. + * + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l9069.htm + * @see Based on: https://cldr.unicode.org/ + * * @example * ```typescript * parseCurrency("R$ 1.234,56"); // returns 1234.56 From e10f87878c59009b4b75be33f82dfa9863f30b0c Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:46 -0300 Subject: [PATCH 10/75] refactor(words): always return lower case, drop the case option --- .../apply-words-case/apply-words-case.ts | 27 ---------- .../number-to-words/number-to-words.ts | 10 ---- .../convert-date-to-words.test.ts | 49 +++---------------- .../convert-date-to-words.ts | 19 +++---- .../convert-number-to-words.test.ts | 36 +++----------- .../convert-number-to-words.ts | 12 ++--- 6 files changed, 25 insertions(+), 128 deletions(-) delete mode 100644 src/_internals/apply-words-case/apply-words-case.ts diff --git a/src/_internals/apply-words-case/apply-words-case.ts b/src/_internals/apply-words-case/apply-words-case.ts deleted file mode 100644 index fddb580a..00000000 --- a/src/_internals/apply-words-case/apply-words-case.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { type WordsCase } from "../number-to-words/number-to-words"; - -/** - * Applies a `WordsCase` to a "por extenso" string already written out in lowercase. - * - * `"sentence"` capitalizes only the first letter; `"upper"` uppercases the whole string with - * `toLocaleUpperCase("pt-BR")`, which keeps accents intact ("três" -> "TRÊS"). Any value other - * than `"sentence"` or `"upper"` (including `"lower"`, `undefined` or an invalid value) returns - * `text` unchanged, since it is already written in lowercase. - * - * @param {string} text - The lowercase "por extenso" string to transform. - * @param {WordsCase} [wordsCase] - The case to apply. Defaults to `"lower"` (no change). - * @returns {string} `text` with the requested case applied. - * - * @example - * ```typescript - * applyWordsCase("três reais"); // "três reais" - * applyWordsCase("três reais", "sentence"); // "Três reais" - * applyWordsCase("três reais", "upper"); // "TRÊS REAIS" - * ``` - */ -export const applyWordsCase = (text: string, wordsCase?: WordsCase): string => { - if (wordsCase === "upper") return text.toLocaleUpperCase("pt-BR"); - if (wordsCase === "sentence") return text.charAt(0).toLocaleUpperCase("pt-BR") + text.slice(1); - - return text; -}; diff --git a/src/_internals/number-to-words/number-to-words.ts b/src/_internals/number-to-words/number-to-words.ts index 94935cb3..de359086 100644 --- a/src/_internals/number-to-words/number-to-words.ts +++ b/src/_internals/number-to-words/number-to-words.ts @@ -12,16 +12,6 @@ import { /** The grammatical gender `convertNumberToWords` agrees the number it writes out with. */ export type NumberToWordsGender = "masculine" | "feminine"; -/** - * Letter case applied to the final "por extenso" string of `convertNumberToWords`, - * `convertCurrencyToWords` and `convertDateToWords`. `"lower"` leaves the string as produced - * (every word already lowercase); `"sentence"` capitalizes only its first letter; `"upper"` - * uppercases the whole string with the "pt-BR" locale, which keeps accents intact - * ("três" -> "TRÊS", "março" -> "MARÇO"). Defaults to `"lower"`; any other value is ignored and - * `"lower"` is used instead. - */ -export type WordsCase = "lower" | "sentence" | "upper"; - export type NumberToWordsOptions = { /** Grammatical gender used to agree "um/dois" and the 100-999 group ("duzentos/duzentas", etc.) with the noun the number qualifies. Only the thousands group and the final 0-999 group are affected: the multiplier of "milhão/bilhão/trilhão" always agrees with those (masculine) nouns. Defaults to `"masculine"`. */ gender?: NumberToWordsGender; diff --git a/src/convert-date-to-words/convert-date-to-words.test.ts b/src/convert-date-to-words/convert-date-to-words.test.ts index 63d6e816..c6e80d87 100644 --- a/src/convert-date-to-words/convert-date-to-words.test.ts +++ b/src/convert-date-to-words/convert-date-to-words.test.ts @@ -1,7 +1,6 @@ import * as fc from "fast-check"; import { MONTH_NAMES, WEEKDAY_NAMES } from "../_internals/constants/number-words"; -import { type WordsCase } from "../_internals/number-to-words/number-to-words"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { convertDateToWords, type ConvertDateToWordsOptions } from "./convert-date-to-words"; @@ -73,41 +72,19 @@ describe("convertDateToWords", () => { expect(convertDateToWords("32/01/2024")).toBe(""); }); - describe("case option", () => { - test("should keep the result lowercase by default", () => { + describe("letter case", () => { + test("should always keep the result lowercase", () => { expect(convertDateToWords("01/01/2024")).toBe( "primeiro de janeiro de dois mil e vinte e quatro", ); - }); - - test("should keep the result lowercase for 'lower'", () => { - expect(convertDateToWords("01/01/2024", { case: "lower" })).toBe( - "primeiro de janeiro de dois mil e vinte e quatro", - ); - }); - - test("should capitalize only the first letter for 'sentence'", () => { - expect(convertDateToWords("01/01/2024", { case: "sentence" })).toBe( - "Primeiro de janeiro de dois mil e vinte e quatro", - ); - expect(convertDateToWords("10/05/1999", { case: "sentence" })).toBe( - "Dez de maio de mil novecentos e noventa e nove", - ); - }); - - test("should uppercase everything for 'upper', keeping accents", () => { - expect(convertDateToWords("02/03/2024", { case: "upper" })).toBe( - "DOIS DE MARÇO DE DOIS MIL E VINTE E QUATRO", + expect(convertDateToWords("02/03/2024")).toBe("dois de março de dois mil e vinte e quatro"); + expect(convertDateToWords("02/03/2024", { weekday: true })).toBe( + "sábado, dois de março de dois mil e vinte e quatro", ); }); + }); - test("should ignore an invalid case value and fall back to 'lower'", () => { - expect( - // @ts-expect-error: intentionally invalid input - convertDateToWords("01/01/2024", { case: "invalid" }), - ).toBe("primeiro de janeiro de dois mil e vinte e quatro"); - }); - + describe("style option", () => { test("should write only the month name and leave day/year as digits for 'month'", () => { expect(convertDateToWords("02/03/2024", { style: "month" })).toBe("2 de março de 2024"); }); @@ -203,15 +180,6 @@ describe("convertDateToWords", () => { "segunda-feira, 1º de janeiro de 2024", ); }); - - test("should combine with the 'case' option", () => { - expect(convertDateToWords("02/03/2024", { weekday: true, case: "sentence" })).toBe( - "Sábado, dois de março de dois mil e vinte e quatro", - ); - expect(convertDateToWords("02/03/2024", { weekday: true, case: "upper" })).toBe( - "SÁBADO, DOIS DE MARÇO DE DOIS MIL E VINTE E QUATRO", - ); - }); }); describe("invalid input", () => { @@ -381,7 +349,7 @@ describe("convertDateToWords", () => { expectDates(cases); }); - test("should reproduce every published brutils 'convert_date_to_text' example (tests/test_date_utils.py, lowercase here because brutils always capitalizes and this library exposes that as case: 'sentence')", () => { + test("should reproduce every published brutils 'convert_date_to_text' example (tests/test_date_utils.py, lowercase here because brutils always capitalizes and this library leaves casing to the caller)", () => { const cases: [string, string][] = [ ["15/08/2024", "quinze de agosto de dois mil e vinte e quatro"], ["01/01/2000", "primeiro de janeiro de dois mil"], @@ -467,7 +435,6 @@ describe("convertDateToWords types", () => { expectTypeOf(convertDateToWords) .parameter(1) .toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf< "full" | "month" | undefined >(); diff --git a/src/convert-date-to-words/convert-date-to-words.ts b/src/convert-date-to-words/convert-date-to-words.ts index d73b74df..9a46902a 100644 --- a/src/convert-date-to-words/convert-date-to-words.ts +++ b/src/convert-date-to-words/convert-date-to-words.ts @@ -1,11 +1,8 @@ -import { applyWordsCase } from "../_internals/apply-words-case/apply-words-case"; import { MONTH_NAMES, WEEKDAY_NAMES } from "../_internals/constants/number-words"; -import { numberToWords, type WordsCase } from "../_internals/number-to-words/number-to-words"; +import { numberToWords } from "../_internals/number-to-words/number-to-words"; /** Options of `convertDateToWords`. */ export type ConvertDateToWordsOptions = { - /** Letter case applied to the result: `"lower"` (unchanged), `"sentence"` (capitalizes only the first letter) or `"upper"` (uppercases everything, keeping accents). Defaults to `"lower"`; an invalid value is ignored and `"lower"` is used instead. */ - case?: WordsCase; /** Output style: `"full"` spells out the day, month and year (`"dois de março de dois mil e vinte e quatro"`); `"month"` spells out only the month name and leaves the day and year as digits (`"2 de março de 2024"`, day 1 as `"1º"`). Defaults to `"full"`; an invalid value is ignored and `"full"` is used instead. */ style?: "full" | "month"; /** Prefixes the pt-BR weekday name (lowercase) followed by a comma, e.g. `"sábado, dois de março de dois mil e vinte e quatro"`. The weekday is derived from the resolved calendar date (the `Date`'s local calendar date, or the parsed civil date for a string). Defaults to `false`. */ @@ -48,14 +45,14 @@ const dayToWords = (day: number, monthStyle: boolean): string => { * thousands comma that `convertNumberToWords`/`convertCurrencyToWords` use (`1999` reads as * `"mil novecentos e noventa e nove"`, not `"mil, novecentos e noventa e nove"`), matching how a * date is read aloud. `options.weekday` prefixes the pt-BR weekday name (lowercase) followed by - * a comma. February 29th is accepted on the leap years of the proleptic Gregorian calendar + * a comma. The result is always lowercase; apply any other casing to it yourself. + * February 29th is accepted on the leap years of the proleptic Gregorian calendar * (divisible by 4, except centuries that are not divisible by 400). Returns `""` when `value` is * not one of those forms, is an invalid `Date`, names a day/month that does not exist (e.g. * `"31/04/2024"` or `"29/02/2023"`), or falls before year 1, which has no year to write out. * * @param {Date|string} value - The date to convert: a `Date`, `"dd/mm/yyyy"` or ISO `"yyyy-mm-dd"`. * @param {ConvertDateToWordsOptions} [options] - Optional formatting options. - * @param {WordsCase} [options.case] - Letter case applied to the result. Defaults to `"lower"`. * @param {"full"|"month"} [options.style] - Output style. Defaults to `"full"`. * @param {boolean} [options.weekday] - Prefixes the pt-BR weekday name and a comma. Defaults to `false`. * @returns {string} The date written out in Portuguese, or `""` for invalid input. @@ -65,7 +62,6 @@ const dayToWords = (day: number, monthStyle: boolean): string => { * convertDateToWords("01/01/2024"); // "primeiro de janeiro de dois mil e vinte e quatro" * convertDateToWords("2024-01-02"); // "dois de janeiro de dois mil e vinte e quatro" * convertDateToWords(new Date(2024, 0, 1)); // "primeiro de janeiro de dois mil e vinte e quatro" - * convertDateToWords("01/01/2024", { case: "sentence" }); // "Primeiro de janeiro de dois mil e vinte e quatro" * convertDateToWords("02/03/2024", { style: "month" }); // "2 de março de 2024" * convertDateToWords("01/01/2024", { style: "month" }); // "1º de janeiro de 2024" * convertDateToWords("02/03/2024", { weekday: true }); // "sábado, dois de março de dois mil e vinte e quatro" @@ -119,10 +115,7 @@ export const convertDateToWords = ( const yearWords = isMonthStyle ? String(year) : numberToWords(year).replaceAll(", ", " "); const dateWords = `${dayToWords(day, isMonthStyle)} de ${monthName} de ${yearWords}`; - const result = - options?.weekday === true - ? `${WEEKDAY_NAMES[getWeekdayIndex(year, month, day)]}, ${dateWords}` - : dateWords; - - return applyWordsCase(result, options?.case); + return options?.weekday === true + ? `${WEEKDAY_NAMES[getWeekdayIndex(year, month, day)]}, ${dateWords}` + : dateWords; }; diff --git a/src/convert-number-to-words/convert-number-to-words.test.ts b/src/convert-number-to-words/convert-number-to-words.test.ts index 33c1dc7a..7ba46944 100644 --- a/src/convert-number-to-words/convert-number-to-words.test.ts +++ b/src/convert-number-to-words/convert-number-to-words.test.ts @@ -3,7 +3,6 @@ import * as fc from "fast-check"; import { NUMBER_TO_WORDS_MAX_VALUE, type NumberToWordsGender, - type WordsCase, } from "../_internals/number-to-words/number-to-words"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { convertNumberToWords, type ConvertNumberToWordsOptions } from "./convert-number-to-words"; @@ -88,29 +87,11 @@ describe("convertNumberToWords", () => { }); }); - describe("case option", () => { - test("should keep the result lowercase by default", () => { + describe("letter case", () => { + test("should always keep the result lowercase", () => { expect(convertNumberToWords(123)).toBe("cento e vinte e três"); - }); - - test("should keep the result lowercase for 'lower'", () => { - expect(convertNumberToWords(123, { case: "lower" })).toBe("cento e vinte e três"); - }); - - test("should capitalize only the first letter for 'sentence'", () => { - expect(convertNumberToWords(123, { case: "sentence" })).toBe("Cento e vinte e três"); - expect(convertNumberToWords(3, { case: "sentence" })).toBe("Três"); - }); - - test("should uppercase everything for 'upper', keeping accents", () => { - expect(convertNumberToWords(3, { case: "upper" })).toBe("TRÊS"); - expect(convertNumberToWords(50, { case: "upper" })).toBe("CINQUENTA"); - expect(convertNumberToWords(-3, { case: "upper" })).toBe("MENOS TRÊS"); - }); - - test("should ignore an invalid case value and fall back to 'lower'", () => { - // @ts-expect-error: intentionally invalid input - expect(convertNumberToWords(123, { case: "invalid" })).toBe("cento e vinte e três"); + expect(convertNumberToWords(3)).toBe("três"); + expect(convertNumberToWords(-3)).toBe("menos três"); }); }); @@ -599,14 +580,12 @@ describe("convertNumberToWords", () => { ); }); - test("should uppercase the result the same way as the lower case result, for the 'upper' case option", () => { + test("should never return a character in upper case", () => { fc.assert( fc.property(inRangeIntegerArbitrary, (value) => { - const lower = convertNumberToWords(value); + const words = convertNumberToWords(value); - expect(convertNumberToWords(value, { case: "upper" })).toBe( - lower.toLocaleUpperCase("pt-BR"), - ); + expect(words).toBe(words.toLocaleLowerCase("pt-BR")); }), ); }); @@ -622,7 +601,6 @@ describe("convertNumberToWords types", () => { expectTypeOf().toEqualTypeOf< NumberToWordsGender | undefined >(); - expectTypeOf().toEqualTypeOf(); expectTypeOf(convertNumberToWords).returns.toEqualTypeOf(); }); }); diff --git a/src/convert-number-to-words/convert-number-to-words.ts b/src/convert-number-to-words/convert-number-to-words.ts index 1e8ac3ae..60c8ef6c 100644 --- a/src/convert-number-to-words/convert-number-to-words.ts +++ b/src/convert-number-to-words/convert-number-to-words.ts @@ -1,17 +1,13 @@ -import { applyWordsCase } from "../_internals/apply-words-case/apply-words-case"; import { NUMBER_TO_WORDS_MAX_VALUE, type NumberToWordsGender, numberToWords, - type WordsCase, } from "../_internals/number-to-words/number-to-words"; /** Options of `convertNumberToWords`. */ export type ConvertNumberToWordsOptions = { /** Grammatical gender used to agree "um/dois" and the hundreds group ("duzentos/duzentas", etc.) with the noun the number qualifies. Defaults to `"masculine"`. */ gender?: NumberToWordsGender; - /** Letter case applied to the result: `"lower"` (unchanged), `"sentence"` (capitalizes only the first letter) or `"upper"` (uppercases everything, keeping accents). Defaults to `"lower"`; an invalid value is ignored and `"lower"` is used instead. */ - case?: WordsCase; }; /** @@ -25,10 +21,11 @@ export type ConvertNumberToWordsOptions = { * only writes out whole numbers, it never spells out a decimal part (use * `convertCurrencyToWords` for a monetary amount with cents). * + * The result is always lowercase; apply any other casing to it yourself. + * * @param {number} value - The integer to convert. * @param {ConvertNumberToWordsOptions} [options] - Optional formatting options. * @param {NumberToWordsGender} [options.gender] - Grammatical gender for "um/dois" and the hundreds group. Defaults to `"masculine"`. - * @param {WordsCase} [options.case] - Letter case applied to the result. Defaults to `"lower"`. * @returns {string} The cardinal number written out in Portuguese, or `""` for invalid input. * * @example @@ -38,7 +35,7 @@ export type ConvertNumberToWordsOptions = { * convertNumberToWords(2000000); // "dois milhões" * convertNumberToWords(-42); // "menos quarenta e dois" * convertNumberToWords(2, { gender: "feminine" }); // "duas" - * convertNumberToWords(3, { case: "upper" }); // "TRÊS" + * convertNumberToWords(12.9); // "doze" (truncated toward zero) * convertNumberToWords(NaN); // "" * ``` * @@ -57,7 +54,6 @@ export const convertNumberToWords = ( if (Math.abs(truncated) > NUMBER_TO_WORDS_MAX_VALUE) return ""; const words = numberToWords(Math.abs(truncated), { gender: options?.gender }); - const result = truncated < 0 ? `menos ${words}` : words; - return applyWordsCase(result, options?.case); + return truncated < 0 ? `menos ${words}` : words; }; From 6f2b2e883559104b63f235992db2f3adc0533502 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:46 -0300 Subject: [PATCH 11/75] feat(types): export the public types from the subpath entries --- src/get-cities/get-cities.test.ts | 17 +++++++++++++++++ src/get-cities/get-cities.ts | 17 +++++++++++------ .../get-municipalities.test.ts | 9 +++++++++ src/get-municipalities/get-municipalities.ts | 12 +++++++++++- .../get-municipality-by-code.ts | 2 ++ .../get-state-by-ibge-code.ts | 2 ++ .../get-state-name-by-code.ts | 2 ++ src/get-states/get-states.ts | 2 ++ 8 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/get-cities/get-cities.test.ts b/src/get-cities/get-cities.test.ts index be93e5f3..1b3aa49d 100644 --- a/src/get-cities/get-cities.test.ts +++ b/src/get-cities/get-cities.test.ts @@ -38,11 +38,28 @@ describe("getCities", () => { expect(cities).toEqual(sorted); }); + it("should sort every per-state list with the pt-BR comparator", () => { + for (const state of getStates()) { + const cities = getCities(state.code); + const sorted = [...cities].sort((a, b) => a.localeCompare(b, "pt-BR")); + + expect(cities).toEqual(sorted); + } + }); + it("should return empty array if state does not exist", () => { // @ts-expect-error: intentionally invalid input expect(getCities("ACC")).toEqual([]); }); + it("should return empty array for a truthy state that is not a string instead of throwing", () => { + expect(getCities(Object.create(null))).toEqual([]); + // @ts-expect-error: intentionally invalid input + expect(getCities(35)).toEqual([]); + // @ts-expect-error: intentionally invalid input + expect(getCities(["SP"])).toEqual([]); + }); + it("should return empty array for inherited Object property names instead of throwing", () => { // @ts-expect-error: intentionally invalid input expect(getCities("toString")).toEqual([]); diff --git a/src/get-cities/get-cities.ts b/src/get-cities/get-cities.ts index 81402207..6eded094 100644 --- a/src/get-cities/get-cities.ts +++ b/src/get-cities/get-cities.ts @@ -1,16 +1,21 @@ import { DATA as CITIES_DATA } from "../_internals/constants/cities"; import { type StateCode } from "../_internals/constants/states"; +export type { StateCode } from "../_internals/constants/states"; + let allCitiesCache: string[] | undefined; /** * Returns a list of city names for a given Brazilian state, or all cities if no state is specified. * - * If a state code is provided, the function returns its cities sorted alphabetically. - * If no state is provided, it returns all cities from all states, sorted with - * `localeCompare` in the "pt-BR" locale so accented names land where a Brazilian reader - * expects them (the combined, sorted list is computed once and cached; every call returns - * a fresh copy). + * If a state code is provided, the function returns its cities sorted with `localeCompare` + * in the "pt-BR" locale. If no state is provided, it returns all cities from all states, + * sorted the same way so accented names land where a Brazilian reader expects them (the + * combined, sorted list is computed once and cached; every call returns a fresh copy). + * + * Every falsy `state` asks for the full list, so `getCities(null)` and `getCities("")` return + * every city. The sibling `getMunicipalities` is stricter and only reads an omitted (or + * `undefined`) state code that way, returning `[]` for `null` and `""`. * * @param {StateCode} [state] - The code of the Brazilian state to filter cities by. Optional. * @returns {string[]} An array of city names, sorted alphabetically. Returns an empty array if the state is not found. @@ -33,7 +38,7 @@ export const getCities = (state?: StateCode): string[] => { return [...allCitiesCache]; } - if (!Object.hasOwn(CITIES_DATA, state)) return []; + if (typeof state !== "string" || !Object.hasOwn(CITIES_DATA, state)) return []; return CITIES_DATA[state].map(([name]) => name); }; diff --git a/src/get-municipalities/get-municipalities.test.ts b/src/get-municipalities/get-municipalities.test.ts index 18f7af21..dd69fd99 100644 --- a/src/get-municipalities/get-municipalities.test.ts +++ b/src/get-municipalities/get-municipalities.test.ts @@ -30,6 +30,15 @@ describe("getMunicipalities", () => { expect(names).toEqual(sortedNames); }); + it("should sort every per-state list with the pt-BR comparator", () => { + for (const state of getStates()) { + const names = getMunicipalities(state.code).map((municipality) => municipality.name); + const sortedNames = [...names].sort((a, b) => a.localeCompare(b, "pt-BR")); + + expect(names).toEqual(sortedNames); + } + }); + it("should return municipality objects shaped as { code, name, stateCode }", () => { const saoPaulo = getMunicipalities("SP").find( (municipality) => municipality.name === "São Paulo", diff --git a/src/get-municipalities/get-municipalities.ts b/src/get-municipalities/get-municipalities.ts index 82132108..75c7a371 100644 --- a/src/get-municipalities/get-municipalities.ts +++ b/src/get-municipalities/get-municipalities.ts @@ -2,6 +2,9 @@ import { DATA as CITIES_DATA, type Municipality } from "../_internals/constants/ import { type StateCode } from "../_internals/constants/states"; import { getStates } from "../get-states/get-states"; +export type { Municipality } from "../_internals/constants/cities"; +export type { StateCode } from "../_internals/constants/states"; + const buildMunicipalities = (stateCode: StateCode): Municipality[] => CITIES_DATA[stateCode].map(([name, code]) => ({ code, name, stateCode })); @@ -10,7 +13,13 @@ const buildMunicipalities = (stateCode: StateCode): Municipality[] => * * If `stateCode` is provided, only municipalities of that state are returned. If it is * omitted, every municipality of every state is returned, sorted with `localeCompare` in the - * "pt-BR" locale so accented names land where a Brazilian reader expects them. + * "pt-BR" locale so accented names land where a Brazilian reader expects them. Every per-state + * list is sorted the same way. + * + * Only an omitted (or `undefined`) `stateCode` asks for the full list: any other value that is + * not a known state code, `null` and `""` included, returns `[]`. The sibling `getCities` is + * looser and treats every falsy `state` as "no state given", so `getCities(null)` returns the + * full list where `getMunicipalities(null)` returns `[]`. * * @param {StateCode} [stateCode] - The two letter code of the Brazilian state to filter by. * @returns {Municipality[]} A fresh array of fresh `Municipality` objects. Empty when @@ -21,6 +30,7 @@ const buildMunicipalities = (stateCode: StateCode): Municipality[] => * getMunicipalities("SP")[0]; // { code: "3500105", name: "Adamantina", stateCode: "SP" } * getMunicipalities().length; // every municipality of every state * getMunicipalities("ZZ"); // [] + * getMunicipalities(null); // [] (only an omitted state code asks for the full list) * ``` * * @see Official: https://servicodados.ibge.gov.br/api/docs/localidades diff --git a/src/get-municipality-by-code/get-municipality-by-code.ts b/src/get-municipality-by-code/get-municipality-by-code.ts index 0fe0a02f..c4a2c216 100644 --- a/src/get-municipality-by-code/get-municipality-by-code.ts +++ b/src/get-municipality-by-code/get-municipality-by-code.ts @@ -3,6 +3,8 @@ import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { getStates } from "../get-states/get-states"; +export type { Municipality } from "../_internals/constants/cities"; + /** * Looks up a Brazilian municipality by its 7 digit IBGE code, published by the IBGE. * diff --git a/src/get-state-by-ibge-code/get-state-by-ibge-code.ts b/src/get-state-by-ibge-code/get-state-by-ibge-code.ts index 42d30a4e..7cc229b9 100644 --- a/src/get-state-by-ibge-code/get-state-by-ibge-code.ts +++ b/src/get-state-by-ibge-code/get-state-by-ibge-code.ts @@ -2,6 +2,8 @@ import { DATA, type State } from "../_internals/constants/states"; import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +export type { State } from "../_internals/constants/states"; + /** * Retrieves the Brazilian state whose 2-digit IBGE code ("cUF", the Código da Unidade da * Federação) matches the given value. diff --git a/src/get-state-name-by-code/get-state-name-by-code.ts b/src/get-state-name-by-code/get-state-name-by-code.ts index 6bf60631..5970c7ca 100644 --- a/src/get-state-name-by-code/get-state-name-by-code.ts +++ b/src/get-state-name-by-code/get-state-name-by-code.ts @@ -1,5 +1,7 @@ import { DATA, type StateName } from "../_internals/constants/states"; +export type { StateName } from "../_internals/constants/states"; + /** * Retrieves the full name of a Brazilian state given its two-letter code (sigla). * diff --git a/src/get-states/get-states.ts b/src/get-states/get-states.ts index 3125fdc5..460e2876 100644 --- a/src/get-states/get-states.ts +++ b/src/get-states/get-states.ts @@ -1,5 +1,7 @@ import { DATA, type State } from "../_internals/constants/states"; +export type { State } from "../_internals/constants/states"; + /** * Retrieves a list of all Brazilian states with their codes and names. * From 40563db04992ba86c67932db629421fb36b1a1df Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:46 -0300 Subject: [PATCH 12/75] ci(tree-shaking): fail the job when the base measurement fails --- .github/workflows/build.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cbcbedc6..8c447991 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,15 +69,13 @@ jobs: if: ${{ github.event_name == 'pull_request' }} id: base run: | - if [ -f base/scripts/tree-shaking.ts ]; then - node scripts/tree-shaking.ts --json head.json - (cd base && npm ci && npm run build && node scripts/tree-shaking.ts --json ../base.json --surviving ../head.json) || true - fi - if [ -f base.json ]; then - echo "measured=true" >> "$GITHUB_OUTPUT" - else + if [ ! -f base/scripts/tree-shaking.ts ]; then echo "measured=false" >> "$GITHUB_OUTPUT" + exit 0 fi + node scripts/tree-shaking.ts --json head.json + (cd base && npm ci && npm run build && node scripts/tree-shaking.ts --json ../base.json --surviving ../head.json) + echo "measured=true" >> "$GITHUB_OUTPUT" - name: Compare against base if: ${{ github.event_name == 'pull_request' }} From 01b7d325fd0ac40548ae64f28f258a8fe22b2fec Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:42:46 -0300 Subject: [PATCH 13/75] docs: cite the annexes, decrees and manuals behind the identifiers and refresh the generated pages --- docs/getting-started.md | 16 +- docs/llms-full.txt | 312 ++++++++++++------ docs/llms.txt | 23 +- docs/pt-br/getting-started.md | 16 +- docs/pt-br/utilities.md | 297 +++++++++++------ docs/utilities.md | 295 +++++++++++------ scripts/banks.ts | 2 +- scripts/legal-natures.ts | 17 +- src/_internals/constants/certidao.ts | 19 +- .../constants.ts | 12 +- .../convert-license-plate-to-mercosul.test.ts | 2 +- .../convert-license-plate-to-mercosul.ts | 9 +- src/format-boleto/format-boleto.ts | 10 +- src/format-certidao/format-certidao.ts | 19 +- src/format-cns/format-cns.ts | 11 +- src/format-passport/format-passport.ts | 1 + src/generate-boleto/generate-boleto.test.ts | 44 ++- src/generate-boleto/generate-boleto.ts | 26 +- src/generate-cpf/generate-cpf.ts | 7 +- src/generate-passport/generate-passport.ts | 1 + src/get-bank-by-code/get-bank-by-code.ts | 4 +- src/get-bank-by-ispb/get-bank-by-ispb.ts | 4 +- src/get-banks/get-banks.ts | 4 +- src/get-boleto-info/constants.ts | 20 +- src/get-boleto-info/get-boleto-info.test.ts | 30 +- src/get-boleto-info/get-boleto-info.ts | 24 +- src/index.test.ts | 17 +- src/index.ts | 21 +- src/is-valid-boleto/is-valid-boleto.ts | 14 +- src/is-valid-certidao/is-valid-certidao.ts | 19 +- src/is-valid-cns/is-valid-cns.ts | 7 +- src/is-valid-legal-nature/constants.ts | 11 +- src/is-valid-passport/is-valid-passport.ts | 4 + src/parse-boleto/parse-boleto.ts | 10 +- src/parse-certidao/constants.ts | 18 +- src/parse-certidao/parse-certidao.ts | 21 +- src/parse-passport/parse-passport.ts | 1 + src/remove-accents/remove-accents.ts | 3 + 38 files changed, 896 insertions(+), 475 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 8027214e..c7ed8ebf 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -69,13 +69,13 @@ A handful of utils are the exception: each embeds an official dataset, so it wei | Util | Dataset | Minified | Gzipped | | --- | --- | --- | --- | -| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 IBGE municipalities, with names and codes | 156 KB | 50 KB | -| `getCities` | 5571 IBGE municipality names | 153 KB | 49 KB | -| `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 113 KB | 24 KB | -| `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 110 KB | 27 KB | -| `isValidCnae` · `getCnae` | CNAE 2.3 subclasses | 93 KB | 21 KB | -| `isValidCfop` · `getCfop` | CFOP operation descriptions | 55 KB | 5.4 KB | -| `getBanks` · `getBankByCode` | Banco Central STR participants (COMPE + ISPB) | 28 KB | 7.3 KB | +| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 IBGE municipalities, with names and codes | 155.9 KB | 50.0 KB | +| `getCities` | 5571 IBGE municipality names | 153.6 KB | 49.4 KB | +| `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 113.4 KB | 24.0 KB | +| `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 118.4 KB | 30.2 KB | +| `isValidCnae` · `getCnae` | CNAE 2.3 subclasses | 93.6 KB | 21.1 KB | +| `isValidCfop` · `getCfop` | CFOP operation descriptions | 68.3 KB | 6.5 KB | +| `getBanks` · `getBankByCode` | Banco Central STR participants (COMPE + ISPB) | 37.9 KB | 9.3 KB | Importing any of them from the root, even alongside a single small util, pulls that whole dataset into your main bundle, because this package ships as a single ESM module: a dynamic `import()` of the root (`await import('@brazilian-utils/brazilian-utils')`) still resolves to that same one file, so it can't be split out on its own. A bundler doing code-splitting needs a separate module to split *into*. @@ -97,4 +97,4 @@ getMunicipalityByCode('3550308'); Every util is available this way, as `@brazilian-utils/brazilian-utils/` (kebab-case, matching the function name: `isValidCpf` → `is-valid-cpf`), for the same lazy-loading/code-splitting reason. -Pick one style per util in a given app: a bundler treats the root import and the subpath import as two unrelated modules, so importing `getCities` from both the root *and* `/get-cities` in the same app bundles the 153 KB city table twice, once in each module's own output. +Pick one style per util in a given app: a bundler treats the root import and the subpath import as two unrelated modules, so importing `getCities` from both the root *and* `/get-cities` in the same app bundles the 153.6 KB city table twice, once in each module's own output. diff --git a/docs/llms-full.txt b/docs/llms-full.txt index f4e491e2..d8ad9ee0 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -103,6 +103,7 @@ - [isHoliday](#isholiday) - [isBusinessDay](#isbusinessday) - [addBusinessDays](#addbusinessdays) + - [subBusinessDays](#subbusinessdays) - [differenceInBusinessDays](#differenceinbusinessdays) - [convertDateToWords](#convertdatetowords) - [formatVoterId](#formatvoterid) @@ -206,13 +207,13 @@ A handful of utils are the exception: each embeds an official dataset, so it wei | Util | Dataset | Minified | Gzipped | | --- | --- | --- | --- | -| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 IBGE municipalities, with names and codes | 156 KB | 50 KB | -| `getCities` | 5571 IBGE municipality names | 153 KB | 49 KB | -| `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 113 KB | 24 KB | -| `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 110 KB | 27 KB | -| `isValidCnae` · `getCnae` | CNAE 2.3 subclasses | 93 KB | 21 KB | -| `isValidCfop` · `getCfop` | CFOP operation descriptions | 55 KB | 5.4 KB | -| `getBanks` · `getBankByCode` | Banco Central STR participants (COMPE + ISPB) | 28 KB | 7.3 KB | +| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 IBGE municipalities, with names and codes | 155.9 KB | 50.0 KB | +| `getCities` | 5571 IBGE municipality names | 153.6 KB | 49.4 KB | +| `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 113.4 KB | 24.0 KB | +| `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 118.4 KB | 30.2 KB | +| `isValidCnae` · `getCnae` | CNAE 2.3 subclasses | 93.6 KB | 21.1 KB | +| `isValidCfop` · `getCfop` | CFOP operation descriptions | 68.3 KB | 6.5 KB | +| `getBanks` · `getBankByCode` | Banco Central STR participants (COMPE + ISPB) | 37.9 KB | 9.3 KB | Importing any of them from the root, even alongside a single small util, pulls that whole dataset into your main bundle, because this package ships as a single ESM module: a dynamic `import()` of the root (`await import('@brazilian-utils/brazilian-utils')`) still resolves to that same one file, so it can't be split out on its own. A bundler doing code-splitting needs a separate module to split *into*. @@ -234,13 +235,13 @@ getMunicipalityByCode('3550308'); Every util is available this way, as `@brazilian-utils/brazilian-utils/` (kebab-case, matching the function name: `isValidCpf` → `is-valid-cpf`), for the same lazy-loading/code-splitting reason. -Pick one style per util in a given app: a bundler treats the root import and the subpath import as two unrelated modules, so importing `getCities` from both the root *and* `/get-cities` in the same app bundles the 153 KB city table twice, once in each module's own output. +Pick one style per util in a given app: a bundler treats the root import and the subpath import as two unrelated modules, so importing `getCities` from both the root *and* `/get-cities` in the same app bundles the 153.6 KB city table twice, once in each module's own output. ## Utilities Here you will find all the utilities available for use. -> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function (including `capitalize`) returns an empty value of its return type: `""` for strings, `0` for `parseCurrency`. `formatCurrency` returns `""` for a non-finite number. +> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function (including `capitalize`) returns an empty value of its return type: `""` for strings, `0` for `parseCurrency`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. ### isValidCpf @@ -277,7 +278,7 @@ parseCpf('746.506.880-00'); // 74650688000 ### generateCpf -Generate a valid random CPF. +Generate a valid random CPF. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generateCpf } from '@brazilian-utils/brazilian-utils' @@ -350,7 +351,7 @@ generateCnpj(2); // alphanumeric CNPJ, e.g. 'Q0SLFMBD7VX439' ### isValidBoleto -Check if boleto ([brazilian payment method](https://en.wikipedia.org/wiki/Boleto)) is valid. Supports both the 47 digit "cobrança bancária" boleto and the "boleto de arrecadação" (convênio/tributos): either its 48 digit linha digitável or its 44 digit barcode, both starting with `8`. +Check if boleto ([brazilian payment method](https://en.wikipedia.org/wiki/Boleto)) is valid. Supports both the 47 digit "cobrança bancária" boleto and the "boleto de arrecadação" (convênio/tributos): either its 48 digit linha digitável or its 44 digit barcode, both starting with `8`. One leniency is kept from 2.3.0: the código de moeda in position 4 of the cobrança bancária barcode is not checked, although Carta-Circular BCB nº 2.926/2000 fixes it at `9` (real), so a slip carrying any other moeda digit still validates. ```javascript import { isValidBoleto } from '@brazilian-utils/brazilian-utils'; @@ -384,7 +385,7 @@ parseBoleto('00190.00009 01149.718601 68524.522114 6 75860000102656'); // 001900 ### generateBoleto -Generate a valid random boleto. Pass `{ type: "arrecadacao" }` (typed as `GenerateBoletoOptions`) to generate a boleto de arrecadação instead of the default "bancario" (cobrança bancária) type. +Generate a valid random boleto. Pass `{ type: "arrecadacao" }` (typed as `GenerateBoletoOptions`) to generate a boleto de arrecadação instead of the default "bancario" (cobrança bancária) type. An arrecadação slip draws its segment from 1 to 7 (segment 9 is the banks' own) and its value identifier from all four values, `6` and `8` for an effective amount and `7` and `9` for a reference quantity, so both `hasEffectiveValue` branches of `getBoletoInfo` are reachable. ```javascript import { generateBoleto } from '@brazilian-utils/brazilian-utils'; @@ -395,7 +396,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ### getBoletoInfo -Extract information from a boleto (amount, expiration date, bank code). Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). For a boleto de arrecadação, the result, typed as `BoletoInfo`, has no `bankCode`/`expirationDate` and instead carries `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. +Extract information from a boleto (amount, expiration date, bank code). Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. For a boleto de arrecadação, the result, typed as `BoletoInfo`, has no `bankCode`/`expirationDate` and instead carries `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -447,7 +448,7 @@ parsePixKey('+5551998259765'); // { type: 'phone', value: '+5551998259765' } ### isValidPixPayload -Check if a Pix BR Code payload (the string behind a Pix QR Code and behind "Pix copia e cola") is valid: well-formed TLV structure, the mandatory objects present, one of the "Merchant Account Information" templates carrying the `br.gov.bcb.pix` GUI with a key or a URL, a "Point of Initiation Method" object (`01`) that agrees with it (a key requires a static payload, so `01` is absent or `"11"`; a URL requires a dynamic one, so `01` is `"12"`), an amount (`54`) greater than zero in a static payload, and a matching CRC-16. The key itself is not checked against the DICT formats, use `isValidPixKey` for that. Payloads that carry the location in an Unreserved Template (IDs 80 to 99), as the "QR Code composto" of Pix Automático (Pix recorrente) does, are out of scope and reported as invalid. +Check if a Pix BR Code payload (the string behind a Pix QR Code and behind "Pix copia e cola") is valid: well-formed TLV structure, the mandatory objects present, one of the "Merchant Account Information" templates carrying the `br.gov.bcb.pix` GUI with a key or a URL, and a matching CRC-16. The "Point of Initiation Method" object (`01`) is advisory: the Manual do BR Code marks it optional and only assigns a meaning to the value `"12"` ("só pode ser utilizado uma vez"), so it may be absent from either shape and only a value outside `{"11", "12"}` makes the payload invalid. When a payload built around a key carries an amount (`54`), that amount must be greater than zero, unless the payload is a Pix Saque BR Code, i.e. unless it carries the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`) as §2.6 of the Pix manual prescribes; rejecting `"0"`/`"0.00"` without `fss` is a deliberate restriction of this library, not a rule of the manual. The key itself is not checked against the DICT formats, use `isValidPixKey` for that. Payloads that carry the location in an Unreserved Template (IDs 80 to 99), as the "QR Code composto" of Pix Automático (Pix recorrente) does, are out of scope and reported as invalid. ```javascript import { isValidPixPayload } from '@brazilian-utils/brazilian-utils'; @@ -462,7 +463,7 @@ isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (broken CRC) ### parsePixPayload -Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The result is typed as `PixPayload`; `pointOfInitiation` is typed as `PixPointOfInitiation` (`"static"` or `"dynamic"`). The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`), and the "Point of Initiation Method" object (`01`) must agree with it: a key belongs to a static payload (`01` absent or `"11"`) and a `url` to a dynamic one (`01` set to `"12"`), so any other pairing returns `null`. A static payload that states an amount must state one greater than zero (`54` set to `0.00` is reserved for the Pix Saque/Troco BR Code, which is out of scope), and in a dynamic payload the amount and the `txid` are ignored, as the manual mandates. Payloads whose location lives in an Unreserved Template (IDs 80 to 99, Pix Automático) are out of scope and return `null`. +Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The result is typed as `PixPayload`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Payloads whose location lives in an Unreserved Template (IDs 80 to 99, Pix Automático) are out of scope and return `null`. ```javascript import { parsePixPayload } from '@brazilian-utils/brazilian-utils'; @@ -474,13 +475,14 @@ parsePixPayload( // { // key: '123e4567-e12b-12d1-a456-426655440000', // merchantName: 'Fulano de Tal', -// merchantCity: 'BRASILIA' +// merchantCity: 'BRASILIA', +// pointOfInitiation: 'static' // } ``` ### generatePixPayload -Generates the payload of a Pix BR Code. Exactly one of `params.key` or `params.url` must be given (part of `GeneratePixPayloadParams`); `null` is returned when both or neither are given. `url` must be a PSP location as the Bacen manual defines it: a host name with a path, without a scheme (`pix.example.com/qr/v2/1234`); a dynamic payload cannot carry `amount` or `txid`, which belong to the PSP location, and an `amount` that rounds to `0.00` is rejected. +Generates the payload of a Pix BR Code. Exactly one of `params.key` or `params.url` must be given (part of `GeneratePixPayloadParams`); `null` is returned when both or neither are given. `url` must be a PSP location as the Bacen manual defines it: a host name with a path, without a scheme (`pix.example.com/qr/v2/1234`); a dynamic payload cannot carry `amount` or `txid`, which belong to the PSP location. The amount is written with the two decimal places the BR Code takes, so one that rounds to `0.00` and one that does not survive that round trip (`0.005`, `123.456`) are both rejected rather than written as a different sum. The Pix Saque BR Code, which announces the `fss` of sub-object 26-03, is parsed by `parsePixPayload` but not generated here. When `params.key` is given, it is normalized to its DICT canonical form by `parsePixKey` and the payload is static. When `params.url` is given instead (the PSP location, without a URL scheme, e.g. `"pix.example.com/qr/v2/1234"`), the payload is dynamic per the Manual de Padrões para Iniciação do Pix: the URL takes the key's place in the "Merchant Account Information" template and the "Point of Initiation Method" object is set to dynamic (`12`); `params.url` can be at most 77 characters. `merchantName`, `merchantCity` and `description` are folded to printable ASCII (accents dropped) and truncated to what the BR Code allows. `parsePixPayload` already parses both shapes, so `parsePixPayload(generatePixPayload({ url, ... }))` round-trips. @@ -507,21 +509,25 @@ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // nu ### isValidNfeKey -Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document that shares the same 44 digit layout: NF-e (modelo 55), NFC-e (modelo 65), CT-e (modelo 57), MDF-e (modelo 58) and CT-e OS (modelo 67, the Conhecimento de Transporte Eletrônico para Outros Serviços of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/aj_009_07)). Accepts whitespace between digit groups (the common display mask) and the `NFe` prefix found in the `Id` attribute of the document's XML. The emission type (`tpEmis`) must be one of the codes the MOC assigns, 1 to 7 or 9; 8 is not assigned and makes the key invalid. +Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e (65), CT-e (57), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para Outros Serviços of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), GTV-e (64, the CT-e Guia de Transporte de Valores), BP-e (63), NF3e (66) and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed differently. Accepts whitespace between digit groups (the common display mask) and the `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the document's XML. + +The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` for the CT-e, `{1, 5, 7, 8}` for the CT-e OS, `{1, 2, 7, 8}` for the GTV-e, `{1, 2, 3}` for the MDF-e and `{1, 2}` for the BP-e, the NF3e and the NFCom. Code 8, the authorização pela SVC-SP, is assigned by the [CT-e MOC 4.00](https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais) only, never by the NF-e one; the domains of the [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), the [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) and the [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) come from their own manuals. For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, which forbids the twenty repeated and sequential `cNF` values it lists and a `cNF` equal to the document number. Rejecting a document number of all zeros, on the other hand, is a choice of this library: no MOC rule was found forbidding it. ```javascript import { isValidNfeKey } from '@brazilian-utils/brazilian-utils'; isValidNfeKey('35170458716523000119550010000000121000123458'); // true (NF-e, SP) isValidNfeKey('NFe35170458716523000119550010000000121000123458'); // true (XML Id prefix) +isValidNfeKey('CTe35170458716523000119570010000000128000123452'); // true (CT-e authorised by the SVC-SP) isValidNfeKey('3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458'); // true (masked) isValidNfeKey('99170458716523000119550010000000121000123458'); // false (invalid cUF) -isValidNfeKey('35170458716523000119550010000000128000123455'); // false (tpEmis 8 is not assigned) +isValidNfeKey('35170458716523000119550010000000128000123455'); // false (the NF-e MOC does not assign tpEmis 8) +isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 00000000, rule B03-10) ``` ### formatNfeKey -Format a DF-e (NF-e, NFC-e, CT-e, MDF-e or CT-e OS) access key into groups of 4 digits separated by spaces, the common display form printed on the DANFE. +Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. ```javascript import { formatNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -532,7 +538,7 @@ formatNfeKey('35170458716523000119550010000000121000123458'); ### parseNfeKey -Parses a DF-e access key into its fields (state, year, month, taxId, model, series, number, emissionType, code, checkDigit). Accepts the same input forms as `isValidNfeKey` and returns `null` when the key is not valid. The result is typed as `NfeKey`. +Parses a DF-e access key into its fields (state, year, month, taxId, model, series, number, emissionType, code, checkDigit). Accepts the same input forms as `isValidNfeKey` and returns `null` when the key is not valid. The result is typed as `NfeKey`, whose `model` is an `NfeKeyModel`. NFCom (`'62'`) and NF3e (`'66'`) spend position 36 of the key on `nSiteAutoriz`, the site of the authorizer that received the document, so for those two models the result also carries `authorizationSite` and `code` is 7 digits instead of 8. ```javascript import { parseNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -541,6 +547,10 @@ parseNfeKey('35170458716523000119550010000000121000123458'); // { state: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '55', // series: 1, number: 12, emissionType: 1, code: '00012345', checkDigit: 8 } +parseNfeKey('35170458716523000119620010000000121000123450'); +// { state: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', +// series: 1, number: 12, emissionType: 1, authorizationSite: 0, code: '0012345', checkDigit: 0 } + parseNfeKey('invalid'); // null ``` @@ -570,7 +580,7 @@ isValidPhone('11900000000', { accept: [] }); // false ### formatPhone -Format phone number according to Brazilian patterns. `options.mask` (typed as `PhoneMask`) accepts `"sn"` (default, subscriber number only, 9 digits, no DDD), `"nanp"` (DDD + subscriber number, 11 digits), `"e164"` (`"+5511987654321"`), `"international"` (`"+55 11 98765-4321"`, the way a Brazilian number is printed for foreign callers), `"service"` (`"0800 123 4567"` or `"4004-1234"`, the conventional groupings for service numbers) or `"auto"`. `"auto"` picks `"international"` when `value` carries a Brazilian country code (`+55`, `0055` or a bare `55` followed by 10 or 11 digits), `"service"` when `value` is a service number, and otherwise falls back to the digit count: `"nanp"` when `value` has more digits than a bare subscriber number, `"sn"` when it does not. `"e164"` and `"international"` drop the country code from `value` first, under the rule documented in `parsePhone`, and fall back to the `"service"` presentation for a service number, since those have no E.164 form. If `value` includes a DDD, pass `{ mask: 'auto' }` (or `'nanp'`) explicitly, since the default `"sn"` mask assumes no DDD and silently truncates one if present. +Format phone number according to Brazilian patterns. `options.mask` (typed as `PhoneMask`) accepts `"sn"` (default, subscriber number only, 9 digits, no DDD), `"nanp"` (DDD + subscriber number, `"(00) 00000-0000"` for the 11 digits of a mobile and `"(00) 0000-0000"` for the 10 digits of a landline, any other length keeping the 11 digit grouping), `"e164"` (`"+5511987654321"`), `"international"` (`"+55 11 98765-4321"`, the way a Brazilian number is printed for foreign callers), `"service"` (`"0800 123 4567"` or `"4004-1234"`, the conventional groupings for service numbers) or `"auto"`. `"auto"` picks `"international"` when `value` carries a Brazilian country code (`+55`, `0055` or a bare `55` followed by 10 or 11 digits), `"service"` when `value` is a service number, and otherwise falls back to the digit count: `"nanp"` when `value` has more digits than a bare subscriber number, `"sn"` when it does not. `"e164"` and `"international"` drop the country code from `value` first, under the rule documented in `parsePhone`, and fall back to the `"service"` presentation for a service number, since those have no E.164 form. If `value` includes a DDD, pass `{ mask: 'auto' }` (or `'nanp'`) explicitly, since the default `"sn"` mask assumes no DDD and silently truncates one if present. A `mask` outside the union falls back to the default `"sn"` instead of throwing. ```javascript import { formatPhone } from '@brazilian-utils/brazilian-utils'; @@ -578,6 +588,8 @@ import { formatPhone } from '@brazilian-utils/brazilian-utils'; formatPhone('987654321'); // 98765-4321 (default "sn", no DDD) formatPhone('11900000000', { mask: 'nanp' }); // (11) 90000-0000 formatPhone('11900000000', { mask: 'auto' }); // (11) 90000-0000 +formatPhone('1130000000', { mask: 'nanp' }); // (11) 3000-0000 (10 digit landline) +formatPhone('1130000000', { mask: 'auto' }); // (11) 3000-0000 (10 digit landline) formatPhone('11987654321', { mask: 'e164' }); // +5511987654321 formatPhone('+5511987654321', { mask: 'international' }); // +55 11 98765-4321 formatPhone('08001234567', { mask: 'service' }); // 0800 123 4567 @@ -601,7 +613,7 @@ parsePhone('55987654321'); // 55987654321 (area code 55, not mistaken for the +5 ### isValidMobilePhone -Check if mobile phone number is valid. `options.version` (typed as `PhoneVersion`) controls which mobile numbering rule is enforced: `1` (default) is the pre-Resolução Anatel 749/2022 format, kept for 2.3.0 compatibility, whose first number digit (after the DDD) may be 6, 7, 8 or 9; `2` enforces only 9, a stricter subset of the resolution's art. 12 I (Serviço Móvel Pessoal). +Check if mobile phone number is valid. `options.version` (typed as `PhoneVersion`) controls which mobile numbering rule is enforced: `1` (default) is the pre-Resolução Anatel 749/2022 format, kept for 2.3.0 compatibility, whose first number digit (after the DDD) may be 6, 7, 8 or 9; `2` enforces only 9, a stricter subset of the resolution's art. 12 I (Serviço Móvel Pessoal). Version `1` also does not carve out the `700` prefix, which art. 12 II reserves for the Serviço Móvel Global por Satélite rather than SMP, so `isValidMobilePhone('11700123456')` is `true` for a number outside SMP; version `2` rejects it. ```javascript import { isValidMobilePhone } from '@brazilian-utils/brazilian-utils'; @@ -623,7 +635,7 @@ isValidLandlinePhone('1130000000'); // true ### isValidServicePhone -Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`; `112` and `911` are accepted too, as mobile-only aliases of `190` that Anatel lists alongside the other 3-digit codes). Only the structure is checked, the number does not have to be assigned to anyone. +Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`; `112` and `911` are accepted too, as mobile-only aliases of `190` that Anatel lists alongside the other 3-digit codes). Only the structure is checked, the number does not have to be assigned to anyone. Anatel publishes no allocation for the abbreviated numbers, so only the conventional `300X` and `400X` roots are recognised: other "Número Único" carrier prefixes in market use, such as `4020` and `4062`, are out of scope and are rejected. ```javascript import { isValidServicePhone } from '@brazilian-utils/brazilian-utils'; @@ -691,14 +703,17 @@ isValidLicensePlate('ABC1234EXTRA'); // false (too many characters) ### isValidRenavam -Check if RENAVAM (Registro Nacional de Veículos Automotores) is valid. Supports both the old format (9 digits) and the new format (11 digits). +Check if RENAVAM (Registro Nacional de Veículos Automotores) is valid. Supports both the old format (9 digits) and the new format (11 digits). Any spaces, dots and hyphens around/between the digits are ignored, but any other character, a letter in particular, makes the value invalid. A registration whose digits are all the same is rejected as well. ```javascript import { isValidRenavam } from '@brazilian-utils/brazilian-utils'; isValidRenavam('639884962'); // true (9 digits, old format) isValidRenavam('00639884962'); // true (11 digits, new format) +isValidRenavam('0063988.4962'); // true (dots and hyphens are ignored) isValidRenavam('12345678901'); // false (invalid checksum) +isValidRenavam('00000000000'); // false (repeated digits) +isValidRenavam('ab00639884962'); // false (letters are rejected) ``` ### isValidPis @@ -713,7 +728,7 @@ isValidPis('12056412547'); // false ### formatPis -Format PIS number. +Format PIS number. `options.pad` (part of `FormatPisOptions`) left-pads the value with zeros to the full 11 digits before masking. ```javascript import { formatPis } from '@brazilian-utils/brazilian-utils'; @@ -734,12 +749,13 @@ parsePis('123.45678.90-1'); // 12345678901 ### formatCep -Format CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). +Format CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (part of `FormatCepOptions`) left-pads the value with zeros to the full 8 digits before masking. ```javascript import { formatCep } from '@brazilian-utils/brazilian-utils'; formatCep('92500000'); // 92500-000 +formatCep('9250000', { pad: true }); // 09250-000 ``` ### parseCep @@ -754,7 +770,7 @@ parseCep('92500-000'); // 92500000 ### getAddressInfoByCep -Fetch address information for a given CEP using multiple providers. Defaults to `['viacep', 'brasilapi']`. The `'widenet'` provider is deprecated (its endpoint no longer responds) and excluded from the default list, but it can still be requested explicitly via `options.providers` (typed as `CepProvider[]`). The resolved address is typed as `AddressInfo`. +Fetch address information for a given CEP using multiple providers. Defaults to `['viacep', 'brasilapi']`. The `'widenet'` provider is deprecated (its endpoint no longer responds) and excluded from the default list, but it can still be requested explicitly via `options.providers` (typed as `CepProvider[]`). The resolved address is typed as `AddressInfo`. A transient network failure is retried twice per provider, with a 250 ms linear backoff (250 ms, then 500 ms), so a provider that keeps failing is tried up to 3 times and adds about 750 ms before the next provider is reached; an HTTP error status or a non-retryable failure is not retried. ```javascript import { getAddressInfoByCep } from '@brazilian-utils/brazilian-utils'; @@ -774,22 +790,25 @@ const address = await getAddressInfoByCep(1310100); ### isValidProcessoJuridico -Validate the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119). +Validate the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119). The CNJ mask separators (whitespace, `.` and `-`) are accepted between the `NNNNNNN-DD.AAAA.J.TR.OOOO` fields, but any other character, a letter in particular, makes the value invalid. ```javascript import { isValidProcessoJuridico } from '@brazilian-utils/brazilian-utils'; isValidProcessoJuridico('00020802520125150049'); // true +isValidProcessoJuridico('0002080-25.2012.5.15.0049'); // true (CNJ mask) +isValidProcessoJuridico('ab00020802520125150049'); // false (letters are rejected) ``` ### formatProcessoJuridico -Format the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119) (mask `NNNNNNN-DD.AAAA.J.TR.OOOO`). +Format the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119) (mask `NNNNNNN-DD.AAAA.J.TR.OOOO`). `options.pad` (part of `FormatProcessoJuridicoOptions`) left-pads the value with zeros to the full 20 digits before masking. ```javascript import { formatProcessoJuridico } from '@brazilian-utils/brazilian-utils'; formatProcessoJuridico('00020802520125150049'); // 0002080-25.2012.5.15.0049 +formatProcessoJuridico('20802520125150049', { pad: true }); // 0002080-25.2012.5.15.0049 ``` ### parseProcessoJuridico @@ -804,7 +823,7 @@ parseProcessoJuridico('0002080-25.2012.5.15.0049'); // 00020802520125150049 ### isValidIe -Check if inscrição estadual (state registration) is valid. The state code is case-insensitive. Notable per-state rules: GO accepts prefixes `10`, `11` and `15`; PA accepts `15` and `75`-`79`; MS accepts `28` and `50`; SP has a produtor rural pattern `P0MMMSSSSD000`; TO uses 11-digit type codes (`01`, `02`, `03`, `99`). +Check if inscrição estadual (state registration) is valid. The state code is case-insensitive. Notable per-state rules: GO accepts prefixes `10`, `11` and `15`; PA accepts `15` and `75`-`79`; MS accepts `28` and `50`; SP has a produtor rural pattern `P0MMMSSSSD000`; TO uses 11-digit type codes (`01`, `02`, `03`, `99`). TO also accepts a 9-digit form, applying the same modulus 11 rule to the first eight digits; the SINTEGRA page documents only the 11-digit one, so that shape is 2.3.0 behaviour kept for compatibility rather than a published rule. An all-zero registration is accepted wherever the published formula yields a check digit of 0 for it (AM, BA with 8 or 9 digits, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, SC, SE, SP and TO with 9 digits), unlike `isValidCpf` and `isValidCnpj`, which reject repeated digits. ```javascript import { isValidIe } from '@brazilian-utils/brazilian-utils'; @@ -825,7 +844,7 @@ Banks validated by their published check digit algorithm: | Santander | `033` | 4 digits | 8 digits | weights `9,7,3,1,0,0,9,7,1,3,1,9,7,3` over agency + `"00"` + account, tens discarded | | Banrisul | `041` | 4 digits | 9 digits | weights `3,2,4,7,6,5,4,3,2`; remainder 0 gives `0` and remainder 1 gives `6`; `account` is tipo (2 digits) + conta (7 digits) | | Caixa Econômica Federal | `104` | 4 digits | 11 digits | mod11 over agency + account; `account` is operação (3 digits) + conta (8 digits) | -| Bradesco | `237` | 4 digits | 7 digits | mod11 with weights 2..7; `digit` may be `"P"` (often rendered as `"0"`) | +| Bradesco | `237` | 4 digits | 7 digits | mod11 with weights 2..7; remainder 0 gives `0` and remainder 1 gives `"P"` | | Nubank | `260` | 4 digits | 5-13 digits | Verhoeff check digit over the account, leading zeros dropped | | Itaú Unibanco | `341` | 4 digits | 5 digits | mod10 over agency + account | | HSBC / Kirton Bank | `399` | 4 digits | 6 digits | weights `8,9,2,3,4,5,6,7,8,9` over agency + account; remainder 10 gives `0` | @@ -835,17 +854,15 @@ Banks validated by structure only, because they publish no check digit rule. The | Bank | Code | | Bank | Code | | --- | --- | --- | --- | --- | -| Inter | `077` | | PicPay | `380` | -| Ailos | `085` | | Cora | `403` | -| XP | `102` | | Pan | `623` | -| Unicred | `136` | | BV | `655` | -| Stone | `197` | | Daycoval | `707` | -| BTG Pactual | `208` | | Modal | `746` | -| Original | `212` | | Sicredi | `748` | -| PagBank | `290` | | Sicoob | `756` | -| BMG | `318` | | | | -| Mercado Pago | `323` | | | | -| C6 | `336` | | | | +| Inter | `077` | | Mercado Pago | `323` | +| Ailos | `085` | | C6 | `336` | +| XP | `102` | | PicPay | `380` | +| Unicred | `136` | | Cora | `403` | +| Stone | `197` | | Pan | `623` | +| BTG Pactual | `208` | | BV | `655` | +| Original | `212` | | Daycoval | `707` | +| PagBank | `290` | | Sicredi | `748` | +| BMG | `318` | | Sicoob | `756` | When `digit` has 2 characters, the generic fallback chains mod10 followed by mod11 over the account, the same way CPF/CNPJ check digits are chained. @@ -960,7 +977,7 @@ getBankByIspb('99999999'); // null ### isValidIban -Check if a Brazilian IBAN (International Bank Account Number) is valid, per Bacen's [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf) (Circular BCB nº 3.625/2013): `BR` + 2 ISO 7064 MOD 97-10 check digits + 8 digit ISPB + 5 digit branch + 10 digit account + 1 letter account type (any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 alphanumeric owner indicator, 29 characters total. Only Brazilian IBANs (country code `BR`) are recognized; any other country returns `false`, since this package does not carry the field layout of the other 90+ ISO 13616 countries. Accepts the usual grouping spaces and is case-insensitive. The value has to be written in the ISO 13616 print format: letters and digits in groups separated by a single space, with optional surrounding whitespace. Any other character makes the value something other than an IBAN, so it is rejected instead of being stripped. +Check if a Brazilian IBAN (International Bank Account Number) is valid, per Bacen's [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf) (Circular BCB nº 3.625/2013): `BR` + 2 ISO 7064 MOD 97-10 check digits + 8 digit ISPB + 5 digit branch + 10 digit account + 1 letter account type (any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 owner indicator (`1` for the first or only holder up to `9` for the ninth, then `A` to `Z` from the tenth, so `0` is rejected), 29 characters total. Only Brazilian IBANs (country code `BR`) are recognized; any other country returns `false`, since this package does not carry the field layout of the other 90+ ISO 13616 countries. Is case-insensitive and accepts both forms an IBAN is written in: compact (`'BR1500000000000010932840814P2'`) or in the ISO 13616 print format, letters and digits in groups separated by a single space, with optional surrounding whitespace either way. Only a character outside letters and digits, or a separator other than a single space, makes the value something other than an IBAN, so it is rejected instead of being stripped. ```javascript import { isValidIban } from '@brazilian-utils/brazilian-utils'; @@ -974,7 +991,7 @@ isValidIban('DE89370400440532013000'); // false (non Brazilian IBAN) ### formatIban -Format a Brazilian IBAN by grouping it in blocks of 4 characters, the ISO 13616 "print" presentation used on statements and bank forms. Does not validate the check digits or the field layout; formats whatever is given, up to the 29 character length of a Brazilian IBAN, as far as it goes, so the function can also be used as an input mask. Use `isValidIban` to check validity. The value still has to be written in the ISO 13616 print format (letters and digits in groups separated by a single space, with optional surrounding whitespace); any other character returns an empty string instead of being quietly dropped. +Format an IBAN in the ISO 13616 print grouping, blocks of 4 characters, the presentation used on statements and bank forms. Does not validate the check digits or the field layout; formats whatever is given, up to the 29 character length of a Brazilian IBAN, as far as it goes, so the function can also be used as an input mask, and an IBAN of another country is grouped the same way up to that length. Use `isValidIban` to check validity. The value may be compact (`'BR1500000000000010932840814P2'`), already in the ISO 13616 print format (letters and digits in groups separated by a single space) or a partial value still being typed (`'BR15'`), in every case with optional surrounding whitespace; only a character outside letters and digits, or a separator other than a single space, returns an empty string instead of being quietly dropped. ```javascript import { formatIban } from '@brazilian-utils/brazilian-utils'; @@ -987,7 +1004,7 @@ formatIban('BR1500000000000010932840814P-2'); // '' (hyphens are not part of an ### parseIban -Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator). Accepts the same input forms as `isValidIban` (grouping spaces, lowercase) and returns `null` whenever `isValidIban` would return `false`, including a value carrying any character other than letters, digits and the grouping spaces of the print format. The result is typed as `Iban`, whose `accountType` is a `string`. +Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups separated by a single space), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying any character other than letters, digits and those single grouping spaces. The result is typed as `Iban`, whose `accountType` is a `string`. ```javascript import { parseIban } from '@brazilian-utils/brazilian-utils'; @@ -1009,7 +1026,7 @@ parseIban('BR1500000000000010932840814P-2'); // null (hyphens are not part of an ### isValidCreditCard -Check if a payment card number is valid using the Luhn algorithm ([ISO/IEC 7812-1](https://www.iso.org/standard/70484.html)). Accepts the usual mask characters (spaces, hyphens) between digits. Performs no brand detection (Visa, Mastercard, Amex...), issuer range lookup or expiration/CVV checks, only the digit count (12 to 19) and the Luhn check digit. A `number` is only accepted when it is a non-negative safe integer: anything above `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 digits) has already been rounded to a different number before the function sees it, so pass a longer PAN as a string. +Check if a payment card number is valid using the Luhn algorithm ([ISO/IEC 7812-1](https://www.iso.org/standard/70484.html)). Accepts the usual mask characters (spaces, hyphens) between digits and whitespace around the value; any other character makes the value invalid. Performs no brand detection (Visa, Mastercard, Amex...), issuer range lookup or expiration/CVV checks, only the digit count (12 to 19) and the Luhn check digit. A `number` is only accepted when it is a non-negative safe integer: anything above `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 digits) has already been rounded to a different number before the function sees it, so pass a longer PAN as a string. ```javascript import { isValidCreditCard } from '@brazilian-utils/brazilian-utils'; @@ -1019,29 +1036,42 @@ isValidCreditCard('5555555555554444'); // true (Mastercard test number) isValidCreditCard('378282246310005'); // true (American Express test number) isValidCreditCard('4111 1111 1111 1111'); // true (spaced mask) isValidCreditCard('4111111111111112'); // false (bad check digit) +isValidCreditCard('4111a1111b1111c1111'); // false (letters between the digits) isValidCreditCard(4111111111111111111); // false (above 2^53 - 1, pass it as a string) ``` ### capitalize -Transforms the first letter into a capital one of each word ignoring prepositions. Words are separated by whitespace, by `-` and by `/`, so `'MOGI-GUAÇU'` becomes `'Mogi-Guaçu'` and `'SANTANA/RS'` becomes `'Santana/Rs'`. Every run of whitespace (tabs, newlines, repeated spaces) collapses into a single space, and the leading and trailing whitespace is dropped. `options.upperCaseWords` defaults to `[]`, so no acronym is upper-cased unless you list it, and the comparison against both `upperCaseWords` and `lowerCaseWords` is case-insensitive (pt-BR locale). Options are typed as `CapitalizeOptions`. +Transforms the first letter into a capital one of each word, the way a Brazilian name, company name or address is written, with no options needed. Words are separated by whitespace, by `-` and by `/`, so `'MOGI-GUAÇU'` becomes `'Mogi-Guaçu'`. Every run of whitespace (tabs, newlines, repeated spaces) collapses into a single space, and the leading and trailing whitespace is dropped. + +`options.lowerCaseWords` defaults to the Portuguese prepositions, articles and conjunctions that stay in lower case inside a proper name (`de`, `da`, `do`, `e`, ...), except when one of them is the first word. `options.upperCaseWords` defaults to the company designations and document abbreviations written in upper case in Brazilian usage (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) plus the roman numerals that appear in names and addresses (`II` through `XXIII`, except `VI`, which collides with the pt-BR verb form "vi"). `SA` without punctuation is deliberately absent, since it is indistinguishable from the surname "Sá" typed without its accent, while `ME` does match the pronoun "me" (`'diga-me'` becomes `'Diga-ME'`), so pass your own `upperCaseWords` when the input is free text rather than a name. `S/A` and `S/S` are matched across the slash even though a slash separates words. A two letter word that follows a `/` is upper-cased when it is the code of a Brazilian state (`'porto alegre/rs'` becomes `'Porto Alegre/RS'`); that rule is structural and stays on even when `upperCaseWords` is given, while a state code that does not follow a `/` is left alone. + +Either list given in `options` replaces its default entirely, and the comparison against both is case-insensitive (pt-BR locale). Options are typed as `CapitalizeOptions`. ```javascript import { capitalize } from '@brazilian-utils/brazilian-utils'; -capitalize('josé e maria'); // José e Maria +capitalize('jose da silva'); // Jose da Silva +capitalize('JOSÉ DA SILVA'); // José da Silva +capitalize('empresa ltda'); // Empresa LTDA +capitalize('banco do brasil s.a.'); // Banco do Brasil S.A. +capitalize('casa de carnes s/a'); // Casa de Carnes S/A ("S/A" is matched across the slash) +capitalize('mogi-guaçu'); // Mogi-Guaçu ("-" starts a new word) +capitalize('santana/rs'); // Santana/RS ("RS" is a state code right after a "/") +capitalize('porto alegre/rs'); // Porto Alegre/RS +capitalize('santana rs'); // Santana Rs (no "/", so "rs" is just a word) +capitalize('rua xv de novembro'); // Rua XV de Novembro (roman numeral, "de" stays lower case) +capitalize('joão paulo ii'); // João Paulo II +capitalize('de'); // De (a preposition keeps its capital when it is the first word) +capitalize('empresa ltda', { upperCaseWords: [] }); // Empresa Ltda (the list given replaces the default one) capitalize('josé Ama MARIA', { lowerCaseWords: ['ama'] }); // José ama Maria -capitalize('doc inválido', { upperCaseWords: ['DOC'] }); // DOC Inválido -capitalize('MOGI-GUAÇU'); // Mogi-Guaçu ("-" starts a new word) -capitalize('SANTANA/RS', { upperCaseWords: ['RS'] }); // Santana/RS ("/" starts a new word, so "RS" matches) -capitalize('empresa ltda'); // Empresa Ltda (no default acronyms) -capitalize('empresa ltda', { upperCaseWords: ['LTDA'] }); // Empresa LTDA (case-insensitive match) +capitalize('doc inválido', { upperCaseWords: ['DOC'] }); // DOC Inválido (case-insensitive match) capitalize(' josé maria '); // José Maria (every run of whitespace, tabs and newlines included, collapses into one space) ``` ### formatCurrency -Formats an integer or float to a string in the BRL pattern. A `number` is formatted as-is (sign and decimals preserved). A `string` input is read by the same rule as `parseCurrency`, except that a value written without any separator stays in whole units: the last `,` or `.` followed by 1 to 2 digits is the decimal separator, every other `,` or `.` is a thousands separator, and a `-` written before the first digit is preserved. So `'1.234,56'` formats as `1.234,56`, `'-10.5'` as `-10,50` and `'1234'` as `1.234,00`. `precision` is clamped to `0..20` (the range `Intl.NumberFormat` accepts) and defaults to 2. A value that is not a finite number (`NaN`, `Infinity`, `-Infinity`) formats as an empty string. Options are typed as `FormatCurrencyOptions`. +Formats an integer or float to a string in the BRL pattern. A `number` is formatted as-is (sign and decimals preserved). A `string` input is read by the same rule as `parseCurrency`, except that a value written without any separator stays in whole units: the last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator, every other `,` or `.` is a thousands separator, and a `-` written before the first digit is preserved. So `'1.234,56'` formats as `1.234,56`, `'-10.5'` as `-10,50` and `'1234'` as `1.234,00`. `precision` is clamped to `0..20` (the range `Intl.NumberFormat` accepts), defaults to 2, and falls back to 2 when it is not a finite number. A value that is not a finite number (`NaN`, `Infinity`, `-Infinity`) formats as an empty string, and so does a value that cannot be coerced to a number (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. Options are typed as `FormatCurrencyOptions`. ```javascript import { formatCurrency } from '@brazilian-utils/brazilian-utils'; @@ -1059,7 +1089,7 @@ formatCurrency(Number.NaN); // "" (non finite numbers format as an empty string) ### parseCurrency -Transforms a string to an integer or float format. The last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator; every other `,` or `.` is a thousands separator. So `'R$ 1.234,56'` parses to `1234.56`, `'R$ 1.234'` to `1234`, `'1,5'` to `1.5` and `'12.34'` to `12.34`. A value written without any separator keeps the cents convention and is divided by `10 ** precision`, so `'1234'` parses to `12.34`. A `-` written before the first digit is preserved, so `'-R$ 1,00'` parses to `-1`. `precision` (default 2, clamped to `0..20`) controls how many digits are treated as minor units. Options are typed as `ParseCurrencyOptions`. +Transforms a string to an integer or float format. The last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator; every other `,` or `.` is a thousands separator. So `'R$ 1.234,56'` parses to `1234.56`, `'R$ 1.234'` to `1234`, `'1,5'` to `1.5` and `'12.34'` to `12.34`. A value written without any separator keeps the cents convention and is divided by `10 ** precision`, so `'1234'` parses to `12.34`. A `-` written before the first digit is preserved, so `'-R$ 1,00'` parses to `-1`. `precision` (default 2, clamped to `0..20`, and falling back to 2 when it is not a finite number) controls how many digits are treated as minor units. Options are typed as `ParseCurrencyOptions`. ```javascript import { parseCurrency } from '@brazilian-utils/brazilian-utils'; @@ -1077,7 +1107,7 @@ parseCurrency(''); // 0 ### convertNumberToWords -Formats an integer as its Brazilian Portuguese cardinal number words ("por extenso"), e.g. `1235` becomes `"mil, duzentos e trinta e cinco"`. Only integers from `-999999999999999` to `999999999999999` (999 trillion in absolute value) are supported; anything outside that range, `NaN` or a non-finite value returns `""`. A non-integer `value` is truncated toward zero before conversion. `options.gender` (part of `ConvertNumberToWordsOptions`) agrees "um/dois" and the hundreds group ("duzentos/duzentas", etc.) with the noun the number qualifies, defaulting to `"masculine"`. `options.case` sets the letter case of the result: `"lower"` (default, unchanged), `"sentence"` (capitalizes only the first letter) or `"upper"` (uppercases everything with the "pt-BR" locale, keeping accents, e.g. "três" -> "TRÊS"). An invalid `gender`/`case` value is ignored and the default is used. +Formats an integer as its Brazilian Portuguese cardinal number words ("por extenso"), e.g. `1235` becomes `"mil, duzentos e trinta e cinco"`. Only integers from `-999999999999999` to `999999999999999` (999 trillion in absolute value) are supported; anything outside that range, `NaN` or a non-finite value returns `""`. A non-integer `value` is truncated toward zero before conversion. `options.gender` (part of `ConvertNumberToWordsOptions`) agrees "um/dois" and the hundreds group ("duzentos/duzentas", etc.) with the noun the number qualifies, defaulting to `"masculine"`. An invalid `gender` value is ignored and the default is used. The result is always lowercase; apply any other casing to it yourself. ```javascript import { convertNumberToWords } from '@brazilian-utils/brazilian-utils'; @@ -1087,13 +1117,13 @@ convertNumberToWords(1001); // "mil e um" convertNumberToWords(2000000); // "dois milhões" convertNumberToWords(-42); // "menos quarenta e dois" convertNumberToWords(2, { gender: 'feminine' }); // "duas" -convertNumberToWords(3, { case: 'upper' }); // "TRÊS" +convertNumberToWords(12.9); // "doze" (truncated toward zero) convertNumberToWords(NaN); // "" ``` ### convertCurrencyToWords -Formats a monetary amount in Brazilian Reais as its "por extenso" textual representation, the style used to write out the amount by hand on cheques and contracts, e.g. `1523.45` becomes `"mil, quinhentos e vinte e três reais e quarenta e cinco centavos"`. `value` is truncated (not rounded) to 2 decimal places. The singular noun is used for exactly 1 ("um real", "um centavo") and "de" is inserted before "reais" when the amount is a round million, billion or trillion of reais. An amount that truncates to nothing becomes `"zero reais"` with no "menos" prefix, any other negative amount is prefixed with "menos", and invalid input returns `""`. Above `Number.MAX_SAFE_INTEGER / 100` reais (about 90 trillion) a double cannot carry cents, so the amount is read as a whole number of reais. `options.case` (part of `ConvertCurrencyToWordsOptions`) sets the letter case of the result: `"lower"` (default), `"sentence"` (capitalizes only the first letter) or `"upper"` (uppercases everything, keeping accents). An invalid `case` value is ignored and `"lower"` is used. +Formats a monetary amount in Brazilian Reais as its "por extenso" textual representation, the style used to write out the amount by hand on cheques and contracts, e.g. `1523.45` becomes `"mil, quinhentos e vinte e três reais e quarenta e cinco centavos"`. `value` is truncated (not rounded) to 2 decimal places. The singular noun is used for exactly 1 ("um real", "um centavo") and "de" is inserted before "reais" when the amount is a round million, billion or trillion of reais. An amount that truncates to nothing becomes `"zero reais"` with no "menos" prefix, any other negative amount is prefixed with "menos", and invalid input returns `""`. Above `Number.MAX_SAFE_INTEGER / 100` reais (about 90 trillion) a double cannot carry cents, so the amount is read as a whole number of reais. It takes no options: the result is always lowercase; apply any other casing to it yourself. ```javascript import { convertCurrencyToWords } from '@brazilian-utils/brazilian-utils'; @@ -1105,7 +1135,6 @@ convertCurrencyToWords(1000000); // "um milhão de reais" convertCurrencyToWords(0); // "zero reais" convertCurrencyToWords(-5.5); // "menos cinco reais e cinquenta centavos" convertCurrencyToWords(-0.001); // "zero reais" (truncates to nothing) -convertCurrencyToWords(1000, { case: 'upper' }); // "MIL REAIS" ``` ### getStates @@ -1207,7 +1236,7 @@ getTimezoneByState('ZZ'); // null ### getCities -Get Brazilian cities. Returns all cities if no state is provided, or cities from a specific state. Each call returns a fresh array, so mutating the result never affects subsequent calls. An unknown state code (or a non-`StateCode` value) returns an empty array instead of throwing. +Get Brazilian cities. Returns all cities if no state is provided, or cities from a specific state. Each call returns a fresh array, so mutating the result never affects subsequent calls. An unknown state code (or a non-`StateCode` value) returns an empty array instead of throwing, except for a falsy one: `getCities(null)` and `getCities('')` are read as "no state given" and return every city, where the stricter `getMunicipalities` returns `[]` for them. ```javascript import { getCities } from '@brazilian-utils/brazilian-utils'; @@ -1245,11 +1274,22 @@ getCities('SP'); // ] ``` -`getCities` embeds all 5571 IBGE municipality names (~153 KB minified, ~49 KB gzipped) and is one of the few heavy exceptions in this otherwise tree-shakeable package. See [Bundle size](getting-started.md#bundle-size) for how to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities` instead of the root import. +`getCities` embeds all 5571 IBGE municipality names (~153.6 KB minified, ~49.4 KB gzipped) and is one of the few heavy exceptions in this otherwise tree-shakeable package. See [Bundle size](getting-started.md#bundle-size) for how to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities` instead of the root import. ### getHolidays -Get Brazilian holidays for a given year. Returns national holidays and optionally state-specific holidays. Each holiday (typed as `Holiday`) has a `type` field (`HolidayType`: `"national"`, `"state"`, `"optional"` or `"religious"`). "Dia da Consciência Negra" (Nov 20) is a national holiday from 2024 onward (Lei nº 14.759/2023). Before that, MT and RJ still carry their own state-level entry named `"Consciência Negra"` on the same date. Results are memoized per `year`/`stateCode`, but each call still returns a fresh copy. An unknown/invalid `stateCode` is ignored, returning national holidays only. +Get Brazilian holidays for a given year. Returns national holidays and optionally state-specific holidays. Each holiday (typed as `Holiday`) has a `type` field (`HolidayType`: `"national"`, `"state"`, `"optional"` or `"religious"`). "Dia da Consciência Negra" (Nov 20) is a national holiday from 2024 onward (Lei nº 14.759/2023). Before that, MT and RJ still carry their own state-level entry named `"Consciência Negra"` on the same date. Results are memoized per `year`/`stateCode`, but each call still returns a fresh copy. An unknown/invalid `stateCode` is ignored, returning national holidays only; the lookup reads own properties only, so `"__proto__"`, `"constructor"` and the like are unknown state codes rather than a crash. + +Only one state holiday per UF is a feriado civil under [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm), art. 1º, II, which authorises "a data magna do Estado fixada em lei estadual" in the singular; the other entries rest on ordinary state laws and are reported because they are observed in practice. Notable per-state rules: + +- **SC** — [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) moves both state holidays, "Dia do Estado de Santa Catarina" (Aug 11) and "Dia de Santa Catarina de Alexandria" (Nov 25), to the following Sunday whenever they fall Monday to Friday, so Monday Aug 11 2025 is a business day in SC and the holiday lands on Sunday Aug 17. +- **DF** — [Lei distrital nº 72/1989](https://www.sinj.df.gov.br/sinj/Norma/18459/Lei_72_27_12_1989.html), art. 1º parágrafo único, declares Corpus Christi a feriado. With `stateCode: 'DF'` the single Corpus Christi entry comes back typed `"state"` instead of `"optional"`; it is replaced, not duplicated. +- **GO** — [Lei GO nº 20.756/2020](https://legisla.casacivil.go.gov.br/pesquisa_legislacao/100979/lei-20756), art. 269, II, lists three feriados estaduais: Jul 26 (Fundação da Cidade de Goiás), Oct 24 (Lançamento da Pedra Fundamental de Goiânia) and Oct 28 (Dia do Servidor Público). +- **AL** — Sep 16 is a feriado estadual from 2024 ([Lei AL nº 9.358/2024](https://sapl.al.al.leg.br/norma/3117)) and only a ponto facultativo (`"optional"`) before that. +- **PB** — Jul 26 ("Morte de João Pessoa") is emitted up to 2015 only: [Lei PB nº 10.601/2015](https://sapl.al.pb.leg.br/norma/11988), art. 2º, revoked its basis. +- **TO** — Mar 18 ("Autonomia do Estado do Tocantins") is emitted up to 2008 only: [Lei TO nº 2.013/2009](https://www.al.to.leg.br/arquivo/15724) rewrote the clause that declared the feriado into a commemorative provision. + +The statutory date is what is returned. SC's shift above is the only observance shift modelled; Acre's Tuesday-to-Thursday shift and the Goiás decrees that may move Jul 26 and Oct 28 are not. ```javascript import { getHolidays } from '@brazilian-utils/brazilian-utils'; @@ -1296,7 +1336,7 @@ formatPassport('AB-123.456'); // 'AB123456' ### generatePassport -Generate a random valid Brazilian passport number. +Generate a random valid Brazilian passport number. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generatePassport } from '@brazilian-utils/brazilian-utils'; @@ -1317,7 +1357,7 @@ parsePassport(' AB 123 456 '); // 'AB123456' ### generateCep -Generate a random CEP. +Generate a random CEP. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generateCep } from '@brazilian-utils/brazilian-utils'; @@ -1327,7 +1367,7 @@ generateCep(); // '92500000' ### formatCnh -Format CNH. +Format CNH. `options.pad` (part of `FormatCnhOptions`) left-pads the value with zeros to the full 11 digits before masking. ```javascript import { formatCnh } from '@brazilian-utils/brazilian-utils'; @@ -1338,17 +1378,19 @@ formatCnh('2650306461', { pad: true }); // 026503064-61 ### isValidCnh -Check if CNH is valid. +Check if CNH is valid. Spaces, dots and hyphens around/between the digits are ignored, but any other character, a letter in particular, makes the value invalid. ```javascript import { isValidCnh } from '@brazilian-utils/brazilian-utils'; isValidCnh('00000000119'); // true +isValidCnh('000000001-19'); // true (hyphen before the check digits) +isValidCnh('ab00000000119'); // false (letters are rejected) ``` ### generateCnh -Generate a valid random CNH. +Generate a valid random CNH. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generateCnh } from '@brazilian-utils/brazilian-utils'; @@ -1381,9 +1423,9 @@ const ceps = await getCepInfoByAddress({ // [ // { -// cep: '01310100', +// cep: '01310-100', // logradouro: 'Avenida Paulista', -// complemento: 'lado par', +// complemento: 'de 612 a 1510 - lado par', // bairro: 'Bela Vista', // localidade: 'São Paulo', // uf: 'SP' @@ -1426,7 +1468,7 @@ isValidLegalNature('9999'); // false ### generateLegalNature -Generate a random valid legal nature code. +Generate a random valid legal nature code. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generateLegalNature } from '@brazilian-utils/brazilian-utils'; @@ -1464,6 +1506,8 @@ Look a legal nature code up in the official IBGE/CONCLA table. import { getLegalNature } from '@brazilian-utils/brazilian-utils'; getLegalNature('2062'); // { code: '2062', description: 'Sociedade Empresária Limitada' } +getLegalNature('206-2'); // { code: '2062', description: 'Sociedade Empresária Limitada' } +getLegalNature(206.2); // { code: '2062', description: 'Sociedade Empresária Limitada' } getLegalNature('0000'); // null ``` @@ -1493,7 +1537,7 @@ formatLicensePlate('abc1d23'); // 'ABC1D23' ### generateLicensePlate -Generate a random license plate in the chosen format. +Generate a random license plate in the chosen format. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generateLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -1575,7 +1619,7 @@ await getMunicipality({ code: '123' }); ### getMunicipalities -Get Brazilian municipalities published by the IBGE. Returns all municipalities if no state is provided, or municipalities from a specific state. Each municipality is returned as `{ code, name, stateCode }`, where `code` is the 7-digit IBGE municipality code. Results are sorted by name with `localeCompare` in the "pt-BR" locale. Each call returns a fresh array of fresh objects, so mutating the result never affects subsequent calls. An unknown state code returns an empty array instead of throwing. +Get Brazilian municipalities published by the IBGE. Returns all municipalities if no state is provided, or municipalities from a specific state. Each municipality is returned as `{ code, name, stateCode }`, where `code` is the 7-digit IBGE municipality code. Results are sorted by name with `localeCompare` in the "pt-BR" locale. Each call returns a fresh array of fresh objects, so mutating the result never affects subsequent calls. An unknown state code returns an empty array instead of throwing. Only an omitted (or `undefined`) `stateCode` asks for the full list: `getMunicipalities(null)` and `getMunicipalities('')` return `[]`, where the looser `getCities(null)` and `getCities('')` return every city. ```javascript import { getMunicipalities } from '@brazilian-utils/brazilian-utils'; @@ -1638,7 +1682,7 @@ isHoliday(); // false ### isBusinessDay -Check if a date is a Brazilian business day (dia útil). Returns `false` for Saturdays, Sundays, and Brazilian holidays returned by `getHolidays` for `value`'s local calendar day (year/month/day as read locally), the same convention used by `isHoliday`. `options.includeOptional` (part of `IsBusinessDayOptions`) defaults to `true`, so optional-type holidays (`Holiday.type === "optional"`, i.e. Carnaval and Corpus Christi) also count as non-business days; pass `false` to only treat statutory holidays this way. `options.stateCode` also considers that state's holidays; an unknown/invalid `stateCode` is ignored, falling back to national holidays only. A `value` that is not a valid `Date` returns `false`. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `false`. +Check if a date is a Brazilian business day (dia útil). Returns `false` for Saturdays, Sundays, and Brazilian holidays returned by `getHolidays` for `value`'s local calendar day (year/month/day as read locally), the same convention used by `isHoliday`. `options.includeOptional` (part of `BusinessDayOptions`, the option type every business day utility shares) defaults to `true`, so optional-type holidays (`Holiday.type === "optional"`, i.e. Carnaval and Corpus Christi) also count as non-business days; pass `false` to only treat statutory holidays this way. `options.stateCode` also considers that state's holidays; an unknown/invalid `stateCode` is ignored, falling back to national holidays only. A `value` that is not a valid `Date` returns `false`. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `false`. ```javascript import { isBusinessDay } from '@brazilian-utils/brazilian-utils'; @@ -1655,38 +1699,55 @@ isBusinessDay(new Date('not a date')); // false ### addBusinessDays -Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and Brazilian holidays exactly as `isBusinessDay` defines them (same `stateCode`/`includeOptional` options). Returns a new `Date`; the input `date` (part of `AddBusinessDaysParams`) is never mutated, and its time-of-day is preserved in the result. `days: 0` returns a new `Date` equal to `date`, unchanged, even when `date` itself falls on a weekend or holiday, this mirrors the verified behavior of [date-fns' `addBusinessDays(date, 0)`](https://date-fns.org/docs/addBusinessDays), which also does not roll the input to the next business day. A negative `days` walks backwards, one business day at a time, also like date-fns. Returns `null` on bad input: a `date` that is not a valid `Date`, a `days` that is not a finite integer, or a `stateCode` that is not a string. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it (or, for `addBusinessDays`, a walk that leaves it) returns `null`. +Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and Brazilian holidays exactly as `isBusinessDay` defines them (same `BusinessDayOptions`). The signature is date-fns': `addBusinessDays(date, amount, options?)`. Returns a new `Date`; the input `date` is never mutated, and its time-of-day is preserved in the result. An `amount` of `0` returns a new `Date` equal to `date`, unchanged, even when `date` itself falls on a weekend or holiday, this mirrors the verified behavior of [date-fns' `addBusinessDays(date, 0)`](https://date-fns.org/docs/addBusinessDays), which also does not roll the input to the next business day. A negative `amount` walks backwards, one business day at a time, also like date-fns. Returns `null` on bad input: a `date` that is not a valid `Date`, an `amount` that is not a finite integer, or a `stateCode` that is not a string; an `options` that is not an object at all is ignored, exactly as `isBusinessDay` ignores it. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it, or a walk that leaves it, returns `null`. ```javascript import { addBusinessDays } from '@brazilian-utils/brazilian-utils'; -addBusinessDays({ date: new Date(2024, 0, 2, 12), days: 1 }); // Date, 2024-01-03 12:00 (next day is already a business day) -addBusinessDays({ date: new Date(2024, 11, 31, 12), days: 1 }); // Date, 2025-01-02 12:00 (2025-01-01 is Ano novo, skipped) -addBusinessDays({ date: new Date(2024, 0, 5, 12), days: -1 }); // Date, 2024-01-04 12:00 (walks backwards) -addBusinessDays({ date: new Date(2024, 0, 6, 12), days: 0 }); // Date, 2024-01-06 12:00 (unchanged, even though Saturday is not a business day) -addBusinessDays({ date: new Date(2024, 6, 8, 12), days: 1, stateCode: 'SP' }); // Date, 2024-07-10 12:00 (2024-07-09 is Revolução Constitucionalista in SP, skipped) -addBusinessDays({ date: new Date('not a date'), days: 1 }); // null -addBusinessDays({ date: new Date(2024, 0, 2), days: 1.5 }); // null (not an integer) +addBusinessDays(new Date(2024, 0, 2, 12), 1); // Date, 2024-01-03 12:00 (next day is already a business day) +addBusinessDays(new Date(2024, 11, 31, 12), 1); // Date, 2025-01-02 12:00 (2025-01-01 is Ano novo, skipped) +addBusinessDays(new Date(2024, 0, 5, 12), -1); // Date, 2024-01-04 12:00 (walks backwards) +addBusinessDays(new Date(2024, 0, 6, 12), 0); // Date, 2024-01-06 12:00 (unchanged, even though Saturday is not a business day) +addBusinessDays(new Date(2024, 6, 8, 12), 1, { stateCode: 'SP' }); // Date, 2024-07-10 12:00 (2024-07-09 is Revolução Constitucionalista in SP, skipped) +addBusinessDays(new Date('not a date'), 1); // null +addBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) +``` + +### subBusinessDays + +Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too. A negative `amount` walks forwards. + +```javascript +import { subBusinessDays } from '@brazilian-utils/brazilian-utils'; + +subBusinessDays(new Date(2024, 0, 5, 12), 1); // Date, 2024-01-04 12:00 (previous day is already a business day) +subBusinessDays(new Date(2024, 0, 8, 12), 1); // Date, 2024-01-05 12:00 (walks back over the weekend) +subBusinessDays(new Date(2025, 0, 2, 12), 1); // Date, 2024-12-31 12:00 (2025-01-01 is Ano novo, skipped) +subBusinessDays(new Date(2024, 0, 5, 12), -1); // Date, 2024-01-08 12:00 (walks forwards) +subBusinessDays(new Date(2024, 0, 6, 12), 0); // Date, 2024-01-06 12:00 (unchanged, even though Saturday is not a business day) +subBusinessDays(new Date(2024, 6, 10, 12), 1, { stateCode: 'SP' }); // Date, 2024-07-08 12:00 (2024-07-09 is Revolução Constitucionalista in SP, skipped) +subBusinessDays(new Date('not a date'), 1); // null +subBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) ``` ### differenceInBusinessDays -Count the number of Brazilian business days (dias úteis) between two dates, mirroring the semantics of [date-fns' `differenceInBusinessDays`](https://date-fns.org/docs/differenceInBusinessDays) (verified against its source): `params.from` is counted when it is itself a business day, `params.to` is never counted, and every business day strictly in between is counted once. Only the calendar day of each `Date` matters, the time of day is ignored. Business days are determined exactly like `isBusinessDay` (same `stateCode`/`includeOptional` options). `from`/`to` on the same calendar day return `0`; a `to` before `from` returns a negative number. Returns `null` on bad input: a `from`/`to` that is not a valid `Date`, or a `stateCode` that is not a string. Parameters are typed as `DifferenceInBusinessDaysParams`. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it (or, for `addBusinessDays`, a walk that leaves it) returns `null`. +Count the number of Brazilian business days (dias úteis) between two dates, mirroring the semantics of [date-fns' `differenceInBusinessDays`](https://date-fns.org/docs/differenceInBusinessDays) (verified against its source), argument order included: `differenceInBusinessDays(laterDate, earlierDate, options?)`. The walk starts at `earlierDate` and stops just before `laterDate`, so `earlierDate` is counted when it is itself a business day, `laterDate` is never counted, and every business day strictly in between is counted once. Only the calendar day of each `Date` matters, the time of day is ignored. Business days are determined exactly like `isBusinessDay` (same `BusinessDayOptions`). The result is positive when `laterDate` is after `earlierDate` and negative when it is before it; two dates on the same calendar day return `0`. Returns `null` on bad input: a date that is not a valid `Date`, or a `stateCode` that is not a string; an `options` that is not an object at all is ignored. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `null`. ```javascript import { differenceInBusinessDays } from '@brazilian-utils/brazilian-utils'; -differenceInBusinessDays({ from: new Date(2024, 0, 1), to: new Date(2024, 0, 2) }); // 0 (Jan 1 is Ano novo) -differenceInBusinessDays({ from: new Date(2024, 0, 2), to: new Date(2024, 0, 3) }); // 1 (Jan 2 counted, a Tuesday) -differenceInBusinessDays({ from: new Date(2024, 0, 3), to: new Date(2024, 0, 2) }); // -1 (to before from) -differenceInBusinessDays({ from: new Date(2024, 0, 2), to: new Date(2024, 0, 2) }); // 0 (same day) -differenceInBusinessDays({ from: new Date(2024, 6, 8), to: new Date(2024, 6, 10), stateCode: 'SP' }); // 1 (2024-07-09 is a state holiday in SP) -differenceInBusinessDays({ from: new Date('not a date'), to: new Date() }); // null +differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 1)); // 0 (Jan 1 is Ano novo, not counted) +differenceInBusinessDays(new Date(2024, 0, 3), new Date(2024, 0, 2)); // 1 (Jan 2 counted, a Tuesday; Jan 3 is not) +differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 3)); // -1 (the later date comes first, so the count is negative) +differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 2)); // 0 (same day) +differenceInBusinessDays(new Date(2024, 6, 10), new Date(2024, 6, 8), { stateCode: 'SP' }); // 1 (2024-07-09 is a state holiday in SP) +differenceInBusinessDays(new Date(), new Date('not a date')); // null ``` ### convertDateToWords -Formats a date as its Brazilian Portuguese "por extenso" textual representation, e.g. `"01/01/2024"` becomes `"primeiro de janeiro de dois mil e vinte e quatro"`. Accepts a `Date` (read by its local calendar date, the same convention used by `isHoliday`) or a string in `"dd/mm/yyyy"` or ISO `"yyyy-mm-dd"` format. With the default `options.style` of `"full"`, day 1 is written as "primeiro" and every other day uses the cardinal number; with `"month"`, only the month name is spelled out and the day/year are left as digits (day 1 as `"1º"`, e.g. `"2 de março de 2024"`, `"1º de janeiro de 2024"`). Month names are lowercase. In `"full"` style the year is written out as a cardinal number without the thousands comma that `convertNumberToWords`/`convertCurrencyToWords` use (`1999` reads as `"mil novecentos e noventa e nove"`, not `"mil, novecentos e noventa e nove"`), matching how a date is read aloud. `options.weekday` (default `false`) prefixes the pt-BR weekday name in lowercase followed by a comma (`"sábado, dois de março de dois mil e vinte e quatro"`), computed from the resolved calendar date. `options.case` sets the letter case of the whole result: `"lower"` (default), `"sentence"` (capitalizes only the first letter) or `"upper"` (uppercases everything, keeping accents). Invalid `case`/`style` values are ignored and the default is used. February 29th is accepted on the leap years of the proleptic Gregorian calendar (divisible by 4, except centuries not divisible by 400). Returns `""` for an invalid `Date`, a malformed string, a day/month that does not exist, or a date before year 1. +Formats a date as its Brazilian Portuguese "por extenso" textual representation, e.g. `"01/01/2024"` becomes `"primeiro de janeiro de dois mil e vinte e quatro"`. Accepts a `Date` (read by its local calendar date, the same convention used by `isHoliday`) or a string in `"dd/mm/yyyy"` or ISO `"yyyy-mm-dd"` format. With the default `options.style` of `"full"`, day 1 is written as "primeiro" and every other day uses the cardinal number; with `"month"`, only the month name is spelled out and the day/year are left as digits (day 1 as `"1º"`, e.g. `"2 de março de 2024"`, `"1º de janeiro de 2024"`). Month names are lowercase. In `"full"` style the year is written out as a cardinal number without the thousands comma that `convertNumberToWords`/`convertCurrencyToWords` use (`1999` reads as `"mil novecentos e noventa e nove"`, not `"mil, novecentos e noventa e nove"`), matching how a date is read aloud. `options.weekday` (default `false`) prefixes the pt-BR weekday name in lowercase followed by a comma (`"sábado, dois de março de dois mil e vinte e quatro"`), computed from the resolved calendar date. An invalid `style` value is ignored and the default is used. The result is always lowercase; apply any other casing to it yourself. February 29th is accepted on the leap years of the proleptic Gregorian calendar (divisible by 4, except centuries not divisible by 400). Returns `""` for an invalid `Date`, a malformed string, a day/month that does not exist, or a date before year 1. ```javascript import { convertDateToWords } from '@brazilian-utils/brazilian-utils'; @@ -1694,7 +1755,6 @@ import { convertDateToWords } from '@brazilian-utils/brazilian-utils'; convertDateToWords('01/01/2024'); // "primeiro de janeiro de dois mil e vinte e quatro" convertDateToWords('2024-01-02'); // "dois de janeiro de dois mil e vinte e quatro" convertDateToWords(new Date(2024, 0, 1)); // "primeiro de janeiro de dois mil e vinte e quatro" -convertDateToWords('01/01/2024', { case: 'sentence' }); // "Primeiro de janeiro de dois mil e vinte e quatro" convertDateToWords('02/03/2024', { style: 'month' }); // "2 de março de 2024" convertDateToWords('01/01/2024', { style: 'month' }); // "1º de janeiro de 2024" convertDateToWords('02/03/2024', { weekday: true }); // "sábado, dois de março de dois mil e vinte e quatro" @@ -1717,7 +1777,7 @@ formatVoterId('1234567880191'); // '1234 5678 8 01 91' (13-digit SP/MG voter id) ### isValidVoterId -Check if a voter ID number is valid. Accepts both the standard 12-digit id and the 13-digit id issued by São Paulo (UF `01`) and Minas Gerais (UF `02`). +Check if a voter ID number is valid. Accepts both the standard 12-digit id and the 13-digit id issued by São Paulo (UF `01`) and Minas Gerais (UF `02`). Whitespace and dots are accepted around and between the `0000 0000 00 00` groups, but any other character, a letter in particular, makes the value invalid. ```javascript import { generateVoterId, isValidVoterId } from '@brazilian-utils/brazilian-utils'; @@ -1754,6 +1814,8 @@ parseVoterId('1234 5678 8 01 91'); // '1234567880191' (13-digit SP/MG voter id) Check if a CNS (Cartão Nacional de Saúde) number is valid, the unique SUS (Sistema Único de Saúde) user identifier. Definitive cards (starting with 1 or 2) are validated with the same mod 11 weighting used for PIS numbers over an embedded 11 digit base, adjusting the base by +2 when the raw check digit computes to 10. Provisional cards (starting with 7, 8 or 9) are validated instead by a single weighted sum (weights 15 down to 1) that must be a multiple of 11. The value has to be written as the 15 digits, optionally split into the printed groups of 3-4-4-4 by whitespace or the usual mask characters; letters among the digits are rejected instead of being read past. +The two routines come from the [ANVISA CNS validation page](https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/), which sits behind a bot filter and answers HTTP 403 to non-browser clients. The [e-SUS APS page](https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html) documents the same algorithm and is reachable without a browser, but applies the provisional routine to numbers starting with 5, 7, 8 or 9; this implementation follows ANVISA and rejects a 5-prefixed number even when its weighted sum checks out. + ```javascript import { isValidCns } from '@brazilian-utils/brazilian-utils'; @@ -1770,14 +1832,14 @@ Format a CNS (Cartão Nacional de Saúde) number into the common display groups ```javascript import { formatCns } from '@brazilian-utils/brazilian-utils'; -formatCns('123456789010001'); // '123 4567 8901 0001' -formatCns(123456789010001); // '123 4567 8901 0001' +formatCns('123456789010000'); // '123 4567 8901 0000' +formatCns(123456789010000); // '123 4567 8901 0000' formatCns('89010001', { pad: true }); // '000 0000 8901 0001' ``` ### isValidCertidao -Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with weights cycling from 2 to 10 and back through 0. Accepts the usual mask characters and whitespace between/around groups. The layout is the in-force one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023, in the wording of the Provimento CN nº 182/2024); the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). +Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with weights cycling from 2 to 10 and back through 0. Accepts the usual mask characters and whitespace between/around groups. The layout is the one [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023) currently publishes, with inciso II and §§ 1º to 5º in the redação of the Provimento CN nº 237/2026 and the rest of the article in that of the Provimento CN nº 182/2024; the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). The serviço digits are fixed at `55`, the code [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) assigns to the registro civil das pessoas naturais, so a matrícula carrying any other pair in the ninth and tenth positions is rejected however good its check digits are. The book-type digit always has to name one of the nine book types (the same `CertidaoType` returned by `parseCertidao`), so a matrícula whose digit is `0` is rejected however good its check digits are, the same way `parseCertidao` returns `null` for it. `options.accept` (part of `IsValidCertidaoOptions`) narrows that to the listed types; it defaults to every type, and a value that is not an array falls back to that default. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. @@ -1924,7 +1986,7 @@ formatCaepf('184', { pad: true }); // 000.000.000/001-84 ### isValidRegistroProfissional -Check the structure of a professional council registration number (registro/inscrição profissional). Options are typed as `IsValidRegistroProfissionalOptions`: `options.council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `options.stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits and the tipo de registro (`"O"` Originário, `"P"` Provisório or `"T"` Transferido, which says nothing about the professional category), as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1) and in the Resolução CFC nº 1.707/2023. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. The OAB, the CFM and the CFO publish no format for the numbers they issue, so the digit ranges accepted for `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative. CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). +Check the structure of a professional council registration number (registro/inscrição profissional). Options are typed as `IsValidRegistroProfissionalOptions`: `options.council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `options.stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits, the tipo de registro (`"O"` Originário or `"P"` Provisório, which says nothing about the professional category) and the check digit, as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). A Registro Transferido or Secundário appends `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, per that same item and [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: the Manual's own examples are `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` and `PI-111222/O-5 S-AC`. Both UFs must be real state codes, and `options.stateCode` is compared against the originating one. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. Only the CRC shape and those CRP regional codes rest on a published source: the CFP page publishes no length for the inscription number itself, and the OAB, the CFM and the CFO publish no format at all, so the digit ranges accepted for `"CRP"`, `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative (the OAB/SP public search field is `maxlength="7"`, and the CFM documents `300`-prefixed and `P`-suffixed CRMs, none of which these shapes express). CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). ```javascript import { isValidRegistroProfissional } from '@brazilian-utils/brazilian-utils'; @@ -1933,6 +1995,8 @@ isValidRegistroProfissional('123456/SP', { council: 'OAB' }); // true isValidRegistroProfissional('123456-RJ', { council: 'OAB', stateCode: 'SP' }); // false (UF mismatch) isValidRegistroProfissional('06/12345', { council: 'CRP' }); // true isValidRegistroProfissional('SP-123456/O-3', { council: 'CRC' }); // true +isValidRegistroProfissional('SP-123456/O-3 T-MG', { council: 'CRC' }); // true (registro transferido) +isValidRegistroProfissional('SP-123456/T-3', { council: 'CRC' }); // false ("T" is not a tipo de registro) ``` ### isValidVin @@ -1950,7 +2014,7 @@ isValidVin('1HGCM8263IA004352'); // false (contains the excluded letter I) ### isValidCbo -Check if a CBO (Classificação Brasileira de Ocupações) code exists in the MTE occupation table. Accepts the code with or without the hyphen mask, or as a number. A string is only read as a code when it is written in one of those forms (the 6 digits, or the `NNNN-NN` mask, with the usual separators between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +Check if a CBO (Classificação Brasileira de Ocupações) code exists in the MTE occupation table. Accepts the code with or without the hyphen mask, or as a number. A string is only read as a code when it is written in one of those forms (the 6 digits, or the `NNNN-NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. ```javascript import { isValidCbo } from '@brazilian-utils/brazilian-utils'; @@ -1963,7 +2027,7 @@ isValidCbo('2124abc05'); // false (not a documented form) isValidCbo(-212405); // false (not a non-negative safe integer) ``` -The occupation titles come from the [official CBO 2002 tables published by the MTE](http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf). +The occupation titles come from the [official CBO 2002 occupation table published by the MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). ### getCbo @@ -1977,11 +2041,11 @@ getCbo('000000'); // null getCbo('2124abc05'); // null (not a documented form) ``` -The occupation titles come from the [official CBO 2002 tables published by the MTE](http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf). +The occupation titles come from the [official CBO 2002 occupation table published by the MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). ### isValidCnae -Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the CNAE 2.3 table published by IBGE. Accepts the code with or without the `NNNN-N/NN` mask, or as a number. A string is only read as a code when it is written in one of those forms (the 7 digits, or the mask, with the usual separators between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the CNAE 2.3 table published by IBGE. Accepts the code with or without the `NNNN-N/NN` mask, or as a number. A string is only read as a code when it is written in one of those forms (the 7 digits, or the mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. ```javascript import { isValidCnae } from '@brazilian-utils/brazilian-utils'; @@ -1995,12 +2059,18 @@ isValidCnae(-111301); // false (not a non-negative safe integer) ### formatCnae -Format a CNAE (Classificação Nacional de Atividades Econômicas) subclass code. +Format a CNAE (Classificação Nacional de Atividades Econômicas) subclass code. `options.pad` (part of `FormatCnaeOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes, which is what an input being typed into needs; with `true` the value is first left padded with zeros to the 7 digits of a complete subclass code, so it always comes back fully masked. A number is treated exactly like the string of its digits, so it is only padded under `pad: true`. Only digits and the mask characters are accepted; anything else gives `''`, and so does a number that is not a non-negative safe integer. ```javascript import { formatCnae } from '@brazilian-utils/brazilian-utils'; formatCnae('6201501'); // 6201-5/01 +formatCnae('62'); // 62 (masked as far as it goes) +formatCnae('62015'); // 6201-5 +formatCnae('62', { pad: true }); // 0000-0/62 (padded to 7 digits first) +formatCnae(111301, { pad: true }); // 0111-3/01 +formatCnae('abc6201501'); // '' (not a documented form) +formatCnae(-6201501); // '' (not a non-negative safe integer) ``` ### getCnae @@ -2017,7 +2087,7 @@ getCnae('0111abc301'); // null (not a documented form) ### isValidNcm -Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. Accepts the code with or without the dotted mask, or as a number. +Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. Accepts the code with or without the dotted mask, or as a number. A string is only read as a code when it is written in one of those forms (the 8 digits, or the `NNNN.NN.NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. ```javascript import { isValidNcm } from '@brazilian-utils/brazilian-utils'; @@ -2025,40 +2095,55 @@ import { isValidNcm } from '@brazilian-utils/brazilian-utils'; isValidNcm('8471.30.12'); // true isValidNcm('84713012'); // true isValidNcm('00000000'); // false +isValidNcm('abc01012100'); // false (not a documented form) +isValidNcm(-84713012); // false (not a non-negative safe integer) ``` ### formatNcm -Format an NCM (Nomenclatura Comum do Mercosul) code. +Format an NCM (Nomenclatura Comum do Mercosul) code. `options.pad` (part of `FormatNcmOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes, which is what an input being typed into needs; with `true` the value is first left padded with zeros to the 8 digits of a complete code, so it always comes back fully masked. A number is treated exactly like the string of its digits, so it is only padded under `pad: true`. Only digits and the mask characters are accepted; anything else gives `''`, and so does a number that is not a non-negative safe integer. ```javascript import { formatNcm } from '@brazilian-utils/brazilian-utils'; formatNcm('84713012'); // 8471.30.12 +formatNcm('8471'); // 8471 (masked as far as it goes) +formatNcm('847130'); // 8471.30 +formatNcm('8471', { pad: true }); // 0000.84.71 (padded to 8 digits first) +formatNcm('abc8471'); // '' (not a documented form) +formatNcm(-84713012); // '' (not a non-negative safe integer) ``` ### isValidCfop -Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table (Ajuste SINIEF 07/2001 and updates). Only operable codes count: the group and subgroup headings of the official nomenclature, the codes ending in `00` and `50` (1000, 1100, 1150, 5350, ...), are section titles rather than codes a document can carry, so they are rejected. +Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table. The table is the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), the text in force (current wording given by Ajuste SINIEF 03/24, last amended by Ajuste SINIEF 39/25), not the frozen 2001 text of Ajuste SINIEF 07/01. Only operable codes count: the group and subgroup headings of the official nomenclature, the codes ending in `00` and `50` (1000, 1100, 1150, 5350, ...), are section titles rather than codes a document can carry, so they are rejected. + +A string is only read as a code when it is written in one of the documented forms (the 4 digits, or the `N.NNN` form the annex prints, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. ```javascript import { isValidCfop } from '@brazilian-utils/brazilian-utils'; isValidCfop('5102'); // true +isValidCfop('1.101'); // true +isValidCfop('7504'); // true (added by the 2022 rewrite of the annex) isValidCfop('0000'); // false isValidCfop('1150'); // false (a subgroup heading, not an operable code) +isValidCfop('abc5102'); // false (not a documented form) +isValidCfop(-5102); // false (not a non-negative safe integer) ``` ### getCfop -Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description. The group and subgroup headings of the official nomenclature, the codes ending in `00` and `50`, are not in the table and give `null`. +Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description, as the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24) words it. The group and subgroup headings of the official nomenclature, the codes ending in `00` and `50`, are not in the table and give `null`. Same input rules as `isValidCfop`. ```javascript import { getCfop } from '@brazilian-utils/brazilian-utils'; -getCfop('5102'); // { code: '5102', description: 'Venda de mercadoria adquirida ou recebida de terceiros' } +getCfop('1101'); // { code: '1101', description: 'Compra para industrialização ou produção rural' } +getCfop('7504'); // { code: '7504', description: 'Exportação de mercadoria que foi objeto de formação de lote de exportação' } getCfop('0000'); // null getCfop('5350'); // null (a subgroup heading, not an operable code) +getCfop('abc5102'); // null (not a documented form) ``` ### isValidCst @@ -2067,33 +2152,44 @@ Check if a CST (Código de Situação Tributária) code is valid for a given tax | Tax | Format | Accepted codes | | --- | --- | --- | -| `icms` | 3 digits (origem + CST) | origem `0`-`8` + one of `00`, `10`, `20`, `30`, `40`, `41`, `50`, `51`, `60`, `70`, `90` | +| `icms` | 3 digits (origem + CST) | origem `0`-`8` + one of `00`, `02`, `10`, `15`, `20`, `30`, `40`, `41`, `50`, `51`, `53`, `60`, `61`, `70`, `90` | | `ipi` | 2 digits | `00`, `01`, `02`, `03`, `04`, `05`, `49`, `50`, `51`, `52`, `53`, `54`, `55`, `99` | | `pis` | 2 digits | `01`-`09`, `49`, `50`-`56`, `60`-`67`, `70`-`75`, `98`, `99` | | `cofins` | 2 digits | same table as `pis` | `options.tax` (part of `IsValidCstOptions`) is optional: omit it to accept a code that exists in any one of the four tables above. +The ICMS Tabela B is the one in force: the [consolidated Anexo I of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), whose current wording came from [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (effective 01.12.23) and which [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) amended by revoking items 12, 13, 52, 72 and 74 (effective 09.07.24). `02`, `15`, `53` and `61` are its monofasia de combustíveis codes. + +A string is only read as a code when it is written in one of the documented forms (the 2 or 3 digits, with a single separator between them and optional surrounding whitespace), and a number only when it is a non-negative safe integer. + ```javascript import { isValidCst } from '@brazilian-utils/brazilian-utils'; isValidCst('000', { tax: 'icms' }); // true isValidCst('110', { tax: 'icms' }); // true +isValidCst('002', { tax: 'icms' }); // true (monofasia de combustíveis) isValidCst('06', { tax: 'pis' }); // true isValidCst('99', { tax: 'ipi' }); // true isValidCst('110'); // true (found in the icms table, tax omitted) isValidCst('999'); // false (not in any table) +isValidCst('abc110'); // false (not a documented form) +isValidCst(-110); // false (not a non-negative safe integer) ``` ### isValidCsosn -Check if a CSOSN (Código de Situação da Operação no Simples Nacional) code is one of the 10 codes defined by Ajuste SINIEF 03/2010: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` or `900`. +Check if a CSOSN (Código de Situação da Operação no Simples Nacional) code is one of the 10 codes of the [consolidated Anexo III-A of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), the table Ajuste SINIEF 03/2010 instituted: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` or `900`. + +A string is only read as a code when it is written in one of the documented forms (the 3 digits, with a single separator between them and optional surrounding whitespace), and a number only when it is a non-negative safe integer. ```javascript import { isValidCsosn } from '@brazilian-utils/brazilian-utils'; isValidCsosn('101'); // true isValidCsosn('999'); // false +isValidCsosn('abc101'); // false (not a documented form) +isValidCsosn(-101); // false (not a non-negative safe integer) ``` ### removeAccents diff --git a/docs/llms.txt b/docs/llms.txt index c0834e7c..f4f750cf 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -29,7 +29,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [isValidCep](https://brazilian-utils.com.br/utilities.md#isvalidcep): Check if CEP (brazilian postal code) is valid. - [isValidBoleto](https://brazilian-utils.com.br/utilities.md#isvalidboleto): Check if boleto (brazilian payment method) is valid. - [isValidPixKey](https://brazilian-utils.com.br/utilities.md#isvalidpixkey): Check if a Pix key (chave Pix) is valid: a CPF, a CNPJ, an e-mail address, a Brazilian mobile phone number or a random key (EVP), per the DICT key formats. -- [isValidPixPayload](https://brazilian-utils.com.br/utilities.md#isvalidpixpayload): Check if a Pix BR Code payload (the string behind a Pix QR Code and behind "Pix copia e cola") is valid: well-formed TLV structure, the mandatory objects present, one of the "Merchant Account Information" templates carrying the `br.gov.bcb.pix` GUI with a key or a URL, a "Point of Initiation Method" object (`01`) that agrees with it (a key requires a static payload, so `01` is absent or `"11"`; a URL requires a dynamic one, so `01` is `"12"`), an amount (`54`) greater than zero in a static payload, and a matching CRC-16. +- [isValidPixPayload](https://brazilian-utils.com.br/utilities.md#isvalidpixpayload): Check if a Pix BR Code payload (the string behind a Pix QR Code and behind "Pix copia e cola") is valid: well-formed TLV structure, the mandatory objects present, one of the "Merchant Account Information" templates carrying the `br.gov.bcb.pix` GUI with a key or a URL, and a matching CRC-16. - [isValidNfeKey](https://brazilian-utils.com.br/utilities.md#isvalidnfekey): Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. - [isValidEmail](https://brazilian-utils.com.br/utilities.md#isvalidemail): Check if email is valid. - [isValidPhone](https://brazilian-utils.com.br/utilities.md#isvalidphone): Check if phone number (mobile or landline) is valid. @@ -42,7 +42,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [isValidProcessoJuridico](https://brazilian-utils.com.br/utilities.md#isvalidprocessojuridico): Validate the processo jurídico number according to CNJ's definition. - [isValidIe](https://brazilian-utils.com.br/utilities.md#isvalidie): Check if inscrição estadual (state registration) is valid. - [isValidBankAccount](https://brazilian-utils.com.br/utilities.md#isvalidbankaccount): Check if a Brazilian bank account is valid. -- [isValidIban](https://brazilian-utils.com.br/utilities.md#isvalidiban): Check if a Brazilian IBAN (International Bank Account Number) is valid, per Bacen's Diretrizes de Implementação do IBAN no Brasil (Circular BCB nº 3.625/2013): `BR` + 2 ISO 7064 MOD 97-10 check digits + 8 digit ISPB + 5 digit branch + 10 digit account + 1 letter account type (any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 alphanumeric owner indicator, 29 characters total. +- [isValidIban](https://brazilian-utils.com.br/utilities.md#isvalidiban): Check if a Brazilian IBAN (International Bank Account Number) is valid, per Bacen's Diretrizes de Implementação do IBAN no Brasil (Circular BCB nº 3.625/2013): `BR` + 2 ISO 7064 MOD 97-10 check digits + 8 digit ISPB + 5 digit branch + 10 digit account + 1 letter account type (any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 owner indicator (`1` for the first or only holder up to `9` for the ninth, then `A` to `Z` from the tenth, so `0` is rejected), 29 characters total. - [isValidCreditCard](https://brazilian-utils.com.br/utilities.md#isvalidcreditcard): Check if a payment card number is valid using the Luhn algorithm (ISO/IEC 7812-1). - [isValidPassport](https://brazilian-utils.com.br/utilities.md#isvalidpassport): Check if a Brazilian passport number is valid (2 letters followed by 6 digits). - [isValidCnh](https://brazilian-utils.com.br/utilities.md#isvalidcnh): Check if CNH is valid. @@ -58,21 +58,21 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [isValidCbo](https://brazilian-utils.com.br/utilities.md#isvalidcbo): Check if a CBO (Classificação Brasileira de Ocupações) code exists in the MTE occupation table. - [isValidCnae](https://brazilian-utils.com.br/utilities.md#isvalidcnae): Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the CNAE 2.3 table published by IBGE. - [isValidNcm](https://brazilian-utils.com.br/utilities.md#isvalidncm): Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. -- [isValidCfop](https://brazilian-utils.com.br/utilities.md#isvalidcfop): Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table (Ajuste SINIEF 07/2001 and updates). +- [isValidCfop](https://brazilian-utils.com.br/utilities.md#isvalidcfop): Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table. - [isValidCst](https://brazilian-utils.com.br/utilities.md#isvalidcst): Check if a CST (Código de Situação Tributária) code is valid for a given tax. -- [isValidCsosn](https://brazilian-utils.com.br/utilities.md#isvalidcsosn): Check if a CSOSN (Código de Situação da Operação no Simples Nacional) code is one of the 10 codes defined by Ajuste SINIEF 03/2010: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` or `900`. +- [isValidCsosn](https://brazilian-utils.com.br/utilities.md#isvalidcsosn): Check if a CSOSN (Código de Situação da Operação no Simples Nacional) code is one of the 10 codes of the consolidated Anexo III-A of Convênio SINIEF s/nº 1970, the table Ajuste SINIEF 03/2010 instituted: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` or `900`. ## Formatters (format*) - [formatCpf](https://brazilian-utils.com.br/utilities.md#formatcpf): Format CPF. - [formatCnpj](https://brazilian-utils.com.br/utilities.md#formatcnpj): Format CNPJ. - [formatBoleto](https://brazilian-utils.com.br/utilities.md#formatboleto): Format a boleto number. -- [formatNfeKey](https://brazilian-utils.com.br/utilities.md#formatnfekey): Format a DF-e (NF-e, NFC-e, CT-e, MDF-e or CT-e OS) access key into groups of 4 digits separated by spaces, the common display form printed on the DANFE. +- [formatNfeKey](https://brazilian-utils.com.br/utilities.md#formatnfekey): Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. - [formatPhone](https://brazilian-utils.com.br/utilities.md#formatphone): Format phone number according to Brazilian patterns. - [formatPis](https://brazilian-utils.com.br/utilities.md#formatpis): Format PIS number. - [formatCep](https://brazilian-utils.com.br/utilities.md#formatcep): Format CEP (brazilian postal code). - [formatProcessoJuridico](https://brazilian-utils.com.br/utilities.md#formatprocessojuridico): Format the processo jurídico number according to CNJ's definition (mask `NNNNNNN-DD.AAAA.J.TR.OOOO`). -- [formatIban](https://brazilian-utils.com.br/utilities.md#formatiban): Format a Brazilian IBAN by grouping it in blocks of 4 characters, the ISO 13616 "print" presentation used on statements and bank forms. +- [formatIban](https://brazilian-utils.com.br/utilities.md#formatiban): Format an IBAN in the ISO 13616 print grouping, blocks of 4 characters, the presentation used on statements and bank forms. - [formatCurrency](https://brazilian-utils.com.br/utilities.md#formatcurrency): Formats an integer or float to a string in the BRL pattern. - [formatPassport](https://brazilian-utils.com.br/utilities.md#formatpassport): Format a Brazilian passport number (uppercase, without symbols, capped to 8 characters). - [formatCnh](https://brazilian-utils.com.br/utilities.md#formatcnh): Format CNH. @@ -99,7 +99,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [parsePis](https://brazilian-utils.com.br/utilities.md#parsepis): Remove PIS formatting, keep only digits, and cap the result to 11 digits. - [parseCep](https://brazilian-utils.com.br/utilities.md#parsecep): Remove CEP formatting, keep only digits, and cap the result to 8 digits. - [parseProcessoJuridico](https://brazilian-utils.com.br/utilities.md#parseprocessojuridico): Remove processo jurídico formatting, keep only digits, and cap the result to 20 digits. -- [parseIban](https://brazilian-utils.com.br/utilities.md#parseiban): Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator). +- [parseIban](https://brazilian-utils.com.br/utilities.md#parseiban): Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). - [parseCurrency](https://brazilian-utils.com.br/utilities.md#parsecurrency): Transforms a string to an integer or float format. - [parsePassport](https://brazilian-utils.com.br/utilities.md#parsepassport): Remove all non-alphanumeric characters from a passport number, uppercase the result, and cap it to 8 characters. - [parseCnh](https://brazilian-utils.com.br/utilities.md#parsecnh): Remove CNH formatting, keep only digits, and cap the result to 11 digits. @@ -149,18 +149,19 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [getMunicipalityByCode](https://brazilian-utils.com.br/utilities.md#getmunicipalitybycode): Look up a Brazilian municipality by its 7-digit IBGE code. - [getCbo](https://brazilian-utils.com.br/utilities.md#getcbo): Look a CBO (Classificação Brasileira de Ocupações) code up and get its official occupation title. - [getCnae](https://brazilian-utils.com.br/utilities.md#getcnae): Look a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up and get its formatted code and official description. -- [getCfop](https://brazilian-utils.com.br/utilities.md#getcfop): Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description. +- [getCfop](https://brazilian-utils.com.br/utilities.md#getcfop): Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description, as the consolidated Anexo II of Convênio SINIEF s/nº 1970 words it. ## Other utilities -- [capitalize](https://brazilian-utils.com.br/utilities.md#capitalize): Transforms the first letter into a capital one of each word ignoring prepositions. +- [capitalize](https://brazilian-utils.com.br/utilities.md#capitalize): Transforms the first letter into a capital one of each word, the way a Brazilian name, company name or address is written, with no options needed. - [convertNumberToWords](https://brazilian-utils.com.br/utilities.md#convertnumbertowords): Formats an integer as its Brazilian Portuguese cardinal number words ("por extenso"), e.g. `1235` becomes `"mil, duzentos e trinta e cinco"`. - [convertCurrencyToWords](https://brazilian-utils.com.br/utilities.md#convertcurrencytowords): Formats a monetary amount in Brazilian Reais as its "por extenso" textual representation, the style used to write out the amount by hand on cheques and contracts, e.g. `1523.45` becomes `"mil, quinhentos e vinte e três reais e quarenta e cinco centavos"`. - [convertLicensePlateToMercosul](https://brazilian-utils.com.br/utilities.md#convertlicenseplatetomercosul): Convert an old format Brazilian license plate (`LLLNNNN`) to the Mercosul format (`LLLNLNN`), following the official conversion table: the digit in the 5th position becomes a letter (`0` through `9` mapping to `A` through `J`). - [isHoliday](https://brazilian-utils.com.br/utilities.md#isholiday): Check if a specific date is a Brazilian holiday. - [isBusinessDay](https://brazilian-utils.com.br/utilities.md#isbusinessday): Check if a date is a Brazilian business day (dia útil). -- [addBusinessDays](https://brazilian-utils.com.br/utilities.md#addbusinessdays): Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and Brazilian holidays exactly as `isBusinessDay` defines them (same `stateCode`/`includeOptional` options). -- [differenceInBusinessDays](https://brazilian-utils.com.br/utilities.md#differenceinbusinessdays): Count the number of Brazilian business days (dias úteis) between two dates, mirroring the semantics of date-fns' `differenceInBusinessDays` (verified against its source): `params.from` is counted when it is itself a business day, `params.to` is never counted, and every business day strictly in between is counted once. +- [addBusinessDays](https://brazilian-utils.com.br/utilities.md#addbusinessdays): Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and Brazilian holidays exactly as `isBusinessDay` defines them (same `BusinessDayOptions`). +- [subBusinessDays](https://brazilian-utils.com.br/utilities.md#subbusinessdays): Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too. +- [differenceInBusinessDays](https://brazilian-utils.com.br/utilities.md#differenceinbusinessdays): Count the number of Brazilian business days (dias úteis) between two dates, mirroring the semantics of date-fns' `differenceInBusinessDays` (verified against its source), argument order included: `differenceInBusinessDays(laterDate, earlierDate, options?)`. - [convertDateToWords](https://brazilian-utils.com.br/utilities.md#convertdatetowords): Formats a date as its Brazilian Portuguese "por extenso" textual representation, e.g. `"01/01/2024"` becomes `"primeiro de janeiro de dois mil e vinte e quatro"`. - [removeAccents](https://brazilian-utils.com.br/utilities.md#removeaccents): Remove diacritical marks (accents, tildes, cedillas) from a string, decomposing every accented character into its base letter plus combining marks (Unicode NFD) and dropping the combining marks. diff --git a/docs/pt-br/getting-started.md b/docs/pt-br/getting-started.md index 8f28bc9a..0502032f 100644 --- a/docs/pt-br/getting-started.md +++ b/docs/pt-br/getting-started.md @@ -69,13 +69,13 @@ Alguns utilitários são a exceção: cada um embute um dataset oficial e pesa m | Utilitário | Dataset | Minificado | Gzip | | --- | --- | --- | --- | -| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 municípios do IBGE, com nomes e códigos | 156 KB | 50 KB | -| `getCities` | nomes dos 5571 municípios do IBGE | 153 KB | 49 KB | -| `isValidNcm` | códigos NCM (Nomenclatura Comum do Mercosul) | 113 KB | 24 KB | -| `isValidCbo` · `getCbo` | títulos das ocupações da CBO 2002 | 110 KB | 27 KB | -| `isValidCnae` · `getCnae` | subclasses da CNAE 2.3 | 93 KB | 21 KB | -| `isValidCfop` · `getCfop` | descrições das operações do CFOP | 55 KB | 5,4 KB | -| `getBanks` · `getBankByCode` | participantes do STR do Banco Central (COMPE + ISPB) | 28 KB | 7,3 KB | +| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 municípios do IBGE, com nomes e códigos | 155,9 KB | 50,0 KB | +| `getCities` | nomes dos 5571 municípios do IBGE | 153,6 KB | 49,4 KB | +| `isValidNcm` | códigos NCM (Nomenclatura Comum do Mercosul) | 113,4 KB | 24,0 KB | +| `isValidCbo` · `getCbo` | títulos das ocupações da CBO 2002 | 118,4 KB | 30,2 KB | +| `isValidCnae` · `getCnae` | subclasses da CNAE 2.3 | 93,6 KB | 21,1 KB | +| `isValidCfop` · `getCfop` | descrições das operações do CFOP | 68,3 KB | 6,5 KB | +| `getBanks` · `getBankByCode` | participantes do STR do Banco Central (COMPE + ISPB) | 37,9 KB | 9,3 KB | Importar qualquer um deles da raiz, mesmo ao lado de um único utilitário pequeno, traz todo esse dataset para o seu bundle principal, porque este pacote é publicado como um único módulo ESM: um `import()` dinâmico da raiz (`await import('@brazilian-utils/brazilian-utils')`) ainda resolve para esse mesmo arquivo único, então não há como separá-lo sozinho. Um bundler que faz code-splitting precisa de um módulo separado para separar. @@ -97,4 +97,4 @@ getMunicipalityByCode('3550308'); Todos os utilitários estão disponíveis dessa forma, como `@brazilian-utils/brazilian-utils/` (kebab-case, seguindo o nome da função: `isValidCpf` → `is-valid-cpf`), pelo mesmo motivo de lazy-loading/code-splitting. -Escolha um estilo por utilitário em cada aplicação: um bundler trata o import da raiz e o import do subpath como dois módulos independentes, então importar `getCities` tanto da raiz quanto de `/get-cities` na mesma aplicação inclui a tabela de 153 KB de cidades duas vezes, uma em cada módulo. +Escolha um estilo por utilitário em cada aplicação: um bundler trata o import da raiz e o import do subpath como dois módulos independentes, então importar `getCities` tanto da raiz quanto de `/get-cities` na mesma aplicação inclui a tabela de 153,6 KB de cidades duas vezes, uma em cada módulo. diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 28c5b00a..a7c03052 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -2,7 +2,7 @@ Aqui você encontrará todos os utilitários disponíveis para uso. -> **Tratamento de entrada:** nenhuma função pública síncrona lança exceção com `null`/`undefined` ou um valor de tipo incorreto; as duas funções de rede, `getAddressInfoByCep` e `getCepInfoByAddress`, rejeitam com seus erros tipados (veja as seções delas). Os validadores (`isValid*`) retornam `false`; `isHoliday` retorna `false`; `getHolidays` retorna `[]`; `generateProcessoJuridico` retorna `null`; `getMunicipality` retorna `null` para uma busca malformada/sem correspondência. Todas as demais funções `format*`/`parse*` (incluindo `capitalize`) retornam um valor vazio do seu tipo de retorno: `""` para strings, `0` para `parseCurrency`. `formatCurrency` retorna `""` para um número não finito. +> **Tratamento de entrada:** nenhuma função pública síncrona lança exceção com `null`/`undefined` ou um valor de tipo incorreto; as duas funções de rede, `getAddressInfoByCep` e `getCepInfoByAddress`, rejeitam com seus erros tipados (veja as seções delas). Os validadores (`isValid*`) retornam `false`; `isHoliday` retorna `false`; `getHolidays` retorna `[]`; `generateProcessoJuridico` retorna `null`; `getMunicipality` retorna `null` para uma busca malformada/sem correspondência. Todas as demais funções `format*`/`parse*` (incluindo `capitalize`) retornam um valor vazio do seu tipo de retorno: `""` para strings, `0` para `parseCurrency`. `formatCurrency` retorna `""` para um número não finito e para um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo); `null`, arrays e booleanos passam por `Number()` como no 2.3.0. A única exceção à promessa acima: um objeto criado com `Object.create(null)` não tem `toString`, então as funções `format*`/`parse*` que leem a entrada como texto ainda lançam um `TypeError` para ele, exatamente como na 2.3.0. ## isValidCpf @@ -39,7 +39,7 @@ parseCpf('746.506.880-00'); // 74650688000 ## generateCpf -Gera um CPF válido aleatório. +Gera um CPF válido aleatório. Usa `Math.random()` internamente, então não é criptograficamente seguro. ```javascript import { generateCpf } from '@brazilian-utils/brazilian-utils' @@ -85,7 +85,7 @@ parseCnpj('12.OUT.345/0001-99', { version: 2 }); // 12OUT345000199 ## isValidCep -Valida se o CEP é válido. Aceita entrada como `string` ou `number`; espaços, pontos e hífens ao redor/entre os 8 dígitos são ignorados, mas qualquer outro caractere, uma letra em especial, invalida o valor. +Valida se o CEP ([código de endereçamento postal](https://pt.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)) é válido. Aceita entrada como `string` ou `number`; espaços, pontos e hífens ao redor/entre os 8 dígitos são ignorados, mas qualquer outro caractere, uma letra em especial, invalida o valor. ```javascript import { isValidCep } from '@brazilian-utils/brazilian-utils'; @@ -112,7 +112,7 @@ generateCnpj(2); // CNPJ alfanumérico, ex. 'Q0SLFMBD7VX439' ## isValidBoleto -Valida se o boleto é válido. Suporta tanto o boleto de "cobrança bancária" de 47 dígitos quanto o "boleto de arrecadação" (convênio/tributos): seja a linha digitável de 48 dígitos, seja o código de barras de 44 dígitos, ambos iniciados com `8`. +Valida se o boleto ([meio de pagamento brasileiro](https://pt.wikipedia.org/wiki/Boleto_banc%C3%A1rio)) é válido. Suporta tanto o boleto de "cobrança bancária" de 47 dígitos quanto o "boleto de arrecadação" (convênio/tributos): seja a linha digitável de 48 dígitos, seja o código de barras de 44 dígitos, ambos iniciados com `8`. Uma tolerância é mantida desde a 2.3.0: o código de moeda na posição 4 do código de barras da cobrança bancária não é verificado, embora a Carta-Circular BCB nº 2.926/2000 o fixe em `9` (real), então um boleto com qualquer outro dígito de moeda continua válido. ```javascript import { isValidBoleto } from '@brazilian-utils/brazilian-utils'; @@ -146,7 +146,7 @@ parseBoleto('00190.00009 01149.718601 68524.522114 6 75860000102656'); // 001900 ## generateBoleto -Gera um boleto válido aleatório. Informe `{ type: "arrecadacao" }` (tipado como `GenerateBoletoOptions`) para gerar um boleto de arrecadação em vez do tipo padrão "bancario" (cobrança bancária). +Gera um boleto válido aleatório. Informe `{ type: "arrecadacao" }` (tipado como `GenerateBoletoOptions`) para gerar um boleto de arrecadação em vez do tipo padrão "bancario" (cobrança bancária). Um boleto de arrecadação sorteia o segmento entre 1 e 7 (o segmento 9 é de uso dos próprios bancos) e o identificador de valor entre os quatro valores possíveis, `6` e `8` para valor efetivo e `7` e `9` para quantidade de moeda, de modo que os dois ramos de `hasEffectiveValue` do `getBoletoInfo` sejam alcançáveis. ```javascript import { generateBoleto } from '@brazilian-utils/brazilian-utils'; @@ -157,7 +157,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ## getBoletoInfo -Extrai informações de um boleto (valor, data de vencimento, código do banco). Aceita opcionalmente `{ referenceDate }` (tipado como `GetBoletoInfoOptions`) para resolver o ciclo do "fator de vencimento" a partir de uma data específica em vez de agora (o ciclo do fator reiniciou em 22/02/2025, segundo a FEBRABAN). Para um boleto de arrecadação, o resultado, tipado como `BoletoInfo`, não tem `bankCode`/`expirationDate` e traz em vez disso `type: "arrecadacao"`, `segment`, `value` e `hasEffectiveValue`. +Extrai informações de um boleto (valor, data de vencimento, código do banco). Aceita opcionalmente `{ referenceDate }` (tipado como `GetBoletoInfoOptions`) para resolver o ciclo do "fator de vencimento" a partir de uma data específica em vez de agora (o ciclo do fator reiniciou em 22/02/2025, segundo a FEBRABAN). Nem a FEBRABAN nem o Banco Central publicam uma forma de distinguir um fator do ciclo antigo de um do ciclo novo, então todo fator resolve para uma de duas datas separadas por 9000 dias e o `referenceDate` escolhe entre elas por meio das janelas de segurança da própria biblioteca: o mesmo boleto pode passar a resolver para a outra candidata com o tempo, então informe `referenceDate` explicitamente sempre que a resposta precisar ser estável. Para um boleto de arrecadação, o resultado, tipado como `BoletoInfo`, não tem `bankCode`/`expirationDate` e traz em vez disso `type: "arrecadacao"`, `segment`, `value` e `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -209,7 +209,7 @@ parsePixKey('+5551998259765'); // { type: 'phone', value: '+5551998259765' } ## isValidPixPayload -Valida se um payload de BR Code Pix (a string por trás de um QR Code Pix e do "Pix copia e cola") é válido: estrutura TLV bem formada, objetos obrigatórios presentes, um dos templates "Merchant Account Information" carregando o GUI `br.gov.bcb.pix` junto com uma chave ou uma URL, um objeto "Point of Initiation Method" (`01`) coerente com ele (uma chave exige um payload estático, com `01` ausente ou `"11"`; uma URL exige um dinâmico, com `01` igual a `"12"`), um valor (`54`) maior que zero em um payload estático, e um CRC-16 que confere. A chave em si não é validada contra os formatos do DICT, use `isValidPixKey` para isso. Payloads que trazem a localização em um Unreserved Template (IDs 80 a 99), como o "QR Code composto" do Pix Automático (Pix recorrente), estão fora de escopo e são considerados inválidos. +Valida se um payload de BR Code Pix (a string por trás de um QR Code Pix e do "Pix copia e cola") é válido: estrutura TLV bem formada, objetos obrigatórios presentes, um dos templates "Merchant Account Information" carregando o GUI `br.gov.bcb.pix` junto com uma chave ou uma URL, e um CRC-16 que confere. O objeto "Point of Initiation Method" (`01`) é informativo: o Manual do BR Code o marca como opcional e só atribui significado ao valor `"12"` ("só pode ser utilizado uma vez"), então ele pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` torna o payload inválido. Quando um payload construído em torno de uma chave traz um valor (`54`), esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque, ou seja, a menos que traga o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`) como prescreve o §2.6 do manual do Pix; rejeitar `"0"`/`"0.00"` sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. A chave em si não é validada contra os formatos do DICT, use `isValidPixKey` para isso. Payloads que trazem a localização em um Unreserved Template (IDs 80 a 99), como o "QR Code composto" do Pix Automático (Pix recorrente), estão fora de escopo e são considerados inválidos. ```javascript import { isValidPixPayload } from '@brazilian-utils/brazilian-utils'; @@ -224,7 +224,7 @@ isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (CRC quebrado) ## parsePixPayload -Interpreta um payload de BR Code Pix e retorna seus campos. O payload é validado pelo `isValidPixPayload` primeiro, então uma estrutura malformada, um CRC quebrado ou um objeto obrigatório ausente retornam `null` em vez de um resultado parcial. Um payload estático vem com `key`, um dinâmico com `url`. O resultado é tipado como `PixPayload`; `pointOfInitiation` é tipado como `PixPointOfInitiation` (`"static"` ou `"dynamic"`). As informações da conta do recebedor devem trazer exatamente uma chave ou uma `url` (verificada com a mesma regra de localização de PSP do `generatePixPayload`), e o objeto "Point of Initiation Method" (`01`) precisa ser coerente com isso: uma chave pertence a um payload estático (`01` ausente ou `"11"`) e uma `url` a um dinâmico (`01` igual a `"12"`), então qualquer outra combinação retorna `null`. Um payload estático que informa um valor precisa informar um valor maior que zero (`54` igual a `0.00` é reservado ao BR Code de Pix Saque/Troco, que está fora de escopo), e em um payload dinâmico o valor e o `txid` são ignorados, como o manual determina. Payloads cuja localização fica em um Unreserved Template (IDs 80 a 99, Pix Automático) estão fora de escopo e retornam `null`. +Interpreta um payload de BR Code Pix e retorna seus campos. O payload é validado pelo `isValidPixPayload` primeiro, então uma estrutura malformada, um CRC quebrado ou um objeto obrigatório ausente retornam `null` em vez de um resultado parcial. Um payload estático vem com `key`, um dinâmico com `url`. O resultado é tipado como `PixPayload`; `pointOfInitiation` está sempre presente e é tipado como `PixPointOfInitiation`, `"dynamic"` quando o payload traz uma localização de PSP ou quando o objeto "Point of Initiation Method" (`01`) é `"12"`, e `"static"` nos demais casos. As informações da conta do recebedor devem trazer exatamente uma chave ou uma `url` (verificada com a mesma regra de localização de PSP do `generatePixPayload`); o próprio `01` é informativo, então pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` retorna `null`. Quando um payload construído em torno de uma chave traz um valor, esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque: o §2.6 do manual do Pix coloca o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`), devolvido como `withdrawalFacilitator`, e `54` igual a `"0"` ou `"0.00"` é aceito junto dele. Rejeitar um valor zero sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. Quando o payload traz uma localização de PSP, o valor e o `txid` são ignorados, como o manual determina. Payloads cuja localização fica em um Unreserved Template (IDs 80 a 99, Pix Automático) estão fora de escopo e retornam `null`. ```javascript import { parsePixPayload } from '@brazilian-utils/brazilian-utils'; @@ -236,13 +236,14 @@ parsePixPayload( // { // key: '123e4567-e12b-12d1-a456-426655440000', // merchantName: 'Fulano de Tal', -// merchantCity: 'BRASILIA' +// merchantCity: 'BRASILIA', +// pointOfInitiation: 'static' // } ``` ## generatePixPayload -Gera o payload de um BR Code Pix. Exatamente um entre `params.key` e `params.url` deve ser informado (parte de `GeneratePixPayloadParams`); `null` é retornado quando ambos ou nenhum são informados. `url` deve ser uma localização de PSP como o manual do Bacen define: um host com caminho, sem esquema (`pix.example.com/qr/v2/1234`); um payload dinâmico não pode carregar `amount` nem `txid`, que pertencem à localização do PSP, e um `amount` que arredonda para `0.00` é rejeitado. +Gera o payload de um BR Code Pix. Exatamente um entre `params.key` e `params.url` deve ser informado (parte de `GeneratePixPayloadParams`); `null` é retornado quando ambos ou nenhum são informados. `url` deve ser uma localização de PSP como o manual do Bacen define: um host com caminho, sem esquema (`pix.example.com/qr/v2/1234`); um payload dinâmico não pode carregar `amount` nem `txid`, que pertencem à localização do PSP. O valor é escrito com as duas casas decimais que o BR Code aceita, então tanto um que arredonda para `0.00` quanto um que não sobrevive a esse round-trip (`0.005`, `123.456`) são rejeitados, em vez de escritos como uma quantia diferente. O BR Code de Pix Saque, que anuncia o `fss` do subobjeto 26-03, é interpretado pelo `parsePixPayload`, mas não é gerado aqui. Quando `params.key` é informado, ela é normalizada para a forma canônica do DICT pelo `parsePixKey` e o payload é estático. Quando `params.url` é informado no lugar (a localização do PSP, sem o esquema da URL, ex.: `"pix.example.com/qr/v2/1234"`), o payload é dinâmico conforme o Manual de Padrões para Iniciação do Pix: a URL ocupa o lugar da chave no template "Merchant Account Information" e o objeto "Point of Initiation Method" é definido como dinâmico (`12`); `params.url` pode ter no máximo 77 caracteres. `merchantName`, `merchantCity` e `description` são convertidos para ASCII imprimível (acentos removidos) e truncados ao que o BR Code permite. O `parsePixPayload` já interpreta os dois formatos, então `parsePixPayload(generatePixPayload({ url, ... }))` forma um round-trip. @@ -269,21 +270,25 @@ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // nu ## isValidNfeKey -Valida se uma chave de acesso de DF-e (Documento Fiscal eletrônico) é válida. Cobre todos os documentos que compartilham o mesmo layout de 44 dígitos: NF-e (modelo 55), NFC-e (modelo 65), CT-e (modelo 57), MDF-e (modelo 58) e CT-e OS (modelo 67, o Conhecimento de Transporte Eletrônico para Outros Serviços do [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/aj_009_07)). Aceita espaços entre os grupos de dígitos (a máscara de exibição usual) e o prefixo `NFe` encontrado no atributo `Id` do XML do documento. A forma de emissão (`tpEmis`) precisa ser um dos códigos atribuídos pelo MOC, de 1 a 7 ou 9; o 8 não é atribuído e torna a chave inválida. +Valida se uma chave de acesso de DF-e (Documento Fiscal eletrônico) é válida. Cobre todos os documentos cuja chave de acesso é a mesma string de 44 dígitos: NF-e (modelo 55), NFC-e (65), CT-e (57), MDF-e (58), CT-e OS (67, o Conhecimento de Transporte Eletrônico para Outros Serviços do [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), GTV-e (64, o CT-e Guia de Transporte de Valores), BP-e (63), NF3e (66) e NFCom (62). O CF-e-SAT (59) fica de fora: sua "chave de consulta" de 44 posições é composta de outro jeito. Aceita espaços entre os grupos de dígitos (a máscara de exibição usual) e os prefixos `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` e `NFCom` encontrados no atributo `Id` do XML do documento. + +A forma de emissão (`tpEmis`) é conferida contra os códigos que o MOC daquele modelo atribui, então o conjunto aceito muda com o modelo: de 1 a 7 e 9 para NF-e e NFC-e, `{1, 3, 4, 5, 7, 8}` para o CT-e, `{1, 5, 7, 8}` para o CT-e OS, `{1, 2, 7, 8}` para a GTV-e, `{1, 2, 3}` para o MDF-e e `{1, 2}` para o BP-e, a NF3e e a NFCom. O código 8, a autorização pela SVC-SP, é atribuído somente pelo [MOC do CT-e 4.00](https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais), nunca pelo da NF-e; os domínios do [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), da [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) e da [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) vêm dos manuais deles. Para NF-e e NFC-e o código numérico também é conferido contra a regra B03-10 do MOC da NF-e, que proíbe os vinte valores repetidos e sequenciais de `cNF` que ela lista e um `cNF` igual ao número do documento. Já rejeitar um número de documento todo zerado é uma escolha desta biblioteca: nenhuma regra de MOC foi encontrada proibindo isso. ```javascript import { isValidNfeKey } from '@brazilian-utils/brazilian-utils'; isValidNfeKey('35170458716523000119550010000000121000123458'); // true (NF-e, SP) isValidNfeKey('NFe35170458716523000119550010000000121000123458'); // true (prefixo Id do XML) +isValidNfeKey('CTe35170458716523000119570010000000128000123452'); // true (CT-e autorizado pela SVC-SP) isValidNfeKey('3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458'); // true (com máscara) isValidNfeKey('99170458716523000119550010000000121000123458'); // false (cUF inválido) -isValidNfeKey('35170458716523000119550010000000128000123455'); // false (tpEmis 8 não é atribuído) +isValidNfeKey('35170458716523000119550010000000128000123455'); // false (o MOC da NF-e não atribui tpEmis 8) +isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 00000000, regra B03-10) ``` ## formatNfeKey -Formata uma chave de acesso de DF-e (NF-e, NFC-e, CT-e, MDF-e ou CT-e OS) em grupos de 4 dígitos separados por espaço, a forma de exibição usual impressa na DANFE. +Formata uma chave de acesso de DF-e (Documento Fiscal eletrônico) em grupos de 4 dígitos separados por espaço, a forma em que todo documento auxiliar a imprime: o DANFE da NF-e e da NFC-e, o DACTE do CT-e, do CT-e OS e da GTV-e, o DAMDFE do MDF-e, o DABPE do BP-e, o DANF3E da NF3e e o DANFE-COM da NFCom. ```javascript import { formatNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -294,7 +299,7 @@ formatNfeKey('35170458716523000119550010000000121000123458'); ## parseNfeKey -Interpreta uma chave de acesso de DF-e e retorna seus campos (state, year, month, taxId, model, series, number, emissionType, code, checkDigit). Aceita as mesmas formas de entrada do `isValidNfeKey` e retorna `null` quando a chave não é válida. O resultado é tipado como `NfeKey`. +Interpreta uma chave de acesso de DF-e e retorna seus campos (state, year, month, taxId, model, series, number, emissionType, code, checkDigit). Aceita as mesmas formas de entrada do `isValidNfeKey` e retorna `null` quando a chave não é válida. O resultado é tipado como `NfeKey`, cujo `model` é um `NfeKeyModel`. A NFCom (`'62'`) e a NF3e (`'66'`) gastam a posição 36 da chave com o `nSiteAutoriz`, o site do autorizador que recebeu o documento, então para esses dois modelos o resultado também traz `authorizationSite` e o `code` tem 7 dígitos em vez de 8. ```javascript import { parseNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -303,6 +308,10 @@ parseNfeKey('35170458716523000119550010000000121000123458'); // { state: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '55', // series: 1, number: 12, emissionType: 1, code: '00012345', checkDigit: 8 } +parseNfeKey('35170458716523000119620010000000121000123450'); +// { state: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', +// series: 1, number: 12, emissionType: 1, authorizationSite: 0, code: '0012345', checkDigit: 0 } + parseNfeKey('invalid'); // null ``` @@ -332,7 +341,7 @@ isValidPhone('11900000000', { accept: [] }); // false ## formatPhone -Formata número de telefone de acordo com padrões brasileiros. `options.mask` (tipado como `PhoneMask`) aceita `"sn"` (padrão, apenas o número assinante, 9 dígitos, sem DDD), `"nanp"` (DDD + número assinante, 11 dígitos), `"e164"` (`"+5511987654321"`), `"international"` (`"+55 11 98765-4321"`, a forma como um número brasileiro é exibido para quem liga do exterior), `"service"` (`"0800 123 4567"` ou `"4004-1234"`, os agrupamentos convencionais para números de serviço) ou `"auto"`. O `"auto"` usa `"international"` quando `value` traz um código de país brasileiro (`+55`, `0055` ou um `55` seguido de 10 ou 11 dígitos), `"service"` quando `value` é um número de serviço e, nos demais casos, decide pela quantidade de dígitos: `"nanp"` quando `value` tem mais dígitos que um número assinante isolado, `"sn"` quando não tem. `"e164"` e `"international"` removem antes o código de país (regra documentada em `parsePhone`) e recaem para a apresentação `"service"` no caso de um número de serviço, já que esses não têm forma E.164. Se `value` incluir o DDD, informe `{ mask: 'auto' }` (ou `'nanp'`) explicitamente, já que a máscara padrão `"sn"` assume que não há DDD e trunca silenciosamente um DDD presente. +Formata número de telefone de acordo com padrões brasileiros. `options.mask` (tipado como `PhoneMask`) aceita `"sn"` (padrão, apenas o número assinante, 9 dígitos, sem DDD), `"nanp"` (DDD + número assinante, `"(00) 00000-0000"` para os 11 dígitos de um celular e `"(00) 0000-0000"` para os 10 dígitos de um fixo, mantendo o agrupamento de 11 dígitos em qualquer outro tamanho), `"e164"` (`"+5511987654321"`), `"international"` (`"+55 11 98765-4321"`, a forma como um número brasileiro é exibido para quem liga do exterior), `"service"` (`"0800 123 4567"` ou `"4004-1234"`, os agrupamentos convencionais para números de serviço) ou `"auto"`. O `"auto"` usa `"international"` quando `value` traz um código de país brasileiro (`+55`, `0055` ou um `55` seguido de 10 ou 11 dígitos), `"service"` quando `value` é um número de serviço e, nos demais casos, decide pela quantidade de dígitos: `"nanp"` quando `value` tem mais dígitos que um número assinante isolado, `"sn"` quando não tem. `"e164"` e `"international"` removem antes o código de país (regra documentada em `parsePhone`) e recaem para a apresentação `"service"` no caso de um número de serviço, já que esses não têm forma E.164. Se `value` incluir o DDD, informe `{ mask: 'auto' }` (ou `'nanp'`) explicitamente, já que a máscara padrão `"sn"` assume que não há DDD e trunca silenciosamente um DDD presente. Uma `mask` fora da união recai para o padrão `"sn"` em vez de lançar erro. ```javascript import { formatPhone } from '@brazilian-utils/brazilian-utils'; @@ -340,6 +349,8 @@ import { formatPhone } from '@brazilian-utils/brazilian-utils'; formatPhone('987654321'); // 98765-4321 (padrão "sn", sem DDD) formatPhone('11900000000', { mask: 'nanp' }); // (11) 90000-0000 formatPhone('11900000000', { mask: 'auto' }); // (11) 90000-0000 +formatPhone('1130000000', { mask: 'nanp' }); // (11) 3000-0000 (fixo de 10 dígitos) +formatPhone('1130000000', { mask: 'auto' }); // (11) 3000-0000 (fixo de 10 dígitos) formatPhone('11987654321', { mask: 'e164' }); // +5511987654321 formatPhone('+5511987654321', { mask: 'international' }); // +55 11 98765-4321 formatPhone('08001234567', { mask: 'service' }); // 0800 123 4567 @@ -363,7 +374,7 @@ parsePhone('55987654321'); // 55987654321 (DDD 55, não confundido com o código ## isValidMobilePhone -Valida se o número de telefone celular é válido. `options.version` (tipado como `PhoneVersion`) controla qual regra de numeração celular é aplicada: `1` (padrão) é o formato anterior à Resolução Anatel 749/2022, mantido por compatibilidade com a 2.3.0, cujo primeiro dígito do número (após o DDD) pode ser 6, 7, 8 ou 9; `2` exige apenas 9, um subconjunto mais restrito do art. 12 I da resolução (Serviço Móvel Pessoal). +Valida se o número de telefone celular é válido. `options.version` (tipado como `PhoneVersion`) controla qual regra de numeração celular é aplicada: `1` (padrão) é o formato anterior à Resolução Anatel 749/2022, mantido por compatibilidade com a 2.3.0, cujo primeiro dígito do número (após o DDD) pode ser 6, 7, 8 ou 9; `2` exige apenas 9, um subconjunto mais restrito do art. 12 I da resolução (Serviço Móvel Pessoal). A versão `1` também não exclui o prefixo `700`, que o art. 12 II reserva ao Serviço Móvel Global por Satélite e não ao SMP, então `isValidMobilePhone('11700123456')` é `true` para um número fora do SMP; a versão `2` o rejeita. ```javascript import { isValidMobilePhone } from '@brazilian-utils/brazilian-utils'; @@ -385,7 +396,7 @@ isValidLandlinePhone('1130000000'); // true ## isValidServicePhone -Valida se um número de telefone é um número de serviço brasileiro válido, discado sem DDD: os Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` e `0900` (11 dígitos no total), os números abreviados `300X`/`400X` (8 dígitos), e os códigos de 3 dígitos dos Códigos de Acesso a Serviços de Utilidade Pública designados pela Anatel (ex.: `190`, `192`; `112` e `911` também são aceitos, como aliases exclusivos de celular do `190` que a Anatel lista junto aos demais códigos de 3 dígitos). Apenas a estrutura é verificada, o número não precisa estar atribuído a ninguém. +Valida se um número de telefone é um número de serviço brasileiro válido, discado sem DDD: os Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` e `0900` (11 dígitos no total), os números abreviados `300X`/`400X` (8 dígitos), e os códigos de 3 dígitos dos Códigos de Acesso a Serviços de Utilidade Pública designados pela Anatel (ex.: `190`, `192`; `112` e `911` também são aceitos, como aliases exclusivos de celular do `190` que a Anatel lista junto aos demais códigos de 3 dígitos). Apenas a estrutura é verificada, o número não precisa estar atribuído a ninguém. A Anatel não publica alocação para os números abreviados, então apenas as raízes convencionais `300X` e `400X` são reconhecidas: outros prefixos de "Número Único" usados no mercado, como `4020` e `4062`, estão fora de escopo e são rejeitados. ```javascript import { isValidServicePhone } from '@brazilian-utils/brazilian-utils'; @@ -453,14 +464,17 @@ isValidLicensePlate('ABC1234EXTRA'); // false (caracteres em excesso) ## isValidRenavam -Valida se o RENAVAM (Registro Nacional de Veículos Automotores) é válido. Suporta tanto o formato antigo (9 dígitos) quanto o novo formato (11 dígitos). +Valida se o RENAVAM (Registro Nacional de Veículos Automotores) é válido. Suporta tanto o formato antigo (9 dígitos) quanto o novo formato (11 dígitos). Espaços, pontos e hífens ao redor/entre os dígitos são ignorados, mas qualquer outro caractere, uma letra em especial, invalida o valor. Um registro com todos os dígitos iguais também é rejeitado. ```javascript import { isValidRenavam } from '@brazilian-utils/brazilian-utils'; isValidRenavam('639884962'); // true (9 dígitos, formato antigo) isValidRenavam('00639884962'); // true (11 dígitos, formato novo) +isValidRenavam('0063988.4962'); // true (pontos e hífens são ignorados) isValidRenavam('12345678901'); // false (checksum inválido) +isValidRenavam('00000000000'); // false (dígitos repetidos) +isValidRenavam('ab00639884962'); // false (letras são rejeitadas) ``` ## isValidPis @@ -475,7 +489,7 @@ isValidPis('12056412547'); // false ## formatPis -Formata número de PIS. +Formata número de PIS. `options.pad` (parte de `FormatPisOptions`) completa o valor com zeros à esquerda até os 11 dígitos antes de aplicar a máscara. ```javascript import { formatPis } from '@brazilian-utils/brazilian-utils'; @@ -496,12 +510,13 @@ parsePis('123.45678.90-1'); // 12345678901 ## formatCep -Formata o CEP. +Formata o CEP ([código de endereçamento postal](https://pt.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (parte de `FormatCepOptions`) completa o valor com zeros à esquerda até os 8 dígitos antes de aplicar a máscara. ```javascript import { formatCep } from '@brazilian-utils/brazilian-utils'; formatCep('92500000'); // 92500-000 +formatCep('9250000', { pad: true }); // 09250-000 ``` ## parseCep @@ -516,7 +531,7 @@ parseCep('92500-000'); // 92500000 ## getAddressInfoByCep -Busca informações de endereço para um CEP usando múltiplos provedores. O padrão é `['viacep', 'brasilapi']`. O provedor `'widenet'` está descontinuado (seu endpoint não responde mais) e foi excluído da lista padrão, mas ainda pode ser solicitado explicitamente via `options.providers` (tipado como `CepProvider[]`). O endereço retornado é tipado como `AddressInfo`. +Busca informações de endereço para um CEP usando múltiplos provedores. O padrão é `['viacep', 'brasilapi']`. O provedor `'widenet'` está descontinuado (seu endpoint não responde mais) e foi excluído da lista padrão, mas ainda pode ser solicitado explicitamente via `options.providers` (tipado como `CepProvider[]`). O endereço retornado é tipado como `AddressInfo`. Uma falha transitória de rede é repetida duas vezes por provedor, com backoff linear de 250 ms (250 ms e depois 500 ms), então um provedor que continua falhando é tentado até 3 vezes e acrescenta cerca de 750 ms antes de o próximo provedor ser consultado; um status de erro HTTP ou uma falha não recuperável não é repetida. ```javascript import { getAddressInfoByCep } from '@brazilian-utils/brazilian-utils'; @@ -536,22 +551,25 @@ const address = await getAddressInfoByCep(1310100); ## isValidProcessoJuridico -Valida o número do processo jurídico de acordo com definição do [CNJ](https://atos.cnj.jus.br/atos/detalhar/119). +Valida o número do processo jurídico de acordo com definição do [CNJ](https://atos.cnj.jus.br/atos/detalhar/119). Os separadores da máscara do CNJ (espaços, `.` e `-`) são aceitos entre os campos `NNNNNNN-DD.AAAA.J.TR.OOOO`, mas qualquer outro caractere, uma letra em especial, invalida o valor. ```javascript import { isValidProcessoJuridico } from '@brazilian-utils/brazilian-utils'; isValidProcessoJuridico('00020802520125150049'); // true +isValidProcessoJuridico('0002080-25.2012.5.15.0049'); // true (máscara do CNJ) +isValidProcessoJuridico('ab00020802520125150049'); // false (letras são rejeitadas) ``` ## formatProcessoJuridico -Formata um número no formato definido pelo [CNJ](https://atos.cnj.jus.br/atos/detalhar/119) (máscara `NNNNNNN-DD.AAAA.J.TR.OOOO`). +Formata um número no formato definido pelo [CNJ](https://atos.cnj.jus.br/atos/detalhar/119) (máscara `NNNNNNN-DD.AAAA.J.TR.OOOO`). `options.pad` (parte de `FormatProcessoJuridicoOptions`) completa o valor com zeros à esquerda até os 20 dígitos antes de aplicar a máscara. ```javascript import { formatProcessoJuridico } from '@brazilian-utils/brazilian-utils'; formatProcessoJuridico('00020802520125150049'); // 0002080-25.2012.5.15.0049 +formatProcessoJuridico('20802520125150049', { pad: true }); // 0002080-25.2012.5.15.0049 ``` ## parseProcessoJuridico @@ -566,7 +584,7 @@ parseProcessoJuridico('0002080-25.2012.5.15.0049'); // 00020802520125150049 ## isValidIe -Valida se a inscrição estadual de um estado é válida. A UF é case-insensitive. Regras notáveis por estado: GO aceita os prefixos `10`, `11` e `15`; PA aceita `15` e `75`-`79`; MS aceita `28` e `50`; SP tem o padrão de produtor rural `P0MMMSSSSD000`; TO usa códigos de tipo de 11 dígitos (`01`, `02`, `03`, `99`). +Valida se a inscrição estadual de um estado é válida. A UF é case-insensitive. Regras notáveis por estado: GO aceita os prefixos `10`, `11` e `15`; PA aceita `15` e `75`-`79`; MS aceita `28` e `50`; SP tem o padrão de produtor rural `P0MMMSSSSD000`; TO usa códigos de tipo de 11 dígitos (`01`, `02`, `03`, `99`). O TO também aceita uma forma de 9 dígitos, aplicando a mesma regra módulo 11 sobre os oito primeiros dígitos; a página do SINTEGRA documenta apenas a de 11 dígitos, então essa forma é comportamento da 2.3.0 mantido por compatibilidade, e não regra publicada. Uma inscrição só de zeros é aceita em todo estado cuja fórmula publicada produz dígito verificador 0 para ela (AM, BA com 8 ou 9 dígitos, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, SC, SE, SP e TO com 9 dígitos), diferente de `isValidCpf` e `isValidCnpj`, que rejeitam dígitos repetidos. ```javascript import { isValidIe } from '@brazilian-utils/brazilian-utils'; @@ -587,7 +605,7 @@ Bancos validados pelo algoritmo de dígito verificador publicado: | Santander | `033` | 4 dígitos | 8 dígitos | pesos `9,7,3,1,0,0,9,7,1,3,1,9,7,3` sobre agência + `"00"` + conta, desprezando as dezenas | | Banrisul | `041` | 4 dígitos | 9 dígitos | pesos `3,2,4,7,6,5,4,3,2`; resto 0 gera `0` e resto 1 gera `6`; `account` é tipo (2 dígitos) + conta (7 dígitos) | | Caixa Econômica Federal | `104` | 4 dígitos | 11 dígitos | mod11 sobre agência + conta; `account` é operação (3 dígitos) + conta (8 dígitos) | -| Bradesco | `237` | 4 dígitos | 7 dígitos | mod11 com pesos 2..7; `digit` pode ser `"P"` (geralmente exibido como `"0"`) | +| Bradesco | `237` | 4 dígitos | 7 dígitos | mod11 com pesos 2..7; resto 0 gera `0` e resto 1 gera `"P"` | | Nubank | `260` | 4 dígitos | 5-13 dígitos | dígito de Verhoeff sobre a conta, ignorando zeros à esquerda | | Itaú Unibanco | `341` | 4 dígitos | 5 dígitos | mod10 sobre agência + conta | | HSBC / Kirton Bank | `399` | 4 dígitos | 6 dígitos | pesos `8,9,2,3,4,5,6,7,8,9` sobre agência + conta; resto 10 gera `0` | @@ -597,17 +615,15 @@ Bancos validados apenas pela estrutura, por não publicarem regra de dígito ver | Banco | Código | | Banco | Código | | --- | --- | --- | --- | --- | -| Inter | `077` | | PicPay | `380` | -| Ailos | `085` | | Cora | `403` | -| XP | `102` | | Pan | `623` | -| Unicred | `136` | | BV | `655` | -| Stone | `197` | | Daycoval | `707` | -| BTG Pactual | `208` | | Modal | `746` | -| Original | `212` | | Sicredi | `748` | -| PagBank | `290` | | Sicoob | `756` | -| BMG | `318` | | | | -| Mercado Pago | `323` | | | | -| C6 | `336` | | | | +| Inter | `077` | | Mercado Pago | `323` | +| Ailos | `085` | | C6 | `336` | +| XP | `102` | | PicPay | `380` | +| Unicred | `136` | | Cora | `403` | +| Stone | `197` | | Pan | `623` | +| BTG Pactual | `208` | | BV | `655` | +| Original | `212` | | Daycoval | `707` | +| PagBank | `290` | | Sicredi | `748` | +| BMG | `318` | | Sicoob | `756` | Quando `digit` tem 2 caracteres, o fallback genérico encadeia mod10 seguido de mod11 sobre a conta, do mesmo jeito que os dígitos de CPF/CNPJ são encadeados. @@ -722,7 +738,7 @@ getBankByIspb('99999999'); // null ## isValidIban -Valida se um IBAN (International Bank Account Number) brasileiro é válido, conforme as [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf) do Bacen (Circular BCB nº 3.625/2013): `BR` + 2 dígitos verificadores ISO 7064 MOD 97-10 + 8 dígitos de ISPB + 5 dígitos de agência + 10 dígitos de conta + 1 letra de tipo de conta (qualquer letra, normalmente `C` para conta corrente ou `P` para conta poupança) + 1 caractere alfanumérico de titularidade, totalizando 29 caracteres. Somente IBANs brasileiros (código de país `BR`) são reconhecidos; qualquer outro país retorna `false`, já que este pacote não conhece o layout de campos dos outros mais de 90 países da ISO 13616. Aceita os espaços de agrupamento usuais e não diferencia maiúsculas de minúsculas. O valor precisa estar escrito no formato impresso da ISO 13616: letras e dígitos em grupos separados por um único espaço, com espaços em branco opcionais no início e no fim. Qualquer outro caractere faz do valor algo que não é um IBAN, então ele é rejeitado em vez de removido. +Valida se um IBAN (International Bank Account Number) brasileiro é válido, conforme as [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf) do Bacen (Circular BCB nº 3.625/2013): `BR` + 2 dígitos verificadores ISO 7064 MOD 97-10 + 8 dígitos de ISPB + 5 dígitos de agência + 10 dígitos de conta + 1 letra de tipo de conta (qualquer letra, normalmente `C` para conta corrente ou `P` para conta poupança) + 1 indicador de titularidade (`1` para o primeiro ou único titular até `9` para o nono, depois `A` a `Z` a partir do décimo, então `0` é rejeitado), totalizando 29 caracteres. Somente IBANs brasileiros (código de país `BR`) são reconhecidos; qualquer outro país retorna `false`, já que este pacote não conhece o layout de campos dos outros mais de 90 países da ISO 13616. Não diferencia maiúsculas de minúsculas e aceita as duas formas em que um IBAN é escrito: compacta (`'BR1500000000000010932840814P2'`) ou no formato impresso da ISO 13616, letras e dígitos em grupos separados por um único espaço, em ambos os casos com espaços em branco opcionais no início e no fim. Apenas um caractere fora de letras e dígitos, ou um separador diferente de um único espaço, faz do valor algo que não é um IBAN, então ele é rejeitado em vez de removido. ```javascript import { isValidIban } from '@brazilian-utils/brazilian-utils'; @@ -736,7 +752,7 @@ isValidIban('DE89370400440532013000'); // false (IBAN não brasileiro) ## formatIban -Formata um IBAN brasileiro agrupando-o em blocos de 4 caracteres, a apresentação "impressa" da ISO 13616 usada em extratos e formulários bancários. Não valida os dígitos verificadores nem o layout dos campos; formata o que for passado, até o limite de 29 caracteres de um IBAN brasileiro, até onde for possível, então a função também pode ser usada como máscara de digitação. Use `isValidIban` para verificar a validade. O valor ainda precisa estar escrito no formato impresso da ISO 13616 (letras e dígitos em grupos separados por um único espaço, com espaços em branco opcionais no início e no fim); qualquer outro caractere resulta em uma string vazia, em vez de ser descartado silenciosamente. +Formata um IBAN no agrupamento impresso da ISO 13616, blocos de 4 caracteres, a apresentação usada em extratos e formulários bancários. Não valida os dígitos verificadores nem o layout dos campos; formata o que for passado, até o limite de 29 caracteres de um IBAN brasileiro, até onde for possível, então a função também pode ser usada como máscara de digitação, e um IBAN de outro país é agrupado do mesmo jeito até esse limite. Use `isValidIban` para verificar a validade. O valor pode ser compacto (`'BR1500000000000010932840814P2'`), já estar no formato impresso da ISO 13616 (letras e dígitos em grupos separados por um único espaço) ou ser um valor parcial ainda sendo digitado (`'BR15'`), em todos os casos com espaços em branco opcionais no início e no fim; apenas um caractere fora de letras e dígitos, ou um separador diferente de um único espaço, resulta em uma string vazia, em vez de ser descartado silenciosamente. ```javascript import { formatIban } from '@brazilian-utils/brazilian-utils'; @@ -749,7 +765,7 @@ formatIban('BR1500000000000010932840814P-2'); // '' (hífen não faz parte de um ## parseIban -Interpreta um IBAN brasileiro em seus campos: 2 (código do país, sempre `BR`) + 2 (dígitos verificadores ISO 7064 MOD 97-10) + 8 (ISPB) + 5 (agência) + 10 (conta) + 1 (tipo de conta, qualquer letra, normalmente `C` para conta corrente ou `P` para conta poupança) + 1 (indicador do titular). Aceita as mesmas formas de entrada que `isValidIban` (espaços de agrupamento, minúsculas) e retorna `null` sempre que `isValidIban` retornaria `false`, inclusive quando o valor carrega qualquer caractere além de letras, dígitos e os espaços de agrupamento do formato impresso. O resultado é tipado como `Iban`, cujo `accountType` é uma `string`. +Interpreta um IBAN brasileiro em seus campos: 2 (código do país, sempre `BR`) + 2 (dígitos verificadores ISO 7064 MOD 97-10) + 8 (ISPB) + 5 (agência) + 10 (conta) + 1 (tipo de conta, qualquer letra, normalmente `C` para conta corrente ou `P` para conta poupança) + 1 (indicador do titular, `1` a `9` e depois `A` a `Z`). Aceita as mesmas formas de entrada que `isValidIban`, compacta ou no formato impresso da ISO 13616 (grupos separados por um único espaço), em ambos os casos com espaços em branco opcionais no início e no fim e sem diferenciar maiúsculas de minúsculas, e retorna `null` sempre que `isValidIban` retornaria `false`, inclusive quando o valor carrega qualquer caractere além de letras, dígitos e esses espaços de agrupamento. O resultado é tipado como `Iban`, cujo `accountType` é uma `string`. ```javascript import { parseIban } from '@brazilian-utils/brazilian-utils'; @@ -771,7 +787,7 @@ parseIban('BR1500000000000010932840814P-2'); // null (hífen não faz parte de u ## isValidCreditCard -Valida se um número de cartão de pagamento é válido usando o algoritmo de Luhn ([ISO/IEC 7812-1](https://www.iso.org/standard/70484.html)). Aceita os caracteres de máscara usuais (espaços, hifens) entre os dígitos. Não faz detecção de bandeira (Visa, Mastercard, Amex...), consulta de faixa de emissor nem validação de validade/CVV, verifica apenas a quantidade de dígitos (12 a 19) e o dígito verificador de Luhn. Um `number` só é aceito quando é um inteiro seguro não negativo: qualquer valor acima de `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 dígitos) já chega arredondado para outro número, então passe cartões mais longos como string. +Valida se um número de cartão de pagamento é válido usando o algoritmo de Luhn ([ISO/IEC 7812-1](https://www.iso.org/standard/70484.html)). Aceita os caracteres de máscara usuais (espaços, hifens) entre os dígitos e espaços ao redor do valor; qualquer outro caractere invalida o valor. Não faz detecção de bandeira (Visa, Mastercard, Amex...), consulta de faixa de emissor nem validação de validade/CVV, verifica apenas a quantidade de dígitos (12 a 19) e o dígito verificador de Luhn. Um `number` só é aceito quando é um inteiro seguro não negativo: qualquer valor acima de `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 dígitos) já chega arredondado para outro número, então passe cartões mais longos como string. ```javascript import { isValidCreditCard } from '@brazilian-utils/brazilian-utils'; @@ -781,29 +797,42 @@ isValidCreditCard('5555555555554444'); // true (número de teste Mastercard) isValidCreditCard('378282246310005'); // true (número de teste American Express) isValidCreditCard('4111 1111 1111 1111'); // true (máscara com espaços) isValidCreditCard('4111111111111112'); // false (dígito verificador inválido) +isValidCreditCard('4111a1111b1111c1111'); // false (letras entre os dígitos) isValidCreditCard(4111111111111111111); // false (acima de 2^53 - 1, passe como string) ``` ## capitalize -Transforma primeira letra de cada palavra em maiúscula ignorando preposições. As palavras são separadas por espaço em branco, por `-` e por `/`, então `'MOGI-GUAÇU'` vira `'Mogi-Guaçu'` e `'SANTANA/RS'` vira `'Santana/Rs'`. Toda sequência de espaços em branco (tabs, quebras de linha, espaços repetidos) vira um único espaço, e o espaço no início e no fim é descartado. `options.upperCaseWords` tem como padrão `[]`, ou seja, nenhuma sigla é colocada em maiúsculas a menos que você a liste, e a comparação com `upperCaseWords` e `lowerCaseWords` é case-insensitive (locale pt-BR). As opções são tipadas como `CapitalizeOptions`. +Transforma a primeira letra de cada palavra em maiúscula do jeito que se escreve um nome, uma razão social ou um endereço brasileiro, sem precisar de opções. As palavras são separadas por espaço em branco, por `-` e por `/`, então `'MOGI-GUAÇU'` vira `'Mogi-Guaçu'`. Toda sequência de espaços em branco (tabs, quebras de linha, espaços repetidos) vira um único espaço, e o espaço no início e no fim é descartado. + +`options.lowerCaseWords` tem como padrão as preposições, artigos e conjunções que permanecem em minúsculas dentro de um nome próprio (`de`, `da`, `do`, `e`, ...), exceto quando uma delas é a primeira palavra. `options.upperCaseWords` tem como padrão as designações societárias e as abreviações de documentos escritas em maiúsculas no uso brasileiro (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) mais os algarismos romanos que aparecem em nomes e endereços (de `II` a `XXIII`, exceto `VI`, que colide com a forma verbal "vi"). `SA` sem pontuação ficou de fora de propósito, por ser indistinguível do sobrenome "Sá" digitado sem o acento, enquanto `ME` casa também com o pronome "me" (`'diga-me'` vira `'Diga-ME'`), então informe o seu próprio `upperCaseWords` quando a entrada for texto livre em vez de um nome. `S/A` e `S/S` são reconhecidos mesmo com a barra no meio, embora a barra separe palavras. Uma palavra de duas letras logo depois de uma `/` vira maiúscula quando é a sigla de um estado brasileiro (`'porto alegre/rs'` vira `'Porto Alegre/RS'`); essa regra é estrutural e continua valendo mesmo com `upperCaseWords` informado, enquanto uma sigla de estado que não venha depois de uma `/` é deixada como está. + +Qualquer uma das listas informada em `options` substitui inteiramente a lista padrão correspondente, e a comparação com as duas é case-insensitive (locale pt-BR). As opções são tipadas como `CapitalizeOptions`. ```javascript import { capitalize } from '@brazilian-utils/brazilian-utils'; -capitalize('josé e maria'); // José e Maria +capitalize('jose da silva'); // Jose da Silva +capitalize('JOSÉ DA SILVA'); // José da Silva +capitalize('empresa ltda'); // Empresa LTDA +capitalize('banco do brasil s.a.'); // Banco do Brasil S.A. +capitalize('casa de carnes s/a'); // Casa de Carnes S/A ("S/A" é reconhecido com a barra no meio) +capitalize('mogi-guaçu'); // Mogi-Guaçu ("-" inicia uma nova palavra) +capitalize('santana/rs'); // Santana/RS ("RS" é sigla de estado logo depois de uma "/") +capitalize('porto alegre/rs'); // Porto Alegre/RS +capitalize('santana rs'); // Santana Rs (sem "/", "rs" é só uma palavra) +capitalize('rua xv de novembro'); // Rua XV de Novembro (algarismo romano, "de" fica em minúsculas) +capitalize('joão paulo ii'); // João Paulo II +capitalize('de'); // De (uma preposição mantém a maiúscula quando é a primeira palavra) +capitalize('empresa ltda', { upperCaseWords: [] }); // Empresa Ltda (a lista informada substitui a padrão) capitalize('josé Ama MARIA', { lowerCaseWords: ['ama'] }); // José ama Maria -capitalize('doc inválido', { upperCaseWords: ['DOC'] }); // DOC Inválido -capitalize('MOGI-GUAÇU'); // Mogi-Guaçu ("-" inicia uma nova palavra) -capitalize('SANTANA/RS', { upperCaseWords: ['RS'] }); // Santana/RS ("/" inicia uma nova palavra, então "RS" corresponde) -capitalize('empresa ltda'); // Empresa Ltda (sem siglas padrão) -capitalize('empresa ltda', { upperCaseWords: ['LTDA'] }); // Empresa LTDA (comparação case-insensitive) +capitalize('doc inválido', { upperCaseWords: ['DOC'] }); // DOC Inválido (comparação case-insensitive) capitalize(' josé maria '); // José Maria (toda sequência de espaço em branco, tabs e quebras de linha inclusive, vira um único espaço) ``` ## formatCurrency -Formata um número inteiro ou float para uma string no padrão BRL. Um `number` é formatado como está (sinal e decimais preservados). Uma entrada em `string` é lida pela mesma regra do `parseCurrency`, com a diferença de que um valor escrito sem nenhum separador permanece em unidades inteiras: o último `,` ou `.` seguido de 1 ou 2 dígitos é o separador decimal, todo outro `,` ou `.` é separador de milhar, e um `-` escrito antes do primeiro dígito é preservado. Assim `'1.234,56'` vira `1.234,56`, `'-10.5'` vira `-10,50` e `'1234'` vira `1.234,00`. `precision` é limitado ao intervalo `0..20` (o aceito pelo `Intl.NumberFormat`) e o padrão é 2. Um valor que não seja um número finito (`NaN`, `Infinity`, `-Infinity`) vira string vazia. As opções são tipadas como `FormatCurrencyOptions`. +Formata um número inteiro ou float para uma string no padrão BRL. Um `number` é formatado como está (sinal e decimais preservados). Uma entrada em `string` é lida pela mesma regra do `parseCurrency`, com a diferença de que um valor escrito sem nenhum separador permanece em unidades inteiras: o último `,` ou `.` seguido de 1 ou 2 dígitos (ou de até `precision` dígitos, quando esse valor for maior) é o separador decimal, todo outro `,` ou `.` é separador de milhar, e um `-` escrito antes do primeiro dígito é preservado. Assim `'1.234,56'` vira `1.234,56`, `'-10.5'` vira `-10,50` e `'1234'` vira `1.234,00`. `precision` é limitado ao intervalo `0..20` (o aceito pelo `Intl.NumberFormat`), o padrão é 2 e volta a 2 quando não é um número finito. Um valor que não seja um número finito (`NaN`, `Infinity`, `-Infinity`) vira string vazia, e um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo) também; `null`, arrays e booleanos passam por `Number()` como no 2.3.0. As opções são tipadas como `FormatCurrencyOptions`. ```javascript import { formatCurrency } from '@brazilian-utils/brazilian-utils'; @@ -821,7 +850,7 @@ formatCurrency(Number.NaN); // "" (números não finitos viram string vazia) ## parseCurrency -Transforma uma string para o formato de inteiro ou float. O último `,` ou `.` seguido de 1 ou 2 dígitos (ou de até `precision` dígitos, quando esse valor for maior) é o separador decimal; todo outro `,` ou `.` é separador de milhar. Assim `'R$ 1.234,56'` vira `1234.56`, `'R$ 1.234'` vira `1234`, `'1,5'` vira `1.5` e `'12.34'` vira `12.34`. Um valor escrito sem nenhum separador mantém a convenção de centavos e é dividido por `10 ** precision`, então `'1234'` vira `12.34`. Um `-` escrito antes do primeiro dígito é preservado, então `'-R$ 1,00'` vira `-1`. `precision` (padrão 2, limitado a `0..20`) controla quantos dígitos são tratados como centavos. As opções são tipadas como `ParseCurrencyOptions`. +Transforma uma string para o formato de inteiro ou float. O último `,` ou `.` seguido de 1 ou 2 dígitos (ou de até `precision` dígitos, quando esse valor for maior) é o separador decimal; todo outro `,` ou `.` é separador de milhar. Assim `'R$ 1.234,56'` vira `1234.56`, `'R$ 1.234'` vira `1234`, `'1,5'` vira `1.5` e `'12.34'` vira `12.34`. Um valor escrito sem nenhum separador mantém a convenção de centavos e é dividido por `10 ** precision`, então `'1234'` vira `12.34`. Um `-` escrito antes do primeiro dígito é preservado, então `'-R$ 1,00'` vira `-1`. `precision` (padrão 2, limitado a `0..20`, e voltando a 2 quando não é um número finito) controla quantos dígitos são tratados como centavos. As opções são tipadas como `ParseCurrencyOptions`. ```javascript import { parseCurrency } from '@brazilian-utils/brazilian-utils'; @@ -839,7 +868,7 @@ parseCurrency(''); // 0 ## convertNumberToWords -Formata um número inteiro por extenso em português do Brasil, ex.: `1235` vira `"mil, duzentos e trinta e cinco"`. Só são suportados inteiros de `-999999999999999` a `999999999999999` (999 trilhões em valor absoluto); fora desse intervalo, `NaN` ou um valor não finito retornam `""`. Um `value` não inteiro é truncado em direção a zero antes da conversão. `options.gender` (parte de `ConvertNumberToWordsOptions`) concorda "um/dois" e a centena ("duzentos/duzentas" etc.) com o substantivo que o número qualifica, com padrão `"masculine"`. `options.case` define a caixa do resultado: `"lower"` (padrão, sem alteração), `"sentence"` (só a primeira letra em maiúscula) ou `"upper"` (tudo em maiúscula pelo locale "pt-BR", preservando os acentos, ex.: "três" -> "TRÊS"). Um valor inválido de `gender`/`case` é ignorado e o padrão é usado. +Formata um número inteiro por extenso em português do Brasil, ex.: `1235` vira `"mil, duzentos e trinta e cinco"`. Só são suportados inteiros de `-999999999999999` a `999999999999999` (999 trilhões em valor absoluto); fora desse intervalo, `NaN` ou um valor não finito retornam `""`. Um `value` não inteiro é truncado em direção a zero antes da conversão. `options.gender` (parte de `ConvertNumberToWordsOptions`) concorda "um/dois" e a centena ("duzentos/duzentas" etc.) com o substantivo que o número qualifica, com padrão `"masculine"`. Um valor inválido de `gender` é ignorado e o padrão é usado. O resultado sai sempre em minúsculas; aplique qualquer outra caixa por conta própria. ```javascript import { convertNumberToWords } from '@brazilian-utils/brazilian-utils'; @@ -849,13 +878,13 @@ convertNumberToWords(1001); // "mil e um" convertNumberToWords(2000000); // "dois milhões" convertNumberToWords(-42); // "menos quarenta e dois" convertNumberToWords(2, { gender: 'feminine' }); // "duas" -convertNumberToWords(3, { case: 'upper' }); // "TRÊS" +convertNumberToWords(12.9); // "doze" (truncado em direção a zero) convertNumberToWords(NaN); // "" ``` ## convertCurrencyToWords -Formata um valor monetário em Reais por extenso, no estilo usado para escrever o valor à mão em cheques e contratos, ex.: `1523.45` vira `"mil, quinhentos e vinte e três reais e quarenta e cinco centavos"`. O `value` é truncado (não arredondado) para 2 casas decimais. O substantivo no singular é usado para exatamente 1 ("um real", "um centavo") e "de" é inserido antes de "reais" quando o valor é um milhão, bilhão ou trilhão de reais redondo. Um valor que trunca para nada vira `"zero reais"`, sem o prefixo "menos"; qualquer outro valor negativo recebe o prefixo "menos", e uma entrada inválida retorna `""`. Acima de `Number.MAX_SAFE_INTEGER / 100` reais (cerca de 90 trilhões) um double não consegue carregar centavos, então o valor é lido como um número inteiro de reais. `options.case` (parte de `ConvertCurrencyToWordsOptions`) define a caixa do resultado: `"lower"` (padrão), `"sentence"` (só a primeira letra em maiúscula) ou `"upper"` (tudo em maiúscula, preservando os acentos). Um valor inválido de `case` é ignorado e `"lower"` é usado. +Formata um valor monetário em Reais por extenso, no estilo usado para escrever o valor à mão em cheques e contratos, ex.: `1523.45` vira `"mil, quinhentos e vinte e três reais e quarenta e cinco centavos"`. O `value` é truncado (não arredondado) para 2 casas decimais. O substantivo no singular é usado para exatamente 1 ("um real", "um centavo") e "de" é inserido antes de "reais" quando o valor é um milhão, bilhão ou trilhão de reais redondo. Um valor que trunca para nada vira `"zero reais"`, sem o prefixo "menos"; qualquer outro valor negativo recebe o prefixo "menos", e uma entrada inválida retorna `""`. Acima de `Number.MAX_SAFE_INTEGER / 100` reais (cerca de 90 trilhões) um double não consegue carregar centavos, então o valor é lido como um número inteiro de reais. Não recebe opções: o resultado sai sempre em minúsculas; aplique qualquer outra caixa por conta própria. ```javascript import { convertCurrencyToWords } from '@brazilian-utils/brazilian-utils'; @@ -867,7 +896,6 @@ convertCurrencyToWords(1000000); // "um milhão de reais" convertCurrencyToWords(0); // "zero reais" convertCurrencyToWords(-5.5); // "menos cinco reais e cinquenta centavos" convertCurrencyToWords(-0.001); // "zero reais" (trunca para nada) -convertCurrencyToWords(1000, { case: 'upper' }); // "MIL REAIS" ``` ## getStates @@ -969,7 +997,7 @@ getTimezoneByState('ZZ'); // null ## getCities -Retorna as cidades brasileiras. Retorna todas as cidades se nenhum estado for fornecido, ou cidades de um estado específico. Cada chamada retorna um array novo, então alterar o resultado nunca afeta chamadas seguintes. Um código de estado desconhecido (ou um valor que não seja `StateCode`) retorna um array vazio em vez de lançar erro. +Retorna as cidades brasileiras. Retorna todas as cidades se nenhum estado for fornecido, ou cidades de um estado específico. Cada chamada retorna um array novo, então alterar o resultado nunca afeta chamadas seguintes. Um código de estado desconhecido (ou um valor que não seja `StateCode`) retorna um array vazio em vez de lançar erro, exceto quando é um valor falsy: `getCities(null)` e `getCities('')` são lidos como "nenhum estado informado" e retornam todas as cidades, enquanto o mais estrito `getMunicipalities` retorna `[]` para eles. ```javascript import { getCities } from '@brazilian-utils/brazilian-utils'; @@ -1007,11 +1035,22 @@ getCities('SP'); // ] ``` -`getCities` embute os nomes dos 5571 municípios do IBGE (~153 KB minificado, ~49 KB com gzip) e é uma das poucas exceções pesadas neste pacote, que é tree-shakeable no restante. Veja [Tamanho do bundle](getting-started.md#tamanho-do-bundle) para saber como carregá-lo sob demanda via `@brazilian-utils/brazilian-utils/get-cities` em vez do import da raiz. +`getCities` embute os nomes dos 5571 municípios do IBGE (~153,6 KB minificado, ~49,4 KB com gzip) e é uma das poucas exceções pesadas neste pacote, que é tree-shakeable no restante. Veja [Tamanho do bundle](getting-started.md#tamanho-do-bundle) para saber como carregá-lo sob demanda via `@brazilian-utils/brazilian-utils/get-cities` em vez do import da raiz. ## getHolidays -Retorna feriados brasileiros para um determinado ano. Retorna feriados nacionais e opcionalmente feriados estaduais. Cada feriado (tipado como `Holiday`) tem um campo `type` (`HolidayType`: `"national"`, `"state"`, `"optional"` ou `"religious"`). O "Dia da Consciência Negra" (20 de novembro) é feriado nacional a partir de 2024 (Lei nº 14.759/2023). Antes disso, MT e RJ ainda trazem seu próprio feriado estadual chamado `"Consciência Negra"` na mesma data. Os resultados são memoizados por `year`/`stateCode`, mas cada chamada ainda retorna uma cópia nova. Um `stateCode` desconhecido/inválido é ignorado, retornando apenas os feriados nacionais. +Retorna feriados brasileiros para um determinado ano. Retorna feriados nacionais e opcionalmente feriados estaduais. Cada feriado (tipado como `Holiday`) tem um campo `type` (`HolidayType`: `"national"`, `"state"`, `"optional"` ou `"religious"`). O "Dia da Consciência Negra" (20 de novembro) é feriado nacional a partir de 2024 (Lei nº 14.759/2023). Antes disso, MT e RJ ainda trazem seu próprio feriado estadual chamado `"Consciência Negra"` na mesma data. Os resultados são memoizados por `year`/`stateCode`, mas cada chamada ainda retorna uma cópia nova. Um `stateCode` desconhecido/inválido é ignorado, retornando apenas os feriados nacionais; a busca lê apenas propriedades próprias, então `"__proto__"`, `"constructor"` e afins são códigos desconhecidos como qualquer outro, e não uma exceção. + +Apenas um feriado estadual por UF é feriado civil pela [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm), art. 1º, II, que autoriza "a data magna do Estado fixada em lei estadual", no singular; as demais entradas se apoiam em leis estaduais ordinárias e são reportadas por serem observadas na prática. Regras notáveis por estado: + +- **SC** — a [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) transfere os dois feriados estaduais, "Dia do Estado de Santa Catarina" (11/08) e "Dia de Santa Catarina de Alexandria" (25/11), para o domingo subsequente sempre que caem de segunda a sexta, então a segunda-feira 11/08/2025 é dia útil em SC e o feriado cai no domingo 17/08. +- **DF** — a [Lei distrital nº 72/1989](https://www.sinj.df.gov.br/sinj/Norma/18459/Lei_72_27_12_1989.html), art. 1º parágrafo único, declara Corpus Christi feriado. Com `stateCode: 'DF'` a única entrada de Corpus Christi volta tipada como `"state"` em vez de `"optional"`; ela é substituída, não duplicada. +- **GO** — a [Lei GO nº 20.756/2020](https://legisla.casacivil.go.gov.br/pesquisa_legislacao/100979/lei-20756), art. 269, II, lista três feriados estaduais: 26/07 (Fundação da Cidade de Goiás), 24/10 (Lançamento da Pedra Fundamental de Goiânia) e 28/10 (Dia do Servidor Público). +- **AL** — 16/09 é feriado estadual a partir de 2024 ([Lei AL nº 9.358/2024](https://sapl.al.al.leg.br/norma/3117)) e apenas ponto facultativo (`"optional"`) antes disso. +- **PB** — 26/07 ("Morte de João Pessoa") é emitido apenas até 2015: a [Lei PB nº 10.601/2015](https://sapl.al.pb.leg.br/norma/11988), art. 2º, revogou a sua base. +- **TO** — 18/03 ("Autonomia do Estado do Tocantins") é emitido apenas até 2008: a [Lei TO nº 2.013/2009](https://www.al.to.leg.br/arquivo/15724) transformou em meramente comemorativo o dispositivo que declarava o feriado. + +A data retornada é a legal. O deslocamento de SC acima é o único modelado; o de Acre (feriados de terça a quinta transferidos para a sexta) e os decretos goianos que podem mover 26/07 e 28/10 não são. ```javascript import { getHolidays } from '@brazilian-utils/brazilian-utils'; @@ -1058,7 +1097,7 @@ formatPassport('AB-123.456'); // 'AB123456' ## generatePassport -Gera um número de passaporte brasileiro válido aleatoriamente. +Gera um número de passaporte brasileiro válido aleatoriamente. Usa `Math.random()` internamente, então não é criptograficamente seguro. ```javascript import { generatePassport } from '@brazilian-utils/brazilian-utils'; @@ -1079,7 +1118,7 @@ parsePassport(' AB 123 456 '); // 'AB123456' ## generateCep -Gera um CEP aleatório. +Gera um CEP aleatório. Usa `Math.random()` internamente, então não é criptograficamente seguro. ```javascript import { generateCep } from '@brazilian-utils/brazilian-utils'; @@ -1089,7 +1128,7 @@ generateCep(); // '92500000' ## formatCnh -Formata a CNH. +Formata a CNH. `options.pad` (parte de `FormatCnhOptions`) completa o valor com zeros à esquerda até os 11 dígitos antes de aplicar a máscara. ```javascript import { formatCnh } from '@brazilian-utils/brazilian-utils'; @@ -1100,17 +1139,19 @@ formatCnh('2650306461', { pad: true }); // 026503064-61 ## isValidCnh -Valida se a CNH é válida. +Valida se a CNH é válida. Espaços, pontos e hífens ao redor/entre os dígitos são ignorados, mas qualquer outro caractere, uma letra em especial, invalida o valor. ```javascript import { isValidCnh } from '@brazilian-utils/brazilian-utils'; isValidCnh('00000000119'); // true +isValidCnh('000000001-19'); // true (hífen antes dos dígitos verificadores) +isValidCnh('ab00000000119'); // false (letras são rejeitadas) ``` ## generateCnh -Gera uma CNH válida aleatória. +Gera uma CNH válida aleatória. Usa `Math.random()` internamente, então não é criptograficamente seguro. ```javascript import { generateCnh } from '@brazilian-utils/brazilian-utils'; @@ -1143,9 +1184,9 @@ const ceps = await getCepInfoByAddress({ // [ // { -// cep: '01310100', +// cep: '01310-100', // logradouro: 'Avenida Paulista', -// complemento: 'lado par', +// complemento: 'de 612 a 1510 - lado par', // bairro: 'Bela Vista', // localidade: 'São Paulo', // uf: 'SP' @@ -1188,7 +1229,7 @@ isValidLegalNature('9999'); // false ## generateLegalNature -Gera um código de natureza jurídica válido aleatório. +Gera um código de natureza jurídica válido aleatório. Usa `Math.random()` internamente, então não é criptograficamente seguro. ```javascript import { generateLegalNature } from '@brazilian-utils/brazilian-utils'; @@ -1226,6 +1267,8 @@ Busca um código de natureza jurídica na tabela oficial do IBGE/CONCLA. import { getLegalNature } from '@brazilian-utils/brazilian-utils'; getLegalNature('2062'); // { code: '2062', description: 'Sociedade Empresária Limitada' } +getLegalNature('206-2'); // { code: '2062', description: 'Sociedade Empresária Limitada' } +getLegalNature(206.2); // { code: '2062', description: 'Sociedade Empresária Limitada' } getLegalNature('0000'); // null ``` @@ -1255,7 +1298,7 @@ formatLicensePlate('abc1d23'); // 'ABC1D23' ## generateLicensePlate -Gera uma placa aleatória no formato escolhido. +Gera uma placa aleatória no formato escolhido. Usa `Math.random()` internamente, então não é criptograficamente seguro. ```javascript import { generateLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -1337,7 +1380,7 @@ await getMunicipality({ code: '123' }); ## getMunicipalities -Retorna os municípios brasileiros publicados pelo IBGE. Retorna todos os municípios se nenhum estado for fornecido, ou os municípios de um estado específico. Cada município é retornado como `{ code, name, stateCode }`, onde `code` é o código IBGE de 7 dígitos do município. Os resultados são ordenados por nome com `localeCompare` no locale "pt-BR". Cada chamada retorna um array novo com objetos novos, então alterar o resultado nunca afeta chamadas seguintes. Um código de estado desconhecido retorna um array vazio em vez de lançar erro. +Retorna os municípios brasileiros publicados pelo IBGE. Retorna todos os municípios se nenhum estado for fornecido, ou os municípios de um estado específico. Cada município é retornado como `{ code, name, stateCode }`, onde `code` é o código IBGE de 7 dígitos do município. Os resultados são ordenados por nome com `localeCompare` no locale "pt-BR". Cada chamada retorna um array novo com objetos novos, então alterar o resultado nunca afeta chamadas seguintes. Um código de estado desconhecido retorna um array vazio em vez de lançar erro. Só um `stateCode` omitido (ou `undefined`) pede a lista completa: `getMunicipalities(null)` e `getMunicipalities('')` retornam `[]`, enquanto os mais permissivos `getCities(null)` e `getCities('')` retornam todas as cidades. ```javascript import { getMunicipalities } from '@brazilian-utils/brazilian-utils'; @@ -1400,7 +1443,7 @@ isHoliday(); // false ## isBusinessDay -Verifica se uma data é um dia útil no Brasil. Retorna `false` para sábados, domingos e feriados brasileiros retornados por `getHolidays` para a data local de `value` (ano/mês/dia lidos localmente), a mesma convenção usada por `isHoliday`. `options.includeOptional` (parte de `IsBusinessDayOptions`) tem valor padrão `true`, então feriados do tipo opcional (`Holiday.type === "optional"`, ou seja, Carnaval e Corpus Christi) também contam como dias não úteis; passe `false` para considerar apenas os feriados estatutários. `options.stateCode` também considera os feriados daquele estado; um `stateCode` desconhecido/inválido é ignorado, retornando apenas os feriados nacionais. Um `value` que não é um `Date` válido retorna `false`. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele retorna `false`. +Verifica se uma data é um dia útil no Brasil. Retorna `false` para sábados, domingos e feriados brasileiros retornados por `getHolidays` para a data local de `value` (ano/mês/dia lidos localmente), a mesma convenção usada por `isHoliday`. `options.includeOptional` (parte de `BusinessDayOptions`, o tipo de opções que todos os utilitários de dias úteis compartilham) tem valor padrão `true`, então feriados do tipo opcional (`Holiday.type === "optional"`, ou seja, Carnaval e Corpus Christi) também contam como dias não úteis; passe `false` para considerar apenas os feriados estatutários. `options.stateCode` também considera os feriados daquele estado; um `stateCode` desconhecido/inválido é ignorado, retornando apenas os feriados nacionais. Um `value` que não é um `Date` válido retorna `false`. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele retorna `false`. ```javascript import { isBusinessDay } from '@brazilian-utils/brazilian-utils'; @@ -1417,38 +1460,55 @@ isBusinessDay(new Date('not a date')); // false ## addBusinessDays -Adiciona um número de dias úteis brasileiros a uma data, pulando sábados, domingos e feriados brasileiros exatamente como `isBusinessDay` os define (mesmas opções `stateCode`/`includeOptional`). Retorna um novo `Date`; a `date` de entrada (parte de `AddBusinessDaysParams`) nunca é alterada, e seu horário é preservado no resultado. `days: 0` retorna um novo `Date` igual a `date`, sem alterações, mesmo quando `date` cai em um fim de semana ou feriado, isso reflete o comportamento verificado de [`addBusinessDays(date, 0)` do date-fns](https://date-fns.org/docs/addBusinessDays), que também não avança a entrada para o próximo dia útil. Um `days` negativo anda para trás, um dia útil por vez, também como no date-fns. Retorna `null` em caso de entrada inválida: uma `date` que não é um `Date` válido, um `days` que não é um número inteiro finito, ou um `stateCode` que não é uma string. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele (ou, no `addBusinessDays`, um percurso que sai dele) retorna `null`. +Adiciona um número de dias úteis brasileiros a uma data, pulando sábados, domingos e feriados brasileiros exatamente como `isBusinessDay` os define (as mesmas `BusinessDayOptions`). A assinatura é a do date-fns: `addBusinessDays(date, amount, options?)`. Retorna um novo `Date`; a `date` de entrada nunca é alterada, e seu horário é preservado no resultado. Um `amount` igual a `0` retorna um novo `Date` igual a `date`, sem alterações, mesmo quando `date` cai em um fim de semana ou feriado, isso reflete o comportamento verificado de [`addBusinessDays(date, 0)` do date-fns](https://date-fns.org/docs/addBusinessDays), que também não avança a entrada para o próximo dia útil. Um `amount` negativo anda para trás, um dia útil por vez, também como no date-fns. Retorna `null` em caso de entrada inválida: uma `date` que não é um `Date` válido, um `amount` que não é um número inteiro finito, ou um `stateCode` que não é uma string; um `options` que não é um objeto é ignorado, exatamente como o `isBusinessDay` o ignora. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele, ou um percurso que sai dele, retorna `null`. ```javascript import { addBusinessDays } from '@brazilian-utils/brazilian-utils'; -addBusinessDays({ date: new Date(2024, 0, 2, 12), days: 1 }); // Date, 2024-01-03 12:00 (o dia seguinte já é útil) -addBusinessDays({ date: new Date(2024, 11, 31, 12), days: 1 }); // Date, 2025-01-02 12:00 (2025-01-01 é Ano novo, pulado) -addBusinessDays({ date: new Date(2024, 0, 5, 12), days: -1 }); // Date, 2024-01-04 12:00 (anda para trás) -addBusinessDays({ date: new Date(2024, 0, 6, 12), days: 0 }); // Date, 2024-01-06 12:00 (sem alteração, mesmo sendo sábado) -addBusinessDays({ date: new Date(2024, 6, 8, 12), days: 1, stateCode: 'SP' }); // Date, 2024-07-10 12:00 (2024-07-09 é a Revolução Constitucionalista em SP, pulado) -addBusinessDays({ date: new Date('not a date'), days: 1 }); // null -addBusinessDays({ date: new Date(2024, 0, 2), days: 1.5 }); // null (não é um número inteiro) +addBusinessDays(new Date(2024, 0, 2, 12), 1); // Date, 2024-01-03 12:00 (o dia seguinte já é útil) +addBusinessDays(new Date(2024, 11, 31, 12), 1); // Date, 2025-01-02 12:00 (2025-01-01 é Ano novo, pulado) +addBusinessDays(new Date(2024, 0, 5, 12), -1); // Date, 2024-01-04 12:00 (anda para trás) +addBusinessDays(new Date(2024, 0, 6, 12), 0); // Date, 2024-01-06 12:00 (sem alteração, mesmo sendo sábado) +addBusinessDays(new Date(2024, 6, 8, 12), 1, { stateCode: 'SP' }); // Date, 2024-07-10 12:00 (2024-07-09 é a Revolução Constitucionalista em SP, pulado) +addBusinessDays(new Date('not a date'), 1); // null +addBusinessDays(new Date(2024, 0, 2), 1.5); // null (não é um número inteiro) +``` + +## subBusinessDays + +Subtrai um número de dias úteis brasileiros de uma data: `subBusinessDays(date, amount, options?)` é `addBusinessDays(date, -amount, options)`, e é exatamente assim que a função é implementada, então tudo o que vale acima vale aqui (o horário preservado, a entrada intacta, um `amount` igual a `0` devolvendo a data sem alterações, o intervalo de 1900 a 2099 e os casos de `null`). Um `amount` negativo anda para frente. + +```javascript +import { subBusinessDays } from '@brazilian-utils/brazilian-utils'; + +subBusinessDays(new Date(2024, 0, 5, 12), 1); // Date, 2024-01-04 12:00 (o dia anterior já é útil) +subBusinessDays(new Date(2024, 0, 8, 12), 1); // Date, 2024-01-05 12:00 (anda para trás passando pelo fim de semana) +subBusinessDays(new Date(2025, 0, 2, 12), 1); // Date, 2024-12-31 12:00 (2025-01-01 é Ano novo, pulado) +subBusinessDays(new Date(2024, 0, 5, 12), -1); // Date, 2024-01-08 12:00 (anda para frente) +subBusinessDays(new Date(2024, 0, 6, 12), 0); // Date, 2024-01-06 12:00 (sem alteração, mesmo sendo sábado) +subBusinessDays(new Date(2024, 6, 10, 12), 1, { stateCode: 'SP' }); // Date, 2024-07-08 12:00 (2024-07-09 é a Revolução Constitucionalista em SP, pulado) +subBusinessDays(new Date('not a date'), 1); // null +subBusinessDays(new Date(2024, 0, 2), 1.5); // null (não é um número inteiro) ``` ## differenceInBusinessDays -Conta o número de dias úteis brasileiros entre duas datas, refletindo a semântica de [`differenceInBusinessDays` do date-fns](https://date-fns.org/docs/differenceInBusinessDays) (verificada em seu código-fonte): `params.from` é contado quando ele próprio é um dia útil, `params.to` nunca é contado, e cada dia útil estritamente entre os dois é contado uma vez. Só a data de calendário de cada `Date` importa, o horário é ignorado. Os dias úteis são determinados exatamente como em `isBusinessDay` (mesmas opções `stateCode`/`includeOptional`). `from`/`to` no mesmo dia de calendário retornam `0`; um `to` anterior a `from` retorna um número negativo. Retorna `null` em caso de entrada inválida: um `from`/`to` que não é um `Date` válido, ou um `stateCode` que não é uma string. Os parâmetros são tipados como `DifferenceInBusinessDaysParams`. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele (ou, no `addBusinessDays`, um percurso que sai dele) retorna `null`. +Conta o número de dias úteis brasileiros entre duas datas, refletindo a semântica de [`differenceInBusinessDays` do date-fns](https://date-fns.org/docs/differenceInBusinessDays) (verificada em seu código-fonte), inclusive a ordem dos argumentos: `differenceInBusinessDays(laterDate, earlierDate, options?)`. O percurso começa em `earlierDate` e para logo antes de `laterDate`, então `earlierDate` é contado quando ele próprio é um dia útil, `laterDate` nunca é contado, e cada dia útil estritamente entre os dois é contado uma vez. Só a data de calendário de cada `Date` importa, o horário é ignorado. Os dias úteis são determinados exatamente como em `isBusinessDay` (as mesmas `BusinessDayOptions`). O resultado é positivo quando `laterDate` é posterior a `earlierDate` e negativo quando é anterior; duas datas no mesmo dia de calendário retornam `0`. Retorna `null` em caso de entrada inválida: uma data que não é um `Date` válido, ou um `stateCode` que não é uma string; um `options` que não é um objeto é ignorado. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele retorna `null`. ```javascript import { differenceInBusinessDays } from '@brazilian-utils/brazilian-utils'; -differenceInBusinessDays({ from: new Date(2024, 0, 1), to: new Date(2024, 0, 2) }); // 0 (01/01 é Ano novo) -differenceInBusinessDays({ from: new Date(2024, 0, 2), to: new Date(2024, 0, 3) }); // 1 (02/01 contado, uma terça-feira) -differenceInBusinessDays({ from: new Date(2024, 0, 3), to: new Date(2024, 0, 2) }); // -1 (to anterior a from) -differenceInBusinessDays({ from: new Date(2024, 0, 2), to: new Date(2024, 0, 2) }); // 0 (mesmo dia) -differenceInBusinessDays({ from: new Date(2024, 6, 8), to: new Date(2024, 6, 10), stateCode: 'SP' }); // 1 (09/07/2024 é feriado estadual em SP) -differenceInBusinessDays({ from: new Date('not a date'), to: new Date() }); // null +differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 1)); // 0 (01/01 é Ano novo, não contado) +differenceInBusinessDays(new Date(2024, 0, 3), new Date(2024, 0, 2)); // 1 (02/01 contado, uma terça-feira; 03/01 não) +differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 3)); // -1 (a data posterior vem primeiro, então a contagem é negativa) +differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 2)); // 0 (mesmo dia) +differenceInBusinessDays(new Date(2024, 6, 10), new Date(2024, 6, 8), { stateCode: 'SP' }); // 1 (09/07/2024 é feriado estadual em SP) +differenceInBusinessDays(new Date(), new Date('not a date')); // null ``` ## convertDateToWords -Formata uma data por extenso em português do Brasil, ex.: `"01/01/2024"` vira `"primeiro de janeiro de dois mil e vinte e quatro"`. Aceita um `Date` (lido pela sua data de calendário local, a mesma convenção usada por `isHoliday`) ou uma string no formato `"dd/mm/yyyy"` ou ISO `"yyyy-mm-dd"`. Com o `options.style` padrão `"full"`, o dia 1 é escrito como "primeiro" e os demais dias usam o número cardinal; com `"month"`, só o nome do mês é escrito por extenso e o dia/ano ficam em dígitos (o dia 1 como `"1º"`, ex.: `"2 de março de 2024"`, `"1º de janeiro de 2024"`). Os nomes dos meses ficam em minúsculo. No estilo `"full"` o ano é escrito por extenso sem a vírgula de milhar que `convertNumberToWords`/`convertCurrencyToWords` usam (`1999` vira `"mil novecentos e noventa e nove"`, não `"mil, novecentos e noventa e nove"`), do jeito que uma data é lida em voz alta. `options.weekday` (padrão `false`) prefixa o nome do dia da semana em pt-BR minúsculo seguido de vírgula (`"sábado, dois de março de dois mil e vinte e quatro"`), calculado a partir da data de calendário resolvida. `options.case` define a caixa de todo o resultado: `"lower"` (padrão), `"sentence"` (só a primeira letra em maiúscula) ou `"upper"` (tudo em maiúscula, preservando os acentos). Valores inválidos de `case`/`style` são ignorados e o padrão é usado. O dia 29 de fevereiro é aceito nos anos bissextos do calendário gregoriano proléptico (divisíveis por 4, exceto séculos não divisíveis por 400). Retorna `""` para um `Date` inválido, uma string malformada, um dia/mês que não existe ou uma data anterior ao ano 1. +Formata uma data por extenso em português do Brasil, ex.: `"01/01/2024"` vira `"primeiro de janeiro de dois mil e vinte e quatro"`. Aceita um `Date` (lido pela sua data de calendário local, a mesma convenção usada por `isHoliday`) ou uma string no formato `"dd/mm/yyyy"` ou ISO `"yyyy-mm-dd"`. Com o `options.style` padrão `"full"`, o dia 1 é escrito como "primeiro" e os demais dias usam o número cardinal; com `"month"`, só o nome do mês é escrito por extenso e o dia/ano ficam em dígitos (o dia 1 como `"1º"`, ex.: `"2 de março de 2024"`, `"1º de janeiro de 2024"`). Os nomes dos meses ficam em minúsculo. No estilo `"full"` o ano é escrito por extenso sem a vírgula de milhar que `convertNumberToWords`/`convertCurrencyToWords` usam (`1999` vira `"mil novecentos e noventa e nove"`, não `"mil, novecentos e noventa e nove"`), do jeito que uma data é lida em voz alta. `options.weekday` (padrão `false`) prefixa o nome do dia da semana em pt-BR minúsculo seguido de vírgula (`"sábado, dois de março de dois mil e vinte e quatro"`), calculado a partir da data de calendário resolvida. Um valor inválido de `style` é ignorado e o padrão é usado. O resultado sai sempre em minúsculas; aplique qualquer outra caixa por conta própria. O dia 29 de fevereiro é aceito nos anos bissextos do calendário gregoriano proléptico (divisíveis por 4, exceto séculos não divisíveis por 400). Retorna `""` para um `Date` inválido, uma string malformada, um dia/mês que não existe ou uma data anterior ao ano 1. ```javascript import { convertDateToWords } from '@brazilian-utils/brazilian-utils'; @@ -1456,7 +1516,6 @@ import { convertDateToWords } from '@brazilian-utils/brazilian-utils'; convertDateToWords('01/01/2024'); // "primeiro de janeiro de dois mil e vinte e quatro" convertDateToWords('2024-01-02'); // "dois de janeiro de dois mil e vinte e quatro" convertDateToWords(new Date(2024, 0, 1)); // "primeiro de janeiro de dois mil e vinte e quatro" -convertDateToWords('01/01/2024', { case: 'sentence' }); // "Primeiro de janeiro de dois mil e vinte e quatro" convertDateToWords('02/03/2024', { style: 'month' }); // "2 de março de 2024" convertDateToWords('01/01/2024', { style: 'month' }); // "1º de janeiro de 2024" convertDateToWords('02/03/2024', { weekday: true }); // "sábado, dois de março de dois mil e vinte e quatro" @@ -1479,7 +1538,7 @@ formatVoterId('1234567880191'); // '1234 5678 8 01 91' (título de 13 dígitos S ## isValidVoterId -Valida se um título de eleitor é válido. Aceita tanto o título padrão de 12 dígitos quanto o título de 13 dígitos emitido por São Paulo (UF `01`) e Minas Gerais (UF `02`). +Valida se um título de eleitor é válido. Aceita tanto o título padrão de 12 dígitos quanto o título de 13 dígitos emitido por São Paulo (UF `01`) e Minas Gerais (UF `02`). Espaços e pontos são aceitos ao redor e entre os grupos `0000 0000 00 00`, mas qualquer outro caractere, uma letra em especial, invalida o valor. ```javascript import { generateVoterId, isValidVoterId } from '@brazilian-utils/brazilian-utils'; @@ -1516,6 +1575,8 @@ parseVoterId('1234 5678 8 01 91'); // '1234567880191' (título de 13 dígitos SP Verifica se um número de CNS (Cartão Nacional de Saúde) é válido, o identificador único do usuário do SUS (Sistema Único de Saúde). Cartões definitivos (iniciados em 1 ou 2) são validados com a mesma ponderação módulo 11 usada no PIS sobre uma base de 11 dígitos embutida, ajustando a base em +2 quando o dígito verificador bruto resulta em 10. Cartões provisórios (iniciados em 7, 8 ou 9) são validados por uma soma ponderada única (pesos de 15 a 1) que deve ser múltipla de 11. O valor precisa vir escrito como os 15 dígitos, opcionalmente separados nos grupos impressos de 3-4-4-4 por espaços ou pelos caracteres de máscara usuais; letras no meio dos dígitos são rejeitadas em vez de ignoradas. +As duas rotinas vêm da [página de validação de CNS da ANVISA](https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/), que fica atrás de um filtro de bots e responde HTTP 403 a clientes que não sejam navegadores. A [página do e-SUS APS](https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html) documenta o mesmo algoritmo e é acessível sem navegador, mas aplica a rotina de provisórios a números iniciados em 5, 7, 8 ou 9; esta implementação segue a ANVISA e rejeita um número iniciado em 5 mesmo quando a soma ponderada fecha. + ```javascript import { isValidCns } from '@brazilian-utils/brazilian-utils'; @@ -1532,14 +1593,14 @@ Formata um número de CNS (Cartão Nacional de Saúde) nos grupos de exibição ```javascript import { formatCns } from '@brazilian-utils/brazilian-utils'; -formatCns('123456789010001'); // '123 4567 8901 0001' -formatCns(123456789010001); // '123 4567 8901 0001' +formatCns('123456789010000'); // '123 4567 8901 0000' +formatCns(123456789010000); // '123 4567 8901 0000' formatCns('89010001', { pad: true }); // '000 0000 8901 0001' ``` ## isValidCertidao -Verifica se a matrícula de uma certidão de registro civil (nascimento, casamento, óbito e os demais atos mantidos por uma serventia de registro civil das pessoas naturais) é válida. A matrícula tem 32 dígitos distribuídos em 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), e os dois dígitos verificadores usam módulo 11 com pesos ciclando de 2 a 10 e voltando por 0. Aceita os caracteres de máscara usuais e espaços entre e ao redor dos grupos. O layout é o em vigor do [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023, na redação do Provimento CN nº 182/2024); a própria matrícula foi instituída pelo já revogado [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311). Os dígitos verificadores estão detalhados em [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e implementado pelo [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) e pelo [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). +Verifica se a matrícula de uma certidão de registro civil (nascimento, casamento, óbito e os demais atos mantidos por uma serventia de registro civil das pessoas naturais) é válida. A matrícula tem 32 dígitos distribuídos em 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), e os dois dígitos verificadores usam módulo 11 com pesos ciclando de 2 a 10 e voltando por 0. Aceita os caracteres de máscara usuais e espaços entre e ao redor dos grupos. O layout é o publicado atualmente no [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023), com o inciso II e os §§ 1º a 5º na redação do Provimento CN nº 237/2026 e o restante do artigo na do Provimento CN nº 182/2024; a própria matrícula foi instituída pelo já revogado [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311). Os dígitos verificadores estão detalhados em [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e implementado pelo [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) e pelo [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). Os dígitos do serviço são fixos em `55`, o código que o [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) atribui ao registro civil das pessoas naturais, então uma matrícula com qualquer outro par na nona e décima posições é rejeitada por mais que os dígitos verificadores confiram. O dígito do tipo de livro sempre precisa nomear um dos nove tipos de livro (o mesmo `CertidaoType` retornado por `parseCertidao`), então uma matrícula cujo dígito é `0` é rejeitada por mais que os dígitos verificadores confiram, do mesmo jeito que `parseCertidao` devolve `null` para ela. `options.accept` (parte de `IsValidCertidaoOptions`) restringe ainda mais aos tipos listados; o padrão é aceitar todos os tipos, e um valor que não seja um array volta para esse padrão. Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. @@ -1686,7 +1747,7 @@ formatCaepf('184', { pad: true }); // 000.000.000/001-84 ## isValidRegistroProfissional -Verifica a estrutura de um número de registro/inscrição profissional. As opções são tipadas como `IsValidRegistroProfissionalOptions`: `options.council` escolhe o conselho emissor (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` ou `"CRC"`) e o `options.stateCode` opcional verifica a UF embutida (ignorado para `"CRP"`, cujo prefixo de 2 dígitos é um código regional, não uma UF literal). É apenas uma verificação estrutural: a quantidade de dígitos e a UF são validadas, mas nenhum dígito verificador é calculado, mesmo para o CRC, cujo formato inclui um. Um registro no CRC é a UF, 6 dígitos e o tipo de registro (`"O"` Originário, `"P"` Provisório ou `"T"` Transferido, que nada diz sobre a categoria profissional), conforme o [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1) e a Resolução CFC nº 1.707/2023. O código regional do CRP precisa ser um dos [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) do sistema CFP, de CRP-01 a CRP-24. A OAB, o CFM e o CFO não publicam o formato dos números que emitem, então as faixas de dígitos aceitas para `"OAB"`, `"CRM"` e `"CRO"` são convencionais, não normativas. O CREA não é suportado: seu formato de registro não pôde ser confirmado em uma fonte oficial e publicamente documentada após a unificação nacional de 2016 (RNP). +Verifica a estrutura de um número de registro/inscrição profissional. As opções são tipadas como `IsValidRegistroProfissionalOptions`: `options.council` escolhe o conselho emissor (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` ou `"CRC"`) e o `options.stateCode` opcional verifica a UF embutida (ignorado para `"CRP"`, cujo prefixo de 2 dígitos é um código regional, não uma UF literal). É apenas uma verificação estrutural: a quantidade de dígitos e a UF são validadas, mas nenhum dígito verificador é calculado, mesmo para o CRC, cujo formato inclui um. Um registro no CRC é a UF, 6 dígitos, o tipo de registro (`"O"` Originário ou `"P"` Provisório, que nada diz sobre a categoria profissional) e o dígito verificador, conforme o [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). Um Registro Transferido ou Secundário acrescenta `"T"` ou `"S"` e a UF do CRC de destino **depois** do dígito verificador, conforme esse mesmo item e a [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: os exemplos do próprio Manual são `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` e `PI-111222/O-5 S-AC`. As duas UFs precisam ser códigos reais, e o `options.stateCode` é comparado com a de origem. O código regional do CRP precisa ser um dos [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) do sistema CFP, de CRP-01 a CRP-24. Só o formato do CRC e esses códigos regionais do CRP se apoiam em fonte publicada: a página do CFP não publica o tamanho do número de inscrição, e a OAB, o CFM e o CFO não publicam formato algum, então as faixas de dígitos aceitas para `"CRP"`, `"OAB"`, `"CRM"` e `"CRO"` são convencionais, não normativas (a busca pública da OAB/SP tem `maxlength="7"`, e o CFM documenta CRMs com prefixo `300` e sufixo `P`, nenhum deles expresso por esses formatos). O CREA não é suportado: seu formato de registro não pôde ser confirmado em uma fonte oficial e publicamente documentada após a unificação nacional de 2016 (RNP). ```javascript import { isValidRegistroProfissional } from '@brazilian-utils/brazilian-utils'; @@ -1695,6 +1756,8 @@ isValidRegistroProfissional('123456/SP', { council: 'OAB' }); // true isValidRegistroProfissional('123456-RJ', { council: 'OAB', stateCode: 'SP' }); // false (UF divergente) isValidRegistroProfissional('06/12345', { council: 'CRP' }); // true isValidRegistroProfissional('SP-123456/O-3', { council: 'CRC' }); // true +isValidRegistroProfissional('SP-123456/O-3 T-MG', { council: 'CRC' }); // true (registro transferido) +isValidRegistroProfissional('SP-123456/T-3', { council: 'CRC' }); // false ("T" não é tipo de registro) ``` ## isValidVin @@ -1712,7 +1775,7 @@ isValidVin('1HGCM8263IA004352'); // false (contém a letra excluída I) ## isValidCbo -Valida se um código CBO (Classificação Brasileira de Ocupações) existe na tabela de ocupações do MTE. Aceita o código com ou sem a máscara de hífen, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 6 dígitos, ou a máscara `NNNN-NN`, com os separadores usuais entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. +Valida se um código CBO (Classificação Brasileira de Ocupações) existe na tabela de ocupações do MTE. Aceita o código com ou sem a máscara de hífen, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 6 dígitos, ou a máscara `NNNN-NN`, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. ```javascript import { isValidCbo } from '@brazilian-utils/brazilian-utils'; @@ -1725,7 +1788,7 @@ isValidCbo('2124abc05'); // false (não é uma forma documentada) isValidCbo(-212405); // false (não é um inteiro seguro não negativo) ``` -Os títulos das ocupações vêm das [tabelas oficiais da CBO 2002 publicadas pelo MTE](http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf). +Os títulos das ocupações vêm da [tabela oficial de ocupações da CBO 2002 publicada pelo MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). ## getCbo @@ -1739,11 +1802,11 @@ getCbo('000000'); // null getCbo('2124abc05'); // null (não é uma forma documentada) ``` -Os títulos das ocupações vêm das [tabelas oficiais da CBO 2002 publicadas pelo MTE](http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf). +Os títulos das ocupações vêm da [tabela oficial de ocupações da CBO 2002 publicada pelo MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). ## isValidCnae -Valida se um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas) existe na tabela CNAE 2.3 publicada pelo IBGE. Aceita o código com ou sem a máscara `NNNN-N/NN`, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 7 dígitos, ou a máscara, com os separadores usuais entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. +Valida se um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas) existe na tabela CNAE 2.3 publicada pelo IBGE. Aceita o código com ou sem a máscara `NNNN-N/NN`, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 7 dígitos, ou a máscara, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. ```javascript import { isValidCnae } from '@brazilian-utils/brazilian-utils'; @@ -1757,12 +1820,18 @@ isValidCnae(-111301); // false (não é um inteiro seguro não negativo) ## formatCnae -Formata um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas). +Formata um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas). `options.pad` (parte de `FormatCnaeOptions`) funciona exatamente como em `formatCpf`/`formatCep`: com o padrão `false` a máscara é aplicada progressivamente, até onde o valor vai, que é o que um campo sendo digitado precisa; com `true` o valor é primeiro completado com zeros à esquerda até os 7 dígitos de uma subclasse completa, então ele sempre volta com a máscara inteira. Um número é tratado exatamente como a string dos seus dígitos, ou seja, só é completado com `pad: true`. Só dígitos e os caracteres de máscara são aceitos; qualquer outra coisa retorna `''`, assim como um número que não seja um inteiro seguro não negativo. ```javascript import { formatCnae } from '@brazilian-utils/brazilian-utils'; formatCnae('6201501'); // 6201-5/01 +formatCnae('62'); // 62 (máscara aplicada até onde o valor vai) +formatCnae('62015'); // 6201-5 +formatCnae('62', { pad: true }); // 0000-0/62 (completado até 7 dígitos antes) +formatCnae(111301, { pad: true }); // 0111-3/01 +formatCnae('abc6201501'); // '' (não é uma forma documentada) +formatCnae(-6201501); // '' (não é um inteiro seguro não negativo) ``` ## getCnae @@ -1779,7 +1848,7 @@ getCnae('0111abc301'); // null (não é uma forma documentada) ## isValidNcm -Valida se um código NCM (Nomenclatura Comum do Mercosul) existe na tabela vigente publicada pelo Siscomex/MDIC. Aceita o código com ou sem a máscara de pontos, ou como número. +Valida se um código NCM (Nomenclatura Comum do Mercosul) existe na tabela vigente publicada pelo Siscomex/MDIC. Aceita o código com ou sem a máscara de pontos, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 8 dígitos, ou a máscara `NNNN.NN.NN`, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. ```javascript import { isValidNcm } from '@brazilian-utils/brazilian-utils'; @@ -1787,40 +1856,55 @@ import { isValidNcm } from '@brazilian-utils/brazilian-utils'; isValidNcm('8471.30.12'); // true isValidNcm('84713012'); // true isValidNcm('00000000'); // false +isValidNcm('abc01012100'); // false (não é uma forma documentada) +isValidNcm(-84713012); // false (não é um inteiro seguro não negativo) ``` ## formatNcm -Formata um código NCM (Nomenclatura Comum do Mercosul). +Formata um código NCM (Nomenclatura Comum do Mercosul). `options.pad` (parte de `FormatNcmOptions`) funciona exatamente como em `formatCpf`/`formatCep`: com o padrão `false` a máscara é aplicada progressivamente, até onde o valor vai, que é o que um campo sendo digitado precisa; com `true` o valor é primeiro completado com zeros à esquerda até os 8 dígitos de um código completo, então ele sempre volta com a máscara inteira. Um número é tratado exatamente como a string dos seus dígitos, ou seja, só é completado com `pad: true`. Só dígitos e os caracteres de máscara são aceitos; qualquer outra coisa retorna `''`, assim como um número que não seja um inteiro seguro não negativo. ```javascript import { formatNcm } from '@brazilian-utils/brazilian-utils'; formatNcm('84713012'); // 8471.30.12 +formatNcm('8471'); // 8471 (máscara aplicada até onde o valor vai) +formatNcm('847130'); // 8471.30 +formatNcm('8471', { pad: true }); // 0000.84.71 (completado até 8 dígitos antes) +formatNcm('abc8471'); // '' (não é uma forma documentada) +formatNcm(-84713012); // '' (não é um inteiro seguro não negativo) ``` ## isValidCfop -Valida se um código CFOP (Código Fiscal de Operações e Prestações) existe na tabela oficial (Ajuste SINIEF 07/2001 e atualizações). Só os códigos operáveis contam: os títulos de grupo e subgrupo da nomenclatura oficial, os códigos terminados em `00` e `50` (1000, 1100, 1150, 5350, ...), são títulos de seção e não códigos que um documento pode carregar, então são rejeitados. +Valida se um código CFOP (Código Fiscal de Operações e Prestações) existe na tabela oficial. A tabela é o [Anexo II consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), o texto vigente (redação atual dada pelo Ajuste SINIEF 03/24, última alteração pelo Ajuste SINIEF 39/25), e não o texto congelado de 2001 do Ajuste SINIEF 07/01. Só os códigos operáveis contam: os títulos de grupo e subgrupo da nomenclatura oficial, os códigos terminados em `00` e `50` (1000, 1100, 1150, 5350, ...), são títulos de seção e não códigos que um documento pode carregar, então são rejeitados. + +Uma string só é lida como código quando está escrita em uma das formas documentadas (os 4 dígitos, ou a forma `N.NNN` impressa no anexo, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. ```javascript import { isValidCfop } from '@brazilian-utils/brazilian-utils'; isValidCfop('5102'); // true +isValidCfop('1.101'); // true +isValidCfop('7504'); // true (incluído na reescrita de 2022 do anexo) isValidCfop('0000'); // false isValidCfop('1150'); // false (título de subgrupo, não é um código operável) +isValidCfop('abc5102'); // false (não é uma forma documentada) +isValidCfop(-5102); // false (não é um inteiro seguro não negativo) ``` ## getCfop -Busca um código CFOP (Código Fiscal de Operações e Prestações) e retorna seu código e a descrição oficial. Os títulos de grupo e subgrupo da nomenclatura oficial, os códigos terminados em `00` e `50`, não estão na tabela e retornam `null`. +Busca um código CFOP (Código Fiscal de Operações e Prestações) e retorna seu código e a descrição oficial, na redação do [Anexo II consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24). Os títulos de grupo e subgrupo da nomenclatura oficial, os códigos terminados em `00` e `50`, não estão na tabela e retornam `null`. Valem as mesmas regras de entrada de `isValidCfop`. ```javascript import { getCfop } from '@brazilian-utils/brazilian-utils'; -getCfop('5102'); // { code: '5102', description: 'Venda de mercadoria adquirida ou recebida de terceiros' } +getCfop('1101'); // { code: '1101', description: 'Compra para industrialização ou produção rural' } +getCfop('7504'); // { code: '7504', description: 'Exportação de mercadoria que foi objeto de formação de lote de exportação' } getCfop('0000'); // null getCfop('5350'); // null (título de subgrupo, não é um código operável) +getCfop('abc5102'); // null (não é uma forma documentada) ``` ## isValidCst @@ -1829,33 +1913,44 @@ Valida um código de CST (Código de Situação Tributária) para um tributo. In | Tributo | Formato | Códigos aceitos | | --- | --- | --- | -| `icms` | 3 dígitos (origem + CST) | origem `0`-`8` + um de `00`, `10`, `20`, `30`, `40`, `41`, `50`, `51`, `60`, `70`, `90` | +| `icms` | 3 dígitos (origem + CST) | origem `0`-`8` + um de `00`, `02`, `10`, `15`, `20`, `30`, `40`, `41`, `50`, `51`, `53`, `60`, `61`, `70`, `90` | | `ipi` | 2 dígitos | `00`, `01`, `02`, `03`, `04`, `05`, `49`, `50`, `51`, `52`, `53`, `54`, `55`, `99` | | `pis` | 2 dígitos | `01`-`09`, `49`, `50`-`56`, `60`-`67`, `70`-`75`, `98`, `99` | | `cofins` | 2 dígitos | mesma tabela do `pis` | `options.tax` (parte de `IsValidCstOptions`) é opcional: omita-o para aceitar um código que exista em qualquer uma das quatro tabelas acima. +A Tabela B do ICMS é a vigente: o [Anexo I consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), cuja redação atual veio do [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (efeitos a partir de 01.12.23) e que o [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) alterou revogando os itens 12, 13, 52, 72 e 74 (efeitos a partir de 09.07.24). `02`, `15`, `53` e `61` são seus códigos de monofasia de combustíveis. + +Uma string só é lida como código quando está escrita em uma das formas documentadas (os 2 ou 3 dígitos, com um único separador entre eles e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. + ```javascript import { isValidCst } from '@brazilian-utils/brazilian-utils'; isValidCst('000', { tax: 'icms' }); // true isValidCst('110', { tax: 'icms' }); // true +isValidCst('002', { tax: 'icms' }); // true (monofasia de combustíveis) isValidCst('06', { tax: 'pis' }); // true isValidCst('99', { tax: 'ipi' }); // true isValidCst('110'); // true (encontrado na tabela icms, tax omitido) isValidCst('999'); // false (não existe em nenhuma tabela) +isValidCst('abc110'); // false (não é uma forma documentada) +isValidCst(-110); // false (não é um inteiro seguro não negativo) ``` ## isValidCsosn -Valida se um código de CSOSN (Código de Situação da Operação no Simples Nacional) é um dos 10 códigos definidos pelo Ajuste SINIEF 03/2010: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` ou `900`. +Valida se um código de CSOSN (Código de Situação da Operação no Simples Nacional) é um dos 10 códigos do [Anexo III-A consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), a tabela instituída pelo Ajuste SINIEF 03/2010: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` ou `900`. + +Uma string só é lida como código quando está escrita em uma das formas documentadas (os 3 dígitos, com um único separador entre eles e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. ```javascript import { isValidCsosn } from '@brazilian-utils/brazilian-utils'; isValidCsosn('101'); // true isValidCsosn('999'); // false +isValidCsosn('abc101'); // false (não é uma forma documentada) +isValidCsosn(-101); // false (não é um inteiro seguro não negativo) ``` ## removeAccents diff --git a/docs/utilities.md b/docs/utilities.md index 44d56ce7..b57bc5b1 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -2,7 +2,7 @@ Here you will find all the utilities available for use. -> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function (including `capitalize`) returns an empty value of its return type: `""` for strings, `0` for `parseCurrency`. `formatCurrency` returns `""` for a non-finite number. +> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function (including `capitalize`) returns an empty value of its return type: `""` for strings, `0` for `parseCurrency`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. ## isValidCpf @@ -39,7 +39,7 @@ parseCpf('746.506.880-00'); // 74650688000 ## generateCpf -Generate a valid random CPF. +Generate a valid random CPF. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generateCpf } from '@brazilian-utils/brazilian-utils' @@ -112,7 +112,7 @@ generateCnpj(2); // alphanumeric CNPJ, e.g. 'Q0SLFMBD7VX439' ## isValidBoleto -Check if boleto ([brazilian payment method](https://en.wikipedia.org/wiki/Boleto)) is valid. Supports both the 47 digit "cobrança bancária" boleto and the "boleto de arrecadação" (convênio/tributos): either its 48 digit linha digitável or its 44 digit barcode, both starting with `8`. +Check if boleto ([brazilian payment method](https://en.wikipedia.org/wiki/Boleto)) is valid. Supports both the 47 digit "cobrança bancária" boleto and the "boleto de arrecadação" (convênio/tributos): either its 48 digit linha digitável or its 44 digit barcode, both starting with `8`. One leniency is kept from 2.3.0: the código de moeda in position 4 of the cobrança bancária barcode is not checked, although Carta-Circular BCB nº 2.926/2000 fixes it at `9` (real), so a slip carrying any other moeda digit still validates. ```javascript import { isValidBoleto } from '@brazilian-utils/brazilian-utils'; @@ -146,7 +146,7 @@ parseBoleto('00190.00009 01149.718601 68524.522114 6 75860000102656'); // 001900 ## generateBoleto -Generate a valid random boleto. Pass `{ type: "arrecadacao" }` (typed as `GenerateBoletoOptions`) to generate a boleto de arrecadação instead of the default "bancario" (cobrança bancária) type. +Generate a valid random boleto. Pass `{ type: "arrecadacao" }` (typed as `GenerateBoletoOptions`) to generate a boleto de arrecadação instead of the default "bancario" (cobrança bancária) type. An arrecadação slip draws its segment from 1 to 7 (segment 9 is the banks' own) and its value identifier from all four values, `6` and `8` for an effective amount and `7` and `9` for a reference quantity, so both `hasEffectiveValue` branches of `getBoletoInfo` are reachable. ```javascript import { generateBoleto } from '@brazilian-utils/brazilian-utils'; @@ -157,7 +157,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ## getBoletoInfo -Extract information from a boleto (amount, expiration date, bank code). Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). For a boleto de arrecadação, the result, typed as `BoletoInfo`, has no `bankCode`/`expirationDate` and instead carries `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. +Extract information from a boleto (amount, expiration date, bank code). Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. For a boleto de arrecadação, the result, typed as `BoletoInfo`, has no `bankCode`/`expirationDate` and instead carries `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -209,7 +209,7 @@ parsePixKey('+5551998259765'); // { type: 'phone', value: '+5551998259765' } ## isValidPixPayload -Check if a Pix BR Code payload (the string behind a Pix QR Code and behind "Pix copia e cola") is valid: well-formed TLV structure, the mandatory objects present, one of the "Merchant Account Information" templates carrying the `br.gov.bcb.pix` GUI with a key or a URL, a "Point of Initiation Method" object (`01`) that agrees with it (a key requires a static payload, so `01` is absent or `"11"`; a URL requires a dynamic one, so `01` is `"12"`), an amount (`54`) greater than zero in a static payload, and a matching CRC-16. The key itself is not checked against the DICT formats, use `isValidPixKey` for that. Payloads that carry the location in an Unreserved Template (IDs 80 to 99), as the "QR Code composto" of Pix Automático (Pix recorrente) does, are out of scope and reported as invalid. +Check if a Pix BR Code payload (the string behind a Pix QR Code and behind "Pix copia e cola") is valid: well-formed TLV structure, the mandatory objects present, one of the "Merchant Account Information" templates carrying the `br.gov.bcb.pix` GUI with a key or a URL, and a matching CRC-16. The "Point of Initiation Method" object (`01`) is advisory: the Manual do BR Code marks it optional and only assigns a meaning to the value `"12"` ("só pode ser utilizado uma vez"), so it may be absent from either shape and only a value outside `{"11", "12"}` makes the payload invalid. When a payload built around a key carries an amount (`54`), that amount must be greater than zero, unless the payload is a Pix Saque BR Code, i.e. unless it carries the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`) as §2.6 of the Pix manual prescribes; rejecting `"0"`/`"0.00"` without `fss` is a deliberate restriction of this library, not a rule of the manual. The key itself is not checked against the DICT formats, use `isValidPixKey` for that. Payloads that carry the location in an Unreserved Template (IDs 80 to 99), as the "QR Code composto" of Pix Automático (Pix recorrente) does, are out of scope and reported as invalid. ```javascript import { isValidPixPayload } from '@brazilian-utils/brazilian-utils'; @@ -224,7 +224,7 @@ isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (broken CRC) ## parsePixPayload -Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The result is typed as `PixPayload`; `pointOfInitiation` is typed as `PixPointOfInitiation` (`"static"` or `"dynamic"`). The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`), and the "Point of Initiation Method" object (`01`) must agree with it: a key belongs to a static payload (`01` absent or `"11"`) and a `url` to a dynamic one (`01` set to `"12"`), so any other pairing returns `null`. A static payload that states an amount must state one greater than zero (`54` set to `0.00` is reserved for the Pix Saque/Troco BR Code, which is out of scope), and in a dynamic payload the amount and the `txid` are ignored, as the manual mandates. Payloads whose location lives in an Unreserved Template (IDs 80 to 99, Pix Automático) are out of scope and return `null`. +Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The result is typed as `PixPayload`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Payloads whose location lives in an Unreserved Template (IDs 80 to 99, Pix Automático) are out of scope and return `null`. ```javascript import { parsePixPayload } from '@brazilian-utils/brazilian-utils'; @@ -236,13 +236,14 @@ parsePixPayload( // { // key: '123e4567-e12b-12d1-a456-426655440000', // merchantName: 'Fulano de Tal', -// merchantCity: 'BRASILIA' +// merchantCity: 'BRASILIA', +// pointOfInitiation: 'static' // } ``` ## generatePixPayload -Generates the payload of a Pix BR Code. Exactly one of `params.key` or `params.url` must be given (part of `GeneratePixPayloadParams`); `null` is returned when both or neither are given. `url` must be a PSP location as the Bacen manual defines it: a host name with a path, without a scheme (`pix.example.com/qr/v2/1234`); a dynamic payload cannot carry `amount` or `txid`, which belong to the PSP location, and an `amount` that rounds to `0.00` is rejected. +Generates the payload of a Pix BR Code. Exactly one of `params.key` or `params.url` must be given (part of `GeneratePixPayloadParams`); `null` is returned when both or neither are given. `url` must be a PSP location as the Bacen manual defines it: a host name with a path, without a scheme (`pix.example.com/qr/v2/1234`); a dynamic payload cannot carry `amount` or `txid`, which belong to the PSP location. The amount is written with the two decimal places the BR Code takes, so one that rounds to `0.00` and one that does not survive that round trip (`0.005`, `123.456`) are both rejected rather than written as a different sum. The Pix Saque BR Code, which announces the `fss` of sub-object 26-03, is parsed by `parsePixPayload` but not generated here. When `params.key` is given, it is normalized to its DICT canonical form by `parsePixKey` and the payload is static. When `params.url` is given instead (the PSP location, without a URL scheme, e.g. `"pix.example.com/qr/v2/1234"`), the payload is dynamic per the Manual de Padrões para Iniciação do Pix: the URL takes the key's place in the "Merchant Account Information" template and the "Point of Initiation Method" object is set to dynamic (`12`); `params.url` can be at most 77 characters. `merchantName`, `merchantCity` and `description` are folded to printable ASCII (accents dropped) and truncated to what the BR Code allows. `parsePixPayload` already parses both shapes, so `parsePixPayload(generatePixPayload({ url, ... }))` round-trips. @@ -269,21 +270,25 @@ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // nu ## isValidNfeKey -Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document that shares the same 44 digit layout: NF-e (modelo 55), NFC-e (modelo 65), CT-e (modelo 57), MDF-e (modelo 58) and CT-e OS (modelo 67, the Conhecimento de Transporte Eletrônico para Outros Serviços of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/aj_009_07)). Accepts whitespace between digit groups (the common display mask) and the `NFe` prefix found in the `Id` attribute of the document's XML. The emission type (`tpEmis`) must be one of the codes the MOC assigns, 1 to 7 or 9; 8 is not assigned and makes the key invalid. +Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e (65), CT-e (57), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para Outros Serviços of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), GTV-e (64, the CT-e Guia de Transporte de Valores), BP-e (63), NF3e (66) and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed differently. Accepts whitespace between digit groups (the common display mask) and the `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the document's XML. + +The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` for the CT-e, `{1, 5, 7, 8}` for the CT-e OS, `{1, 2, 7, 8}` for the GTV-e, `{1, 2, 3}` for the MDF-e and `{1, 2}` for the BP-e, the NF3e and the NFCom. Code 8, the authorização pela SVC-SP, is assigned by the [CT-e MOC 4.00](https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais) only, never by the NF-e one; the domains of the [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), the [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) and the [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) come from their own manuals. For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, which forbids the twenty repeated and sequential `cNF` values it lists and a `cNF` equal to the document number. Rejecting a document number of all zeros, on the other hand, is a choice of this library: no MOC rule was found forbidding it. ```javascript import { isValidNfeKey } from '@brazilian-utils/brazilian-utils'; isValidNfeKey('35170458716523000119550010000000121000123458'); // true (NF-e, SP) isValidNfeKey('NFe35170458716523000119550010000000121000123458'); // true (XML Id prefix) +isValidNfeKey('CTe35170458716523000119570010000000128000123452'); // true (CT-e authorised by the SVC-SP) isValidNfeKey('3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458'); // true (masked) isValidNfeKey('99170458716523000119550010000000121000123458'); // false (invalid cUF) -isValidNfeKey('35170458716523000119550010000000128000123455'); // false (tpEmis 8 is not assigned) +isValidNfeKey('35170458716523000119550010000000128000123455'); // false (the NF-e MOC does not assign tpEmis 8) +isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 00000000, rule B03-10) ``` ## formatNfeKey -Format a DF-e (NF-e, NFC-e, CT-e, MDF-e or CT-e OS) access key into groups of 4 digits separated by spaces, the common display form printed on the DANFE. +Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. ```javascript import { formatNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -294,7 +299,7 @@ formatNfeKey('35170458716523000119550010000000121000123458'); ## parseNfeKey -Parses a DF-e access key into its fields (state, year, month, taxId, model, series, number, emissionType, code, checkDigit). Accepts the same input forms as `isValidNfeKey` and returns `null` when the key is not valid. The result is typed as `NfeKey`. +Parses a DF-e access key into its fields (state, year, month, taxId, model, series, number, emissionType, code, checkDigit). Accepts the same input forms as `isValidNfeKey` and returns `null` when the key is not valid. The result is typed as `NfeKey`, whose `model` is an `NfeKeyModel`. NFCom (`'62'`) and NF3e (`'66'`) spend position 36 of the key on `nSiteAutoriz`, the site of the authorizer that received the document, so for those two models the result also carries `authorizationSite` and `code` is 7 digits instead of 8. ```javascript import { parseNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -303,6 +308,10 @@ parseNfeKey('35170458716523000119550010000000121000123458'); // { state: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '55', // series: 1, number: 12, emissionType: 1, code: '00012345', checkDigit: 8 } +parseNfeKey('35170458716523000119620010000000121000123450'); +// { state: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', +// series: 1, number: 12, emissionType: 1, authorizationSite: 0, code: '0012345', checkDigit: 0 } + parseNfeKey('invalid'); // null ``` @@ -332,7 +341,7 @@ isValidPhone('11900000000', { accept: [] }); // false ## formatPhone -Format phone number according to Brazilian patterns. `options.mask` (typed as `PhoneMask`) accepts `"sn"` (default, subscriber number only, 9 digits, no DDD), `"nanp"` (DDD + subscriber number, 11 digits), `"e164"` (`"+5511987654321"`), `"international"` (`"+55 11 98765-4321"`, the way a Brazilian number is printed for foreign callers), `"service"` (`"0800 123 4567"` or `"4004-1234"`, the conventional groupings for service numbers) or `"auto"`. `"auto"` picks `"international"` when `value` carries a Brazilian country code (`+55`, `0055` or a bare `55` followed by 10 or 11 digits), `"service"` when `value` is a service number, and otherwise falls back to the digit count: `"nanp"` when `value` has more digits than a bare subscriber number, `"sn"` when it does not. `"e164"` and `"international"` drop the country code from `value` first, under the rule documented in `parsePhone`, and fall back to the `"service"` presentation for a service number, since those have no E.164 form. If `value` includes a DDD, pass `{ mask: 'auto' }` (or `'nanp'`) explicitly, since the default `"sn"` mask assumes no DDD and silently truncates one if present. +Format phone number according to Brazilian patterns. `options.mask` (typed as `PhoneMask`) accepts `"sn"` (default, subscriber number only, 9 digits, no DDD), `"nanp"` (DDD + subscriber number, `"(00) 00000-0000"` for the 11 digits of a mobile and `"(00) 0000-0000"` for the 10 digits of a landline, any other length keeping the 11 digit grouping), `"e164"` (`"+5511987654321"`), `"international"` (`"+55 11 98765-4321"`, the way a Brazilian number is printed for foreign callers), `"service"` (`"0800 123 4567"` or `"4004-1234"`, the conventional groupings for service numbers) or `"auto"`. `"auto"` picks `"international"` when `value` carries a Brazilian country code (`+55`, `0055` or a bare `55` followed by 10 or 11 digits), `"service"` when `value` is a service number, and otherwise falls back to the digit count: `"nanp"` when `value` has more digits than a bare subscriber number, `"sn"` when it does not. `"e164"` and `"international"` drop the country code from `value` first, under the rule documented in `parsePhone`, and fall back to the `"service"` presentation for a service number, since those have no E.164 form. If `value` includes a DDD, pass `{ mask: 'auto' }` (or `'nanp'`) explicitly, since the default `"sn"` mask assumes no DDD and silently truncates one if present. A `mask` outside the union falls back to the default `"sn"` instead of throwing. ```javascript import { formatPhone } from '@brazilian-utils/brazilian-utils'; @@ -340,6 +349,8 @@ import { formatPhone } from '@brazilian-utils/brazilian-utils'; formatPhone('987654321'); // 98765-4321 (default "sn", no DDD) formatPhone('11900000000', { mask: 'nanp' }); // (11) 90000-0000 formatPhone('11900000000', { mask: 'auto' }); // (11) 90000-0000 +formatPhone('1130000000', { mask: 'nanp' }); // (11) 3000-0000 (10 digit landline) +formatPhone('1130000000', { mask: 'auto' }); // (11) 3000-0000 (10 digit landline) formatPhone('11987654321', { mask: 'e164' }); // +5511987654321 formatPhone('+5511987654321', { mask: 'international' }); // +55 11 98765-4321 formatPhone('08001234567', { mask: 'service' }); // 0800 123 4567 @@ -363,7 +374,7 @@ parsePhone('55987654321'); // 55987654321 (area code 55, not mistaken for the +5 ## isValidMobilePhone -Check if mobile phone number is valid. `options.version` (typed as `PhoneVersion`) controls which mobile numbering rule is enforced: `1` (default) is the pre-Resolução Anatel 749/2022 format, kept for 2.3.0 compatibility, whose first number digit (after the DDD) may be 6, 7, 8 or 9; `2` enforces only 9, a stricter subset of the resolution's art. 12 I (Serviço Móvel Pessoal). +Check if mobile phone number is valid. `options.version` (typed as `PhoneVersion`) controls which mobile numbering rule is enforced: `1` (default) is the pre-Resolução Anatel 749/2022 format, kept for 2.3.0 compatibility, whose first number digit (after the DDD) may be 6, 7, 8 or 9; `2` enforces only 9, a stricter subset of the resolution's art. 12 I (Serviço Móvel Pessoal). Version `1` also does not carve out the `700` prefix, which art. 12 II reserves for the Serviço Móvel Global por Satélite rather than SMP, so `isValidMobilePhone('11700123456')` is `true` for a number outside SMP; version `2` rejects it. ```javascript import { isValidMobilePhone } from '@brazilian-utils/brazilian-utils'; @@ -385,7 +396,7 @@ isValidLandlinePhone('1130000000'); // true ## isValidServicePhone -Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`; `112` and `911` are accepted too, as mobile-only aliases of `190` that Anatel lists alongside the other 3-digit codes). Only the structure is checked, the number does not have to be assigned to anyone. +Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`; `112` and `911` are accepted too, as mobile-only aliases of `190` that Anatel lists alongside the other 3-digit codes). Only the structure is checked, the number does not have to be assigned to anyone. Anatel publishes no allocation for the abbreviated numbers, so only the conventional `300X` and `400X` roots are recognised: other "Número Único" carrier prefixes in market use, such as `4020` and `4062`, are out of scope and are rejected. ```javascript import { isValidServicePhone } from '@brazilian-utils/brazilian-utils'; @@ -453,14 +464,17 @@ isValidLicensePlate('ABC1234EXTRA'); // false (too many characters) ## isValidRenavam -Check if RENAVAM (Registro Nacional de Veículos Automotores) is valid. Supports both the old format (9 digits) and the new format (11 digits). +Check if RENAVAM (Registro Nacional de Veículos Automotores) is valid. Supports both the old format (9 digits) and the new format (11 digits). Any spaces, dots and hyphens around/between the digits are ignored, but any other character, a letter in particular, makes the value invalid. A registration whose digits are all the same is rejected as well. ```javascript import { isValidRenavam } from '@brazilian-utils/brazilian-utils'; isValidRenavam('639884962'); // true (9 digits, old format) isValidRenavam('00639884962'); // true (11 digits, new format) +isValidRenavam('0063988.4962'); // true (dots and hyphens are ignored) isValidRenavam('12345678901'); // false (invalid checksum) +isValidRenavam('00000000000'); // false (repeated digits) +isValidRenavam('ab00639884962'); // false (letters are rejected) ``` ## isValidPis @@ -475,7 +489,7 @@ isValidPis('12056412547'); // false ## formatPis -Format PIS number. +Format PIS number. `options.pad` (part of `FormatPisOptions`) left-pads the value with zeros to the full 11 digits before masking. ```javascript import { formatPis } from '@brazilian-utils/brazilian-utils'; @@ -496,12 +510,13 @@ parsePis('123.45678.90-1'); // 12345678901 ## formatCep -Format CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). +Format CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (part of `FormatCepOptions`) left-pads the value with zeros to the full 8 digits before masking. ```javascript import { formatCep } from '@brazilian-utils/brazilian-utils'; formatCep('92500000'); // 92500-000 +formatCep('9250000', { pad: true }); // 09250-000 ``` ## parseCep @@ -516,7 +531,7 @@ parseCep('92500-000'); // 92500000 ## getAddressInfoByCep -Fetch address information for a given CEP using multiple providers. Defaults to `['viacep', 'brasilapi']`. The `'widenet'` provider is deprecated (its endpoint no longer responds) and excluded from the default list, but it can still be requested explicitly via `options.providers` (typed as `CepProvider[]`). The resolved address is typed as `AddressInfo`. +Fetch address information for a given CEP using multiple providers. Defaults to `['viacep', 'brasilapi']`. The `'widenet'` provider is deprecated (its endpoint no longer responds) and excluded from the default list, but it can still be requested explicitly via `options.providers` (typed as `CepProvider[]`). The resolved address is typed as `AddressInfo`. A transient network failure is retried twice per provider, with a 250 ms linear backoff (250 ms, then 500 ms), so a provider that keeps failing is tried up to 3 times and adds about 750 ms before the next provider is reached; an HTTP error status or a non-retryable failure is not retried. ```javascript import { getAddressInfoByCep } from '@brazilian-utils/brazilian-utils'; @@ -536,22 +551,25 @@ const address = await getAddressInfoByCep(1310100); ## isValidProcessoJuridico -Validate the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119). +Validate the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119). The CNJ mask separators (whitespace, `.` and `-`) are accepted between the `NNNNNNN-DD.AAAA.J.TR.OOOO` fields, but any other character, a letter in particular, makes the value invalid. ```javascript import { isValidProcessoJuridico } from '@brazilian-utils/brazilian-utils'; isValidProcessoJuridico('00020802520125150049'); // true +isValidProcessoJuridico('0002080-25.2012.5.15.0049'); // true (CNJ mask) +isValidProcessoJuridico('ab00020802520125150049'); // false (letters are rejected) ``` ## formatProcessoJuridico -Format the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119) (mask `NNNNNNN-DD.AAAA.J.TR.OOOO`). +Format the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119) (mask `NNNNNNN-DD.AAAA.J.TR.OOOO`). `options.pad` (part of `FormatProcessoJuridicoOptions`) left-pads the value with zeros to the full 20 digits before masking. ```javascript import { formatProcessoJuridico } from '@brazilian-utils/brazilian-utils'; formatProcessoJuridico('00020802520125150049'); // 0002080-25.2012.5.15.0049 +formatProcessoJuridico('20802520125150049', { pad: true }); // 0002080-25.2012.5.15.0049 ``` ## parseProcessoJuridico @@ -566,7 +584,7 @@ parseProcessoJuridico('0002080-25.2012.5.15.0049'); // 00020802520125150049 ## isValidIe -Check if inscrição estadual (state registration) is valid. The state code is case-insensitive. Notable per-state rules: GO accepts prefixes `10`, `11` and `15`; PA accepts `15` and `75`-`79`; MS accepts `28` and `50`; SP has a produtor rural pattern `P0MMMSSSSD000`; TO uses 11-digit type codes (`01`, `02`, `03`, `99`). +Check if inscrição estadual (state registration) is valid. The state code is case-insensitive. Notable per-state rules: GO accepts prefixes `10`, `11` and `15`; PA accepts `15` and `75`-`79`; MS accepts `28` and `50`; SP has a produtor rural pattern `P0MMMSSSSD000`; TO uses 11-digit type codes (`01`, `02`, `03`, `99`). TO also accepts a 9-digit form, applying the same modulus 11 rule to the first eight digits; the SINTEGRA page documents only the 11-digit one, so that shape is 2.3.0 behaviour kept for compatibility rather than a published rule. An all-zero registration is accepted wherever the published formula yields a check digit of 0 for it (AM, BA with 8 or 9 digits, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, SC, SE, SP and TO with 9 digits), unlike `isValidCpf` and `isValidCnpj`, which reject repeated digits. ```javascript import { isValidIe } from '@brazilian-utils/brazilian-utils'; @@ -587,7 +605,7 @@ Banks validated by their published check digit algorithm: | Santander | `033` | 4 digits | 8 digits | weights `9,7,3,1,0,0,9,7,1,3,1,9,7,3` over agency + `"00"` + account, tens discarded | | Banrisul | `041` | 4 digits | 9 digits | weights `3,2,4,7,6,5,4,3,2`; remainder 0 gives `0` and remainder 1 gives `6`; `account` is tipo (2 digits) + conta (7 digits) | | Caixa Econômica Federal | `104` | 4 digits | 11 digits | mod11 over agency + account; `account` is operação (3 digits) + conta (8 digits) | -| Bradesco | `237` | 4 digits | 7 digits | mod11 with weights 2..7; `digit` may be `"P"` (often rendered as `"0"`) | +| Bradesco | `237` | 4 digits | 7 digits | mod11 with weights 2..7; remainder 0 gives `0` and remainder 1 gives `"P"` | | Nubank | `260` | 4 digits | 5-13 digits | Verhoeff check digit over the account, leading zeros dropped | | Itaú Unibanco | `341` | 4 digits | 5 digits | mod10 over agency + account | | HSBC / Kirton Bank | `399` | 4 digits | 6 digits | weights `8,9,2,3,4,5,6,7,8,9` over agency + account; remainder 10 gives `0` | @@ -597,17 +615,15 @@ Banks validated by structure only, because they publish no check digit rule. The | Bank | Code | | Bank | Code | | --- | --- | --- | --- | --- | -| Inter | `077` | | PicPay | `380` | -| Ailos | `085` | | Cora | `403` | -| XP | `102` | | Pan | `623` | -| Unicred | `136` | | BV | `655` | -| Stone | `197` | | Daycoval | `707` | -| BTG Pactual | `208` | | Modal | `746` | -| Original | `212` | | Sicredi | `748` | -| PagBank | `290` | | Sicoob | `756` | -| BMG | `318` | | | | -| Mercado Pago | `323` | | | | -| C6 | `336` | | | | +| Inter | `077` | | Mercado Pago | `323` | +| Ailos | `085` | | C6 | `336` | +| XP | `102` | | PicPay | `380` | +| Unicred | `136` | | Cora | `403` | +| Stone | `197` | | Pan | `623` | +| BTG Pactual | `208` | | BV | `655` | +| Original | `212` | | Daycoval | `707` | +| PagBank | `290` | | Sicredi | `748` | +| BMG | `318` | | Sicoob | `756` | When `digit` has 2 characters, the generic fallback chains mod10 followed by mod11 over the account, the same way CPF/CNPJ check digits are chained. @@ -722,7 +738,7 @@ getBankByIspb('99999999'); // null ## isValidIban -Check if a Brazilian IBAN (International Bank Account Number) is valid, per Bacen's [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf) (Circular BCB nº 3.625/2013): `BR` + 2 ISO 7064 MOD 97-10 check digits + 8 digit ISPB + 5 digit branch + 10 digit account + 1 letter account type (any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 alphanumeric owner indicator, 29 characters total. Only Brazilian IBANs (country code `BR`) are recognized; any other country returns `false`, since this package does not carry the field layout of the other 90+ ISO 13616 countries. Accepts the usual grouping spaces and is case-insensitive. The value has to be written in the ISO 13616 print format: letters and digits in groups separated by a single space, with optional surrounding whitespace. Any other character makes the value something other than an IBAN, so it is rejected instead of being stripped. +Check if a Brazilian IBAN (International Bank Account Number) is valid, per Bacen's [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf) (Circular BCB nº 3.625/2013): `BR` + 2 ISO 7064 MOD 97-10 check digits + 8 digit ISPB + 5 digit branch + 10 digit account + 1 letter account type (any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 owner indicator (`1` for the first or only holder up to `9` for the ninth, then `A` to `Z` from the tenth, so `0` is rejected), 29 characters total. Only Brazilian IBANs (country code `BR`) are recognized; any other country returns `false`, since this package does not carry the field layout of the other 90+ ISO 13616 countries. Is case-insensitive and accepts both forms an IBAN is written in: compact (`'BR1500000000000010932840814P2'`) or in the ISO 13616 print format, letters and digits in groups separated by a single space, with optional surrounding whitespace either way. Only a character outside letters and digits, or a separator other than a single space, makes the value something other than an IBAN, so it is rejected instead of being stripped. ```javascript import { isValidIban } from '@brazilian-utils/brazilian-utils'; @@ -736,7 +752,7 @@ isValidIban('DE89370400440532013000'); // false (non Brazilian IBAN) ## formatIban -Format a Brazilian IBAN by grouping it in blocks of 4 characters, the ISO 13616 "print" presentation used on statements and bank forms. Does not validate the check digits or the field layout; formats whatever is given, up to the 29 character length of a Brazilian IBAN, as far as it goes, so the function can also be used as an input mask. Use `isValidIban` to check validity. The value still has to be written in the ISO 13616 print format (letters and digits in groups separated by a single space, with optional surrounding whitespace); any other character returns an empty string instead of being quietly dropped. +Format an IBAN in the ISO 13616 print grouping, blocks of 4 characters, the presentation used on statements and bank forms. Does not validate the check digits or the field layout; formats whatever is given, up to the 29 character length of a Brazilian IBAN, as far as it goes, so the function can also be used as an input mask, and an IBAN of another country is grouped the same way up to that length. Use `isValidIban` to check validity. The value may be compact (`'BR1500000000000010932840814P2'`), already in the ISO 13616 print format (letters and digits in groups separated by a single space) or a partial value still being typed (`'BR15'`), in every case with optional surrounding whitespace; only a character outside letters and digits, or a separator other than a single space, returns an empty string instead of being quietly dropped. ```javascript import { formatIban } from '@brazilian-utils/brazilian-utils'; @@ -749,7 +765,7 @@ formatIban('BR1500000000000010932840814P-2'); // '' (hyphens are not part of an ## parseIban -Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator). Accepts the same input forms as `isValidIban` (grouping spaces, lowercase) and returns `null` whenever `isValidIban` would return `false`, including a value carrying any character other than letters, digits and the grouping spaces of the print format. The result is typed as `Iban`, whose `accountType` is a `string`. +Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups separated by a single space), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying any character other than letters, digits and those single grouping spaces. The result is typed as `Iban`, whose `accountType` is a `string`. ```javascript import { parseIban } from '@brazilian-utils/brazilian-utils'; @@ -771,7 +787,7 @@ parseIban('BR1500000000000010932840814P-2'); // null (hyphens are not part of an ## isValidCreditCard -Check if a payment card number is valid using the Luhn algorithm ([ISO/IEC 7812-1](https://www.iso.org/standard/70484.html)). Accepts the usual mask characters (spaces, hyphens) between digits. Performs no brand detection (Visa, Mastercard, Amex...), issuer range lookup or expiration/CVV checks, only the digit count (12 to 19) and the Luhn check digit. A `number` is only accepted when it is a non-negative safe integer: anything above `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 digits) has already been rounded to a different number before the function sees it, so pass a longer PAN as a string. +Check if a payment card number is valid using the Luhn algorithm ([ISO/IEC 7812-1](https://www.iso.org/standard/70484.html)). Accepts the usual mask characters (spaces, hyphens) between digits and whitespace around the value; any other character makes the value invalid. Performs no brand detection (Visa, Mastercard, Amex...), issuer range lookup or expiration/CVV checks, only the digit count (12 to 19) and the Luhn check digit. A `number` is only accepted when it is a non-negative safe integer: anything above `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 digits) has already been rounded to a different number before the function sees it, so pass a longer PAN as a string. ```javascript import { isValidCreditCard } from '@brazilian-utils/brazilian-utils'; @@ -781,29 +797,42 @@ isValidCreditCard('5555555555554444'); // true (Mastercard test number) isValidCreditCard('378282246310005'); // true (American Express test number) isValidCreditCard('4111 1111 1111 1111'); // true (spaced mask) isValidCreditCard('4111111111111112'); // false (bad check digit) +isValidCreditCard('4111a1111b1111c1111'); // false (letters between the digits) isValidCreditCard(4111111111111111111); // false (above 2^53 - 1, pass it as a string) ``` ## capitalize -Transforms the first letter into a capital one of each word ignoring prepositions. Words are separated by whitespace, by `-` and by `/`, so `'MOGI-GUAÇU'` becomes `'Mogi-Guaçu'` and `'SANTANA/RS'` becomes `'Santana/Rs'`. Every run of whitespace (tabs, newlines, repeated spaces) collapses into a single space, and the leading and trailing whitespace is dropped. `options.upperCaseWords` defaults to `[]`, so no acronym is upper-cased unless you list it, and the comparison against both `upperCaseWords` and `lowerCaseWords` is case-insensitive (pt-BR locale). Options are typed as `CapitalizeOptions`. +Transforms the first letter into a capital one of each word, the way a Brazilian name, company name or address is written, with no options needed. Words are separated by whitespace, by `-` and by `/`, so `'MOGI-GUAÇU'` becomes `'Mogi-Guaçu'`. Every run of whitespace (tabs, newlines, repeated spaces) collapses into a single space, and the leading and trailing whitespace is dropped. + +`options.lowerCaseWords` defaults to the Portuguese prepositions, articles and conjunctions that stay in lower case inside a proper name (`de`, `da`, `do`, `e`, ...), except when one of them is the first word. `options.upperCaseWords` defaults to the company designations and document abbreviations written in upper case in Brazilian usage (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) plus the roman numerals that appear in names and addresses (`II` through `XXIII`, except `VI`, which collides with the pt-BR verb form "vi"). `SA` without punctuation is deliberately absent, since it is indistinguishable from the surname "Sá" typed without its accent, while `ME` does match the pronoun "me" (`'diga-me'` becomes `'Diga-ME'`), so pass your own `upperCaseWords` when the input is free text rather than a name. `S/A` and `S/S` are matched across the slash even though a slash separates words. A two letter word that follows a `/` is upper-cased when it is the code of a Brazilian state (`'porto alegre/rs'` becomes `'Porto Alegre/RS'`); that rule is structural and stays on even when `upperCaseWords` is given, while a state code that does not follow a `/` is left alone. + +Either list given in `options` replaces its default entirely, and the comparison against both is case-insensitive (pt-BR locale). Options are typed as `CapitalizeOptions`. ```javascript import { capitalize } from '@brazilian-utils/brazilian-utils'; -capitalize('josé e maria'); // José e Maria +capitalize('jose da silva'); // Jose da Silva +capitalize('JOSÉ DA SILVA'); // José da Silva +capitalize('empresa ltda'); // Empresa LTDA +capitalize('banco do brasil s.a.'); // Banco do Brasil S.A. +capitalize('casa de carnes s/a'); // Casa de Carnes S/A ("S/A" is matched across the slash) +capitalize('mogi-guaçu'); // Mogi-Guaçu ("-" starts a new word) +capitalize('santana/rs'); // Santana/RS ("RS" is a state code right after a "/") +capitalize('porto alegre/rs'); // Porto Alegre/RS +capitalize('santana rs'); // Santana Rs (no "/", so "rs" is just a word) +capitalize('rua xv de novembro'); // Rua XV de Novembro (roman numeral, "de" stays lower case) +capitalize('joão paulo ii'); // João Paulo II +capitalize('de'); // De (a preposition keeps its capital when it is the first word) +capitalize('empresa ltda', { upperCaseWords: [] }); // Empresa Ltda (the list given replaces the default one) capitalize('josé Ama MARIA', { lowerCaseWords: ['ama'] }); // José ama Maria -capitalize('doc inválido', { upperCaseWords: ['DOC'] }); // DOC Inválido -capitalize('MOGI-GUAÇU'); // Mogi-Guaçu ("-" starts a new word) -capitalize('SANTANA/RS', { upperCaseWords: ['RS'] }); // Santana/RS ("/" starts a new word, so "RS" matches) -capitalize('empresa ltda'); // Empresa Ltda (no default acronyms) -capitalize('empresa ltda', { upperCaseWords: ['LTDA'] }); // Empresa LTDA (case-insensitive match) +capitalize('doc inválido', { upperCaseWords: ['DOC'] }); // DOC Inválido (case-insensitive match) capitalize(' josé maria '); // José Maria (every run of whitespace, tabs and newlines included, collapses into one space) ``` ## formatCurrency -Formats an integer or float to a string in the BRL pattern. A `number` is formatted as-is (sign and decimals preserved). A `string` input is read by the same rule as `parseCurrency`, except that a value written without any separator stays in whole units: the last `,` or `.` followed by 1 to 2 digits is the decimal separator, every other `,` or `.` is a thousands separator, and a `-` written before the first digit is preserved. So `'1.234,56'` formats as `1.234,56`, `'-10.5'` as `-10,50` and `'1234'` as `1.234,00`. `precision` is clamped to `0..20` (the range `Intl.NumberFormat` accepts) and defaults to 2. A value that is not a finite number (`NaN`, `Infinity`, `-Infinity`) formats as an empty string. Options are typed as `FormatCurrencyOptions`. +Formats an integer or float to a string in the BRL pattern. A `number` is formatted as-is (sign and decimals preserved). A `string` input is read by the same rule as `parseCurrency`, except that a value written without any separator stays in whole units: the last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator, every other `,` or `.` is a thousands separator, and a `-` written before the first digit is preserved. So `'1.234,56'` formats as `1.234,56`, `'-10.5'` as `-10,50` and `'1234'` as `1.234,00`. `precision` is clamped to `0..20` (the range `Intl.NumberFormat` accepts), defaults to 2, and falls back to 2 when it is not a finite number. A value that is not a finite number (`NaN`, `Infinity`, `-Infinity`) formats as an empty string, and so does a value that cannot be coerced to a number (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. Options are typed as `FormatCurrencyOptions`. ```javascript import { formatCurrency } from '@brazilian-utils/brazilian-utils'; @@ -821,7 +850,7 @@ formatCurrency(Number.NaN); // "" (non finite numbers format as an empty string) ## parseCurrency -Transforms a string to an integer or float format. The last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator; every other `,` or `.` is a thousands separator. So `'R$ 1.234,56'` parses to `1234.56`, `'R$ 1.234'` to `1234`, `'1,5'` to `1.5` and `'12.34'` to `12.34`. A value written without any separator keeps the cents convention and is divided by `10 ** precision`, so `'1234'` parses to `12.34`. A `-` written before the first digit is preserved, so `'-R$ 1,00'` parses to `-1`. `precision` (default 2, clamped to `0..20`) controls how many digits are treated as minor units. Options are typed as `ParseCurrencyOptions`. +Transforms a string to an integer or float format. The last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator; every other `,` or `.` is a thousands separator. So `'R$ 1.234,56'` parses to `1234.56`, `'R$ 1.234'` to `1234`, `'1,5'` to `1.5` and `'12.34'` to `12.34`. A value written without any separator keeps the cents convention and is divided by `10 ** precision`, so `'1234'` parses to `12.34`. A `-` written before the first digit is preserved, so `'-R$ 1,00'` parses to `-1`. `precision` (default 2, clamped to `0..20`, and falling back to 2 when it is not a finite number) controls how many digits are treated as minor units. Options are typed as `ParseCurrencyOptions`. ```javascript import { parseCurrency } from '@brazilian-utils/brazilian-utils'; @@ -839,7 +868,7 @@ parseCurrency(''); // 0 ## convertNumberToWords -Formats an integer as its Brazilian Portuguese cardinal number words ("por extenso"), e.g. `1235` becomes `"mil, duzentos e trinta e cinco"`. Only integers from `-999999999999999` to `999999999999999` (999 trillion in absolute value) are supported; anything outside that range, `NaN` or a non-finite value returns `""`. A non-integer `value` is truncated toward zero before conversion. `options.gender` (part of `ConvertNumberToWordsOptions`) agrees "um/dois" and the hundreds group ("duzentos/duzentas", etc.) with the noun the number qualifies, defaulting to `"masculine"`. `options.case` sets the letter case of the result: `"lower"` (default, unchanged), `"sentence"` (capitalizes only the first letter) or `"upper"` (uppercases everything with the "pt-BR" locale, keeping accents, e.g. "três" -> "TRÊS"). An invalid `gender`/`case` value is ignored and the default is used. +Formats an integer as its Brazilian Portuguese cardinal number words ("por extenso"), e.g. `1235` becomes `"mil, duzentos e trinta e cinco"`. Only integers from `-999999999999999` to `999999999999999` (999 trillion in absolute value) are supported; anything outside that range, `NaN` or a non-finite value returns `""`. A non-integer `value` is truncated toward zero before conversion. `options.gender` (part of `ConvertNumberToWordsOptions`) agrees "um/dois" and the hundreds group ("duzentos/duzentas", etc.) with the noun the number qualifies, defaulting to `"masculine"`. An invalid `gender` value is ignored and the default is used. The result is always lowercase; apply any other casing to it yourself. ```javascript import { convertNumberToWords } from '@brazilian-utils/brazilian-utils'; @@ -849,13 +878,13 @@ convertNumberToWords(1001); // "mil e um" convertNumberToWords(2000000); // "dois milhões" convertNumberToWords(-42); // "menos quarenta e dois" convertNumberToWords(2, { gender: 'feminine' }); // "duas" -convertNumberToWords(3, { case: 'upper' }); // "TRÊS" +convertNumberToWords(12.9); // "doze" (truncated toward zero) convertNumberToWords(NaN); // "" ``` ## convertCurrencyToWords -Formats a monetary amount in Brazilian Reais as its "por extenso" textual representation, the style used to write out the amount by hand on cheques and contracts, e.g. `1523.45` becomes `"mil, quinhentos e vinte e três reais e quarenta e cinco centavos"`. `value` is truncated (not rounded) to 2 decimal places. The singular noun is used for exactly 1 ("um real", "um centavo") and "de" is inserted before "reais" when the amount is a round million, billion or trillion of reais. An amount that truncates to nothing becomes `"zero reais"` with no "menos" prefix, any other negative amount is prefixed with "menos", and invalid input returns `""`. Above `Number.MAX_SAFE_INTEGER / 100` reais (about 90 trillion) a double cannot carry cents, so the amount is read as a whole number of reais. `options.case` (part of `ConvertCurrencyToWordsOptions`) sets the letter case of the result: `"lower"` (default), `"sentence"` (capitalizes only the first letter) or `"upper"` (uppercases everything, keeping accents). An invalid `case` value is ignored and `"lower"` is used. +Formats a monetary amount in Brazilian Reais as its "por extenso" textual representation, the style used to write out the amount by hand on cheques and contracts, e.g. `1523.45` becomes `"mil, quinhentos e vinte e três reais e quarenta e cinco centavos"`. `value` is truncated (not rounded) to 2 decimal places. The singular noun is used for exactly 1 ("um real", "um centavo") and "de" is inserted before "reais" when the amount is a round million, billion or trillion of reais. An amount that truncates to nothing becomes `"zero reais"` with no "menos" prefix, any other negative amount is prefixed with "menos", and invalid input returns `""`. Above `Number.MAX_SAFE_INTEGER / 100` reais (about 90 trillion) a double cannot carry cents, so the amount is read as a whole number of reais. It takes no options: the result is always lowercase; apply any other casing to it yourself. ```javascript import { convertCurrencyToWords } from '@brazilian-utils/brazilian-utils'; @@ -867,7 +896,6 @@ convertCurrencyToWords(1000000); // "um milhão de reais" convertCurrencyToWords(0); // "zero reais" convertCurrencyToWords(-5.5); // "menos cinco reais e cinquenta centavos" convertCurrencyToWords(-0.001); // "zero reais" (truncates to nothing) -convertCurrencyToWords(1000, { case: 'upper' }); // "MIL REAIS" ``` ## getStates @@ -969,7 +997,7 @@ getTimezoneByState('ZZ'); // null ## getCities -Get Brazilian cities. Returns all cities if no state is provided, or cities from a specific state. Each call returns a fresh array, so mutating the result never affects subsequent calls. An unknown state code (or a non-`StateCode` value) returns an empty array instead of throwing. +Get Brazilian cities. Returns all cities if no state is provided, or cities from a specific state. Each call returns a fresh array, so mutating the result never affects subsequent calls. An unknown state code (or a non-`StateCode` value) returns an empty array instead of throwing, except for a falsy one: `getCities(null)` and `getCities('')` are read as "no state given" and return every city, where the stricter `getMunicipalities` returns `[]` for them. ```javascript import { getCities } from '@brazilian-utils/brazilian-utils'; @@ -1007,11 +1035,22 @@ getCities('SP'); // ] ``` -`getCities` embeds all 5571 IBGE municipality names (~153 KB minified, ~49 KB gzipped) and is one of the few heavy exceptions in this otherwise tree-shakeable package. See [Bundle size](getting-started.md#bundle-size) for how to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities` instead of the root import. +`getCities` embeds all 5571 IBGE municipality names (~153.6 KB minified, ~49.4 KB gzipped) and is one of the few heavy exceptions in this otherwise tree-shakeable package. See [Bundle size](getting-started.md#bundle-size) for how to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities` instead of the root import. ## getHolidays -Get Brazilian holidays for a given year. Returns national holidays and optionally state-specific holidays. Each holiday (typed as `Holiday`) has a `type` field (`HolidayType`: `"national"`, `"state"`, `"optional"` or `"religious"`). "Dia da Consciência Negra" (Nov 20) is a national holiday from 2024 onward (Lei nº 14.759/2023). Before that, MT and RJ still carry their own state-level entry named `"Consciência Negra"` on the same date. Results are memoized per `year`/`stateCode`, but each call still returns a fresh copy. An unknown/invalid `stateCode` is ignored, returning national holidays only. +Get Brazilian holidays for a given year. Returns national holidays and optionally state-specific holidays. Each holiday (typed as `Holiday`) has a `type` field (`HolidayType`: `"national"`, `"state"`, `"optional"` or `"religious"`). "Dia da Consciência Negra" (Nov 20) is a national holiday from 2024 onward (Lei nº 14.759/2023). Before that, MT and RJ still carry their own state-level entry named `"Consciência Negra"` on the same date. Results are memoized per `year`/`stateCode`, but each call still returns a fresh copy. An unknown/invalid `stateCode` is ignored, returning national holidays only; the lookup reads own properties only, so `"__proto__"`, `"constructor"` and the like are unknown state codes rather than a crash. + +Only one state holiday per UF is a feriado civil under [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm), art. 1º, II, which authorises "a data magna do Estado fixada em lei estadual" in the singular; the other entries rest on ordinary state laws and are reported because they are observed in practice. Notable per-state rules: + +- **SC** — [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) moves both state holidays, "Dia do Estado de Santa Catarina" (Aug 11) and "Dia de Santa Catarina de Alexandria" (Nov 25), to the following Sunday whenever they fall Monday to Friday, so Monday Aug 11 2025 is a business day in SC and the holiday lands on Sunday Aug 17. +- **DF** — [Lei distrital nº 72/1989](https://www.sinj.df.gov.br/sinj/Norma/18459/Lei_72_27_12_1989.html), art. 1º parágrafo único, declares Corpus Christi a feriado. With `stateCode: 'DF'` the single Corpus Christi entry comes back typed `"state"` instead of `"optional"`; it is replaced, not duplicated. +- **GO** — [Lei GO nº 20.756/2020](https://legisla.casacivil.go.gov.br/pesquisa_legislacao/100979/lei-20756), art. 269, II, lists three feriados estaduais: Jul 26 (Fundação da Cidade de Goiás), Oct 24 (Lançamento da Pedra Fundamental de Goiânia) and Oct 28 (Dia do Servidor Público). +- **AL** — Sep 16 is a feriado estadual from 2024 ([Lei AL nº 9.358/2024](https://sapl.al.al.leg.br/norma/3117)) and only a ponto facultativo (`"optional"`) before that. +- **PB** — Jul 26 ("Morte de João Pessoa") is emitted up to 2015 only: [Lei PB nº 10.601/2015](https://sapl.al.pb.leg.br/norma/11988), art. 2º, revoked its basis. +- **TO** — Mar 18 ("Autonomia do Estado do Tocantins") is emitted up to 2008 only: [Lei TO nº 2.013/2009](https://www.al.to.leg.br/arquivo/15724) rewrote the clause that declared the feriado into a commemorative provision. + +The statutory date is what is returned. SC's shift above is the only observance shift modelled; Acre's Tuesday-to-Thursday shift and the Goiás decrees that may move Jul 26 and Oct 28 are not. ```javascript import { getHolidays } from '@brazilian-utils/brazilian-utils'; @@ -1058,7 +1097,7 @@ formatPassport('AB-123.456'); // 'AB123456' ## generatePassport -Generate a random valid Brazilian passport number. +Generate a random valid Brazilian passport number. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generatePassport } from '@brazilian-utils/brazilian-utils'; @@ -1079,7 +1118,7 @@ parsePassport(' AB 123 456 '); // 'AB123456' ## generateCep -Generate a random CEP. +Generate a random CEP. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generateCep } from '@brazilian-utils/brazilian-utils'; @@ -1089,7 +1128,7 @@ generateCep(); // '92500000' ## formatCnh -Format CNH. +Format CNH. `options.pad` (part of `FormatCnhOptions`) left-pads the value with zeros to the full 11 digits before masking. ```javascript import { formatCnh } from '@brazilian-utils/brazilian-utils'; @@ -1100,17 +1139,19 @@ formatCnh('2650306461', { pad: true }); // 026503064-61 ## isValidCnh -Check if CNH is valid. +Check if CNH is valid. Spaces, dots and hyphens around/between the digits are ignored, but any other character, a letter in particular, makes the value invalid. ```javascript import { isValidCnh } from '@brazilian-utils/brazilian-utils'; isValidCnh('00000000119'); // true +isValidCnh('000000001-19'); // true (hyphen before the check digits) +isValidCnh('ab00000000119'); // false (letters are rejected) ``` ## generateCnh -Generate a valid random CNH. +Generate a valid random CNH. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generateCnh } from '@brazilian-utils/brazilian-utils'; @@ -1143,9 +1184,9 @@ const ceps = await getCepInfoByAddress({ // [ // { -// cep: '01310100', +// cep: '01310-100', // logradouro: 'Avenida Paulista', -// complemento: 'lado par', +// complemento: 'de 612 a 1510 - lado par', // bairro: 'Bela Vista', // localidade: 'São Paulo', // uf: 'SP' @@ -1188,7 +1229,7 @@ isValidLegalNature('9999'); // false ## generateLegalNature -Generate a random valid legal nature code. +Generate a random valid legal nature code. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generateLegalNature } from '@brazilian-utils/brazilian-utils'; @@ -1226,6 +1267,8 @@ Look a legal nature code up in the official IBGE/CONCLA table. import { getLegalNature } from '@brazilian-utils/brazilian-utils'; getLegalNature('2062'); // { code: '2062', description: 'Sociedade Empresária Limitada' } +getLegalNature('206-2'); // { code: '2062', description: 'Sociedade Empresária Limitada' } +getLegalNature(206.2); // { code: '2062', description: 'Sociedade Empresária Limitada' } getLegalNature('0000'); // null ``` @@ -1255,7 +1298,7 @@ formatLicensePlate('abc1d23'); // 'ABC1D23' ## generateLicensePlate -Generate a random license plate in the chosen format. +Generate a random license plate in the chosen format. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generateLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -1337,7 +1380,7 @@ await getMunicipality({ code: '123' }); ## getMunicipalities -Get Brazilian municipalities published by the IBGE. Returns all municipalities if no state is provided, or municipalities from a specific state. Each municipality is returned as `{ code, name, stateCode }`, where `code` is the 7-digit IBGE municipality code. Results are sorted by name with `localeCompare` in the "pt-BR" locale. Each call returns a fresh array of fresh objects, so mutating the result never affects subsequent calls. An unknown state code returns an empty array instead of throwing. +Get Brazilian municipalities published by the IBGE. Returns all municipalities if no state is provided, or municipalities from a specific state. Each municipality is returned as `{ code, name, stateCode }`, where `code` is the 7-digit IBGE municipality code. Results are sorted by name with `localeCompare` in the "pt-BR" locale. Each call returns a fresh array of fresh objects, so mutating the result never affects subsequent calls. An unknown state code returns an empty array instead of throwing. Only an omitted (or `undefined`) `stateCode` asks for the full list: `getMunicipalities(null)` and `getMunicipalities('')` return `[]`, where the looser `getCities(null)` and `getCities('')` return every city. ```javascript import { getMunicipalities } from '@brazilian-utils/brazilian-utils'; @@ -1400,7 +1443,7 @@ isHoliday(); // false ## isBusinessDay -Check if a date is a Brazilian business day (dia útil). Returns `false` for Saturdays, Sundays, and Brazilian holidays returned by `getHolidays` for `value`'s local calendar day (year/month/day as read locally), the same convention used by `isHoliday`. `options.includeOptional` (part of `IsBusinessDayOptions`) defaults to `true`, so optional-type holidays (`Holiday.type === "optional"`, i.e. Carnaval and Corpus Christi) also count as non-business days; pass `false` to only treat statutory holidays this way. `options.stateCode` also considers that state's holidays; an unknown/invalid `stateCode` is ignored, falling back to national holidays only. A `value` that is not a valid `Date` returns `false`. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `false`. +Check if a date is a Brazilian business day (dia útil). Returns `false` for Saturdays, Sundays, and Brazilian holidays returned by `getHolidays` for `value`'s local calendar day (year/month/day as read locally), the same convention used by `isHoliday`. `options.includeOptional` (part of `BusinessDayOptions`, the option type every business day utility shares) defaults to `true`, so optional-type holidays (`Holiday.type === "optional"`, i.e. Carnaval and Corpus Christi) also count as non-business days; pass `false` to only treat statutory holidays this way. `options.stateCode` also considers that state's holidays; an unknown/invalid `stateCode` is ignored, falling back to national holidays only. A `value` that is not a valid `Date` returns `false`. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `false`. ```javascript import { isBusinessDay } from '@brazilian-utils/brazilian-utils'; @@ -1417,38 +1460,55 @@ isBusinessDay(new Date('not a date')); // false ## addBusinessDays -Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and Brazilian holidays exactly as `isBusinessDay` defines them (same `stateCode`/`includeOptional` options). Returns a new `Date`; the input `date` (part of `AddBusinessDaysParams`) is never mutated, and its time-of-day is preserved in the result. `days: 0` returns a new `Date` equal to `date`, unchanged, even when `date` itself falls on a weekend or holiday, this mirrors the verified behavior of [date-fns' `addBusinessDays(date, 0)`](https://date-fns.org/docs/addBusinessDays), which also does not roll the input to the next business day. A negative `days` walks backwards, one business day at a time, also like date-fns. Returns `null` on bad input: a `date` that is not a valid `Date`, a `days` that is not a finite integer, or a `stateCode` that is not a string. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it (or, for `addBusinessDays`, a walk that leaves it) returns `null`. +Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and Brazilian holidays exactly as `isBusinessDay` defines them (same `BusinessDayOptions`). The signature is date-fns': `addBusinessDays(date, amount, options?)`. Returns a new `Date`; the input `date` is never mutated, and its time-of-day is preserved in the result. An `amount` of `0` returns a new `Date` equal to `date`, unchanged, even when `date` itself falls on a weekend or holiday, this mirrors the verified behavior of [date-fns' `addBusinessDays(date, 0)`](https://date-fns.org/docs/addBusinessDays), which also does not roll the input to the next business day. A negative `amount` walks backwards, one business day at a time, also like date-fns. Returns `null` on bad input: a `date` that is not a valid `Date`, an `amount` that is not a finite integer, or a `stateCode` that is not a string; an `options` that is not an object at all is ignored, exactly as `isBusinessDay` ignores it. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it, or a walk that leaves it, returns `null`. ```javascript import { addBusinessDays } from '@brazilian-utils/brazilian-utils'; -addBusinessDays({ date: new Date(2024, 0, 2, 12), days: 1 }); // Date, 2024-01-03 12:00 (next day is already a business day) -addBusinessDays({ date: new Date(2024, 11, 31, 12), days: 1 }); // Date, 2025-01-02 12:00 (2025-01-01 is Ano novo, skipped) -addBusinessDays({ date: new Date(2024, 0, 5, 12), days: -1 }); // Date, 2024-01-04 12:00 (walks backwards) -addBusinessDays({ date: new Date(2024, 0, 6, 12), days: 0 }); // Date, 2024-01-06 12:00 (unchanged, even though Saturday is not a business day) -addBusinessDays({ date: new Date(2024, 6, 8, 12), days: 1, stateCode: 'SP' }); // Date, 2024-07-10 12:00 (2024-07-09 is Revolução Constitucionalista in SP, skipped) -addBusinessDays({ date: new Date('not a date'), days: 1 }); // null -addBusinessDays({ date: new Date(2024, 0, 2), days: 1.5 }); // null (not an integer) +addBusinessDays(new Date(2024, 0, 2, 12), 1); // Date, 2024-01-03 12:00 (next day is already a business day) +addBusinessDays(new Date(2024, 11, 31, 12), 1); // Date, 2025-01-02 12:00 (2025-01-01 is Ano novo, skipped) +addBusinessDays(new Date(2024, 0, 5, 12), -1); // Date, 2024-01-04 12:00 (walks backwards) +addBusinessDays(new Date(2024, 0, 6, 12), 0); // Date, 2024-01-06 12:00 (unchanged, even though Saturday is not a business day) +addBusinessDays(new Date(2024, 6, 8, 12), 1, { stateCode: 'SP' }); // Date, 2024-07-10 12:00 (2024-07-09 is Revolução Constitucionalista in SP, skipped) +addBusinessDays(new Date('not a date'), 1); // null +addBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) +``` + +## subBusinessDays + +Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too. A negative `amount` walks forwards. + +```javascript +import { subBusinessDays } from '@brazilian-utils/brazilian-utils'; + +subBusinessDays(new Date(2024, 0, 5, 12), 1); // Date, 2024-01-04 12:00 (previous day is already a business day) +subBusinessDays(new Date(2024, 0, 8, 12), 1); // Date, 2024-01-05 12:00 (walks back over the weekend) +subBusinessDays(new Date(2025, 0, 2, 12), 1); // Date, 2024-12-31 12:00 (2025-01-01 is Ano novo, skipped) +subBusinessDays(new Date(2024, 0, 5, 12), -1); // Date, 2024-01-08 12:00 (walks forwards) +subBusinessDays(new Date(2024, 0, 6, 12), 0); // Date, 2024-01-06 12:00 (unchanged, even though Saturday is not a business day) +subBusinessDays(new Date(2024, 6, 10, 12), 1, { stateCode: 'SP' }); // Date, 2024-07-08 12:00 (2024-07-09 is Revolução Constitucionalista in SP, skipped) +subBusinessDays(new Date('not a date'), 1); // null +subBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) ``` ## differenceInBusinessDays -Count the number of Brazilian business days (dias úteis) between two dates, mirroring the semantics of [date-fns' `differenceInBusinessDays`](https://date-fns.org/docs/differenceInBusinessDays) (verified against its source): `params.from` is counted when it is itself a business day, `params.to` is never counted, and every business day strictly in between is counted once. Only the calendar day of each `Date` matters, the time of day is ignored. Business days are determined exactly like `isBusinessDay` (same `stateCode`/`includeOptional` options). `from`/`to` on the same calendar day return `0`; a `to` before `from` returns a negative number. Returns `null` on bad input: a `from`/`to` that is not a valid `Date`, or a `stateCode` that is not a string. Parameters are typed as `DifferenceInBusinessDaysParams`. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it (or, for `addBusinessDays`, a walk that leaves it) returns `null`. +Count the number of Brazilian business days (dias úteis) between two dates, mirroring the semantics of [date-fns' `differenceInBusinessDays`](https://date-fns.org/docs/differenceInBusinessDays) (verified against its source), argument order included: `differenceInBusinessDays(laterDate, earlierDate, options?)`. The walk starts at `earlierDate` and stops just before `laterDate`, so `earlierDate` is counted when it is itself a business day, `laterDate` is never counted, and every business day strictly in between is counted once. Only the calendar day of each `Date` matters, the time of day is ignored. Business days are determined exactly like `isBusinessDay` (same `BusinessDayOptions`). The result is positive when `laterDate` is after `earlierDate` and negative when it is before it; two dates on the same calendar day return `0`. Returns `null` on bad input: a date that is not a valid `Date`, or a `stateCode` that is not a string; an `options` that is not an object at all is ignored. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `null`. ```javascript import { differenceInBusinessDays } from '@brazilian-utils/brazilian-utils'; -differenceInBusinessDays({ from: new Date(2024, 0, 1), to: new Date(2024, 0, 2) }); // 0 (Jan 1 is Ano novo) -differenceInBusinessDays({ from: new Date(2024, 0, 2), to: new Date(2024, 0, 3) }); // 1 (Jan 2 counted, a Tuesday) -differenceInBusinessDays({ from: new Date(2024, 0, 3), to: new Date(2024, 0, 2) }); // -1 (to before from) -differenceInBusinessDays({ from: new Date(2024, 0, 2), to: new Date(2024, 0, 2) }); // 0 (same day) -differenceInBusinessDays({ from: new Date(2024, 6, 8), to: new Date(2024, 6, 10), stateCode: 'SP' }); // 1 (2024-07-09 is a state holiday in SP) -differenceInBusinessDays({ from: new Date('not a date'), to: new Date() }); // null +differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 1)); // 0 (Jan 1 is Ano novo, not counted) +differenceInBusinessDays(new Date(2024, 0, 3), new Date(2024, 0, 2)); // 1 (Jan 2 counted, a Tuesday; Jan 3 is not) +differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 3)); // -1 (the later date comes first, so the count is negative) +differenceInBusinessDays(new Date(2024, 0, 2), new Date(2024, 0, 2)); // 0 (same day) +differenceInBusinessDays(new Date(2024, 6, 10), new Date(2024, 6, 8), { stateCode: 'SP' }); // 1 (2024-07-09 is a state holiday in SP) +differenceInBusinessDays(new Date(), new Date('not a date')); // null ``` ## convertDateToWords -Formats a date as its Brazilian Portuguese "por extenso" textual representation, e.g. `"01/01/2024"` becomes `"primeiro de janeiro de dois mil e vinte e quatro"`. Accepts a `Date` (read by its local calendar date, the same convention used by `isHoliday`) or a string in `"dd/mm/yyyy"` or ISO `"yyyy-mm-dd"` format. With the default `options.style` of `"full"`, day 1 is written as "primeiro" and every other day uses the cardinal number; with `"month"`, only the month name is spelled out and the day/year are left as digits (day 1 as `"1º"`, e.g. `"2 de março de 2024"`, `"1º de janeiro de 2024"`). Month names are lowercase. In `"full"` style the year is written out as a cardinal number without the thousands comma that `convertNumberToWords`/`convertCurrencyToWords` use (`1999` reads as `"mil novecentos e noventa e nove"`, not `"mil, novecentos e noventa e nove"`), matching how a date is read aloud. `options.weekday` (default `false`) prefixes the pt-BR weekday name in lowercase followed by a comma (`"sábado, dois de março de dois mil e vinte e quatro"`), computed from the resolved calendar date. `options.case` sets the letter case of the whole result: `"lower"` (default), `"sentence"` (capitalizes only the first letter) or `"upper"` (uppercases everything, keeping accents). Invalid `case`/`style` values are ignored and the default is used. February 29th is accepted on the leap years of the proleptic Gregorian calendar (divisible by 4, except centuries not divisible by 400). Returns `""` for an invalid `Date`, a malformed string, a day/month that does not exist, or a date before year 1. +Formats a date as its Brazilian Portuguese "por extenso" textual representation, e.g. `"01/01/2024"` becomes `"primeiro de janeiro de dois mil e vinte e quatro"`. Accepts a `Date` (read by its local calendar date, the same convention used by `isHoliday`) or a string in `"dd/mm/yyyy"` or ISO `"yyyy-mm-dd"` format. With the default `options.style` of `"full"`, day 1 is written as "primeiro" and every other day uses the cardinal number; with `"month"`, only the month name is spelled out and the day/year are left as digits (day 1 as `"1º"`, e.g. `"2 de março de 2024"`, `"1º de janeiro de 2024"`). Month names are lowercase. In `"full"` style the year is written out as a cardinal number without the thousands comma that `convertNumberToWords`/`convertCurrencyToWords` use (`1999` reads as `"mil novecentos e noventa e nove"`, not `"mil, novecentos e noventa e nove"`), matching how a date is read aloud. `options.weekday` (default `false`) prefixes the pt-BR weekday name in lowercase followed by a comma (`"sábado, dois de março de dois mil e vinte e quatro"`), computed from the resolved calendar date. An invalid `style` value is ignored and the default is used. The result is always lowercase; apply any other casing to it yourself. February 29th is accepted on the leap years of the proleptic Gregorian calendar (divisible by 4, except centuries not divisible by 400). Returns `""` for an invalid `Date`, a malformed string, a day/month that does not exist, or a date before year 1. ```javascript import { convertDateToWords } from '@brazilian-utils/brazilian-utils'; @@ -1456,7 +1516,6 @@ import { convertDateToWords } from '@brazilian-utils/brazilian-utils'; convertDateToWords('01/01/2024'); // "primeiro de janeiro de dois mil e vinte e quatro" convertDateToWords('2024-01-02'); // "dois de janeiro de dois mil e vinte e quatro" convertDateToWords(new Date(2024, 0, 1)); // "primeiro de janeiro de dois mil e vinte e quatro" -convertDateToWords('01/01/2024', { case: 'sentence' }); // "Primeiro de janeiro de dois mil e vinte e quatro" convertDateToWords('02/03/2024', { style: 'month' }); // "2 de março de 2024" convertDateToWords('01/01/2024', { style: 'month' }); // "1º de janeiro de 2024" convertDateToWords('02/03/2024', { weekday: true }); // "sábado, dois de março de dois mil e vinte e quatro" @@ -1479,7 +1538,7 @@ formatVoterId('1234567880191'); // '1234 5678 8 01 91' (13-digit SP/MG voter id) ## isValidVoterId -Check if a voter ID number is valid. Accepts both the standard 12-digit id and the 13-digit id issued by São Paulo (UF `01`) and Minas Gerais (UF `02`). +Check if a voter ID number is valid. Accepts both the standard 12-digit id and the 13-digit id issued by São Paulo (UF `01`) and Minas Gerais (UF `02`). Whitespace and dots are accepted around and between the `0000 0000 00 00` groups, but any other character, a letter in particular, makes the value invalid. ```javascript import { generateVoterId, isValidVoterId } from '@brazilian-utils/brazilian-utils'; @@ -1516,6 +1575,8 @@ parseVoterId('1234 5678 8 01 91'); // '1234567880191' (13-digit SP/MG voter id) Check if a CNS (Cartão Nacional de Saúde) number is valid, the unique SUS (Sistema Único de Saúde) user identifier. Definitive cards (starting with 1 or 2) are validated with the same mod 11 weighting used for PIS numbers over an embedded 11 digit base, adjusting the base by +2 when the raw check digit computes to 10. Provisional cards (starting with 7, 8 or 9) are validated instead by a single weighted sum (weights 15 down to 1) that must be a multiple of 11. The value has to be written as the 15 digits, optionally split into the printed groups of 3-4-4-4 by whitespace or the usual mask characters; letters among the digits are rejected instead of being read past. +The two routines come from the [ANVISA CNS validation page](https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/), which sits behind a bot filter and answers HTTP 403 to non-browser clients. The [e-SUS APS page](https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html) documents the same algorithm and is reachable without a browser, but applies the provisional routine to numbers starting with 5, 7, 8 or 9; this implementation follows ANVISA and rejects a 5-prefixed number even when its weighted sum checks out. + ```javascript import { isValidCns } from '@brazilian-utils/brazilian-utils'; @@ -1532,14 +1593,14 @@ Format a CNS (Cartão Nacional de Saúde) number into the common display groups ```javascript import { formatCns } from '@brazilian-utils/brazilian-utils'; -formatCns('123456789010001'); // '123 4567 8901 0001' -formatCns(123456789010001); // '123 4567 8901 0001' +formatCns('123456789010000'); // '123 4567 8901 0000' +formatCns(123456789010000); // '123 4567 8901 0000' formatCns('89010001', { pad: true }); // '000 0000 8901 0001' ``` ## isValidCertidao -Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with weights cycling from 2 to 10 and back through 0. Accepts the usual mask characters and whitespace between/around groups. The layout is the in-force one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023, in the wording of the Provimento CN nº 182/2024); the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). +Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with weights cycling from 2 to 10 and back through 0. Accepts the usual mask characters and whitespace between/around groups. The layout is the one [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023) currently publishes, with inciso II and §§ 1º to 5º in the redação of the Provimento CN nº 237/2026 and the rest of the article in that of the Provimento CN nº 182/2024; the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). The serviço digits are fixed at `55`, the code [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) assigns to the registro civil das pessoas naturais, so a matrícula carrying any other pair in the ninth and tenth positions is rejected however good its check digits are. The book-type digit always has to name one of the nine book types (the same `CertidaoType` returned by `parseCertidao`), so a matrícula whose digit is `0` is rejected however good its check digits are, the same way `parseCertidao` returns `null` for it. `options.accept` (part of `IsValidCertidaoOptions`) narrows that to the listed types; it defaults to every type, and a value that is not an array falls back to that default. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. @@ -1686,7 +1747,7 @@ formatCaepf('184', { pad: true }); // 000.000.000/001-84 ## isValidRegistroProfissional -Check the structure of a professional council registration number (registro/inscrição profissional). Options are typed as `IsValidRegistroProfissionalOptions`: `options.council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `options.stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits and the tipo de registro (`"O"` Originário, `"P"` Provisório or `"T"` Transferido, which says nothing about the professional category), as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1) and in the Resolução CFC nº 1.707/2023. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. The OAB, the CFM and the CFO publish no format for the numbers they issue, so the digit ranges accepted for `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative. CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). +Check the structure of a professional council registration number (registro/inscrição profissional). Options are typed as `IsValidRegistroProfissionalOptions`: `options.council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `options.stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits, the tipo de registro (`"O"` Originário or `"P"` Provisório, which says nothing about the professional category) and the check digit, as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). A Registro Transferido or Secundário appends `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, per that same item and [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: the Manual's own examples are `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` and `PI-111222/O-5 S-AC`. Both UFs must be real state codes, and `options.stateCode` is compared against the originating one. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. Only the CRC shape and those CRP regional codes rest on a published source: the CFP page publishes no length for the inscription number itself, and the OAB, the CFM and the CFO publish no format at all, so the digit ranges accepted for `"CRP"`, `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative (the OAB/SP public search field is `maxlength="7"`, and the CFM documents `300`-prefixed and `P`-suffixed CRMs, none of which these shapes express). CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). ```javascript import { isValidRegistroProfissional } from '@brazilian-utils/brazilian-utils'; @@ -1695,6 +1756,8 @@ isValidRegistroProfissional('123456/SP', { council: 'OAB' }); // true isValidRegistroProfissional('123456-RJ', { council: 'OAB', stateCode: 'SP' }); // false (UF mismatch) isValidRegistroProfissional('06/12345', { council: 'CRP' }); // true isValidRegistroProfissional('SP-123456/O-3', { council: 'CRC' }); // true +isValidRegistroProfissional('SP-123456/O-3 T-MG', { council: 'CRC' }); // true (registro transferido) +isValidRegistroProfissional('SP-123456/T-3', { council: 'CRC' }); // false ("T" is not a tipo de registro) ``` ## isValidVin @@ -1712,7 +1775,7 @@ isValidVin('1HGCM8263IA004352'); // false (contains the excluded letter I) ## isValidCbo -Check if a CBO (Classificação Brasileira de Ocupações) code exists in the MTE occupation table. Accepts the code with or without the hyphen mask, or as a number. A string is only read as a code when it is written in one of those forms (the 6 digits, or the `NNNN-NN` mask, with the usual separators between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +Check if a CBO (Classificação Brasileira de Ocupações) code exists in the MTE occupation table. Accepts the code with or without the hyphen mask, or as a number. A string is only read as a code when it is written in one of those forms (the 6 digits, or the `NNNN-NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. ```javascript import { isValidCbo } from '@brazilian-utils/brazilian-utils'; @@ -1725,7 +1788,7 @@ isValidCbo('2124abc05'); // false (not a documented form) isValidCbo(-212405); // false (not a non-negative safe integer) ``` -The occupation titles come from the [official CBO 2002 tables published by the MTE](http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf). +The occupation titles come from the [official CBO 2002 occupation table published by the MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). ## getCbo @@ -1739,11 +1802,11 @@ getCbo('000000'); // null getCbo('2124abc05'); // null (not a documented form) ``` -The occupation titles come from the [official CBO 2002 tables published by the MTE](http://www.mtecbo.gov.br/cbosite/pages/downloads.jsf). +The occupation titles come from the [official CBO 2002 occupation table published by the MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). ## isValidCnae -Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the CNAE 2.3 table published by IBGE. Accepts the code with or without the `NNNN-N/NN` mask, or as a number. A string is only read as a code when it is written in one of those forms (the 7 digits, or the mask, with the usual separators between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the CNAE 2.3 table published by IBGE. Accepts the code with or without the `NNNN-N/NN` mask, or as a number. A string is only read as a code when it is written in one of those forms (the 7 digits, or the mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. ```javascript import { isValidCnae } from '@brazilian-utils/brazilian-utils'; @@ -1757,12 +1820,18 @@ isValidCnae(-111301); // false (not a non-negative safe integer) ## formatCnae -Format a CNAE (Classificação Nacional de Atividades Econômicas) subclass code. +Format a CNAE (Classificação Nacional de Atividades Econômicas) subclass code. `options.pad` (part of `FormatCnaeOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes, which is what an input being typed into needs; with `true` the value is first left padded with zeros to the 7 digits of a complete subclass code, so it always comes back fully masked. A number is treated exactly like the string of its digits, so it is only padded under `pad: true`. Only digits and the mask characters are accepted; anything else gives `''`, and so does a number that is not a non-negative safe integer. ```javascript import { formatCnae } from '@brazilian-utils/brazilian-utils'; formatCnae('6201501'); // 6201-5/01 +formatCnae('62'); // 62 (masked as far as it goes) +formatCnae('62015'); // 6201-5 +formatCnae('62', { pad: true }); // 0000-0/62 (padded to 7 digits first) +formatCnae(111301, { pad: true }); // 0111-3/01 +formatCnae('abc6201501'); // '' (not a documented form) +formatCnae(-6201501); // '' (not a non-negative safe integer) ``` ## getCnae @@ -1779,7 +1848,7 @@ getCnae('0111abc301'); // null (not a documented form) ## isValidNcm -Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. Accepts the code with or without the dotted mask, or as a number. +Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. Accepts the code with or without the dotted mask, or as a number. A string is only read as a code when it is written in one of those forms (the 8 digits, or the `NNNN.NN.NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. ```javascript import { isValidNcm } from '@brazilian-utils/brazilian-utils'; @@ -1787,40 +1856,55 @@ import { isValidNcm } from '@brazilian-utils/brazilian-utils'; isValidNcm('8471.30.12'); // true isValidNcm('84713012'); // true isValidNcm('00000000'); // false +isValidNcm('abc01012100'); // false (not a documented form) +isValidNcm(-84713012); // false (not a non-negative safe integer) ``` ## formatNcm -Format an NCM (Nomenclatura Comum do Mercosul) code. +Format an NCM (Nomenclatura Comum do Mercosul) code. `options.pad` (part of `FormatNcmOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes, which is what an input being typed into needs; with `true` the value is first left padded with zeros to the 8 digits of a complete code, so it always comes back fully masked. A number is treated exactly like the string of its digits, so it is only padded under `pad: true`. Only digits and the mask characters are accepted; anything else gives `''`, and so does a number that is not a non-negative safe integer. ```javascript import { formatNcm } from '@brazilian-utils/brazilian-utils'; formatNcm('84713012'); // 8471.30.12 +formatNcm('8471'); // 8471 (masked as far as it goes) +formatNcm('847130'); // 8471.30 +formatNcm('8471', { pad: true }); // 0000.84.71 (padded to 8 digits first) +formatNcm('abc8471'); // '' (not a documented form) +formatNcm(-84713012); // '' (not a non-negative safe integer) ``` ## isValidCfop -Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table (Ajuste SINIEF 07/2001 and updates). Only operable codes count: the group and subgroup headings of the official nomenclature, the codes ending in `00` and `50` (1000, 1100, 1150, 5350, ...), are section titles rather than codes a document can carry, so they are rejected. +Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table. The table is the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), the text in force (current wording given by Ajuste SINIEF 03/24, last amended by Ajuste SINIEF 39/25), not the frozen 2001 text of Ajuste SINIEF 07/01. Only operable codes count: the group and subgroup headings of the official nomenclature, the codes ending in `00` and `50` (1000, 1100, 1150, 5350, ...), are section titles rather than codes a document can carry, so they are rejected. + +A string is only read as a code when it is written in one of the documented forms (the 4 digits, or the `N.NNN` form the annex prints, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. ```javascript import { isValidCfop } from '@brazilian-utils/brazilian-utils'; isValidCfop('5102'); // true +isValidCfop('1.101'); // true +isValidCfop('7504'); // true (added by the 2022 rewrite of the annex) isValidCfop('0000'); // false isValidCfop('1150'); // false (a subgroup heading, not an operable code) +isValidCfop('abc5102'); // false (not a documented form) +isValidCfop(-5102); // false (not a non-negative safe integer) ``` ## getCfop -Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description. The group and subgroup headings of the official nomenclature, the codes ending in `00` and `50`, are not in the table and give `null`. +Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description, as the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24) words it. The group and subgroup headings of the official nomenclature, the codes ending in `00` and `50`, are not in the table and give `null`. Same input rules as `isValidCfop`. ```javascript import { getCfop } from '@brazilian-utils/brazilian-utils'; -getCfop('5102'); // { code: '5102', description: 'Venda de mercadoria adquirida ou recebida de terceiros' } +getCfop('1101'); // { code: '1101', description: 'Compra para industrialização ou produção rural' } +getCfop('7504'); // { code: '7504', description: 'Exportação de mercadoria que foi objeto de formação de lote de exportação' } getCfop('0000'); // null getCfop('5350'); // null (a subgroup heading, not an operable code) +getCfop('abc5102'); // null (not a documented form) ``` ## isValidCst @@ -1829,33 +1913,44 @@ Check if a CST (Código de Situação Tributária) code is valid for a given tax | Tax | Format | Accepted codes | | --- | --- | --- | -| `icms` | 3 digits (origem + CST) | origem `0`-`8` + one of `00`, `10`, `20`, `30`, `40`, `41`, `50`, `51`, `60`, `70`, `90` | +| `icms` | 3 digits (origem + CST) | origem `0`-`8` + one of `00`, `02`, `10`, `15`, `20`, `30`, `40`, `41`, `50`, `51`, `53`, `60`, `61`, `70`, `90` | | `ipi` | 2 digits | `00`, `01`, `02`, `03`, `04`, `05`, `49`, `50`, `51`, `52`, `53`, `54`, `55`, `99` | | `pis` | 2 digits | `01`-`09`, `49`, `50`-`56`, `60`-`67`, `70`-`75`, `98`, `99` | | `cofins` | 2 digits | same table as `pis` | `options.tax` (part of `IsValidCstOptions`) is optional: omit it to accept a code that exists in any one of the four tables above. +The ICMS Tabela B is the one in force: the [consolidated Anexo I of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), whose current wording came from [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (effective 01.12.23) and which [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) amended by revoking items 12, 13, 52, 72 and 74 (effective 09.07.24). `02`, `15`, `53` and `61` are its monofasia de combustíveis codes. + +A string is only read as a code when it is written in one of the documented forms (the 2 or 3 digits, with a single separator between them and optional surrounding whitespace), and a number only when it is a non-negative safe integer. + ```javascript import { isValidCst } from '@brazilian-utils/brazilian-utils'; isValidCst('000', { tax: 'icms' }); // true isValidCst('110', { tax: 'icms' }); // true +isValidCst('002', { tax: 'icms' }); // true (monofasia de combustíveis) isValidCst('06', { tax: 'pis' }); // true isValidCst('99', { tax: 'ipi' }); // true isValidCst('110'); // true (found in the icms table, tax omitted) isValidCst('999'); // false (not in any table) +isValidCst('abc110'); // false (not a documented form) +isValidCst(-110); // false (not a non-negative safe integer) ``` ## isValidCsosn -Check if a CSOSN (Código de Situação da Operação no Simples Nacional) code is one of the 10 codes defined by Ajuste SINIEF 03/2010: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` or `900`. +Check if a CSOSN (Código de Situação da Operação no Simples Nacional) code is one of the 10 codes of the [consolidated Anexo III-A of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), the table Ajuste SINIEF 03/2010 instituted: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` or `900`. + +A string is only read as a code when it is written in one of the documented forms (the 3 digits, with a single separator between them and optional surrounding whitespace), and a number only when it is a non-negative safe integer. ```javascript import { isValidCsosn } from '@brazilian-utils/brazilian-utils'; isValidCsosn('101'); // true isValidCsosn('999'); // false +isValidCsosn('abc101'); // false (not a documented form) +isValidCsosn(-101); // false (not a non-negative safe integer) ``` ## removeAccents diff --git a/scripts/banks.ts b/scripts/banks.ts index 31674705..9608bd94 100644 --- a/scripts/banks.ts +++ b/scripts/banks.ts @@ -173,7 +173,7 @@ const main = async (): Promise => { * Brazilian STR (Sistema de Transferência de Reservas) participants that have a compensation * code (commonly known as COMPE), published by Banco Central do Brasil. Generated by * \`scripts/banks.ts\`. - * @see ${BACEN_CSV_URL} + * @see Official: ${BACEN_CSV_URL} */ export type Bank = { /** Compensation code (COMPE), 3 digits, zero-padded. */ diff --git a/scripts/legal-natures.ts b/scripts/legal-natures.ts index c5c40428..0d5cf6c2 100644 --- a/scripts/legal-natures.ts +++ b/scripts/legal-natures.ts @@ -193,6 +193,12 @@ const main = async (): Promise => { Object.entries(LEGACY_LEGAL_NATURE).filter(([code]) => !(code in current)), ); + const legacyCodes = Object.keys(legacy); + + const typoFixedCodes = Object.entries(current) + .filter(([, description]) => Object.values(TYPO_FIXES).includes(description)) + .map(([code]) => code); + await writeFile( resolve(scriptsDir, "..", OUTPUT_PATH), `/** @@ -200,14 +206,21 @@ const main = async (): Promise => { * * Generated by \`node ./scripts/legal-natures.ts\`. Do not edit by hand. * - * @see ${SOURCE_PAGE_URL} - * @see ${SOURCE_URL} + * ${codes.length} of the ${codes.length + legacyCodes.length} entries are the official codes from the CONCLA 2021 table; the other + * ${legacyCodes.length} (${legacyCodes.join(", ")}) are legacy codes kept for 2.3.0 + * compatibility. These codes fix an accent typo of the official PDF: ${typoFixedCodes.join(", ")}. + * + * @see Official: ${SOURCE_PAGE_URL} + * @see Official: ${SOURCE_URL} */ export const LEGAL_NATURE: Record = { ${stringifyEntries(current)} ${stringifyEntries(legacy)} }; + +/** Mask characters (hyphen, dot, whitespace) tolerated around a legal nature code. */ +export const MASK_REGEX = /[-.\\s]/g; `, ); }; diff --git a/src/_internals/constants/certidao.ts b/src/_internals/constants/certidao.ts index f821a69e..9c70a70e 100644 --- a/src/_internals/constants/certidao.ts +++ b/src/_internals/constants/certidao.ts @@ -3,14 +3,17 @@ * 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + * 3 (folha) + 7 (termo) + 2 (dígitos verificadores). * - * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 Código Nacional de Normas da - * Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento CNJ nº 149/2023), art. 473 - * in the wording of the Provimento CN nº 182, de 17/09/2024: the in-force layout of the 32 - * digit matrícula. - * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 Provimento CNJ nº 2, de 27/04/2009, - * which instituted the modelos únicos de certidão and the matrícula (revoked; historical). - * @see Based on: http://ghiorzi.org/DVnew.htm Worked example of the two check digits - * (sums 288 and 309). + * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 + * Código Nacional de Normas da Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento + * CNJ nº 149/2023), art. 473 as currently published: the in-force layout of the 32 digit + * matrícula. Inciso II and §§ 1º to 5º carry the redação of the Provimento CN nº 237, de + * 13/07/2026; the rest of the article, and the digit layout this library depends on, come from the + * Provimento CN nº 182, de 17/09/2024. + * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 + * Provimento CNJ nº 2, de 27/04/2009, which instituted the modelos únicos de certidão and the + * matrícula (revoked; historical). + * @see Based on: http://ghiorzi.org/DVnew.htm + * Worked example of the two check digits (sums 288 and 309). * @see Based on: https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts * Reference implementation, and the source of the matrículas used as test vectors. * @see Based on: https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php diff --git a/src/convert-license-plate-to-mercosul/constants.ts b/src/convert-license-plate-to-mercosul/constants.ts index edb2c799..1daac82d 100644 --- a/src/convert-license-plate-to-mercosul/constants.ts +++ b/src/convert-license-plate-to-mercosul/constants.ts @@ -1,12 +1,14 @@ /** - * Official digit to letter conversion table used to turn an old format plate's 5th character - * into the Mercosul format's embedded letter (0=A, 1=B, ..., 9=J). + * Digit to letter conversion table used to turn an old format plate's 5th character into the + * Mercosul format's embedded letter (0=A, 1=B, ..., 9=J). * - * Resolução CONTRAN nº 969/2022, art. 2º § 4º. The linked DOU PDF has no annexes; the table - * above comes from Anexo II, published separately on the CONTRAN resolutions page. + * Resolução CONTRAN nº 969/2022, art. 2º § 4º, is what requires the substitution. The table + * itself is Anexo II of that resolution, which is not published at a stable public URL: the + * linked DOU PDF carries no annexes and the CONTRAN resolutions index does not host the annex + * either, so it is cited as `Based on:` rather than as an official document a reader can open. * * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022.pdf - * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes + * @see Based on: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes */ export const DIGIT_TO_MERCOSUL_LETTER: Record = { "0": "A", diff --git a/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.test.ts b/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.test.ts index f2dd52ba..5fb76116 100644 --- a/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.test.ts +++ b/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.test.ts @@ -26,7 +26,7 @@ describe("convertLicensePlateToMercosul", () => { expect(convertLicensePlateToMercosul("ABC1D23")).toBe(""); }); - test("when it is a Mercosul motorcycle plate", () => { + test("when it is the withdrawn LLLNNLN sequence", () => { expect(convertLicensePlateToMercosul("ABC12D3")).toBe(""); }); diff --git a/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.ts b/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.ts index bb9b9709..c1064ea2 100644 --- a/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.ts +++ b/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.ts @@ -20,11 +20,14 @@ import { DIGIT_TO_MERCOSUL_LETTER } from "./constants"; * convertLicensePlateToMercosul("invalid"); // "" * ``` * - * Resolução CONTRAN nº 969/2022, art. 2º § 4º. The linked DOU PDF has no annexes; the digit to - * letter table comes from Anexo II, published separately on the CONTRAN resolutions page. + * Resolução CONTRAN nº 969/2022, art. 2º § 4º, is what requires the substitution of the second + * numeric character. The digit to letter table itself is Anexo II of that resolution, which is + * not published at a stable public URL: the linked DOU PDF carries no annexes and the CONTRAN + * resolutions index does not host the annex either, so the table below is cited as `Based on:` + * rather than as an official document a reader can open. * * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022.pdf - * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes + * @see Based on: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes */ export const convertLicensePlateToMercosul = (value: string): string => { if (getFormatLicensePlate(value) !== "LLLNNNN") return ""; diff --git a/src/format-boleto/format-boleto.ts b/src/format-boleto/format-boleto.ts index f643f5be..3be6ad91 100644 --- a/src/format-boleto/format-boleto.ts +++ b/src/format-boleto/format-boleto.ts @@ -33,12 +33,14 @@ export type FormatBoletoOptions = { * // "82630000001-1 09880010070-2 02410202400-0 00020510451-9" * ``` * - * Carta-Circular BCB nº 2.926/2000 specifies the linha digitável fields, the módulo 11 check - * digit (using 1 for remainders 0, 10 and 1) and the fator de vencimento behind the 47 digit - * cobrança bancária slip; the FEBRABAN layout index covers the arrecadação slip. + * Carta-Circular BCB nº 2.926/2000 specifies the linha digitável fields and the módulo 11 + * check digit (using 1 for remainders 0, 10 and 1) of the 47 digit cobrança bancária slip, + * including the position of the fator de vencimento field. The FEBRABAN "Layout Padrão de + * Arrecadação/Recebimento com Utilização do Código de Barras" and the FEBRABAN layout index + * cover the arrecadação slip. * - * @see Official: https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf * @see Official: https://www.bcb.gov.br/pre/normativos/c_circ/2000/pdf/c_circ_2926_v1_O.pdf + * @see Official: https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf * @see Official: https://portal.febraban.org.br/pagina/3425/33/pt-br/layout-febraban */ export const formatBoleto = (value: string | number, options?: FormatBoletoOptions): string => { diff --git a/src/format-certidao/format-certidao.ts b/src/format-certidao/format-certidao.ts index fc0910f4..873fa517 100644 --- a/src/format-certidao/format-certidao.ts +++ b/src/format-certidao/format-certidao.ts @@ -32,14 +32,17 @@ export type FormatCertidaoOptions = { * // "000000 01 55 2010 1 00020 112 0000120 87" * ``` * - * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 Código Nacional de Normas da - * Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento CNJ nº 149/2023), art. 473 - * in the wording of the Provimento CN nº 182, de 17/09/2024: the in-force layout of the 32 - * digit matrícula. - * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 Provimento CNJ nº 2, de 27/04/2009, - * which instituted the modelos únicos de certidão and the matrícula (revoked; historical). - * @see Based on: http://ghiorzi.org/DVnew.htm Worked example of the two check digits - * (sums 288 and 309). + * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 + * Código Nacional de Normas da Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento + * CNJ nº 149/2023), art. 473 as currently published: the in-force layout of the 32 digit + * matrícula. Inciso II and §§ 1º to 5º carry the redação of the Provimento CN nº 237, de + * 13/07/2026; the rest of the article, and the digit layout this library depends on, come from the + * Provimento CN nº 182, de 17/09/2024. + * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 + * Provimento CNJ nº 2, de 27/04/2009, which instituted the modelos únicos de certidão and the + * matrícula (revoked; historical). + * @see Based on: http://ghiorzi.org/DVnew.htm + * Worked example of the two check digits (sums 288 and 309). * @see Based on: https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts * Reference implementation, and the source of the matrículas used as test vectors. * @see Based on: https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php diff --git a/src/format-cns/format-cns.ts b/src/format-cns/format-cns.ts index deea847b..48931c16 100644 --- a/src/format-cns/format-cns.ts +++ b/src/format-cns/format-cns.ts @@ -19,14 +19,19 @@ export type FormatCnsOptions = { * * @example * ```typescript - * formatCns("123456789010001"); // "123 4567 8901 0001" - * formatCns(123456789010001); // "123 4567 8901 0001" + * formatCns("123456789010000"); // "123 4567 8901 0000" + * formatCns(123456789010000); // "123 4567 8901 0000" * formatCns("89010001", { pad: true }); // "000 0000 8901 0001" * ``` * * @see Official: https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/ + * ANVISA's two validation routines, the ones implemented here. The page sits behind a bot filter + * and answers HTTP 403 to every non-browser client, so it has to be opened in a browser. * @see Based on: https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html - * e-SUS APS documentation of the same DATASUS algorithm, reachable without a browser. + * e-SUS APS documentation of the same DATASUS algorithm, reachable without a browser. It applies + * the provisional routine to numbers starting with 5, 7, 8 or 9; this implementation follows the + * ANVISA page, which restricts it to 7, 8 and 9, so a 5 prefixed number is rejected even when its + * weighted sum checks out. */ export const formatCns = (value: string | number, options?: FormatCnsOptions): string => isNullish(value) diff --git a/src/format-passport/format-passport.ts b/src/format-passport/format-passport.ts index 5fd38f11..df742b39 100644 --- a/src/format-passport/format-passport.ts +++ b/src/format-passport/format-passport.ts @@ -14,5 +14,6 @@ import { parsePassport } from "../parse-passport/parse-passport"; * formatPassport("") // "" * * @see Official: https://www.gov.br/pf/pt-br/assuntos/passaporte + * @see Official: https://www.gov.br/pf/pt-br/assuntos/passaporte/ajuda/duvidas_/caderneta/caderneta-numero-onde-fica-e */ export const formatPassport = (passport: string): string => parsePassport(passport); diff --git a/src/generate-boleto/generate-boleto.test.ts b/src/generate-boleto/generate-boleto.test.ts index c1547af7..318e92c9 100644 --- a/src/generate-boleto/generate-boleto.test.ts +++ b/src/generate-boleto/generate-boleto.test.ts @@ -12,6 +12,25 @@ import { type GenerateBoletoOptions, generateBoleto } from "./generate-boleto"; const drawArrecadacaoSegment = (): number => getBoletoInfo(generateBoleto({ type: "arrecadacao" }))?.segment ?? 0; +const drawArrecadacaoIdentifier = (algorithmDraw: number, valueDraw: number): string => { + const draws = [0, algorithmDraw, valueDraw]; + const originalRandom = Math.random; + let call = 0; + + try { + Math.random = (): number => { + const draw = draws[call] ?? 0; + call++; + + return draw; + }; + + return generateBoleto({ type: "arrecadacao" })[2]; + } finally { + Math.random = originalRandom; + } +}; + describe("generateBoleto", () => { test("should generate a valid boleto", () => { const boleto = generateBoleto(); @@ -93,18 +112,23 @@ describe("generateBoleto", () => { expect(segments.size).toBeGreaterThan(1); }); - test("should pick the value identifier (position 3) from the same Math.random() draw that selects the check digit algorithm, modulo 11 below 0.5 ('8') and modulo 10 at or above 0.5 ('6')", () => { - const originalRandom = Math.random; + test("should pick the value identifier (position 3) from all four values, the algorithm draw choosing modulo 11 ('8', '9') below 0.5 and modulo 10 ('6', '7') at or above it, and the value draw choosing an effective amount ('8', '6') below 0.5 and a reference quantity ('9', '7') at or above it", () => { + expect(drawArrecadacaoIdentifier(0.3, 0.3)).toBe("8"); + expect(drawArrecadacaoIdentifier(0.3, 0.5)).toBe("9"); + expect(drawArrecadacaoIdentifier(0.5, 0.3)).toBe("6"); + expect(drawArrecadacaoIdentifier(0.5, 0.5)).toBe("7"); + }); - try { - Math.random = () => 0.3; - expect(generateBoleto({ type: "arrecadacao" })[2]).toBe("8"); + test("should generate both an effective amount and a reference quantity across many draws", () => { + const flags = new Set( + Array.from( + { length: 200 }, + () => getBoletoInfo(generateBoleto({ type: "arrecadacao" }))?.hasEffectiveValue, + ), + ); - Math.random = () => 0.5; - expect(generateBoleto({ type: "arrecadacao" })[2]).toBe("6"); - } finally { - Math.random = originalRandom; - } + expect(flags.has(true)).toBe(true); + expect(flags.has(false)).toBe(true); }); }); diff --git a/src/generate-boleto/generate-boleto.ts b/src/generate-boleto/generate-boleto.ts index 194042c5..4704233e 100644 --- a/src/generate-boleto/generate-boleto.ts +++ b/src/generate-boleto/generate-boleto.ts @@ -9,6 +9,11 @@ export type GenerateBoletoOptions = { type?: "bancario" | "arrecadacao"; }; +const ARRECADACAO_VALUE_IDENTIFIERS = { + mod10: { effective: "6", reference: "7" }, + mod11: { effective: "8", reference: "9" }, +}; + const generateBancario = (): string => { const p1Base = generateRandomNumber(9); const p2Base = generateRandomNumber(10); @@ -39,12 +44,17 @@ const generateBancario = (): string => { const generateArrecadacao = (): string => { const segment = ARRECADACAO_SEGMENTS[Math.floor(Math.random() * ARRECADACAO_SEGMENTS.length)]; const useMod11 = Math.random() < 0.5; + const hasEffectiveValue = Math.random() < 0.5; const checkDigit = useMod11 ? (value: string): number => mod11(value, { variant: "arrecadacao" }) : mod10; + const identifier = + ARRECADACAO_VALUE_IDENTIFIERS[useMod11 ? "mod11" : "mod10"][ + hasEffectiveValue ? "effective" : "reference" + ]; const body = generateRandomNumber(40); - const head = `${ARRECADACAO_PRODUCT}${segment}${useMod11 ? "8" : "6"}`; + const head = `${ARRECADACAO_PRODUCT}${segment}${identifier}`; const barcode = head + checkDigit(head + body) + body; let line = ""; @@ -62,6 +72,10 @@ const generateArrecadacao = (): string => { * * Uses `Math.random()` internally, so it is not cryptographically secure, do not use for security purposes. * + * An arrecadação slip draws its segment from 1 to 7 (segment 9 is the banks' own) and its value + * identifier from all four values, `6` and `8` for an effective amount and `7` and `9` for a + * reference quantity, so both `hasEffectiveValue` branches of `getBoletoInfo` are reachable. + * * @param {GenerateBoletoOptions} [options] - Optional options. * @param {string} options.type - `"bancario"` (default) or `"arrecadacao"`. * @returns {string} A valid 47-digit boleto string without formatting, or a 48-digit one for arrecadação. @@ -72,12 +86,14 @@ const generateArrecadacao = (): string => { * generateBoleto({ type: "arrecadacao" }); // "846100000005246100291102005460339004695895061080" * ``` * - * Carta-Circular BCB nº 2.926/2000 specifies the linha digitável fields, the módulo 11 check - * digit (using 1 for remainders 0, 10 and 1) and the fator de vencimento behind the 47 digit - * cobrança bancária slip; the FEBRABAN layout index covers the arrecadação slip. + * Carta-Circular BCB nº 2.926/2000 specifies the linha digitável fields and the módulo 11 + * check digit (using 1 for remainders 0, 10 and 1) of the 47 digit cobrança bancária slip, + * including the position of the fator de vencimento field. The FEBRABAN "Layout Padrão de + * Arrecadação/Recebimento com Utilização do Código de Barras" and the FEBRABAN layout index + * cover the arrecadação slip. * - * @see Official: https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf * @see Official: https://www.bcb.gov.br/pre/normativos/c_circ/2000/pdf/c_circ_2926_v1_O.pdf + * @see Official: https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf * @see Official: https://portal.febraban.org.br/pagina/3425/33/pt-br/layout-febraban */ export const generateBoleto = (options?: GenerateBoletoOptions): string => diff --git a/src/generate-cpf/generate-cpf.ts b/src/generate-cpf/generate-cpf.ts index 115a1e10..709c6c6e 100644 --- a/src/generate-cpf/generate-cpf.ts +++ b/src/generate-cpf/generate-cpf.ts @@ -28,9 +28,14 @@ const calculateCheckDigit = (base: string, weight: number): string => { * generateCpf("SP"); // "12345678810" (with the SP state code, 8, in the 9th digit) * ``` * + * The região fiscal digit in the 9th position comes from the Receita Federal's folheto + * "Cadastros: CPF e CNPJ"; the check digit rule (`REGRA_VALIDA_CPF`) is specified, with a worked + * example (`280012389-38`), in the Receita Federal's Manual e-Financeira, Anexo II. + * * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/meu-cpf * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/educacao-fiscal/educacao_fiscal/folhetos-orientativos/cadastros-dig.pdf - * @see Based on: https://github.com/brazilian-utils/brutils-python/blob/main/brutils/cpf.py + * @see Official: http://sped.rfb.gov.br/arquivo/show/8231 + * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/cpf.py */ export const generateCpf = (state?: StateCode): string => { let base = generateRandomNumber(BASE_LENGTH) + getStateCode(state); diff --git a/src/generate-passport/generate-passport.ts b/src/generate-passport/generate-passport.ts index 05c905e1..6e1f63fc 100644 --- a/src/generate-passport/generate-passport.ts +++ b/src/generate-passport/generate-passport.ts @@ -13,6 +13,7 @@ import { ALPHABET_LENGTH, CHAR_CODE_A, DIGITS_LENGTH, LETTERS_LENGTH } from "./c * generatePassport() // "ZS840088" * * @see Official: https://www.gov.br/pf/pt-br/assuntos/passaporte + * @see Official: https://www.gov.br/pf/pt-br/assuntos/passaporte/ajuda/duvidas_/caderneta/caderneta-numero-onde-fica-e */ export const generatePassport = (): string => { const letters = Array.from({ length: LETTERS_LENGTH }, () => diff --git a/src/get-bank-by-code/get-bank-by-code.ts b/src/get-bank-by-code/get-bank-by-code.ts index add89935..0292707f 100644 --- a/src/get-bank-by-code/get-bank-by-code.ts +++ b/src/get-bank-by-code/get-bank-by-code.ts @@ -19,8 +19,8 @@ const CODE_LENGTH = 3; * ``` * * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv - * @see Official: https://brasilapi.com.br/api/banks/v1 Fallback source used by the dataset - * generator (`scripts/banks.ts`) when the Bacen CSV request fails. + * @see Based on: https://brasilapi.com.br/api/banks/v1 + * Fallback source used by the dataset generator (`scripts/banks.ts`) when the Bacen CSV request fails. */ export const getBankByCode = (code: string | number): Bank | null => { if (!isLookupCode(code)) return null; diff --git a/src/get-bank-by-ispb/get-bank-by-ispb.ts b/src/get-bank-by-ispb/get-bank-by-ispb.ts index cefd3156..e53f968c 100644 --- a/src/get-bank-by-ispb/get-bank-by-ispb.ts +++ b/src/get-bank-by-ispb/get-bank-by-ispb.ts @@ -23,8 +23,8 @@ const ISPB_LENGTH = 8; * ``` * * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv - * @see Official: https://brasilapi.com.br/api/banks/v1 Fallback source used by the dataset - * generator (`scripts/banks.ts`) when the Bacen CSV request fails. + * @see Based on: https://brasilapi.com.br/api/banks/v1 + * Fallback source used by the dataset generator (`scripts/banks.ts`) when the Bacen CSV request fails. */ export const getBankByIspb = (value: string | number): Bank | null => { if (!isLookupCode(value)) return null; diff --git a/src/get-banks/get-banks.ts b/src/get-banks/get-banks.ts index 345f19cf..8735809e 100644 --- a/src/get-banks/get-banks.ts +++ b/src/get-banks/get-banks.ts @@ -15,7 +15,7 @@ import { BANKS, type Bank } from "../_internals/constants/banks"; * ``` * * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv - * @see Official: https://brasilapi.com.br/api/banks/v1 Fallback source used by the dataset - * generator (`scripts/banks.ts`) when the Bacen CSV request fails. + * @see Based on: https://brasilapi.com.br/api/banks/v1 + * Fallback source used by the dataset generator (`scripts/banks.ts`) when the Bacen CSV request fails. */ export const getBanks = (): Bank[] => BANKS.map((bank) => Object.assign({}, bank)); diff --git a/src/get-boleto-info/constants.ts b/src/get-boleto-info/constants.ts index 57378edf..d99ef14e 100644 --- a/src/get-boleto-info/constants.ts +++ b/src/get-boleto-info/constants.ts @@ -1,11 +1,23 @@ /** - * The "fator de vencimento" (expiration factor) counts days since a FEBRABAN base date and - * cycles every `CYCLE_LENGTH` days once it reaches its 4-digit maximum. FEBRABAN Comunicado - * FB-009/2023 sets the current cycle boundary: the factor reached its maximum, 9999, on - * 21/02/2025 and restarted at 1000 on 22/02/2025. + * The "fator de vencimento" (expiration factor) counts days since the base date 07/10/1997 that + * Carta-Circular BCB nº 2.926/2000 places in positions 6-9 of the barcode, and cycles every + * `CYCLE_LENGTH` days once it reaches its 4-digit maximum: it reached 9999 on 21/02/2025 and + * restarted at 1000 on 22/02/2025. FEBRABAN announced that reset in Comunicado FB-009/2023, + * which is not published on FEBRABAN's public site; the Bradesco cobrança layout manual below + * reproduces the rule and its correlation table. * + * `RANGE_BEFORE` and `RANGE_AFTER` are a heuristic of this library, not a published rule. + * Neither FEBRABAN nor the Banco Central publishes any way of telling an old cycle factor from + * a new cycle one, so every factor resolves to either of two dates `CYCLE_LENGTH` days apart. + * These two windows pick between them, which means the date a factor resolves to depends on the + * `referenceDate` given to `getBoletoInfo` and can change as that reference moves. + * + * @see Official: https://www.bcb.gov.br/pre/normativos/c_circ/2000/pdf/c_circ_2926_v1_O.pdf * @see Official: https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf * @see Official: https://portal.febraban.org.br/pagina/3425/33/pt-br/layout-febraban + * @see Based on: https://banco.bradesco/assets/pessoajuridica/pdf/4008-524-0121-layout-cobranca-versao-portugues.pdf + * Bradesco "Layout da Cobrança" manual: base date 07/10/1997, 03/07/2000 = 1000, 21/02/2025 = 9999 + * and a restart at 1000 on 22/02/2025. */ export const DAY_IN_MS = 86_400_000; diff --git a/src/get-boleto-info/get-boleto-info.test.ts b/src/get-boleto-info/get-boleto-info.test.ts index adf592b7..ad249122 100644 --- a/src/get-boleto-info/get-boleto-info.test.ts +++ b/src/get-boleto-info/get-boleto-info.test.ts @@ -17,6 +17,14 @@ const withFactor = { "9999": "00190000090114971860168524522114799990000102656", }; +const REFERENCE_DATE = new Date(2025, 5, 15); + +const CANONICAL_INFO = { + amount: 102_656, + expirationDate: new Date(2018, 6, 15), + bankCode: "001", +}; + const ARRECADACAO_LINE = "846100000005246100291102005460339004695895061080"; const ARRECADACAO_BARCODE = "84610000000246100291100054603390069589506108"; @@ -33,19 +41,17 @@ describe("getBoletoInfo", () => { describe("should return boleto info", () => { test("when boleto is valid without mask", () => { - expect(getBoletoInfo("00190000090114971860168524522114675860000102656")).toStrictEqual({ - amount: 102_656, - expirationDate: new Date(2018, 6, 15), - bankCode: "001", - }); + const info = getBoletoInfo(withFactor["7586"], { referenceDate: REFERENCE_DATE }); + + expect(info).toStrictEqual(CANONICAL_INFO); }); test("when boleto is valid with mask", () => { - expect(getBoletoInfo("0019000009 01149.718601 68524.522114 6 75860000102656")).toStrictEqual({ - amount: 102_656, - expirationDate: new Date(2018, 6, 15), - bankCode: "001", - }); + const masked = "0019000009 01149.718601 68524.522114 6 75860000102656"; + + expect(getBoletoInfo(masked, { referenceDate: REFERENCE_DATE })).toStrictEqual( + CANONICAL_INFO, + ); }); test("when the amount field is all zeros (same fixture as the 'valid without mask' boleto, amount positions 37-46 zeroed and the main check digit recalculated)", () => { @@ -54,7 +60,7 @@ describe("getBoletoInfo", () => { }); describe("fator de vencimento (fixtures share a banco 001, R$ 1.026,56 slip with only the factor and check digits changed; FEBRABAN restarted the factor at 1000 on 22/02/2025 right after it reached 9999 on 21/02/2025, so the same factor can map to two dates 9000 days apart, and referenceDate pins which cycle wins)", () => { - const referenceDate = new Date(2025, 5, 15); + const referenceDate = REFERENCE_DATE; test("should return null when there is no fator de vencimento", () => { expect(getBoletoInfo(withFactor["0000"], { referenceDate })?.expirationDate).toBeNull(); @@ -103,7 +109,7 @@ describe("getBoletoInfo", () => { ).toStrictEqual(new Date(2000, 6, 3)); }); - test("should resolve a factor inside the safety range to its closest candidate (fixture '7586' with the factor changed to 6614 and the main check digit recalculated: with referenceDate 15/06/2025 neither cycle candidate falls inside the accepted control range, landing in the 'range de segurança' the FEBRABAN manual describes, so the closest one is used anyway)", () => { + test("should resolve a factor inside the safety range to its closest candidate (fixture '7586' with the factor changed to 6614 and the main check digit recalculated: with referenceDate 15/06/2025 neither cycle candidate falls inside the accepted control range, landing in the safety window RANGE_BEFORE/RANGE_AFTER define, which is a heuristic of this library rather than a published FEBRABAN rule, so the closest one is used anyway)", () => { expect( getBoletoInfo("00190000090114971860168524522114466140000102656", { referenceDate, diff --git a/src/get-boleto-info/get-boleto-info.ts b/src/get-boleto-info/get-boleto-info.ts index 6592c39f..71ab25dd 100644 --- a/src/get-boleto-info/get-boleto-info.ts +++ b/src/get-boleto-info/get-boleto-info.ts @@ -84,6 +84,12 @@ export type GetBoletoInfoOptions = { * barcode, both starting with `8`. Arrecadação bank slips also return `type`, `segment`, * `value` and `hasEffectiveValue`, and have no `bankCode` nor `expirationDate`. * + * Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle fator de + * vencimento from a new cycle one, so every factor resolves to either of two dates 9000 days + * apart. `referenceDate` (now by default) picks between them through the library's own safety + * windows, which means the same slip can resolve to the other candidate as time passes: pass + * `referenceDate` explicitly whenever the answer has to stay stable. + * * @param {string} value - The boleto digitable line (can be with or without mask). * @param {GetBoletoInfoOptions} [options] - Optional options. * @param {Date} options.referenceDate - Date used to resolve the "fator de vencimento" cycle. Defaults to now. @@ -91,21 +97,29 @@ export type GetBoletoInfoOptions = { * * @example * ```typescript - * getBoletoInfo('00190000090114971860168524522114675860000102656'); + * getBoletoInfo('00190000090114971860168524522114675860000102656', { + * referenceDate: new Date(2025, 5, 15), + * }); * // { amount: 102656, expirationDate: new Date(2018, 6, 15), bankCode: '001' } * * getBoletoInfo('846100000005246100291102005460339004695895061080'); * // { amount: 2461, expirationDate: null, bankCode: '', type: 'arrecadacao', segment: 4, value: 24.61, hasEffectiveValue: true } * ``` * - * Carta-Circular BCB nº 2.926/2000 specifies the linha digitável fields, the módulo 11 check - * digit (using 1 for remainders 0, 10 and 1) and the fator de vencimento behind the 47 digit - * cobrança bancária slip; the FEBRABAN layout index covers the arrecadação slip. See + * Carta-Circular BCB nº 2.926/2000 specifies the linha digitável fields and the módulo 11 + * check digit (using 1 for remainders 0, 10 and 1) of the 47 digit cobrança bancária slip, + * including the position of the fator de vencimento field. The FEBRABAN "Layout Padrão de + * Arrecadação/Recebimento com Utilização do Código de Barras" and the FEBRABAN layout index + * cover the arrecadação slip. The 22/02/2025 reset of the fator de vencimento is in neither: + * the Bradesco cobrança layout manual below reproduces the FEBRABAN rule. See * `src/get-boleto-info/constants.ts` for the fator de vencimento cycle base date and reset. * - * @see Official: https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf * @see Official: https://www.bcb.gov.br/pre/normativos/c_circ/2000/pdf/c_circ_2926_v1_O.pdf + * @see Official: https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf * @see Official: https://portal.febraban.org.br/pagina/3425/33/pt-br/layout-febraban + * @see Based on: https://banco.bradesco/assets/pessoajuridica/pdf/4008-524-0121-layout-cobranca-versao-portugues.pdf + * Bradesco "Layout da Cobrança" manual: base date 07/10/1997, 03/07/2000 = 1000, 21/02/2025 = 9999 + * and a restart at 1000 on 22/02/2025. */ export const getBoletoInfo = ( value: string, diff --git a/src/index.test.ts b/src/index.test.ts index f67d90aa..8fff53af 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,10 +1,10 @@ import { describe, expect, test } from "./_internals/test/runtime"; import { - type AddBusinessDaysParams, type AddressInfo, type AreaCodeInfo, type Bank, type BoletoInfo, + type BusinessDayOptions, type CapitalizeOptions, type Cbo, type CepAddressInfo, @@ -13,21 +13,21 @@ import { type CertidaoType, type Cfop, type Cnae, - type ConvertCurrencyToWordsOptions, type ConvertDateToWordsOptions, type ConvertNumberToWordsOptions, - type DifferenceInBusinessDaysParams, type FormatBoletoOptions, type FormatCaepfOptions, type FormatCeiOptions, type FormatCepOptions, type FormatCertidaoOptions, + type FormatCnaeOptions, type FormatCnhOptions, type FormatCnoOptions, type FormatCnpjOptions, type FormatCnsOptions, type FormatCpfOptions, type FormatCurrencyOptions, + type FormatNcmOptions, type FormatPhoneOptions, type FormatPisOptions, type FormatProcessoJuridicoOptions, @@ -46,7 +46,6 @@ import { type Holiday, type HolidayType, type Iban, - type IsBusinessDayOptions, type IsHolidayOptions, type IsValidBankAccountOptions, type IsValidBankAccountParams, @@ -76,7 +75,6 @@ import { type State, type StateCode, type StateName, - type WordsCase, } from "./index"; import * as brazilianUtils from "./index"; @@ -222,6 +220,7 @@ const PUBLIC = [ "parseProcessoJuridico", "parseVoterId", "removeAccents", + "subBusinessDays", ].sort(); const NETWORK_ENTRY_POINTS = new Set(["getAddressInfoByCep", "getCepInfoByAddress"]); @@ -251,11 +250,11 @@ describe("Public API", () => { test("should export every documented public type", () => { const publicTypes: Partial<{ - AddBusinessDaysParams: AddBusinessDaysParams; AddressInfo: AddressInfo; AreaCodeInfo: AreaCodeInfo; Bank: Bank; BoletoInfo: BoletoInfo; + BusinessDayOptions: BusinessDayOptions; CapitalizeOptions: CapitalizeOptions; Cbo: Cbo; CepAddressInfo: CepAddressInfo; @@ -264,21 +263,21 @@ describe("Public API", () => { CertidaoType: CertidaoType; Cfop: Cfop; Cnae: Cnae; - ConvertCurrencyToWordsOptions: ConvertCurrencyToWordsOptions; ConvertDateToWordsOptions: ConvertDateToWordsOptions; ConvertNumberToWordsOptions: ConvertNumberToWordsOptions; - DifferenceInBusinessDaysParams: DifferenceInBusinessDaysParams; FormatBoletoOptions: FormatBoletoOptions; FormatCaepfOptions: FormatCaepfOptions; FormatCeiOptions: FormatCeiOptions; FormatCepOptions: FormatCepOptions; FormatCertidaoOptions: FormatCertidaoOptions; + FormatCnaeOptions: FormatCnaeOptions; FormatCnhOptions: FormatCnhOptions; FormatCnoOptions: FormatCnoOptions; FormatCnpjOptions: FormatCnpjOptions; FormatCnsOptions: FormatCnsOptions; FormatCpfOptions: FormatCpfOptions; FormatCurrencyOptions: FormatCurrencyOptions; + FormatNcmOptions: FormatNcmOptions; FormatPhoneOptions: FormatPhoneOptions; FormatPisOptions: FormatPisOptions; FormatProcessoJuridicoOptions: FormatProcessoJuridicoOptions; @@ -297,7 +296,6 @@ describe("Public API", () => { Holiday: Holiday; HolidayType: HolidayType; Iban: Iban; - IsBusinessDayOptions: IsBusinessDayOptions; IsHolidayOptions: IsHolidayOptions; IsValidBankAccountOptions: IsValidBankAccountOptions; IsValidBankAccountParams: IsValidBankAccountParams; @@ -327,7 +325,6 @@ describe("Public API", () => { State: State; StateCode: StateCode; StateName: StateName; - WordsCase: WordsCase; }> = {}; expect(publicTypes).toEqual({}); diff --git a/src/index.ts b/src/index.ts index 1729cb11..c64f074c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,10 @@ export type { Bank } from "./_internals/constants/banks"; export type { Municipality } from "./_internals/constants/cities"; export type { State, StateCode, StateName } from "./_internals/constants/states"; -export type { NumberToWordsGender, WordsCase } from "./_internals/number-to-words/number-to-words"; -export { type AddBusinessDaysParams, addBusinessDays } from "./add-business-days/add-business-days"; +export type { NumberToWordsGender } from "./_internals/number-to-words/number-to-words"; +export { addBusinessDays } from "./add-business-days/add-business-days"; export { type CapitalizeOptions, capitalize } from "./capitalize/capitalize"; -export { - type ConvertCurrencyToWordsOptions, - convertCurrencyToWords, -} from "./convert-currency-to-words/convert-currency-to-words"; +export { convertCurrencyToWords } from "./convert-currency-to-words/convert-currency-to-words"; export { type ConvertDateToWordsOptions, convertDateToWords, @@ -17,16 +14,13 @@ export { type ConvertNumberToWordsOptions, convertNumberToWords, } from "./convert-number-to-words/convert-number-to-words"; -export { - type DifferenceInBusinessDaysParams, - differenceInBusinessDays, -} from "./difference-in-business-days/difference-in-business-days"; +export { differenceInBusinessDays } from "./difference-in-business-days/difference-in-business-days"; export { type FormatBoletoOptions, formatBoleto } from "./format-boleto/format-boleto"; export { type FormatCaepfOptions, formatCaepf } from "./format-caepf/format-caepf"; export { type FormatCeiOptions, formatCei } from "./format-cei/format-cei"; export { type FormatCepOptions, formatCep } from "./format-cep/format-cep"; export { type FormatCertidaoOptions, formatCertidao } from "./format-certidao/format-certidao"; -export { formatCnae } from "./format-cnae/format-cnae"; +export { type FormatCnaeOptions, formatCnae } from "./format-cnae/format-cnae"; export { type FormatCnhOptions, formatCnh } from "./format-cnh/format-cnh"; export { type FormatCnoOptions, formatCno } from "./format-cno/format-cno"; export { type FormatCnpjOptions, formatCnpj } from "./format-cnpj/format-cnpj"; @@ -36,7 +30,7 @@ export { type FormatCurrencyOptions, formatCurrency } from "./format-currency/fo export { formatIban } from "./format-iban/format-iban"; export { formatLegalNature } from "./format-legal-nature/format-legal-nature"; export { formatLicensePlate } from "./format-license-plate/format-license-plate"; -export { formatNcm } from "./format-ncm/format-ncm"; +export { type FormatNcmOptions, formatNcm } from "./format-ncm/format-ncm"; export { formatNfeKey } from "./format-nfe-key/format-nfe-key"; export { formatPassport } from "./format-passport/format-passport"; export { type FormatPhoneOptions, type PhoneMask, formatPhone } from "./format-phone/format-phone"; @@ -125,7 +119,7 @@ export { getStateCodeByName } from "./get-state-code-by-name/get-state-code-by-n export { getStateNameByCode } from "./get-state-name-by-code/get-state-name-by-code"; export { getStates } from "./get-states/get-states"; export { getTimezoneByState } from "./get-timezone-by-state/get-timezone-by-state"; -export { type IsBusinessDayOptions, isBusinessDay } from "./is-business-day/is-business-day"; +export { type BusinessDayOptions, isBusinessDay } from "./is-business-day/is-business-day"; export { type IsHolidayOptions, isHoliday } from "./is-holiday/is-holiday"; export { type IsValidBankAccountOptions, @@ -205,6 +199,7 @@ export { export { parseProcessoJuridico } from "./parse-processo-juridico/parse-processo-juridico"; export { parseVoterId } from "./parse-voter-id/parse-voter-id"; export { removeAccents } from "./remove-accents/remove-accents"; +export { subBusinessDays } from "./sub-business-days/sub-business-days"; /** * The bank account `isValidBankAccount` checks: the bank, the agency and the account with its diff --git a/src/is-valid-boleto/is-valid-boleto.ts b/src/is-valid-boleto/is-valid-boleto.ts index 56f148f8..1a2e02f5 100644 --- a/src/is-valid-boleto/is-valid-boleto.ts +++ b/src/is-valid-boleto/is-valid-boleto.ts @@ -37,6 +37,10 @@ const isValidCheckDigit = (boleto: string): boolean => { * "arrecadação" (convênio/tributos) bank slip: 48 digit linha digitável or 44 digit * barcode, both starting with `8`. * + * One leniency is kept from 2.3.0: the código de moeda in position 4 of the cobrança bancária + * barcode is not checked, although Carta-Circular BCB nº 2.926/2000 fixes it at `9` (real), so + * a slip carrying any other moeda digit still validates. + * * @param {string} value - The bank slip number to validate. * @returns {boolean} True if the bank slip number is valid, false otherwise. * @@ -47,12 +51,14 @@ const isValidCheckDigit = (boleto: string): boolean => { * isValidBoleto("846100000005246100291102005460339004695895061080"); // true (arrecadação) * ``` * - * Carta-Circular BCB nº 2.926/2000 specifies the linha digitável fields, the módulo 11 check - * digit (using 1 for remainders 0, 10 and 1) and the fator de vencimento behind the 47 digit - * cobrança bancária slip; the FEBRABAN layout index covers the arrecadação slip. + * Carta-Circular BCB nº 2.926/2000 specifies the linha digitável fields and the módulo 11 + * check digit (using 1 for remainders 0, 10 and 1) of the 47 digit cobrança bancária slip, + * including the position of the fator de vencimento field. The FEBRABAN "Layout Padrão de + * Arrecadação/Recebimento com Utilização do Código de Barras" and the FEBRABAN layout index + * cover the arrecadação slip. * - * @see Official: https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf * @see Official: https://www.bcb.gov.br/pre/normativos/c_circ/2000/pdf/c_circ_2926_v1_O.pdf + * @see Official: https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf * @see Official: https://portal.febraban.org.br/pagina/3425/33/pt-br/layout-febraban */ export const isValidBoleto = (value: string): boolean => { diff --git a/src/is-valid-certidao/is-valid-certidao.ts b/src/is-valid-certidao/is-valid-certidao.ts index f8ea9e54..cfc4898c 100644 --- a/src/is-valid-certidao/is-valid-certidao.ts +++ b/src/is-valid-certidao/is-valid-certidao.ts @@ -67,14 +67,17 @@ const getCheckDigit = (value: string): number => { * isValidCertidao("104539 01 55 2013 1 00012 021 0000123 21", { accept: ["death"] }); // false * ``` * - * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 Código Nacional de Normas da - * Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento CNJ nº 149/2023), art. 473 - * in the wording of the Provimento CN nº 182, de 17/09/2024: the in-force layout of the 32 - * digit matrícula. - * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 Provimento CNJ nº 2, de 27/04/2009, - * which instituted the modelos únicos de certidão and the matrícula (revoked; historical). - * @see Based on: http://ghiorzi.org/DVnew.htm Worked example of the two check digits - * (sums 288 and 309). + * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 + * Código Nacional de Normas da Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento + * CNJ nº 149/2023), art. 473 as currently published: the in-force layout of the 32 digit + * matrícula. Inciso II and §§ 1º to 5º carry the redação of the Provimento CN nº 237, de + * 13/07/2026; the rest of the article, and the digit layout this library depends on, come from the + * Provimento CN nº 182, de 17/09/2024. + * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 + * Provimento CNJ nº 2, de 27/04/2009, which instituted the modelos únicos de certidão and the + * matrícula (revoked; historical). + * @see Based on: http://ghiorzi.org/DVnew.htm + * Worked example of the two check digits (sums 288 and 309). * @see Based on: https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts * Reference implementation, and the source of the matrículas used as test vectors. * @see Based on: https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php diff --git a/src/is-valid-cns/is-valid-cns.ts b/src/is-valid-cns/is-valid-cns.ts index 3e4e7b34..a8e44cf7 100644 --- a/src/is-valid-cns/is-valid-cns.ts +++ b/src/is-valid-cns/is-valid-cns.ts @@ -58,8 +58,13 @@ const isValidProvisional = (digits: string): boolean => * ``` * * @see Official: https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/ + * ANVISA's two validation routines, the ones implemented here. The page sits behind a bot filter + * and answers HTTP 403 to every non-browser client, so it has to be opened in a browser. * @see Based on: https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html - * e-SUS APS documentation of the same DATASUS algorithm, reachable without a browser. + * e-SUS APS documentation of the same DATASUS algorithm, reachable without a browser. It applies + * the provisional routine to numbers starting with 5, 7, 8 or 9; this implementation follows the + * ANVISA page, which restricts it to 7, 8 and 9, so a 5 prefixed number is rejected even when its + * weighted sum checks out. */ export const isValidCns = (value: string | number): boolean => { if (typeof value !== "string" && typeof value !== "number") return false; diff --git a/src/is-valid-legal-nature/constants.ts b/src/is-valid-legal-nature/constants.ts index e83e24a9..5f786e66 100644 --- a/src/is-valid-legal-nature/constants.ts +++ b/src/is-valid-legal-nature/constants.ts @@ -3,12 +3,12 @@ * * Generated by `node ./scripts/legal-natures.ts`. Do not edit by hand. * - * 92 of the 100 entries are the official codes from the CONCLA 2021 table; the other 8 - * (2076, 2100, 2208, 3042, 3050, 3093, 3123, 5002) are legacy codes kept for 2.3.0 - * compatibility. Code 3298 fixes an accent typo of the official PDF ("Referendária"). + * 92 of the 100 entries are the official codes from the CONCLA 2021 table; the other + * 8 (2076, 2100, 2208, 3042, 3050, 3093, 3123, 5002) are legacy codes kept for 2.3.0 + * compatibility. These codes fix an accent typo of the official PDF: 3298. * - * @see https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 - * @see https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf + * @see Official: https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 + * @see Official: https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf */ export const LEGAL_NATURE: Record = { "1015": "Órgão Público do Poder Executivo Federal", @@ -114,4 +114,5 @@ export const LEGAL_NATURE: Record = { "5002": "Organização Internacional e Outras Instituições Extraterritoriais", }; +/** Mask characters (hyphen, dot, whitespace) tolerated around a legal nature code. */ export const MASK_REGEX = /[-.\s]/g; diff --git a/src/is-valid-passport/is-valid-passport.ts b/src/is-valid-passport/is-valid-passport.ts index 188c9119..1721497f 100644 --- a/src/is-valid-passport/is-valid-passport.ts +++ b/src/is-valid-passport/is-valid-passport.ts @@ -22,7 +22,11 @@ import { PASSPORT_REGEX } from "./constants"; * isValidPassport("12345678") // false * isValidPassport("DC-221345extra") // false * + * The Polícia Federal passport FAQ states the layout: "Ele é composto por duas letras - chamadas + * de 'série', e por seis dígitos subsequentes. Por exemplo: Passaporte CS265436." + * * @see Official: https://www.gov.br/pf/pt-br/assuntos/passaporte + * @see Official: https://www.gov.br/pf/pt-br/assuntos/passaporte/ajuda/duvidas_/caderneta/caderneta-numero-onde-fica-e */ export const isValidPassport = (passport: string | number): boolean => { if (typeof passport !== "string") return false; diff --git a/src/parse-boleto/parse-boleto.ts b/src/parse-boleto/parse-boleto.ts index caf9008f..e8b324b3 100644 --- a/src/parse-boleto/parse-boleto.ts +++ b/src/parse-boleto/parse-boleto.ts @@ -21,12 +21,14 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * // "826300000011098800100702024102024000000205104519" * ``` * - * Carta-Circular BCB nº 2.926/2000 specifies the linha digitável fields, the módulo 11 check - * digit (using 1 for remainders 0, 10 and 1) and the fator de vencimento behind the 47 digit - * cobrança bancária slip; the FEBRABAN layout index covers the arrecadação slip. + * Carta-Circular BCB nº 2.926/2000 specifies the linha digitável fields and the módulo 11 + * check digit (using 1 for remainders 0, 10 and 1) of the 47 digit cobrança bancária slip, + * including the position of the fator de vencimento field. The FEBRABAN "Layout Padrão de + * Arrecadação/Recebimento com Utilização do Código de Barras" and the FEBRABAN layout index + * cover the arrecadação slip. * - * @see Official: https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf * @see Official: https://www.bcb.gov.br/pre/normativos/c_circ/2000/pdf/c_circ_2926_v1_O.pdf + * @see Official: https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf * @see Official: https://portal.febraban.org.br/pagina/3425/33/pt-br/layout-febraban */ export const parseBoleto = (value: string | number): string => { diff --git a/src/parse-certidao/constants.ts b/src/parse-certidao/constants.ts index 1d682790..47f8d9dd 100644 --- a/src/parse-certidao/constants.ts +++ b/src/parse-certidao/constants.ts @@ -10,13 +10,17 @@ * IV of the revoked Provimento CNJ nº 63/2017 and are kept because matrículas issued under it * are still in circulation. * - * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 Código Nacional de Normas da - * Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento CNJ nº 149/2023), art. 473 - * in the wording of the Provimento CN nº 182, de 17/09/2024: the in-force layout of the 32 - * digit matrícula. - * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 Provimento CNJ nº 2, de 27/04/2009, - * which instituted the modelos únicos de certidão and the matrícula (revoked; historical). - * @see Based on: http://ghiorzi.org/DVnew.htm Description of the nine books and their codes. + * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 + * Código Nacional de Normas da Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento + * CNJ nº 149/2023), art. 473 as currently published: the in-force layout of the 32 digit + * matrícula. Inciso II and §§ 1º to 5º carry the redação of the Provimento CN nº 237, de + * 13/07/2026; the rest of the article, and the digit layout this library depends on, come from the + * Provimento CN nº 182, de 17/09/2024. + * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 + * Provimento CNJ nº 2, de 27/04/2009, which instituted the modelos únicos de certidão and the + * matrícula (revoked; historical). + * @see Based on: http://ghiorzi.org/DVnew.htm + * Description of the nine books and their codes. * @see Based on: https://github.com/Casilhero/brazilian-validators/blob/main/src/Support/CertidaoInfo.php * Reference implementation agreeing on the same nine books, in the same order. */ diff --git a/src/parse-certidao/parse-certidao.ts b/src/parse-certidao/parse-certidao.ts index 977e19d5..77655805 100644 --- a/src/parse-certidao/parse-certidao.ts +++ b/src/parse-certidao/parse-certidao.ts @@ -28,7 +28,7 @@ export type CertidaoType = export type Certidao = { /** The 6 digit CNS (Código Nacional de Serventia) of the serventia that issued the act. */ registryCns: string; - /** Acervo the book belongs to: "01" the serventia's own, "02" a collection it absorbed. */ + /** Acervo the book belongs to: "01" the serventia's own acervo; 02 and up, one per incorporated acervo. */ acervo: string; /** Service rendered by the serventia, always "55", the registro civil das pessoas naturais. */ service: string; @@ -71,14 +71,17 @@ export type Certidao = { * parseCertidao("invalid"); // null * ``` * - * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 Código Nacional de Normas da - * Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento CNJ nº 149/2023), art. 473 - * in the wording of the Provimento CN nº 182, de 17/09/2024: the in-force layout of the 32 - * digit matrícula. - * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 Provimento CNJ nº 2, de 27/04/2009, - * which instituted the modelos únicos de certidão and the matrícula (revoked; historical). - * @see Based on: http://ghiorzi.org/DVnew.htm Worked example of the two check digits - * (sums 288 and 309). + * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 + * Código Nacional de Normas da Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento + * CNJ nº 149/2023), art. 473 as currently published: the in-force layout of the 32 digit + * matrícula. Inciso II and §§ 1º to 5º carry the redação of the Provimento CN nº 237, de + * 13/07/2026; the rest of the article, and the digit layout this library depends on, come from the + * Provimento CN nº 182, de 17/09/2024. + * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 + * Provimento CNJ nº 2, de 27/04/2009, which instituted the modelos únicos de certidão and the + * matrícula (revoked; historical). + * @see Based on: http://ghiorzi.org/DVnew.htm + * Worked example of the two check digits (sums 288 and 309). * @see Based on: https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts * Reference implementation, and the source of the matrículas used as test vectors. * @see Based on: https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php diff --git a/src/parse-passport/parse-passport.ts b/src/parse-passport/parse-passport.ts index 88971fb4..c0550562 100644 --- a/src/parse-passport/parse-passport.ts +++ b/src/parse-passport/parse-passport.ts @@ -14,6 +14,7 @@ import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/s * parsePassport("Ab -. 123456") // "AB123456" * * @see Official: https://www.gov.br/pf/pt-br/assuntos/passaporte + * @see Official: https://www.gov.br/pf/pt-br/assuntos/passaporte/ajuda/duvidas_/caderneta/caderneta-numero-onde-fica-e */ export const parsePassport = (passport: string): string => typeof passport === "string" ? sanitizeToAlphanumeric(passport).slice(0, PASSPORT_LENGTH) : ""; diff --git a/src/remove-accents/remove-accents.ts b/src/remove-accents/remove-accents.ts index 2217fa8b..230246fe 100644 --- a/src/remove-accents/remove-accents.ts +++ b/src/remove-accents/remove-accents.ts @@ -9,6 +9,9 @@ const COMBINING_MARKS_REGEX = /\p{M}/gu; * @returns {string} The text with every diacritical mark removed. `""` when `value` is not a * non-empty string. * + * @see Official: https://unicode.org/reports/tr15/ + * @see Official: https://www.unicode.org/reports/tr44/#General_Category_Values + * * @example * ```typescript * removeAccents("São Paulo"); // "Sao Paulo" From 033d6bd727719e5339da1353abb10b25add2761a Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:25:31 -0300 Subject: [PATCH 14/75] fix(municipality): return a fresh pair and overload the return type on the lookup direction --- src/get-municipality/get-municipality.test.ts | 26 ++++-- src/get-municipality/get-municipality.ts | 79 +++++++++++++++---- 2 files changed, 80 insertions(+), 25 deletions(-) diff --git a/src/get-municipality/get-municipality.test.ts b/src/get-municipality/get-municipality.test.ts index 67477bdb..6703bdbe 100644 --- a/src/get-municipality/get-municipality.test.ts +++ b/src/get-municipality/get-municipality.test.ts @@ -71,11 +71,15 @@ describe("getMunicipality", () => { ); }); - it("should return the same cached tuple instance across repeated calls with the same code", async () => { + it("should return a fresh pair, so mutating it leaves a later lookup of the same code intact", async () => { const first = await getMunicipality({ code: "3550308" }); - const second = await getMunicipality({ code: "3550308" }); - expect(first).toBe(second); + expect(first).toStrictEqual(["São Paulo", "SP"]); + + first?.fill("Mutated"); + + expect(first).toStrictEqual(["Mutated", "Mutated"]); + await expect(getMunicipality({ code: "3550308" })).resolves.toStrictEqual(["São Paulo", "SP"]); }); it("should resolve a known Boa Esperança do Norte/MT lookup", async () => { @@ -213,9 +217,10 @@ describe("getMunicipality", () => { }); }); +const lookUpEither = (options: GetMunicipalityOptions) => getMunicipality(options); + describe("getMunicipality types", () => { - it("should take a code or a name plus uf and resolve to a pair, a name or null", () => { - expectTypeOf(getMunicipality).parameter(0).toEqualTypeOf(); + it("should take a code or a name plus uf", () => { expectTypeOf().toEqualTypeOf< GetMunicipalityByCodeOptions | GetMunicipalityByNameOptions >(); @@ -224,8 +229,13 @@ describe("getMunicipality types", () => { municipalityName: string; uf: string; }>(); - expectTypeOf(getMunicipality).returns.resolves.toEqualTypeOf< - [string, string] | string | null - >(); + }); + + it("should overload the return type on the direction of the lookup", () => { + const byCode: GetMunicipalityByCodeOptions = { code: "3550308" }; + const byName: GetMunicipalityByNameOptions = { municipalityName: "São Paulo", uf: "SP" }; + expectTypeOf(getMunicipality(byCode)).resolves.toEqualTypeOf<[string, string] | null>(); + expectTypeOf(getMunicipality(byName)).resolves.toEqualTypeOf(); + expectTypeOf(lookUpEither).returns.resolves.toEqualTypeOf<[string, string] | string | null>(); }); }); diff --git a/src/get-municipality/get-municipality.ts b/src/get-municipality/get-municipality.ts index e052104a..fb1e3752 100644 --- a/src/get-municipality/get-municipality.ts +++ b/src/get-municipality/get-municipality.ts @@ -29,6 +29,7 @@ const normalizeName = (value: string): string => const getMunicipalityByCode = (code: string | number): [string, string] | null => { if (!isLookupCode(code)) return null; + // Stryker disable next-line ConditionalExpression: this guard only memoizes; CITIES_DATA is a module level constant that is never written to, so rebuilding the index on every call produces the very same entries, and each lookup already returns a fresh copy of the pair, leaving the repeated work unobservable. if (!codeIndex) { codeIndex = new Map(); @@ -42,7 +43,9 @@ const getMunicipalityByCode = (code: string | number): [string, string] | null = // `Map#get` never throws and simply misses for a key of the wrong shape (a malformed, too // short or too long code), so only the sign and the decimal point of a numeric `code`, which // `sanitizeToDigits` would silently drop, have to be pre-validated above. - return codeIndex.get(sanitizeToDigits(code)) ?? null; + const entry = codeIndex.get(sanitizeToDigits(code)); + + return entry ? [...entry] : null; }; const getMunicipalityCodeByName = ({ @@ -69,17 +72,55 @@ const getMunicipalityCodeByName = ({ }; /** - * Looks a Brazilian municipality up in the offline IBGE "localidades" dataset. + * Looks a Brazilian municipality up by its IBGE code in the offline IBGE "localidades" dataset. + * + * A `code` given as a number must be a non-negative integer: a sign and a decimal point are not + * digits, so `-3550308` and `355030.8` are rejected instead of being read as `3550308`. * - * Given a `code` it resolves the municipality name and its UF; given a `municipalityName` - * and a `uf` it resolves the IBGE code. The name lookup ignores accents and casing, and every - * run of whitespace collapses into a single space, so `"sao paulo"` matches `"São Paulo"`; a - * name written without the space does not, since only the runs that are there collapse. The - * casing is folded to upper case, the direction Unicode expands `"ß"` to `"SS"` in, so - * `"Paßos"` matches `"Passos"`. - * Validation failures and unknown municipalities are reported as `null`. A `code` given as a - * number must be a non-negative integer: a sign and a decimal point are not digits, so - * `-3550308` and `355030.8` are rejected instead of being read as `3550308`. + * @param {GetMunicipalityByCodeOptions} options - The `{ code }` query. + * @returns {Promise<[string, string] | null>} A fresh `[name, uf]` pair, which the caller owns + * and may mutate, or null when the code is malformed or unknown. + * + * @example + * ```typescript + * await getMunicipality({ code: "3550308" }); // ["São Paulo", "SP"] + * await getMunicipality({ code: 3550308 }); // ["São Paulo", "SP"] + * ``` + * + * @see Official: https://servicodados.ibge.gov.br/api/docs/localidades + */ +export function getMunicipality( + options: GetMunicipalityByCodeOptions, +): Promise<[string, string] | null>; + +/** + * Looks a Brazilian municipality's IBGE code up in the offline IBGE "localidades" dataset. + * + * The name lookup ignores accents and casing, and every run of whitespace collapses into a + * single space, so `"sao paulo"` matches `"São Paulo"`; a name written without the space does + * not, since only the runs that are there collapse. The casing is folded to upper case, the + * direction Unicode expands `"ß"` to `"SS"` in, so `"Paßos"` matches `"Passos"`. + * + * @param {GetMunicipalityByNameOptions} options - The `{ municipalityName, uf }` query. + * @returns {Promise} The 7 digit IBGE code, or null when the state code or the + * municipality is unknown. + * + * @example + * ```typescript + * await getMunicipality({ municipalityName: "sao paulo", uf: "sp" }); // "3550308" + * ``` + * + * @see Official: https://servicodados.ibge.gov.br/api/docs/localidades + */ +export function getMunicipality(options: GetMunicipalityByNameOptions): Promise; + +/** + * Looks a Brazilian municipality up in the offline IBGE "localidades" dataset, from a query + * whose direction is only known at run time. + * + * Given a `code` it resolves the municipality name and its UF; given a `municipalityName` and a + * `uf` it resolves the IBGE code. Validation failures and unknown municipalities are reported + * as `null`. * * @param {GetMunicipalityOptions} options - Either `{ code }` or `{ municipalityName, uf }`. * @returns {Promise<[string, string] | string | null>} The `[name, uf]` pair when looking up @@ -89,16 +130,20 @@ const getMunicipalityCodeByName = ({ * * @example * ```typescript - * await getMunicipality({ code: "3550308" }); // ["São Paulo", "SP"] - * await getMunicipality({ code: 3550308 }); // ["São Paulo", "SP"] - * await getMunicipality({ municipalityName: "sao paulo", uf: "sp" }); // "3550308" + * const lookUp = (options: GetMunicipalityOptions) => getMunicipality(options); + * + * await lookUp({ code: "3550308" }); // ["São Paulo", "SP"] * ``` * * @see Official: https://servicodados.ibge.gov.br/api/docs/localidades */ -export const getMunicipality = ( +export function getMunicipality( + options: GetMunicipalityOptions, +): Promise<[string, string] | string | null>; + +export function getMunicipality( options: GetMunicipalityOptions, -): Promise<[string, string] | null | string> => { +): Promise<[string, string] | string | null> { if (isNullish(options) || typeof options !== "object" || Array.isArray(options)) { return Promise.resolve(null); } @@ -108,4 +153,4 @@ export const getMunicipality = ( } return Promise.resolve(getMunicipalityCodeByName(options)); -}; +} From f0abf9270dd10f075b4df90df608bbeb0b75c3f2 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:25:32 -0300 Subject: [PATCH 15/75] ci(datasets): validate both bank outputs before writing either file --- scripts/banks.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/banks.ts b/scripts/banks.ts index 9608bd94..dd49d72a 100644 --- a/scripts/banks.ts +++ b/scripts/banks.ts @@ -165,11 +165,8 @@ const main = async (): Promise => { throw new Error("Refusing to write an empty bank dataset"); } - console.log(`Generated ${sorted.length} banks from ${source}`); - - await writeFile( - resolve(scriptsDir, "..", "./src/_internals/constants/banks.ts"), - `/** + const banksPath = resolve(scriptsDir, "..", "./src/_internals/constants/banks.ts"); + const banksFile = `/** * Brazilian STR (Sistema de Transferência de Reservas) participants that have a compensation * code (commonly known as COMPE), published by Banco Central do Brasil. Generated by * \`scripts/banks.ts\`. @@ -184,8 +181,7 @@ export type Bank = { name: string; }; -export const BANKS: Bank[] = ${JSON.stringify(sorted)};`, - ); +export const BANKS: Bank[] = ${JSON.stringify(sorted)};`; const compeCodes = sorted.map((bank) => bank.code).join(""); const constantsPath = resolve(scriptsDir, "..", "./src/is-valid-bank-account/constants.ts"); @@ -200,6 +196,9 @@ export const BANKS: Bank[] = ${JSON.stringify(sorted)};`, throw new Error("COMPE_CODES literal not found in src/is-valid-bank-account/constants.ts"); } + console.log(`Generated ${sorted.length} banks from ${source}`); + + await writeFile(banksPath, banksFile); await writeFile(constantsPath, updated); console.log(`Updated COMPE_CODES with ${sorted.length} codes`); }; From 89cb3d5e23df2464675da88e8ab7ed52d7f6c52a Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:25:32 -0300 Subject: [PATCH 16/75] fix(holidays): move the Santa Catarina state holidays to Sunday per the 1999, 2004 and 2005 laws --- src/get-holidays/constants.ts | 84 +++++++++++++++++++++++++-- src/get-holidays/get-holidays.test.ts | 74 +++++++++++++++++++++++ src/get-holidays/get-holidays.ts | 10 +++- src/is-holiday/is-holiday.ts | 18 ++++-- 4 files changed, 174 insertions(+), 12 deletions(-) diff --git a/src/get-holidays/constants.ts b/src/get-holidays/constants.ts index 010ff73a..b3eb0e6b 100644 --- a/src/get-holidays/constants.ts +++ b/src/get-holidays/constants.ts @@ -42,6 +42,30 @@ export const PB_MORTE_JOAO_PESSOA_UNTIL_YEAR = 2016; /** First year Tocantins' 18 March is no longer a holiday: Lei TO nº 2.013/2009 repealed the feriado clause on 18/02/2009. */ export const TO_AUTONOMIA_UNTIL_YEAR = 2009; +/** + * First year Santa Catarina's 25 November moves to the following Sunday: Lei SC nº 11.213, de + * 11/11/1999, added the transfer clause to Lei SC nº 10.306/1996 and, by its art. 2º, entered + * into force on the day it was published (DO 16.290, de 12/11/1999), thirteen days before that + * year's 25 November. + */ +export const SC_ALEXANDRIA_TRANSFER_SINCE_YEAR = 1999; + +/** + * The one year Santa Catarina's 25 November is observed on the statutory date again: art. 3º of + * Lei SC nº 12.906, de 22/01/2004, revoked Lei SC nº 11.213/1999 outright and its own art. 1º did + * not carry the transfer clause forward, leaving 2004 without one until Lei SC nº 13.408/2005 + * reinstated it. + */ +export const SC_ALEXANDRIA_TRANSFER_GAP_YEAR = 2004; + +/** + * First year Santa Catarina's 11 August and 25 November both move to the following Sunday: Lei SC + * nº 13.408, de 15/07/2005, added the transfer clause covering the two dates and entered into + * force on the day it was published (DO 17.680, de 15/07/2005), before that year's 11 August. Up + * to 2004 the 11 August holiday was always observed on the date itself. + */ +export const SC_NEXT_SUNDAY_TRANSFER_SINCE_YEAR = 2005; + /** * Feriados estaduais, um `@see` por entrada. * @@ -52,7 +76,10 @@ export const TO_AUTONOMIA_UNTIL_YEAR = 2009; * because art. 1º, II covers them. * * The statutory date is what is emitted. Three states shift the observed date and only Santa - * Catarina's shift is modelled here (`nextSundayWhenWeekday`): Acre moves feriados falling from + * Catarina's shift is modelled here (`nextSundayWhenWeekday`, from + * `SC_ALEXANDRIA_TRANSFER_SINCE_YEAR` on for 25 November, apart from the + * `SC_ALEXANDRIA_TRANSFER_GAP_YEAR` gap, and from `SC_NEXT_SUNDAY_TRANSFER_SINCE_YEAR` on for + * 11 August): Acre moves feriados falling from * Tuesday to Thursday on to the following Friday (Lei AC nº 2.126/2009, except 15/06), and the * Goiás executive may move 26/07 and 28/10 to a nearby dia útil by decree (Lei GO nº 20.756/2020, * art. 269, § 1º), neither of which can be resolved from a year alone. @@ -167,10 +194,30 @@ export const TO_AUTONOMIA_UNTIL_YEAR = 2009; * Lei SC nº 10.306/1996, art. 1º, in the wording of Lei SC nº 12.906/2004: "É considerada data * magna do Estado o dia 11 de agosto, Dia do Estado de Santa Catarina, e dia de Santa Catarina de * Alexandria, dia 25 de novembro". + * @see Official: http://leis.alesc.sc.gov.br/html/1999/11213_1999_lei.html + * Lei SC nº 11.213, de 11 de novembro de 1999, which added to art. 1º of Lei SC nº 10.306/1996 the + * parágrafo único transferring 25 November alone: "Sempre que o dia 25 de novembro coincidir com + * dia útil da semana, o feriado e os eventos alusivos à data serão transferidos para o domingo + * subseqüente". Its art. 2º put it in force on the day it was published (DO 16.290, de 12/11/1999), + * thirteen days before that year's 25 November, so the 25 November transfer starts in 1999 and not + * in 2005. The Anexo of the in-force Lei SC nº 18.531/2022 credits the same clause to "10.306, de + * 1996; 11.213, de 1999 e 12.906, de 2004". + * @see Official: http://leis.alesc.sc.gov.br/html/2004/12906_2004_lei.html + * Lei SC nº 12.906, de 22 de janeiro de 2004, which added 11 August to the caput of art. 1º of Lei + * SC nº 10.306/1996 and, by its art. 3º, "Revoga-se a Lei nº 11.213, de 11 de novembro de 1999" + * without restating the transfer clause. It entered into force on the day it was published (DO + * 17.320, de 22/01/2004), before that year's 25 November, so 2004 is the one year in which neither + * date is transferred. * @see Official: http://leis.alesc.sc.gov.br/html/2005/13408_2005_lei.html - * Lei SC nº 13.408/2005, which added the parágrafo único transferring both dates to the following - * Sunday. Lei SC nº 16.719/2015, cited here before, was revoked by Lei SC nº 17.335/2017, itself - * consolidated and revoked by Lei SC nº 18.531/2022. + * Lei SC nº 13.408, de 15/07/2005, which reinstated the parágrafo único, this time transferring + * both dates to the following Sunday, and, by its art. 2º, entered into force on the day it was + * published (DO 17.680, de 15/07/2005): "Sempre que o dia 11 de agosto e o dia 25 de novembro + * coincidirem com dias úteis da semana, os feriados e os eventos alusivos às datas serão + * transferidos para o domingo subseqüente". Both of that year's dates fall after it. The two + * holidays are therefore split by year: 11 August is fixed up to 2004 and transferring from 2005 + * on, while 25 November is fixed up to 1998, transferring from 1999 to 2003, fixed again in 2004 + * and transferring from 2005 on. Lei SC nº 16.719/2015, cited here before, was revoked by Lei SC nº + * 17.335/2017, itself consolidated and revoked by Lei SC nº 18.531/2022. * @see Official: https://www.al.sp.gov.br/documentacao/estudos-e-manuais/feriado-9-julho/artigo.htm * Lei SP nº 9.497/1997 (PL 710/1995), Revolução Constitucionalista (09/07) * @see Official: https://www.al.sp.gov.br/repositorio/legislacao/lei/2023/lei-17746-12.09.2023.html @@ -291,13 +338,42 @@ export const STATE_HOLIDAYS: Partial> = { name: "Dia do Estado de Santa Catarina", day: 11, month: 8, + until: SC_NEXT_SUNDAY_TRANSFER_SINCE_YEAR, + }, + { + name: "Dia do Estado de Santa Catarina", + day: 11, + month: 8, + nextSundayWhenWeekday: true, + since: SC_NEXT_SUNDAY_TRANSFER_SINCE_YEAR, + }, + { + name: "Dia de Santa Catarina de Alexandria", + day: 25, + month: 11, + until: SC_ALEXANDRIA_TRANSFER_SINCE_YEAR, + }, + { + name: "Dia de Santa Catarina de Alexandria", + day: 25, + month: 11, nextSundayWhenWeekday: true, + since: SC_ALEXANDRIA_TRANSFER_SINCE_YEAR, + until: SC_ALEXANDRIA_TRANSFER_GAP_YEAR, + }, + { + name: "Dia de Santa Catarina de Alexandria", + day: 25, + month: 11, + since: SC_ALEXANDRIA_TRANSFER_GAP_YEAR, + until: SC_NEXT_SUNDAY_TRANSFER_SINCE_YEAR, }, { name: "Dia de Santa Catarina de Alexandria", day: 25, month: 11, nextSundayWhenWeekday: true, + since: SC_NEXT_SUNDAY_TRANSFER_SINCE_YEAR, }, ], SP: [ diff --git a/src/get-holidays/get-holidays.test.ts b/src/get-holidays/get-holidays.test.ts index 0ecf6f10..813a69b5 100644 --- a/src/get-holidays/get-holidays.test.ts +++ b/src/get-holidays/get-holidays.test.ts @@ -506,6 +506,80 @@ describe("getHolidays", () => { }); }); + test("should keep the Santa Catarina 11 August holiday on its statutory weekday before 2005, the year Lei SC nº 13.408/2005 extended the transfer to it (11/08/2003 is a Monday)", () => { + expect(getHolidays({ year: 2003, stateCode: "SC" })).toContainEqual({ + name: "Dia do Estado de Santa Catarina", + date: new Date(2003, 7, 11), + type: "state", + }); + }); + + test("should keep the Santa Catarina 25 November holiday on its statutory weekday before 1999, the year Lei SC nº 11.213/1999 introduced its transfer (25/11/1998 is a Wednesday)", () => { + expect(getHolidays({ year: 1998, stateCode: "SC" })).toContainEqual({ + name: "Dia de Santa Catarina de Alexandria", + date: new Date(1998, 10, 25), + type: "state", + }); + }); + + test("should move the Santa Catarina 25 November holiday to the following Sunday from 1999 on, the year Lei SC nº 11.213, de 11/11/1999, entered into force thirteen days before it (25/11/1999 is a Thursday)", () => { + expect(getHolidays({ year: 1999, stateCode: "SC" })).toContainEqual({ + name: "Dia de Santa Catarina de Alexandria", + date: new Date(1999, 10, 28), + type: "state", + }); + }); + + test("should move the Santa Catarina 25 November holiday into the next month when the following Sunday falls there (25/11/2002 is a Monday, so the holiday lands on 01/12/2002)", () => { + expect(getHolidays({ year: 2002, stateCode: "SC" })).toContainEqual({ + name: "Dia de Santa Catarina de Alexandria", + date: new Date(2002, 11, 1), + type: "state", + }); + }); + + test("should keep the Santa Catarina 25 November holiday on its statutory weekday in 2004, the one year art. 3º of Lei SC nº 12.906/2004 left it without a transfer clause (25/11/2004 is a Thursday)", () => { + expect(getHolidays({ year: 2004, stateCode: "SC" })).toContainEqual({ + name: "Dia de Santa Catarina de Alexandria", + date: new Date(2004, 10, 25), + type: "state", + }); + }); + + test("should move the Santa Catarina 25 November holiday again from 2005 on, the year Lei SC nº 13.408/2005 reinstated the transfer (25/11/2005 is a Friday)", () => { + expect(getHolidays({ year: 2005, stateCode: "SC" })).toContainEqual({ + name: "Dia de Santa Catarina de Alexandria", + date: new Date(2005, 10, 27), + type: "state", + }); + }); + + test("should switch to the Sunday transfer exactly in 2005, the year Lei SC nº 13.408, de 15/07/2005, entered into force (11/08/2004 is a Wednesday and stays, 11/08/2005 a Thursday and moves to 14/08)", () => { + expect(getHolidays({ year: 2004, stateCode: "SC" })).toContainEqual({ + name: "Dia do Estado de Santa Catarina", + date: new Date(2004, 7, 11), + type: "state", + }); + expect(getHolidays({ year: 2005, stateCode: "SC" })).toContainEqual({ + name: "Dia do Estado de Santa Catarina", + date: new Date(2005, 7, 14), + type: "state", + }); + }); + + test("should list each Santa Catarina holiday exactly once in every year the four 25 November ranges and the two 11 August ranges border on", () => { + for (const year of [1998, 1999, 2003, 2004, 2005, 2025]) { + const names = getHolidays({ year, stateCode: "SC" }).map((holiday) => holiday.name); + + expect(names.filter((name) => name === "Dia do Estado de Santa Catarina")).toEqual([ + "Dia do Estado de Santa Catarina", + ]); + expect(names.filter((name) => name === "Dia de Santa Catarina de Alexandria")).toEqual([ + "Dia de Santa Catarina de Alexandria", + ]); + } + }); + test("should treat a prototype chain key as an unknown stateCode instead of throwing", () => { const nationalHolidays = getHolidays(2024); diff --git a/src/get-holidays/get-holidays.ts b/src/get-holidays/get-holidays.ts index 3e84dc2d..8c1a6700 100644 --- a/src/get-holidays/get-holidays.ts +++ b/src/get-holidays/get-holidays.ts @@ -141,9 +141,13 @@ const computeHolidays = (year: number, stateCode: StateCode | undefined): Holida * authorizes "a data magna do Estado fixada em lei estadual" in the singular; the other entries * of `STATE_HOLIDAYS` rest on ordinary state laws and are reported because they are observed in * practice. The date returned is the statutory one. Santa Catarina's two holidays are the only - * observance shift the table models (both move to the following Sunday when they fall Monday to - * Friday); Acre's Tuesday-to-Thursday shift and the Goiás decrees that may move 26/07 and 28/10 - * are not, because neither can be resolved from a year alone. + * observance shift the table models: each moves to the following Sunday when it falls Monday to + * Friday, 11 August from 2005 on, when Lei SC nº 13.408/2005 extended the transfer to it, and + * 25 November from 1999 on, when Lei SC nº 11.213/1999 first introduced it, except in 2004, the + * year art. 3º of Lei SC nº 12.906/2004 left it without a transfer clause. Outside those ranges + * each holiday stays on 11 August or 25 November. Acre's Tuesday-to-Thursday shift and the Goiás decrees + * that may move 26/07 and 28/10 are not modelled, because neither can be resolved from a year + * alone. * * @param {number} year - The year for which to retrieve holidays (must be between 1900 and 2099) * @returns {Holiday[]} An array of holidays sorted by date diff --git a/src/is-holiday/is-holiday.ts b/src/is-holiday/is-holiday.ts index acf594e2..d59e51b0 100644 --- a/src/is-holiday/is-holiday.ts +++ b/src/is-holiday/is-holiday.ts @@ -20,14 +20,22 @@ export type IsHolidayOptions = { * "2024-12-24" in local time, so build `targetDate` from local components * (`new Date(2024, 11, 25)`) or from a full ISO datetime when you mean a specific local day. * - * If `stateCode` is provided but is not a valid/known state code, it is ignored and only - * national holidays are considered (same behavior as `getHolidays`). The lookup is an - * own-property one, so a prototype-chain key such as `"__proto__"` or `"constructor"` is an - * unknown state code like any other. + * An invalid `stateCode` is treated in two different ways, depending on its type: + * + * - a string that is not a known state code is ignored, and only national holidays are + * considered, the same behavior as `getHolidays`. The lookup is an own-property one, so a + * prototype-chain key such as `"__proto__"` or `"constructor"` is an unknown state code like + * any other; + * - a `stateCode` that is present and is not a string at all (a number, `null`, an object) is + * rejected rather than ignored: `isHoliday` returns `false` without looking at the date, even + * when that date is a national holiday. `undefined`, or an absent property, is the only + * non-string value that stands for "no state" instead. * * The date a state holiday is checked against is the statutory one, except for Santa Catarina's * two holidays, which `getHolidays` moves to the following Sunday when they fall Monday to - * Friday, as Lei SC nº 18.531/2022 requires. + * Friday: 11 August from 2005 on, as Lei SC nº 13.408/2005 introduced, and 25 November from 1999 + * on, as Lei SC nº 11.213/1999 introduced, save for 2004, the year art. 3º of Lei SC nº + * 12.906/2004 left that date without a transfer clause. Lei SC nº 18.531/2022 now carries both. * * @param {IsHolidayOptions} [options] - Options for the check. * @param {Date} options.targetDate - The date to check. From e93549970b95aaf7767e9ad328a51f7abb6adce8 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:25:32 -0300 Subject: [PATCH 17/75] =?UTF-8?q?docs:=20describe=20the=20acervo=20codes?= =?UTF-8?q?=20of=20art.=20473,=20the=20arrecada=C3=A7=C3=A3o=20result=20an?= =?UTF-8?q?d=20the=20legal-nature=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/llms-full.txt | 33 ++++++++++++++++++++++---- docs/pt-br/utilities.md | 33 ++++++++++++++++++++++---- docs/utilities.md | 33 ++++++++++++++++++++++---- scripts/legal-natures.ts | 3 ++- src/get-boleto-info/get-boleto-info.ts | 4 +++- src/is-valid-legal-nature/constants.ts | 3 ++- src/parse-certidao/parse-certidao.ts | 11 ++++++++- 7 files changed, 101 insertions(+), 19 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index d8ad9ee0..46ff0d75 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -396,7 +396,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ### getBoletoInfo -Extract information from a boleto (amount, expiration date, bank code). Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. For a boleto de arrecadação, the result, typed as `BoletoInfo`, has no `bankCode`/`expirationDate` and instead carries `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. +Extract information from a boleto (amount, expiration date, bank code). Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -1282,7 +1282,7 @@ Get Brazilian holidays for a given year. Returns national holidays and optionall Only one state holiday per UF is a feriado civil under [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm), art. 1º, II, which authorises "a data magna do Estado fixada em lei estadual" in the singular; the other entries rest on ordinary state laws and are reported because they are observed in practice. Notable per-state rules: -- **SC** — [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) moves both state holidays, "Dia do Estado de Santa Catarina" (Aug 11) and "Dia de Santa Catarina de Alexandria" (Nov 25), to the following Sunday whenever they fall Monday to Friday, so Monday Aug 11 2025 is a business day in SC and the holiday lands on Sunday Aug 17. +- **SC** — [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) moves both state holidays, "Dia do Estado de Santa Catarina" (Aug 11) and "Dia de Santa Catarina de Alexandria" (Nov 25), to the following Sunday whenever they fall Monday to Friday, so Monday Aug 11 2025 is a business day in SC and the holiday lands on Sunday Aug 17. The transfer starts in 2005, the year [Lei SC nº 13.408/2005](http://leis.alesc.sc.gov.br/html/2005/13408_2005_lei.html) first introduced it (published and in force on Jul 15 2005); up to 2004 both holidays stay on Aug 11 and Nov 25 whatever weekday they fall on. - **DF** — [Lei distrital nº 72/1989](https://www.sinj.df.gov.br/sinj/Norma/18459/Lei_72_27_12_1989.html), art. 1º parágrafo único, declares Corpus Christi a feriado. With `stateCode: 'DF'` the single Corpus Christi entry comes back typed `"state"` instead of `"optional"`; it is replaced, not duplicated. - **GO** — [Lei GO nº 20.756/2020](https://legisla.casacivil.go.gov.br/pesquisa_legislacao/100979/lei-20756), art. 269, II, lists three feriados estaduais: Jul 26 (Fundação da Cidade de Goiás), Oct 24 (Lançamento da Pedra Fundamental de Goiânia) and Oct 28 (Dia do Servidor Público). - **AL** — Sep 16 is a feriado estadual from 2024 ([Lei AL nº 9.358/2024](https://sapl.al.al.leg.br/norma/3117)) and only a ponto facultativo (`"optional"`) before that. @@ -1596,7 +1596,7 @@ generatePis(); // '91077906857' ### getMunicipality -Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. A single function handles both directions, based on whether `options` has a `code` or a `municipalityName`/`uf`. `code` accepts both `string` and `number` input and must be exactly 7 digits, otherwise the function resolves to `null`. A `code` given as a number must be a non-negative integer: a sign and a decimal point are not digits, so `-3550308` and `355030.8` resolve to `null` instead of being read as `3550308`. Resolution is entirely offline, from a bundled IBGE dataset: no network request is made. The municipality name match ignores accents and casing. An unknown municipality, an unknown UF or invalid input all resolve to `null`. +Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. A single function handles both directions, based on whether `options` has a `code` or a `municipalityName`/`uf`. `code` accepts both `string` and `number` input and must be exactly 7 digits, otherwise the function resolves to `null`. A `code` given as a number must be a non-negative integer: a sign and a decimal point are not digits, so `-3550308` and `355030.8` resolve to `null` instead of being read as `3550308`. Resolution is entirely offline, from a bundled IBGE dataset: no network request is made. The municipality name match ignores accents and casing. An unknown municipality, an unknown UF or invalid input all resolve to `null`. The `[name, uf]` pair is a fresh array on every call, so mutating the result never affects subsequent lookups. ```javascript import { getMunicipality } from '@brazilian-utils/brazilian-utils'; @@ -1617,6 +1617,29 @@ await getMunicipality({ code: '123' }); // null (not 7 digits) ``` +In TypeScript the return type follows the direction of the lookup: a `{ code }` query resolves to `[string, string] | null`, a `{ municipalityName, uf }` query resolves to `string | null`, and a query whose direction is only known at run time (a variable typed as `GetMunicipalityOptions`) resolves to the union of both. + +```typescript +import { + getMunicipality, + type GetMunicipalityByCodeOptions, + type GetMunicipalityByNameOptions, + type GetMunicipalityOptions, +} from '@brazilian-utils/brazilian-utils'; + +const byCode: GetMunicipalityByCodeOptions = { code: '3550308' }; +const byName: GetMunicipalityByNameOptions = { municipalityName: 'sao paulo', uf: 'sp' }; + +await getMunicipality(byCode); +// Promise<[string, string] | null> + +await getMunicipality(byName); +// Promise + +const lookUp = (options: GetMunicipalityOptions) => getMunicipality(options); +// (options: GetMunicipalityOptions) => Promise<[string, string] | string | null> +``` + ### getMunicipalities Get Brazilian municipalities published by the IBGE. Returns all municipalities if no state is provided, or municipalities from a specific state. Each municipality is returned as `{ code, name, stateCode }`, where `code` is the 7-digit IBGE municipality code. Results are sorted by name with `localeCompare` in the "pt-BR" locale. Each call returns a fresh array of fresh objects, so mutating the result never affects subsequent calls. An unknown state code returns an empty array instead of throwing. Only an omitted (or `undefined`) `stateCode` asks for the full list: `getMunicipalities(null)` and `getMunicipalities('')` return `[]`, where the looser `getCities(null)` and `getCities('')` return every city. @@ -1670,7 +1693,7 @@ getMunicipalityByCode('123'); // null (not 7 digits) ### isHoliday -Check if a specific date is a Brazilian holiday. The check compares `targetDate`'s local calendar date (year/month/day as read locally), not its underlying UTC instant. Returns `false` when `targetDate` is missing or not a valid `Date`. +Check if a specific date is a Brazilian holiday. The check compares `targetDate`'s local calendar date (year/month/day as read locally), not its underlying UTC instant. Returns `false` when `targetDate` is missing or not a valid `Date`. An invalid `stateCode` is treated in two different ways: a string that is not a known state code is ignored and only national holidays are considered, the same as `getHolidays`, while a `stateCode` that is present and is not a string at all (a number, `null`, an object) is rejected and makes the call return `false` even for a national holiday. ```javascript import { isHoliday } from '@brazilian-utils/brazilian-utils'; @@ -1884,7 +1907,7 @@ The `Certidao` result carries: | Key | Description | | --- | --- | | `registryCns` | The 6 digit CNS (Código Nacional de Serventia) of the serventia that issued the act. | -| `acervo` | Acervo the book belongs to: `"01"` the serventia's own, `"02"` a collection it absorbed. | +| `acervo` | Acervo the book belongs to: `"01"` the serventia's own, `"02"` and up one per acervo it absorbed. [Art. 473, §§ 3º to 5º](https://atos.cnj.jus.br/atos/detalhar/5243) splits the absorbed ones by the date the origin serventia was extinguished or deactivated: up to 31/12/2009 the matrícula carries the CNS of the incorporating unit and an acervo code from `"02"` up, one per incorporation; from 01/01/2010 on it carries the CNS of the incorporated unit itself and the code `"01"`, counted as that unit's own acervo; and an acervo split between two or more successor serventias gets each successor's own CNS with the code `"02"`. | | `service` | Service rendered by the serventia, always `"55"`, the registro civil das pessoas naturais. | | `year` | Four digit year the act was recorded. | | `type` | The book the act belongs to: `"birth"`, `"marriage"`, `"religious-marriage"`, `"death"`, `"stillbirth"`, `"banns"`, `"other"`, `"emancipation"` or `"interdiction"`. | diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index a7c03052..e7e109b3 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -157,7 +157,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ## getBoletoInfo -Extrai informações de um boleto (valor, data de vencimento, código do banco). Aceita opcionalmente `{ referenceDate }` (tipado como `GetBoletoInfoOptions`) para resolver o ciclo do "fator de vencimento" a partir de uma data específica em vez de agora (o ciclo do fator reiniciou em 22/02/2025, segundo a FEBRABAN). Nem a FEBRABAN nem o Banco Central publicam uma forma de distinguir um fator do ciclo antigo de um do ciclo novo, então todo fator resolve para uma de duas datas separadas por 9000 dias e o `referenceDate` escolhe entre elas por meio das janelas de segurança da própria biblioteca: o mesmo boleto pode passar a resolver para a outra candidata com o tempo, então informe `referenceDate` explicitamente sempre que a resposta precisar ser estável. Para um boleto de arrecadação, o resultado, tipado como `BoletoInfo`, não tem `bankCode`/`expirationDate` e traz em vez disso `type: "arrecadacao"`, `segment`, `value` e `hasEffectiveValue`. +Extrai informações de um boleto (valor, data de vencimento, código do banco). Aceita opcionalmente `{ referenceDate }` (tipado como `GetBoletoInfoOptions`) para resolver o ciclo do "fator de vencimento" a partir de uma data específica em vez de agora (o ciclo do fator reiniciou em 22/02/2025, segundo a FEBRABAN). Nem a FEBRABAN nem o Banco Central publicam uma forma de distinguir um fator do ciclo antigo de um do ciclo novo, então todo fator resolve para uma de duas datas separadas por 9000 dias e o `referenceDate` escolhe entre elas por meio das janelas de segurança da própria biblioteca: o mesmo boleto pode passar a resolver para a outra candidata com o tempo, então informe `referenceDate` explicitamente sempre que a resposta precisar ser estável. Para um boleto de arrecadação, o resultado, tipado como `BoletoInfo`, continua trazendo as duas chaves, porém vazias, `bankCode: ''` e `expirationDate: null`, já que o boleto não tem código de banco nem fator de vencimento, e acrescenta `type: "arrecadacao"`, `segment`, `value` e `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -1043,7 +1043,7 @@ Retorna feriados brasileiros para um determinado ano. Retorna feriados nacionais Apenas um feriado estadual por UF é feriado civil pela [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm), art. 1º, II, que autoriza "a data magna do Estado fixada em lei estadual", no singular; as demais entradas se apoiam em leis estaduais ordinárias e são reportadas por serem observadas na prática. Regras notáveis por estado: -- **SC** — a [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) transfere os dois feriados estaduais, "Dia do Estado de Santa Catarina" (11/08) e "Dia de Santa Catarina de Alexandria" (25/11), para o domingo subsequente sempre que caem de segunda a sexta, então a segunda-feira 11/08/2025 é dia útil em SC e o feriado cai no domingo 17/08. +- **SC** — a [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) transfere os dois feriados estaduais, "Dia do Estado de Santa Catarina" (11/08) e "Dia de Santa Catarina de Alexandria" (25/11), para o domingo subsequente sempre que caem de segunda a sexta, então a segunda-feira 11/08/2025 é dia útil em SC e o feriado cai no domingo 17/08. A transferência começa em 2005, ano em que a [Lei SC nº 13.408/2005](http://leis.alesc.sc.gov.br/html/2005/13408_2005_lei.html) a introduziu (publicada e em vigor em 15/07/2005); até 2004 os dois feriados ficam em 11/08 e 25/11 em qualquer dia da semana. - **DF** — a [Lei distrital nº 72/1989](https://www.sinj.df.gov.br/sinj/Norma/18459/Lei_72_27_12_1989.html), art. 1º parágrafo único, declara Corpus Christi feriado. Com `stateCode: 'DF'` a única entrada de Corpus Christi volta tipada como `"state"` em vez de `"optional"`; ela é substituída, não duplicada. - **GO** — a [Lei GO nº 20.756/2020](https://legisla.casacivil.go.gov.br/pesquisa_legislacao/100979/lei-20756), art. 269, II, lista três feriados estaduais: 26/07 (Fundação da Cidade de Goiás), 24/10 (Lançamento da Pedra Fundamental de Goiânia) e 28/10 (Dia do Servidor Público). - **AL** — 16/09 é feriado estadual a partir de 2024 ([Lei AL nº 9.358/2024](https://sapl.al.al.leg.br/norma/3117)) e apenas ponto facultativo (`"optional"`) antes disso. @@ -1357,7 +1357,7 @@ generatePis(); // '91077906857' ## getMunicipality -Busca informações de município por código IBGE, ou obtém o código IBGE a partir do nome do município e UF. Uma única função cobre as duas direções, dependendo se `options` tem `code` ou `municipalityName`/`uf`. `code` aceita tanto `string` quanto `number` e deve ter exatamente 7 dígitos, caso contrário a função resolve para `null`. Um `code` informado como número precisa ser um inteiro não negativo: sinal e ponto decimal não são dígitos, então `-3550308` e `355030.8` resolvem para `null` em vez de serem lidos como `3550308`. A resolução é totalmente offline, a partir de um dataset do IBGE embutido na biblioteca: nenhuma requisição de rede é feita. A comparação do nome do município ignora acentos e diferenças entre maiúsculas/minúsculas. Um município desconhecido, uma UF desconhecida ou uma entrada inválida resolvem para `null`. +Busca informações de município por código IBGE, ou obtém o código IBGE a partir do nome do município e UF. Uma única função cobre as duas direções, dependendo se `options` tem `code` ou `municipalityName`/`uf`. `code` aceita tanto `string` quanto `number` e deve ter exatamente 7 dígitos, caso contrário a função resolve para `null`. Um `code` informado como número precisa ser um inteiro não negativo: sinal e ponto decimal não são dígitos, então `-3550308` e `355030.8` resolvem para `null` em vez de serem lidos como `3550308`. A resolução é totalmente offline, a partir de um dataset do IBGE embutido na biblioteca: nenhuma requisição de rede é feita. A comparação do nome do município ignora acentos e diferenças entre maiúsculas/minúsculas. Um município desconhecido, uma UF desconhecida ou uma entrada inválida resolvem para `null`. O par `[name, uf]` é um array novo a cada chamada, então alterar o resultado nunca afeta as buscas seguintes. ```javascript import { getMunicipality } from '@brazilian-utils/brazilian-utils'; @@ -1378,6 +1378,29 @@ await getMunicipality({ code: '123' }); // null (não tem 7 dígitos) ``` +Em TypeScript o tipo de retorno acompanha a direção da busca: uma consulta `{ code }` resolve para `[string, string] | null`, uma consulta `{ municipalityName, uf }` resolve para `string | null`, e uma consulta cuja direção só é conhecida em tempo de execução (uma variável tipada como `GetMunicipalityOptions`) resolve para a união das duas. + +```typescript +import { + getMunicipality, + type GetMunicipalityByCodeOptions, + type GetMunicipalityByNameOptions, + type GetMunicipalityOptions, +} from '@brazilian-utils/brazilian-utils'; + +const byCode: GetMunicipalityByCodeOptions = { code: '3550308' }; +const byName: GetMunicipalityByNameOptions = { municipalityName: 'sao paulo', uf: 'sp' }; + +await getMunicipality(byCode); +// Promise<[string, string] | null> + +await getMunicipality(byName); +// Promise + +const lookUp = (options: GetMunicipalityOptions) => getMunicipality(options); +// (options: GetMunicipalityOptions) => Promise<[string, string] | string | null> +``` + ## getMunicipalities Retorna os municípios brasileiros publicados pelo IBGE. Retorna todos os municípios se nenhum estado for fornecido, ou os municípios de um estado específico. Cada município é retornado como `{ code, name, stateCode }`, onde `code` é o código IBGE de 7 dígitos do município. Os resultados são ordenados por nome com `localeCompare` no locale "pt-BR". Cada chamada retorna um array novo com objetos novos, então alterar o resultado nunca afeta chamadas seguintes. Um código de estado desconhecido retorna um array vazio em vez de lançar erro. Só um `stateCode` omitido (ou `undefined`) pede a lista completa: `getMunicipalities(null)` e `getMunicipalities('')` retornam `[]`, enquanto os mais permissivos `getCities(null)` e `getCities('')` retornam todas as cidades. @@ -1431,7 +1454,7 @@ getMunicipalityByCode('123'); // null (não tem 7 dígitos) ## isHoliday -Verifica se uma data específica é feriado brasileiro. A verificação compara a data local do `targetDate` (ano/mês/dia lidos localmente), não seu instante UTC subjacente. Retorna `false` quando `targetDate` está ausente ou não é um `Date` válido. +Verifica se uma data específica é feriado brasileiro. A verificação compara a data local do `targetDate` (ano/mês/dia lidos localmente), não seu instante UTC subjacente. Retorna `false` quando `targetDate` está ausente ou não é um `Date` válido. Um `stateCode` inválido é tratado de duas formas diferentes: uma string que não é um código de estado conhecido é ignorada e só os feriados nacionais são considerados, igual ao `getHolidays`, enquanto um `stateCode` presente que não é uma string (um número, `null`, um objeto) é rejeitado e faz a chamada retornar `false` mesmo em um feriado nacional. ```javascript import { isHoliday } from '@brazilian-utils/brazilian-utils'; @@ -1645,7 +1668,7 @@ O resultado `Certidao` traz: | Chave | Descrição | | --- | --- | | `registryCns` | O CNS (Código Nacional de Serventia) de 6 dígitos da serventia que lavrou o ato. | -| `acervo` | Acervo a que o livro pertence: `"01"` acervo próprio, `"02"` acervo incorporado. | +| `acervo` | Acervo a que o livro pertence: `"01"` acervo próprio, `"02"` em diante um por acervo incorporado. O [art. 473, §§ 3º a 5º](https://atos.cnj.jus.br/atos/detalhar/5243) separa os incorporados pela data em que a serventia de origem foi extinta ou desativada: até 31/12/2009 a matrícula leva o CNS da unidade incorporadora e um código de acervo a partir de `"02"`, um por incorporação; a partir de 1º/01/2010 leva o CNS da própria unidade incorporada e o código `"01"`, considerado acervo próprio dessa unidade; e um acervo fracionado entre duas ou mais serventias sucessoras leva o CNS próprio de cada sucessora com o código `"02"`. | | `service` | Serviço prestado pela serventia, sempre `"55"`, o registro civil das pessoas naturais. | | `year` | Ano do registro, com 4 dígitos. | | `type` | Livro a que o ato pertence: `"birth"`, `"marriage"`, `"religious-marriage"`, `"death"`, `"stillbirth"`, `"banns"`, `"other"`, `"emancipation"` ou `"interdiction"`. | diff --git a/docs/utilities.md b/docs/utilities.md index b57bc5b1..60ef6f12 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -157,7 +157,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ## getBoletoInfo -Extract information from a boleto (amount, expiration date, bank code). Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. For a boleto de arrecadação, the result, typed as `BoletoInfo`, has no `bankCode`/`expirationDate` and instead carries `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. +Extract information from a boleto (amount, expiration date, bank code). Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -1043,7 +1043,7 @@ Get Brazilian holidays for a given year. Returns national holidays and optionall Only one state holiday per UF is a feriado civil under [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm), art. 1º, II, which authorises "a data magna do Estado fixada em lei estadual" in the singular; the other entries rest on ordinary state laws and are reported because they are observed in practice. Notable per-state rules: -- **SC** — [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) moves both state holidays, "Dia do Estado de Santa Catarina" (Aug 11) and "Dia de Santa Catarina de Alexandria" (Nov 25), to the following Sunday whenever they fall Monday to Friday, so Monday Aug 11 2025 is a business day in SC and the holiday lands on Sunday Aug 17. +- **SC** — [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) moves both state holidays, "Dia do Estado de Santa Catarina" (Aug 11) and "Dia de Santa Catarina de Alexandria" (Nov 25), to the following Sunday whenever they fall Monday to Friday, so Monday Aug 11 2025 is a business day in SC and the holiday lands on Sunday Aug 17. The transfer starts in 2005, the year [Lei SC nº 13.408/2005](http://leis.alesc.sc.gov.br/html/2005/13408_2005_lei.html) first introduced it (published and in force on Jul 15 2005); up to 2004 both holidays stay on Aug 11 and Nov 25 whatever weekday they fall on. - **DF** — [Lei distrital nº 72/1989](https://www.sinj.df.gov.br/sinj/Norma/18459/Lei_72_27_12_1989.html), art. 1º parágrafo único, declares Corpus Christi a feriado. With `stateCode: 'DF'` the single Corpus Christi entry comes back typed `"state"` instead of `"optional"`; it is replaced, not duplicated. - **GO** — [Lei GO nº 20.756/2020](https://legisla.casacivil.go.gov.br/pesquisa_legislacao/100979/lei-20756), art. 269, II, lists three feriados estaduais: Jul 26 (Fundação da Cidade de Goiás), Oct 24 (Lançamento da Pedra Fundamental de Goiânia) and Oct 28 (Dia do Servidor Público). - **AL** — Sep 16 is a feriado estadual from 2024 ([Lei AL nº 9.358/2024](https://sapl.al.al.leg.br/norma/3117)) and only a ponto facultativo (`"optional"`) before that. @@ -1357,7 +1357,7 @@ generatePis(); // '91077906857' ## getMunicipality -Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. A single function handles both directions, based on whether `options` has a `code` or a `municipalityName`/`uf`. `code` accepts both `string` and `number` input and must be exactly 7 digits, otherwise the function resolves to `null`. A `code` given as a number must be a non-negative integer: a sign and a decimal point are not digits, so `-3550308` and `355030.8` resolve to `null` instead of being read as `3550308`. Resolution is entirely offline, from a bundled IBGE dataset: no network request is made. The municipality name match ignores accents and casing. An unknown municipality, an unknown UF or invalid input all resolve to `null`. +Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. A single function handles both directions, based on whether `options` has a `code` or a `municipalityName`/`uf`. `code` accepts both `string` and `number` input and must be exactly 7 digits, otherwise the function resolves to `null`. A `code` given as a number must be a non-negative integer: a sign and a decimal point are not digits, so `-3550308` and `355030.8` resolve to `null` instead of being read as `3550308`. Resolution is entirely offline, from a bundled IBGE dataset: no network request is made. The municipality name match ignores accents and casing. An unknown municipality, an unknown UF or invalid input all resolve to `null`. The `[name, uf]` pair is a fresh array on every call, so mutating the result never affects subsequent lookups. ```javascript import { getMunicipality } from '@brazilian-utils/brazilian-utils'; @@ -1378,6 +1378,29 @@ await getMunicipality({ code: '123' }); // null (not 7 digits) ``` +In TypeScript the return type follows the direction of the lookup: a `{ code }` query resolves to `[string, string] | null`, a `{ municipalityName, uf }` query resolves to `string | null`, and a query whose direction is only known at run time (a variable typed as `GetMunicipalityOptions`) resolves to the union of both. + +```typescript +import { + getMunicipality, + type GetMunicipalityByCodeOptions, + type GetMunicipalityByNameOptions, + type GetMunicipalityOptions, +} from '@brazilian-utils/brazilian-utils'; + +const byCode: GetMunicipalityByCodeOptions = { code: '3550308' }; +const byName: GetMunicipalityByNameOptions = { municipalityName: 'sao paulo', uf: 'sp' }; + +await getMunicipality(byCode); +// Promise<[string, string] | null> + +await getMunicipality(byName); +// Promise + +const lookUp = (options: GetMunicipalityOptions) => getMunicipality(options); +// (options: GetMunicipalityOptions) => Promise<[string, string] | string | null> +``` + ## getMunicipalities Get Brazilian municipalities published by the IBGE. Returns all municipalities if no state is provided, or municipalities from a specific state. Each municipality is returned as `{ code, name, stateCode }`, where `code` is the 7-digit IBGE municipality code. Results are sorted by name with `localeCompare` in the "pt-BR" locale. Each call returns a fresh array of fresh objects, so mutating the result never affects subsequent calls. An unknown state code returns an empty array instead of throwing. Only an omitted (or `undefined`) `stateCode` asks for the full list: `getMunicipalities(null)` and `getMunicipalities('')` return `[]`, where the looser `getCities(null)` and `getCities('')` return every city. @@ -1431,7 +1454,7 @@ getMunicipalityByCode('123'); // null (not 7 digits) ## isHoliday -Check if a specific date is a Brazilian holiday. The check compares `targetDate`'s local calendar date (year/month/day as read locally), not its underlying UTC instant. Returns `false` when `targetDate` is missing or not a valid `Date`. +Check if a specific date is a Brazilian holiday. The check compares `targetDate`'s local calendar date (year/month/day as read locally), not its underlying UTC instant. Returns `false` when `targetDate` is missing or not a valid `Date`. An invalid `stateCode` is treated in two different ways: a string that is not a known state code is ignored and only national holidays are considered, the same as `getHolidays`, while a `stateCode` that is present and is not a string at all (a number, `null`, an object) is rejected and makes the call return `false` even for a national holiday. ```javascript import { isHoliday } from '@brazilian-utils/brazilian-utils'; @@ -1645,7 +1668,7 @@ The `Certidao` result carries: | Key | Description | | --- | --- | | `registryCns` | The 6 digit CNS (Código Nacional de Serventia) of the serventia that issued the act. | -| `acervo` | Acervo the book belongs to: `"01"` the serventia's own, `"02"` a collection it absorbed. | +| `acervo` | Acervo the book belongs to: `"01"` the serventia's own, `"02"` and up one per acervo it absorbed. [Art. 473, §§ 3º to 5º](https://atos.cnj.jus.br/atos/detalhar/5243) splits the absorbed ones by the date the origin serventia was extinguished or deactivated: up to 31/12/2009 the matrícula carries the CNS of the incorporating unit and an acervo code from `"02"` up, one per incorporation; from 01/01/2010 on it carries the CNS of the incorporated unit itself and the code `"01"`, counted as that unit's own acervo; and an acervo split between two or more successor serventias gets each successor's own CNS with the code `"02"`. | | `service` | Service rendered by the serventia, always `"55"`, the registro civil das pessoas naturais. | | `year` | Four digit year the act was recorded. | | `type` | The book the act belongs to: `"birth"`, `"marriage"`, `"religious-marriage"`, `"death"`, `"stillbirth"`, `"banns"`, `"other"`, `"emancipation"` or `"interdiction"`. | diff --git a/scripts/legal-natures.ts b/scripts/legal-natures.ts index 0d5cf6c2..2d8bcb3b 100644 --- a/scripts/legal-natures.ts +++ b/scripts/legal-natures.ts @@ -208,7 +208,8 @@ const main = async (): Promise => { * * ${codes.length} of the ${codes.length + legacyCodes.length} entries are the official codes from the CONCLA 2021 table; the other * ${legacyCodes.length} (${legacyCodes.join(", ")}) are legacy codes kept for 2.3.0 - * compatibility. These codes fix an accent typo of the official PDF: ${typoFixedCodes.join(", ")}. + * compatibility. Separately, and unrelated to those legacy codes, the descriptions of the + * following official codes fix an accent typo of the PDF: ${typoFixedCodes.join(", ")}. * * @see Official: ${SOURCE_PAGE_URL} * @see Official: ${SOURCE_URL} diff --git a/src/get-boleto-info/get-boleto-info.ts b/src/get-boleto-info/get-boleto-info.ts index 71ab25dd..803326a0 100644 --- a/src/get-boleto-info/get-boleto-info.ts +++ b/src/get-boleto-info/get-boleto-info.ts @@ -82,7 +82,9 @@ export type GetBoletoInfoOptions = { * Supports the 47 digit "cobrança bancária" linha digitável and, additionally, the * "arrecadação" (convênio/tributos) bank slip: 48 digit linha digitável or 44 digit * barcode, both starting with `8`. Arrecadação bank slips also return `type`, `segment`, - * `value` and `hasEffectiveValue`, and have no `bankCode` nor `expirationDate`. + * `value` and `hasEffectiveValue`, and, carrying neither a bank code nor a fator de vencimento, + * come back with `bankCode` set to `""` and `expirationDate` set to `null` rather than with those + * two keys missing. * * Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle fator de * vencimento from a new cycle one, so every factor resolves to either of two dates 9000 days diff --git a/src/is-valid-legal-nature/constants.ts b/src/is-valid-legal-nature/constants.ts index 5f786e66..6d5e62e9 100644 --- a/src/is-valid-legal-nature/constants.ts +++ b/src/is-valid-legal-nature/constants.ts @@ -5,7 +5,8 @@ * * 92 of the 100 entries are the official codes from the CONCLA 2021 table; the other * 8 (2076, 2100, 2208, 3042, 3050, 3093, 3123, 5002) are legacy codes kept for 2.3.0 - * compatibility. These codes fix an accent typo of the official PDF: 3298. + * compatibility. Separately, and unrelated to those legacy codes, the descriptions of the + * following official codes fix an accent typo of the PDF: 3298. * * @see Official: https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 * @see Official: https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf diff --git a/src/parse-certidao/parse-certidao.ts b/src/parse-certidao/parse-certidao.ts index 77655805..eee96e7a 100644 --- a/src/parse-certidao/parse-certidao.ts +++ b/src/parse-certidao/parse-certidao.ts @@ -28,7 +28,16 @@ export type CertidaoType = export type Certidao = { /** The 6 digit CNS (Código Nacional de Serventia) of the serventia that issued the act. */ registryCns: string; - /** Acervo the book belongs to: "01" the serventia's own acervo; 02 and up, one per incorporated acervo. */ + /** + * Acervo the book belongs to: `"01"` the serventia's own acervo; `"02"` and up, one per + * incorporated acervo. Art. 473, §§ 3º to 5º splits the incorporated ones by the date the + * origin serventia was extinguished or deactivated: up to 31 December 2009 the matrícula + * carries the CNS of the incorporating unit and an acervo code from `"02"` up, one per + * incorporation in their numeric order; from 1 January 2010 on it carries the CNS of the + * incorporated unit itself and the acervo code `"01"`, counted as that unit's own acervo. When + * one acervo is split between two or more successor serventias, each of them uses its own CNS + * with the acervo code `"02"`. + */ acervo: string; /** Service rendered by the serventia, always "55", the registro civil das pessoas naturais. */ service: string; From 03e5c11a8996676c489730eda317dc17d2b4cfa2 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:00 -0300 Subject: [PATCH 18/75] fix(business-days): return false from isBusinessDay for a non-string state code - `isBusinessDay(date, { stateCode: 5 })`, `null` or an object returned the plain weekday answer, silently ignoring the option, while `isHoliday` rejects the same value; both now return `false`, and only `undefined` stands for "no state" - JSDoc and docs describe the split the same way `isHoliday` documents it --- src/is-business-day/is-business-day.test.ts | 16 +++++++++++++ src/is-business-day/is-business-day.ts | 25 ++++++++++++++++----- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/is-business-day/is-business-day.test.ts b/src/is-business-day/is-business-day.test.ts index f6c44e46..314b179c 100644 --- a/src/is-business-day/is-business-day.test.ts +++ b/src/is-business-day/is-business-day.test.ts @@ -69,6 +69,22 @@ describe("isBusinessDay", () => { expect(isBusinessDay(new Date(2024, 6, 9, 12), { stateCode: "XX" })).toBe(true); }); + it("should return false for a stateCode that is present and is not a string, as isHoliday does, instead of ignoring it", () => { + // @ts-expect-error: intentionally invalid input + expect(isBusinessDay(new Date(2024, 6, 9, 12), { stateCode: 5 })).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isBusinessDay(new Date(2024, 6, 9, 12), { stateCode: null })).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isBusinessDay(new Date(2024, 6, 9, 12), { stateCode: {} })).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isBusinessDay(new Date(2024, 6, 9, 12), { stateCode: ["SP"] })).toBe(false); + }); + + it("should read an explicit undefined stateCode as no state at all, the only non-string value that is not rejected", () => { + expect(isBusinessDay(new Date(2024, 6, 9, 12), { stateCode: undefined })).toBe(true); + expect(isBusinessDay(new Date(2024, 0, 1, 12), { stateCode: undefined })).toBe(false); + }); + it("should treat a prototype chain key as an unknown stateCode instead of throwing", () => { for (const stateCode of PROTOTYPE_KEYS) { // @ts-expect-error: intentionally invalid input diff --git a/src/is-business-day/is-business-day.ts b/src/is-business-day/is-business-day.ts index c508182f..9c724124 100644 --- a/src/is-business-day/is-business-day.ts +++ b/src/is-business-day/is-business-day.ts @@ -30,10 +30,18 @@ const WEEKEND_DAYS = new Set([0, 6]); * they are not statutory holidays. Pass `false` to only treat statutory (`"national"` and * `"state"`) holidays as non-business days. * - * If `options.stateCode` is provided but is not a valid/known state code, it is ignored - * and only national holidays are considered (same behavior as `getHolidays`/`isHoliday`). - * The lookup is an own-property one, so a prototype-chain key such as `"__proto__"` or - * `"constructor"` is an unknown state code like any other. + * An invalid `options.stateCode` is treated in two different ways, depending on its type, the + * same split `isHoliday` makes: + * + * - a string that is not a known state code is ignored, and only national holidays are + * considered, the same behavior as `getHolidays`. The lookup is an own-property one, so a + * prototype-chain key such as `"__proto__"` or `"constructor"` is an unknown state code like + * any other; + * - a `stateCode` that is present and is not a string at all (a number, `null`, an object) is + * rejected rather than ignored: `isBusinessDay` returns `false` without looking at the date, + * even when that date is an ordinary Tuesday. `undefined`, or an absent property, is the only + * non-string value that stands for "no state" instead. `addBusinessDays`, `subBusinessDays` + * and `differenceInBusinessDays` reject the same value with `null`. * * Two state rules change what `includeOptional: false` answers. The Distrito Federal declares * Corpus Christi a feriado (Lei distrital nº 72/1989, art. 1º parágrafo único), so with @@ -49,8 +57,9 @@ const WEEKEND_DAYS = new Set([0, 6]); * @param {StateCode} [options.stateCode] - Brazilian state code whose state holidays are also considered. * @param {boolean} [options.includeOptional] - Whether optional holidays count as non-business days (default: `true`). * @returns {boolean} True when `value` is a business day, false otherwise. Bad input also - * returns false: a `value` that is not a valid `Date` (including non-`Date` values) or a - * `value` outside the supported 1900-2099 range. + * returns false: a `value` that is not a valid `Date` (including non-`Date` values), a + * `value` outside the supported 1900-2099 range, or a `stateCode` that is present and is not a + * string. * * @example * ```typescript @@ -64,6 +73,7 @@ const WEEKEND_DAYS = new Set([0, 6]); * isBusinessDay(new Date("not a date")); // false * isBusinessDay(new Date(2100, 0, 4)); // false (a Monday, but 2100 is outside the supported range) * ``` + * isBusinessDay(new Date(2024, 6, 9), { stateCode: 5 }); // false (a non-string stateCode is rejected) * * The underlying holidays are the ones `getHolidays` computes; see its JSDoc (and * `src/get-holidays/constants.ts` for state holidays) for the full set of laws behind them. @@ -93,6 +103,9 @@ export const isBusinessDay = (value: Date, options?: BusinessDayOptions): boolea if (WEEKEND_DAYS.has(value.getDay())) return false; const stateCode = options?.stateCode; + + if (stateCode !== undefined && typeof stateCode !== "string") return false; + const includeOptional = options?.includeOptional ?? true; const month = value.getMonth(); From edf457770d2e706bfe5d88ce0a37c91d6b8e425a Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:01 -0300 Subject: [PATCH 19/75] fix(types): re-export the option and state types from the calendar and area code subpaths - `@brazilian-utils/brazilian-utils/add-business-days`, `sub-business-days` and `difference-in-business-days` now export `BusinessDayOptions`; `get-holidays`, `is-holiday` and `is-business-day` export `StateCode`; `get-area-code-info` exports `State`, `StateCode` and `StateName`, so a consumer typing the signatures from the subpath no longer hits TS2459 - verified with a `--module nodenext --strict` consumer importing every type from each subpath --- src/add-business-days/add-business-days.ts | 2 ++ src/difference-in-business-days/difference-in-business-days.ts | 2 ++ src/get-area-code-info/get-area-code-info.ts | 2 ++ src/get-holidays/get-holidays.ts | 2 ++ src/is-business-day/is-business-day.ts | 2 ++ src/is-holiday/is-holiday.ts | 2 ++ src/sub-business-days/sub-business-days.ts | 2 ++ 7 files changed, 14 insertions(+) diff --git a/src/add-business-days/add-business-days.ts b/src/add-business-days/add-business-days.ts index bed87cad..4d5fe37c 100644 --- a/src/add-business-days/add-business-days.ts +++ b/src/add-business-days/add-business-days.ts @@ -1,6 +1,8 @@ import { isSupportedHolidayYear } from "../_internals/is-supported-holiday-year/is-supported-holiday-year"; import { type BusinessDayOptions, isBusinessDay } from "../is-business-day/is-business-day"; +export type { BusinessDayOptions } from "../is-business-day/is-business-day"; + /** * Adds a number of Brazilian business days (dias úteis) to a date. * diff --git a/src/difference-in-business-days/difference-in-business-days.ts b/src/difference-in-business-days/difference-in-business-days.ts index fd757449..cd7050d2 100644 --- a/src/difference-in-business-days/difference-in-business-days.ts +++ b/src/difference-in-business-days/difference-in-business-days.ts @@ -1,6 +1,8 @@ import { isSupportedHolidayYear } from "../_internals/is-supported-holiday-year/is-supported-holiday-year"; import { type BusinessDayOptions, isBusinessDay } from "../is-business-day/is-business-day"; +export type { BusinessDayOptions } from "../is-business-day/is-business-day"; + const toLocalDayTimestamp = (date: Date): number => Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()); diff --git a/src/get-area-code-info/get-area-code-info.ts b/src/get-area-code-info/get-area-code-info.ts index aa2440ee..d8db7dec 100644 --- a/src/get-area-code-info/get-area-code-info.ts +++ b/src/get-area-code-info/get-area-code-info.ts @@ -3,6 +3,8 @@ import { DATA, type State, type StateCode, type StateName } from "../_internals/ import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +export type { State, StateCode, StateName } from "../_internals/constants/states"; + /** The state, and the region it belongs to, that `getAreaCodeInfo` returns for a DDD. */ export type AreaCodeInfo = { /** The DDD (area code) as a number, e.g. `11`. */ diff --git a/src/get-holidays/get-holidays.ts b/src/get-holidays/get-holidays.ts index 8c1a6700..e6679f70 100644 --- a/src/get-holidays/get-holidays.ts +++ b/src/get-holidays/get-holidays.ts @@ -9,6 +9,8 @@ import { STATE_HOLIDAYS, } from "./constants"; +export type { StateCode } from "../_internals/constants/states"; + /** The class a holiday returned by `getHolidays` falls into. */ export type HolidayType = "national" | "state" | "optional" | "religious"; diff --git a/src/is-business-day/is-business-day.ts b/src/is-business-day/is-business-day.ts index 9c724124..50b1fcc8 100644 --- a/src/is-business-day/is-business-day.ts +++ b/src/is-business-day/is-business-day.ts @@ -2,6 +2,8 @@ import { type StateCode } from "../_internals/constants/states"; import { isSupportedHolidayYear } from "../_internals/is-supported-holiday-year/is-supported-holiday-year"; import { getHolidays } from "../get-holidays/get-holidays"; +export type { StateCode } from "../_internals/constants/states"; + /** * Options shared by every business day util (`isBusinessDay`, `addBusinessDays`, * `subBusinessDays` and `differenceInBusinessDays`): which holidays count as non-business days. diff --git a/src/is-holiday/is-holiday.ts b/src/is-holiday/is-holiday.ts index d59e51b0..6707826e 100644 --- a/src/is-holiday/is-holiday.ts +++ b/src/is-holiday/is-holiday.ts @@ -2,6 +2,8 @@ import { type StateCode } from "../_internals/constants/states"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { getHolidays } from "../get-holidays/get-holidays"; +export type { StateCode } from "../_internals/constants/states"; + /** The options `isHoliday` takes: the date to check and, optionally, the state whose holidays also count. */ export type IsHolidayOptions = { /** The date to check, read by its local calendar day. */ diff --git a/src/sub-business-days/sub-business-days.ts b/src/sub-business-days/sub-business-days.ts index 8e692058..02e0eec8 100644 --- a/src/sub-business-days/sub-business-days.ts +++ b/src/sub-business-days/sub-business-days.ts @@ -1,6 +1,8 @@ import { addBusinessDays } from "../add-business-days/add-business-days"; import { type BusinessDayOptions } from "../is-business-day/is-business-day"; +export type { BusinessDayOptions } from "../is-business-day/is-business-day"; + /** * Subtracts a number of Brazilian business days (dias úteis) from a date. * From 02bd64f4c63737629794f95615cab3ee2af71bcb Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:01 -0300 Subject: [PATCH 20/75] fix(cep): reject with the typed errors for a bad providers list, a missing UF and a BrasilAPI 404 - `getAddressInfoByCep(cep, { providers: null })` rejected with a raw `TypeError` from the array spread; it now rejects with `GetAddressInfoByCepValidationError` - BrasilAPI answers an unknown CEP with HTTP 404, which was reported as a service error; it is now `GetAddressInfoByCepNotFoundError`, so the not-found branch is reachable with `providers: ["brasilapi"]` alone - `getCepInfoByAddress` without `federalUnit`, or with a non-object argument, rejected with a raw `TypeError` although the JSDoc promised `GetCepInfoByAddressValidationError`; it now does - BrasilAPI and ViaCEP are third-party services and are cited as `Based on:`; the retry note says the providers are raced concurrently --- .../get-address-info-by-cep.test.ts | 40 ++++++++++++++++ .../get-address-info-by-cep.ts | 42 +++++++++++++--- .../get-cep-info-by-address.test.ts | 48 +++++++++++++++++++ .../get-cep-info-by-address.ts | 24 +++++++--- 4 files changed, 141 insertions(+), 13 deletions(-) diff --git a/src/get-address-info-by-cep/get-address-info-by-cep.test.ts b/src/get-address-info-by-cep/get-address-info-by-cep.test.ts index 1bf2a67a..1ad2b8c8 100644 --- a/src/get-address-info-by-cep/get-address-info-by-cep.test.ts +++ b/src/get-address-info-by-cep/get-address-info-by-cep.test.ts @@ -272,6 +272,8 @@ describe("getAddressInfoByCep", () => { const requestedUrls = fetchMock.mock.calls.map(([input]: [FetchInput]) => requestUrl(input), ); + expect(requestedUrls.some((url: string) => url.includes("viacep.com.br"))).toBe(true); + expect(requestedUrls.some((url: string) => url.includes("brasilapi.com.br"))).toBe(true); expect(requestedUrls.some((url: string) => url.includes("widenet"))).toBe(false); }); @@ -302,6 +304,24 @@ describe("getAddressInfoByCep", () => { ).rejects.toThrow("Nenhum provedor válido especificado"); }); + it("should throw GetAddressInfoByCepValidationError for a providers value that is not an array, null included", async () => { + await Promise.all( + [null, "viacep", 5, {}, true].map((providers) => + expect( + // @ts-expect-error: intentionally invalid input + getAddressInfoByCep(VALID_CEP, { providers }), + ).rejects.toThrow(GetAddressInfoByCepValidationError), + ), + ); + }); + + it("should include the Portuguese message for a providers value that is not an array", async () => { + await expect( + // @ts-expect-error: intentionally invalid input + getAddressInfoByCep(VALID_CEP, { providers: null }), + ).rejects.toThrow("Nenhum provedor válido especificado"); + }); + it("should use only specified providers", async () => { const result = await getAddressInfoByCep(VALID_CEP, { providers: ["brasilapi"], @@ -512,6 +532,26 @@ describe("getAddressInfoByCep", () => { ); }); + it("should throw GetAddressInfoByCepNotFoundError when BrasilAPI answers 404, the status it reports an unknown CEP with", async () => { + setupFetchMock(fetchMock, { + brasilapi: createJsonResponse({ errors: [{ message: "CEP não encontrado" }] }, 404), + }); + + await expect(getAddressInfoByCep(VALID_CEP, { providers: ["brasilapi"] })).rejects.toThrow( + GetAddressInfoByCepNotFoundError, + ); + }); + + it("should throw GetAddressInfoByCepServiceError when BrasilAPI answers a non-404 error status", async () => { + setupFetchMock(fetchMock, { + brasilapi: createJsonResponse({}, 500), + }); + + await expect(getAddressInfoByCep(VALID_CEP, { providers: ["brasilapi"] })).rejects.toThrow( + GetAddressInfoByCepServiceError, + ); + }); + it("should throw GetAddressInfoByCepNotFoundError when every provider returns a payload that is not an object", async () => { setupFetchMock(fetchMock, { brasilapi: createJsonResponse(null), diff --git a/src/get-address-info-by-cep/get-address-info-by-cep.ts b/src/get-address-info-by-cep/get-address-info-by-cep.ts index 21508aa6..f3b1f6b3 100644 --- a/src/get-address-info-by-cep/get-address-info-by-cep.ts +++ b/src/get-address-info-by-cep/get-address-info-by-cep.ts @@ -59,6 +59,16 @@ export type GetAddressInfoByCepOptions = { type ProviderPayload = Record; +/** + * The status BrasilAPI answers an unknown CEP with, alongside an `errors` body. ViaCEP and + * Widenet report a miss inside a 200 body instead, so BrasilAPI is the only provider whose + * not-found signal is an HTTP status and the only one that needs it mapped before `response.ok` + * turns it into a service failure. + * + * @see Based on: https://brasilapi.com.br/docs#tag/CEP + */ +const BRASIL_API_NOT_FOUND_STATUS = 404; + const asString = (value: unknown): string => (typeof value === "string" ? value : ""); const readPayload = async (response: Response): Promise => { @@ -126,6 +136,12 @@ const fetchWidenet = async (cep: string): Promise => { const fetchBrasilApi = async (cep: string): Promise => { const response = await fetchWithRetry(`https://brasilapi.com.br/api/cep/v1/${cep}`); + if (response.status === BRASIL_API_NOT_FOUND_STATUS) { + // Stryker disable next-line StringLiteral: only `instanceof GetAddressInfoByCepNotFoundError` + // is checked when aggregating provider failures below, so this message is never observable. + throw new GetAddressInfoByCepNotFoundError("CEP não encontrado"); + } + if (!response.ok) { // Stryker disable next-line StringLiteral: only `instanceof GetAddressInfoByCepNotFoundError` // is checked when aggregating provider failures below, so this message is never observable. @@ -160,19 +176,27 @@ const providerMap: Record Promise> = * Fetches address information for a given CEP using multiple providers simultaneously. * Returns the result from the first provider that responds successfully. * + * The providers are started together and raced with `Promise.any`, not tried one after the + * other, so a provider that is retrying delays nothing for the others: its retries only push + * back the moment its own failure lands, and therefore the moment an all-failed rejection can + * surface. + * * @param {string|number} cep - The CEP (Brazilian postal code) to search for. Can be a string or number. * @param {GetAddressInfoByCepOptions} options - Optional configuration for the function. * @param {CepProvider[]} options.providers - List of providers to use. Defaults to `["viacep", "brasilapi"]` * if not specified (the deprecated `"widenet"` provider is excluded from the default list, but can still * be requested explicitly). * @returns {Promise} A promise that resolves to the address information. - * @throws {GetAddressInfoByCepValidationError} If the CEP format is invalid. + * @throws {GetAddressInfoByCepValidationError} If the CEP format is invalid, or if + * `options.providers` is given and names no known provider: an empty array, an array of unknown + * names, and a value that is not an array at all (`null` included) all reject this way rather + * than with a raw `TypeError`. * @throws {GetAddressInfoByCepNotFoundError} If the CEP is not found in any of the services. * @throws {GetAddressInfoByCepServiceError} If all services are unavailable. * * @example * ```typescript - * // Using all providers (default) + * // Using the default providers (["viacep", "brasilapi"]) * const address = await getAddressInfoByCep("01310100"); * * // Using specific providers @@ -185,8 +209,10 @@ const providerMap: Record Promise> = * ``` * * @see Official: https://www.correios.com.br/enviar/precisa-de-ajuda/tudo-sobre-cep - * @see Official: https://viacep.com.br/ Default `"viacep"` provider. - * @see Official: https://brasilapi.com.br/docs#tag/CEP Default `"brasilapi"` provider. + * @see Based on: https://viacep.com.br/ + * ViaCEP, one of the two default providers. A third-party service, not a Correios one. + * @see Based on: https://brasilapi.com.br/docs#tag/CEP + * BrasilAPI, the other default provider. A third-party service, not a Correios one. */ export const getAddressInfoByCep = async ( cep: string | number, @@ -206,14 +232,16 @@ export const getAddressInfoByCep = async ( let providersToUse: CepProvider[]; if (options?.providers === undefined) { - providersToUse = ["viacep", "brasilapi"] as CepProvider[]; - } else { + providersToUse = ["viacep", "brasilapi"]; + } else if (Array.isArray(options.providers)) { // An empty `options.providers` array also filters down to an empty `providersToUse` below, // which already reports the same validation error, so there is no dedicated check for it here. - providersToUse = options.providers.filter((p) => Object.hasOwn(providerMap, p)); + providersToUse = options.providers.filter((provider) => Object.hasOwn(providerMap, provider)); if (providersToUse.length === 0) { throw new GetAddressInfoByCepValidationError("Nenhum provedor válido especificado"); } + } else { + throw new GetAddressInfoByCepValidationError("Nenhum provedor válido especificado"); } let notFound = false; diff --git a/src/get-cep-info-by-address/get-cep-info-by-address.test.ts b/src/get-cep-info-by-address/get-cep-info-by-address.test.ts index 5b3b5f6e..f4d485f0 100644 --- a/src/get-cep-info-by-address/get-cep-info-by-address.test.ts +++ b/src/get-cep-info-by-address/get-cep-info-by-address.test.ts @@ -86,6 +86,54 @@ describe("getCepInfoByAddress", () => { ).rejects.toThrow("Invalid UF: XX"); }); + it("should throw GetCepInfoByAddressValidationError when params is not an object, instead of a raw TypeError", async () => { + await Promise.all( + [undefined, null, "SP", 5, true].map((params) => + expect( + // @ts-expect-error: intentionally invalid input + getCepInfoByAddress(params), + ).rejects.toThrow(GetCepInfoByAddressValidationError), + ), + ); + + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("should include the message when params is not an object, reporting the argument rather than the UF", async () => { + await expect( + // @ts-expect-error: intentionally invalid input + getCepInfoByAddress(), + ).rejects.toThrow("UF, city and street are required"); + await expect( + // @ts-expect-error: intentionally invalid input + getCepInfoByAddress("SP"), + ).rejects.toThrow("UF, city and street are required"); + }); + + it("should throw GetCepInfoByAddressValidationError when federalUnit is missing or is not a string, instead of a raw TypeError", async () => { + await Promise.all( + [undefined, null, 35, {}, ["SP"]].map((federalUnit) => + expect( + // @ts-expect-error: intentionally invalid input + getCepInfoByAddress({ federalUnit, city: "São Paulo", street: "Avenida Paulista" }), + ).rejects.toThrow(GetCepInfoByAddressValidationError), + ), + ); + + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("should include the message when federalUnit is not a string", async () => { + await expect( + getCepInfoByAddress({ + // @ts-expect-error: intentionally invalid input + federalUnit: 35, + city: "São Paulo", + street: "Avenida Paulista", + }), + ).rejects.toThrow("Invalid UF: a two letter string is required"); + }); + it("should accept a federal unit with surrounding whitespace and lowercase letters", async () => { mockAddressListOnce([SAMPLE_ADDRESS]); diff --git a/src/get-cep-info-by-address/get-cep-info-by-address.ts b/src/get-cep-info-by-address/get-cep-info-by-address.ts index 0b1d2ea4..451b108b 100644 --- a/src/get-cep-info-by-address/get-cep-info-by-address.ts +++ b/src/get-cep-info-by-address/get-cep-info-by-address.ts @@ -1,5 +1,6 @@ import { DATA as STATES, type StateCode } from "../_internals/constants/states"; import { fetchWithRetry } from "../_internals/fetch-with-retry/fetch-with-retry"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; import { removeAccents } from "../remove-accents/remove-accents"; /** Base class of every error `getCepInfoByAddress` rejects with. */ @@ -78,6 +79,8 @@ const isCepAddressInfoArray = (value: unknown): value is CepAddressInfo[] => Arr * @param {string} params.street - The street name, or part of it. * @returns {Promise} Every address matching the query. * @throws {GetCepInfoByAddressValidationError} When the UF, city or street is missing or invalid. + * A `params` that is not an object at all (omitted, `null`, a string) and a `federalUnit` that is + * not a string reject this way too, rather than with a raw `TypeError`. * @throws {GetCepInfoByAddressNotFoundError} When no address matches the query. * @throws {GetCepInfoByAddressError} When ViaCEP answers with an HTTP error status. A request * that cannot be performed at all rejects with the underlying `fetch` error instead. @@ -89,13 +92,22 @@ const isCepAddressInfoArray = (value: unknown): value is CepAddressInfo[] => Arr * ``` * * @see Official: https://www.correios.com.br/enviar/precisa-de-ajuda/tudo-sobre-cep - * @see Official: https://viacep.com.br/ + * @see Based on: https://viacep.com.br/ + * ViaCEP, the service queried. A third-party service, not a Correios one. */ -export const getCepInfoByAddress = async ({ - federalUnit, - city, - street, -}: GetCepInfoByAddressOptions): Promise => { +export const getCepInfoByAddress = async ( + params: GetCepInfoByAddressOptions, +): Promise => { + if (isNullish(params) || typeof params !== "object") { + throw new GetCepInfoByAddressValidationError("UF, city and street are required"); + } + + const { federalUnit, city, street } = params; + + if (typeof federalUnit !== "string") { + throw new GetCepInfoByAddressValidationError("Invalid UF: a two letter string is required"); + } + const normalizedUf = federalUnit.trim().toUpperCase(); if (!isStateCode(normalizedUf)) { From aeef62cf1d3da070122c264c4f5412aa8c814b08 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:01 -0300 Subject: [PATCH 21/75] fix(email): cap the final domain label at 63 letters - the WHATWG label cap applied to every label but the last, which only required two or more letters; `user@example.` followed by 64 letters was accepted and is now rejected, 63 stays valid --- src/is-valid-email/is-valid-email.test.ts | 8 ++++++++ src/is-valid-email/is-valid-email.ts | 7 ++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/is-valid-email/is-valid-email.test.ts b/src/is-valid-email/is-valid-email.test.ts index fdff24e0..78af6f08 100644 --- a/src/is-valid-email/is-valid-email.test.ts +++ b/src/is-valid-email/is-valid-email.test.ts @@ -59,6 +59,10 @@ describe("isValidEmail", () => { test("when a domain label is longer than the 63 characters WHATWG allows", () => { expect(isValidEmail(`user@${"a".repeat(64)}.com`)).toBe(false); }); + + test("when the final domain label is longer than the 63 characters WHATWG allows", () => { + expect(isValidEmail(`user@example.${"a".repeat(64)}`)).toBe(false); + }); }); describe("should return true", () => { @@ -80,6 +84,10 @@ describe("isValidEmail", () => { expect(isValidEmail(`user@${"a".repeat(63)}.com`)).toBe(true); }); + test("when the final domain label is exactly 63 characters long", () => { + expect(isValidEmail(`user@example.${"a".repeat(63)}`)).toBe(true); + }); + test("when is a valid email with special characters", () => { expect(isValidEmail("user+tag@example.co.uk")).toBe(true); }); diff --git a/src/is-valid-email/is-valid-email.ts b/src/is-valid-email/is-valid-email.ts index adbfe275..63783372 100644 --- a/src/is-valid-email/is-valid-email.ts +++ b/src/is-valid-email/is-valid-email.ts @@ -1,5 +1,5 @@ const EMAIL_REGEX = - /^(?!\.)(?!.*\.\.)([a-z0-9_'+\-.]*)[a-z0-9_+-]@(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i; + /^(?!\.)(?!.*\.\.)([a-z0-9_'+\-.]*)[a-z0-9_+-]@(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i; /** * Validates if an email address is valid. @@ -16,9 +16,10 @@ const EMAIL_REGEX = * * The WHATWG HTML "valid e-mail address" definition is narrowed further: the local part is * limited to letters, digits and `_'+-.`, it may not start with a dot or contain two dots in a - * row, and the domain must carry at least one dot and end in an alphabetic label of two or more + * row, and the domain must carry at least one dot and end in an alphabetic label of 2 to 63 * letters. Each dotted label follows the WHATWG production `[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?`, - * so a label may neither start nor end with a hyphen nor exceed 63 characters. It is a practical + * so a label may neither start nor end with a hyphen nor exceed 63 characters, and the final + * label is capped at the same 63 characters. It is a practical * subset of that WHATWG definition, not of IETF RFC 5322: quoted local parts and address * literals are rejected. * From ee2f02b6e6a7508390c7a2a076f20f3d1249ad13 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:01 -0300 Subject: [PATCH 22/75] fix(boleto): keep the fator de vencimento inside the first cycle for an early reference date - a `referenceDate` before the scheme (factor 8841 with 01/01/2000) resolved the factor to a date before the 07/10/1997 base, which no boleto can denote; the cycle search is now clamped to the first cycle, so 8841 gives 21/12/2021 and 9999 gives 21/02/2025 for any earlier reference - the fixtures 7000 and 8841 derive from the same banco 001 slip with the factor and check digit recomputed --- src/get-boleto-info/constants.ts | 9 +++++++ src/get-boleto-info/get-boleto-info.test.ts | 30 ++++++++++++++++++++- src/get-boleto-info/get-boleto-info.ts | 10 +++++-- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/get-boleto-info/constants.ts b/src/get-boleto-info/constants.ts index d99ef14e..76aabacc 100644 --- a/src/get-boleto-info/constants.ts +++ b/src/get-boleto-info/constants.ts @@ -27,6 +27,15 @@ export const BASE_DATE_DAY = 7; export const CYCLE_LENGTH = 9000; +/** + * The earliest cycle the two candidate search may consider. The factor only started carrying + * 1000 on 03/07/2000, so the base date is the oldest day the field can denote: a negative cycle + * would place a factor before 07/10/1997, a date no fator de vencimento can express (and one the + * same function already refuses to read out of a literal factor below `MIN_FACTOR`). A + * `referenceDate` early enough to make the arithmetic yield a negative cycle is clamped here. + */ +export const FIRST_CYCLE = 0; + export const MIN_FACTOR = 1000; export const RANGE_BEFORE = 3000; diff --git a/src/get-boleto-info/get-boleto-info.test.ts b/src/get-boleto-info/get-boleto-info.test.ts index ad249122..2e46991c 100644 --- a/src/get-boleto-info/get-boleto-info.test.ts +++ b/src/get-boleto-info/get-boleto-info.test.ts @@ -11,8 +11,10 @@ const withFactor = { "1000": "00190000090114971860168524522114210000000102656", "1001": "00190000090114971860168524522114810010000102656", "5000": "00190000090114971860168524522114350000000102656", + "7000": "00190000090114971860168524522114970000000102656", "7586": "00190000090114971860168524522114675860000102656", "7654": "00190000090114971860168524522114576540000102656", + "8841": "00190000090114971860168524522114488410000102656", "8999": "00190000090114971860168524522114489990000102656", "9999": "00190000090114971860168524522114799990000102656", }; @@ -117,12 +119,38 @@ describe("getBoletoInfo", () => { ).toStrictEqual(new Date(2015, 10, 16)); }); - test("should accept a factor whose difference from the reference date is exactly RANGE_AFTER (5500 days), even though the other cycle candidate (3500 days before the reference, on the other side) is numerically closer", () => { + test("should prefer the candidate inside the control range over the closest one (factor 1000 with referenceDate 16/06/2011: the old cycle date 03/07/2000 is 4000 days back, past RANGE_BEFORE, while the new cycle date 22/02/2025 is 5000 days ahead, inside RANGE_AFTER)", () => { + expect( + getBoletoInfo(withFactor["1000"], { referenceDate: new Date(2011, 5, 16) })?.expirationDate, + ).toStrictEqual(new Date(2025, 1, 22)); + }); + + test("should accept a factor whose difference from the reference date is exactly RANGE_AFTER (5500 days)", () => { expect( getBoletoInfo(withFactor["1000"], { referenceDate: new Date(1985, 5, 12) })?.expirationDate, ).toStrictEqual(new Date(2000, 6, 3)); }); + test("should never resolve a factor to a date before the 07/10/1997 base date, even when the reference date predates the scheme: the cycle search is clamped to the first cycle, so each factor below gives the single date it is able to denote", () => { + const preSchemeReference = new Date(2000, 0, 1); + + expect( + getBoletoInfo(withFactor["7000"], { referenceDate: preSchemeReference })?.expirationDate, + ).toStrictEqual(new Date(2016, 11, 6)); + expect( + getBoletoInfo(withFactor["8841"], { referenceDate: preSchemeReference })?.expirationDate, + ).toStrictEqual(new Date(2021, 11, 21)); + expect( + getBoletoInfo(withFactor["9999"], { referenceDate: preSchemeReference })?.expirationDate, + ).toStrictEqual(new Date(2025, 1, 21)); + }); + + test("should keep the clamped answer stable while the reference date is still before the first cycle", () => { + expect( + getBoletoInfo(withFactor["8841"], { referenceDate: new Date(2003, 0, 1) })?.expirationDate, + ).toStrictEqual(new Date(2021, 11, 21)); + }); + test("should default the reference date to now", () => { const now = new Date(); diff --git a/src/get-boleto-info/get-boleto-info.ts b/src/get-boleto-info/get-boleto-info.ts index 803326a0..16d4024d 100644 --- a/src/get-boleto-info/get-boleto-info.ts +++ b/src/get-boleto-info/get-boleto-info.ts @@ -7,6 +7,7 @@ import { BASE_DATE_YEAR, CYCLE_LENGTH, DAY_IN_MS, + FIRST_CYCLE, MIN_FACTOR, RANGE_AFTER, RANGE_BEFORE, @@ -43,7 +44,10 @@ const getExpirationDate = (factor: number, referenceDate: Date): Date | null => if (!Number.isFinite(factor) || factor < MIN_FACTOR) return null; const reference = toDayNumber(referenceDate); - const cycle = Math.floor((reference - getBaseDayNumber() - factor) / CYCLE_LENGTH); + const cycle = Math.max( + FIRST_CYCLE, + Math.floor((reference - getBaseDayNumber() - factor) / CYCLE_LENGTH), + ); let closest = 0; let closestDistance = Number.POSITIVE_INFINITY; @@ -90,7 +94,9 @@ export type GetBoletoInfoOptions = { * vencimento from a new cycle one, so every factor resolves to either of two dates 9000 days * apart. `referenceDate` (now by default) picks between them through the library's own safety * windows, which means the same slip can resolve to the other candidate as time passes: pass - * `referenceDate` explicitly whenever the answer has to stay stable. + * `referenceDate` explicitly whenever the answer has to stay stable. The search never goes below + * the first cycle, so a `referenceDate` older than the scheme itself still resolves a factor to + * the oldest date that factor can denote rather than to one before the 07/10/1997 base date. * * @param {string} value - The boleto digitable line (can be with or without mask). * @param {GetBoletoInfoOptions} [options] - Optional options. From cfc5ed028a3d3a47cbd8374dbf9a863af7e7d249 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:01 -0300 Subject: [PATCH 23/75] fix(service-phone): drop 112 and 911 and add 141 per the Anatel Ato 43.151/2004 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - neither 112 nor 911 is designated by Anatel: art. 13 of Resolução 749/2022 keeps every 3-digit series outside `1N₂N₁` in reserva técnica, and neither code is in the Anexo of Ato 43.151/2004 nor in Ato 12.712/2024; the handset convention that maps them to 190/192 is not a numbering designation - 141 (Centro de Valorização da Vida) is in the Anexo of Ato 43.151/2004 and is now accepted - the 4-digit `300X`/`400X` codes were withdrawn by art. 2º, II of the same Ato and art. 43, I of Resolução 86/1998, which the note now says instead of "never published" --- src/_internals/constants/service-phone.ts | 26 ++++++++++++++----- .../is-valid-service-phone.test.ts | 7 ++++- .../is-valid-service-phone.ts | 18 +++++++++---- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/_internals/constants/service-phone.ts b/src/_internals/constants/service-phone.ts index bf2a7eeb..72ed5903 100644 --- a/src/_internals/constants/service-phone.ts +++ b/src/_internals/constants/service-phone.ts @@ -16,20 +16,33 @@ * library does not enforce, since it validates structure only. * - **Código de Acesso a Serviços de Utilidade Pública (SUP)**, art. 13-14: 3 digits, with the * whole `1N₂N₁` range destined to SUP and every other 3-digit series held in reserva técnica. - * Individual codes are designated one by one by Anatel Ato, so the codes below are the - * consolidated list Anatel publishes, not the full `100`-`199` range (see `isValidServicePhone` - * for the `112`/`911` mobile-alias note). + * Individual codes are designated one by one by Anatel Ato, the consolidated table being the + * Anexo of Ato nº 43.151/2004, so the codes below are the ones Anatel has designated rather + * than the full `100`-`199` range. `112` and `911` are *not* among them: `911` is not even + * inside the `1N₂N₁` address space art. 13 destines to SUP, and neither code appears in the + * Anexo of Ato nº 43.151/2004 or in Ato nº 12.712/2024. Their routing on Brazilian handsets is + * a GSM convention of the handset, not an Anatel designation, so both are rejected here. * - **The abbreviated `300X`/`400X` numbers** (`3003-1234`, `4004-1234`) are *not* a regulatory * category at all. They are ordinary 8-digit geographic STFC user numbers (art. 11 assigns * `2`-`6` as the first digit of a fixed-line number) whose 4-digit prefix a carrier licenses * in many DDDs at once and points at a single customer, marketed as "Número Único". Anatel - * neither names them nor publishes an allocated list, so the roots below are the conventional - * ones rather than an official allocation. + * withdrew the 4-digit special service codes instead of allocating them: Resolução nº 86/1998 + * art. 43 I, in the wording of Resolução nº 229/2000, ordered the prestadoras de STFC to + * release every 4-character code in use, and Ato nº 43.151/2004 art. 2º II repeated the order + * with a 180-day deadline. So the roots below are the conventional ones the market settled on, + * not an official allocation. * * Display formatting is convention too: no Anatel document specifies one. `0800 123 4567` (4-3-4) * is the grouping used on gov.br, and `4004-1234` the one carriers print. * * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749 + * Resolução Anatel nº 749/2022, the Regulamento de Numeração in force. + * @see Official: https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151 + * Ato Anatel nº 43.151/2004, whose Anexo is the consolidated SUP designation table. + * @see Official: https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2024/1953-ato-12712 + * Ato Anatel nº 12.712/2024, the CNG designation table (items 10.6 and 12.1). + * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86 + * Resolução Anatel nº 86/1998 (revoked), art. 43 I: the release of the 4-character codes. */ export const SERVICE_PHONE_NON_GEOGRAPHIC_PREFIXES = [ @@ -60,7 +73,6 @@ export const SERVICE_PHONE_UTILITY_CODES = [ "105", "106", "111", - "112", "115", "116", "117", @@ -78,6 +90,7 @@ export const SERVICE_PHONE_UTILITY_CODES = [ "135", "136", "138", + "141", "142", "145", "146", @@ -117,5 +130,4 @@ export const SERVICE_PHONE_UTILITY_CODES = [ "197", "198", "199", - "911", ] as const; diff --git a/src/is-valid-service-phone/is-valid-service-phone.test.ts b/src/is-valid-service-phone/is-valid-service-phone.test.ts index 70610ebf..100714bc 100644 --- a/src/is-valid-service-phone/is-valid-service-phone.test.ts +++ b/src/is-valid-service-phone/is-valid-service-phone.test.ts @@ -58,6 +58,11 @@ describe("isValidServicePhone", () => { expect(isValidServicePhone("200")).toBe(false); expect(isValidServicePhone("999")).toBe(false); }); + + test("for the handset emergency aliases 112 and 911, which Anatel designates in neither the Anexo of Ato nº 43.151/2004 nor Ato nº 12.712/2024 (and 911 falls outside the 1N₂N₁ range Resolução nº 749/2022 art. 13 destines to public utility services)", () => { + expect(isValidServicePhone("112")).toBe(false); + expect(isValidServicePhone("911")).toBe(false); + }); }); describe("should return true", () => { @@ -93,8 +98,8 @@ describe("isValidServicePhone", () => { test("for the public utility codes", () => { expect(isValidServicePhone("100")).toBe(true); expect(isValidServicePhone("102")).toBe(true); - expect(isValidServicePhone("112")).toBe(true); expect(isValidServicePhone("136")).toBe(true); + expect(isValidServicePhone("141")).toBe(true); expect(isValidServicePhone("156")).toBe(true); expect(isValidServicePhone("180")).toBe(true); expect(isValidServicePhone("181")).toBe(true); diff --git a/src/is-valid-service-phone/is-valid-service-phone.ts b/src/is-valid-service-phone/is-valid-service-phone.ts index 24bf5b4f..d6f61747 100644 --- a/src/is-valid-service-phone/is-valid-service-phone.ts +++ b/src/is-valid-service-phone/is-valid-service-phone.ts @@ -23,12 +23,17 @@ const UTILITY_CODES: readonly string[] = SERVICE_PHONE_UTILITY_CODES; * - the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900`, each followed by * 7 digits (11 in total, the shorter, extinct `0800` + 6 form is rejected); * - the abbreviated `300X` and `400X` numbers, followed by 4 digits, e.g. `3003-1234`. Anatel - * publishes no allocation for these, so the accepted roots are the conventional ones. Only - * `300X` and `400X` are recognised: other "Número Único" carrier prefixes in market use, such - * as `4020` and `4062`, are out of scope and are rejected; + * withdrew the 4-digit codes rather than allocating them (Resolução nº 86/1998 art. 43 I and + * Ato nº 43.151/2004 art. 2º II both ordered them released), so the accepted roots are the + * conventional ones the market settled on. Only `300X` and `400X` are recognised: other + * "Número Único" carrier prefixes in market use, such as `4020` and `4062`, are out of scope + * and are rejected; * - the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated, - * e.g. `190` and `192`. Undesignated codes in the `1XX` range are rejected. `112` and `911` - * are accepted too: Anatel lists them alongside the `1XX` codes as mobile-only aliases of `190`. + * e.g. `190` and `192`, the consolidated table being the Anexo of Ato nº 43.151/2004. + * Undesignated codes in the `1XX` range are rejected, and so are `112` and `911`: Anatel + * designates neither, and `911` is not even inside the `1N₂N₁` range Resolução nº 749/2022 + * art. 13 destines to public utility services. Handsets route both by GSM convention, which + * is not a numbering designation. * * Only the structure is checked: the number does not have to be assigned to anyone, and the * `0500` rule that encodes a donation amount in the last two digits is not enforced. @@ -45,6 +50,9 @@ const UTILITY_CODES: readonly string[] = SERVICE_PHONE_UTILITY_CODES; * ``` * * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749 + * Resolução Anatel nº 749/2022, arts. 13, 14, 18 and 28. + * @see Official: https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151 + * Ato Anatel nº 43.151/2004, whose Anexo designates the 3-digit public utility codes. */ export const isValidServicePhone = (value: string): boolean => { if (typeof value !== "string") return false; From 7d23f3a5314515641b96a9358e169489070e6ace Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:01 -0300 Subject: [PATCH 24/75] fix(nfe-key): return an empty string from formatNfeKey for a value that is not a key - `formatNfeKey(Object.create(null))` threw from the sanitizer and `formatNfeKey(-1)` returned `"1"`; the function now applies the same `isLookupCode` gate as the other formatters and returns `""` for a negative number, a fraction, an array, a `Date` or a null-prototype object --- src/format-nfe-key/format-nfe-key.test.ts | 14 ++++++++++++++ src/format-nfe-key/format-nfe-key.ts | 8 ++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/format-nfe-key/format-nfe-key.test.ts b/src/format-nfe-key/format-nfe-key.test.ts index 68ae4622..75ad072e 100644 --- a/src/format-nfe-key/format-nfe-key.test.ts +++ b/src/format-nfe-key/format-nfe-key.test.ts @@ -1,5 +1,7 @@ import * as fc from "fast-check"; +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { formatNfeKey } from "./format-nfe-key"; @@ -44,6 +46,14 @@ describe("formatNfeKey", () => { expect(formatNfeKey([])).toBe(""); // @ts-expect-error: intentionally invalid input expect(formatNfeKey(true)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatNfeKey(-11)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatNfeKey(1.1)).toBe(""); + }); + + test("should return an empty string for an object with a null prototype, which has no toString", () => { + expect(formatNfeKey(Object.create(null))).toBe(""); }); describe("properties", () => { @@ -80,6 +90,10 @@ describe("formatNfeKey", () => { ), ); }); + + test("should never throw for any garbage input", () => { + expectNeverThrows(formatNfeKey, anyGarbage); + }); }); }); diff --git a/src/format-nfe-key/format-nfe-key.ts b/src/format-nfe-key/format-nfe-key.ts index bc25ee87..dba794d9 100644 --- a/src/format-nfe-key/format-nfe-key.ts +++ b/src/format-nfe-key/format-nfe-key.ts @@ -1,5 +1,5 @@ import { format } from "../_internals/format/format"; -import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { PATTERN } from "./constants"; @@ -9,6 +9,10 @@ import { PATTERN } from "./constants"; * NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the * MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. * + * Anything that is not a string is only read when it is a non-negative safe integer, so a value + * with no usable digit representation (a negative or fractional number, an object, a value with + * a null prototype) gives `""` instead of throwing. + * * @param {string} value - The access key value to be formatted. * @returns {string} The formatted access key, e.g. "3520 0612 3456 ...". * @@ -22,4 +26,4 @@ import { PATTERN } from "./constants"; * Manual de Orientação do Contribuinte (MOC) NF-e, "chave de acesso". */ export const formatNfeKey = (value: string): string => - isNullish(value) ? "" : format({ value: sanitizeToDigits(value), pattern: PATTERN }); + isLookupCode(value) ? format({ value: sanitizeToDigits(value), pattern: PATTERN }) : ""; From 40bc6a8fd3dcfff5427406397d693cc0007d93c1 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:01 -0300 Subject: [PATCH 25/75] fix(cst): accept a separator only after the origin digit - Tabela A of Ajuste SINIEF 20/2012 is one origin digit and Tabela B two digits, so the only boundary in an ICMS CST is after the first digit; `"00-"`, `"0-0"`, `"11-0"`, `"0.0"` and `"4-9"` were accepted and are now rejected, while `"0 10"`, `"0.10"`, `"0/10"` and `"1-10"` stay valid --- src/is-valid-cst/constants.ts | 15 +++++++++------ src/is-valid-cst/is-valid-cst.test.ts | 19 +++++++++++++++++-- src/is-valid-cst/is-valid-cst.ts | 14 +++++++++----- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/is-valid-cst/constants.ts b/src/is-valid-cst/constants.ts index 84ea07aa..6b091391 100644 --- a/src/is-valid-cst/constants.ts +++ b/src/is-valid-cst/constants.ts @@ -9,8 +9,9 @@ * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23 * Ajuste SINIEF 39/23, which gave Tabela B its current wording with effect from 01.12.23. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24 - * Ajuste SINIEF 20/24, which revoked items 12, 13, 52, 72 and 74 of Tabela B with effect from - * 09.07.24. + * Ajuste SINIEF 20/24, which struck items 12, 13, 52, 72 and 74 from Tabela B (effects from + * 09.07.24) before they ever took effect: Ajuste SINIEF 39/23 had added them "sem efeitos", + * so those codes were never in force. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/1994/aj_003_94 * Ajuste SINIEF 03/1994, which instituted the ICMS CST as the two digit code AB. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2000/AJ_006_00 @@ -94,8 +95,10 @@ export const PIS_COFINS_CST_CODES = [ ] as const; /** - * Shape a CST code has to be written in: the 2 digits of the IPI, PIS and COFINS tables or - * the 3 digits of the ICMS form (origin digit + Tabela B code), optionally split by a - * single whitespace or mask character, the way documents print the origin apart ("0 10"). + * Shape a CST code has to be written in: the 2 digits of the IPI, PIS and COFINS tables, or + * the 3 digits of the ICMS form (origin digit + Tabela B code) with an optional single + * whitespace or mask character after the origin digit, the way documents print the origin + * apart ("0 10"). That is the only boundary a printed CST has: the Tabela B code is a single + * two digit code, so a separator inside it, or a trailing one, is not a CST. */ -export const CST_FORMAT_REGEX = /^\d[\s.\-/]?\d[\s.\-/]?\d?$/; +export const CST_FORMAT_REGEX = /^(?:\d{2}|\d[\s.\-/]?\d{2})$/; diff --git a/src/is-valid-cst/is-valid-cst.test.ts b/src/is-valid-cst/is-valid-cst.test.ts index 7a861ffd..6ce2e24e 100644 --- a/src/is-valid-cst/is-valid-cst.test.ts +++ b/src/is-valid-cst/is-valid-cst.test.ts @@ -33,7 +33,7 @@ describe("isValidCst", () => { expect(isValidCst("061", { tax: "icms" })).toBe(true); }); - it("should return false for the codes Ajuste SINIEF 20/24 revoked (12, 13, 52, 72 and 74)", () => { + it('should return false for the codes Ajuste SINIEF 39/23 added "sem efeitos" and Ajuste SINIEF 20/24 struck before they took effect (12, 13, 52, 72 and 74)', () => { expect(isValidCst("012", { tax: "icms" })).toBe(false); expect(isValidCst("013", { tax: "icms" })).toBe(false); expect(isValidCst("052", { tax: "icms" })).toBe(false); @@ -144,15 +144,30 @@ describe("isValidCst", () => { expect(isValidCst(undefined, { tax: "icms" })).toBe(false); }); - it("should accept a single separator between the digits and surrounding whitespace", () => { + it("should accept a single separator after the origin digit and surrounding whitespace", () => { expect(isValidCst(" 1-10 ", { tax: "icms" })).toBe(true); expect(isValidCst("0 10", { tax: "icms" })).toBe(true); + expect(isValidCst("0.10", { tax: "icms" })).toBe(true); + expect(isValidCst("0/10", { tax: "icms" })).toBe(true); }); it("should return false when more than one separator sits between two digits", () => { expect(isValidCst("1--10", { tax: "icms" })).toBe(false); }); + it("should return false when a separator does not sit right after the origin digit", () => { + expect(isValidCst("00-", { tax: "icms" })).toBe(false); + expect(isValidCst("0-0", { tax: "icms" })).toBe(false); + expect(isValidCst("11-0", { tax: "icms" })).toBe(false); + expect(isValidCst("0.0", { tax: "icms" })).toBe(false); + expect(isValidCst("4-9", { tax: "ipi" })).toBe(false); + expect(isValidCst("00-")).toBe(false); + expect(isValidCst("0-0")).toBe(false); + expect(isValidCst("11-0")).toBe(false); + expect(isValidCst("0.0")).toBe(false); + expect(isValidCst("4-9")).toBe(false); + }); + it("should return false for a string that is not a documented form", () => { expect(isValidCst("abc110", { tax: "icms" })).toBe(false); }); diff --git a/src/is-valid-cst/is-valid-cst.ts b/src/is-valid-cst/is-valid-cst.ts index 411c1c22..e947db9c 100644 --- a/src/is-valid-cst/is-valid-cst.ts +++ b/src/is-valid-cst/is-valid-cst.ts @@ -44,9 +44,12 @@ const isValidForTax = (digits: string, tax: "icms" | "ipi" | "pis" | "cofins"): * `options.tax` is optional. When it is omitted, the code is valid as long as it exists in any * one of the four tables above; when it is given, only that table is consulted. * - * A string is only read as a code when it is written in one of the documented forms: the 2 or - * 3 digits, with a single separator between them and optional surrounding whitespace. - * Anything else (`"abc110"`) is rejected instead of having its digits picked out. A number is + * A string is only read as a code when it is written in one of the documented forms: the 2 + * digits of a Tabela B code, or the 3 digits of the ICMS form with an optional single + * separator after the origin digit, plus optional surrounding whitespace. The origin digit is + * the only boundary a printed CST has, so `"0 10"` and `"1-10"` are read while `"0-0"`, + * `"11-0"` and `"00-"` are not. Anything else (`"abc110"`) is rejected instead of having its + * digits picked out. A number is * only read as a code when it is a non-negative safe integer, since a sign, a decimal point or * a rounded magnitude would otherwise be read as a code the caller never wrote. * @@ -61,8 +64,9 @@ const isValidForTax = (digits: string, tax: "icms" | "ipi" | "pis" | "cofins"): * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23 * Ajuste SINIEF 39/23, which gave Tabela B its current wording with effect from 01.12.23. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24 - * Ajuste SINIEF 20/24, which revoked items 12, 13, 52, 72 and 74 of Tabela B with effect from - * 09.07.24. + * Ajuste SINIEF 20/24, which struck items 12, 13, 52, 72 and 74 from Tabela B (effects from + * 09.07.24) before they ever took effect: Ajuste SINIEF 39/23 had added them "sem efeitos", + * so those codes were never in force. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/1994/aj_003_94 * Ajuste SINIEF 03/1994, which instituted the ICMS CST as the two digit code AB. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2000/AJ_006_00 From c16f096241d0aa7e6c65899d8c9af88adfc3d0bc Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:02 -0300 Subject: [PATCH 26/75] fix(pix): reject a dynamic payload that carries a Pix Saque facilitator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic template to `00` GUI and `25` URL only; a CRC-valid payload combining a URL with the `fss` field of the static Pix Saque template (§2.6) was parsed with a `withdrawalFacilitator` and is now rejected - the Unreserved Templates (IDs 80 to 99) note says they are ignored, as the tests assert, and the DICT reference points at Bacen's API-DICT page instead of the archived GitHub repository --- .../is-valid-pix-payload.ts | 15 +++++++++++---- .../parse-pix-payload.test.ts | 8 ++++++++ src/parse-pix-payload/parse-pix-payload.ts | 18 ++++++++++++++---- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/is-valid-pix-payload/is-valid-pix-payload.ts b/src/is-valid-pix-payload/is-valid-pix-payload.ts index f3203542..92f47c6e 100644 --- a/src/is-valid-pix-payload/is-valid-pix-payload.ts +++ b/src/is-valid-pix-payload/is-valid-pix-payload.ts @@ -16,14 +16,20 @@ import { parsePixPayload } from "../parse-pix-payload/parse-pix-payload"; * greater than zero, unless it is a Pix Saque BR Code, i.e. unless it carries the ISPB of the * "facilitador de serviço de saque" in sub-object 26-03 (`fss`) as §2.6 of the Pix manual * prescribes; rejecting `"0"`/`"0.00"` without `fss` is a deliberate restriction of this - * library, not a rule of the manual. + * library, not a rule of the manual. A `fss` written next to a PSP location makes the payload + * invalid: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to + * exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template + * of §2.6. * * The key itself is not checked against the DICT formats: the manual states a static QR Code * can be generated with a key that is not (or is no longer) registered, so use `isValidPixKey` * when that matters. * - * Payloads that carry the location in an Unreserved Template (IDs 80 to 99), as the "QR Code - * composto" of Pix Automático (Pix recorrente) does, are out of scope and reported as invalid. + * Unreserved Templates (IDs 80 to 99) are ignored. The "QR Code composto" of Pix Automático + * (Pix recorrente) writes its recurrence location in one of them: when such a payload also + * carries a payment location in 26-25, as the composite example of the Pix manual does, it is + * accepted here and read as an ordinary dynamic payload, its recurrence location dropped. Only + * a payload with no Pix template at all in IDs 26 to 51 is reported as invalid. * * @param {string} value - The BR Code payload to validate. * @returns {boolean} True if the payload is a valid Pix BR Code, false otherwise. @@ -41,6 +47,7 @@ import { parsePixPayload } from "../parse-pix-payload/parse-pix-payload"; * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/spb_docs/ManualBRCode.pdf * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf * @see Official: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. - * @see Official: https://github.com/bacen/pix-dict-api DICT OpenAPI spec. + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html + * DICT (Diretório de Identificadores de Contas Transacionais) API specification. */ export const isValidPixPayload = (value: string): boolean => parsePixPayload(value) !== null; diff --git a/src/parse-pix-payload/parse-pix-payload.test.ts b/src/parse-pix-payload/parse-pix-payload.test.ts index 73c4c1b7..57d3913c 100644 --- a/src/parse-pix-payload/parse-pix-payload.test.ts +++ b/src/parse-pix-payload/parse-pix-payload.test.ts @@ -249,6 +249,14 @@ describe("parsePixPayload", () => { expect(parsePixPayload(buildWithdrawalPayload("1234567x", "0.00"))).toBeNull(); }); + test("when the fss of a Pix Saque is written next to a PSP location", () => { + const payload = + "00020126600014br.gov.bcb.pix2526pix.example.com/qr/v2/12340308123456785204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***6304DA55"; + + expect(hasValidCrc(payload)).toBe(true); + expect(parsePixPayload(payload)).toBeNull(); + }); + test("when the additional data template is malformed", () => { const merchantAccountInformation = tlv("00", "br.gov.bcb.pix") + tlv("01", "some-key"); diff --git a/src/parse-pix-payload/parse-pix-payload.ts b/src/parse-pix-payload/parse-pix-payload.ts index 477c0d5a..a5959f58 100644 --- a/src/parse-pix-payload/parse-pix-payload.ts +++ b/src/parse-pix-payload/parse-pix-payload.ts @@ -146,6 +146,7 @@ const resolveMerchantKeyInfo = (fields: TlvFields): MerchantKeyInfo | null => { if ((key === undefined) === (url === undefined)) return null; if (key !== undefined && !key) return null; if (url !== undefined && !isValidPixUrl(url)) return null; + if (withdrawalFacilitator !== undefined && url !== undefined) return null; if ( withdrawalFacilitator !== undefined && !WITHDRAWAL_FACILITATOR_REGEX.test(withdrawalFacilitator) @@ -216,8 +217,12 @@ const buildPixPayload = ( * overrun them, so they are not enforced here, and neither is the 77 character limit of the * Pix key field (26-01). * - * Payloads that carry the location in an Unreserved Template (IDs 80 to 99), as the "QR Code - * composto" of Pix Automático (Pix recorrente) does, are out of scope and rejected. + * Unreserved Templates (IDs 80 to 99) are ignored. The "QR Code composto" of Pix Automático + * (Pix recorrente) writes its recurrence location in one of them: when such a payload also + * carries a payment location in 26-25, as the composite example of the Pix manual does, it is + * parsed here as an ordinary dynamic payload and its recurrence location is dropped, so a + * consumer that has to tell the two apart cannot rely on this parser. Only a payload with no + * Pix template at all in IDs 26 to 51 returns `null`. * * The merchant account information must carry exactly one of a Pix key (26-01) or a PSP * location (26-25); the location is checked with the same host and path rule @@ -237,7 +242,11 @@ const buildPixPayload = ( * válido […] indica que esse é um QR Code para Pix Saque", whose amount is settled at payment * time. So `54` set to `"0"` or `"0.00"` is accepted together with `fss` and rejected without * it; that rejection is a deliberate restriction of this library, not a rule of the manual, - * whose field table allows `"0"` in any payload. A `fss` that is not 8 digits is rejected. + * whose field table allows `"0"` in any payload. A `fss` that is not 8 digits is rejected, and + * so is a `fss` written next to a PSP location: §2.7 of the Manual de Padrões para Iniciação do + * Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), while + * `fss` belongs to the static template of §2.6, whose §2.6.1 states that "não há funcionalidade + * de Pix Troco para QR Codes estáticos, apenas para QR Codes dinâmicos". * * @param {string} value - The BR Code payload to be parsed. * @returns {PixPayload|null} The Pix data of the payload, or `null` when it is not a valid Pix @@ -260,7 +269,8 @@ const buildPixPayload = ( * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/spb_docs/ManualBRCode.pdf * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf * @see Official: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. - * @see Official: https://github.com/bacen/pix-dict-api DICT OpenAPI spec. + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html + * DICT (Diretório de Identificadores de Contas Transacionais) API specification. */ export const parsePixPayload = (value: string): PixPayload | null => { if (typeof value !== "string") return null; From f7eb3bba910aeca253283c26429b4b4851dbfb87 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:02 -0300 Subject: [PATCH 27/75] fix(caepf): reject a repeated base like CEI and CNO do - `00000000000012` and `11111111111192` carry the check digits their repeated 12-digit base produces and were accepted; the CEI and CNO validators already reject a repeated base, and CAEPF now does the same --- src/is-valid-caepf/constants.ts | 5 +++++ src/is-valid-caepf/is-valid-caepf.test.ts | 11 +++++++++++ src/is-valid-caepf/is-valid-caepf.ts | 24 +++++++++++++++++------ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/is-valid-caepf/constants.ts b/src/is-valid-caepf/constants.ts index d761533d..83dd992c 100644 --- a/src/is-valid-caepf/constants.ts +++ b/src/is-valid-caepf/constants.ts @@ -3,6 +3,11 @@ * "000.000.000/000-00", the first 9 being the CPF base of the holder, the next 3 the sequence * of the holder's registrations and the last 2 the check digits. * + * The weights below are the CNPJ's modulus 11 in the formulation of the cited reference: read + * from the right they cycle from 9 down to 2, and the check digit is the remainder itself, with + * a remainder of 10 read as 0 — the same digit the CNPJ's 2-to-9 weights with `11 - remainder` + * produce. + * * The Receita Federal does not publish the check digit rule of the CAEPF, the shift of 12 * included, so the calculation follows the reference implementations cited below. * diff --git a/src/is-valid-caepf/is-valid-caepf.test.ts b/src/is-valid-caepf/is-valid-caepf.test.ts index 887340c2..0335de0e 100644 --- a/src/is-valid-caepf/is-valid-caepf.test.ts +++ b/src/is-valid-caepf/is-valid-caepf.test.ts @@ -38,6 +38,11 @@ describe("isValidCaepf", () => { expect(isValidCaepf("abc.118.610/001-84")).toBe(false); }); + test("when a valid registration is followed or preceded by a letter", () => { + expect(isValidCaepf("293.118.610/001-84a")).toBe(false); + expect(isValidCaepf("a293.118.610/001-84")).toBe(false); + }); + test("when it has 14 digits but an unsupported separator", () => { expect(isValidCaepf("293#118#610#001#84")).toBe(false); }); @@ -47,6 +52,12 @@ describe("isValidCaepf", () => { expect(isValidCaepf("11111111111111")).toBe(false); }); + test("when the 12 digit base is a repeated digit, as isValidCei and isValidCno reject it", () => { + expect(isValidCaepf("00000000000012")).toBe(false); + expect(isValidCaepf("000.000.000/000-12")).toBe(false); + expect(isValidCaepf("11111111111192")).toBe(false); + }); + test("when the check digits do not match (29311861000185, Casilhero/brazilian-validators CaepfTest)", () => { expect(isValidCaepf("29311861000185")).toBe(false); }); diff --git a/src/is-valid-caepf/is-valid-caepf.ts b/src/is-valid-caepf/is-valid-caepf.ts index f3679710..6fdf3102 100644 --- a/src/is-valid-caepf/is-valid-caepf.ts +++ b/src/is-valid-caepf/is-valid-caepf.ts @@ -1,4 +1,5 @@ import { generateChecksum } from "../_internals/generate-checksum/generate-checksum"; +import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { CAEPF_BASE_LENGTH, @@ -17,12 +18,19 @@ const getCheckDigit = (base: string, weights: number[]): number => * The CAEPF replaced the CEI for individuals who hire employees, such as rural producers and * notary officials. It has 14 digits printed as "000.000.000/000-00": the 9 digit CPF base of * the holder, a 3 digit sequence for the holder's several registrations and 2 check digits. - * Both check digits use the modulus 11 of the CNPJ, weights cycling from 2 to 9 from the right, - * with a remainder of 10 read as 0. The pair is then shifted by 12, wrapping around 100, so a - * CAEPF whose plain modulus 11 digits would be 72 is printed with 84. + * Both check digits are the CNPJ's modulus 11 in the formulation of the cited reference: the + * weights cycle from 9 down to 2 from the right and the check digit is the remainder itself, + * with a remainder of 10 read as 0 — the same digit the CNPJ's 2-to-9 weights with + * `11 - remainder` produce. The pair is then shifted by 12, wrapping around 100, so a CAEPF + * whose plain modulus 11 digits would be 72 is printed with 84. * - * The Receita Federal does not publish the check digit rule of the CAEPF, the shift of 12 - * included, so the calculation follows the reference implementations cited below. + * A base whose 12 digits are all the same is rejected before the check digits are computed, the + * way `isValidCei` and `isValidCno` reject a repeated CEI/CNO number, so the otherwise + * well-formed `"00000000000012"` is invalid. + * + * The Receita Federal does not publish the check digit rule of the CAEPF, the shift of 12 and + * the repeated-base rejection included, so the calculation follows the reference implementations + * cited below. * * @param {string|number} value - The CAEPF value to be validated. * @returns {boolean} True if the CAEPF is valid, false otherwise. @@ -33,7 +41,8 @@ const getCheckDigit = (base: string, weights: number[]): number => * isValidCaepf("41142260000101"); // true * isValidCaepf(29311861000184); // true * isValidCaepf("29311861000185"); // false (invalid check digits) - * isValidCaepf("00000000000000"); // false (repeated digits) + * isValidCaepf("00000000000000"); // false (invalid check digits) + * isValidCaepf("00000000000012"); // false (repeated base digits) * ``` * * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/caepf @@ -54,6 +63,9 @@ export const isValidCaepf = (value: string | number): boolean => { if (!CAEPF_FORMAT_REGEX.test(String(value).trim())) return false; const base = digits.slice(0, CAEPF_BASE_LENGTH); + + if (isRepeatedDigits(base)) return false; + const first = getCheckDigit(base, CAEPF_FIRST_WEIGHTS); const second = getCheckDigit(`${base}${first}`, CAEPF_SECOND_WEIGHTS); const expected = (first * 10 + second + CAEPF_CHECK_DIGITS_OFFSET) % 100; From fac0769735b385fef628dae8d84752763716d626 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:02 -0300 Subject: [PATCH 28/75] fix(cns,cei): accept a run of separators like the other document formats - the CNS and CEI format patterns allowed at most one separator between groups while CPF, CNPJ and CAEPF accept any run; both now accept `[\s.\-/]*`, a widening only, and the ANVISA worked example `898 0000 0004 3208` is pinned for `isValidCns` and `formatCns` --- src/_internals/constants/cei.ts | 8 +++++++- src/_internals/constants/cns.ts | 6 ++++-- .../is-valid-cei-cno-number/is-valid-cei-cno-number.ts | 5 +++++ src/is-valid-cei/is-valid-cei.test.ts | 5 +++++ src/is-valid-cei/is-valid-cei.ts | 5 +++++ src/is-valid-cno/is-valid-cno.ts | 5 +++++ src/is-valid-cns/is-valid-cns.test.ts | 5 +++++ src/is-valid-cns/is-valid-cns.ts | 5 +++-- 8 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/_internals/constants/cei.ts b/src/_internals/constants/cei.ts index d11fbce1..d89d6253 100644 --- a/src/_internals/constants/cei.ts +++ b/src/_internals/constants/cei.ts @@ -23,6 +23,12 @@ export const CEI_BASE_LENGTH = 11; export const CEI_WEIGHTS = [7, 4, 1, 8, 5, 2, 1, 6, 3, 7, 4]; -export const CEI_FORMAT_REGEX = /^\d{2}[\s.\-/]?\d{3}[\s.\-/]?\d{5}[\s.\-/]?\d{2}$/; +/** + * Shape a CEI/CNO number has to be written in: the 12 digits, optionally split into the printed + * groups of 2, 3, 5 and 2 by whitespace or the usual mask characters. A run of separators is + * tolerated between two groups, not just a single one, which is what the CPF, CNPJ, CAEPF and + * certidão regexes of this library do. + */ +export const CEI_FORMAT_REGEX = /^\d{2}[\s.\-/]*\d{3}[\s.\-/]*\d{5}[\s.\-/]*\d{2}$/; export const CEI_PATTERN = "00.000.00000/00"; diff --git a/src/_internals/constants/cns.ts b/src/_internals/constants/cns.ts index 6d9b9e79..8756653e 100644 --- a/src/_internals/constants/cns.ts +++ b/src/_internals/constants/cns.ts @@ -13,9 +13,11 @@ /** * Shape a CNS number has to be written in: the 15 digits, optionally split into the printed - * groups of 3, 4, 4 and 4 by whitespace or the usual mask characters. + * groups of 3, 4, 4 and 4 by whitespace or the usual mask characters. A run of separators is + * tolerated between two groups, not just a single one, which is what the CPF, CNPJ, CAEPF and + * certidão regexes of this library do. */ -export const CNS_FORMAT_REGEX = /^\d{3}[\s.\-/]?\d{4}[\s.\-/]?\d{4}[\s.\-/]?\d{4}$/; +export const CNS_FORMAT_REGEX = /^\d{3}[\s.\-/]*\d{4}[\s.\-/]*\d{4}[\s.\-/]*\d{4}$/; /** Digits of the PIS/PASEP/NIS derived base embedded in a definitive CNS (starts with 1 or 2). */ export const CNS_DEFINITIVE_BASE_LENGTH = 11; diff --git a/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.ts b/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.ts index 54071830..f5d74564 100644 --- a/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.ts +++ b/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.ts @@ -12,6 +12,11 @@ import { sanitizeToDigits } from "../sanitize-to-digits/sanitize-to-digits"; * that sum to its units part and takes the complement of the units digit of the result to 10, * mapping 10 back to 0. * + * The value has to be written as the 12 digits, optionally split into the printed groups of 2, + * 3, 5 and 2 by whitespace or the usual mask characters, a run of them between two groups + * included; anything else, a letter among the digits included, is rejected instead of being + * read past. + * * The Receita Federal does not publish the check digit rule of the CEI/CNO numbering, so the * calculation follows the reference implementations cited below, cross-checked against the CNO * open data of the Receita Federal. diff --git a/src/is-valid-cei/is-valid-cei.test.ts b/src/is-valid-cei/is-valid-cei.test.ts index 2850055a..4d8d7bd3 100644 --- a/src/is-valid-cei/is-valid-cei.test.ts +++ b/src/is-valid-cei/is-valid-cei.test.ts @@ -46,6 +46,11 @@ describe("isValidCei", () => { expect(isValidCei("aa.583.00249/85")).toBe(false); }); + test("when a valid registration is followed or preceded by a letter", () => { + expect(isValidCei("11.583.00249/85a")).toBe(false); + expect(isValidCei("a11.583.00249/85")).toBe(false); + }); + test("when every digit is the same", () => { expect(isValidCei("000000000000")).toBe(false); expect(isValidCei("111111111111")).toBe(false); diff --git a/src/is-valid-cei/is-valid-cei.ts b/src/is-valid-cei/is-valid-cei.ts index f40e8e72..9d2130da 100644 --- a/src/is-valid-cei/is-valid-cei.ts +++ b/src/is-valid-cei/is-valid-cei.ts @@ -10,6 +10,11 @@ import { isValidCeiCnoNumber } from "../_internals/is-valid-cei-cno-number/is-va * mapping 10 back to 0. The CEI was replaced by the CNO for construction works and by the CAEPF * for individuals, but numbers already issued keep their meaning and their check digit. * + * The value has to be written as the 12 digits, optionally split into the printed groups of 2, + * 3, 5 and 2 by whitespace or the usual mask characters, a run of them between two groups + * included; anything else, a letter among the digits included, is rejected instead of being + * read past. + * * The Receita Federal does not publish the check digit rule of the CEI/CNO numbering, so the * calculation follows the reference implementations cited below, cross-checked against the CNO * open data of the Receita Federal. diff --git a/src/is-valid-cno/is-valid-cno.ts b/src/is-valid-cno/is-valid-cno.ts index f49fa7d3..ea760e12 100644 --- a/src/is-valid-cno/is-valid-cno.ts +++ b/src/is-valid-cno/is-valid-cno.ts @@ -9,6 +9,11 @@ import { isValidCeiCnoNumber } from "../_internals/is-valid-cei-cno-number/is-va * the weights 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 and 4. A work registered under a legacy CEI keeps * the same number in the CNO, so both registries validate identically. * + * The value has to be written as the 12 digits, optionally split into the printed groups of 2, + * 3, 5 and 2 by whitespace or the usual mask characters, a run of them between two groups + * included; anything else, a letter among the digits included, is rejected instead of being + * read past. + * * The Receita Federal does not publish the check digit rule of the CEI/CNO numbering, so the * calculation follows the reference implementations cited below, cross-checked against the CNO * open data of the Receita Federal. diff --git a/src/is-valid-cns/is-valid-cns.test.ts b/src/is-valid-cns/is-valid-cns.test.ts index 61f00a0c..8eee2100 100644 --- a/src/is-valid-cns/is-valid-cns.test.ts +++ b/src/is-valid-cns/is-valid-cns.test.ts @@ -138,6 +138,11 @@ describe("isValidCns", () => { expect(isValidCns("800000000000001")).toBe(true); }); + test("for 898 0000 0004 3208, the only concrete CNS the ANVISA page prints (weighted sum 396)", () => { + expect(isValidCns("898000000043208")).toBe(true); + expect(isValidCns("898 0000 0004 3208")).toBe(true); + }); + test("for a provisional CNS starting with 9", () => { expect(isValidCns("900000000000008")).toBe(true); }); diff --git a/src/is-valid-cns/is-valid-cns.ts b/src/is-valid-cns/is-valid-cns.ts index a8e44cf7..774edac5 100644 --- a/src/is-valid-cns/is-valid-cns.ts +++ b/src/is-valid-cns/is-valid-cns.ts @@ -42,8 +42,9 @@ const isValidProvisional = (digits: string): boolean => * be a multiple of 11. * * The value has to be written as the 15 digits, optionally split into the printed groups of 3, - * 4, 4 and 4 by whitespace or the usual mask characters; anything else, a letter among the - * digits included, is rejected instead of being read past. + * 4, 4 and 4 by whitespace or the usual mask characters, a run of them between two groups + * included; anything else, a letter among the digits included, is rejected instead of being + * read past. * * @param {string|number} value - The CNS value to be validated. * @returns {boolean} True if the CNS is valid, false otherwise. From 95f195890587c804a4c4bd5fc870c4f604891766 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:02 -0300 Subject: [PATCH 29/75] refactor(format): read the obfuscate option truthily like pad - `formatCpf(value, { obfuscate: 1 })` did not obfuscate because the option was compared with `=== true` while `pad` is read truthily; the two new options now follow the same rule --- src/format-cnpj/format-cnpj.test.ts | 5 +++++ src/format-cnpj/format-cnpj.ts | 7 ++++--- src/format-cpf/format-cpf.test.ts | 5 +++++ src/format-cpf/format-cpf.ts | 8 ++++---- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/format-cnpj/format-cnpj.test.ts b/src/format-cnpj/format-cnpj.test.ts index 55e7b54a..c36ab344 100644 --- a/src/format-cnpj/format-cnpj.test.ts +++ b/src/format-cnpj/format-cnpj.test.ts @@ -140,6 +140,11 @@ describe("formatCnpj", () => { ); }); + it("should obfuscate on any truthy obfuscate value, the way pad is read", () => { + // @ts-expect-error: intentionally not a boolean + expect(formatCnpj("46843485000186", { obfuscate: 1 })).toBe("**.843.485/0001-**"); + }); + it("should behave exactly as without the option when obfuscate is false or absent", () => { expect(formatCnpj("46843485000186", { obfuscate: false })).toBe("46.843.485/0001-86"); expect(formatCnpj("46843485000186")).toBe("46.843.485/0001-86"); diff --git a/src/format-cnpj/format-cnpj.ts b/src/format-cnpj/format-cnpj.ts index ba966cab..be34e327 100644 --- a/src/format-cnpj/format-cnpj.ts +++ b/src/format-cnpj/format-cnpj.ts @@ -10,7 +10,7 @@ export type FormatCnpjOptions = { pad?: boolean; /** Which CNPJ format to read: `1` numeric only, `2` alphanumeric (default: `1`). */ version?: 1 | 2; - /** Whether to hide the first 2 digits and the 2 check digits with `*` (default: `false`). */ + /** Whether to hide the first 2 digits and the 2 check digits with `*` (default: `false`, read for truthiness like `pad`). */ obfuscate?: boolean; }; @@ -29,7 +29,8 @@ const sanitize = (value: string | number, version?: FormatCnpjOptions["version"] * @param {FormatCnpjOptions} [options] - Optional configuration for formatting the CNPJ. * @param {boolean} options.pad - If true, the value will be padded with leading zeros if necessary. * @param {1|2} options.version - The version of the CNPJ to be sanitized. - * @param {boolean} options.obfuscate - If true, hides the first 2 digits and the 2 check digits. + * @param {boolean} options.obfuscate - If truthy, hides the first 2 digits and the 2 check + * digits. Read for truthiness, the way `pad` is, so a non-boolean such as `1` obfuscates too. * @returns {string} The formatted CNPJ string in the pattern "00.000.000/0000-00". * * @example @@ -52,6 +53,6 @@ export const formatCnpj = (value: string | number, options?: FormatCnpjOptions): return format({ pad: options?.pad, value: sanitize(value, options?.version), - pattern: options?.obfuscate === true ? OBFUSCATED_PATTERN : PATTERN, + pattern: (options?.obfuscate ?? false) ? OBFUSCATED_PATTERN : PATTERN, }); }; diff --git a/src/format-cpf/format-cpf.test.ts b/src/format-cpf/format-cpf.test.ts index ea39c628..b1cba888 100644 --- a/src/format-cpf/format-cpf.test.ts +++ b/src/format-cpf/format-cpf.test.ts @@ -99,6 +99,11 @@ describe("formatCpf", () => { expect(formatCpf("9438", { obfuscate: true })).toBe("***.8"); }); + it("should obfuscate on any truthy obfuscate value, the way pad is read", () => { + // @ts-expect-error: intentionally not a boolean + expect(formatCpf("94389575104", { obfuscate: 1 })).toBe("***.895.751-**"); + }); + it("should behave exactly as without the option when obfuscate is false or absent", () => { expect(formatCpf("94389575104", { obfuscate: false })).toBe("943.895.751-04"); expect(formatCpf("94389575104")).toBe("943.895.751-04"); diff --git a/src/format-cpf/format-cpf.ts b/src/format-cpf/format-cpf.ts index d949c01a..31faa6b0 100644 --- a/src/format-cpf/format-cpf.ts +++ b/src/format-cpf/format-cpf.ts @@ -7,7 +7,7 @@ import { OBFUSCATED_PATTERN, PATTERN } from "./constants"; export type FormatCpfOptions = { /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ pad?: boolean; - /** Whether to hide the first 3 digits and the 2 check digits with `*` (default: `false`). */ + /** Whether to hide the first 3 digits and the 2 check digits with `*` (default: `false`, read for truthiness like `pad`). */ obfuscate?: boolean; }; @@ -17,7 +17,8 @@ export type FormatCpfOptions = { * @param {string|number} value - The CPF value to be formatted. It can be a string or a number. * @param {FormatCpfOptions} [options] - Optional formatting options. * @param {boolean} options.pad - If true, the value will be padded with leading zeros if necessary. - * @param {boolean} options.obfuscate - If true, hides the first 3 digits and the 2 check digits. + * @param {boolean} options.obfuscate - If truthy, hides the first 3 digits and the 2 check + * digits. Read for truthiness, the way `pad` is, so a non-boolean such as `1` obfuscates too. * @returns {string} The formatted CPF string in the pattern "000.000.000-00". * * @example @@ -29,7 +30,6 @@ export type FormatCpfOptions = { * ``` * * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/meu-cpf - * @see Official: http://sped.rfb.gov.br/arquivo/show/8231 * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/cpf.py */ export const formatCpf = (value: string | number, options?: FormatCpfOptions): string => { @@ -38,6 +38,6 @@ export const formatCpf = (value: string | number, options?: FormatCpfOptions): s return format({ pad: options?.pad, value: sanitizeToDigits(value), - pattern: options?.obfuscate === true ? OBFUSCATED_PATTERN : PATTERN, + pattern: (options?.obfuscate ?? false) ? OBFUSCATED_PATTERN : PATTERN, }); }; From af8a8bd3263c7fe98013019e1b5d62ea294bf650 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:02 -0300 Subject: [PATCH 30/75] ci(datasets): reject a zero bank code and parse the NCM dates strictly - BrasilAPI currently lists three institutions with `code: 0`; the guard was `< 0`, so they would have been written as `"000"`, and is now `<= 0` like the Bacen path - `parseBrDate` accepted `31/02/2026` as 03/03/2026 through `Date.UTC`; it now requires an exact `dd/mm/yyyy` and a round-trip of the components, and the three clock reads take one `Date` --- scripts/banks.ts | 2 +- scripts/ncm.ts | 37 ++++++++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/scripts/banks.ts b/scripts/banks.ts index dd49d72a..d0ce0823 100644 --- a/scripts/banks.ts +++ b/scripts/banks.ts @@ -120,7 +120,7 @@ const fetchFromBrasilApi = async (): Promise => { for (const entry of json) { if (!isBrasilApiBank(entry) || typeof entry.code !== "number") continue; - if (!Number.isInteger(entry.code) || entry.code < 0 || entry.code > 999) continue; + if (!Number.isInteger(entry.code) || entry.code <= 0 || entry.code > 999) continue; const ispb = entry.ispb; diff --git a/scripts/ncm.ts b/scripts/ncm.ts index 8f8bca5c..26687ad5 100644 --- a/scripts/ncm.ts +++ b/scripts/ncm.ts @@ -34,19 +34,43 @@ const isNcmResponse = (value: unknown): value is NcmResponse => Array.isArray(value.Nomenclaturas) && value.Nomenclaturas.every((entry) => isNcmEntry(entry)); +const BR_DATE_REGEX = /^(\d{2})\/(\d{2})\/(\d{4})$/; + /** * Parses a Siscomex `dd/mm/yyyy` date into a `Date` at UTC midnight. * + * The string must have the exact `dd/mm/yyyy` shape and name a day that exists: `Date.UTC` + * rolls impossible dates over (`31/02/2026` would become 3 March 2026) and would silently + * widen the in-force window, so the parsed components are compared back against the date. + * An invalid `Date` is returned otherwise, which makes every comparison in `isInForce` false. + * * @param {string} date - A date string in `dd/mm/yyyy` format. - * @returns {Date} The parsed date. + * @returns {Date} The parsed date, or an invalid `Date`. */ const parseBrDate = (date: string): Date => { - const [day, month, year] = date.split("/").map(Number); - return new Date(Date.UTC(year, month - 1, day)); + const match = BR_DATE_REGEX.exec(date); + + if (match === null) return new Date(Number.NaN); + + const day = Number(match[1]); + const month = Number(match[2]); + const year = Number(match[3]); + const parsed = new Date(Date.UTC(year, month - 1, day)); + + if ( + parsed.getUTCFullYear() !== year || + parsed.getUTCMonth() + 1 !== month || + parsed.getUTCDate() !== day + ) { + return new Date(Number.NaN); + } + + return parsed; }; /** - * Whether `today` falls within `[Data_Inicio, Data_Fim]` (both inclusive). + * Whether `today` falls within `[Data_Inicio, Data_Fim]` (both inclusive). An entry whose + * boundaries are not both valid `dd/mm/yyyy` dates is treated as not in force. * * @param {NcmEntry} entry - The Siscomex NCM entry to check. * @param {Date} today - The reference date. @@ -70,9 +94,8 @@ const main = async (): Promise => { throw new Error("Siscomex NCM payload is not a Nomenclaturas response"); } - const today = new Date( - Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate()), - ); + const now = new Date(); + const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); const codes = json.Nomenclaturas.filter( (entry) => /^[\d.]{10}$/.test(entry.Codigo) && isInForce(entry, today), From 75f875dca086fc2ee86bcd531dbcd70cc28e42b6 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:02 -0300 Subject: [PATCH 31/75] ci(release): hide the CI and build sections from the changelog - the 2.4.0 entry would list twenty-one workflow commits that mean nothing to a consumer of the package; the sections stay in the commit history and drop out of `CHANGELOG.md` --- release-please-config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release-please-config.json b/release-please-config.json index fc2cdc16..723971a0 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -11,8 +11,8 @@ { "type": "perf", "section": "Performance" }, { "type": "revert", "section": "Reverts" }, { "type": "docs", "section": "Documentation" }, - { "type": "build", "section": "Build System" }, - { "type": "ci", "section": "CI" }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "CI", "hidden": true }, { "type": "deps", "section": "Dependencies" }, { "type": "chore", "scope": "deps", "section": "Dependencies" }, { "type": "chore", "scope": "data", "section": "Data" }, From b86b153d548f44bd93017c0beb923e6d366d414d Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:02 -0300 Subject: [PATCH 32/75] test: pin the published worked examples and the boleto moeda leniency - `280012389-38` from the e-Financeira manual (ADE Cofis 10/2026) for `isValidCpf`, `0321418-40` from SINTEGRA PE for `isValidIe`, the AM and MS rows moved to a derived-from-formula list - a 47-digit line whose barcode moeda is not 9 is still accepted, as documented - `formatVoterId` drops the digits past the last slot; the processo tests read the year once --- src/format-voter-id/format-voter-id.test.ts | 7 +++++++ .../generate-processo-juridico.test.ts | 12 ++++++++---- src/is-valid-boleto/is-valid-boleto.test.ts | 4 ++++ src/is-valid-cpf/is-valid-cpf.test.ts | 5 +++++ src/is-valid-ie/is-valid-ie.test.ts | 14 ++++++++++++-- 5 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/format-voter-id/format-voter-id.test.ts b/src/format-voter-id/format-voter-id.test.ts index c766ef71..b7e6ae19 100644 --- a/src/format-voter-id/format-voter-id.test.ts +++ b/src/format-voter-id/format-voter-id.test.ts @@ -27,6 +27,13 @@ describe("formatVoterId", () => { expect(formatVoterId("1234567880299")).toBe("1234 5678 8 02 99"); }); + it("should drop the digits past the last slot of the pattern", () => { + expect(formatVoterId("1234567880191")).toBe("1234 5678 8 01 91"); + expect(formatVoterId("12345678801912")).toBe("1234 5678 8 01 91"); + expect(formatVoterId("123456788019123")).toBe("1234 5678 8 01 91"); + expect(formatVoterId("12345678803991")).toBe("1234 5678 80 39"); + }); + it("should keep the 12-digit grouping for a 13-digit value whose UF cannot carry 9 sequential digits", () => { expect(formatVoterId("1234567880399")).toBe("1234 5678 80 39"); }); diff --git a/src/generate-processo-juridico/generate-processo-juridico.test.ts b/src/generate-processo-juridico/generate-processo-juridico.test.ts index 92adccc5..9c1fa604 100644 --- a/src/generate-processo-juridico/generate-processo-juridico.test.ts +++ b/src/generate-processo-juridico/generate-processo-juridico.test.ts @@ -28,10 +28,11 @@ describe("generateProcessoJuridico", () => { }); it("should honor the year and court options", () => { - const value = generateProcessoJuridico({ year: currentYear(), court: 5 }); + const year = currentYear(); + const value = generateProcessoJuridico({ year, court: 5 }); expect(value).not.toBe(null); - expect((value as string).slice(9, 13)).toBe(String(currentYear())); + expect((value as string).slice(9, 13)).toBe(String(year)); expect((value as string).charAt(13)).toBe("5"); expect(isValidProcessoJuridico(value as string)).toBe(true); }); @@ -88,9 +89,11 @@ describe("generateProcessoJuridico", () => { const court = fc.integer({ min: 1, max: 9 }); test("should embed every accepted year and court in a valid number", () => { + const thisYear = currentYear(); + fc.assert( fc.property(year, court, (chosenYear, chosenCourt) => { - fc.pre(chosenYear >= currentYear()); + fc.pre(chosenYear >= thisYear); const value = generateProcessoJuridico({ year: chosenYear, court: chosenCourt }); @@ -105,10 +108,11 @@ describe("generateProcessoJuridico", () => { test("should return null for every year outside the accepted range", () => { const outOfRangeYears = fc.integer({ min: -9999, max: 999_999 }); + const thisYear = currentYear(); fc.assert( fc.property(outOfRangeYears, (invalidYear) => { - fc.pre(invalidYear < currentYear() || invalidYear > 9999); + fc.pre(invalidYear < thisYear || invalidYear > 9999); expect(generateProcessoJuridico({ year: invalidYear })).toBe(null); }), diff --git a/src/is-valid-boleto/is-valid-boleto.test.ts b/src/is-valid-boleto/is-valid-boleto.test.ts index fc5892d1..eac30538 100644 --- a/src/is-valid-boleto/is-valid-boleto.test.ts +++ b/src/is-valid-boleto/is-valid-boleto.test.ts @@ -63,6 +63,10 @@ describe("isValidBoleto", () => { test("when is a boleto valid with mask", () => { expect(isValidBoleto("0019000009 01149.718601 68524.522114 6 75860000102656")).toBe(true); }); + + test("when the código de moeda is not 9 (same fixture as the boleto valid without mask, with the moeda in barcode position 4 changed to 7 and both the campo 1 and the DV geral recalculated): Carta-Circular BCB nº 2.926/2000 fixes that position at 9, and the leniency kept from 2.3.0 accepts any other digit", () => { + expect(isValidBoleto("00170000010114971860168524522114275860000102656")).toBe(true); + }); }); describe("arrecadação", () => { diff --git a/src/is-valid-cpf/is-valid-cpf.test.ts b/src/is-valid-cpf/is-valid-cpf.test.ts index 13abe749..3d153dd4 100644 --- a/src/is-valid-cpf/is-valid-cpf.test.ts +++ b/src/is-valid-cpf/is-valid-cpf.test.ts @@ -94,6 +94,11 @@ describe("isValidCpf", () => { expect(isValidCpf("12345678909 ")).toBe(true); }); + test("when it is the worked example the RFB Manual da e-Financeira prints", () => { + expect(isValidCpf("28001238938")).toBe(true); + expect(isValidCpf("280.012.389-38")).toBe(true); + }); + test("should return true for randomly generated CPFs", () => { for (let i = 0; i < 100; i++) { expect(isValidCpf(generateCpf())).toBe(true); diff --git a/src/is-valid-ie/is-valid-ie.test.ts b/src/is-valid-ie/is-valid-ie.test.ts index 1c15ab43..659c628d 100644 --- a/src/is-valid-ie/is-valid-ie.test.ts +++ b/src/is-valid-ie/is-valid-ie.test.ts @@ -763,7 +763,6 @@ describe("isValidIe", () => { ["AC", "01.004.823/001-12"], ["AL", "240000048"], ["AP", "030123459"], - ["AM", "99.999.999-0"], ["BA", "123456-63"], ["BA", "612345-57"], ["BA", "1000003-06"], @@ -772,11 +771,11 @@ describe("isValidIe", () => { ["GO", "10.987.654-7"], ["MA", "120000385"], ["MG", "062.307.904/0081"], - ["MS", "280000006"], ["MT", "0013000001-9"], ["PA", "15999999-5"], ["PA", "75000002-3"], ["PB", "06000001-5"], + ["PE", "0321418-40"], ["PI", "012345679"], ["PR", "123.45678-50"], ["RN", "20.040.040-1"], @@ -805,6 +804,17 @@ describe("isValidIe", () => { expect(isValidIe(stateCode, ie)).toBe(true); } }); + + const derivedFromPublishedFormula: [StateCode, string][] = [ + ["AM", "99.999.999-0"], + ["MS", "280000006"], + ]; + + test("should accept the values derived from the formulas the AM and MS pages publish", () => { + for (const [stateCode, ie] of derivedFromPublishedFormula) { + expect(isValidIe(stateCode, ie)).toBe(true); + } + }); }); describe("state code lookup", () => { From 1d3aeb0da27ea95d9e3719170eb8958c4aa88193 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:46:02 -0300 Subject: [PATCH 33/75] docs: cite the acts and ajustes behind the fiscal and calendar utilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - license plates: the Anexo I layout `LLLNLNN` and the Anexo II conversion table of Resolução CONTRAN 969/2022 are cited from the annexes PDF SENATRAN publishes - CPF: the e-Financeira manual is cited through ADE Cofis 10/2026 on SIJUT; the SPED host is gone - NF-e keys: models 67 and 64 cite Ajuste SINIEF 36/19 and 03/20, the all-zero nNF rejection cites the `TNF` pattern of the leiaute, tpEmis 3 is the Regime Especial NFF - CFOP cites Ajuste SINIEF 39/25; the CST items 12, 13, 52, 72 and 74 were never in force - holidays: Lei 10.607/2002 added Finados and folded in Tiradentes, national since Lei 1.266/1950; the Portaria MGI sources three of the four Easter-derived entries, Páscoa is arithmetic - Resolução Anatel 263/2001 is `Based on:` (revoked); DDD 61 is seated in the DF, which holds one of its thirteen municipalities - CONCLA, TSE and iso.org answer 403 to non-browser clients, noted above their citations - CAEPF weights, CNS sum, certidão codes 8 and 9, IE all-zero note, passport, generators using `Math.random()`, getCities/getMunicipalities case sensitivity, CEP retry and default providers, Unreserved Templates, formatCurrency precision limit, the DF-e model list, the bundle-size table, CONTRIBUTING scripts and the input-handling note, in English and Portuguese, plus llms.txt --- CONTRIBUTING.md | 40 ++++---- docs/getting-started.md | 18 ++-- docs/llms-full.txt | 91 ++++++++++--------- docs/llms.txt | 4 +- docs/pt-br/getting-started.md | 18 ++-- docs/pt-br/utilities.md | 77 ++++++++-------- docs/utilities.md | 73 ++++++++------- scripts/cfop.ts | 2 + src/_internals/constants/cfop.ts | 2 + src/_internals/constants/iban.ts | 3 + .../constants.ts | 12 ++- .../convert-license-plate-to-mercosul.ts | 10 +- src/format-cns/format-cns.test.ts | 5 + src/format-currency/format-currency.ts | 3 +- .../format-legal-nature.ts | 4 + .../format-license-plate.ts | 5 + src/format-voter-id/format-voter-id.ts | 8 +- src/generate-cnpj/generate-cnpj.ts | 2 +- src/generate-cpf/generate-cpf.ts | 13 ++- .../generate-legal-nature.ts | 4 + .../generate-license-plate.ts | 21 ++++- .../generate-pix-payload.ts | 10 +- src/generate-voter-id/generate-voter-id.ts | 3 + src/get-area-code-info/get-area-code-info.ts | 16 ++-- .../get-area-codes-by-state.ts | 9 +- src/get-cfop/get-cfop.ts | 2 + src/get-cities/get-cities.ts | 6 ++ .../get-format-license-plate.ts | 9 ++ src/get-holidays/get-holidays.ts | 22 +++-- src/get-legal-nature/get-legal-nature.ts | 4 + src/get-legal-natures/get-legal-natures.ts | 4 + src/get-municipalities/get-municipalities.ts | 6 ++ .../get-state-by-ibge-code.ts | 3 +- src/is-business-day/is-business-day.ts | 20 ++-- src/is-holiday/is-holiday.ts | 10 +- src/is-valid-cfop/is-valid-cfop.ts | 2 + src/is-valid-cpf/is-valid-cpf.ts | 13 ++- .../is-valid-credit-card.ts | 5 +- src/is-valid-ie/is-valid-ie.ts | 4 +- src/is-valid-legal-nature/constants.ts | 4 + .../is-valid-legal-nature.ts | 4 + .../is-valid-license-plate.ts | 9 ++ src/is-valid-nfe-key/is-valid-nfe-key.test.ts | 2 +- src/is-valid-nfe-key/is-valid-nfe-key.ts | 8 +- src/is-valid-pix-key/is-valid-pix-key.ts | 5 +- src/is-valid-vin/constants.ts | 3 + src/is-valid-vin/is-valid-vin.ts | 4 + src/is-valid-voter-id/is-valid-voter-id.ts | 3 + src/parse-certidao/constants.ts | 8 +- src/parse-certidao/parse-certidao.ts | 8 +- src/parse-cpf/parse-cpf.ts | 1 - src/parse-legal-nature/parse-legal-nature.ts | 4 + src/parse-nfe-key/constants.ts | 23 +++-- src/parse-nfe-key/parse-nfe-key.ts | 17 +++- src/parse-pix-key/parse-pix-key.ts | 5 +- src/parse-voter-id/parse-voter-id.ts | 3 + 56 files changed, 439 insertions(+), 235 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f200213e..dfb08dfd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,24 +28,28 @@ and is invoked through the `npm` scripts below, so you don't need to install any ### Useful scripts -| Command | What it does | -| ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `npm run check` | Runs `vp check`: format check, lint and type-check together. Run this before opening a PR. | -| `npm run check:fix` | Same as above, but auto-fixes what it can. | -| `npm run format` / `npm run format:check` | Formats the codebase / checks formatting with `vp fmt`. | -| `npm run lint` / `npm run lint:fix` | Lints the codebase with `vp lint`. | -| `npm run test` | Runs the unit test suite with `vp test`. | -| `npm run test:coverage` | Runs tests with coverage (`vp test run --coverage`). | -| `npm run test:bun` | Runs the test suite on [Bun](https://bun.sh) (`bun test src`). | -| `npm run test:deno` | Runs the test suite on [Deno](https://deno.com) (`deno test`). | -| `npm run test:chrome-browser`, `npm run test:firefox-browser`, `npm run test:edge-browser`, `npm run test:safari-browser` | Runs the test suite in real browsers via `vp test --browser.enabled`. | -| `npm run build` | Builds the library with `vp build`. | -| `npm run check:duplication` | Runs [jscpd](https://jscpd.dev) over `src` and `scripts`; any copy-pasted block of 5+ lines / 50+ tokens fails. | -| `npm run check:unused` | Runs [knip](https://knip.dev): unused files, exports, types and dependencies fail. | -| `npm run test:mutation` | Runs [Stryker](https://stryker-mutator.io) mutation tests (`stryker run`); pass `-- --mutate src//.ts` for one file. | -| `npm run check:api` | Builds the package and runs API Extractor over `dist/brazilian-utils.d.ts`: a public type without a doc comment, or a type the API refers to without exporting, fails. | -| `npm run check:commits` | Checks the commit messages since `origin/main` with commitlint (Conventional Commits). | -| `npm run check:lockfile` | Checks `package-lock.json` only resolves to the npm registry over HTTPS with integrity hashes (lockfile-lint). | +| Command | What it does | +| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `npm run check` | Runs `vp check`: format check, lint and type-check together. Run this before opening a PR. | +| `npm run check:fix` | Same as above, but auto-fixes what it can. | +| `npm run format` / `npm run format:check` | Formats the codebase / checks formatting with `vp fmt`. | +| `npm run lint` / `npm run lint:fix` | Lints the codebase with `vp lint`. | +| `npm run test` | Runs the unit test suite with `vp test`. | +| `npm run test:coverage` | Runs tests with coverage (`vp test run --coverage`). | +| `npm run test:bun` | Runs the test suite on [Bun](https://bun.sh) (`bun test src`). | +| `npm run test:deno` | Runs the test suite on [Deno](https://deno.com) (`deno test`). | +| `npm run test:live` | Runs the live CEP-provider test against the real network (`RUN_LIVE_CEP_TESTS=1 vp test src/get-address-info-by-cep/get-address-info-by-cep.test.ts`); not part of the regular test run, only of the scheduled `Live tests` workflow. | +| `npm run test:chrome-browser`, `npm run test:firefox-browser`, `npm run test:edge-browser`, `npm run test:safari-browser` | Runs the test suite in real browsers via `vp test --browser.enabled`. | +| `npm run build` | Builds the library for publishing with `vp pack` (also runs attw and publint over the built output). | +| `npm run build:data` | Regenerates the datasets under `src/_internals/constants` from the IBGE/CONCLA sources (`scripts/data.ts`); run by the scheduled `Update datasets` workflow. | +| `npm run build:llms` | Regenerates `docs/llms.txt` and `docs/llms-full.txt` from the docs (`scripts/llms.ts`); CI fails if they're out of date. | +| `npm run check:dependencies` | Fails if `package.json` declares any runtime `dependencies` (this package ships zero by design). | +| `npm run check:duplication` | Runs [jscpd](https://jscpd.dev) over `src` and `scripts`; any copy-pasted block of 5+ lines / 50+ tokens fails. | +| `npm run check:unused` | Runs [knip](https://knip.dev): unused files, exports, types and dependencies fail. | +| `npm run test:mutation` | Runs [Stryker](https://stryker-mutator.io) mutation tests (`stryker run`); pass `-- --mutate src//.ts` for one file. | +| `npm run check:api` | Builds the package and runs API Extractor over `dist/brazilian-utils.d.ts`: a public type without a doc comment, or a type the API refers to without exporting, fails. | +| `npm run check:commits` | Checks the commit messages since `origin/main` with commitlint (Conventional Commits). | +| `npm run check:lockfile` | Checks `package-lock.json` only resolves to the npm registry over HTTPS with integrity hashes (lockfile-lint). | Before opening a pull request, make sure `npm run check` and `npm run test` both pass locally. If your change touches runtime behavior, also consider running the Bun/Deno scripts above. The library is diff --git a/docs/getting-started.md b/docs/getting-started.md index c7ed8ebf..13413d79 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -63,19 +63,19 @@ You can check a list of utilities [by clicking here](utilities.md). ## Bundle size -The package is tree-shakeable: importing one util from the root pulls in only that util's code, not the rest of the library. `isValidCpf`, for example, adds roughly 0.7 KB minified to your bundle. A bundler that supports tree-shaking (webpack, Rollup, esbuild, Vite, etc.) drops every other util. +The package is tree-shakeable: importing one util from the root pulls in only that util's code, not the rest of the library. `isValidCpf`, for example, adds roughly 1.2 KB minified (0.6 KB gzipped) to your bundle. A bundler that supports tree-shaking (webpack, Rollup, esbuild, Vite, etc.) drops every other util. A handful of utils are the exception: each embeds an official dataset, so it weighs far more than every other util combined. These are their single-import sizes, minified and gzipped: | Util | Dataset | Minified | Gzipped | | --- | --- | --- | --- | -| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 IBGE municipalities, with names and codes | 155.9 KB | 50.0 KB | -| `getCities` | 5571 IBGE municipality names | 153.6 KB | 49.4 KB | -| `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 113.4 KB | 24.0 KB | -| `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 118.4 KB | 30.2 KB | -| `isValidCnae` · `getCnae` | CNAE 2.3 subclasses | 93.6 KB | 21.1 KB | -| `isValidCfop` · `getCfop` | CFOP operation descriptions | 68.3 KB | 6.5 KB | -| `getBanks` · `getBankByCode` | Banco Central STR participants (COMPE + ISPB) | 37.9 KB | 9.3 KB | +| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 IBGE municipalities, with names and codes | 156.2 KB | 50.2 KB | +| `getCities` | 5571 IBGE municipality names | 154.0 KB | 49.7 KB | +| `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 113.8 KB | 24.3 KB | +| `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 118.8 KB | 30.4 KB | +| `isValidCnae` · `getCnae` | CNAE 2.3 subclasses | 94.0 KB | 21.3 KB | +| `isValidCfop` · `getCfop` | CFOP operation descriptions | 68.7 KB | 6.8 KB | +| `getBanks` · `getBankByCode` | Banco Central STR participants (COMPE + ISPB) | 38.3 KB | 9.6 KB | Importing any of them from the root, even alongside a single small util, pulls that whole dataset into your main bundle, because this package ships as a single ESM module: a dynamic `import()` of the root (`await import('@brazilian-utils/brazilian-utils')`) still resolves to that same one file, so it can't be split out on its own. A bundler doing code-splitting needs a separate module to split *into*. @@ -97,4 +97,4 @@ getMunicipalityByCode('3550308'); Every util is available this way, as `@brazilian-utils/brazilian-utils/` (kebab-case, matching the function name: `isValidCpf` → `is-valid-cpf`), for the same lazy-loading/code-splitting reason. -Pick one style per util in a given app: a bundler treats the root import and the subpath import as two unrelated modules, so importing `getCities` from both the root *and* `/get-cities` in the same app bundles the 153.6 KB city table twice, once in each module's own output. +Pick one style per util in a given app: a bundler treats the root import and the subpath import as two unrelated modules, so importing `getCities` from both the root *and* `/get-cities` in the same app bundles the 154.0 KB city table twice, once in each module's own output. diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 46ff0d75..528fbac1 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -201,19 +201,19 @@ You can check a list of utilities [by clicking here](utilities.md). ### Bundle size -The package is tree-shakeable: importing one util from the root pulls in only that util's code, not the rest of the library. `isValidCpf`, for example, adds roughly 0.7 KB minified to your bundle. A bundler that supports tree-shaking (webpack, Rollup, esbuild, Vite, etc.) drops every other util. +The package is tree-shakeable: importing one util from the root pulls in only that util's code, not the rest of the library. `isValidCpf`, for example, adds roughly 1.2 KB minified (0.6 KB gzipped) to your bundle. A bundler that supports tree-shaking (webpack, Rollup, esbuild, Vite, etc.) drops every other util. A handful of utils are the exception: each embeds an official dataset, so it weighs far more than every other util combined. These are their single-import sizes, minified and gzipped: | Util | Dataset | Minified | Gzipped | | --- | --- | --- | --- | -| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 IBGE municipalities, with names and codes | 155.9 KB | 50.0 KB | -| `getCities` | 5571 IBGE municipality names | 153.6 KB | 49.4 KB | -| `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 113.4 KB | 24.0 KB | -| `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 118.4 KB | 30.2 KB | -| `isValidCnae` · `getCnae` | CNAE 2.3 subclasses | 93.6 KB | 21.1 KB | -| `isValidCfop` · `getCfop` | CFOP operation descriptions | 68.3 KB | 6.5 KB | -| `getBanks` · `getBankByCode` | Banco Central STR participants (COMPE + ISPB) | 37.9 KB | 9.3 KB | +| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 IBGE municipalities, with names and codes | 156.2 KB | 50.2 KB | +| `getCities` | 5571 IBGE municipality names | 154.0 KB | 49.7 KB | +| `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 113.8 KB | 24.3 KB | +| `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 118.8 KB | 30.4 KB | +| `isValidCnae` · `getCnae` | CNAE 2.3 subclasses | 94.0 KB | 21.3 KB | +| `isValidCfop` · `getCfop` | CFOP operation descriptions | 68.7 KB | 6.8 KB | +| `getBanks` · `getBankByCode` | Banco Central STR participants (COMPE + ISPB) | 38.3 KB | 9.6 KB | Importing any of them from the root, even alongside a single small util, pulls that whole dataset into your main bundle, because this package ships as a single ESM module: a dynamic `import()` of the root (`await import('@brazilian-utils/brazilian-utils')`) still resolves to that same one file, so it can't be split out on its own. A bundler doing code-splitting needs a separate module to split *into*. @@ -235,13 +235,13 @@ getMunicipalityByCode('3550308'); Every util is available this way, as `@brazilian-utils/brazilian-utils/` (kebab-case, matching the function name: `isValidCpf` → `is-valid-cpf`), for the same lazy-loading/code-splitting reason. -Pick one style per util in a given app: a bundler treats the root import and the subpath import as two unrelated modules, so importing `getCities` from both the root *and* `/get-cities` in the same app bundles the 153.6 KB city table twice, once in each module's own output. +Pick one style per util in a given app: a bundler treats the root import and the subpath import as two unrelated modules, so importing `getCities` from both the root *and* `/get-cities` in the same app bundles the 154.0 KB city table twice, once in each module's own output. ## Utilities Here you will find all the utilities available for use. -> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function (including `capitalize`) returns an empty value of its return type: `""` for strings, `0` for `parseCurrency`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. +> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function returns an empty value of its return type: every `format*` function, `capitalize`, and the string-returning `parse*` functions (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) return `""`; `parseCurrency` returns `0`; the object/tuple parsers — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — return `null`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. ### isValidCpf @@ -256,7 +256,7 @@ isValidCpf('111 444 777 35'); // true (whitespace mask) ### formatCpf -Format CPF. `options.obfuscate` (part of `FormatCpfOptions`) hides the first 3 digits and the 2 check digits (`***.456.789-**`), the gov.br / Receita Federal display convention, applied after `pad`. +Format CPF. `options.obfuscate` (part of `FormatCpfOptions`) hides the first 3 digits and the 2 check digits (`***.456.789-**`), the gov.br / Receita Federal display convention, applied after `pad`. It is read for truthiness, the way `pad` is, so any truthy value obfuscates. ```javascript import { formatCpf } from '@brazilian-utils/brazilian-utils'; @@ -300,7 +300,7 @@ isValidCnpj('q0slfmbd7vx439', { version: 2 }); // true (lowercase alphanumeric) ### formatCnpj -Format CNPJ. `options.obfuscate` (part of `FormatCnpjOptions`) hides the first 2 digits and the 2 check digits (`**.345.678/0001-**`), the gov.br / Receita Federal display convention. It applies to both versions and comes after `pad`. +Format CNPJ. `options.obfuscate` (part of `FormatCnpjOptions`) hides the first 2 digits and the 2 check digits (`**.345.678/0001-**`), the gov.br / Receita Federal display convention. It applies to both versions and comes after `pad`, and is read for truthiness, the way `pad` is, so any truthy value obfuscates. ```javascript import { formatCnpj } from '@brazilian-utils/brazilian-utils'; @@ -340,7 +340,7 @@ isValidCep('12345'); // false (invalid length) ### generateCnpj -Generate a valid random CNPJ. +Generate a valid random CNPJ. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generateCnpj } from '@brazilian-utils/brazilian-utils' @@ -396,7 +396,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ### getBoletoInfo -Extract information from a boleto (amount, expiration date, bank code). Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. +Extract information from a boleto (amount, expiration date, bank code). Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. The cycle search never goes below the first cycle, so a `referenceDate` older than the scheme itself still resolves a factor to the oldest date that factor can denote rather than to one before the 07/10/1997 base date. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -448,7 +448,7 @@ parsePixKey('+5551998259765'); // { type: 'phone', value: '+5551998259765' } ### isValidPixPayload -Check if a Pix BR Code payload (the string behind a Pix QR Code and behind "Pix copia e cola") is valid: well-formed TLV structure, the mandatory objects present, one of the "Merchant Account Information" templates carrying the `br.gov.bcb.pix` GUI with a key or a URL, and a matching CRC-16. The "Point of Initiation Method" object (`01`) is advisory: the Manual do BR Code marks it optional and only assigns a meaning to the value `"12"` ("só pode ser utilizado uma vez"), so it may be absent from either shape and only a value outside `{"11", "12"}` makes the payload invalid. When a payload built around a key carries an amount (`54`), that amount must be greater than zero, unless the payload is a Pix Saque BR Code, i.e. unless it carries the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`) as §2.6 of the Pix manual prescribes; rejecting `"0"`/`"0.00"` without `fss` is a deliberate restriction of this library, not a rule of the manual. The key itself is not checked against the DICT formats, use `isValidPixKey` for that. Payloads that carry the location in an Unreserved Template (IDs 80 to 99), as the "QR Code composto" of Pix Automático (Pix recorrente) does, are out of scope and reported as invalid. +Check if a Pix BR Code payload (the string behind a Pix QR Code and behind "Pix copia e cola") is valid: well-formed TLV structure, the mandatory objects present, one of the "Merchant Account Information" templates carrying the `br.gov.bcb.pix` GUI with a key or a URL, and a matching CRC-16. The "Point of Initiation Method" object (`01`) is advisory: the Manual do BR Code marks it optional and only assigns a meaning to the value `"12"` ("só pode ser utilizado uma vez"), so it may be absent from either shape and only a value outside `{"11", "12"}` makes the payload invalid. When a payload built around a key carries an amount (`54`), that amount must be greater than zero, unless the payload is a Pix Saque BR Code, i.e. unless it carries the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`) as §2.6 of the Pix manual prescribes; rejecting `"0"`/`"0.00"` without `fss` is a deliberate restriction of this library, not a rule of the manual. A `fss` written next to a PSP location makes the payload invalid: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template of §2.6. The key itself is not checked against the DICT formats, use `isValidPixKey` for that. Unreserved Templates (IDs 80 to 99) are ignored: the "QR Code composto" of Pix Automático (Pix recorrente) writes its recurrence location in one of them, and when such a payload also carries a payment location in 26-25, as the composite example of the Pix manual does, it is accepted and read as an ordinary dynamic payload with the recurrence location dropped. Only a payload with no Pix template at all in IDs 26 to 51 is reported as invalid. ```javascript import { isValidPixPayload } from '@brazilian-utils/brazilian-utils'; @@ -463,7 +463,7 @@ isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (broken CRC) ### parsePixPayload -Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The result is typed as `PixPayload`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Payloads whose location lives in an Unreserved Template (IDs 80 to 99, Pix Automático) are out of scope and return `null`. +Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The result is typed as `PixPayload`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. A `fss` written next to a PSP location returns `null`: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template of §2.6. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Unreserved Templates (IDs 80 to 99) are ignored: a "QR Code composto" of Pix Automático that also carries a payment location in 26-25 is parsed as an ordinary dynamic payload and its recurrence location is dropped, so a consumer that has to tell the two apart cannot rely on this parser. Only a payload with no Pix template at all in IDs 26 to 51 returns `null`. ```javascript import { parsePixPayload } from '@brazilian-utils/brazilian-utils'; @@ -509,9 +509,9 @@ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // nu ### isValidNfeKey -Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e (65), CT-e (57), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para Outros Serviços of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), GTV-e (64, the CT-e Guia de Transporte de Valores), BP-e (63), NF3e (66) and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed differently. Accepts whitespace between digit groups (the common display mask) and the `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the document's XML. +Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e (65), CT-e (57, the Conhecimento de Transporte Eletrônico instituted by the cláusula primeira of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para Outros Serviços instituted by the cláusula primeira of the [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, the CT-e Guia de Transporte de Valores instituted by the cláusula primeira of the [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed differently. Accepts whitespace between digit groups (the common display mask) and the `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the document's XML. -The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` for the CT-e, `{1, 5, 7, 8}` for the CT-e OS, `{1, 2, 7, 8}` for the GTV-e, `{1, 2, 3}` for the MDF-e and `{1, 2}` for the BP-e, the NF3e and the NFCom. Code 8, the authorização pela SVC-SP, is assigned by the [CT-e MOC 4.00](https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais) only, never by the NF-e one; the domains of the [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), the [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) and the [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) come from their own manuals. For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, which forbids the twenty repeated and sequential `cNF` values it lists and a `cNF` equal to the document number. Rejecting a document number of all zeros, on the other hand, is a choice of this library: no MOC rule was found forbidding it. +The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` for the CT-e, `{1, 5, 7, 8}` for the CT-e OS, `{1, 2, 7, 8}` for the GTV-e, `{1, 2, 3}` for the MDF-e and `{1, 2}` for the BP-e, the NF3e and the NFCom. Code 8, the authorização pela SVC-SP, is assigned by the [CT-e MOC 4.00](https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais) only, never by the NF-e one; the domains of the [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), the [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) and the [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) come from their own manuals. For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, which forbids the twenty repeated and sequential `cNF` values it lists and a `cNF` equal to the document number. A document number of all zeros is turned down for every model, following the leiaute rather than a choice of this library: `tiposBasico_v4.00.xsd` of the [NF-e schema package](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) types `nNF` as `TNF`, whose pattern is `[1-9]{1}[0-9]{0,8}`, and the Anexo I of every other model repeats the same regex for its own number field. ```javascript import { isValidNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -527,7 +527,7 @@ isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 000 ### formatNfeKey -Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. +Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. A value that is not a string is only read when it is a non-negative safe integer, so anything with no usable digit representation (a negative or fractional number, an object, an object created with `Object.create(null)`) gives `''`. ```javascript import { formatNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -635,7 +635,7 @@ isValidLandlinePhone('1130000000'); // true ### isValidServicePhone -Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`; `112` and `911` are accepted too, as mobile-only aliases of `190` that Anatel lists alongside the other 3-digit codes). Only the structure is checked, the number does not have to be assigned to anyone. Anatel publishes no allocation for the abbreviated numbers, so only the conventional `300X` and `400X` roots are recognised: other "Número Único" carrier prefixes in market use, such as `4020` and `4062`, are out of scope and are rejected. +Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`), whose consolidated table is the Anexo of [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151). `112` and `911` are rejected: Anatel designates neither, and `911` is not even inside the `1N₂N₁` range art. 13 of [Resolução nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749) destines to public utility services, so the way handsets route them is a GSM convention rather than a numbering designation. Only the structure is checked, the number does not have to be assigned to anyone. Anatel withdrew the 4-digit codes instead of allocating them (art. 43 I of [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86) and art. 2º II of the Ato above both ordered them released), so only the conventional `300X` and `400X` roots are recognised: other "Número Único" carrier prefixes in market use, such as `4020` and `4062`, are out of scope and are rejected. ```javascript import { isValidServicePhone } from '@brazilian-utils/brazilian-utils'; @@ -650,7 +650,7 @@ isValidServicePhone('11987654321'); // false (geographic number) Get the state (and its region) a Brazilian DDD (area code) belongs to, out of the 67 DDDs in use under the Anatel Plano Geral de Numeração. Accepts a string or a non-negative integer number, stripping any non-digit characters before matching. Exports the `AreaCodeInfo` type. -`stateCode` is always a single state: the one that holds all but a handful of the DDD's municipalities. Four DDDs straddle a state border, and for those `stateCodes` lists the other states too. DDD 61 is the widest of them, serving the Distrito Federal and the twelve Goiás municipalities of the Entorno do Distrito Federal (Águas Lindas de Goiás, Cabeceiras, Cidade Ocidental, Cristalina, Formosa, Luziânia, Novo Gama, Padre Bernardo, Planaltina, Santo Antônio do Descoberto, Valparaíso de Goiás and Vila Boa). The other three are 42, shared by Paraná and Porto União (SC), 47, shared by Santa Catarina and Rio Negro (PR), and 49, shared by Santa Catarina and Barracão (PR). +`stateCode` is always a single state: the one the DDD is seated in, the state of the city the code was allocated around, which is not necessarily the state holding most of its municipalities. Four DDDs straddle a state border, and for those `stateCodes` lists the other states too. DDD 61 is the widest of them, serving the Distrito Federal and the twelve Goiás municipalities of the Entorno do Distrito Federal (Águas Lindas de Goiás, Cabeceiras, Cidade Ocidental, Cristalina, Formosa, Luziânia, Novo Gama, Padre Bernardo, Planaltina, Santo Antônio do Descoberto, Valparaíso de Goiás and Vila Boa), so its `stateCode` is `'DF'` even though the Distrito Federal holds only one of its thirteen municipalities, Brasília. The other three are 42, shared by Paraná and Porto União (SC), 47, shared by Santa Catarina and Rio Negro (PR), and 49, shared by Santa Catarina and Barracão (PR), and there the seat does hold every municipality but the one named. ```javascript import { getAreaCodeInfo } from '@brazilian-utils/brazilian-utils'; @@ -770,7 +770,7 @@ parseCep('92500-000'); // 92500000 ### getAddressInfoByCep -Fetch address information for a given CEP using multiple providers. Defaults to `['viacep', 'brasilapi']`. The `'widenet'` provider is deprecated (its endpoint no longer responds) and excluded from the default list, but it can still be requested explicitly via `options.providers` (typed as `CepProvider[]`). The resolved address is typed as `AddressInfo`. A transient network failure is retried twice per provider, with a 250 ms linear backoff (250 ms, then 500 ms), so a provider that keeps failing is tried up to 3 times and adds about 750 ms before the next provider is reached; an HTTP error status or a non-retryable failure is not retried. +Fetch address information for a given CEP using multiple providers. Defaults to `['viacep', 'brasilapi']`. The `'widenet'` provider is deprecated (its endpoint no longer responds) and excluded from the default list, but it can still be requested explicitly via `options.providers` (typed as `CepProvider[]`). The resolved address is typed as `AddressInfo`. A transient network failure is retried twice per provider, with a 250 ms linear backoff (250 ms, then 500 ms), so a provider that keeps failing is tried up to 3 times and adds about 750 ms before its own failure lands; an HTTP error status or a non-retryable failure is not retried. The providers are started together and raced with `Promise.any`, not queried one after the other, so those retries delay nothing for the other providers, only the moment an all-failed rejection can surface. An `options.providers` that names no known provider rejects with `GetAddressInfoByCepValidationError` ("Nenhum provedor válido especificado"): an empty array, an array of unknown names, and a value that is not an array at all, `null` included. With `providers: ['brasilapi']`, a CEP BrasilAPI does not know rejects with `GetAddressInfoByCepNotFoundError`, since BrasilAPI signals a miss with HTTP 404; any other error status is still a `GetAddressInfoByCepServiceError`. ```javascript import { getAddressInfoByCep } from '@brazilian-utils/brazilian-utils'; @@ -823,7 +823,7 @@ parseProcessoJuridico('0002080-25.2012.5.15.0049'); // 00020802520125150049 ### isValidIe -Check if inscrição estadual (state registration) is valid. The state code is case-insensitive. Notable per-state rules: GO accepts prefixes `10`, `11` and `15`; PA accepts `15` and `75`-`79`; MS accepts `28` and `50`; SP has a produtor rural pattern `P0MMMSSSSD000`; TO uses 11-digit type codes (`01`, `02`, `03`, `99`). TO also accepts a 9-digit form, applying the same modulus 11 rule to the first eight digits; the SINTEGRA page documents only the 11-digit one, so that shape is 2.3.0 behaviour kept for compatibility rather than a published rule. An all-zero registration is accepted wherever the published formula yields a check digit of 0 for it (AM, BA with 8 or 9 digits, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, SC, SE, SP and TO with 9 digits), unlike `isValidCpf` and `isValidCnpj`, which reject repeated digits. +Check if inscrição estadual (state registration) is valid. The state code is case-insensitive. Notable per-state rules: GO accepts prefixes `10`, `11` and `15`; PA accepts `15` and `75`-`79`; MS accepts `28` and `50`; SP has a produtor rural pattern `P0MMMSSSSD000`; TO uses 11-digit type codes (`01`, `02`, `03`, `99`). TO also accepts a 9-digit form, applying the same modulus 11 rule to the first eight digits; the SINTEGRA page documents only the 11-digit one, so that shape is 2.3.0 behaviour kept for compatibility rather than a published rule. An all-zero registration is accepted wherever the published formula yields a check digit of 0 for it (AM, BA with 8 or 9 digits, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, SC, SE, SP and TO with 9 digits), unlike `isValidCpf` and `isValidCnpj`, which reject repeated digits. AM is on that list through the second branch of its published formula only: the page's first branch, `Se Soma < 11 Então Dígito = 11 - Soma`, gives 11 for an all-zero registration, while the `resto <= 1 ⇒ 0` branch, the one implemented here, gives 0. ```javascript import { isValidIe } from '@brazilian-utils/brazilian-utils'; @@ -840,11 +840,11 @@ Banks validated by their published check digit algorithm: | Bank | Code | Agency | Account | Notes | | --- | --- | --- | --- | --- | -| Banco do Brasil | `001` | 4-5 digits | 8-10 digits | mod11 with weights 9..2; `digit` may be `"X"` | +| Banco do Brasil | `001` | 4-5 digits | 8-10 digits | mod11 with weights 2..9 cycling from the right; `digit` may be `"X"` | | Santander | `033` | 4 digits | 8 digits | weights `9,7,3,1,0,0,9,7,1,3,1,9,7,3` over agency + `"00"` + account, tens discarded | | Banrisul | `041` | 4 digits | 9 digits | weights `3,2,4,7,6,5,4,3,2`; remainder 0 gives `0` and remainder 1 gives `6`; `account` is tipo (2 digits) + conta (7 digits) | | Caixa Econômica Federal | `104` | 4 digits | 11 digits | mod11 over agency + account; `account` is operação (3 digits) + conta (8 digits) | -| Bradesco | `237` | 4 digits | 7 digits | mod11 with weights 2..7; remainder 0 gives `0` and remainder 1 gives `"P"` | +| Bradesco | `237` | 4 digits | 7 digits | mod11 with weights 2..7 cycling from the right; remainder 0 gives `0` and remainder 1 gives `"P"` | | Nubank | `260` | 4 digits | 5-13 digits | Verhoeff check digit over the account, leading zeros dropped | | Itaú Unibanco | `341` | 4 digits | 5 digits | mod10 over agency + account | | HSBC / Kirton Bank | `399` | 4 digits | 6 digits | weights `8,9,2,3,4,5,6,7,8,9` over agency + account; remainder 10 gives `0` | @@ -1071,7 +1071,7 @@ capitalize(' josé maria '); // José Maria (every run of whitespace, tabs a ### formatCurrency -Formats an integer or float to a string in the BRL pattern. A `number` is formatted as-is (sign and decimals preserved). A `string` input is read by the same rule as `parseCurrency`, except that a value written without any separator stays in whole units: the last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator, every other `,` or `.` is a thousands separator, and a `-` written before the first digit is preserved. So `'1.234,56'` formats as `1.234,56`, `'-10.5'` as `-10,50` and `'1234'` as `1.234,00`. `precision` is clamped to `0..20` (the range `Intl.NumberFormat` accepts), defaults to 2, and falls back to 2 when it is not a finite number. A value that is not a finite number (`NaN`, `Infinity`, `-Infinity`) formats as an empty string, and so does a value that cannot be coerced to a number (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. Options are typed as `FormatCurrencyOptions`. +Formats an integer or float to a string in the BRL pattern. A `number` is formatted as-is (sign and decimals preserved). A `string` input is read by the same rule as `parseCurrency`, except that a value written without any separator stays in whole units: the last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator, every other `,` or `.` is a thousands separator, and a `-` written before the first digit is preserved. So `'1.234,56'` formats as `1.234,56`, `'-10.5'` as `-10,50` and `'1234'` as `1.234,00`. `precision` is clamped to `0..20` (the package limit, the bound Node 20 still enforces on `Intl.NumberFormat`), defaults to 2, and falls back to 2 when it is not a finite number. A value that is not a finite number (`NaN`, `Infinity`, `-Infinity`) formats as an empty string, and so does a value that cannot be coerced to a number (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. Options are typed as `FormatCurrencyOptions`. ```javascript import { formatCurrency } from '@brazilian-utils/brazilian-utils'; @@ -1178,7 +1178,7 @@ getStates(); ### getStateByIbgeCode -Get the Brazilian state whose 2-digit IBGE code ("cUF", the Código da Unidade da Federação) matches the given value. This is the same 2-digit UF code found in the first field of every DF-e access key (chave de acesso) issued for NF-e, NFC-e, CT-e and MDF-e documents. Accepts a string or a non-negative integer number, stripping any non-digit characters before matching. Exports the `State` type. +Get the Brazilian state whose 2-digit IBGE code ("cUF", the Código da Unidade da Federação) matches the given value. This is the same 2-digit UF code found in the first field of every DF-e access key (chave de acesso) issued for any of the models `isValidNfeKey` covers: NF-e (55), NFC-e (65), CT-e (57), MDF-e (58), CT-e OS (67), GTV-e (64), BP-e (63), NF3e (66) and NFCom (62). Accepts a string or a non-negative integer number, stripping any non-digit characters before matching. Exports the `State` type. ```javascript import { getStateByIbgeCode } from '@brazilian-utils/brazilian-utils'; @@ -1236,7 +1236,7 @@ getTimezoneByState('ZZ'); // null ### getCities -Get Brazilian cities. Returns all cities if no state is provided, or cities from a specific state. Each call returns a fresh array, so mutating the result never affects subsequent calls. An unknown state code (or a non-`StateCode` value) returns an empty array instead of throwing, except for a falsy one: `getCities(null)` and `getCities('')` are read as "no state given" and return every city, where the stricter `getMunicipalities` returns `[]` for them. +Get Brazilian cities. Returns all cities if no state is provided, or cities from a specific state. Each call returns a fresh array, so mutating the result never affects subsequent calls. An unknown state code (or a non-`StateCode` value) returns an empty array instead of throwing, except for a falsy one: `getCities(null)` and `getCities('')` are read as "no state given" and return every city, where the stricter `getMunicipalities` returns `[]` for them. The state code is matched exactly, case included: `getCities('sp')` returns `[]` where `getCities('SP')` returns the 645 São Paulo cities. `getCities` and `getMunicipalities` are the only state-taking lookups that are case-sensitive; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` and `getMunicipality` all fold case. ```javascript import { getCities } from '@brazilian-utils/brazilian-utils'; @@ -1274,7 +1274,7 @@ getCities('SP'); // ] ``` -`getCities` embeds all 5571 IBGE municipality names (~153.6 KB minified, ~49.4 KB gzipped) and is one of the few heavy exceptions in this otherwise tree-shakeable package. See [Bundle size](getting-started.md#bundle-size) for how to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities` instead of the root import. +`getCities` embeds all 5571 IBGE municipality names (~154.0 KB minified, ~49.7 KB gzipped) and is one of the few heavy exceptions in this otherwise tree-shakeable package. See [Bundle size](getting-started.md#bundle-size) for how to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities` instead of the root import. ### getHolidays @@ -1282,7 +1282,7 @@ Get Brazilian holidays for a given year. Returns national holidays and optionall Only one state holiday per UF is a feriado civil under [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm), art. 1º, II, which authorises "a data magna do Estado fixada em lei estadual" in the singular; the other entries rest on ordinary state laws and are reported because they are observed in practice. Notable per-state rules: -- **SC** — [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) moves both state holidays, "Dia do Estado de Santa Catarina" (Aug 11) and "Dia de Santa Catarina de Alexandria" (Nov 25), to the following Sunday whenever they fall Monday to Friday, so Monday Aug 11 2025 is a business day in SC and the holiday lands on Sunday Aug 17. The transfer starts in 2005, the year [Lei SC nº 13.408/2005](http://leis.alesc.sc.gov.br/html/2005/13408_2005_lei.html) first introduced it (published and in force on Jul 15 2005); up to 2004 both holidays stay on Aug 11 and Nov 25 whatever weekday they fall on. +- **SC** — [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) moves both state holidays, "Dia do Estado de Santa Catarina" (Aug 11) and "Dia de Santa Catarina de Alexandria" (Nov 25), to the following Sunday whenever they fall Monday to Friday, so Monday Aug 11 2025 is a business day in SC and the holiday lands on Sunday Aug 17. The two dates did not start transferring together. Aug 11 transfers from 2005 on, the year [Lei SC nº 13.408/2005](http://leis.alesc.sc.gov.br/html/2005/13408_2005_lei.html) extended the clause to it (published and in force on Jul 15 2005), and stays on Aug 11 before that. Nov 25 transfers from 1999 on, the year [Lei SC nº 11.213/1999](http://leis.alesc.sc.gov.br/html/1999/11213_1999_lei.html) first introduced the clause (published and in force on Nov 12 1999, thirteen days before that year's Nov 25), with a one-year gap: art. 3º of [Lei SC nº 12.906/2004](http://leis.alesc.sc.gov.br/html/2004/12906_2004_lei.html) revoked that law without restating the clause, so Nov 25 2004 alone stays on the statutory date until Lei SC nº 13.408/2005 reinstated the transfer. So Nov 25 1999 (a Thursday) lands on Sunday Nov 28, Nov 25 2002 (a Monday) on Sunday Dec 1, Nov 25 2004 (a Thursday) stays put, and Nov 25 2005 (a Friday) lands on Sunday Nov 27. - **DF** — [Lei distrital nº 72/1989](https://www.sinj.df.gov.br/sinj/Norma/18459/Lei_72_27_12_1989.html), art. 1º parágrafo único, declares Corpus Christi a feriado. With `stateCode: 'DF'` the single Corpus Christi entry comes back typed `"state"` instead of `"optional"`; it is replaced, not duplicated. - **GO** — [Lei GO nº 20.756/2020](https://legisla.casacivil.go.gov.br/pesquisa_legislacao/100979/lei-20756), art. 269, II, lists three feriados estaduais: Jul 26 (Fundação da Cidade de Goiás), Oct 24 (Lançamento da Pedra Fundamental de Goiânia) and Oct 28 (Dia do Servidor Público). - **AL** — Sep 16 is a feriado estadual from 2024 ([Lei AL nº 9.358/2024](https://sapl.al.al.leg.br/norma/3117)) and only a ponto facultativo (`"optional"`) before that. @@ -1312,7 +1312,7 @@ getHolidays({ year: 2024, stateCode: 'SP' }); ### isValidPassport -Check if a Brazilian passport number is valid (2 letters followed by 6 digits). Accepts both `string` and `number` input; the input is case-insensitive and any non-alphanumeric characters (spaces, dots, hyphens) are ignored. +Check if a Brazilian passport number is valid (2 letters followed by 6 digits). Accepts both `string` and `number` input; the input is case-insensitive and any non-alphanumeric characters (spaces, dots, hyphens) are ignored. A number is accepted for symmetry with `formatPassport`/`parsePassport` but is never valid: the decimal form of a number never starts with the two letters a passport number needs. ```javascript import { isValidPassport } from '@brazilian-utils/brazilian-utils'; @@ -1410,7 +1410,7 @@ parseCnh('026503064-61'); // '02650306461' ### getCepInfoByAddress -Fetch CEPs from an address using ViaCEP. Throws `GetCepInfoByAddressValidationError` when the UF, city or street is missing/invalid, `GetCepInfoByAddressNotFoundError` when no address matches the query, and `GetCepInfoByAddressError` when ViaCEP itself answers with an HTTP error status. A request that cannot be performed at all (a transport failure) rejects with the underlying `fetch` error instead. +Fetch CEPs from an address using ViaCEP. Throws `GetCepInfoByAddressValidationError` when the UF, city or street is missing/invalid — including when the argument is not an object at all (omitted, `null`, a string) and when `federalUnit` is not a string, neither of which leaks a raw `TypeError` — `GetCepInfoByAddressNotFoundError` when no address matches the query, and `GetCepInfoByAddressError` when ViaCEP itself answers with an HTTP error status. A request that cannot be performed at all (a transport failure) rejects with the underlying `fetch` error instead. ```javascript import { getCepInfoByAddress } from '@brazilian-utils/brazilian-utils'; @@ -1546,6 +1546,8 @@ generateLicensePlate(); // 'ABC1D23' (Mercosul, the default) generateLicensePlate('LLLNNNN'); // 'ABC1234' ``` +A `format` string outside the two supported literals is not rejected: it is used verbatim, character by character, with `L` producing a letter and every other position a digit. So `generateLicensePlate('LLLNNLN')` returns a plate in the withdrawn motorcycle sequence, which `isValidLicensePlate` rejects; `generateLicensePlate('bogus')` returns five digits; and `generateLicensePlate('')` returns an empty string. Only a non-string falls back to the Mercosul default. This is the 2.3.0 behaviour, kept for the JavaScript callers the TypeScript type cannot reach. + ### getFormatLicensePlate Detect the normalized format of a license plate. @@ -1586,7 +1588,7 @@ convertLicensePlateToMercosul('ABC1D23'); // '' (already Mercosul) ### generatePis -Generate a valid random PIS. +Generate a valid random PIS. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generatePis } from '@brazilian-utils/brazilian-utils'; @@ -1642,7 +1644,7 @@ const lookUp = (options: GetMunicipalityOptions) => getMunicipality(options); ### getMunicipalities -Get Brazilian municipalities published by the IBGE. Returns all municipalities if no state is provided, or municipalities from a specific state. Each municipality is returned as `{ code, name, stateCode }`, where `code` is the 7-digit IBGE municipality code. Results are sorted by name with `localeCompare` in the "pt-BR" locale. Each call returns a fresh array of fresh objects, so mutating the result never affects subsequent calls. An unknown state code returns an empty array instead of throwing. Only an omitted (or `undefined`) `stateCode` asks for the full list: `getMunicipalities(null)` and `getMunicipalities('')` return `[]`, where the looser `getCities(null)` and `getCities('')` return every city. +Get Brazilian municipalities published by the IBGE. Returns all municipalities if no state is provided, or municipalities from a specific state. Each municipality is returned as `{ code, name, stateCode }`, where `code` is the 7-digit IBGE municipality code. Results are sorted by name with `localeCompare` in the "pt-BR" locale. Each call returns a fresh array of fresh objects, so mutating the result never affects subsequent calls. An unknown state code returns an empty array instead of throwing. Only an omitted (or `undefined`) `stateCode` asks for the full list: `getMunicipalities(null)` and `getMunicipalities('')` return `[]`, where the looser `getCities(null)` and `getCities('')` return every city. The state code is matched exactly, case included: `getMunicipalities('sp')` returns `[]` where `getMunicipalities('SP')` returns the 645 São Paulo municipalities. `getMunicipalities` and `getCities` are the only state-taking lookups that are case-sensitive; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` and `getMunicipality` all fold case. ```javascript import { getMunicipalities } from '@brazilian-utils/brazilian-utils'; @@ -1705,7 +1707,7 @@ isHoliday(); // false ### isBusinessDay -Check if a date is a Brazilian business day (dia útil). Returns `false` for Saturdays, Sundays, and Brazilian holidays returned by `getHolidays` for `value`'s local calendar day (year/month/day as read locally), the same convention used by `isHoliday`. `options.includeOptional` (part of `BusinessDayOptions`, the option type every business day utility shares) defaults to `true`, so optional-type holidays (`Holiday.type === "optional"`, i.e. Carnaval and Corpus Christi) also count as non-business days; pass `false` to only treat statutory holidays this way. `options.stateCode` also considers that state's holidays; an unknown/invalid `stateCode` is ignored, falling back to national holidays only. A `value` that is not a valid `Date` returns `false`. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `false`. +Check if a date is a Brazilian business day (dia útil). Returns `false` for Saturdays, Sundays, and Brazilian holidays returned by `getHolidays` for `value`'s local calendar day (year/month/day as read locally), the same convention used by `isHoliday`. `options.includeOptional` (part of `BusinessDayOptions`, the option type every business day utility shares) defaults to `true`, so optional-type holidays (`Holiday.type === "optional"`, i.e. Carnaval and Corpus Christi) also count as non-business days; pass `false` to only treat statutory holidays this way. `options.stateCode` also considers that state's holidays; a string that is not a known state code is ignored, falling back to national holidays only, while a `stateCode` that is present and is not a string at all (a number, `null`, an object) is rejected and makes the call return `false` even for an ordinary weekday, the same split `isHoliday` makes and the value `addBusinessDays`, `subBusinessDays` and `differenceInBusinessDays` reject with `null`. A `value` that is not a valid `Date` returns `false`. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `false`. ```javascript import { isBusinessDay } from '@brazilian-utils/brazilian-utils'; @@ -1835,7 +1837,7 @@ parseVoterId('1234 5678 8 01 91'); // '1234567880191' (13-digit SP/MG voter id) ### isValidCns -Check if a CNS (Cartão Nacional de Saúde) number is valid, the unique SUS (Sistema Único de Saúde) user identifier. Definitive cards (starting with 1 or 2) are validated with the same mod 11 weighting used for PIS numbers over an embedded 11 digit base, adjusting the base by +2 when the raw check digit computes to 10. Provisional cards (starting with 7, 8 or 9) are validated instead by a single weighted sum (weights 15 down to 1) that must be a multiple of 11. The value has to be written as the 15 digits, optionally split into the printed groups of 3-4-4-4 by whitespace or the usual mask characters; letters among the digits are rejected instead of being read past. +Check if a CNS (Cartão Nacional de Saúde) number is valid, the unique SUS (Sistema Único de Saúde) user identifier. Definitive cards (starting with 1 or 2) are validated over an embedded 11 digit PIS/PASEP/NIS derived base weighted 15 down to 5; when the raw digit computes to 10, DATASUS raises the weighted sum by 2, recomputes the digit and marks the card with the suffix `001` instead of `000`. Provisional cards (starting with 7, 8 or 9) are validated instead by a single weighted sum (weights 15 down to 1) that must be a multiple of 11. The value has to be written as the 15 digits, optionally split into the printed groups of 3-4-4-4 by whitespace or the usual mask characters, a run of them between two groups included; letters among the digits are rejected instead of being read past. The two routines come from the [ANVISA CNS validation page](https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/), which sits behind a bot filter and answers HTTP 403 to non-browser clients. The [e-SUS APS page](https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html) documents the same algorithm and is reachable without a browser, but applies the provisional routine to numbers starting with 5, 7, 8 or 9; this implementation follows ANVISA and rejects a 5-prefixed number even when its weighted sum checks out. @@ -1880,7 +1882,7 @@ isValidCertidao('104539 01 55 2013 1 00012 021 0000123 21', { accept: ['death'] ### parseCertidao -Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. [Art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lists the codes 1 to 7; the codes 8 (emancipação) and 9 (interdição) come from Anexo IV of the revoked Provimento CNJ nº 63/2017, as listed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm), and are kept because matrículas issued under it are still in circulation. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. +Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. [Art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lists the codes 1 to 7; no CNJ primary text reachable today publishes the other two, the Anexo IV of the revoked Provimento CNJ nº 63/2017 included, which lists the same seven. The codes 8 (emancipação) and 9 (interdição) come from the references the check digit rule rests on: [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) both print the nine book list. They are kept because matrículas carrying them circulate. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. ```javascript import { parseCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1931,7 +1933,7 @@ formatCertidao('1552010100020112000012087', { pad: true }); // 000000 01 55 2010 ### isValidCei -Check if a CEI (Cadastro Específico do INSS) number is valid. The CEI identifies an employer with no CNPJ, such as a construction work or a rural producer: 12 digits printed as `00.000.00000/00`, the last one a check digit calculated over the 11 base digits with the weights 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 and 4. Accepts the usual mask characters and whitespace between/around groups. The Receita Federal does not publish this check digit rule, so it follows the reference implementations of [yii2-br-validator](https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php) and [Bigai.Documentos.Brasil](https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs), cross-checked against the [Cadastro Nacional de Obras (CNO) open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) of the Receita Federal. +Check if a CEI (Cadastro Específico do INSS) number is valid. The CEI identifies an employer with no CNPJ, such as a construction work or a rural producer: 12 digits printed as `00.000.00000/00`, the last one a check digit calculated over the 11 base digits with the weights 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 and 4. Accepts the usual mask characters and whitespace between/around groups, a run of them between two groups included. The Receita Federal does not publish this check digit rule, so it follows the reference implementations of [yii2-br-validator](https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php) and [Bigai.Documentos.Brasil](https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs), cross-checked against the [Cadastro Nacional de Obras (CNO) open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) of the Receita Federal. ```javascript import { isValidCei } from '@brazilian-utils/brazilian-utils'; @@ -1983,7 +1985,7 @@ formatCno('979', { pad: true }); // 00.000.00009/79 ### isValidCaepf -Check if a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number is valid. The CAEPF replaced the CEI for individuals who hire employees: 14 digits printed as `000.000.000/000-00`, formed by the 9 digit CPF base of the holder, a 3 digit sequence for the holder's several registrations and 2 check digits. Both check digits use the modulus 11 of the CNPJ, and the resulting pair is then shifted by 12, wrapping around 100. The Receita Federal does not publish the layout or the check digit rule: both are described by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented the same way by [brazilian-values](https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts). +Check if a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number is valid. The CAEPF replaced the CEI for individuals who hire employees: 14 digits printed as `000.000.000/000-00`, formed by the 9 digit CPF base of the holder, a 3 digit sequence for the holder's several registrations and 2 check digits. Both check digits are the CNPJ's modulus 11 in the formulation of the cited reference: the weights cycle from 9 down to 2 from the right and the check digit is the remainder itself, with a remainder of 10 read as 0 — the same digit the CNPJ's 2-to-9 weights with `11 - remainder` produce. The resulting pair is then shifted by 12, wrapping around 100. A base whose 12 digits are all the same is rejected before the check digits are computed, the way `isValidCei` and `isValidCno` reject a repeated CEI/CNO number, so the otherwise well-formed `00000000000012` is invalid. The Receita Federal does not publish the layout or the check digit rule: both are described by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented the same way by [brazilian-values](https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts). ```javascript import { isValidCaepf } from '@brazilian-utils/brazilian-utils'; @@ -1992,7 +1994,8 @@ isValidCaepf('293.118.610/001-84'); // true isValidCaepf('41142260000101'); // true isValidCaepf(29311861000184); // true isValidCaepf('29311861000185'); // false (invalid check digits) -isValidCaepf('00000000000000'); // false (repeated digits) +isValidCaepf('00000000000000'); // false (invalid check digits) +isValidCaepf('00000000000012'); // false (repeated base digits) ``` ### formatCaepf @@ -2139,7 +2142,7 @@ formatNcm(-84713012); // '' (not a non-negative safe integer) ### isValidCfop -Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table. The table is the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), the text in force (current wording given by Ajuste SINIEF 03/24, last amended by Ajuste SINIEF 39/25), not the frozen 2001 text of Ajuste SINIEF 07/01. Only operable codes count: the group and subgroup headings of the official nomenclature, the codes ending in `00` and `50` (1000, 1100, 1150, 5350, ...), are section titles rather than codes a document can carry, so they are rejected. +Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table. The table is the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), the text in force (current wording given by Ajuste SINIEF 03/24, last amended by [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25)), not the frozen 2001 text of Ajuste SINIEF 07/01. Only operable codes count: the group and subgroup headings of the official nomenclature, the codes ending in `00` and `50` (1000, 1100, 1150, 5350, ...), are section titles rather than codes a document can carry, so they are rejected. A string is only read as a code when it is written in one of the documented forms (the 4 digits, or the `N.NNN` form the annex prints, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. @@ -2157,7 +2160,7 @@ isValidCfop(-5102); // false (not a non-negative safe integer) ### getCfop -Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description, as the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24) words it. The group and subgroup headings of the official nomenclature, the codes ending in `00` and `50`, are not in the table and give `null`. Same input rules as `isValidCfop`. +Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description, as the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24) words it, in the text in force, last amended by [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25). The group and subgroup headings of the official nomenclature, the codes ending in `00` and `50`, are not in the table and give `null`. Same input rules as `isValidCfop`. ```javascript import { getCfop } from '@brazilian-utils/brazilian-utils'; @@ -2182,9 +2185,9 @@ Check if a CST (Código de Situação Tributária) code is valid for a given tax `options.tax` (part of `IsValidCstOptions`) is optional: omit it to accept a code that exists in any one of the four tables above. -The ICMS Tabela B is the one in force: the [consolidated Anexo I of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), whose current wording came from [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (effective 01.12.23) and which [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) amended by revoking items 12, 13, 52, 72 and 74 (effective 09.07.24). `02`, `15`, `53` and `61` are its monofasia de combustíveis codes. +The ICMS Tabela B is the one in force: the [consolidated Anexo I of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), whose current wording came from [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (effective 01.12.23) and which [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) amended by striking items 12, 13, 52, 72 and 74 (effects from 09.07.24) before they ever took effect: 39/23 had added them "sem efeitos", so those codes were never in force. `02`, `15`, `53` and `61` are its monofasia de combustíveis codes. -A string is only read as a code when it is written in one of the documented forms (the 2 or 3 digits, with a single separator between them and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +A string is only read as a code when it is written in one of the documented forms (the 2 digits of a Tabela B code, or the 3 digits of the ICMS form with an optional single separator after the origin digit, plus optional surrounding whitespace), and a number only when it is a non-negative safe integer. The origin digit is the only boundary a printed CST has, so `'0 10'` and `'1-10'` are read while `'0-0'`, `'11-0'` and `'00-'` are not. ```javascript import { isValidCst } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/llms.txt b/docs/llms.txt index f4f750cf..2d9dac25 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -35,7 +35,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [isValidPhone](https://brazilian-utils.com.br/utilities.md#isvalidphone): Check if phone number (mobile or landline) is valid. - [isValidMobilePhone](https://brazilian-utils.com.br/utilities.md#isvalidmobilephone): Check if mobile phone number is valid. - [isValidLandlinePhone](https://brazilian-utils.com.br/utilities.md#isvalidlandlinephone): Check if landline phone number is valid. -- [isValidServicePhone](https://brazilian-utils.com.br/utilities.md#isvalidservicephone): Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`; `112` and `911` are accepted too, as mobile-only aliases of `190` that Anatel lists alongside the other 3-digit codes). +- [isValidServicePhone](https://brazilian-utils.com.br/utilities.md#isvalidservicephone): Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`), whose consolidated table is the Anexo of Ato Anatel nº 43.151/2004. - [isValidLicensePlate](https://brazilian-utils.com.br/utilities.md#isvalidlicenseplate): Check if license plate is valid. - [isValidRenavam](https://brazilian-utils.com.br/utilities.md#isvalidrenavam): Check if RENAVAM (Registro Nacional de Veículos Automotores) is valid. - [isValidPis](https://brazilian-utils.com.br/utilities.md#isvalidpis): Check if PIS is valid. @@ -149,7 +149,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [getMunicipalityByCode](https://brazilian-utils.com.br/utilities.md#getmunicipalitybycode): Look up a Brazilian municipality by its 7-digit IBGE code. - [getCbo](https://brazilian-utils.com.br/utilities.md#getcbo): Look a CBO (Classificação Brasileira de Ocupações) code up and get its official occupation title. - [getCnae](https://brazilian-utils.com.br/utilities.md#getcnae): Look a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up and get its formatted code and official description. -- [getCfop](https://brazilian-utils.com.br/utilities.md#getcfop): Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description, as the consolidated Anexo II of Convênio SINIEF s/nº 1970 words it. +- [getCfop](https://brazilian-utils.com.br/utilities.md#getcfop): Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description, as the consolidated Anexo II of Convênio SINIEF s/nº 1970 words it, in the text in force, last amended by Ajuste SINIEF 39/25. ## Other utilities diff --git a/docs/pt-br/getting-started.md b/docs/pt-br/getting-started.md index 0502032f..df828bbe 100644 --- a/docs/pt-br/getting-started.md +++ b/docs/pt-br/getting-started.md @@ -63,19 +63,19 @@ Você pode conferir a lista de utilitários [clicando aqui](utilities.md). ## Tamanho do bundle -O pacote é tree-shakeable: importar um utilitário da raiz traz apenas o código daquele utilitário, não o resto da biblioteca. `isValidCpf`, por exemplo, adiciona cerca de 0,7 KB minificado ao seu bundle. Um bundler com suporte a tree-shaking (webpack, Rollup, esbuild, Vite etc.) descarta todos os outros utilitários. +O pacote é tree-shakeable: importar um utilitário da raiz traz apenas o código daquele utilitário, não o resto da biblioteca. `isValidCpf`, por exemplo, adiciona cerca de 1,2 KB minificado (0,6 KB com gzip) ao seu bundle. Um bundler com suporte a tree-shaking (webpack, Rollup, esbuild, Vite etc.) descarta todos os outros utilitários. Alguns utilitários são a exceção: cada um embute um dataset oficial e pesa muito mais que todos os outros utilitários somados. Estes são os tamanhos de um import isolado, minificado e com gzip: | Utilitário | Dataset | Minificado | Gzip | | --- | --- | --- | --- | -| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 municípios do IBGE, com nomes e códigos | 155,9 KB | 50,0 KB | -| `getCities` | nomes dos 5571 municípios do IBGE | 153,6 KB | 49,4 KB | -| `isValidNcm` | códigos NCM (Nomenclatura Comum do Mercosul) | 113,4 KB | 24,0 KB | -| `isValidCbo` · `getCbo` | títulos das ocupações da CBO 2002 | 118,4 KB | 30,2 KB | -| `isValidCnae` · `getCnae` | subclasses da CNAE 2.3 | 93,6 KB | 21,1 KB | -| `isValidCfop` · `getCfop` | descrições das operações do CFOP | 68,3 KB | 6,5 KB | -| `getBanks` · `getBankByCode` | participantes do STR do Banco Central (COMPE + ISPB) | 37,9 KB | 9,3 KB | +| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 municípios do IBGE, com nomes e códigos | 156,2 KB | 50,2 KB | +| `getCities` | nomes dos 5571 municípios do IBGE | 154,0 KB | 49,7 KB | +| `isValidNcm` | códigos NCM (Nomenclatura Comum do Mercosul) | 113,8 KB | 24,3 KB | +| `isValidCbo` · `getCbo` | títulos das ocupações da CBO 2002 | 118,8 KB | 30,4 KB | +| `isValidCnae` · `getCnae` | subclasses da CNAE 2.3 | 94,0 KB | 21,3 KB | +| `isValidCfop` · `getCfop` | descrições das operações do CFOP | 68,7 KB | 6,8 KB | +| `getBanks` · `getBankByCode` | participantes do STR do Banco Central (COMPE + ISPB) | 38,3 KB | 9,6 KB | Importar qualquer um deles da raiz, mesmo ao lado de um único utilitário pequeno, traz todo esse dataset para o seu bundle principal, porque este pacote é publicado como um único módulo ESM: um `import()` dinâmico da raiz (`await import('@brazilian-utils/brazilian-utils')`) ainda resolve para esse mesmo arquivo único, então não há como separá-lo sozinho. Um bundler que faz code-splitting precisa de um módulo separado para separar. @@ -97,4 +97,4 @@ getMunicipalityByCode('3550308'); Todos os utilitários estão disponíveis dessa forma, como `@brazilian-utils/brazilian-utils/` (kebab-case, seguindo o nome da função: `isValidCpf` → `is-valid-cpf`), pelo mesmo motivo de lazy-loading/code-splitting. -Escolha um estilo por utilitário em cada aplicação: um bundler trata o import da raiz e o import do subpath como dois módulos independentes, então importar `getCities` tanto da raiz quanto de `/get-cities` na mesma aplicação inclui a tabela de 153,6 KB de cidades duas vezes, uma em cada módulo. +Escolha um estilo por utilitário em cada aplicação: um bundler trata o import da raiz e o import do subpath como dois módulos independentes, então importar `getCities` tanto da raiz quanto de `/get-cities` na mesma aplicação inclui a tabela de 154,0 KB de cidades duas vezes, uma em cada módulo. diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index e7e109b3..408cde9c 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -2,7 +2,7 @@ Aqui você encontrará todos os utilitários disponíveis para uso. -> **Tratamento de entrada:** nenhuma função pública síncrona lança exceção com `null`/`undefined` ou um valor de tipo incorreto; as duas funções de rede, `getAddressInfoByCep` e `getCepInfoByAddress`, rejeitam com seus erros tipados (veja as seções delas). Os validadores (`isValid*`) retornam `false`; `isHoliday` retorna `false`; `getHolidays` retorna `[]`; `generateProcessoJuridico` retorna `null`; `getMunicipality` retorna `null` para uma busca malformada/sem correspondência. Todas as demais funções `format*`/`parse*` (incluindo `capitalize`) retornam um valor vazio do seu tipo de retorno: `""` para strings, `0` para `parseCurrency`. `formatCurrency` retorna `""` para um número não finito e para um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo); `null`, arrays e booleanos passam por `Number()` como no 2.3.0. A única exceção à promessa acima: um objeto criado com `Object.create(null)` não tem `toString`, então as funções `format*`/`parse*` que leem a entrada como texto ainda lançam um `TypeError` para ele, exatamente como na 2.3.0. +> **Tratamento de entrada:** nenhuma função pública síncrona lança exceção com `null`/`undefined` ou um valor de tipo incorreto; as duas funções de rede, `getAddressInfoByCep` e `getCepInfoByAddress`, rejeitam com seus erros tipados (veja as seções delas). Os validadores (`isValid*`) retornam `false`; `isHoliday` retorna `false`; `getHolidays` retorna `[]`; `generateProcessoJuridico` retorna `null`; `getMunicipality` retorna `null` para uma busca malformada/sem correspondência. Todas as demais funções `format*`/`parse*` retornam um valor vazio do seu tipo de retorno: toda função `format*`, `capitalize`, e as funções `parse*` que retornam string (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) retornam `""`; `parseCurrency` retorna `0`; os parsers que retornam objeto/tupla — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — retornam `null`. `formatCurrency` retorna `""` para um número não finito e para um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo); `null`, arrays e booleanos passam por `Number()` como no 2.3.0. A única exceção à promessa acima: um objeto criado com `Object.create(null)` não tem `toString`, então as funções `format*`/`parse*` que leem a entrada como texto ainda lançam um `TypeError` para ele, exatamente como na 2.3.0. ## isValidCpf @@ -17,7 +17,7 @@ isValidCpf('111 444 777 35'); // true (máscara com espaços) ## formatCpf -Formata o CPF. `options.obfuscate` (parte de `FormatCpfOptions`) esconde os 3 primeiros dígitos e os 2 dígitos verificadores (`***.456.789-**`), a convenção de exibição do gov.br / Receita Federal, aplicada após o `pad`. +Formata o CPF. `options.obfuscate` (parte de `FormatCpfOptions`) esconde os 3 primeiros dígitos e os 2 dígitos verificadores (`***.456.789-**`), a convenção de exibição do gov.br / Receita Federal, aplicada após o `pad`. É lida por veracidade (truthiness), do mesmo jeito que o `pad`, então qualquer valor verdadeiro esconde os dígitos. ```javascript import { formatCpf } from '@brazilian-utils/brazilian-utils'; @@ -61,7 +61,7 @@ isValidCnpj('q0slfmbd7vx439', { version: 2 }); // true (alfanumérico minúsculo ## formatCnpj -Formata o CNPJ. `options.obfuscate` (parte de `FormatCnpjOptions`) esconde os 2 primeiros dígitos e os 2 dígitos verificadores (`**.345.678/0001-**`), a convenção de exibição do gov.br / Receita Federal. Vale para as duas versões e é aplicada após o `pad`. +Formata o CNPJ. `options.obfuscate` (parte de `FormatCnpjOptions`) esconde os 2 primeiros dígitos e os 2 dígitos verificadores (`**.345.678/0001-**`), a convenção de exibição do gov.br / Receita Federal. Vale para as duas versões, é aplicada após o `pad` e é lida por veracidade (truthiness), do mesmo jeito que o `pad`, então qualquer valor verdadeiro esconde os dígitos. ```javascript import { formatCnpj } from '@brazilian-utils/brazilian-utils'; @@ -101,7 +101,7 @@ isValidCep('12345'); // false (tamanho inválido) ## generateCnpj -Gera um CNPJ válido aleatório. +Gera um CNPJ válido aleatório. Usa `Math.random()` internamente, então não é criptograficamente seguro. ```javascript import { generateCnpj } from '@brazilian-utils/brazilian-utils' @@ -157,7 +157,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ## getBoletoInfo -Extrai informações de um boleto (valor, data de vencimento, código do banco). Aceita opcionalmente `{ referenceDate }` (tipado como `GetBoletoInfoOptions`) para resolver o ciclo do "fator de vencimento" a partir de uma data específica em vez de agora (o ciclo do fator reiniciou em 22/02/2025, segundo a FEBRABAN). Nem a FEBRABAN nem o Banco Central publicam uma forma de distinguir um fator do ciclo antigo de um do ciclo novo, então todo fator resolve para uma de duas datas separadas por 9000 dias e o `referenceDate` escolhe entre elas por meio das janelas de segurança da própria biblioteca: o mesmo boleto pode passar a resolver para a outra candidata com o tempo, então informe `referenceDate` explicitamente sempre que a resposta precisar ser estável. Para um boleto de arrecadação, o resultado, tipado como `BoletoInfo`, continua trazendo as duas chaves, porém vazias, `bankCode: ''` e `expirationDate: null`, já que o boleto não tem código de banco nem fator de vencimento, e acrescenta `type: "arrecadacao"`, `segment`, `value` e `hasEffectiveValue`. +Extrai informações de um boleto (valor, data de vencimento, código do banco). Aceita opcionalmente `{ referenceDate }` (tipado como `GetBoletoInfoOptions`) para resolver o ciclo do "fator de vencimento" a partir de uma data específica em vez de agora (o ciclo do fator reiniciou em 22/02/2025, segundo a FEBRABAN). Nem a FEBRABAN nem o Banco Central publicam uma forma de distinguir um fator do ciclo antigo de um do ciclo novo, então todo fator resolve para uma de duas datas separadas por 9000 dias e o `referenceDate` escolhe entre elas por meio das janelas de segurança da própria biblioteca: o mesmo boleto pode passar a resolver para a outra candidata com o tempo, então informe `referenceDate` explicitamente sempre que a resposta precisar ser estável. A busca de ciclo nunca desce abaixo do primeiro ciclo, então um `referenceDate` anterior ao próprio esquema ainda resolve um fator para a data mais antiga que aquele fator consegue representar, em vez de uma anterior à data-base de 07/10/1997. Para um boleto de arrecadação, o resultado, tipado como `BoletoInfo`, continua trazendo as duas chaves, porém vazias, `bankCode: ''` e `expirationDate: null`, já que o boleto não tem código de banco nem fator de vencimento, e acrescenta `type: "arrecadacao"`, `segment`, `value` e `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -209,7 +209,7 @@ parsePixKey('+5551998259765'); // { type: 'phone', value: '+5551998259765' } ## isValidPixPayload -Valida se um payload de BR Code Pix (a string por trás de um QR Code Pix e do "Pix copia e cola") é válido: estrutura TLV bem formada, objetos obrigatórios presentes, um dos templates "Merchant Account Information" carregando o GUI `br.gov.bcb.pix` junto com uma chave ou uma URL, e um CRC-16 que confere. O objeto "Point of Initiation Method" (`01`) é informativo: o Manual do BR Code o marca como opcional e só atribui significado ao valor `"12"` ("só pode ser utilizado uma vez"), então ele pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` torna o payload inválido. Quando um payload construído em torno de uma chave traz um valor (`54`), esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque, ou seja, a menos que traga o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`) como prescreve o §2.6 do manual do Pix; rejeitar `"0"`/`"0.00"` sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. A chave em si não é validada contra os formatos do DICT, use `isValidPixKey` para isso. Payloads que trazem a localização em um Unreserved Template (IDs 80 a 99), como o "QR Code composto" do Pix Automático (Pix recorrente), estão fora de escopo e são considerados inválidos. +Valida se um payload de BR Code Pix (a string por trás de um QR Code Pix e do "Pix copia e cola") é válido: estrutura TLV bem formada, objetos obrigatórios presentes, um dos templates "Merchant Account Information" carregando o GUI `br.gov.bcb.pix` junto com uma chave ou uma URL, e um CRC-16 que confere. O objeto "Point of Initiation Method" (`01`) é informativo: o Manual do BR Code o marca como opcional e só atribui significado ao valor `"12"` ("só pode ser utilizado uma vez"), então ele pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` torna o payload inválido. Quando um payload construído em torno de uma chave traz um valor (`54`), esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque, ou seja, a menos que traga o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`) como prescreve o §2.6 do manual do Pix; rejeitar `"0"`/`"0.00"` sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. Um `fss` escrito ao lado de uma localização de PSP torna o payload inválido: o §2.7 do Manual de Padrões para Iniciação do Pix mapeia o QR Code dinâmico para exatamente dois subobjetos, `00` (GUI) e `25` (URL), e o `fss` pertence ao template estático do §2.6. A chave em si não é validada contra os formatos do DICT, use `isValidPixKey` para isso. Os Unreserved Templates (IDs 80 a 99) são ignorados: o "QR Code composto" do Pix Automático (Pix recorrente) grava neles a localização de recorrência e, quando esse payload também traz uma localização de pagamento em 26-25, como no exemplo composto do manual do Pix, ele é aceito e lido como um payload dinâmico comum, com a localização de recorrência descartada. Só um payload sem nenhum template Pix nos IDs 26 a 51 é considerado inválido. ```javascript import { isValidPixPayload } from '@brazilian-utils/brazilian-utils'; @@ -224,7 +224,7 @@ isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (CRC quebrado) ## parsePixPayload -Interpreta um payload de BR Code Pix e retorna seus campos. O payload é validado pelo `isValidPixPayload` primeiro, então uma estrutura malformada, um CRC quebrado ou um objeto obrigatório ausente retornam `null` em vez de um resultado parcial. Um payload estático vem com `key`, um dinâmico com `url`. O resultado é tipado como `PixPayload`; `pointOfInitiation` está sempre presente e é tipado como `PixPointOfInitiation`, `"dynamic"` quando o payload traz uma localização de PSP ou quando o objeto "Point of Initiation Method" (`01`) é `"12"`, e `"static"` nos demais casos. As informações da conta do recebedor devem trazer exatamente uma chave ou uma `url` (verificada com a mesma regra de localização de PSP do `generatePixPayload`); o próprio `01` é informativo, então pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` retorna `null`. Quando um payload construído em torno de uma chave traz um valor, esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque: o §2.6 do manual do Pix coloca o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`), devolvido como `withdrawalFacilitator`, e `54` igual a `"0"` ou `"0.00"` é aceito junto dele. Rejeitar um valor zero sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. Quando o payload traz uma localização de PSP, o valor e o `txid` são ignorados, como o manual determina. Payloads cuja localização fica em um Unreserved Template (IDs 80 a 99, Pix Automático) estão fora de escopo e retornam `null`. +Interpreta um payload de BR Code Pix e retorna seus campos. O payload é validado pelo `isValidPixPayload` primeiro, então uma estrutura malformada, um CRC quebrado ou um objeto obrigatório ausente retornam `null` em vez de um resultado parcial. Um payload estático vem com `key`, um dinâmico com `url`. O resultado é tipado como `PixPayload`; `pointOfInitiation` está sempre presente e é tipado como `PixPointOfInitiation`, `"dynamic"` quando o payload traz uma localização de PSP ou quando o objeto "Point of Initiation Method" (`01`) é `"12"`, e `"static"` nos demais casos. As informações da conta do recebedor devem trazer exatamente uma chave ou uma `url` (verificada com a mesma regra de localização de PSP do `generatePixPayload`); o próprio `01` é informativo, então pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` retorna `null`. Quando um payload construído em torno de uma chave traz um valor, esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque: o §2.6 do manual do Pix coloca o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`), devolvido como `withdrawalFacilitator`, e `54` igual a `"0"` ou `"0.00"` é aceito junto dele. Rejeitar um valor zero sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. Um `fss` escrito ao lado de uma localização de PSP retorna `null`: o §2.7 do Manual de Padrões para Iniciação do Pix mapeia o QR Code dinâmico para exatamente dois subobjetos, `00` (GUI) e `25` (URL), e o `fss` pertence ao template estático do §2.6. Quando o payload traz uma localização de PSP, o valor e o `txid` são ignorados, como o manual determina. Os Unreserved Templates (IDs 80 a 99) são ignorados: um "QR Code composto" do Pix Automático que também traga uma localização de pagamento em 26-25 é interpretado como um payload dinâmico comum e sua localização de recorrência é descartada, então quem precisa distinguir os dois não pode se apoiar neste parser. Só um payload sem nenhum template Pix nos IDs 26 a 51 retorna `null`. ```javascript import { parsePixPayload } from '@brazilian-utils/brazilian-utils'; @@ -270,9 +270,9 @@ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // nu ## isValidNfeKey -Valida se uma chave de acesso de DF-e (Documento Fiscal eletrônico) é válida. Cobre todos os documentos cuja chave de acesso é a mesma string de 44 dígitos: NF-e (modelo 55), NFC-e (65), CT-e (57), MDF-e (58), CT-e OS (67, o Conhecimento de Transporte Eletrônico para Outros Serviços do [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), GTV-e (64, o CT-e Guia de Transporte de Valores), BP-e (63), NF3e (66) e NFCom (62). O CF-e-SAT (59) fica de fora: sua "chave de consulta" de 44 posições é composta de outro jeito. Aceita espaços entre os grupos de dígitos (a máscara de exibição usual) e os prefixos `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` e `NFCom` encontrados no atributo `Id` do XML do documento. +Valida se uma chave de acesso de DF-e (Documento Fiscal eletrônico) é válida. Cobre todos os documentos cuja chave de acesso é a mesma string de 44 dígitos: NF-e (modelo 55), NFC-e (65), CT-e (57, o Conhecimento de Transporte Eletrônico instituído pela cláusula primeira do [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, o Conhecimento de Transporte Eletrônico para Outros Serviços instituído pela cláusula primeira do [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, o CT-e Guia de Transporte de Valores instituído pela cláusula primeira do [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) e NFCom (62). O CF-e-SAT (59) fica de fora: sua "chave de consulta" de 44 posições é composta de outro jeito. Aceita espaços entre os grupos de dígitos (a máscara de exibição usual) e os prefixos `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` e `NFCom` encontrados no atributo `Id` do XML do documento. -A forma de emissão (`tpEmis`) é conferida contra os códigos que o MOC daquele modelo atribui, então o conjunto aceito muda com o modelo: de 1 a 7 e 9 para NF-e e NFC-e, `{1, 3, 4, 5, 7, 8}` para o CT-e, `{1, 5, 7, 8}` para o CT-e OS, `{1, 2, 7, 8}` para a GTV-e, `{1, 2, 3}` para o MDF-e e `{1, 2}` para o BP-e, a NF3e e a NFCom. O código 8, a autorização pela SVC-SP, é atribuído somente pelo [MOC do CT-e 4.00](https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais), nunca pelo da NF-e; os domínios do [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), da [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) e da [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) vêm dos manuais deles. Para NF-e e NFC-e o código numérico também é conferido contra a regra B03-10 do MOC da NF-e, que proíbe os vinte valores repetidos e sequenciais de `cNF` que ela lista e um `cNF` igual ao número do documento. Já rejeitar um número de documento todo zerado é uma escolha desta biblioteca: nenhuma regra de MOC foi encontrada proibindo isso. +A forma de emissão (`tpEmis`) é conferida contra os códigos que o MOC daquele modelo atribui, então o conjunto aceito muda com o modelo: de 1 a 7 e 9 para NF-e e NFC-e, `{1, 3, 4, 5, 7, 8}` para o CT-e, `{1, 5, 7, 8}` para o CT-e OS, `{1, 2, 7, 8}` para a GTV-e, `{1, 2, 3}` para o MDF-e e `{1, 2}` para o BP-e, a NF3e e a NFCom. O código 8, a autorização pela SVC-SP, é atribuído somente pelo [MOC do CT-e 4.00](https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais), nunca pelo da NF-e; os domínios do [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), da [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) e da [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) vêm dos manuais deles. Para NF-e e NFC-e o código numérico também é conferido contra a regra B03-10 do MOC da NF-e, que proíbe os vinte valores repetidos e sequenciais de `cNF` que ela lista e um `cNF` igual ao número do documento. Já um número de documento todo zerado é recusado em todos os modelos seguindo o leiaute, não por escolha desta biblioteca: o `tiposBasico_v4.00.xsd` do [pacote de schemas da NF-e](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) tipa o `nNF` como `TNF`, cujo pattern é `[1-9]{1}[0-9]{0,8}`, e o Anexo I de cada um dos outros modelos repete o mesmo regex no seu próprio campo de número. ```javascript import { isValidNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -288,7 +288,7 @@ isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 000 ## formatNfeKey -Formata uma chave de acesso de DF-e (Documento Fiscal eletrônico) em grupos de 4 dígitos separados por espaço, a forma em que todo documento auxiliar a imprime: o DANFE da NF-e e da NFC-e, o DACTE do CT-e, do CT-e OS e da GTV-e, o DAMDFE do MDF-e, o DABPE do BP-e, o DANF3E da NF3e e o DANFE-COM da NFCom. +Formata uma chave de acesso de DF-e (Documento Fiscal eletrônico) em grupos de 4 dígitos separados por espaço, a forma em que todo documento auxiliar a imprime: o DANFE da NF-e e da NFC-e, o DACTE do CT-e, do CT-e OS e da GTV-e, o DAMDFE do MDF-e, o DABPE do BP-e, o DANF3E da NF3e e o DANFE-COM da NFCom. Um valor que não seja string só é lido quando é um inteiro seguro não negativo, então qualquer coisa sem representação utilizável em dígitos (um número negativo ou fracionário, um objeto, um objeto criado com `Object.create(null)`) devolve `''`. ```javascript import { formatNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -396,7 +396,7 @@ isValidLandlinePhone('1130000000'); // true ## isValidServicePhone -Valida se um número de telefone é um número de serviço brasileiro válido, discado sem DDD: os Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` e `0900` (11 dígitos no total), os números abreviados `300X`/`400X` (8 dígitos), e os códigos de 3 dígitos dos Códigos de Acesso a Serviços de Utilidade Pública designados pela Anatel (ex.: `190`, `192`; `112` e `911` também são aceitos, como aliases exclusivos de celular do `190` que a Anatel lista junto aos demais códigos de 3 dígitos). Apenas a estrutura é verificada, o número não precisa estar atribuído a ninguém. A Anatel não publica alocação para os números abreviados, então apenas as raízes convencionais `300X` e `400X` são reconhecidas: outros prefixos de "Número Único" usados no mercado, como `4020` e `4062`, estão fora de escopo e são rejeitados. +Valida se um número de telefone é um número de serviço brasileiro válido, discado sem DDD: os Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` e `0900` (11 dígitos no total), os números abreviados `300X`/`400X` (8 dígitos), e os códigos de 3 dígitos dos Códigos de Acesso a Serviços de Utilidade Pública designados pela Anatel (ex.: `190`, `192`), cuja tabela consolidada é o Anexo do [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151). O `112` e o `911` são rejeitados: a Anatel não designa nenhum dos dois, e o `911` sequer está dentro da faixa `1N₂N₁` que o art. 13 da [Resolução nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749) destina aos serviços de utilidade pública, então o encaminhamento deles nos aparelhos é uma convenção GSM, não uma designação de numeração. Apenas a estrutura é verificada, o número não precisa estar atribuído a ninguém. A Anatel retirou os códigos de 4 caracteres em vez de alocá-los (o art. 43 I da [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86) e o art. 2º II do Ato acima mandaram liberá-los), então apenas as raízes convencionais `300X` e `400X` são reconhecidas: outros prefixos de "Número Único" usados no mercado, como `4020` e `4062`, estão fora de escopo e são rejeitados. ```javascript import { isValidServicePhone } from '@brazilian-utils/brazilian-utils'; @@ -411,7 +411,7 @@ isValidServicePhone('11987654321'); // false (número geográfico) Retorna o estado (e a região) a que um DDD brasileiro pertence, dentre os 67 DDDs em uso no Plano Geral de Numeração da Anatel. Aceita string ou número inteiro não negativo, removendo caracteres não numéricos antes de comparar. Exporta o tipo `AreaCodeInfo`. -`stateCode` é sempre um único estado: aquele que concentra quase todos os municípios do DDD. Quatro DDDs cruzam a divisa de um estado, e para esses o `stateCodes` lista também os demais. O DDD 61 é o mais amplo deles: atende o Distrito Federal e os doze municípios goianos do Entorno do Distrito Federal (Águas Lindas de Goiás, Cabeceiras, Cidade Ocidental, Cristalina, Formosa, Luziânia, Novo Gama, Padre Bernardo, Planaltina, Santo Antônio do Descoberto, Valparaíso de Goiás e Vila Boa). Os outros três são o 42, compartilhado entre o Paraná e Porto União (SC), o 47, entre Santa Catarina e Rio Negro (PR), e o 49, entre Santa Catarina e Barracão (PR). +`stateCode` é sempre um único estado: a sede do DDD, o estado da cidade em torno da qual o código foi alocado, que não é necessariamente o estado que concentra a maioria dos seus municípios. Quatro DDDs cruzam a divisa de um estado, e para esses o `stateCodes` lista também os demais. O DDD 61 é o mais amplo deles: atende o Distrito Federal e os doze municípios goianos do Entorno do Distrito Federal (Águas Lindas de Goiás, Cabeceiras, Cidade Ocidental, Cristalina, Formosa, Luziânia, Novo Gama, Padre Bernardo, Planaltina, Santo Antônio do Descoberto, Valparaíso de Goiás e Vila Boa), então seu `stateCode` é `'DF'` mesmo o Distrito Federal tendo apenas um dos seus treze municípios, Brasília. Os outros três são o 42, compartilhado entre o Paraná e Porto União (SC), o 47, entre Santa Catarina e Rio Negro (PR), e o 49, entre Santa Catarina e Barracão (PR), e neles a sede realmente concentra todos os municípios menos o citado. ```javascript import { getAreaCodeInfo } from '@brazilian-utils/brazilian-utils'; @@ -531,7 +531,7 @@ parseCep('92500-000'); // 92500000 ## getAddressInfoByCep -Busca informações de endereço para um CEP usando múltiplos provedores. O padrão é `['viacep', 'brasilapi']`. O provedor `'widenet'` está descontinuado (seu endpoint não responde mais) e foi excluído da lista padrão, mas ainda pode ser solicitado explicitamente via `options.providers` (tipado como `CepProvider[]`). O endereço retornado é tipado como `AddressInfo`. Uma falha transitória de rede é repetida duas vezes por provedor, com backoff linear de 250 ms (250 ms e depois 500 ms), então um provedor que continua falhando é tentado até 3 vezes e acrescenta cerca de 750 ms antes de o próximo provedor ser consultado; um status de erro HTTP ou uma falha não recuperável não é repetida. +Busca informações de endereço para um CEP usando múltiplos provedores. O padrão é `['viacep', 'brasilapi']`. O provedor `'widenet'` está descontinuado (seu endpoint não responde mais) e foi excluído da lista padrão, mas ainda pode ser solicitado explicitamente via `options.providers` (tipado como `CepProvider[]`). O endereço retornado é tipado como `AddressInfo`. Uma falha transitória de rede é repetida duas vezes por provedor, com backoff linear de 250 ms (250 ms e depois 500 ms), então um provedor que continua falhando é tentado até 3 vezes e acrescenta cerca de 750 ms antes de a sua própria falha se concretizar; um status de erro HTTP ou uma falha não recuperável não é repetida. Os provedores são disparados juntos e disputados com `Promise.any`, não consultados um após o outro, então essas tentativas não atrasam nada para os demais provedores, apenas o momento em que uma rejeição por falha de todos pode aparecer. Um `options.providers` que não nomeia nenhum provedor conhecido rejeita com `GetAddressInfoByCepValidationError` ("Nenhum provedor válido especificado"): um array vazio, um array de nomes desconhecidos e um valor que não é um array, incluindo `null`. Com `providers: ['brasilapi']`, um CEP que a BrasilAPI não conhece rejeita com `GetAddressInfoByCepNotFoundError`, já que a BrasilAPI sinaliza a ausência com HTTP 404; qualquer outro status de erro continua sendo um `GetAddressInfoByCepServiceError`. ```javascript import { getAddressInfoByCep } from '@brazilian-utils/brazilian-utils'; @@ -584,7 +584,7 @@ parseProcessoJuridico('0002080-25.2012.5.15.0049'); // 00020802520125150049 ## isValidIe -Valida se a inscrição estadual de um estado é válida. A UF é case-insensitive. Regras notáveis por estado: GO aceita os prefixos `10`, `11` e `15`; PA aceita `15` e `75`-`79`; MS aceita `28` e `50`; SP tem o padrão de produtor rural `P0MMMSSSSD000`; TO usa códigos de tipo de 11 dígitos (`01`, `02`, `03`, `99`). O TO também aceita uma forma de 9 dígitos, aplicando a mesma regra módulo 11 sobre os oito primeiros dígitos; a página do SINTEGRA documenta apenas a de 11 dígitos, então essa forma é comportamento da 2.3.0 mantido por compatibilidade, e não regra publicada. Uma inscrição só de zeros é aceita em todo estado cuja fórmula publicada produz dígito verificador 0 para ela (AM, BA com 8 ou 9 dígitos, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, SC, SE, SP e TO com 9 dígitos), diferente de `isValidCpf` e `isValidCnpj`, que rejeitam dígitos repetidos. +Valida se a inscrição estadual de um estado é válida. A UF é case-insensitive. Regras notáveis por estado: GO aceita os prefixos `10`, `11` e `15`; PA aceita `15` e `75`-`79`; MS aceita `28` e `50`; SP tem o padrão de produtor rural `P0MMMSSSSD000`; TO usa códigos de tipo de 11 dígitos (`01`, `02`, `03`, `99`). O TO também aceita uma forma de 9 dígitos, aplicando a mesma regra módulo 11 sobre os oito primeiros dígitos; a página do SINTEGRA documenta apenas a de 11 dígitos, então essa forma é comportamento da 2.3.0 mantido por compatibilidade, e não regra publicada. Uma inscrição só de zeros é aceita em todo estado cuja fórmula publicada produz dígito verificador 0 para ela (AM, BA com 8 ou 9 dígitos, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, SC, SE, SP e TO com 9 dígitos), diferente de `isValidCpf` e `isValidCnpj`, que rejeitam dígitos repetidos. O AM entra nessa lista apenas pelo segundo ramo da fórmula publicada: o primeiro ramo da página, `Se Soma < 11 Então Dígito = 11 - Soma`, dá 11 para a inscrição só de zeros, enquanto o ramo `resto <= 1 ⇒ 0`, o implementado aqui, dá 0. ```javascript import { isValidIe } from '@brazilian-utils/brazilian-utils'; @@ -601,11 +601,11 @@ Bancos validados pelo algoritmo de dígito verificador publicado: | Banco | Código | Agência | Conta | Observações | | --- | --- | --- | --- | --- | -| Banco do Brasil | `001` | 4-5 dígitos | 8-10 dígitos | mod11 com pesos 9..2; `digit` pode ser `"X"` | +| Banco do Brasil | `001` | 4-5 dígitos | 8-10 dígitos | mod11 com pesos 2..9 ciclando da direita para a esquerda; `digit` pode ser `"X"` | | Santander | `033` | 4 dígitos | 8 dígitos | pesos `9,7,3,1,0,0,9,7,1,3,1,9,7,3` sobre agência + `"00"` + conta, desprezando as dezenas | | Banrisul | `041` | 4 dígitos | 9 dígitos | pesos `3,2,4,7,6,5,4,3,2`; resto 0 gera `0` e resto 1 gera `6`; `account` é tipo (2 dígitos) + conta (7 dígitos) | | Caixa Econômica Federal | `104` | 4 dígitos | 11 dígitos | mod11 sobre agência + conta; `account` é operação (3 dígitos) + conta (8 dígitos) | -| Bradesco | `237` | 4 dígitos | 7 dígitos | mod11 com pesos 2..7; resto 0 gera `0` e resto 1 gera `"P"` | +| Bradesco | `237` | 4 dígitos | 7 dígitos | mod11 com pesos 2..7 ciclando da direita para a esquerda; resto 0 gera `0` e resto 1 gera `"P"` | | Nubank | `260` | 4 dígitos | 5-13 dígitos | dígito de Verhoeff sobre a conta, ignorando zeros à esquerda | | Itaú Unibanco | `341` | 4 dígitos | 5 dígitos | mod10 sobre agência + conta | | HSBC / Kirton Bank | `399` | 4 dígitos | 6 dígitos | pesos `8,9,2,3,4,5,6,7,8,9` sobre agência + conta; resto 10 gera `0` | @@ -832,7 +832,7 @@ capitalize(' josé maria '); // José Maria (toda sequência de espaço em b ## formatCurrency -Formata um número inteiro ou float para uma string no padrão BRL. Um `number` é formatado como está (sinal e decimais preservados). Uma entrada em `string` é lida pela mesma regra do `parseCurrency`, com a diferença de que um valor escrito sem nenhum separador permanece em unidades inteiras: o último `,` ou `.` seguido de 1 ou 2 dígitos (ou de até `precision` dígitos, quando esse valor for maior) é o separador decimal, todo outro `,` ou `.` é separador de milhar, e um `-` escrito antes do primeiro dígito é preservado. Assim `'1.234,56'` vira `1.234,56`, `'-10.5'` vira `-10,50` e `'1234'` vira `1.234,00`. `precision` é limitado ao intervalo `0..20` (o aceito pelo `Intl.NumberFormat`), o padrão é 2 e volta a 2 quando não é um número finito. Um valor que não seja um número finito (`NaN`, `Infinity`, `-Infinity`) vira string vazia, e um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo) também; `null`, arrays e booleanos passam por `Number()` como no 2.3.0. As opções são tipadas como `FormatCurrencyOptions`. +Formata um número inteiro ou float para uma string no padrão BRL. Um `number` é formatado como está (sinal e decimais preservados). Uma entrada em `string` é lida pela mesma regra do `parseCurrency`, com a diferença de que um valor escrito sem nenhum separador permanece em unidades inteiras: o último `,` ou `.` seguido de 1 ou 2 dígitos (ou de até `precision` dígitos, quando esse valor for maior) é o separador decimal, todo outro `,` ou `.` é separador de milhar, e um `-` escrito antes do primeiro dígito é preservado. Assim `'1.234,56'` vira `1.234,56`, `'-10.5'` vira `-10,50` e `'1234'` vira `1.234,00`. `precision` é limitado ao intervalo `0..20` (o limite do pacote, o que o Node 20 ainda impõe ao `Intl.NumberFormat`), o padrão é 2 e volta a 2 quando não é um número finito. Um valor que não seja um número finito (`NaN`, `Infinity`, `-Infinity`) vira string vazia, e um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo) também; `null`, arrays e booleanos passam por `Number()` como no 2.3.0. As opções são tipadas como `FormatCurrencyOptions`. ```javascript import { formatCurrency } from '@brazilian-utils/brazilian-utils'; @@ -939,7 +939,7 @@ getStates(); ## getStateByIbgeCode -Retorna o estado brasileiro cujo código IBGE de 2 dígitos ("cUF", Código da Unidade da Federação) corresponde ao valor informado. É o mesmo código de UF de 2 dígitos presente no primeiro campo de toda chave de acesso de DF-e (NF-e, NFC-e, CT-e e MDF-e). Aceita string ou número inteiro não negativo, removendo caracteres não numéricos antes de comparar. Exporta o tipo `State`. +Retorna o estado brasileiro cujo código IBGE de 2 dígitos ("cUF", Código da Unidade da Federação) corresponde ao valor informado. É o mesmo código de UF de 2 dígitos presente no primeiro campo de toda chave de acesso de DF-e de qualquer um dos modelos que o `isValidNfeKey` cobre: NF-e (55), NFC-e (65), CT-e (57), MDF-e (58), CT-e OS (67), GTV-e (64), BP-e (63), NF3e (66) e NFCom (62). Aceita string ou número inteiro não negativo, removendo caracteres não numéricos antes de comparar. Exporta o tipo `State`. ```javascript import { getStateByIbgeCode } from '@brazilian-utils/brazilian-utils'; @@ -997,7 +997,7 @@ getTimezoneByState('ZZ'); // null ## getCities -Retorna as cidades brasileiras. Retorna todas as cidades se nenhum estado for fornecido, ou cidades de um estado específico. Cada chamada retorna um array novo, então alterar o resultado nunca afeta chamadas seguintes. Um código de estado desconhecido (ou um valor que não seja `StateCode`) retorna um array vazio em vez de lançar erro, exceto quando é um valor falsy: `getCities(null)` e `getCities('')` são lidos como "nenhum estado informado" e retornam todas as cidades, enquanto o mais estrito `getMunicipalities` retorna `[]` para eles. +Retorna as cidades brasileiras. Retorna todas as cidades se nenhum estado for fornecido, ou cidades de um estado específico. Cada chamada retorna um array novo, então alterar o resultado nunca afeta chamadas seguintes. Um código de estado desconhecido (ou um valor que não seja `StateCode`) retorna um array vazio em vez de lançar erro, exceto quando é um valor falsy: `getCities(null)` e `getCities('')` são lidos como "nenhum estado informado" e retornam todas as cidades, enquanto o mais estrito `getMunicipalities` retorna `[]` para eles. O código do estado é comparado exatamente, inclusive na caixa: `getCities('sp')` retorna `[]` enquanto `getCities('SP')` retorna as 645 cidades paulistas. `getCities` e `getMunicipalities` são as únicas buscas por estado sensíveis à caixa; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` e `getMunicipality` ignoram a caixa. ```javascript import { getCities } from '@brazilian-utils/brazilian-utils'; @@ -1015,7 +1015,7 @@ getCities(); // 'Abaré', // 'Abatiá', // 'Abdon Batista', -// ... 5561 more items +// ... mais 5561 itens // ] // Retorna todas as cidades brasileiras do estado de São Paulo (ordenadas alfabeticamente). @@ -1031,11 +1031,11 @@ getCities('SP'); // "Agudos", // "Alambari", // "Alfredo Marcondes", -// ... 635 more items +// ... mais 635 itens // ] ``` -`getCities` embute os nomes dos 5571 municípios do IBGE (~153,6 KB minificado, ~49,4 KB com gzip) e é uma das poucas exceções pesadas neste pacote, que é tree-shakeable no restante. Veja [Tamanho do bundle](getting-started.md#tamanho-do-bundle) para saber como carregá-lo sob demanda via `@brazilian-utils/brazilian-utils/get-cities` em vez do import da raiz. +`getCities` embute os nomes dos 5571 municípios do IBGE (~154,0 KB minificado, ~49,7 KB com gzip) e é uma das poucas exceções pesadas neste pacote, que é tree-shakeable no restante. Veja [Tamanho do bundle](getting-started.md#tamanho-do-bundle) para saber como carregá-lo sob demanda via `@brazilian-utils/brazilian-utils/get-cities` em vez do import da raiz. ## getHolidays @@ -1043,7 +1043,7 @@ Retorna feriados brasileiros para um determinado ano. Retorna feriados nacionais Apenas um feriado estadual por UF é feriado civil pela [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm), art. 1º, II, que autoriza "a data magna do Estado fixada em lei estadual", no singular; as demais entradas se apoiam em leis estaduais ordinárias e são reportadas por serem observadas na prática. Regras notáveis por estado: -- **SC** — a [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) transfere os dois feriados estaduais, "Dia do Estado de Santa Catarina" (11/08) e "Dia de Santa Catarina de Alexandria" (25/11), para o domingo subsequente sempre que caem de segunda a sexta, então a segunda-feira 11/08/2025 é dia útil em SC e o feriado cai no domingo 17/08. A transferência começa em 2005, ano em que a [Lei SC nº 13.408/2005](http://leis.alesc.sc.gov.br/html/2005/13408_2005_lei.html) a introduziu (publicada e em vigor em 15/07/2005); até 2004 os dois feriados ficam em 11/08 e 25/11 em qualquer dia da semana. +- **SC** — a [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) transfere os dois feriados estaduais, "Dia do Estado de Santa Catarina" (11/08) e "Dia de Santa Catarina de Alexandria" (25/11), para o domingo subsequente sempre que caem de segunda a sexta, então a segunda-feira 11/08/2025 é dia útil em SC e o feriado cai no domingo 17/08. As duas datas não passaram a ser transferidas juntas. O 11/08 é transferido a partir de 2005, ano em que a [Lei SC nº 13.408/2005](http://leis.alesc.sc.gov.br/html/2005/13408_2005_lei.html) estendeu a cláusula a ele (publicada e em vigor em 15/07/2005), e antes disso fica em 11/08. O 25/11 é transferido a partir de 1999, ano em que a [Lei SC nº 11.213/1999](http://leis.alesc.sc.gov.br/html/1999/11213_1999_lei.html) introduziu a cláusula (publicada e em vigor em 12/11/1999, treze dias antes do 25/11 daquele ano), com um intervalo de um ano: o art. 3º da [Lei SC nº 12.906/2004](http://leis.alesc.sc.gov.br/html/2004/12906_2004_lei.html) revogou aquela lei sem repetir a cláusula, então só o 25/11/2004 fica na data estatutária, até a Lei SC nº 13.408/2005 reinstituir a transferência. Assim, o 25/11/1999 (uma quinta-feira) cai no domingo 28/11, o 25/11/2002 (uma segunda-feira) no domingo 01/12, o 25/11/2004 (uma quinta-feira) não se move, e o 25/11/2005 (uma sexta-feira) cai no domingo 27/11. - **DF** — a [Lei distrital nº 72/1989](https://www.sinj.df.gov.br/sinj/Norma/18459/Lei_72_27_12_1989.html), art. 1º parágrafo único, declara Corpus Christi feriado. Com `stateCode: 'DF'` a única entrada de Corpus Christi volta tipada como `"state"` em vez de `"optional"`; ela é substituída, não duplicada. - **GO** — a [Lei GO nº 20.756/2020](https://legisla.casacivil.go.gov.br/pesquisa_legislacao/100979/lei-20756), art. 269, II, lista três feriados estaduais: 26/07 (Fundação da Cidade de Goiás), 24/10 (Lançamento da Pedra Fundamental de Goiânia) e 28/10 (Dia do Servidor Público). - **AL** — 16/09 é feriado estadual a partir de 2024 ([Lei AL nº 9.358/2024](https://sapl.al.al.leg.br/norma/3117)) e apenas ponto facultativo (`"optional"`) antes disso. @@ -1073,7 +1073,7 @@ getHolidays({ year: 2024, stateCode: 'SP' }); ## isValidPassport -Verifica se um número de passaporte brasileiro é válido (2 letras seguidas de 6 dígitos). Aceita tanto `string` quanto `number`; a entrada é case-insensitive e caracteres não alfanuméricos (espaços, pontos, hífens) são ignorados. +Verifica se um número de passaporte brasileiro é válido (2 letras seguidas de 6 dígitos). Aceita tanto `string` quanto `number`; a entrada é case-insensitive e caracteres não alfanuméricos (espaços, pontos, hífens) são ignorados. Um `number` é aceito por simetria com `formatPassport`/`parsePassport`, mas nunca é válido: a forma decimal de um número nunca começa com as duas letras que um número de passaporte exige. ```javascript import { isValidPassport } from '@brazilian-utils/brazilian-utils'; @@ -1171,7 +1171,7 @@ parseCnh('026503064-61'); // '02650306461' ## getCepInfoByAddress -Busca CEPs a partir de um endereço usando a ViaCEP. Lança `GetCepInfoByAddressValidationError` quando a UF, a cidade ou a rua estão ausentes/inválidas, `GetCepInfoByAddressNotFoundError` quando nenhum endereço corresponde à busca, e `GetCepInfoByAddressError` quando a própria ViaCEP responde com um status de erro HTTP. Uma requisição que não pode ser realizada (falha de transporte) rejeita com o erro original do `fetch`. +Busca CEPs a partir de um endereço usando a ViaCEP. Lança `GetCepInfoByAddressValidationError` quando a UF, a cidade ou a rua estão ausentes/inválidas — inclusive quando o argumento não é um objeto (omitido, `null`, uma string) e quando `federalUnit` não é uma string, casos em que nenhum `TypeError` cru escapa — `GetCepInfoByAddressNotFoundError` quando nenhum endereço corresponde à busca, e `GetCepInfoByAddressError` quando a própria ViaCEP responde com um status de erro HTTP. Uma requisição que não pode ser realizada (falha de transporte) rejeita com o erro original do `fetch`. ```javascript import { getCepInfoByAddress } from '@brazilian-utils/brazilian-utils'; @@ -1307,6 +1307,8 @@ generateLicensePlate(); // 'ABC1D23' (Mercosul, o padrão) generateLicensePlate('LLLNNNN'); // 'ABC1234' ``` +Uma string `format` fora dos dois literais suportados não é rejeitada: ela é usada literalmente, caractere a caractere, com `L` produzindo uma letra e qualquer outra posição um dígito. Assim, `generateLicensePlate('LLLNNLN')` devolve uma placa na sequência de motocicleta que foi retirada, que o próprio `isValidLicensePlate` rejeita; `generateLicensePlate('bogus')` devolve cinco dígitos; e `generateLicensePlate('')` devolve uma string vazia. Apenas um valor que não seja string recai no padrão Mercosul. Esse é o comportamento da versão 2.3.0, mantido para as pessoas que chamam a função em JavaScript, onde o tipo do TypeScript não alcança. + ## getFormatLicensePlate Detecta o formato normalizado de uma placa. @@ -1347,7 +1349,7 @@ convertLicensePlateToMercosul('ABC1D23'); // '' (já está no formato Mercosul) ## generatePis -Gera um PIS válido aleatório. +Gera um PIS válido aleatório. Usa `Math.random()` internamente, então não é criptograficamente seguro. ```javascript import { generatePis } from '@brazilian-utils/brazilian-utils'; @@ -1403,7 +1405,7 @@ const lookUp = (options: GetMunicipalityOptions) => getMunicipality(options); ## getMunicipalities -Retorna os municípios brasileiros publicados pelo IBGE. Retorna todos os municípios se nenhum estado for fornecido, ou os municípios de um estado específico. Cada município é retornado como `{ code, name, stateCode }`, onde `code` é o código IBGE de 7 dígitos do município. Os resultados são ordenados por nome com `localeCompare` no locale "pt-BR". Cada chamada retorna um array novo com objetos novos, então alterar o resultado nunca afeta chamadas seguintes. Um código de estado desconhecido retorna um array vazio em vez de lançar erro. Só um `stateCode` omitido (ou `undefined`) pede a lista completa: `getMunicipalities(null)` e `getMunicipalities('')` retornam `[]`, enquanto os mais permissivos `getCities(null)` e `getCities('')` retornam todas as cidades. +Retorna os municípios brasileiros publicados pelo IBGE. Retorna todos os municípios se nenhum estado for fornecido, ou os municípios de um estado específico. Cada município é retornado como `{ code, name, stateCode }`, onde `code` é o código IBGE de 7 dígitos do município. Os resultados são ordenados por nome com `localeCompare` no locale "pt-BR". Cada chamada retorna um array novo com objetos novos, então alterar o resultado nunca afeta chamadas seguintes. Um código de estado desconhecido retorna um array vazio em vez de lançar erro. Só um `stateCode` omitido (ou `undefined`) pede a lista completa: `getMunicipalities(null)` e `getMunicipalities('')` retornam `[]`, enquanto os mais permissivos `getCities(null)` e `getCities('')` retornam todas as cidades. O código do estado é comparado exatamente, inclusive na caixa: `getMunicipalities('sp')` retorna `[]` enquanto `getMunicipalities('SP')` retorna os 645 municípios paulistas. `getMunicipalities` e `getCities` são as únicas buscas por estado sensíveis à caixa; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` e `getMunicipality` ignoram a caixa. ```javascript import { getMunicipalities } from '@brazilian-utils/brazilian-utils'; @@ -1466,7 +1468,7 @@ isHoliday(); // false ## isBusinessDay -Verifica se uma data é um dia útil no Brasil. Retorna `false` para sábados, domingos e feriados brasileiros retornados por `getHolidays` para a data local de `value` (ano/mês/dia lidos localmente), a mesma convenção usada por `isHoliday`. `options.includeOptional` (parte de `BusinessDayOptions`, o tipo de opções que todos os utilitários de dias úteis compartilham) tem valor padrão `true`, então feriados do tipo opcional (`Holiday.type === "optional"`, ou seja, Carnaval e Corpus Christi) também contam como dias não úteis; passe `false` para considerar apenas os feriados estatutários. `options.stateCode` também considera os feriados daquele estado; um `stateCode` desconhecido/inválido é ignorado, retornando apenas os feriados nacionais. Um `value` que não é um `Date` válido retorna `false`. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele retorna `false`. +Verifica se uma data é um dia útil no Brasil. Retorna `false` para sábados, domingos e feriados brasileiros retornados por `getHolidays` para a data local de `value` (ano/mês/dia lidos localmente), a mesma convenção usada por `isHoliday`. `options.includeOptional` (parte de `BusinessDayOptions`, o tipo de opções que todos os utilitários de dias úteis compartilham) tem valor padrão `true`, então feriados do tipo opcional (`Holiday.type === "optional"`, ou seja, Carnaval e Corpus Christi) também contam como dias não úteis; passe `false` para considerar apenas os feriados estatutários. `options.stateCode` também considera os feriados daquele estado; uma string que não é um código de estado conhecido é ignorada, retornando apenas os feriados nacionais, enquanto um `stateCode` presente que não é uma string (um número, `null`, um objeto) é rejeitado e faz a chamada retornar `false` mesmo em um dia de semana comum — a mesma distinção que `isHoliday` faz, e o valor que `addBusinessDays`, `subBusinessDays` e `differenceInBusinessDays` rejeitam com `null`. Um `value` que não é um `Date` válido retorna `false`. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele retorna `false`. ```javascript import { isBusinessDay } from '@brazilian-utils/brazilian-utils'; @@ -1596,7 +1598,7 @@ parseVoterId('1234 5678 8 01 91'); // '1234567880191' (título de 13 dígitos SP ## isValidCns -Verifica se um número de CNS (Cartão Nacional de Saúde) é válido, o identificador único do usuário do SUS (Sistema Único de Saúde). Cartões definitivos (iniciados em 1 ou 2) são validados com a mesma ponderação módulo 11 usada no PIS sobre uma base de 11 dígitos embutida, ajustando a base em +2 quando o dígito verificador bruto resulta em 10. Cartões provisórios (iniciados em 7, 8 ou 9) são validados por uma soma ponderada única (pesos de 15 a 1) que deve ser múltipla de 11. O valor precisa vir escrito como os 15 dígitos, opcionalmente separados nos grupos impressos de 3-4-4-4 por espaços ou pelos caracteres de máscara usuais; letras no meio dos dígitos são rejeitadas em vez de ignoradas. +Verifica se um número de CNS (Cartão Nacional de Saúde) é válido, o identificador único do usuário do SUS (Sistema Único de Saúde). Cartões definitivos (iniciados em 1 ou 2) são validados sobre uma base embutida de 11 dígitos derivada do PIS/PASEP/NIS, ponderada de 15 até 5; quando o dígito bruto resulta em 10, o DATASUS soma 2 à soma ponderada, recalcula o dígito e marca o cartão com o sufixo `001` em vez de `000`. Cartões provisórios (iniciados em 7, 8 ou 9) são validados por uma soma ponderada única (pesos de 15 a 1) que deve ser múltipla de 11. O valor precisa vir escrito como os 15 dígitos, opcionalmente separados nos grupos impressos de 3-4-4-4 por espaços ou pelos caracteres de máscara usuais, inclusive uma sequência deles entre dois grupos; letras no meio dos dígitos são rejeitadas em vez de ignoradas. As duas rotinas vêm da [página de validação de CNS da ANVISA](https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/), que fica atrás de um filtro de bots e responde HTTP 403 a clientes que não sejam navegadores. A [página do e-SUS APS](https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html) documenta o mesmo algoritmo e é acessível sem navegador, mas aplica a rotina de provisórios a números iniciados em 5, 7, 8 ou 9; esta implementação segue a ANVISA e rejeita um número iniciado em 5 mesmo quando a soma ponderada fecha. @@ -1641,7 +1643,7 @@ isValidCertidao('104539 01 55 2013 1 00012 021 0000123 21', { accept: ['death'] ## parseCertidao -Extrai os campos da matrícula de uma certidão de registro civil, retornando `null` quando a matrícula é inválida, o que inclui um código de livro que não é um dos nove livros. O [art. 473, V do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lista os códigos de 1 a 7; os códigos 8 (emancipação) e 9 (interdição) vêm do Anexo IV do revogado Provimento CNJ nº 63/2017, conforme listados em [ghiorzi.org](http://ghiorzi.org/DVnew.htm), e são mantidos porque matrículas emitidas sob ele ainda circulam. Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. +Extrai os campos da matrícula de uma certidão de registro civil, retornando `null` quando a matrícula é inválida, o que inclui um código de livro que não é um dos nove livros. O [art. 473, V do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lista os códigos de 1 a 7; nenhum texto primário do CNJ acessível hoje publica os outros dois, inclusive o Anexo IV do revogado Provimento CNJ nº 63/2017, que lista os mesmos sete. Os códigos 8 (emancipação) e 9 (interdição) vêm das referências em que a regra do dígito verificador se apoia: o [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e o [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) publicam a lista dos nove livros. Eles são mantidos porque matrículas com eles circulam. Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. ```javascript import { parseCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1692,7 +1694,7 @@ formatCertidao('1552010100020112000012087', { pad: true }); // 000000 01 55 2010 ## isValidCei -Verifica se um número de CEI (Cadastro Específico do INSS) é válido. O CEI identifica o empregador sem CNPJ, como uma obra ou um produtor rural: 12 dígitos impressos como `00.000.00000/00`, sendo o último um dígito verificador calculado sobre os 11 dígitos da base com os pesos 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 e 4. Aceita os caracteres de máscara usuais e espaços entre e ao redor dos grupos. A Receita Federal não publica essa regra de dígito verificador, então ela segue as implementações de referência do [yii2-br-validator](https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php) e do [Bigai.Documentos.Brasil](https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs), conferida contra os [dados abertos do Cadastro Nacional de Obras (CNO)](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) da Receita Federal. +Verifica se um número de CEI (Cadastro Específico do INSS) é válido. O CEI identifica o empregador sem CNPJ, como uma obra ou um produtor rural: 12 dígitos impressos como `00.000.00000/00`, sendo o último um dígito verificador calculado sobre os 11 dígitos da base com os pesos 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 e 4. Aceita os caracteres de máscara usuais e espaços entre e ao redor dos grupos, inclusive uma sequência deles entre dois grupos. A Receita Federal não publica essa regra de dígito verificador, então ela segue as implementações de referência do [yii2-br-validator](https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php) e do [Bigai.Documentos.Brasil](https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs), conferida contra os [dados abertos do Cadastro Nacional de Obras (CNO)](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) da Receita Federal. ```javascript import { isValidCei } from '@brazilian-utils/brazilian-utils'; @@ -1744,7 +1746,7 @@ formatCno('979', { pad: true }); // 00.000.00009/79 ## isValidCaepf -Verifica se um número de CAEPF (Cadastro de Atividade Econômica da Pessoa Física) é válido. O CAEPF substituiu o CEI para a pessoa física que contrata empregados: 14 dígitos impressos como `000.000.000/000-00`, formados pela base de 9 dígitos do CPF do titular, um número de ordem de 3 dígitos para os vários cadastros do mesmo titular e 2 dígitos verificadores. Os dois dígitos usam o módulo 11 do CNPJ e o par resultante é somado a 12, com retorno a zero acima de 99. A Receita Federal não publica o layout nem a regra dos dígitos verificadores: os dois estão descritos em [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e são implementados do mesmo jeito pelo [brazilian-values](https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts). +Verifica se um número de CAEPF (Cadastro de Atividade Econômica da Pessoa Física) é válido. O CAEPF substituiu o CEI para a pessoa física que contrata empregados: 14 dígitos impressos como `000.000.000/000-00`, formados pela base de 9 dígitos do CPF do titular, um número de ordem de 3 dígitos para os vários cadastros do mesmo titular e 2 dígitos verificadores. Os dois dígitos verificadores são o módulo 11 do CNPJ na formulação da referência citada: os pesos vão de 9 até 2 da direita para a esquerda e o dígito é o próprio resto, com o resto 10 lido como 0 — o mesmo dígito que os pesos de 2 a 9 do CNPJ com `11 - resto` produzem. O par resultante é somado a 12, com retorno a zero acima de 99. Uma base cujos 12 dígitos são todos iguais é rejeitada antes do cálculo dos dígitos verificadores, do mesmo jeito que `isValidCei` e `isValidCno` rejeitam um número de CEI/CNO repetido, então o `00000000000012`, que de resto é bem formado, é inválido. A Receita Federal não publica o layout nem a regra dos dígitos verificadores: os dois estão descritos em [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e são implementados do mesmo jeito pelo [brazilian-values](https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts). ```javascript import { isValidCaepf } from '@brazilian-utils/brazilian-utils'; @@ -1753,7 +1755,8 @@ isValidCaepf('293.118.610/001-84'); // true isValidCaepf('41142260000101'); // true isValidCaepf(29311861000184); // true isValidCaepf('29311861000185'); // false (dígitos verificadores inválidos) -isValidCaepf('00000000000000'); // false (dígitos repetidos) +isValidCaepf('00000000000000'); // false (dígitos verificadores inválidos) +isValidCaepf('00000000000012'); // false (dígitos da base repetidos) ``` ## formatCaepf @@ -1900,7 +1903,7 @@ formatNcm(-84713012); // '' (não é um inteiro seguro não negativo) ## isValidCfop -Valida se um código CFOP (Código Fiscal de Operações e Prestações) existe na tabela oficial. A tabela é o [Anexo II consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), o texto vigente (redação atual dada pelo Ajuste SINIEF 03/24, última alteração pelo Ajuste SINIEF 39/25), e não o texto congelado de 2001 do Ajuste SINIEF 07/01. Só os códigos operáveis contam: os títulos de grupo e subgrupo da nomenclatura oficial, os códigos terminados em `00` e `50` (1000, 1100, 1150, 5350, ...), são títulos de seção e não códigos que um documento pode carregar, então são rejeitados. +Valida se um código CFOP (Código Fiscal de Operações e Prestações) existe na tabela oficial. A tabela é o [Anexo II consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), o texto vigente (redação atual dada pelo Ajuste SINIEF 03/24, última alteração pelo [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25)), e não o texto congelado de 2001 do Ajuste SINIEF 07/01. Só os códigos operáveis contam: os títulos de grupo e subgrupo da nomenclatura oficial, os códigos terminados em `00` e `50` (1000, 1100, 1150, 5350, ...), são títulos de seção e não códigos que um documento pode carregar, então são rejeitados. Uma string só é lida como código quando está escrita em uma das formas documentadas (os 4 dígitos, ou a forma `N.NNN` impressa no anexo, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. @@ -1918,7 +1921,7 @@ isValidCfop(-5102); // false (não é um inteiro seguro não negativo) ## getCfop -Busca um código CFOP (Código Fiscal de Operações e Prestações) e retorna seu código e a descrição oficial, na redação do [Anexo II consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24). Os títulos de grupo e subgrupo da nomenclatura oficial, os códigos terminados em `00` e `50`, não estão na tabela e retornam `null`. Valem as mesmas regras de entrada de `isValidCfop`. +Busca um código CFOP (Código Fiscal de Operações e Prestações) e retorna seu código e a descrição oficial, na redação do [Anexo II consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), no texto vigente, com última alteração pelo [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25). Os títulos de grupo e subgrupo da nomenclatura oficial, os códigos terminados em `00` e `50`, não estão na tabela e retornam `null`. Valem as mesmas regras de entrada de `isValidCfop`. ```javascript import { getCfop } from '@brazilian-utils/brazilian-utils'; @@ -1943,9 +1946,9 @@ Valida um código de CST (Código de Situação Tributária) para um tributo. In `options.tax` (parte de `IsValidCstOptions`) é opcional: omita-o para aceitar um código que exista em qualquer uma das quatro tabelas acima. -A Tabela B do ICMS é a vigente: o [Anexo I consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), cuja redação atual veio do [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (efeitos a partir de 01.12.23) e que o [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) alterou revogando os itens 12, 13, 52, 72 e 74 (efeitos a partir de 09.07.24). `02`, `15`, `53` e `61` são seus códigos de monofasia de combustíveis. +A Tabela B do ICMS é a vigente: o [Anexo I consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), cuja redação atual veio do [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (efeitos a partir de 01.12.23) e que o [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) alterou suprimindo os itens 12, 13, 52, 72 e 74 (efeitos a partir de 09.07.24) antes que eles chegassem a produzir efeitos: o 39/23 os havia acrescentado "sem efeitos", então esses códigos nunca estiveram em vigor. `02`, `15`, `53` e `61` são seus códigos de monofasia de combustíveis. -Uma string só é lida como código quando está escrita em uma das formas documentadas (os 2 ou 3 dígitos, com um único separador entre eles e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. +Uma string só é lida como código quando está escrita em uma das formas documentadas (os 2 dígitos de um código da Tabela B, ou os 3 dígitos da forma do ICMS com um único separador opcional depois do dígito de origem, além de espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. O dígito de origem é a única fronteira que um CST impresso tem, então `'0 10'` e `'1-10'` são lidos, mas `'0-0'`, `'11-0'` e `'00-'` não. ```javascript import { isValidCst } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/utilities.md b/docs/utilities.md index 60ef6f12..ee082690 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -2,7 +2,7 @@ Here you will find all the utilities available for use. -> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function (including `capitalize`) returns an empty value of its return type: `""` for strings, `0` for `parseCurrency`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. +> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function returns an empty value of its return type: every `format*` function, `capitalize`, and the string-returning `parse*` functions (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) return `""`; `parseCurrency` returns `0`; the object/tuple parsers — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — return `null`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. ## isValidCpf @@ -17,7 +17,7 @@ isValidCpf('111 444 777 35'); // true (whitespace mask) ## formatCpf -Format CPF. `options.obfuscate` (part of `FormatCpfOptions`) hides the first 3 digits and the 2 check digits (`***.456.789-**`), the gov.br / Receita Federal display convention, applied after `pad`. +Format CPF. `options.obfuscate` (part of `FormatCpfOptions`) hides the first 3 digits and the 2 check digits (`***.456.789-**`), the gov.br / Receita Federal display convention, applied after `pad`. It is read for truthiness, the way `pad` is, so any truthy value obfuscates. ```javascript import { formatCpf } from '@brazilian-utils/brazilian-utils'; @@ -61,7 +61,7 @@ isValidCnpj('q0slfmbd7vx439', { version: 2 }); // true (lowercase alphanumeric) ## formatCnpj -Format CNPJ. `options.obfuscate` (part of `FormatCnpjOptions`) hides the first 2 digits and the 2 check digits (`**.345.678/0001-**`), the gov.br / Receita Federal display convention. It applies to both versions and comes after `pad`. +Format CNPJ. `options.obfuscate` (part of `FormatCnpjOptions`) hides the first 2 digits and the 2 check digits (`**.345.678/0001-**`), the gov.br / Receita Federal display convention. It applies to both versions and comes after `pad`, and is read for truthiness, the way `pad` is, so any truthy value obfuscates. ```javascript import { formatCnpj } from '@brazilian-utils/brazilian-utils'; @@ -101,7 +101,7 @@ isValidCep('12345'); // false (invalid length) ## generateCnpj -Generate a valid random CNPJ. +Generate a valid random CNPJ. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generateCnpj } from '@brazilian-utils/brazilian-utils' @@ -157,7 +157,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ## getBoletoInfo -Extract information from a boleto (amount, expiration date, bank code). Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. +Extract information from a boleto (amount, expiration date, bank code). Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. The cycle search never goes below the first cycle, so a `referenceDate` older than the scheme itself still resolves a factor to the oldest date that factor can denote rather than to one before the 07/10/1997 base date. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -209,7 +209,7 @@ parsePixKey('+5551998259765'); // { type: 'phone', value: '+5551998259765' } ## isValidPixPayload -Check if a Pix BR Code payload (the string behind a Pix QR Code and behind "Pix copia e cola") is valid: well-formed TLV structure, the mandatory objects present, one of the "Merchant Account Information" templates carrying the `br.gov.bcb.pix` GUI with a key or a URL, and a matching CRC-16. The "Point of Initiation Method" object (`01`) is advisory: the Manual do BR Code marks it optional and only assigns a meaning to the value `"12"` ("só pode ser utilizado uma vez"), so it may be absent from either shape and only a value outside `{"11", "12"}` makes the payload invalid. When a payload built around a key carries an amount (`54`), that amount must be greater than zero, unless the payload is a Pix Saque BR Code, i.e. unless it carries the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`) as §2.6 of the Pix manual prescribes; rejecting `"0"`/`"0.00"` without `fss` is a deliberate restriction of this library, not a rule of the manual. The key itself is not checked against the DICT formats, use `isValidPixKey` for that. Payloads that carry the location in an Unreserved Template (IDs 80 to 99), as the "QR Code composto" of Pix Automático (Pix recorrente) does, are out of scope and reported as invalid. +Check if a Pix BR Code payload (the string behind a Pix QR Code and behind "Pix copia e cola") is valid: well-formed TLV structure, the mandatory objects present, one of the "Merchant Account Information" templates carrying the `br.gov.bcb.pix` GUI with a key or a URL, and a matching CRC-16. The "Point of Initiation Method" object (`01`) is advisory: the Manual do BR Code marks it optional and only assigns a meaning to the value `"12"` ("só pode ser utilizado uma vez"), so it may be absent from either shape and only a value outside `{"11", "12"}` makes the payload invalid. When a payload built around a key carries an amount (`54`), that amount must be greater than zero, unless the payload is a Pix Saque BR Code, i.e. unless it carries the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`) as §2.6 of the Pix manual prescribes; rejecting `"0"`/`"0.00"` without `fss` is a deliberate restriction of this library, not a rule of the manual. A `fss` written next to a PSP location makes the payload invalid: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template of §2.6. The key itself is not checked against the DICT formats, use `isValidPixKey` for that. Unreserved Templates (IDs 80 to 99) are ignored: the "QR Code composto" of Pix Automático (Pix recorrente) writes its recurrence location in one of them, and when such a payload also carries a payment location in 26-25, as the composite example of the Pix manual does, it is accepted and read as an ordinary dynamic payload with the recurrence location dropped. Only a payload with no Pix template at all in IDs 26 to 51 is reported as invalid. ```javascript import { isValidPixPayload } from '@brazilian-utils/brazilian-utils'; @@ -224,7 +224,7 @@ isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (broken CRC) ## parsePixPayload -Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The result is typed as `PixPayload`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Payloads whose location lives in an Unreserved Template (IDs 80 to 99, Pix Automático) are out of scope and return `null`. +Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The result is typed as `PixPayload`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. A `fss` written next to a PSP location returns `null`: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template of §2.6. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Unreserved Templates (IDs 80 to 99) are ignored: a "QR Code composto" of Pix Automático that also carries a payment location in 26-25 is parsed as an ordinary dynamic payload and its recurrence location is dropped, so a consumer that has to tell the two apart cannot rely on this parser. Only a payload with no Pix template at all in IDs 26 to 51 returns `null`. ```javascript import { parsePixPayload } from '@brazilian-utils/brazilian-utils'; @@ -270,9 +270,9 @@ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // nu ## isValidNfeKey -Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e (65), CT-e (57), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para Outros Serviços of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), GTV-e (64, the CT-e Guia de Transporte de Valores), BP-e (63), NF3e (66) and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed differently. Accepts whitespace between digit groups (the common display mask) and the `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the document's XML. +Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e (65), CT-e (57, the Conhecimento de Transporte Eletrônico instituted by the cláusula primeira of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para Outros Serviços instituted by the cláusula primeira of the [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, the CT-e Guia de Transporte de Valores instituted by the cláusula primeira of the [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed differently. Accepts whitespace between digit groups (the common display mask) and the `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the document's XML. -The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` for the CT-e, `{1, 5, 7, 8}` for the CT-e OS, `{1, 2, 7, 8}` for the GTV-e, `{1, 2, 3}` for the MDF-e and `{1, 2}` for the BP-e, the NF3e and the NFCom. Code 8, the authorização pela SVC-SP, is assigned by the [CT-e MOC 4.00](https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais) only, never by the NF-e one; the domains of the [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), the [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) and the [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) come from their own manuals. For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, which forbids the twenty repeated and sequential `cNF` values it lists and a `cNF` equal to the document number. Rejecting a document number of all zeros, on the other hand, is a choice of this library: no MOC rule was found forbidding it. +The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` for the CT-e, `{1, 5, 7, 8}` for the CT-e OS, `{1, 2, 7, 8}` for the GTV-e, `{1, 2, 3}` for the MDF-e and `{1, 2}` for the BP-e, the NF3e and the NFCom. Code 8, the authorização pela SVC-SP, is assigned by the [CT-e MOC 4.00](https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais) only, never by the NF-e one; the domains of the [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), the [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) and the [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) come from their own manuals. For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, which forbids the twenty repeated and sequential `cNF` values it lists and a `cNF` equal to the document number. A document number of all zeros is turned down for every model, following the leiaute rather than a choice of this library: `tiposBasico_v4.00.xsd` of the [NF-e schema package](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) types `nNF` as `TNF`, whose pattern is `[1-9]{1}[0-9]{0,8}`, and the Anexo I of every other model repeats the same regex for its own number field. ```javascript import { isValidNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -288,7 +288,7 @@ isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 000 ## formatNfeKey -Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. +Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. A value that is not a string is only read when it is a non-negative safe integer, so anything with no usable digit representation (a negative or fractional number, an object, an object created with `Object.create(null)`) gives `''`. ```javascript import { formatNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -396,7 +396,7 @@ isValidLandlinePhone('1130000000'); // true ## isValidServicePhone -Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`; `112` and `911` are accepted too, as mobile-only aliases of `190` that Anatel lists alongside the other 3-digit codes). Only the structure is checked, the number does not have to be assigned to anyone. Anatel publishes no allocation for the abbreviated numbers, so only the conventional `300X` and `400X` roots are recognised: other "Número Único" carrier prefixes in market use, such as `4020` and `4062`, are out of scope and are rejected. +Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`), whose consolidated table is the Anexo of [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151). `112` and `911` are rejected: Anatel designates neither, and `911` is not even inside the `1N₂N₁` range art. 13 of [Resolução nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749) destines to public utility services, so the way handsets route them is a GSM convention rather than a numbering designation. Only the structure is checked, the number does not have to be assigned to anyone. Anatel withdrew the 4-digit codes instead of allocating them (art. 43 I of [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86) and art. 2º II of the Ato above both ordered them released), so only the conventional `300X` and `400X` roots are recognised: other "Número Único" carrier prefixes in market use, such as `4020` and `4062`, are out of scope and are rejected. ```javascript import { isValidServicePhone } from '@brazilian-utils/brazilian-utils'; @@ -411,7 +411,7 @@ isValidServicePhone('11987654321'); // false (geographic number) Get the state (and its region) a Brazilian DDD (area code) belongs to, out of the 67 DDDs in use under the Anatel Plano Geral de Numeração. Accepts a string or a non-negative integer number, stripping any non-digit characters before matching. Exports the `AreaCodeInfo` type. -`stateCode` is always a single state: the one that holds all but a handful of the DDD's municipalities. Four DDDs straddle a state border, and for those `stateCodes` lists the other states too. DDD 61 is the widest of them, serving the Distrito Federal and the twelve Goiás municipalities of the Entorno do Distrito Federal (Águas Lindas de Goiás, Cabeceiras, Cidade Ocidental, Cristalina, Formosa, Luziânia, Novo Gama, Padre Bernardo, Planaltina, Santo Antônio do Descoberto, Valparaíso de Goiás and Vila Boa). The other three are 42, shared by Paraná and Porto União (SC), 47, shared by Santa Catarina and Rio Negro (PR), and 49, shared by Santa Catarina and Barracão (PR). +`stateCode` is always a single state: the one the DDD is seated in, the state of the city the code was allocated around, which is not necessarily the state holding most of its municipalities. Four DDDs straddle a state border, and for those `stateCodes` lists the other states too. DDD 61 is the widest of them, serving the Distrito Federal and the twelve Goiás municipalities of the Entorno do Distrito Federal (Águas Lindas de Goiás, Cabeceiras, Cidade Ocidental, Cristalina, Formosa, Luziânia, Novo Gama, Padre Bernardo, Planaltina, Santo Antônio do Descoberto, Valparaíso de Goiás and Vila Boa), so its `stateCode` is `'DF'` even though the Distrito Federal holds only one of its thirteen municipalities, Brasília. The other three are 42, shared by Paraná and Porto União (SC), 47, shared by Santa Catarina and Rio Negro (PR), and 49, shared by Santa Catarina and Barracão (PR), and there the seat does hold every municipality but the one named. ```javascript import { getAreaCodeInfo } from '@brazilian-utils/brazilian-utils'; @@ -531,7 +531,7 @@ parseCep('92500-000'); // 92500000 ## getAddressInfoByCep -Fetch address information for a given CEP using multiple providers. Defaults to `['viacep', 'brasilapi']`. The `'widenet'` provider is deprecated (its endpoint no longer responds) and excluded from the default list, but it can still be requested explicitly via `options.providers` (typed as `CepProvider[]`). The resolved address is typed as `AddressInfo`. A transient network failure is retried twice per provider, with a 250 ms linear backoff (250 ms, then 500 ms), so a provider that keeps failing is tried up to 3 times and adds about 750 ms before the next provider is reached; an HTTP error status or a non-retryable failure is not retried. +Fetch address information for a given CEP using multiple providers. Defaults to `['viacep', 'brasilapi']`. The `'widenet'` provider is deprecated (its endpoint no longer responds) and excluded from the default list, but it can still be requested explicitly via `options.providers` (typed as `CepProvider[]`). The resolved address is typed as `AddressInfo`. A transient network failure is retried twice per provider, with a 250 ms linear backoff (250 ms, then 500 ms), so a provider that keeps failing is tried up to 3 times and adds about 750 ms before its own failure lands; an HTTP error status or a non-retryable failure is not retried. The providers are started together and raced with `Promise.any`, not queried one after the other, so those retries delay nothing for the other providers, only the moment an all-failed rejection can surface. An `options.providers` that names no known provider rejects with `GetAddressInfoByCepValidationError` ("Nenhum provedor válido especificado"): an empty array, an array of unknown names, and a value that is not an array at all, `null` included. With `providers: ['brasilapi']`, a CEP BrasilAPI does not know rejects with `GetAddressInfoByCepNotFoundError`, since BrasilAPI signals a miss with HTTP 404; any other error status is still a `GetAddressInfoByCepServiceError`. ```javascript import { getAddressInfoByCep } from '@brazilian-utils/brazilian-utils'; @@ -584,7 +584,7 @@ parseProcessoJuridico('0002080-25.2012.5.15.0049'); // 00020802520125150049 ## isValidIe -Check if inscrição estadual (state registration) is valid. The state code is case-insensitive. Notable per-state rules: GO accepts prefixes `10`, `11` and `15`; PA accepts `15` and `75`-`79`; MS accepts `28` and `50`; SP has a produtor rural pattern `P0MMMSSSSD000`; TO uses 11-digit type codes (`01`, `02`, `03`, `99`). TO also accepts a 9-digit form, applying the same modulus 11 rule to the first eight digits; the SINTEGRA page documents only the 11-digit one, so that shape is 2.3.0 behaviour kept for compatibility rather than a published rule. An all-zero registration is accepted wherever the published formula yields a check digit of 0 for it (AM, BA with 8 or 9 digits, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, SC, SE, SP and TO with 9 digits), unlike `isValidCpf` and `isValidCnpj`, which reject repeated digits. +Check if inscrição estadual (state registration) is valid. The state code is case-insensitive. Notable per-state rules: GO accepts prefixes `10`, `11` and `15`; PA accepts `15` and `75`-`79`; MS accepts `28` and `50`; SP has a produtor rural pattern `P0MMMSSSSD000`; TO uses 11-digit type codes (`01`, `02`, `03`, `99`). TO also accepts a 9-digit form, applying the same modulus 11 rule to the first eight digits; the SINTEGRA page documents only the 11-digit one, so that shape is 2.3.0 behaviour kept for compatibility rather than a published rule. An all-zero registration is accepted wherever the published formula yields a check digit of 0 for it (AM, BA with 8 or 9 digits, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, SC, SE, SP and TO with 9 digits), unlike `isValidCpf` and `isValidCnpj`, which reject repeated digits. AM is on that list through the second branch of its published formula only: the page's first branch, `Se Soma < 11 Então Dígito = 11 - Soma`, gives 11 for an all-zero registration, while the `resto <= 1 ⇒ 0` branch, the one implemented here, gives 0. ```javascript import { isValidIe } from '@brazilian-utils/brazilian-utils'; @@ -601,11 +601,11 @@ Banks validated by their published check digit algorithm: | Bank | Code | Agency | Account | Notes | | --- | --- | --- | --- | --- | -| Banco do Brasil | `001` | 4-5 digits | 8-10 digits | mod11 with weights 9..2; `digit` may be `"X"` | +| Banco do Brasil | `001` | 4-5 digits | 8-10 digits | mod11 with weights 2..9 cycling from the right; `digit` may be `"X"` | | Santander | `033` | 4 digits | 8 digits | weights `9,7,3,1,0,0,9,7,1,3,1,9,7,3` over agency + `"00"` + account, tens discarded | | Banrisul | `041` | 4 digits | 9 digits | weights `3,2,4,7,6,5,4,3,2`; remainder 0 gives `0` and remainder 1 gives `6`; `account` is tipo (2 digits) + conta (7 digits) | | Caixa Econômica Federal | `104` | 4 digits | 11 digits | mod11 over agency + account; `account` is operação (3 digits) + conta (8 digits) | -| Bradesco | `237` | 4 digits | 7 digits | mod11 with weights 2..7; remainder 0 gives `0` and remainder 1 gives `"P"` | +| Bradesco | `237` | 4 digits | 7 digits | mod11 with weights 2..7 cycling from the right; remainder 0 gives `0` and remainder 1 gives `"P"` | | Nubank | `260` | 4 digits | 5-13 digits | Verhoeff check digit over the account, leading zeros dropped | | Itaú Unibanco | `341` | 4 digits | 5 digits | mod10 over agency + account | | HSBC / Kirton Bank | `399` | 4 digits | 6 digits | weights `8,9,2,3,4,5,6,7,8,9` over agency + account; remainder 10 gives `0` | @@ -832,7 +832,7 @@ capitalize(' josé maria '); // José Maria (every run of whitespace, tabs a ## formatCurrency -Formats an integer or float to a string in the BRL pattern. A `number` is formatted as-is (sign and decimals preserved). A `string` input is read by the same rule as `parseCurrency`, except that a value written without any separator stays in whole units: the last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator, every other `,` or `.` is a thousands separator, and a `-` written before the first digit is preserved. So `'1.234,56'` formats as `1.234,56`, `'-10.5'` as `-10,50` and `'1234'` as `1.234,00`. `precision` is clamped to `0..20` (the range `Intl.NumberFormat` accepts), defaults to 2, and falls back to 2 when it is not a finite number. A value that is not a finite number (`NaN`, `Infinity`, `-Infinity`) formats as an empty string, and so does a value that cannot be coerced to a number (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. Options are typed as `FormatCurrencyOptions`. +Formats an integer or float to a string in the BRL pattern. A `number` is formatted as-is (sign and decimals preserved). A `string` input is read by the same rule as `parseCurrency`, except that a value written without any separator stays in whole units: the last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator, every other `,` or `.` is a thousands separator, and a `-` written before the first digit is preserved. So `'1.234,56'` formats as `1.234,56`, `'-10.5'` as `-10,50` and `'1234'` as `1.234,00`. `precision` is clamped to `0..20` (the package limit, the bound Node 20 still enforces on `Intl.NumberFormat`), defaults to 2, and falls back to 2 when it is not a finite number. A value that is not a finite number (`NaN`, `Infinity`, `-Infinity`) formats as an empty string, and so does a value that cannot be coerced to a number (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. Options are typed as `FormatCurrencyOptions`. ```javascript import { formatCurrency } from '@brazilian-utils/brazilian-utils'; @@ -939,7 +939,7 @@ getStates(); ## getStateByIbgeCode -Get the Brazilian state whose 2-digit IBGE code ("cUF", the Código da Unidade da Federação) matches the given value. This is the same 2-digit UF code found in the first field of every DF-e access key (chave de acesso) issued for NF-e, NFC-e, CT-e and MDF-e documents. Accepts a string or a non-negative integer number, stripping any non-digit characters before matching. Exports the `State` type. +Get the Brazilian state whose 2-digit IBGE code ("cUF", the Código da Unidade da Federação) matches the given value. This is the same 2-digit UF code found in the first field of every DF-e access key (chave de acesso) issued for any of the models `isValidNfeKey` covers: NF-e (55), NFC-e (65), CT-e (57), MDF-e (58), CT-e OS (67), GTV-e (64), BP-e (63), NF3e (66) and NFCom (62). Accepts a string or a non-negative integer number, stripping any non-digit characters before matching. Exports the `State` type. ```javascript import { getStateByIbgeCode } from '@brazilian-utils/brazilian-utils'; @@ -997,7 +997,7 @@ getTimezoneByState('ZZ'); // null ## getCities -Get Brazilian cities. Returns all cities if no state is provided, or cities from a specific state. Each call returns a fresh array, so mutating the result never affects subsequent calls. An unknown state code (or a non-`StateCode` value) returns an empty array instead of throwing, except for a falsy one: `getCities(null)` and `getCities('')` are read as "no state given" and return every city, where the stricter `getMunicipalities` returns `[]` for them. +Get Brazilian cities. Returns all cities if no state is provided, or cities from a specific state. Each call returns a fresh array, so mutating the result never affects subsequent calls. An unknown state code (or a non-`StateCode` value) returns an empty array instead of throwing, except for a falsy one: `getCities(null)` and `getCities('')` are read as "no state given" and return every city, where the stricter `getMunicipalities` returns `[]` for them. The state code is matched exactly, case included: `getCities('sp')` returns `[]` where `getCities('SP')` returns the 645 São Paulo cities. `getCities` and `getMunicipalities` are the only state-taking lookups that are case-sensitive; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` and `getMunicipality` all fold case. ```javascript import { getCities } from '@brazilian-utils/brazilian-utils'; @@ -1035,7 +1035,7 @@ getCities('SP'); // ] ``` -`getCities` embeds all 5571 IBGE municipality names (~153.6 KB minified, ~49.4 KB gzipped) and is one of the few heavy exceptions in this otherwise tree-shakeable package. See [Bundle size](getting-started.md#bundle-size) for how to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities` instead of the root import. +`getCities` embeds all 5571 IBGE municipality names (~154.0 KB minified, ~49.7 KB gzipped) and is one of the few heavy exceptions in this otherwise tree-shakeable package. See [Bundle size](getting-started.md#bundle-size) for how to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities` instead of the root import. ## getHolidays @@ -1043,7 +1043,7 @@ Get Brazilian holidays for a given year. Returns national holidays and optionall Only one state holiday per UF is a feriado civil under [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm), art. 1º, II, which authorises "a data magna do Estado fixada em lei estadual" in the singular; the other entries rest on ordinary state laws and are reported because they are observed in practice. Notable per-state rules: -- **SC** — [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) moves both state holidays, "Dia do Estado de Santa Catarina" (Aug 11) and "Dia de Santa Catarina de Alexandria" (Nov 25), to the following Sunday whenever they fall Monday to Friday, so Monday Aug 11 2025 is a business day in SC and the holiday lands on Sunday Aug 17. The transfer starts in 2005, the year [Lei SC nº 13.408/2005](http://leis.alesc.sc.gov.br/html/2005/13408_2005_lei.html) first introduced it (published and in force on Jul 15 2005); up to 2004 both holidays stay on Aug 11 and Nov 25 whatever weekday they fall on. +- **SC** — [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) moves both state holidays, "Dia do Estado de Santa Catarina" (Aug 11) and "Dia de Santa Catarina de Alexandria" (Nov 25), to the following Sunday whenever they fall Monday to Friday, so Monday Aug 11 2025 is a business day in SC and the holiday lands on Sunday Aug 17. The two dates did not start transferring together. Aug 11 transfers from 2005 on, the year [Lei SC nº 13.408/2005](http://leis.alesc.sc.gov.br/html/2005/13408_2005_lei.html) extended the clause to it (published and in force on Jul 15 2005), and stays on Aug 11 before that. Nov 25 transfers from 1999 on, the year [Lei SC nº 11.213/1999](http://leis.alesc.sc.gov.br/html/1999/11213_1999_lei.html) first introduced the clause (published and in force on Nov 12 1999, thirteen days before that year's Nov 25), with a one-year gap: art. 3º of [Lei SC nº 12.906/2004](http://leis.alesc.sc.gov.br/html/2004/12906_2004_lei.html) revoked that law without restating the clause, so Nov 25 2004 alone stays on the statutory date until Lei SC nº 13.408/2005 reinstated the transfer. So Nov 25 1999 (a Thursday) lands on Sunday Nov 28, Nov 25 2002 (a Monday) on Sunday Dec 1, Nov 25 2004 (a Thursday) stays put, and Nov 25 2005 (a Friday) lands on Sunday Nov 27. - **DF** — [Lei distrital nº 72/1989](https://www.sinj.df.gov.br/sinj/Norma/18459/Lei_72_27_12_1989.html), art. 1º parágrafo único, declares Corpus Christi a feriado. With `stateCode: 'DF'` the single Corpus Christi entry comes back typed `"state"` instead of `"optional"`; it is replaced, not duplicated. - **GO** — [Lei GO nº 20.756/2020](https://legisla.casacivil.go.gov.br/pesquisa_legislacao/100979/lei-20756), art. 269, II, lists three feriados estaduais: Jul 26 (Fundação da Cidade de Goiás), Oct 24 (Lançamento da Pedra Fundamental de Goiânia) and Oct 28 (Dia do Servidor Público). - **AL** — Sep 16 is a feriado estadual from 2024 ([Lei AL nº 9.358/2024](https://sapl.al.al.leg.br/norma/3117)) and only a ponto facultativo (`"optional"`) before that. @@ -1073,7 +1073,7 @@ getHolidays({ year: 2024, stateCode: 'SP' }); ## isValidPassport -Check if a Brazilian passport number is valid (2 letters followed by 6 digits). Accepts both `string` and `number` input; the input is case-insensitive and any non-alphanumeric characters (spaces, dots, hyphens) are ignored. +Check if a Brazilian passport number is valid (2 letters followed by 6 digits). Accepts both `string` and `number` input; the input is case-insensitive and any non-alphanumeric characters (spaces, dots, hyphens) are ignored. A number is accepted for symmetry with `formatPassport`/`parsePassport` but is never valid: the decimal form of a number never starts with the two letters a passport number needs. ```javascript import { isValidPassport } from '@brazilian-utils/brazilian-utils'; @@ -1171,7 +1171,7 @@ parseCnh('026503064-61'); // '02650306461' ## getCepInfoByAddress -Fetch CEPs from an address using ViaCEP. Throws `GetCepInfoByAddressValidationError` when the UF, city or street is missing/invalid, `GetCepInfoByAddressNotFoundError` when no address matches the query, and `GetCepInfoByAddressError` when ViaCEP itself answers with an HTTP error status. A request that cannot be performed at all (a transport failure) rejects with the underlying `fetch` error instead. +Fetch CEPs from an address using ViaCEP. Throws `GetCepInfoByAddressValidationError` when the UF, city or street is missing/invalid — including when the argument is not an object at all (omitted, `null`, a string) and when `federalUnit` is not a string, neither of which leaks a raw `TypeError` — `GetCepInfoByAddressNotFoundError` when no address matches the query, and `GetCepInfoByAddressError` when ViaCEP itself answers with an HTTP error status. A request that cannot be performed at all (a transport failure) rejects with the underlying `fetch` error instead. ```javascript import { getCepInfoByAddress } from '@brazilian-utils/brazilian-utils'; @@ -1307,6 +1307,8 @@ generateLicensePlate(); // 'ABC1D23' (Mercosul, the default) generateLicensePlate('LLLNNNN'); // 'ABC1234' ``` +A `format` string outside the two supported literals is not rejected: it is used verbatim, character by character, with `L` producing a letter and every other position a digit. So `generateLicensePlate('LLLNNLN')` returns a plate in the withdrawn motorcycle sequence, which `isValidLicensePlate` rejects; `generateLicensePlate('bogus')` returns five digits; and `generateLicensePlate('')` returns an empty string. Only a non-string falls back to the Mercosul default. This is the 2.3.0 behaviour, kept for the JavaScript callers the TypeScript type cannot reach. + ## getFormatLicensePlate Detect the normalized format of a license plate. @@ -1347,7 +1349,7 @@ convertLicensePlateToMercosul('ABC1D23'); // '' (already Mercosul) ## generatePis -Generate a valid random PIS. +Generate a valid random PIS. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generatePis } from '@brazilian-utils/brazilian-utils'; @@ -1403,7 +1405,7 @@ const lookUp = (options: GetMunicipalityOptions) => getMunicipality(options); ## getMunicipalities -Get Brazilian municipalities published by the IBGE. Returns all municipalities if no state is provided, or municipalities from a specific state. Each municipality is returned as `{ code, name, stateCode }`, where `code` is the 7-digit IBGE municipality code. Results are sorted by name with `localeCompare` in the "pt-BR" locale. Each call returns a fresh array of fresh objects, so mutating the result never affects subsequent calls. An unknown state code returns an empty array instead of throwing. Only an omitted (or `undefined`) `stateCode` asks for the full list: `getMunicipalities(null)` and `getMunicipalities('')` return `[]`, where the looser `getCities(null)` and `getCities('')` return every city. +Get Brazilian municipalities published by the IBGE. Returns all municipalities if no state is provided, or municipalities from a specific state. Each municipality is returned as `{ code, name, stateCode }`, where `code` is the 7-digit IBGE municipality code. Results are sorted by name with `localeCompare` in the "pt-BR" locale. Each call returns a fresh array of fresh objects, so mutating the result never affects subsequent calls. An unknown state code returns an empty array instead of throwing. Only an omitted (or `undefined`) `stateCode` asks for the full list: `getMunicipalities(null)` and `getMunicipalities('')` return `[]`, where the looser `getCities(null)` and `getCities('')` return every city. The state code is matched exactly, case included: `getMunicipalities('sp')` returns `[]` where `getMunicipalities('SP')` returns the 645 São Paulo municipalities. `getMunicipalities` and `getCities` are the only state-taking lookups that are case-sensitive; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` and `getMunicipality` all fold case. ```javascript import { getMunicipalities } from '@brazilian-utils/brazilian-utils'; @@ -1466,7 +1468,7 @@ isHoliday(); // false ## isBusinessDay -Check if a date is a Brazilian business day (dia útil). Returns `false` for Saturdays, Sundays, and Brazilian holidays returned by `getHolidays` for `value`'s local calendar day (year/month/day as read locally), the same convention used by `isHoliday`. `options.includeOptional` (part of `BusinessDayOptions`, the option type every business day utility shares) defaults to `true`, so optional-type holidays (`Holiday.type === "optional"`, i.e. Carnaval and Corpus Christi) also count as non-business days; pass `false` to only treat statutory holidays this way. `options.stateCode` also considers that state's holidays; an unknown/invalid `stateCode` is ignored, falling back to national holidays only. A `value` that is not a valid `Date` returns `false`. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `false`. +Check if a date is a Brazilian business day (dia útil). Returns `false` for Saturdays, Sundays, and Brazilian holidays returned by `getHolidays` for `value`'s local calendar day (year/month/day as read locally), the same convention used by `isHoliday`. `options.includeOptional` (part of `BusinessDayOptions`, the option type every business day utility shares) defaults to `true`, so optional-type holidays (`Holiday.type === "optional"`, i.e. Carnaval and Corpus Christi) also count as non-business days; pass `false` to only treat statutory holidays this way. `options.stateCode` also considers that state's holidays; a string that is not a known state code is ignored, falling back to national holidays only, while a `stateCode` that is present and is not a string at all (a number, `null`, an object) is rejected and makes the call return `false` even for an ordinary weekday, the same split `isHoliday` makes and the value `addBusinessDays`, `subBusinessDays` and `differenceInBusinessDays` reject with `null`. A `value` that is not a valid `Date` returns `false`. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `false`. ```javascript import { isBusinessDay } from '@brazilian-utils/brazilian-utils'; @@ -1596,7 +1598,7 @@ parseVoterId('1234 5678 8 01 91'); // '1234567880191' (13-digit SP/MG voter id) ## isValidCns -Check if a CNS (Cartão Nacional de Saúde) number is valid, the unique SUS (Sistema Único de Saúde) user identifier. Definitive cards (starting with 1 or 2) are validated with the same mod 11 weighting used for PIS numbers over an embedded 11 digit base, adjusting the base by +2 when the raw check digit computes to 10. Provisional cards (starting with 7, 8 or 9) are validated instead by a single weighted sum (weights 15 down to 1) that must be a multiple of 11. The value has to be written as the 15 digits, optionally split into the printed groups of 3-4-4-4 by whitespace or the usual mask characters; letters among the digits are rejected instead of being read past. +Check if a CNS (Cartão Nacional de Saúde) number is valid, the unique SUS (Sistema Único de Saúde) user identifier. Definitive cards (starting with 1 or 2) are validated over an embedded 11 digit PIS/PASEP/NIS derived base weighted 15 down to 5; when the raw digit computes to 10, DATASUS raises the weighted sum by 2, recomputes the digit and marks the card with the suffix `001` instead of `000`. Provisional cards (starting with 7, 8 or 9) are validated instead by a single weighted sum (weights 15 down to 1) that must be a multiple of 11. The value has to be written as the 15 digits, optionally split into the printed groups of 3-4-4-4 by whitespace or the usual mask characters, a run of them between two groups included; letters among the digits are rejected instead of being read past. The two routines come from the [ANVISA CNS validation page](https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/), which sits behind a bot filter and answers HTTP 403 to non-browser clients. The [e-SUS APS page](https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html) documents the same algorithm and is reachable without a browser, but applies the provisional routine to numbers starting with 5, 7, 8 or 9; this implementation follows ANVISA and rejects a 5-prefixed number even when its weighted sum checks out. @@ -1641,7 +1643,7 @@ isValidCertidao('104539 01 55 2013 1 00012 021 0000123 21', { accept: ['death'] ## parseCertidao -Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. [Art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lists the codes 1 to 7; the codes 8 (emancipação) and 9 (interdição) come from Anexo IV of the revoked Provimento CNJ nº 63/2017, as listed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm), and are kept because matrículas issued under it are still in circulation. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. +Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. [Art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lists the codes 1 to 7; no CNJ primary text reachable today publishes the other two, the Anexo IV of the revoked Provimento CNJ nº 63/2017 included, which lists the same seven. The codes 8 (emancipação) and 9 (interdição) come from the references the check digit rule rests on: [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) both print the nine book list. They are kept because matrículas carrying them circulate. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. ```javascript import { parseCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1692,7 +1694,7 @@ formatCertidao('1552010100020112000012087', { pad: true }); // 000000 01 55 2010 ## isValidCei -Check if a CEI (Cadastro Específico do INSS) number is valid. The CEI identifies an employer with no CNPJ, such as a construction work or a rural producer: 12 digits printed as `00.000.00000/00`, the last one a check digit calculated over the 11 base digits with the weights 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 and 4. Accepts the usual mask characters and whitespace between/around groups. The Receita Federal does not publish this check digit rule, so it follows the reference implementations of [yii2-br-validator](https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php) and [Bigai.Documentos.Brasil](https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs), cross-checked against the [Cadastro Nacional de Obras (CNO) open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) of the Receita Federal. +Check if a CEI (Cadastro Específico do INSS) number is valid. The CEI identifies an employer with no CNPJ, such as a construction work or a rural producer: 12 digits printed as `00.000.00000/00`, the last one a check digit calculated over the 11 base digits with the weights 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 and 4. Accepts the usual mask characters and whitespace between/around groups, a run of them between two groups included. The Receita Federal does not publish this check digit rule, so it follows the reference implementations of [yii2-br-validator](https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php) and [Bigai.Documentos.Brasil](https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs), cross-checked against the [Cadastro Nacional de Obras (CNO) open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) of the Receita Federal. ```javascript import { isValidCei } from '@brazilian-utils/brazilian-utils'; @@ -1744,7 +1746,7 @@ formatCno('979', { pad: true }); // 00.000.00009/79 ## isValidCaepf -Check if a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number is valid. The CAEPF replaced the CEI for individuals who hire employees: 14 digits printed as `000.000.000/000-00`, formed by the 9 digit CPF base of the holder, a 3 digit sequence for the holder's several registrations and 2 check digits. Both check digits use the modulus 11 of the CNPJ, and the resulting pair is then shifted by 12, wrapping around 100. The Receita Federal does not publish the layout or the check digit rule: both are described by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented the same way by [brazilian-values](https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts). +Check if a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number is valid. The CAEPF replaced the CEI for individuals who hire employees: 14 digits printed as `000.000.000/000-00`, formed by the 9 digit CPF base of the holder, a 3 digit sequence for the holder's several registrations and 2 check digits. Both check digits are the CNPJ's modulus 11 in the formulation of the cited reference: the weights cycle from 9 down to 2 from the right and the check digit is the remainder itself, with a remainder of 10 read as 0 — the same digit the CNPJ's 2-to-9 weights with `11 - remainder` produce. The resulting pair is then shifted by 12, wrapping around 100. A base whose 12 digits are all the same is rejected before the check digits are computed, the way `isValidCei` and `isValidCno` reject a repeated CEI/CNO number, so the otherwise well-formed `00000000000012` is invalid. The Receita Federal does not publish the layout or the check digit rule: both are described by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented the same way by [brazilian-values](https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts). ```javascript import { isValidCaepf } from '@brazilian-utils/brazilian-utils'; @@ -1753,7 +1755,8 @@ isValidCaepf('293.118.610/001-84'); // true isValidCaepf('41142260000101'); // true isValidCaepf(29311861000184); // true isValidCaepf('29311861000185'); // false (invalid check digits) -isValidCaepf('00000000000000'); // false (repeated digits) +isValidCaepf('00000000000000'); // false (invalid check digits) +isValidCaepf('00000000000012'); // false (repeated base digits) ``` ## formatCaepf @@ -1900,7 +1903,7 @@ formatNcm(-84713012); // '' (not a non-negative safe integer) ## isValidCfop -Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table. The table is the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), the text in force (current wording given by Ajuste SINIEF 03/24, last amended by Ajuste SINIEF 39/25), not the frozen 2001 text of Ajuste SINIEF 07/01. Only operable codes count: the group and subgroup headings of the official nomenclature, the codes ending in `00` and `50` (1000, 1100, 1150, 5350, ...), are section titles rather than codes a document can carry, so they are rejected. +Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table. The table is the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), the text in force (current wording given by Ajuste SINIEF 03/24, last amended by [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25)), not the frozen 2001 text of Ajuste SINIEF 07/01. Only operable codes count: the group and subgroup headings of the official nomenclature, the codes ending in `00` and `50` (1000, 1100, 1150, 5350, ...), are section titles rather than codes a document can carry, so they are rejected. A string is only read as a code when it is written in one of the documented forms (the 4 digits, or the `N.NNN` form the annex prints, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. @@ -1918,7 +1921,7 @@ isValidCfop(-5102); // false (not a non-negative safe integer) ## getCfop -Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description, as the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24) words it. The group and subgroup headings of the official nomenclature, the codes ending in `00` and `50`, are not in the table and give `null`. Same input rules as `isValidCfop`. +Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description, as the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24) words it, in the text in force, last amended by [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25). The group and subgroup headings of the official nomenclature, the codes ending in `00` and `50`, are not in the table and give `null`. Same input rules as `isValidCfop`. ```javascript import { getCfop } from '@brazilian-utils/brazilian-utils'; @@ -1943,9 +1946,9 @@ Check if a CST (Código de Situação Tributária) code is valid for a given tax `options.tax` (part of `IsValidCstOptions`) is optional: omit it to accept a code that exists in any one of the four tables above. -The ICMS Tabela B is the one in force: the [consolidated Anexo I of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), whose current wording came from [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (effective 01.12.23) and which [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) amended by revoking items 12, 13, 52, 72 and 74 (effective 09.07.24). `02`, `15`, `53` and `61` are its monofasia de combustíveis codes. +The ICMS Tabela B is the one in force: the [consolidated Anexo I of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), whose current wording came from [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (effective 01.12.23) and which [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) amended by striking items 12, 13, 52, 72 and 74 (effects from 09.07.24) before they ever took effect: 39/23 had added them "sem efeitos", so those codes were never in force. `02`, `15`, `53` and `61` are its monofasia de combustíveis codes. -A string is only read as a code when it is written in one of the documented forms (the 2 or 3 digits, with a single separator between them and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +A string is only read as a code when it is written in one of the documented forms (the 2 digits of a Tabela B code, or the 3 digits of the ICMS form with an optional single separator after the origin digit, plus optional surrounding whitespace), and a number only when it is a non-negative safe integer. The origin digit is the only boundary a printed CST has, so `'0 10'` and `'1-10'` are read while `'0-0'`, `'11-0'` and `'00-'` are not. ```javascript import { isValidCst } from '@brazilian-utils/brazilian-utils'; diff --git a/scripts/cfop.ts b/scripts/cfop.ts index bc616d46..95cec93e 100644 --- a/scripts/cfop.ts +++ b/scripts/cfop.ts @@ -103,6 +103,8 @@ const main = async (): Promise => { * Anexo II of Convênio SINIEF s/nº 1970, the CFOP table in force. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70 * Convênio SINIEF s/nº 1970, the consolidated text the annex belongs to. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25 + * Ajuste SINIEF 39/25, the last amendment the annex carries (CFOP 7.667, from 01.02.26). * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2001/AJ_007_01 * Ajuste SINIEF 07/01, the historical text that gave the CFOP its 4 digit form. */ diff --git a/src/_internals/constants/cfop.ts b/src/_internals/constants/cfop.ts index 7184c7fe..ee47d5fb 100644 --- a/src/_internals/constants/cfop.ts +++ b/src/_internals/constants/cfop.ts @@ -16,6 +16,8 @@ * Anexo II of Convênio SINIEF s/nº 1970, the CFOP table in force. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70 * Convênio SINIEF s/nº 1970, the consolidated text the annex belongs to. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25 + * Ajuste SINIEF 39/25, the last amendment the annex carries (CFOP 7.667, from 01.02.26). * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2001/AJ_007_01 * Ajuste SINIEF 07/01, the historical text that gave the CFOP its 4 digit form. */ diff --git a/src/_internals/constants/iban.ts b/src/_internals/constants/iban.ts index b1b83350..b82cfc01 100644 --- a/src/_internals/constants/iban.ts +++ b/src/_internals/constants/iban.ts @@ -8,6 +8,9 @@ * usual values. Circular BCB nº 3.625/2013 art. 2º § 1º numbers the owner indicator `1` for * the first or only holder, `2` for the second and so on up to the ninth, then `A` to `Z` from * the tenth, so `0` is not a valid owner indicator. + * The two sources disagree on the account type: art. 2º VI of the same Circular calls it "um + * caractere alfanumérico", while the ISO 13616 registry pattern `1!a` makes it a letter, and the + * registry is the form followed here, so a digit in that position is deliberately rejected. * Only Brazilian IBANs follow this layout; every other ISO 13616 country has its own. * @see Official: https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf Circular BCB nº 3.625/2013 * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf Diretrizes de Implementação do IBAN no Brasil diff --git a/src/convert-license-plate-to-mercosul/constants.ts b/src/convert-license-plate-to-mercosul/constants.ts index 1daac82d..adb088ea 100644 --- a/src/convert-license-plate-to-mercosul/constants.ts +++ b/src/convert-license-plate-to-mercosul/constants.ts @@ -2,13 +2,15 @@ * Digit to letter conversion table used to turn an old format plate's 5th character into the * Mercosul format's embedded letter (0=A, 1=B, ..., 9=J). * - * Resolução CONTRAN nº 969/2022, art. 2º § 4º, is what requires the substitution. The table - * itself is Anexo II of that resolution, which is not published at a stable public URL: the - * linked DOU PDF carries no annexes and the CONTRAN resolutions index does not host the annex - * either, so it is cited as `Based on:` rather than as an official document a reader can open. + * Resolução CONTRAN nº 969/2022, art. 2º § 4º, is what requires the substitution, "conforme + * padrão previsto no Anexo II". The table itself is that Anexo II, which calls it a "tabela + * equiparativa, para substituição do antepenúltimo caractere, de número para letra". Its range + * of letters is deliberately limited to `A` through `J`, "apenas para a conversão da PNU para o + * novo sistema de PIV". The annexes are published in a PDF of their own, separate from the + * resolution's text; both are cited below. * * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022.pdf - * @see Based on: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes + * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022anexos.pdf */ export const DIGIT_TO_MERCOSUL_LETTER: Record = { "0": "A", diff --git a/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.ts b/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.ts index c1064ea2..13ccd267 100644 --- a/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.ts +++ b/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.ts @@ -21,13 +21,13 @@ import { DIGIT_TO_MERCOSUL_LETTER } from "./constants"; * ``` * * Resolução CONTRAN nº 969/2022, art. 2º § 4º, is what requires the substitution of the second - * numeric character. The digit to letter table itself is Anexo II of that resolution, which is - * not published at a stable public URL: the linked DOU PDF carries no annexes and the CONTRAN - * resolutions index does not host the annex either, so the table below is cited as `Based on:` - * rather than as an official document a reader can open. + * numeric character, "conforme padrão previsto no Anexo II". Anexo II is the digit to letter + * table, and it prints the same worked example as above: "A placa anterior ABC1234 será + * substituída pela nova placa com o padrão alfanumérico ABC1C34". The annexes are published in a + * PDF of their own, separate from the resolution's text; both are cited below. * * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022.pdf - * @see Based on: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes + * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022anexos.pdf */ export const convertLicensePlateToMercosul = (value: string): string => { if (getFormatLicensePlate(value) !== "LLLNNNN") return ""; diff --git a/src/format-cns/format-cns.test.ts b/src/format-cns/format-cns.test.ts index 687cbbef..e26280fd 100644 --- a/src/format-cns/format-cns.test.ts +++ b/src/format-cns/format-cns.test.ts @@ -13,6 +13,11 @@ describe("formatCns", () => { expect(formatCns("123456789010001")).toBe("123 4567 8901 0001"); }); + it("should round trip 898 0000 0004 3208, the only concrete CNS the ANVISA page prints", () => { + expect(formatCns("898000000043208")).toBe("898 0000 0004 3208"); + expect(formatCns("898 0000 0004 3208")).toBe("898 0000 0004 3208"); + }); + it("should format a number CNS with the space mask", () => { expect(formatCns(123_456_789_010_001)).toBe("123 4567 8901 0001"); }); diff --git a/src/format-currency/format-currency.ts b/src/format-currency/format-currency.ts index 5f646b2e..0d0193d5 100644 --- a/src/format-currency/format-currency.ts +++ b/src/format-currency/format-currency.ts @@ -57,7 +57,8 @@ const toNumber = (value: unknown, precision: number): number => { * symbol, a null-prototype object or a plain object (`Number({})` is `NaN`); every other * value goes through `Number()` the way 2.3.0 did, so `null`, `[]` and `true` still format. * - * The precision is clamped to `0-20`, the range Node's `Intl.NumberFormat` accepts, and a + * The precision is clamped to `0-20`, the package limit, the bound Node 20 still enforces on + * `Intl.NumberFormat` (ES2023 raised it to 100, and newer runtimes accept more), and a * precision that is not a finite number falls back to 2. * * @param {string|number} value - The value to be formatted. Can be a string or a number. diff --git a/src/format-legal-nature/format-legal-nature.ts b/src/format-legal-nature/format-legal-nature.ts index 8283f388..2dc782ae 100644 --- a/src/format-legal-nature/format-legal-nature.ts +++ b/src/format-legal-nature/format-legal-nature.ts @@ -13,6 +13,10 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * formatLegalNature("2062"); // "206-2" * ``` * + * The CONCLA table page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser; the detailed structure PDF next to it is served + * normally. + * * @see Official: https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 * @see Official: https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf */ diff --git a/src/format-license-plate/format-license-plate.ts b/src/format-license-plate/format-license-plate.ts index a425a751..a2045eb9 100644 --- a/src/format-license-plate/format-license-plate.ts +++ b/src/format-license-plate/format-license-plate.ts @@ -19,7 +19,12 @@ import { OLD_FORMAT_SEPARATOR_INDEX } from "./constants"; * formatLicensePlate("1234567"); // "" * ``` * + * The `AAA-1111` shape of the old PNU is art. 2º § 3º of Resolução CONTRAN nº 969/2022; the + * separatorless `LLLNLNN` shape of the Mercosul plate is item 1.2 of its Anexo I, published in a + * PDF of its own. Both are cited below. + * * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022.pdf + * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022anexos.pdf */ export const formatLicensePlate = (value: string): string => { const parsed = parseLicensePlate(value); diff --git a/src/format-voter-id/format-voter-id.ts b/src/format-voter-id/format-voter-id.ts index 69fa08a0..6e702c58 100644 --- a/src/format-voter-id/format-voter-id.ts +++ b/src/format-voter-id/format-voter-id.ts @@ -28,7 +28,13 @@ const LENGTH = 12; * * The 13-digit São Paulo/Minas Gerais grouping is brutils parity, not published by the TSE. A * 14-or-more-digit input is read the same way as a 13-digit one: it is grouped as a São Paulo or - * Minas Gerais id whenever its 10th and 11th digits are "01"/"02", extra trailing digits included. + * Minas Gerais id whenever its 10th and 11th digits are "01"/"02". Both patterns have a fixed + * number of slots, 12 and 13, so anything past the last slot is dropped: + * `formatVoterId("12345678801912")` returns "1234 5678 8 01 91", the same string the 13-digit + * value "1234567880191" produces. + * + * The TSE resolution page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser. * * @see Official: https://www.tse.jus.br/legislacao/compilada/res/2021/resolucao-no-23-659-de-26-de-outubro-de-2021 * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/voter_id.py diff --git a/src/generate-cnpj/generate-cnpj.ts b/src/generate-cnpj/generate-cnpj.ts index a8cf2d75..ac05a2e0 100644 --- a/src/generate-cnpj/generate-cnpj.ts +++ b/src/generate-cnpj/generate-cnpj.ts @@ -63,7 +63,7 @@ const generateAlphanumericCnpj = (): string => { * * Uses `Math.random()` internally, so it is not cryptographically secure, do not use for security purposes. * - * @param {1 | 2} version - The version of the CNPJ to be generated: `1` for the numeric CNPJ and + * @param {1 | 2} [version] - The version of the CNPJ to be generated: `1` for the numeric CNPJ and * `2` for the alphanumeric one. Defaults to `1`, and never throws: `null`, `undefined` and any * other runtime value that is not `2` also generate a version 1 (numeric) CNPJ. * @returns {string} A valid 14-digit CNPJ string without formatting. diff --git a/src/generate-cpf/generate-cpf.ts b/src/generate-cpf/generate-cpf.ts index 709c6c6e..cb4b14b2 100644 --- a/src/generate-cpf/generate-cpf.ts +++ b/src/generate-cpf/generate-cpf.ts @@ -19,7 +19,7 @@ const calculateCheckDigit = (base: string, weight: number): string => { * * Uses `Math.random()` internally, so it is not cryptographically secure, do not use for security purposes. * - * @param {StateCode} state - Optional. The Brazilian state code to generate a CPF for. + * @param {StateCode} [state] - The Brazilian state code to generate a CPF for. * @returns {string} A valid 11-digit CPF string without formatting. * * @example @@ -29,12 +29,17 @@ const calculateCheckDigit = (base: string, weight: number): string => { * ``` * * The região fiscal digit in the 9th position comes from the Receita Federal's folheto - * "Cadastros: CPF e CNPJ"; the check digit rule (`REGRA_VALIDA_CPF`) is specified, with a worked - * example (`280012389-38`), in the Receita Federal's Manual e-Financeira, Anexo II. + * "Cadastros: CPF e CNPJ"; the check digit rule (`REGRA_VALIDA_CPF`) is specified, with the + * worked example `280012389-38`, in the Receita Federal's Manual de Preenchimento da + * e-Financeira, Anexo II — Leiautes Gerais, approved by the Ato Declaratório Executivo Cofis + * nº 10, de 25 de maio de 2026. The manual's own file used to be served from `sped.rfb.gov.br`, + * a host that no longer answers at all, so the approving act is cited below in its place; its + * Receita Federal permalink redirects into the norms viewer, which has to be opened in a + * browser. * * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/meu-cpf * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/educacao-fiscal/educacao_fiscal/folhetos-orientativos/cadastros-dig.pdf - * @see Official: http://sped.rfb.gov.br/arquivo/show/8231 + * @see Official: https://normas.receita.fazenda.gov.br/sijut2consulta/link.action?idAto=151372 * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/cpf.py */ export const generateCpf = (state?: StateCode): string => { diff --git a/src/generate-legal-nature/generate-legal-nature.ts b/src/generate-legal-nature/generate-legal-nature.ts index 72cce239..031b4173 100644 --- a/src/generate-legal-nature/generate-legal-nature.ts +++ b/src/generate-legal-nature/generate-legal-nature.ts @@ -12,6 +12,10 @@ import { LEGAL_NATURE } from "../is-valid-legal-nature/constants"; * generateLegalNature(); // "2062" * ``` * + * The CONCLA table page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser; the detailed structure PDF next to it is served + * normally. + * * @see Official: https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 * @see Official: https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf */ diff --git a/src/generate-license-plate/generate-license-plate.ts b/src/generate-license-plate/generate-license-plate.ts index 946ad6e2..0e19fa25 100644 --- a/src/generate-license-plate/generate-license-plate.ts +++ b/src/generate-license-plate/generate-license-plate.ts @@ -17,9 +17,9 @@ const randomDigit = (): string => Math.floor(Math.random() * 10).toString(); * Uses `Math.random()` internally, so it is not cryptographically secure, do not use for * security purposes. * - * @param {GenerateLicensePlateFormat} format - The format to generate. Defaults to the - * Mercosul format ("LLLNLNN"), the single sequence Resolução CONTRAN nº 969/2022 defines - * for every vehicle, motorcycles included. + * @param {GenerateLicensePlateFormat} [format] - The format to generate. Defaults to the + * Mercosul format ("LLLNLNN"), the single sequence Resolução CONTRAN nº 969/2022 defines for + * every vehicle, motorcycles included. * @returns {string} A randomly generated license plate matching the requested format. * * @example @@ -28,7 +28,22 @@ const randomDigit = (): string => Math.floor(Math.random() * 10).toString(); * generateLicensePlate("LLLNNNN"); // "ABC1234" (old Brazilian format) * ``` * + * The resolution's own text does not spell the sequence out: art. 2º § 2º delegates the + * technical specification to Anexo I, whose item 1.2 reads "O padrão de estampagem é composto de + * 7 (sete) caracteres alfanuméricos, em alto relevo, na sequência LLLNLNN" and whose item 1.2.1 + * reads `L` as a letter and `N` as a numeral. The annexes are published in a PDF of their own, + * cited below alongside the resolution's text. + * + * A `format` string outside the two supported literals is not rejected: it is used verbatim, + * character by character, `L` producing a letter and every other position a digit, which is the + * 2.3.0 behaviour and is kept for the JavaScript callers the type cannot reach. So + * `generateLicensePlate("LLLNNLN")` returns a plate in the withdrawn motorcycle sequence, which + * `isValidLicensePlate` rejects, `generateLicensePlate("bogus")` returns five digits and + * `generateLicensePlate("")` returns an empty string. Only a non-string falls back to the + * default. Pass one of the two literals to get a plate the library considers valid. + * * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022.pdf + * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022anexos.pdf */ export const generateLicensePlate = ( format: GenerateLicensePlateFormat = DEFAULT_FORMAT, diff --git a/src/generate-pix-payload/generate-pix-payload.ts b/src/generate-pix-payload/generate-pix-payload.ts index a6640ab4..111a6cd7 100644 --- a/src/generate-pix-payload/generate-pix-payload.ts +++ b/src/generate-pix-payload/generate-pix-payload.ts @@ -155,9 +155,10 @@ const resolveFormattedAmount = ( * accepts the others too. The Pix Saque BR Code, which announces the ISPB of the "facilitador de * serviço de saque" in sub-object 26-03 (`fss`), is not generated here, only parsed. * - * Payloads that carry the location in an Unreserved Template (IDs 80 to 99), as the "QR Code - * composto" of Pix Automático (Pix recorrente) does, are out of scope: the location is always - * written in the "Merchant Account Information" template. + * Unreserved Templates (IDs 80 to 99) are never written: the location always goes in the + * "Merchant Account Information" template, so the "QR Code composto" of Pix Automático (Pix + * recorrente), which puts its recurrence location in one of them, is out of scope here. + * `parsePixPayload` does read a composto, but only as an ordinary dynamic payload. * * The merchant name, the merchant city and the description are folded to printable ASCII * (accents are dropped) and truncated to the lengths the BR Code allows, the description to @@ -203,7 +204,8 @@ const resolveFormattedAmount = ( * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/spb_docs/ManualBRCode.pdf * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf * @see Official: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. - * @see Official: https://github.com/bacen/pix-dict-api DICT OpenAPI spec. + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html + * DICT (Diretório de Identificadores de Contas Transacionais) API specification. */ export const generatePixPayload = (params: GeneratePixPayloadParams): string | null => { if (isNullish(params) || typeof params !== "object") return null; diff --git a/src/generate-voter-id/generate-voter-id.ts b/src/generate-voter-id/generate-voter-id.ts index 57102d97..3a3d1abd 100644 --- a/src/generate-voter-id/generate-voter-id.ts +++ b/src/generate-voter-id/generate-voter-id.ts @@ -24,6 +24,9 @@ import { UF_TO_VOTER_ID_CODE } from "../is-valid-voter-id/constants"; * the two-step módulo 11 structure; the weights themselves are not published by the TSE and follow * the community reference cited as `Based on:`. * + * The TSE resolution page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser. + * * @see Official: https://www.tse.jus.br/legislacao/compilada/res/2021/resolucao-no-23-659-de-26-de-outubro-de-2021 * @see Based on: https://siga0984.wordpress.com/2019/05/01/algoritmos-validacao-de-titulo-de-eleitor/ */ diff --git a/src/get-area-code-info/get-area-code-info.ts b/src/get-area-code-info/get-area-code-info.ts index d8db7dec..4472da57 100644 --- a/src/get-area-code-info/get-area-code-info.ts +++ b/src/get-area-code-info/get-area-code-info.ts @@ -25,12 +25,15 @@ export type AreaCodeInfo = { /** * Retrieves the state (and its region) a Brazilian DDD (area code) belongs to. * - * `stateCode` is always a single state: the one that holds all but a handful of the DDD's + * `stateCode` is always a single state: the one the DDD is seated in, the state of the city the + * code was allocated around, which is not necessarily the state holding most of its * municipalities. Four DDDs straddle a state border, and for those `stateCodes` lists the * other states too. DDD 61 is the widest of them, serving the Distrito Federal and the twelve * Goiás municipalities of the Entorno do Distrito Federal, so its `stateCode` is `"DF"` and - * its `stateCodes` is `["DF", "GO"]`. The other three are 42 (`["PR", "SC"]`, for Porto - * União), 47 (`["SC", "PR"]`, for Rio Negro) and 49 (`["SC", "PR"]`, for Barracão). + * its `stateCodes` is `["DF", "GO"]` even though the Distrito Federal holds only one of its + * thirteen municipalities, Brasília. The other three are 42 (`["PR", "SC"]`, for Porto + * União), 47 (`["SC", "PR"]`, for Rio Negro) and 49 (`["SC", "PR"]`, for Barracão), and there + * the seat does hold every municipality but the one named. * * A `areaCode` given as a number must be a non-negative integer: a sign and a decimal point * are not digits, so `-11` and `1.1` are rejected instead of being read as `11`. @@ -41,12 +44,13 @@ export type AreaCodeInfo = { * 67 DDDs in use under the Plano Geral de Numeração. * * Resolução Anatel nº 749/2022, art. 15, defines the Código Nacional (area code); the gov.br - * page below lists the codes actually allocated and links to the Anexo of Resolução Anatel - * nº 263/2001, which gives the Código Nacional of every municipality. + * page below lists the codes actually allocated and links, under "POR MUNICÍPIO", to the Anexo + * of Resolução Anatel nº 263/2001, which gives the Código Nacional of every municipality. * * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749 * @see Official: https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais - * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2001/383-resolucao-263 + * @see Based on: https://informacoes.anatel.gov.br/legislacao/resolucoes/2001/383-resolucao-263 + * Anexo of Resolução nº 263/2001 (revoked; still the table Anatel's Códigos Nacionais page links to). * @see Based on: https://brasilapi.com.br/docs#tag/DDD * * @example diff --git a/src/get-area-codes-by-state/get-area-codes-by-state.ts b/src/get-area-codes-by-state/get-area-codes-by-state.ts index 04a8f88c..5bcd2349 100644 --- a/src/get-area-codes-by-state/get-area-codes-by-state.ts +++ b/src/get-area-codes-by-state/get-area-codes-by-state.ts @@ -27,13 +27,14 @@ import { AREA_CODE_SECONDARY_STATES, AREA_CODE_STATES } from "../_internals/cons * getAreaCodesByState("XX"); // [] * ``` * - * Resolução Anatel nº 749/2022, art. 15, defines the Código Nacional (area code); the gov.br - * page below lists the codes actually allocated and links to the Anexo of Resolução Anatel - * nº 263/2001, which gives the Código Nacional of every municipality. + * Resolução Anatel nº 749/2022, art. 15, defines the Código Nacional (area code). The Anexo the + * gov.br page below links to, giving the Código Nacional of every municipality, is the one this + * inverse lookup was derived from and is no longer in force. * * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749 * @see Official: https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais - * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2001/383-resolucao-263 + * @see Based on: https://informacoes.anatel.gov.br/legislacao/resolucoes/2001/383-resolucao-263 + * Anexo of Resolução nº 263/2001, revoked, and still the table Anatel's page links to. */ export const getAreaCodesByState = (stateCode: string): number[] => { if (typeof stateCode !== "string") return []; diff --git a/src/get-cfop/get-cfop.ts b/src/get-cfop/get-cfop.ts index b7ddca6c..e256b0dd 100644 --- a/src/get-cfop/get-cfop.ts +++ b/src/get-cfop/get-cfop.ts @@ -48,6 +48,8 @@ export type Cfop = { * Anexo II of Convênio SINIEF s/nº 1970, the CFOP table in force. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70 * Convênio SINIEF s/nº 1970, the consolidated text the annex belongs to. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25 + * Ajuste SINIEF 39/25, the last amendment the annex carries (CFOP 7.667, from 01.02.26). * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2001/AJ_007_01 * Ajuste SINIEF 07/01, the historical text that gave the CFOP its 4 digit form. */ diff --git a/src/get-cities/get-cities.ts b/src/get-cities/get-cities.ts index 6eded094..83448c1d 100644 --- a/src/get-cities/get-cities.ts +++ b/src/get-cities/get-cities.ts @@ -17,12 +17,18 @@ let allCitiesCache: string[] | undefined; * every city. The sibling `getMunicipalities` is stricter and only reads an omitted (or * `undefined`) state code that way, returning `[]` for `null` and `""`. * + * The state code is matched exactly, case included: `getCities("sp")` returns `[]` where + * `getCities("SP")` returns the 645 São Paulo cities. `getCities` and `getMunicipalities` are + * the only state-taking lookups that are case-sensitive; `getStateNameByCode`, + * `getTimezoneByState`, `getAreaCodesByState` and `getMunicipality` all fold case. + * * @param {StateCode} [state] - The code of the Brazilian state to filter cities by. Optional. * @returns {string[]} An array of city names, sorted alphabetically. Returns an empty array if the state is not found. * * @example * ```typescript * getCities("SP")[0]; // "Adamantina" + * getCities("sp"); // [] (the state code is case-sensitive here) * getCities().length; // every city of every state * ``` * diff --git a/src/get-format-license-plate/get-format-license-plate.ts b/src/get-format-license-plate/get-format-license-plate.ts index 9c76c3e9..775736f8 100644 --- a/src/get-format-license-plate/get-format-license-plate.ts +++ b/src/get-format-license-plate/get-format-license-plate.ts @@ -29,7 +29,16 @@ export type LicensePlateFormat = "LLLNNNN" | "LLLNLNN"; * getFormatLicensePlate("ABC1234EXTRA"); // null (too many characters) * ``` * + * The resolution's own text does not spell the sequence out: art. 2º § 2º delegates the + * technical specification to Anexo I, whose item 1.2 reads "O padrão de estampagem é composto de + * 7 (sete) caracteres alfanuméricos, em alto relevo, na sequência LLLNLNN" and whose item 1.2.1 + * reads `L` as a letter and `N` as a numeral. Art. 2º § 1º puts a single rear plate of that same + * standard on motorcycles and similar vehicles, and art. 2º § 3º describes the old `AAA-1111` + * PNU it coexists with. The annexes are published in a PDF of their own, cited below alongside + * the resolution's text. + * * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022.pdf + * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022anexos.pdf */ export const getFormatLicensePlate = (value: string): LicensePlateFormat | null => { if (typeof value !== "string") return null; diff --git a/src/get-holidays/get-holidays.ts b/src/get-holidays/get-holidays.ts index e6679f70..65144f81 100644 --- a/src/get-holidays/get-holidays.ts +++ b/src/get-holidays/get-holidays.ts @@ -167,7 +167,13 @@ const computeHolidays = (year: number, stateCode: StateCode | undefined): Holida * Lei 662/1949, the base national holidays law (Ano novo, Dia do trabalhador, Independência do * Brasil, Proclamação da República, Natal). * @see Official: https://www.planalto.gov.br/ccivil_03/leis/2002/l10607.htm - * Lei 10.607/2002, added Tiradentes and Finados to the national holidays. + * Lei 10.607/2002, rewrote that art. 1º into the list in force: it added Finados (2 November) + * to the national holidays and folded in Tiradentes (21 April), already national since art. 3º + * of Lei 1.266/1950, which its own art. 3º revoked. + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/L1266.htm + * Lei 1.266/1950, art. 3º, which first made Tiradentes a national holiday: "É feriado nacional o + * dia 21 de abril, consagrado à glorificação de Tiradentes". Revoked by Lei 10.607/2002 only + * after that law had carried 21 April into Lei 662/1949. * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l6802.htm * Lei 6.802/1980, declared Nossa Senhora Aparecida (12 October) a national holiday. * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2023/lei/l14759.htm @@ -180,14 +186,18 @@ const computeHolidays = (year: number, stateCode: StateCode | undefined): Holida * `STATE_HOLIDAYS`. * @see Official: https://www.in.gov.br/web/dou/-/portaria-mgi-n-11.460-de-29-de-dezembro-de-2025-678388627 * Portaria MGI nº 11.460/2025, the federal executive's annual calendar of feriados nacionais and - * pontos facultativos, reissued every December. It is the source of the typing of the four entries - * derived from Easter, which no federal law declares: "Paixão de Cristo (feriado nacional)" - * (Easter minus 2, emitted as `"Sexta-feira Santa"` typed `national`), "Carnaval (ponto + * pontos facultativos, reissued every December. It is the source of the typing of three of the + * four entries derived from Easter, which no federal law declares: "Paixão de Cristo (feriado + * nacional)" (Easter minus 2, emitted as `"Sexta-feira Santa"` typed `national`), "Carnaval (ponto * facultativo)" (Easter minus 47) and "Corpus Christi (ponto facultativo)" (Easter plus 60), both * typed `optional`. Sexta-feira Santa has no statutory basis of its own: Lei 9.093/1995 art. 2º * places it among the *municipal* religious holidays, and it is typed `national` here because the - * portaria observes it nationwide. Easter itself is emitted as `"Páscoa"` typed `religious`, - * computed with the Meeus/Jones/Butcher algorithm by `resolveStateHolidayDate`. + * portaria observes it nationwide. The fourth entry, Easter Sunday itself, is emitted as + * `"Páscoa"` typed `religious` and has no normative basis at all: the portaria never mentions it, + * no federal law declares it, and its date is derived arithmetically by `resolveStateHolidayDate` + * with the Meeus/Jones/Butcher algorithm. It is a convenience entry, listed because callers + * computing a liturgical calendar expect it, not because it is a holiday anyone observes as a day + * off. * @see Official: state holiday laws are cited individually, one `@see` per holiday, in * `src/get-holidays/constants.ts`. */ diff --git a/src/get-legal-nature/get-legal-nature.ts b/src/get-legal-nature/get-legal-nature.ts index 311cc297..88dfdb4d 100644 --- a/src/get-legal-nature/get-legal-nature.ts +++ b/src/get-legal-nature/get-legal-nature.ts @@ -26,6 +26,10 @@ const lookUp = (code: string): LegalNature | null => { * @returns {LegalNature|null} The matching legal nature entry, or null when the code is unknown * or invalid. * + * The CONCLA table page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser; the detailed structure PDF next to it is served + * normally. + * * @see Official: https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 * @see Official: https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf * diff --git a/src/get-legal-natures/get-legal-natures.ts b/src/get-legal-natures/get-legal-natures.ts index a98b3500..adfc3d83 100644 --- a/src/get-legal-natures/get-legal-natures.ts +++ b/src/get-legal-natures/get-legal-natures.ts @@ -10,6 +10,10 @@ import { LEGAL_NATURE } from "../is-valid-legal-nature/constants"; * getLegalNatures()["2062"]; // "Sociedade Empresária Limitada" * ``` * + * The CONCLA table page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser; the detailed structure PDF next to it is served + * normally. + * * @see Official: https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 * @see Official: https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf */ diff --git a/src/get-municipalities/get-municipalities.ts b/src/get-municipalities/get-municipalities.ts index 75c7a371..a5909b8f 100644 --- a/src/get-municipalities/get-municipalities.ts +++ b/src/get-municipalities/get-municipalities.ts @@ -21,6 +21,11 @@ const buildMunicipalities = (stateCode: StateCode): Municipality[] => * looser and treats every falsy `state` as "no state given", so `getCities(null)` returns the * full list where `getMunicipalities(null)` returns `[]`. * + * The state code is matched exactly, case included: `getMunicipalities("sp")` returns `[]` where + * `getMunicipalities("SP")` returns the 645 São Paulo municipalities. `getMunicipalities` and + * `getCities` are the only state-taking lookups that are case-sensitive; `getStateNameByCode`, + * `getTimezoneByState`, `getAreaCodesByState` and `getMunicipality` all fold case. + * * @param {StateCode} [stateCode] - The two letter code of the Brazilian state to filter by. * @returns {Municipality[]} A fresh array of fresh `Municipality` objects. Empty when * `stateCode` is not a known state. @@ -30,6 +35,7 @@ const buildMunicipalities = (stateCode: StateCode): Municipality[] => * getMunicipalities("SP")[0]; // { code: "3500105", name: "Adamantina", stateCode: "SP" } * getMunicipalities().length; // every municipality of every state * getMunicipalities("ZZ"); // [] + * getMunicipalities("sp"); // [] (the state code is case-sensitive here) * getMunicipalities(null); // [] (only an omitted state code asks for the full list) * ``` * diff --git a/src/get-state-by-ibge-code/get-state-by-ibge-code.ts b/src/get-state-by-ibge-code/get-state-by-ibge-code.ts index 7cc229b9..6c038d6c 100644 --- a/src/get-state-by-ibge-code/get-state-by-ibge-code.ts +++ b/src/get-state-by-ibge-code/get-state-by-ibge-code.ts @@ -9,7 +9,8 @@ export type { State } from "../_internals/constants/states"; * Federação) matches the given value. * * The IBGE code is the same 2-digit UF code found in the first field of every DF-e access key - * (chave de acesso) issued for NF-e, NFC-e, CT-e and MDF-e documents. + * (chave de acesso) issued for any of the models `isValidNfeKey` covers: NF-e (55), NFC-e + * (65), CT-e (57), MDF-e (58), CT-e OS (67), GTV-e (64), BP-e (63), NF3e (66) and NFCom (62). * * A `code` given as a number must be a non-negative integer: a sign and a decimal point are * not digits, so `-35` and `3.5` are rejected instead of being read as `35`. diff --git a/src/is-business-day/is-business-day.ts b/src/is-business-day/is-business-day.ts index 50b1fcc8..6fb40c49 100644 --- a/src/is-business-day/is-business-day.ts +++ b/src/is-business-day/is-business-day.ts @@ -73,9 +73,9 @@ const WEEKEND_DAYS = new Set([0, 6]); * isBusinessDay(new Date(2024, 6, 9), { stateCode: "SP" }); // false (Revolução Constitucionalista) * isBusinessDay(new Date(2024, 6, 9)); // true (state holiday ignored without stateCode) * isBusinessDay(new Date("not a date")); // false + * isBusinessDay(new Date(2024, 6, 9), { stateCode: 5 }); // false (a non-string stateCode is rejected) * isBusinessDay(new Date(2100, 0, 4)); // false (a Monday, but 2100 is outside the supported range) * ``` - * isBusinessDay(new Date(2024, 6, 9), { stateCode: 5 }); // false (a non-string stateCode is rejected) * * The underlying holidays are the ones `getHolidays` computes; see its JSDoc (and * `src/get-holidays/constants.ts` for state holidays) for the full set of laws behind them. @@ -83,7 +83,8 @@ const WEEKEND_DAYS = new Set([0, 6]); * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l0662.htm * Lei 662/1949, the base national holidays law. * @see Official: https://www.planalto.gov.br/ccivil_03/leis/2002/l10607.htm - * Lei 10.607/2002, added Tiradentes and Finados. + * Lei 10.607/2002, added Finados (2 November) and folded in Tiradentes (21 April), which had + * been national since art. 3º of the Lei 1.266/1950 it revoked. * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l6802.htm * Lei 6.802/1980, declared Nossa Senhora Aparecida a national holiday. * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2023/lei/l14759.htm @@ -92,22 +93,25 @@ const WEEKEND_DAYS = new Set([0, 6]); * Lei 9.093/1995, the framework law authorizing state and municipal holidays. * @see Official: https://www.in.gov.br/web/dou/-/portaria-mgi-n-11.460-de-29-de-dezembro-de-2025-678388627 * Portaria MGI nº 11.460/2025, the federal executive's annual calendar of feriados nacionais and - * pontos facultativos: the source of Sexta-feira Santa being observed nationally and of Carnaval - * and Corpus Christi being ponto facultativo, which is what `includeOptional` switches on. + * pontos facultativos: the source of three of the four Easter-derived entries, namely + * Sexta-feira Santa being observed nationally and Carnaval and Corpus Christi being ponto + * facultativo, which is what `includeOptional` switches on. The fourth, Páscoa, has no entry in + * the portaria; `getHolidays` derives Easter Sunday arithmetically with the Meeus/Jones/Butcher + * algorithm, and it never affects this function because Easter is always a Sunday. */ export const isBusinessDay = (value: Date, options?: BusinessDayOptions): boolean => { if (!(value instanceof Date) || Number.isNaN(value.getTime())) return false; + const stateCode = options?.stateCode; + + if (stateCode !== undefined && typeof stateCode !== "string") return false; + const year = value.getFullYear(); if (!isSupportedHolidayYear(year)) return false; if (WEEKEND_DAYS.has(value.getDay())) return false; - const stateCode = options?.stateCode; - - if (stateCode !== undefined && typeof stateCode !== "string") return false; - const includeOptional = options?.includeOptional ?? true; const month = value.getMonth(); diff --git a/src/is-holiday/is-holiday.ts b/src/is-holiday/is-holiday.ts index 6707826e..99bf2481 100644 --- a/src/is-holiday/is-holiday.ts +++ b/src/is-holiday/is-holiday.ts @@ -59,7 +59,8 @@ export type IsHolidayOptions = { * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l0662.htm * Lei 662/1949, the base national holidays law. * @see Official: https://www.planalto.gov.br/ccivil_03/leis/2002/l10607.htm - * Lei 10.607/2002, added Tiradentes and Finados. + * Lei 10.607/2002, added Finados (2 November) and folded in Tiradentes (21 April), which had + * been national since art. 3º of the Lei 1.266/1950 it revoked. * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l6802.htm * Lei 6.802/1980, declared Nossa Senhora Aparecida a national holiday. * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2023/lei/l14759.htm @@ -68,8 +69,11 @@ export type IsHolidayOptions = { * Lei 9.093/1995, the framework law authorizing state and municipal holidays. * @see Official: https://www.in.gov.br/web/dou/-/portaria-mgi-n-11.460-de-29-de-dezembro-de-2025-678388627 * Portaria MGI nº 11.460/2025, the federal executive's annual calendar of feriados nacionais and - * pontos facultativos, the only source behind the Easter-derived entries; see the `getHolidays` - * JSDoc for why Sexta-feira Santa is typed `national` without a law of its own. + * pontos facultativos, the source behind three of the four Easter-derived entries: Sexta-feira + * Santa, Carnaval and Corpus Christi. Páscoa is not one of them; the portaria never mentions + * Easter Sunday, whose date `getHolidays` derives arithmetically with the Meeus/Jones/Butcher + * algorithm. See the `getHolidays` JSDoc for why Sexta-feira Santa is typed `national` without a + * law of its own. */ export const isHoliday = (options?: IsHolidayOptions): boolean => { if (isNullish(options) || typeof options !== "object") { diff --git a/src/is-valid-cfop/is-valid-cfop.ts b/src/is-valid-cfop/is-valid-cfop.ts index d04a0f4a..06eac7a5 100644 --- a/src/is-valid-cfop/is-valid-cfop.ts +++ b/src/is-valid-cfop/is-valid-cfop.ts @@ -39,6 +39,8 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * Anexo II of Convênio SINIEF s/nº 1970, the CFOP table in force. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70 * Convênio SINIEF s/nº 1970, the consolidated text the annex belongs to. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25 + * Ajuste SINIEF 39/25, the last amendment the annex carries (CFOP 7.667, from 01.02.26). * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2001/AJ_007_01 * Ajuste SINIEF 07/01, the historical text that gave the CFOP its 4 digit form. */ diff --git a/src/is-valid-cpf/is-valid-cpf.ts b/src/is-valid-cpf/is-valid-cpf.ts index 26fa1ded..98d5ac10 100644 --- a/src/is-valid-cpf/is-valid-cpf.ts +++ b/src/is-valid-cpf/is-valid-cpf.ts @@ -38,11 +38,18 @@ const isValidChecksum = (cpf: string): boolean => { * isValidCpf("12345678900"); // false (invalid checksum) * ``` * - * The check digit rule (`REGRA_VALIDA_CPF`) is specified, with a worked example - * (`280012389-38`), in the Receita Federal's Manual e-Financeira, Anexo II. + * The check digit rule (`REGRA_VALIDA_CPF`) is specified, with the worked example + * `280012389-38`, in the Receita Federal's Manual de Preenchimento da e-Financeira, Anexo II — + * Leiautes Gerais, approved by the Ato Declaratório Executivo Cofis nº 10, de 25 de maio de + * 2026. The manual states the rule in its mirror form, weights 9 down to 1 "a partir da + * unidade" with "o resto 10 é considerado 0", which is algebraically the same digit as the + * weights 10 down to 2 with `11 - resto` implemented above. The manual's own file used to be + * served from `sped.rfb.gov.br`, a host that no longer answers at all, so the approving act is + * cited below in its place; its Receita Federal permalink redirects into the norms viewer, which + * has to be opened in a browser. * * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/meu-cpf - * @see Official: http://sped.rfb.gov.br/arquivo/show/8231 + * @see Official: https://normas.receita.fazenda.gov.br/sijut2consulta/link.action?idAto=151372 * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/cpf.py */ export const isValidCpf = (cpf: string): boolean => { diff --git a/src/is-valid-credit-card/is-valid-credit-card.ts b/src/is-valid-credit-card/is-valid-credit-card.ts index 02c974d6..ffdc2a3e 100644 --- a/src/is-valid-credit-card/is-valid-credit-card.ts +++ b/src/is-valid-credit-card/is-valid-credit-card.ts @@ -38,7 +38,10 @@ const FORMAT_REGEX = /^\d+(?:[ -]+\d+)*$/; * ``` * * ISO/IEC 7812-1 (issuer identification numbers) caps the PAN at 19 digits but sets no - * minimum; the 12-digit floor here is the de-facto industry minimum (e.g. Maestro). + * minimum; the 12-digit floor here is the de-facto industry minimum (e.g. Maestro). The ISO + * catalogue page sits behind a bot filter and answers HTTP 403 to every non-browser client, so + * it has to be opened in a browser, where it renders the standard's paywalled abstract rather + * than its text. * * @see Official: https://www.iso.org/standard/70484.html */ diff --git a/src/is-valid-ie/is-valid-ie.ts b/src/is-valid-ie/is-valid-ie.ts index b49c961d..605bd2a0 100644 --- a/src/is-valid-ie/is-valid-ie.ts +++ b/src/is-valid-ie/is-valid-ie.ts @@ -525,7 +525,9 @@ const IE_VALIDATORS: Record = { * - An all zero registration is accepted for every state whose published formula yields a * check digit of 0 for it (AM, BA with 8 or 9 digits, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, * SC, SE, SP and TO with 9 digits), unlike isValidCpf and isValidCnpj, which reject repeated - * digits. + * digits. AM is on that list through the second branch of its published formula only: the + * page's first branch, "Se Soma < 11 Então Dígito = 11 - Soma", gives 11 for an all zero + * registration, while the "resto <= 1 ⇒ 0" branch, the one implemented here, gives 0. * * @param {StateCode} stateCode - The state abbreviation (e.g., 'SP', 'RJ', 'MG') * @param {string} ie - The state registration number to validate diff --git a/src/is-valid-legal-nature/constants.ts b/src/is-valid-legal-nature/constants.ts index 6d5e62e9..11e3bb4d 100644 --- a/src/is-valid-legal-nature/constants.ts +++ b/src/is-valid-legal-nature/constants.ts @@ -8,6 +8,10 @@ * compatibility. Separately, and unrelated to those legacy codes, the descriptions of the * following official codes fix an accent typo of the PDF: 3298. * + * The CONCLA table page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser; the detailed structure PDF next to it is served + * normally. + * * @see Official: https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 * @see Official: https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf */ diff --git a/src/is-valid-legal-nature/is-valid-legal-nature.ts b/src/is-valid-legal-nature/is-valid-legal-nature.ts index 87caded0..8417135f 100644 --- a/src/is-valid-legal-nature/is-valid-legal-nature.ts +++ b/src/is-valid-legal-nature/is-valid-legal-nature.ts @@ -18,6 +18,10 @@ import { LEGAL_NATURE, MASK_REGEX } from "./constants"; * isValidLegalNature("0000"); // false * ``` * + * The CONCLA table page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser; the detailed structure PDF next to it is served + * normally. + * * @see Official: https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 * @see Official: https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf */ diff --git a/src/is-valid-license-plate/is-valid-license-plate.ts b/src/is-valid-license-plate/is-valid-license-plate.ts index 09028a7d..3882bb9c 100644 --- a/src/is-valid-license-plate/is-valid-license-plate.ts +++ b/src/is-valid-license-plate/is-valid-license-plate.ts @@ -23,7 +23,16 @@ import { getFormatLicensePlate } from "../get-format-license-plate/get-format-li * isValidLicensePlate("invalid"); // false * ``` * + * The resolution's own text does not spell the sequence out: art. 2º § 2º delegates the + * technical specification to Anexo I, whose item 1.2 reads "O padrão de estampagem é composto de + * 7 (sete) caracteres alfanuméricos, em alto relevo, na sequência LLLNLNN" and whose item 1.2.1 + * reads `L` as a letter and `N` as a numeral. Art. 2º § 1º puts a single rear plate of that same + * standard on motorcycles and similar vehicles, and art. 2º § 3º describes the old `AAA-1111` + * PNU it coexists with. The annexes are published in a PDF of their own, cited below alongside + * the resolution's text. + * * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022.pdf + * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022anexos.pdf */ export const isValidLicensePlate = (value: string): boolean => getFormatLicensePlate(value) !== null; diff --git a/src/is-valid-nfe-key/is-valid-nfe-key.test.ts b/src/is-valid-nfe-key/is-valid-nfe-key.test.ts index ebf3790d..b1894aff 100644 --- a/src/is-valid-nfe-key/is-valid-nfe-key.test.ts +++ b/src/is-valid-nfe-key/is-valid-nfe-key.test.ts @@ -185,7 +185,7 @@ describe("isValidNfeKey", () => { expected: false, }, { - name: "model 67, the CT-e OS of the Ajuste SINIEF 09/07", + name: "model 67, the CT-e OS instituted by the cláusula primeira of the Ajuste SINIEF 36/19", key: "35170458716523000119670010000000121000123458", expected: true, }, diff --git a/src/is-valid-nfe-key/is-valid-nfe-key.ts b/src/is-valid-nfe-key/is-valid-nfe-key.ts index b0edffe7..262d6c1c 100644 --- a/src/is-valid-nfe-key/is-valid-nfe-key.ts +++ b/src/is-valid-nfe-key/is-valid-nfe-key.ts @@ -31,9 +31,13 @@ import { parseNfeKey } from "../parse-nfe-key/parse-nfe-key"; * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf * Manual de Orientação do Contribuinte (MOC) NF-e, "chave de acesso". * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07 - * Ajuste SINIEF 09/07, cláusula primeira, § 3.º, II, "b": the CT-e OS, modelo 67. + * Ajuste SINIEF 09/07, cláusula primeira, caput: the CT-e, modelo 57. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19 + * Ajuste SINIEF 36/19, cláusula primeira: the CT-e OS, modelo 67. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20 + * Ajuste SINIEF 03/20, cláusula primeira: the GTV-e, modelo 64. * @see Official: https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais - * CT-e MOC 4.00, Anexo I: modelo 64 (GTV-e) and the `tpEmis` domains D19, D27 and D15. + * CT-e MOC 4.00, Anexo I: the `tpEmis` domains D19, D27 and D15. * @see Official: https://dfe-portal.svrs.rs.gov.br/BPE/Documentos * BP-e MOC 1.00b, Visão Geral and Anexo I: modelo 63. * @see Official: https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos diff --git a/src/is-valid-pix-key/is-valid-pix-key.ts b/src/is-valid-pix-key/is-valid-pix-key.ts index a49b0935..1cdb5ec3 100644 --- a/src/is-valid-pix-key/is-valid-pix-key.ts +++ b/src/is-valid-pix-key/is-valid-pix-key.ts @@ -30,8 +30,9 @@ export type IsValidPixKeyOptions = { * ``` * * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf - * @see Official: https://github.com/bacen/pix-dict-api DICT (Diretório de Identificadores de - * Contas Transacionais) OpenAPI spec, key format reference. + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html + * DICT (Diretório de Identificadores de Contas Transacionais) API specification, key format + * reference. * @see Official: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. */ export const isValidPixKey = (value: string, options?: IsValidPixKeyOptions): boolean => { diff --git a/src/is-valid-vin/constants.ts b/src/is-valid-vin/constants.ts index 35726df7..121f0639 100644 --- a/src/is-valid-vin/constants.ts +++ b/src/is-valid-vin/constants.ts @@ -5,6 +5,9 @@ * algorithm are a North-American requirement (49 CFR 565.15 / SAE J853), not something * Resolução CONTRAN nº 24/1998 or ABNT NBR 6066 — which define the Brazilian VIN structure — * mandate; many Brazilian-built VINs do not carry a matching check digit. + * The ISO catalogue page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser, where it renders the standard's paywalled + * abstract rather than its text. * @see Official: https://www.iso.org/standard/52200.html * @see Official: https://www.ecfr.gov/current/title-49/section-565.15 * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-Senatran/resolucoes-contran diff --git a/src/is-valid-vin/is-valid-vin.ts b/src/is-valid-vin/is-valid-vin.ts index 28fb5239..daf56033 100644 --- a/src/is-valid-vin/is-valid-vin.ts +++ b/src/is-valid-vin/is-valid-vin.ts @@ -30,6 +30,10 @@ import { * isValidVin("1HGCM82633A00435"); // false (16 characters) * ``` * + * The ISO catalogue page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser, where it renders the standard's paywalled + * abstract rather than its text. + * * @see Official: https://www.iso.org/standard/52200.html * @see Official: https://www.ecfr.gov/current/title-49/section-565.15 * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-Senatran/resolucoes-contran diff --git a/src/is-valid-voter-id/is-valid-voter-id.ts b/src/is-valid-voter-id/is-valid-voter-id.ts index 6ffba7d4..f9bbc3e4 100644 --- a/src/is-valid-voter-id/is-valid-voter-id.ts +++ b/src/is-valid-voter-id/is-valid-voter-id.ts @@ -33,6 +33,9 @@ const FORMAT_REGEX = /^[\s.]*\d{4}[\s.]*\d{4}[\s.]*(?:\d[\s.]*)?\d{2}[\s.]*\d{2} * 13-digit São Paulo/Minas Gerais ids are brutils parity, not published by the TSE — siga0984 uses * a different 9-digit rule for the sequential number. * + * The TSE resolution page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser. + * * @see Official: https://www.tse.jus.br/legislacao/compilada/res/2021/resolucao-no-23-659-de-26-de-outubro-de-2021 * @see Based on: https://siga0984.wordpress.com/2019/05/01/algoritmos-validacao-de-titulo-de-eleitor/ * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/voter_id.py diff --git a/src/parse-certidao/constants.ts b/src/parse-certidao/constants.ts index 47f8d9dd..0663a06c 100644 --- a/src/parse-certidao/constants.ts +++ b/src/parse-certidao/constants.ts @@ -6,9 +6,11 @@ * desdobrado para interdições. * * The in-force art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça - * lists only the codes 1 to 7. The codes 8 (emancipação) and 9 (interdição) come from the Anexo - * IV of the revoked Provimento CNJ nº 63/2017 and are kept because matrículas issued under it - * are still in circulation. + * lists only the codes 1 to 7, and no CNJ primary text reachable today publishes the other two: + * the Anexo IV of the revoked Provimento CNJ nº 63/2017 lists the same seven. The codes 8 + * (emancipação) and 9 (interdição) come from the `Based on:` references below: ghiorzi.org prints + * the nine book list and the cited Casilhero support class maps the same nine. They are kept + * because matrículas carrying them circulate. * * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 * Código Nacional de Normas da Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento diff --git a/src/parse-certidao/parse-certidao.ts b/src/parse-certidao/parse-certidao.ts index eee96e7a..38b28172 100644 --- a/src/parse-certidao/parse-certidao.ts +++ b/src/parse-certidao/parse-certidao.ts @@ -9,9 +9,11 @@ import { CERTIDAO_TYPES } from "./constants"; * `isValidCertidao` accepts a list of them. * * The in-force art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça - * lists only the codes 1 to 7. The codes 8 (`"emancipation"`) and 9 (`"interdiction"`) come from - * the Anexo IV of the revoked Provimento CNJ nº 63/2017 and are kept because matrículas issued - * under it are still in circulation. + * lists only the codes 1 to 7, and no CNJ primary text reachable today publishes the other two: + * the Anexo IV of the revoked Provimento CNJ nº 63/2017 lists the same seven. The codes 8 + * (`"emancipation"`) and 9 (`"interdiction"`) come from the `Based on:` references below: ghiorzi.org and + * validation-br both print the nine book list. They are kept because matrículas carrying them + * circulate. */ export type CertidaoType = | "birth" diff --git a/src/parse-cpf/parse-cpf.ts b/src/parse-cpf/parse-cpf.ts index 00d8079d..4955026f 100644 --- a/src/parse-cpf/parse-cpf.ts +++ b/src/parse-cpf/parse-cpf.ts @@ -14,7 +14,6 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * ``` * * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/meu-cpf - * @see Official: http://sped.rfb.gov.br/arquivo/show/8231 * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/cpf.py */ export const parseCpf = (value: string | number): string => diff --git a/src/parse-legal-nature/parse-legal-nature.ts b/src/parse-legal-nature/parse-legal-nature.ts index d1745002..e76df355 100644 --- a/src/parse-legal-nature/parse-legal-nature.ts +++ b/src/parse-legal-nature/parse-legal-nature.ts @@ -13,6 +13,10 @@ import { LENGTH } from "./constants"; * parseLegalNature("206-2"); // "2062" * ``` * + * The CONCLA table page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser; the detailed structure PDF next to it is served + * normally. + * * @see Official: https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 * @see Official: https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf */ diff --git a/src/parse-nfe-key/constants.ts b/src/parse-nfe-key/constants.ts index 95a3726d..923e75d5 100644 --- a/src/parse-nfe-key/constants.ts +++ b/src/parse-nfe-key/constants.ts @@ -17,9 +17,10 @@ export type ValidModel = (typeof VALID_MODELS)[number]; * The `tpEmis` (forma de emissão) codes each MOC assigns to its own document, so a code that is * meaningful for one document does not make a key of another valid. * - * NF-e and NFC-e (MOC 7.0 Anexo I, field B22): 1 normal, 2 contingência FS-IA, 3 contingência - * SCAN, 4 contingência EPEC, 5 contingência FS-DA, 6 contingência SVC-AN, 7 contingência SVC-RS - * and 9 contingência off-line da NFC-e. + * NF-e and NFC-e (MOC 7.0 Anexo I, field B22): 1 normal, 2 contingência FS-IA, 3 Regime Especial + * NFF, 4 contingência EPEC, 5 contingência FS-DA, 6 contingência SVC-AN, 7 contingência SVC-RS + * and 9 contingência off-line da NFC-e. Code 3 used to be "contingência SCAN"; NT 2021.002 + * redefined it as the Regime Especial da Nota Fiscal Fácil, leaving the value set unchanged. * * CT-e (CT-e MOC 4.00 Anexo I, field D19): 1 normal, 3 Regime Especial NFF, 4 EPEC pela SVC, * 5 contingência FS-DA, 7 autorização pela SVC-RS and 8 autorização pela SVC-SP. CT-e OS @@ -27,7 +28,7 @@ export type ValidModel = (typeof VALID_MODELS)[number]; * 8. Rule G011 of the same annex, "(7=SVC-RS e 8=SVC-SP)", is what makes 8 a real code here, * even though the NF-e MOC never assigns it. * - * MDF-e (MDF-e MOC 3.00 Anexo I, domain D7): 1 normal, 2 contingência off-line and 3 Regime + * MDF-e (MDF-e MOC 3.00b Anexo I, domain D7): 1 normal, 2 contingência off-line and 3 Regime * Especial NFF. NFCom, BP-e and NF3e (their own Anexo I, domain D7): 1 normal and * 2 contingência off-line. */ @@ -79,7 +80,13 @@ export const FORBIDDEN_CODES: readonly string[] = [ "01234567", ]; -/** The models rule B03-10 is written for, the only ones whose `cNF` it constrains. */ +/** + * The models rule B03-10 is written for, the only ones whose `cNF` it constrains. + * + * The scope is stated inconsistently by the sources: the change log of NT 2019.001 v1.40 says + * modelo 65 was taken out of the rule, while MOC 7.0 Anexo I still prints its applicability as + * `55/65`. The MOC being the consolidated text in force, both models are kept here. + */ export const FORBIDDEN_CODE_MODELS: readonly string[] = ["55", "65"]; /** @@ -98,5 +105,9 @@ export const NUMBER_START = 25; /** End (exclusive) of the document number (nNF) inside the 44 digit key. */ export const NUMBER_END = 34; -/** A document number of all zeros is not a valid nNF. */ +/** + * A document number of all zeros is not a valid nNF: the leiaute types `nNF` as `TNF`, whose + * pattern is `[1-9]{1}[0-9]{0,8}` in `tiposBasico_v4.00.xsd`, and the Anexo I of every other + * model repeats the same regex for its own number field. + */ export const ABSENT_NUMBER = "000000000"; diff --git a/src/parse-nfe-key/parse-nfe-key.ts b/src/parse-nfe-key/parse-nfe-key.ts index 059a17fe..7d19de71 100644 --- a/src/parse-nfe-key/parse-nfe-key.ts +++ b/src/parse-nfe-key/parse-nfe-key.ts @@ -91,18 +91,27 @@ const isForbiddenCode = (model: string, code: string, number: number): boolean = * which forbids the twenty repeated and sequential codes it lists and a `cNF` equal to the * document number. That rule arrived with NT 2019.001, so it can turn down a key authorised * before it, and no other MOC states it, which is why it is not applied to the other models. - * Rejecting a document number of all zeros, on the other hand, is a choice of this library: no - * MOC rule was found forbidding it. + * A document number of all zeros is turned down for every model, following the leiaute rather + * than a choice of this library: `nNF` is typed `TNF` in `tiposBasico_v4.00.xsd`, whose pattern + * is `[1-9]{1}[0-9]{0,8}`, and the Anexo I of every other model repeats the same regex for its + * own number field (`nCT`, `nMDF`, `nBP`, `nNF`). * * @param {string} value - The access key value to be parsed. * @returns {NfeKey | null} The parsed access key, or `null` when it is not valid. * * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf * Manual de Orientação do Contribuinte (MOC) NF-e, "chave de acesso". + * @see Official: https://dfe-portal.svrs.rs.gov.br/NFE/Documentos + * NF-e schema package (PL_010b, NT2025.002 v1.30): `tiposBasico_v4.00.xsd`, the `TNF` and + * `TCodUfIBGE` types. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07 - * Ajuste SINIEF 09/07, cláusula primeira, § 3.º, II, "b": the CT-e OS, modelo 67. + * Ajuste SINIEF 09/07, cláusula primeira, caput: the CT-e, modelo 57. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19 + * Ajuste SINIEF 36/19, cláusula primeira: the CT-e OS, modelo 67. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20 + * Ajuste SINIEF 03/20, cláusula primeira: the GTV-e, modelo 64. * @see Official: https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais - * CT-e MOC 4.00, Anexo I: modelo 64 (GTV-e) and the `tpEmis` domains D19, D27 and D15. + * CT-e MOC 4.00, Anexo I: the `tpEmis` domains D19, D27 and D15. * @see Official: https://dfe-portal.svrs.rs.gov.br/BPE/Documentos * BP-e MOC 1.00b, Visão Geral and Anexo I: modelo 63. * @see Official: https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos diff --git a/src/parse-pix-key/parse-pix-key.ts b/src/parse-pix-key/parse-pix-key.ts index 5917d6c0..7ffb1329 100644 --- a/src/parse-pix-key/parse-pix-key.ts +++ b/src/parse-pix-key/parse-pix-key.ts @@ -81,8 +81,9 @@ const resolvePhoneKey = (trimmed: string): PixKey | null => { * ``` * * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf - * @see Official: https://github.com/bacen/pix-dict-api DICT (Diretório de Identificadores de - * Contas Transacionais) OpenAPI spec, key format reference. + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html + * DICT (Diretório de Identificadores de Contas Transacionais) API specification, key format + * reference. * @see Official: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. */ export const parsePixKey = (value: string): PixKey | null => { diff --git a/src/parse-voter-id/parse-voter-id.ts b/src/parse-voter-id/parse-voter-id.ts index 4d1ed719..c812bd69 100644 --- a/src/parse-voter-id/parse-voter-id.ts +++ b/src/parse-voter-id/parse-voter-id.ts @@ -23,6 +23,9 @@ import { EXTENDED_LENGTH, LENGTH } from "./constants"; * 14-or-more-digit input whose 10th and 11th digits are "01"/"02" is read as a 13-digit São Paulo * or Minas Gerais id and capped at 13 digits, discarding anything past that. * + * The TSE resolution page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser. + * * @see Official: https://www.tse.jus.br/legislacao/compilada/res/2021/resolucao-no-23-659-de-26-de-outubro-de-2021 * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/voter_id.py */ From 4d44ec4f8db82f72439336efd6fb4704cb9a25e0 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:39:38 -0300 Subject: [PATCH 34/75] fix(types): re-export the bank and state types from the eight subpaths that still lacked them - `Bank` (get-banks, get-bank-by-code, get-bank-by-ispb) and `StateCode` (generate-cpf, generate-voter-id, is-valid-ie, is-valid-registro-profissional, parse-nfe-key) were only reachable through a content-hashed chunk, so a consumer typing those signatures from the subpath hit TS2459; every public declaration file now names its types from its own entry --- src/generate-cpf/generate-cpf.ts | 5 ++++- src/generate-voter-id/generate-voter-id.ts | 2 ++ src/get-bank-by-code/get-bank-by-code.ts | 2 ++ src/get-bank-by-ispb/get-bank-by-ispb.ts | 2 ++ src/get-banks/get-banks.ts | 2 ++ src/is-valid-ie/is-valid-ie.ts | 2 ++ .../is-valid-registro-profissional.ts | 20 ++++++++++--------- src/parse-nfe-key/parse-nfe-key.ts | 9 +++++++-- 8 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/generate-cpf/generate-cpf.ts b/src/generate-cpf/generate-cpf.ts index cb4b14b2..18e3ea84 100644 --- a/src/generate-cpf/generate-cpf.ts +++ b/src/generate-cpf/generate-cpf.ts @@ -4,6 +4,8 @@ import { generateRandomNumber } from "../_internals/generate-random-number/gener import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; import { BASE_LENGTH, STATE_CODES } from "./constants"; +export type { StateCode } from "../_internals/constants/states"; + const getStateCode = (state?: StateCode): string => { if (state && Object.hasOwn(STATE_CODES, state)) return STATE_CODES[state]; return generateRandomNumber(1); @@ -32,7 +34,8 @@ const calculateCheckDigit = (base: string, weight: number): string => { * "Cadastros: CPF e CNPJ"; the check digit rule (`REGRA_VALIDA_CPF`) is specified, with the * worked example `280012389-38`, in the Receita Federal's Manual de Preenchimento da * e-Financeira, Anexo II — Leiautes Gerais, approved by the Ato Declaratório Executivo Cofis - * nº 10, de 25 de maio de 2026. The manual's own file used to be served from `sped.rfb.gov.br`, + * nº 10, de 19 de maio de 2026 (DOU de 25/05/2026). The manual's own file used to be served from + * `sped.rfb.gov.br`, * a host that no longer answers at all, so the approving act is cited below in its place; its * Receita Federal permalink redirects into the norms viewer, which has to be opened in a * browser. diff --git a/src/generate-voter-id/generate-voter-id.ts b/src/generate-voter-id/generate-voter-id.ts index 3a3d1abd..7c93506b 100644 --- a/src/generate-voter-id/generate-voter-id.ts +++ b/src/generate-voter-id/generate-voter-id.ts @@ -4,6 +4,8 @@ import { type StateCode } from "../_internals/constants/states"; import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; import { UF_TO_VOTER_ID_CODE } from "../is-valid-voter-id/constants"; +export type { StateCode } from "../_internals/constants/states"; + /** * Generates a valid random Brazilian voter id (título de eleitor). * diff --git a/src/get-bank-by-code/get-bank-by-code.ts b/src/get-bank-by-code/get-bank-by-code.ts index 0292707f..199680db 100644 --- a/src/get-bank-by-code/get-bank-by-code.ts +++ b/src/get-bank-by-code/get-bank-by-code.ts @@ -2,6 +2,8 @@ import { BANKS, type Bank } from "../_internals/constants/banks"; import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +export type { Bank } from "../_internals/constants/banks"; + const CODE_LENGTH = 3; /** diff --git a/src/get-bank-by-ispb/get-bank-by-ispb.ts b/src/get-bank-by-ispb/get-bank-by-ispb.ts index e53f968c..2fc3d7c8 100644 --- a/src/get-bank-by-ispb/get-bank-by-ispb.ts +++ b/src/get-bank-by-ispb/get-bank-by-ispb.ts @@ -2,6 +2,8 @@ import { BANKS, type Bank } from "../_internals/constants/banks"; import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +export type { Bank } from "../_internals/constants/banks"; + const ISPB_LENGTH = 8; /** diff --git a/src/get-banks/get-banks.ts b/src/get-banks/get-banks.ts index 8735809e..a8b4e577 100644 --- a/src/get-banks/get-banks.ts +++ b/src/get-banks/get-banks.ts @@ -1,5 +1,7 @@ import { BANKS, type Bank } from "../_internals/constants/banks"; +export type { Bank } from "../_internals/constants/banks"; + /** * Returns every Brazilian bank with a compensation code (COMPE), published by Banco Central * do Brasil in the STR (Sistema de Transferência de Reservas) participants list. diff --git a/src/is-valid-ie/is-valid-ie.ts b/src/is-valid-ie/is-valid-ie.ts index 605bd2a0..a07ffc49 100644 --- a/src/is-valid-ie/is-valid-ie.ts +++ b/src/is-valid-ie/is-valid-ie.ts @@ -15,6 +15,8 @@ import { TO_TYPES, } from "./constants"; +export type { StateCode } from "../_internals/constants/states"; + type IeValidator = (ie: string) => boolean; const checkLength = (ie: string, length: number | number[]): boolean => { diff --git a/src/is-valid-registro-profissional/is-valid-registro-profissional.ts b/src/is-valid-registro-profissional/is-valid-registro-profissional.ts index 85593b93..47e5c6d3 100644 --- a/src/is-valid-registro-profissional/is-valid-registro-profissional.ts +++ b/src/is-valid-registro-profissional/is-valid-registro-profissional.ts @@ -10,6 +10,8 @@ import { type RegistroProfissionalCouncil, } from "./constants"; +export type { StateCode } from "../_internals/constants/states"; + /** The options `isValidRegistroProfissional` takes: the professional council and, optionally, the UF the registration must belong to. */ export type IsValidRegistroProfissionalOptions = { /** The professional council that issued the registration number. */ @@ -109,15 +111,15 @@ const isKnownCrpRegion = (value: string): boolean => { * Conselho Federal de Psicologia: the 24 Conselhos Regionais of the system, numbered CRP-01 to * CRP-24. The page establishes the regional codes only; it publishes no length for the inscription * number itself. - * @see Based on: https://www.oab.org.br/ - * Ordem dos Advogados do Brasil (OAB), which publishes no format for the número de inscrição and - * the seccional. - * @see Based on: https://portal.cfm.org.br/ - * Conselho Federal de Medicina (CRM), which publishes no format for the registration number and - * the UF. - * @see Based on: https://cfo.org.br/ - * Conselho Federal de Odontologia (CRO), which publishes no format for the registration number and - * the UF. + * @see Official: https://www.oab.org.br/ + * Ordem dos Advogados do Brasil (OAB), the federal body that regulates the profession, which + * publishes no format for the número de inscrição and the seccional. + * @see Official: https://portal.cfm.org.br/ + * Conselho Federal de Medicina (CFM), the autarquia federal that regulates the profession, which + * publishes no format for the registration number and the UF. + * @see Official: https://cfo.org.br/ + * Conselho Federal de Odontologia (CFO), the autarquia federal that regulates the profession, + * which publishes no format for the registration number and the UF. */ export const isValidRegistroProfissional = ( value: string, diff --git a/src/parse-nfe-key/parse-nfe-key.ts b/src/parse-nfe-key/parse-nfe-key.ts index 7d19de71..c20ca85c 100644 --- a/src/parse-nfe-key/parse-nfe-key.ts +++ b/src/parse-nfe-key/parse-nfe-key.ts @@ -16,6 +16,8 @@ import { XML_ID_PREFIX_REGEX, } from "./constants"; +export type { StateCode } from "../_internals/constants/states"; + /** * The document models a DF-e access key can carry: `"55"` NF-e, `"57"` CT-e, `"58"` MDF-e, * `"62"` NFCom, `"63"` BP-e, `"64"` GTV-e, `"65"` NFC-e, `"66"` NF3e and `"67"` CT-e OS. @@ -110,8 +112,11 @@ const isForbiddenCode = (model: string, code: string, number: number): boolean = * Ajuste SINIEF 36/19, cláusula primeira: the CT-e OS, modelo 67. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20 * Ajuste SINIEF 03/20, cláusula primeira: the GTV-e, modelo 64. - * @see Official: https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais - * CT-e MOC 4.00, Anexo I: the `tpEmis` domains D19, D27 and D15. + * @see Official: https://dfe-portal.svrs.rs.gov.br/CTE/Documentos + * CT-e MOC 4.00, Anexo I ("MOC CTe 4.00 Anexo I - Leiaute e Regras de Validação"): the `tpEmis` + * domains D19, D27 and D15. Published by the SVRS dfe-portal, like the BP-e, NF3e and NFCom + * manuals below; the cte.fazenda.gov.br manual index answers "Sistema temporariamente + * indisponível" permanently. * @see Official: https://dfe-portal.svrs.rs.gov.br/BPE/Documentos * BP-e MOC 1.00b, Visão Geral and Anexo I: modelo 63. * @see Official: https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos From 1a270035165a8c95f55d93a65052035a08b3240c Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:39:38 -0300 Subject: [PATCH 35/75] fix(package): resolve the subpath declarations under moduleResolution node - `import { isValidCpf } from "@brazilian-utils/brazilian-utils/is-valid-cpf"` failed with TS2307 under the legacy `moduleResolution: "node"`, which ignores the `exports` map; a `typesVersions` block maps every subpath to its declaration file, keeps `dist/*` and `package.json` as they are, and leaves node16 and bundler resolution untouched (verified with three consumer projects, attw and publint) --- package.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/package.json b/package.json index d0d32757..538f29a9 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,19 @@ "main": "./dist/brazilian-utils.umd.cjs", "module": "./dist/brazilian-utils.js", "types": "./dist/brazilian-utils.d.ts", + "typesVersions": { + "*": { + "dist/*": [ + "dist/*" + ], + "package.json": [ + "package.json" + ], + "*": [ + "dist/*.d.ts" + ] + } + }, "exports": { ".": { "import": { From da567b5c13fd50decae99088c42826daff58c87f Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:39:39 -0300 Subject: [PATCH 36/75] ci(tree-shaking): treat any unexpected exit code as a comparison failure - only exit code 2 was read as a failed comparison, so a killed or missing Node process (137, 127) passed the job under the `tree-shaking: accepted` label; every code other than 0 and 1 now maps to the comparison-failure branch --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c447991..f6cee777 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,6 +87,7 @@ jobs: node scripts/tree-shaking.ts --compare base.json --markdown tree-shaking.md code=$? set -e + case "$code" in 0 | 1) ;; *) code=2 ;; esac echo "code=$code" >> "$GITHUB_OUTPUT" exit "$code" else From 811cb10a07e69a38c4d11bb1a6ec45617584814b Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:39:39 -0300 Subject: [PATCH 37/75] feat(cep): declare the unidade, estado and regiao fields of the ViaCEP response - `getCepInfoByAddress` returns the ViaCEP payload unchanged and the service now sends `unidade`, `estado` and `regiao`; `CepAddressInfo` declares them as optional properties and the example is a real current response --- .../get-cep-info-by-address.ts | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/get-cep-info-by-address/get-cep-info-by-address.ts b/src/get-cep-info-by-address/get-cep-info-by-address.ts index 451b108b..e41263de 100644 --- a/src/get-cep-info-by-address/get-cep-info-by-address.ts +++ b/src/get-cep-info-by-address/get-cep-info-by-address.ts @@ -27,7 +27,11 @@ export class GetCepInfoByAddressNotFoundError extends GetCepInfoByAddressError { } } -/** One address returned by `getCepInfoByAddress`, under the field names ViaCEP itself uses. */ +/** + * One address returned by `getCepInfoByAddress`, under the field names ViaCEP itself uses. The + * ViaCEP payload is passed through unchanged, so every field the service sends is present and a + * field it adds later shows up even though it is not declared here. + */ export type CepAddressInfo = { /** The CEP, masked as "00000-000" the way ViaCEP returns it. */ cep: string; @@ -35,12 +39,18 @@ export type CepAddressInfo = { logradouro: string; /** Extra address information, e.g. a house number range. */ complemento: string; + /** Name of the establishment the CEP belongs to, e.g. "AC São Carlos"; empty for a street CEP. */ + unidade?: string; /** Neighborhood name. */ bairro: string; /** City name. */ localidade: string; /** Two letter state code, e.g. "SP". */ uf: string; + /** Full state name, e.g. "Minas Gerais". */ + estado?: string; + /** Region name, e.g. "Sudeste". */ + regiao?: string; /** The 7 digit IBGE municipality code. */ ibge?: string; /** GIA code, used by the São Paulo state tax authority. */ @@ -87,8 +97,24 @@ const isCepAddressInfoArray = (value: unknown): value is CepAddressInfo[] => Arr * * @example * ```typescript - * await getCepInfoByAddress({ federalUnit: "SP", city: "São Paulo", street: "Avenida Paulista" }); - * // [{ cep: "01310-100", logradouro: "Avenida Paulista", ... }] + * await getCepInfoByAddress({ federalUnit: "MG", city: "Ouro Preto", street: "Rua Direita" }); + * // [ + * // { + * // cep: "35411-152", + * // logradouro: "Rua Direita", + * // complemento: "", + * // unidade: "", + * // bairro: "Riacho (Amarantina)", + * // localidade: "Ouro Preto", + * // uf: "MG", + * // estado: "Minas Gerais", + * // regiao: "Sudeste", + * // ibge: "3146107", + * // gia: "", + * // ddd: "31", + * // siafi: "4921" + * // } + * // ] * ``` * * @see Official: https://www.correios.com.br/enviar/precisa-de-ajuda/tudo-sobre-cep From eee5e88d580e05001674273899e210d6d75bf0d1 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:39:39 -0300 Subject: [PATCH 38/75] test(business-days): pin the non-string state code rejection for an amount of 0 - with an amount of 0 the walk runs no day, so the early return was the only observable effect of the guard; the test pins `null` for that case --- src/add-business-days/add-business-days.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/add-business-days/add-business-days.test.ts b/src/add-business-days/add-business-days.test.ts index b4929081..65513c76 100644 --- a/src/add-business-days/add-business-days.test.ts +++ b/src/add-business-days/add-business-days.test.ts @@ -175,6 +175,13 @@ describe("addBusinessDays", () => { expect(addBusinessDays(new Date(2024, 0, 2), 1, { stateCode: 123 })).toBeNull(); }); + it("should return null when the stateCode is not a string even for an amount of 0, which walks no day", () => { + // @ts-expect-error: intentionally invalid input + expect(addBusinessDays(new Date(2024, 0, 2), 0, { stateCode: 123 })).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(addBusinessDays(new Date(2024, 0, 2), 0, { stateCode: null })).toBeNull(); + }); + it("should ignore options that are not an object", () => { // @ts-expect-error: intentionally invalid input expect(addBusinessDays(new Date(2024, 6, 8, 12), 1, "SP")).toEqual(new Date(2024, 6, 9, 12)); From e35bc2221790b1b9b56db90e3949219d99d342af Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:39:39 -0300 Subject: [PATCH 39/75] docs: describe every option used in the examples and correct the remaining citations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `isValidPhone` documents `options.version`; the `version`, `pad` and `symbol` options used in the examples of the CNPJ, boleto, currency and CNS sections are described; `getBoletoInfo` is listed as the one function returning `undefined` - the `generateProcessoJuridico` and `parseProcessoJuridico` examples carry the check digits the CNJ algorithm produces for 2026; the email JSDoc states the local part may not end in a dot or an apostrophe - CNAE 2.3, Ato Anatel 12.712/2024, ADE Cofis 10/2026 (19/05/2026), the CNJ Provimentos 2/2009, 3/2009 and 182/2024, Resolução Anatel 86/1998 art. 43 and the CT-e manuals are cited from the pages that carry them; the federal councils, the Planalto manual and Resolução 263/2001 use one label each; the OpenSSF badge points at the documented URL --- CONTRIBUTING.md | 8 ++- README.md | 2 +- docs/getting-started.md | 2 +- docs/llms-full.txt | 68 +++++++++++-------- docs/llms.txt | 4 +- docs/pt-br/getting-started.md | 2 +- docs/pt-br/utilities.md | 66 ++++++++++-------- docs/utilities.md | 66 ++++++++++-------- scripts/cnae.ts | 14 +++- .../calculate-cei-check-digit.ts | 8 ++- src/_internals/constants/area-codes.ts | 3 +- src/_internals/constants/cei.ts | 8 ++- src/_internals/constants/certidao.ts | 18 +++-- src/_internals/constants/cnae.ts | 14 +++- src/_internals/constants/number-words.ts | 6 +- src/_internals/constants/service-phone.ts | 18 +++-- .../is-valid-cei-cno-number.ts | 8 ++- src/capitalize/capitalize.ts | 6 +- src/capitalize/constants.ts | 26 +++++-- src/format-certidao/format-certidao.ts | 18 +++-- .../generate-processo-juridico.ts | 4 +- .../get-address-info-by-cep.ts | 6 +- src/get-cnae/get-cnae.ts | 4 +- src/get-holidays/constants.ts | 28 +++++--- src/get-holidays/get-holidays.test.ts | 2 +- src/get-holidays/get-holidays.ts | 5 +- .../is-valid-bank-account.ts | 6 +- src/is-valid-cei/is-valid-cei.ts | 8 ++- src/is-valid-certidao/is-valid-certidao.ts | 18 +++-- src/is-valid-cnae/is-valid-cnae.ts | 4 +- src/is-valid-cno/is-valid-cno.ts | 8 ++- src/is-valid-cpf/is-valid-cpf.ts | 4 +- src/is-valid-cst/constants.ts | 7 +- src/is-valid-cst/is-valid-cst.test.ts | 2 +- src/is-valid-cst/is-valid-cst.ts | 7 +- src/is-valid-email/is-valid-email.ts | 6 +- src/is-valid-nfe-key/is-valid-nfe-key.ts | 7 +- src/is-valid-vin/constants.ts | 10 +-- src/is-valid-vin/is-valid-vin.ts | 9 ++- src/parse-certidao/constants.ts | 18 +++-- src/parse-certidao/parse-certidao.ts | 18 +++-- .../parse-processo-juridico.ts | 2 +- 42 files changed, 356 insertions(+), 192 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dfb08dfd..a7772a12 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -114,8 +114,12 @@ When an exported function has a source to credit, list the authoritative source `@see Official:` (a law, regulator, standard body or government dataset), followed by one `@see Based on:` line for every third-party implementation, mirror dataset or reference test vector the code actually relied on (a GitHub repo, a blog article, a community CSV/JSON mirror, -and so on), one `@see` per line. A utility with no located source of either kind (e.g. -`capitalize`, `formatCurrency`) can be left without an `@see` block. See +and so on), one `@see` per line. A regulator's own repository counts as `Official:` even though it +is a GitHub URL: `https://github.com/bacen/pix-api` is the Banco Central publishing the normative +Pix/SPI specification, not a third party reimplementing it. Put the URL alone on the `@see` line +and the description on the lines below it. Every utility in the package currently has at least one +`@see`; if you add one whose behaviour is a plain convention with no locatable source, say so in +prose in the JSDoc instead of inventing a citation. See `src/is-valid-certidao/is-valid-certidao.ts` and `src/is-valid-cei/is-valid-cei.ts` for the style. Shared helpers used by multiple utilities live under `src/_internals/`. Check there before diff --git a/README.md b/README.md index 2423b6d2..35135c07 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![npm version](https://img.shields.io/npm/v/@brazilian-utils/brazilian-utils.svg)](https://www.npmjs.com/package/@brazilian-utils/brazilian-utils) [![Downloads per month](https://img.shields.io/npm/dm/@brazilian-utils/brazilian-utils.svg)](https://www.npmjs.com/package/@brazilian-utils/brazilian-utils) [![License: MIT](https://img.shields.io/github/license/brazilian-utils/javascript.svg)](LICENSE) [![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](CONTRIBUTING.md#zero-runtime-dependencies) [![Bundle size](https://img.shields.io/bundlephobia/minzip/@brazilian-utils/brazilian-utils?label=isValidCpf%20import%20%3C%201%20KB&color=brightgreen)](docs/getting-started.md#bundle-size) [![Tree-shakeable](https://badgen.net/bundlephobia/tree-shaking/@brazilian-utils/brazilian-utils)](docs/getting-started.md#bundle-size) [![TypeScript](https://img.shields.io/npm/types/@brazilian-utils/brazilian-utils)](https://www.npmjs.com/package/@brazilian-utils/brazilian-utils) -[![Build Status](https://github.com/brazilian-utils/javascript/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/brazilian-utils/javascript/actions/workflows/build.yml?query=branch%3Amain) [![Tests](https://github.com/brazilian-utils/javascript/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/brazilian-utils/javascript/actions/workflows/tests.yml?query=branch%3Amain) [![codecov](https://codecov.io/gh/brazilian-utils/javascript/branch/main/graph/badge.svg)](https://codecov.io/gh/brazilian-utils/javascript) [![Mutation tests](https://github.com/brazilian-utils/javascript/actions/workflows/mutation.yml/badge.svg?branch=main)](https://github.com/brazilian-utils/javascript/actions/workflows/mutation.yml?query=branch%3Amain) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/brazilian-utils/javascript/badge)](https://scorecard.dev/viewer/?uri=github.com/brazilian-utils/javascript) +[![Build Status](https://github.com/brazilian-utils/javascript/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/brazilian-utils/javascript/actions/workflows/build.yml?query=branch%3Amain) [![Tests](https://github.com/brazilian-utils/javascript/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/brazilian-utils/javascript/actions/workflows/tests.yml?query=branch%3Amain) [![codecov](https://codecov.io/gh/brazilian-utils/javascript/branch/main/graph/badge.svg)](https://codecov.io/gh/brazilian-utils/javascript) [![Mutation tests](https://github.com/brazilian-utils/javascript/actions/workflows/mutation.yml/badge.svg?branch=main)](https://github.com/brazilian-utils/javascript/actions/workflows/mutation.yml?query=branch%3Amain) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/brazilian-utils/javascript/badge)](https://scorecard.dev/viewer/?uri=github.com/brazilian-utils/javascript) diff --git a/docs/getting-started.md b/docs/getting-started.md index 13413d79..55f67f79 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -73,7 +73,7 @@ A handful of utils are the exception: each embeds an official dataset, so it wei | `getCities` | 5571 IBGE municipality names | 154.0 KB | 49.7 KB | | `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 113.8 KB | 24.3 KB | | `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 118.8 KB | 30.4 KB | -| `isValidCnae` · `getCnae` | CNAE 2.3 subclasses | 94.0 KB | 21.3 KB | +| `isValidCnae` · `getCnae` | CNAE-Subclasses 2.3 | 94.0 KB | 21.3 KB | | `isValidCfop` · `getCfop` | CFOP operation descriptions | 68.7 KB | 6.8 KB | | `getBanks` · `getBankByCode` | Banco Central STR participants (COMPE + ISPB) | 38.3 KB | 9.6 KB | diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 528fbac1..1b27d4ce 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -211,7 +211,7 @@ A handful of utils are the exception: each embeds an official dataset, so it wei | `getCities` | 5571 IBGE municipality names | 154.0 KB | 49.7 KB | | `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 113.8 KB | 24.3 KB | | `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 118.8 KB | 30.4 KB | -| `isValidCnae` · `getCnae` | CNAE 2.3 subclasses | 94.0 KB | 21.3 KB | +| `isValidCnae` · `getCnae` | CNAE-Subclasses 2.3 | 94.0 KB | 21.3 KB | | `isValidCfop` · `getCfop` | CFOP operation descriptions | 68.7 KB | 6.8 KB | | `getBanks` · `getBankByCode` | Banco Central STR participants (COMPE + ISPB) | 38.3 KB | 9.6 KB | @@ -241,7 +241,7 @@ Pick one style per util in a given app: a bundler treats the root import and the Here you will find all the utilities available for use. -> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function returns an empty value of its return type: every `format*` function, `capitalize`, and the string-returning `parse*` functions (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) return `""`; `parseCurrency` returns `0`; the object/tuple parsers — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — return `null`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. +> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `getBoletoInfo` returns `undefined` for an invalid boleto, the one function in the package that returns `undefined`; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function returns an empty value of its return type: every `format*` function, `capitalize`, and the string-returning `parse*` functions (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) return `""`; `parseCurrency` returns `0`; the object/tuple parsers — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — return `null`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. ### isValidCpf @@ -256,7 +256,7 @@ isValidCpf('111 444 777 35'); // true (whitespace mask) ### formatCpf -Format CPF. `options.obfuscate` (part of `FormatCpfOptions`) hides the first 3 digits and the 2 check digits (`***.456.789-**`), the gov.br / Receita Federal display convention, applied after `pad`. It is read for truthiness, the way `pad` is, so any truthy value obfuscates. +Format CPF. `options.pad` (part of `FormatCpfOptions`) left-pads the value with zeros up to the 11 slots of the pattern before masking (default `false`). `options.obfuscate` (same type) hides the first 3 digits and the 2 check digits (`***.456.789-**`), the gov.br / Receita Federal display convention, applied after `pad`. It is read for truthiness, the way `pad` is, so any truthy value obfuscates. ```javascript import { formatCpf } from '@brazilian-utils/brazilian-utils'; @@ -289,7 +289,7 @@ generateCpf('SP'); // the 9th digit is 8, the SP região fiscal code ### isValidCnpj -Check if CNPJ is valid. Supports both the numeric format (`version: 1`, default) and the alphanumeric format (`version: 2`), and accepts the usual mask characters and whitespace. Options are typed as `IsValidCnpjOptions`. +Check if CNPJ is valid. `options.version` (part of `IsValidCnpjOptions`) picks which format is accepted: `1` (default) the numeric-only format, `2` both the numeric and the alphanumeric one; any other value is read as `1`, the way `formatCnpj` and `parseCnpj` read it. The usual mask characters and whitespace are accepted in either version. ```javascript import { isValidCnpj } from '@brazilian-utils/brazilian-utils'; @@ -300,7 +300,7 @@ isValidCnpj('q0slfmbd7vx439', { version: 2 }); // true (lowercase alphanumeric) ### formatCnpj -Format CNPJ. `options.obfuscate` (part of `FormatCnpjOptions`) hides the first 2 digits and the 2 check digits (`**.345.678/0001-**`), the gov.br / Receita Federal display convention. It applies to both versions and comes after `pad`, and is read for truthiness, the way `pad` is, so any truthy value obfuscates. +Format CNPJ. `options.pad` (part of `FormatCnpjOptions`) left-pads the value with zeros up to the 14 slots of the pattern before masking (default `false`). `options.version` (same type) picks which CNPJ format to read: `1` (default) numeric only, `2` alphanumeric. `options.obfuscate` hides the first 2 digits and the 2 check digits (`**.345.678/0001-**`), the gov.br / Receita Federal display convention. It applies to both versions and comes after `pad`, and is read for truthiness, the way `pad` is, so any truthy value obfuscates. ```javascript import { formatCnpj } from '@brazilian-utils/brazilian-utils'; @@ -313,7 +313,7 @@ formatCnpj('12345678000195', { obfuscate: true }); // **.345.678/0001-** ### parseCnpj -Remove CNPJ formatting, return a normalized value, and cap the result to 14 characters. Options are typed as `ParseCnpjOptions`. +Remove CNPJ formatting, return a normalized value, and cap the result to 14 characters. `options.version` (part of `ParseCnpjOptions`) picks which CNPJ format to normalize: `1` (default) keeps digits only, `2` keeps letters and digits, so an alphanumeric CNPJ survives the round trip. ```javascript import { parseCnpj } from '@brazilian-utils/brazilian-utils'; @@ -362,7 +362,7 @@ isValidBoleto('846100000005246100291102005460339004695895061080'); // true (bole ### formatBoleto -Format a boleto number. The arrecadação (convênio/tributos) mask applies only to the 48 digit linha digitável starting with `8`; the 44 digit arrecadação barcode has no display grouping defined by FEBRABAN and keeps the "cobrança bancária" mask instead. +Format a boleto number. `options.pad` (part of `FormatBoletoOptions`) left-pads the value with zeros up to the number of slots in the pattern before masking (default `false`). The arrecadação (convênio/tributos) mask applies only to the 48 digit linha digitável starting with `8`; the 44 digit arrecadação barcode has no display grouping defined by FEBRABAN and keeps the "cobrança bancária" mask instead. ```javascript import { formatBoleto } from '@brazilian-utils/brazilian-utils'; @@ -396,7 +396,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ### getBoletoInfo -Extract information from a boleto (amount, expiration date, bank code). Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. The cycle search never goes below the first cycle, so a `referenceDate` older than the scheme itself still resolves a factor to the oldest date that factor can denote rather than to one before the 07/10/1997 base date. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. +Extract information from a boleto (amount, expiration date, bank code). Returns `undefined` when `value` is not a valid boleto — `isValidBoleto` is checked first — exactly as in 2.3.0, so the result has to be narrowed before it is read. Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. The cycle search never goes below the first cycle, so a `referenceDate` older than the scheme itself still resolves a factor to the oldest date that factor can denote rather than to one before the 07/10/1997 base date. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -511,7 +511,7 @@ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // nu Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e (65), CT-e (57, the Conhecimento de Transporte Eletrônico instituted by the cláusula primeira of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para Outros Serviços instituted by the cláusula primeira of the [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, the CT-e Guia de Transporte de Valores instituted by the cláusula primeira of the [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed differently. Accepts whitespace between digit groups (the common display mask) and the `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the document's XML. -The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` for the CT-e, `{1, 5, 7, 8}` for the CT-e OS, `{1, 2, 7, 8}` for the GTV-e, `{1, 2, 3}` for the MDF-e and `{1, 2}` for the BP-e, the NF3e and the NFCom. Code 8, the authorização pela SVC-SP, is assigned by the [CT-e MOC 4.00](https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais) only, never by the NF-e one; the domains of the [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), the [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) and the [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) come from their own manuals. For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, which forbids the twenty repeated and sequential `cNF` values it lists and a `cNF` equal to the document number. A document number of all zeros is turned down for every model, following the leiaute rather than a choice of this library: `tiposBasico_v4.00.xsd` of the [NF-e schema package](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) types `nNF` as `TNF`, whose pattern is `[1-9]{1}[0-9]{0,8}`, and the Anexo I of every other model repeats the same regex for its own number field. +The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` for the CT-e, `{1, 5, 7, 8}` for the CT-e OS, `{1, 2, 7, 8}` for the GTV-e, `{1, 2, 3}` for the MDF-e and `{1, 2}` for the BP-e, the NF3e and the NFCom. Code 8, the authorização pela SVC-SP, is assigned by the [CT-e MOC 4.00](https://dfe-portal.svrs.rs.gov.br/CTE/Documentos) only, never by the NF-e one; the domains of the [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), the [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) and the [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) come from their own manuals. For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, which forbids the twenty repeated and sequential `cNF` values it lists and a `cNF` equal to the document number. A document number of all zeros is turned down for every model, following the leiaute rather than a choice of this library: `tiposBasico_v4.00.xsd` of the [NF-e schema package](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) types `nNF` as `TNF`, whose pattern is `[1-9]{1}[0-9]{0,8}`, and the Anexo I of every other model repeats the same regex for its own number field. ```javascript import { isValidNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -566,12 +566,13 @@ isValidEmail('john.doe@hotmail.com'); // true ### isValidPhone -Check if phone number (mobile or landline) is valid. A Brazilian country code (`+55`, `0055` or a bare `55`) is accepted and removed before validation, under the rule documented in `parsePhone`. `options.accept` (typed as `PhoneType[]`, part of `IsValidPhoneOptions`) picks which kinds of number count as valid and defaults to `['mobile', 'landline']`; add `'service'` to also accept the non-geographic numbers recognized by `isValidServicePhone`, or pass `[]` to accept none. +Check if phone number (mobile or landline) is valid. A Brazilian country code (`+55`, `0055` or a bare `55`) is accepted and removed before validation, under the rule documented in `parsePhone`. `options.accept` (typed as `PhoneType[]`, part of `IsValidPhoneOptions`) picks which kinds of number count as valid and defaults to `['mobile', 'landline']`; add `'service'` to also accept the non-geographic numbers recognized by `isValidServicePhone`, or pass `[]` to accept none. `options.version` (typed as `PhoneVersion`, part of the same type) is forwarded to `isValidMobilePhone` and picks which mobile numbering rule is enforced: `1` (default) the legacy format, whose first number digit may be 6, 7, 8 or 9, and `2` the current one, which requires 9 and rejects the `700` prefix. It only affects mobile numbers; landline and service numbers are unaffected. ```javascript import { isValidPhone } from '@brazilian-utils/brazilian-utils'; isValidPhone('11900000000'); // true +isValidPhone('11712345678', { version: 2 }); // false (v2 requires 9 as the first mobile digit) isValidPhone('+55 11 98765-4321'); // true (country code accepted) isValidPhone('08001234567'); // false (service numbers rejected by default) isValidPhone('08001234567', { accept: ['service'] }); // true @@ -780,12 +781,12 @@ const address = await getAddressInfoByCep('01310100'); // { cep: '01310100', state: 'SP', city: 'São Paulo', neighborhood: 'Bela Vista', street: 'Avenida Paulista' } // Using specific providers -const address = await getAddressInfoByCep('01310-100', { +const addressFromProviders = await getAddressInfoByCep('01310-100', { providers: ['viacep', 'brasilapi'] }); // Using number input (will be padded automatically) -const address = await getAddressInfoByCep(1310100); +const addressFromNumber = await getAddressInfoByCep(1310100); ``` ### isValidProcessoJuridico @@ -1071,7 +1072,7 @@ capitalize(' josé maria '); // José Maria (every run of whitespace, tabs a ### formatCurrency -Formats an integer or float to a string in the BRL pattern. A `number` is formatted as-is (sign and decimals preserved). A `string` input is read by the same rule as `parseCurrency`, except that a value written without any separator stays in whole units: the last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator, every other `,` or `.` is a thousands separator, and a `-` written before the first digit is preserved. So `'1.234,56'` formats as `1.234,56`, `'-10.5'` as `-10,50` and `'1234'` as `1.234,00`. `precision` is clamped to `0..20` (the package limit, the bound Node 20 still enforces on `Intl.NumberFormat`), defaults to 2, and falls back to 2 when it is not a finite number. A value that is not a finite number (`NaN`, `Infinity`, `-Infinity`) formats as an empty string, and so does a value that cannot be coerced to a number (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. Options are typed as `FormatCurrencyOptions`. +Formats an integer or float to a string in the BRL pattern. A `number` is formatted as-is (sign and decimals preserved). A `string` input is read by the same rule as `parseCurrency`, except that a value written without any separator stays in whole units: the last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator, every other `,` or `.` is a thousands separator, and a `-` written before the first digit is preserved. So `'1.234,56'` formats as `1.234,56`, `'-10.5'` as `-10,50` and `'1234'` as `1.234,00`. `precision` is clamped to `0..20` (the package limit, the bound Node 20 still enforces on `Intl.NumberFormat`), defaults to 2, and falls back to 2 when it is not a finite number. A value that is not a finite number (`NaN`, `Infinity`, `-Infinity`) formats as an empty string, and so does a value that cannot be coerced to a number (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. `options.symbol` prefixes the result with the `R$` currency symbol (default `false`). Options are typed as `FormatCurrencyOptions`. ```javascript import { formatCurrency } from '@brazilian-utils/brazilian-utils'; @@ -1410,25 +1411,32 @@ parseCnh('026503064-61'); // '02650306461' ### getCepInfoByAddress -Fetch CEPs from an address using ViaCEP. Throws `GetCepInfoByAddressValidationError` when the UF, city or street is missing/invalid — including when the argument is not an object at all (omitted, `null`, a string) and when `federalUnit` is not a string, neither of which leaks a raw `TypeError` — `GetCepInfoByAddressNotFoundError` when no address matches the query, and `GetCepInfoByAddressError` when ViaCEP itself answers with an HTTP error status. A request that cannot be performed at all (a transport failure) rejects with the underlying `fetch` error instead. +Fetch CEPs from an address using ViaCEP. Throws `GetCepInfoByAddressValidationError` when the UF, city or street is missing/invalid — including when the argument is not an object at all (omitted, `null`, a string) and when `federalUnit` is not a string, neither of which leaks a raw `TypeError` — `GetCepInfoByAddressNotFoundError` when no address matches the query, and `GetCepInfoByAddressError` when ViaCEP itself answers with an HTTP error status. A request that cannot be performed at all (a transport failure) rejects with the underlying `fetch` error instead. Each item is typed as `CepAddressInfo` and carries the ViaCEP payload unchanged, under ViaCEP's own field names: `cep`, `logradouro`, `complemento`, `unidade`, `bairro`, `localidade`, `uf`, `estado`, `regiao`, `ibge`, `gia`, `ddd` and `siafi`. A broad street name matches many CEPs, so query as narrowly as the address allows. ```javascript import { getCepInfoByAddress } from '@brazilian-utils/brazilian-utils'; const ceps = await getCepInfoByAddress({ - federalUnit: 'SP', - city: 'Sao Paulo', - street: 'Avenida Paulista' + federalUnit: 'MG', + city: 'Ouro Preto', + street: 'Rua Direita' }); // [ // { -// cep: '01310-100', -// logradouro: 'Avenida Paulista', -// complemento: 'de 612 a 1510 - lado par', -// bairro: 'Bela Vista', -// localidade: 'São Paulo', -// uf: 'SP' +// cep: '35411-152', +// logradouro: 'Rua Direita', +// complemento: '', +// unidade: '', +// bairro: 'Riacho (Amarantina)', +// localidade: 'Ouro Preto', +// uf: 'MG', +// estado: 'Minas Gerais', +// regiao: 'Sudeste', +// ibge: '3146107', +// gia: '', +// ddd: '31', +// siafi: '4921' // } // ] ``` @@ -1740,7 +1748,7 @@ addBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) ### subBusinessDays -Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too. A negative `amount` walks forwards. +Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too, `options.stateCode` included. A negative `amount` walks forwards. ```javascript import { subBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1852,7 +1860,7 @@ isValidCns('abc123456789010000'); // false (not written as a CNS) ### formatCns -Format a CNS (Cartão Nacional de Saúde) number into the common display groups of 3-4-4-4 digits separated by spaces. Options are typed as `FormatCnsOptions`. +Format a CNS (Cartão Nacional de Saúde) number into the common display groups of 3-4-4-4 digits separated by spaces. `options.pad` (part of `FormatCnsOptions`) left-pads the value with zeros up to the 15 slots of the pattern before masking (default `false`). ```javascript import { formatCns } from '@brazilian-utils/brazilian-utils'; @@ -1864,7 +1872,7 @@ formatCns('89010001', { pad: true }); // '000 0000 8901 0001' ### isValidCertidao -Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with weights cycling from 2 to 10 and back through 0. Accepts the usual mask characters and whitespace between/around groups. The layout is the one [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023) currently publishes, with inciso II and §§ 1º to 5º in the redação of the Provimento CN nº 237/2026 and the rest of the article in that of the Provimento CN nº 182/2024; the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). +Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with weights cycling from 2 to 10 and back through 0. Accepts the usual mask characters and whitespace between/around groups. The layout is the one [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023) currently publishes, with inciso II and §§ 1º and 3º to 5º in the redação of the Provimento CN nº 237/2026 and the rest of the article, § 2º included, in that of the Provimento CN nº 182/2024; the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311) and got its digit structure from the also revoked [Provimento CNJ nº 3/2009, art. 7º](https://atos.cnj.jus.br/atos/detalhar/1310). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). The serviço digits are fixed at `55`, the code [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) assigns to the registro civil das pessoas naturais, so a matrícula carrying any other pair in the ninth and tenth positions is rejected however good its check digits are. The book-type digit always has to name one of the nine book types (the same `CertidaoType` returned by `parseCertidao`), so a matrícula whose digit is `0` is rejected however good its check digits are, the same way `parseCertidao` returns `null` for it. `options.accept` (part of `IsValidCertidaoOptions`) narrows that to the listed types; it defaults to every type, and a value that is not an array falls back to that default. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. @@ -1959,7 +1967,7 @@ formatCei('249', { pad: true }); // 00.000.00002/49 ### isValidCno -Check if a CNO (Cadastro Nacional de Obras) number is valid. The CNO replaced the CEI for construction works and kept its numbering, so a work registered under a legacy CEI keeps the same number and both registries validate identically: 12 digits printed as `00.000.00000/00` with a check digit calculated over the 11 base digits. The Receita Federal does not publish the check digit rule; it was confirmed against the [Cadastro Nacional de Obras (CNO) open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) of the Receita Federal: every one of the 38432 works registered in Minas Gerais passes this check. +Check if a CNO (Cadastro Nacional de Obras) number is valid. The CNO replaced the CEI for construction works and kept its numbering, so a work registered under a legacy CEI keeps the same number and both registries validate identically: 12 digits printed as `00.000.00000/00` with a check digit calculated over the 11 base digits. The Receita Federal does not publish the check digit rule; it was confirmed against the [Cadastro Nacional de Obras (CNO) open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) of the Receita Federal: every work in the Minas Gerais extract of that dataset passes this check. The catalogue page itself publishes only the dataset's description and download links, not that result. ```javascript import { isValidCno } from '@brazilian-utils/brazilian-utils'; @@ -2027,7 +2035,7 @@ isValidRegistroProfissional('SP-123456/T-3', { council: 'CRC' }); // false ("T" ### isValidVin -Check if a VIN (Vehicle Identification Number / chassi) is valid. Checks the length (17 characters), the excluded letters (`I`, `O`, `Q` are never valid; [ISO 3779:2009](https://www.iso.org/standard/52200.html) structure) and the check digit at the 9th position, with the check digit and transliteration computed per [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15). That check digit is a North-American requirement (49 CFR 565.15 / SAE J853): Resolução CONTRAN nº 24/1998 and ABNT NBR 6066 define the Brazilian VIN structure but do not mandate it, so many Brazilian-built VINs do not carry a matching check digit. This function is therefore a North-American-style structural check, not a universal validator of Brazilian VINs. Case-insensitive and trims surrounding whitespace. +Check if a VIN (Vehicle Identification Number / chassi) is valid. Checks the length (17 characters), the excluded letters (`I`, `O`, `Q` are never valid; [ISO 3779:2009](https://www.iso.org/standard/52200.html) structure) and the check digit at the 9th position, with the check digit and transliteration computed per [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15). That check digit is a North-American requirement (49 CFR 565.15 / SAE J853): [Resolução CONTRAN nº 968/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf) (which revoked Resolução CONTRAN nº 24/1998 from 1 January 2025) and ABNT NBR 6066 define the Brazilian VIN structure but do not mandate it, so many Brazilian-built VINs do not carry a matching check digit. This function is therefore a North-American-style structural check, not a universal validator of Brazilian VINs. Case-insensitive and trims surrounding whitespace. ```javascript import { isValidVin } from '@brazilian-utils/brazilian-utils'; @@ -2071,7 +2079,7 @@ The occupation titles come from the [official CBO 2002 occupation table publishe ### isValidCnae -Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the CNAE 2.3 table published by IBGE. Accepts the code with or without the `NNNN-N/NN` mask, or as a number. A string is only read as a code when it is written in one of those forms (the 7 digits, or the mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the [CNAE-Subclasses 2.3 table published by IBGE](https://concla.ibge.gov.br/busca-online-cnae.html), the current subclass revision of CNAE 2.0. Accepts the code with or without the `NNNN-N/NN` mask, or as a number. A string is only read as a code when it is written in one of those forms (the 7 digits, or the mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. ```javascript import { isValidCnae } from '@brazilian-utils/brazilian-utils'; @@ -2185,7 +2193,7 @@ Check if a CST (Código de Situação Tributária) code is valid for a given tax `options.tax` (part of `IsValidCstOptions`) is optional: omit it to accept a code that exists in any one of the four tables above. -The ICMS Tabela B is the one in force: the [consolidated Anexo I of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), whose current wording came from [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (effective 01.12.23) and which [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) amended by striking items 12, 13, 52, 72 and 74 (effects from 09.07.24) before they ever took effect: 39/23 had added them "sem efeitos", so those codes were never in force. `02`, `15`, `53` and `61` are its monofasia de combustíveis codes. +The ICMS Tabela B is the one in force: the [consolidated Anexo I of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), whose current wording came from [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (effective 01.12.23) and which [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) amended by striking items 12, 13, 52, 72 and 74 (effects from 09.07.24) before they ever took effect: 39/23 had deferred their effect to 1º de outubro de 2024, so the revocation reached them first and those codes were never in force. `02`, `15`, `53` and `61` are its monofasia de combustíveis codes. A string is only read as a code when it is written in one of the documented forms (the 2 digits of a Tabela B code, or the 3 digits of the ICMS form with an optional single separator after the origin digit, plus optional surrounding whitespace), and a number only when it is a non-negative safe integer. The origin digit is the only boundary a printed CST has, so `'0 10'` and `'1-10'` are read while `'0-0'`, `'11-0'` and `'00-'` are not. diff --git a/docs/llms.txt b/docs/llms.txt index 2d9dac25..d700f79e 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -56,7 +56,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [isValidRegistroProfissional](https://brazilian-utils.com.br/utilities.md#isvalidregistroprofissional): Check the structure of a professional council registration number (registro/inscrição profissional). - [isValidVin](https://brazilian-utils.com.br/utilities.md#isvalidvin): Check if a VIN (Vehicle Identification Number / chassi) is valid. - [isValidCbo](https://brazilian-utils.com.br/utilities.md#isvalidcbo): Check if a CBO (Classificação Brasileira de Ocupações) code exists in the MTE occupation table. -- [isValidCnae](https://brazilian-utils.com.br/utilities.md#isvalidcnae): Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the CNAE 2.3 table published by IBGE. +- [isValidCnae](https://brazilian-utils.com.br/utilities.md#isvalidcnae): Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the CNAE-Subclasses 2.3 table published by IBGE, the current subclass revision of CNAE 2.0. - [isValidNcm](https://brazilian-utils.com.br/utilities.md#isvalidncm): Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. - [isValidCfop](https://brazilian-utils.com.br/utilities.md#isvalidcfop): Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table. - [isValidCst](https://brazilian-utils.com.br/utilities.md#isvalidcst): Check if a CST (Código de Situação Tributária) code is valid for a given tax. @@ -160,7 +160,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [isHoliday](https://brazilian-utils.com.br/utilities.md#isholiday): Check if a specific date is a Brazilian holiday. - [isBusinessDay](https://brazilian-utils.com.br/utilities.md#isbusinessday): Check if a date is a Brazilian business day (dia útil). - [addBusinessDays](https://brazilian-utils.com.br/utilities.md#addbusinessdays): Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and Brazilian holidays exactly as `isBusinessDay` defines them (same `BusinessDayOptions`). -- [subBusinessDays](https://brazilian-utils.com.br/utilities.md#subbusinessdays): Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too. +- [subBusinessDays](https://brazilian-utils.com.br/utilities.md#subbusinessdays): Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too, `options.stateCode` included. - [differenceInBusinessDays](https://brazilian-utils.com.br/utilities.md#differenceinbusinessdays): Count the number of Brazilian business days (dias úteis) between two dates, mirroring the semantics of date-fns' `differenceInBusinessDays` (verified against its source), argument order included: `differenceInBusinessDays(laterDate, earlierDate, options?)`. - [convertDateToWords](https://brazilian-utils.com.br/utilities.md#convertdatetowords): Formats a date as its Brazilian Portuguese "por extenso" textual representation, e.g. `"01/01/2024"` becomes `"primeiro de janeiro de dois mil e vinte e quatro"`. - [removeAccents](https://brazilian-utils.com.br/utilities.md#removeaccents): Remove diacritical marks (accents, tildes, cedillas) from a string, decomposing every accented character into its base letter plus combining marks (Unicode NFD) and dropping the combining marks. diff --git a/docs/pt-br/getting-started.md b/docs/pt-br/getting-started.md index df828bbe..20f16f76 100644 --- a/docs/pt-br/getting-started.md +++ b/docs/pt-br/getting-started.md @@ -73,7 +73,7 @@ Alguns utilitários são a exceção: cada um embute um dataset oficial e pesa m | `getCities` | nomes dos 5571 municípios do IBGE | 154,0 KB | 49,7 KB | | `isValidNcm` | códigos NCM (Nomenclatura Comum do Mercosul) | 113,8 KB | 24,3 KB | | `isValidCbo` · `getCbo` | títulos das ocupações da CBO 2002 | 118,8 KB | 30,4 KB | -| `isValidCnae` · `getCnae` | subclasses da CNAE 2.3 | 94,0 KB | 21,3 KB | +| `isValidCnae` · `getCnae` | CNAE-Subclasses 2.3 | 94,0 KB | 21,3 KB | | `isValidCfop` · `getCfop` | descrições das operações do CFOP | 68,7 KB | 6,8 KB | | `getBanks` · `getBankByCode` | participantes do STR do Banco Central (COMPE + ISPB) | 38,3 KB | 9,6 KB | diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 408cde9c..c7b94268 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -2,7 +2,7 @@ Aqui você encontrará todos os utilitários disponíveis para uso. -> **Tratamento de entrada:** nenhuma função pública síncrona lança exceção com `null`/`undefined` ou um valor de tipo incorreto; as duas funções de rede, `getAddressInfoByCep` e `getCepInfoByAddress`, rejeitam com seus erros tipados (veja as seções delas). Os validadores (`isValid*`) retornam `false`; `isHoliday` retorna `false`; `getHolidays` retorna `[]`; `generateProcessoJuridico` retorna `null`; `getMunicipality` retorna `null` para uma busca malformada/sem correspondência. Todas as demais funções `format*`/`parse*` retornam um valor vazio do seu tipo de retorno: toda função `format*`, `capitalize`, e as funções `parse*` que retornam string (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) retornam `""`; `parseCurrency` retorna `0`; os parsers que retornam objeto/tupla — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — retornam `null`. `formatCurrency` retorna `""` para um número não finito e para um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo); `null`, arrays e booleanos passam por `Number()` como no 2.3.0. A única exceção à promessa acima: um objeto criado com `Object.create(null)` não tem `toString`, então as funções `format*`/`parse*` que leem a entrada como texto ainda lançam um `TypeError` para ele, exatamente como na 2.3.0. +> **Tratamento de entrada:** nenhuma função pública síncrona lança exceção com `null`/`undefined` ou um valor de tipo incorreto; as duas funções de rede, `getAddressInfoByCep` e `getCepInfoByAddress`, rejeitam com seus erros tipados (veja as seções delas). Os validadores (`isValid*`) retornam `false`; `isHoliday` retorna `false`; `getHolidays` retorna `[]`; `getBoletoInfo` retorna `undefined` para um boleto inválido, a única função do pacote que retorna `undefined`; `generateProcessoJuridico` retorna `null`; `getMunicipality` retorna `null` para uma busca malformada/sem correspondência. Todas as demais funções `format*`/`parse*` retornam um valor vazio do seu tipo de retorno: toda função `format*`, `capitalize`, e as funções `parse*` que retornam string (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) retornam `""`; `parseCurrency` retorna `0`; os parsers que retornam objeto/tupla — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — retornam `null`. `formatCurrency` retorna `""` para um número não finito e para um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo); `null`, arrays e booleanos passam por `Number()` como no 2.3.0. A única exceção à promessa acima: um objeto criado com `Object.create(null)` não tem `toString`, então as funções `format*`/`parse*` que leem a entrada como texto ainda lançam um `TypeError` para ele, exatamente como na 2.3.0. ## isValidCpf @@ -17,7 +17,7 @@ isValidCpf('111 444 777 35'); // true (máscara com espaços) ## formatCpf -Formata o CPF. `options.obfuscate` (parte de `FormatCpfOptions`) esconde os 3 primeiros dígitos e os 2 dígitos verificadores (`***.456.789-**`), a convenção de exibição do gov.br / Receita Federal, aplicada após o `pad`. É lida por veracidade (truthiness), do mesmo jeito que o `pad`, então qualquer valor verdadeiro esconde os dígitos. +Formata o CPF. `options.pad` (parte de `FormatCpfOptions`) preenche o valor com zeros à esquerda até as 11 posições do padrão antes de aplicar a máscara (padrão `false`). `options.obfuscate` (do mesmo tipo) esconde os 3 primeiros dígitos e os 2 dígitos verificadores (`***.456.789-**`), a convenção de exibição do gov.br / Receita Federal, aplicada após o `pad`. É lida por veracidade (truthiness), do mesmo jeito que o `pad`, então qualquer valor verdadeiro esconde os dígitos. ```javascript import { formatCpf } from '@brazilian-utils/brazilian-utils'; @@ -50,7 +50,7 @@ generateCpf('SP'); // o 9º dígito é 8, o código da região fiscal de SP ## isValidCnpj -Valida se o CNPJ é válido. Suporta tanto o formato numérico (`version: 1`, padrão) quanto o formato alfanumérico (`version: 2`), e aceita os caracteres de máscara usuais e espaços em branco. As opções são tipadas como `IsValidCnpjOptions`. +Valida se o CNPJ é válido. `options.version` (parte de `IsValidCnpjOptions`) escolhe qual formato é aceito: `1` (padrão) apenas o formato numérico, `2` tanto o numérico quanto o alfanumérico; qualquer outro valor é lido como `1`, do mesmo jeito que `formatCnpj` e `parseCnpj` o leem. Os caracteres de máscara usuais e espaços em branco são aceitos nas duas versões. ```javascript import { isValidCnpj } from '@brazilian-utils/brazilian-utils'; @@ -61,7 +61,7 @@ isValidCnpj('q0slfmbd7vx439', { version: 2 }); // true (alfanumérico minúsculo ## formatCnpj -Formata o CNPJ. `options.obfuscate` (parte de `FormatCnpjOptions`) esconde os 2 primeiros dígitos e os 2 dígitos verificadores (`**.345.678/0001-**`), a convenção de exibição do gov.br / Receita Federal. Vale para as duas versões, é aplicada após o `pad` e é lida por veracidade (truthiness), do mesmo jeito que o `pad`, então qualquer valor verdadeiro esconde os dígitos. +Formata o CNPJ. `options.pad` (parte de `FormatCnpjOptions`) preenche o valor com zeros à esquerda até as 14 posições do padrão antes de aplicar a máscara (padrão `false`). `options.version` (do mesmo tipo) escolhe qual formato de CNPJ é lido: `1` (padrão) apenas numérico, `2` alfanumérico. `options.obfuscate` esconde os 2 primeiros dígitos e os 2 dígitos verificadores (`**.345.678/0001-**`), a convenção de exibição do gov.br / Receita Federal. Vale para as duas versões, é aplicada após o `pad` e é lida por veracidade (truthiness), do mesmo jeito que o `pad`, então qualquer valor verdadeiro esconde os dígitos. ```javascript import { formatCnpj } from '@brazilian-utils/brazilian-utils'; @@ -74,7 +74,7 @@ formatCnpj('12345678000195', { obfuscate: true }); // **.345.678/0001-** ## parseCnpj -Remove a formatação do CNPJ, retorna um valor normalizado e limita o resultado a 14 caracteres. As opções são tipadas como `ParseCnpjOptions`. +Remove a formatação do CNPJ, retorna um valor normalizado e limita o resultado a 14 caracteres. `options.version` (parte de `ParseCnpjOptions`) escolhe qual formato de CNPJ é normalizado: `1` (padrão) mantém apenas dígitos, `2` mantém letras e dígitos, de modo que um CNPJ alfanumérico sobrevive à ida e volta. ```javascript import { parseCnpj } from '@brazilian-utils/brazilian-utils'; @@ -123,7 +123,7 @@ isValidBoleto('846100000005246100291102005460339004695895061080'); // true (bole ## formatBoleto -Formata um número de boleto. A máscara de arrecadação (convênio/tributos) só se aplica à linha digitável de 48 dígitos que começa com `8`; o código de barras de arrecadação de 44 dígitos não tem agrupamento de exibição definido pela FEBRABAN e mantém a máscara de "cobrança bancária". +Formata um número de boleto. `options.pad` (parte de `FormatBoletoOptions`) preenche o valor com zeros à esquerda até o número de posições do padrão antes de aplicar a máscara (padrão `false`). A máscara de arrecadação (convênio/tributos) só se aplica à linha digitável de 48 dígitos que começa com `8`; o código de barras de arrecadação de 44 dígitos não tem agrupamento de exibição definido pela FEBRABAN e mantém a máscara de "cobrança bancária". ```javascript import { formatBoleto } from '@brazilian-utils/brazilian-utils'; @@ -157,7 +157,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ## getBoletoInfo -Extrai informações de um boleto (valor, data de vencimento, código do banco). Aceita opcionalmente `{ referenceDate }` (tipado como `GetBoletoInfoOptions`) para resolver o ciclo do "fator de vencimento" a partir de uma data específica em vez de agora (o ciclo do fator reiniciou em 22/02/2025, segundo a FEBRABAN). Nem a FEBRABAN nem o Banco Central publicam uma forma de distinguir um fator do ciclo antigo de um do ciclo novo, então todo fator resolve para uma de duas datas separadas por 9000 dias e o `referenceDate` escolhe entre elas por meio das janelas de segurança da própria biblioteca: o mesmo boleto pode passar a resolver para a outra candidata com o tempo, então informe `referenceDate` explicitamente sempre que a resposta precisar ser estável. A busca de ciclo nunca desce abaixo do primeiro ciclo, então um `referenceDate` anterior ao próprio esquema ainda resolve um fator para a data mais antiga que aquele fator consegue representar, em vez de uma anterior à data-base de 07/10/1997. Para um boleto de arrecadação, o resultado, tipado como `BoletoInfo`, continua trazendo as duas chaves, porém vazias, `bankCode: ''` e `expirationDate: null`, já que o boleto não tem código de banco nem fator de vencimento, e acrescenta `type: "arrecadacao"`, `segment`, `value` e `hasEffectiveValue`. +Extrai informações de um boleto (valor, data de vencimento, código do banco). Retorna `undefined` quando `value` não é um boleto válido — o `isValidBoleto` é verificado antes —, exatamente como na 2.3.0, então o resultado precisa ser estreitado antes de ser lido. Aceita opcionalmente `{ referenceDate }` (tipado como `GetBoletoInfoOptions`) para resolver o ciclo do "fator de vencimento" a partir de uma data específica em vez de agora (o ciclo do fator reiniciou em 22/02/2025, segundo a FEBRABAN). Nem a FEBRABAN nem o Banco Central publicam uma forma de distinguir um fator do ciclo antigo de um do ciclo novo, então todo fator resolve para uma de duas datas separadas por 9000 dias e o `referenceDate` escolhe entre elas por meio das janelas de segurança da própria biblioteca: o mesmo boleto pode passar a resolver para a outra candidata com o tempo, então informe `referenceDate` explicitamente sempre que a resposta precisar ser estável. A busca de ciclo nunca desce abaixo do primeiro ciclo, então um `referenceDate` anterior ao próprio esquema ainda resolve um fator para a data mais antiga que aquele fator consegue representar, em vez de uma anterior à data-base de 07/10/1997. Para um boleto de arrecadação, o resultado, tipado como `BoletoInfo`, continua trazendo as duas chaves, porém vazias, `bankCode: ''` e `expirationDate: null`, já que o boleto não tem código de banco nem fator de vencimento, e acrescenta `type: "arrecadacao"`, `segment`, `value` e `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -272,7 +272,7 @@ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // nu Valida se uma chave de acesso de DF-e (Documento Fiscal eletrônico) é válida. Cobre todos os documentos cuja chave de acesso é a mesma string de 44 dígitos: NF-e (modelo 55), NFC-e (65), CT-e (57, o Conhecimento de Transporte Eletrônico instituído pela cláusula primeira do [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, o Conhecimento de Transporte Eletrônico para Outros Serviços instituído pela cláusula primeira do [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, o CT-e Guia de Transporte de Valores instituído pela cláusula primeira do [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) e NFCom (62). O CF-e-SAT (59) fica de fora: sua "chave de consulta" de 44 posições é composta de outro jeito. Aceita espaços entre os grupos de dígitos (a máscara de exibição usual) e os prefixos `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` e `NFCom` encontrados no atributo `Id` do XML do documento. -A forma de emissão (`tpEmis`) é conferida contra os códigos que o MOC daquele modelo atribui, então o conjunto aceito muda com o modelo: de 1 a 7 e 9 para NF-e e NFC-e, `{1, 3, 4, 5, 7, 8}` para o CT-e, `{1, 5, 7, 8}` para o CT-e OS, `{1, 2, 7, 8}` para a GTV-e, `{1, 2, 3}` para o MDF-e e `{1, 2}` para o BP-e, a NF3e e a NFCom. O código 8, a autorização pela SVC-SP, é atribuído somente pelo [MOC do CT-e 4.00](https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais), nunca pelo da NF-e; os domínios do [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), da [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) e da [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) vêm dos manuais deles. Para NF-e e NFC-e o código numérico também é conferido contra a regra B03-10 do MOC da NF-e, que proíbe os vinte valores repetidos e sequenciais de `cNF` que ela lista e um `cNF` igual ao número do documento. Já um número de documento todo zerado é recusado em todos os modelos seguindo o leiaute, não por escolha desta biblioteca: o `tiposBasico_v4.00.xsd` do [pacote de schemas da NF-e](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) tipa o `nNF` como `TNF`, cujo pattern é `[1-9]{1}[0-9]{0,8}`, e o Anexo I de cada um dos outros modelos repete o mesmo regex no seu próprio campo de número. +A forma de emissão (`tpEmis`) é conferida contra os códigos que o MOC daquele modelo atribui, então o conjunto aceito muda com o modelo: de 1 a 7 e 9 para NF-e e NFC-e, `{1, 3, 4, 5, 7, 8}` para o CT-e, `{1, 5, 7, 8}` para o CT-e OS, `{1, 2, 7, 8}` para a GTV-e, `{1, 2, 3}` para o MDF-e e `{1, 2}` para o BP-e, a NF3e e a NFCom. O código 8, a autorização pela SVC-SP, é atribuído somente pelo [MOC do CT-e 4.00](https://dfe-portal.svrs.rs.gov.br/CTE/Documentos), nunca pelo da NF-e; os domínios do [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), da [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) e da [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) vêm dos manuais deles. Para NF-e e NFC-e o código numérico também é conferido contra a regra B03-10 do MOC da NF-e, que proíbe os vinte valores repetidos e sequenciais de `cNF` que ela lista e um `cNF` igual ao número do documento. Já um número de documento todo zerado é recusado em todos os modelos seguindo o leiaute, não por escolha desta biblioteca: o `tiposBasico_v4.00.xsd` do [pacote de schemas da NF-e](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) tipa o `nNF` como `TNF`, cujo pattern é `[1-9]{1}[0-9]{0,8}`, e o Anexo I de cada um dos outros modelos repete o mesmo regex no seu próprio campo de número. ```javascript import { isValidNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -327,12 +327,13 @@ isValidEmail('john.doe@hotmail.com'); // true ## isValidPhone -Valida se o número de telefone (celular ou residencial) é válido. Um código de país brasileiro (`+55`, `0055` ou um `55` isolado) é aceito e removido antes da validação, seguindo a regra documentada em `parsePhone`. `options.accept` (tipado como `PhoneType[]`, parte de `IsValidPhoneOptions`) define quais tipos de número são aceitos e tem como padrão `['mobile', 'landline']`; adicione `'service'` para também aceitar os números não geográficos reconhecidos por `isValidServicePhone`, ou informe `[]` para não aceitar nenhum. +Valida se o número de telefone (celular ou residencial) é válido. Um código de país brasileiro (`+55`, `0055` ou um `55` isolado) é aceito e removido antes da validação, seguindo a regra documentada em `parsePhone`. `options.accept` (tipado como `PhoneType[]`, parte de `IsValidPhoneOptions`) define quais tipos de número são aceitos e tem como padrão `['mobile', 'landline']`; adicione `'service'` para também aceitar os números não geográficos reconhecidos por `isValidServicePhone`, ou informe `[]` para não aceitar nenhum. `options.version` (tipado como `PhoneVersion`, parte do mesmo tipo) é repassado ao `isValidMobilePhone` e escolhe qual regra de numeração celular é aplicada: `1` (padrão) o formato antigo, cujo primeiro dígito do número pode ser 6, 7, 8 ou 9, e `2` o atual, que exige 9 e rejeita o prefixo `700`. Vale apenas para celulares; números residenciais e de serviço não são afetados. ```javascript import { isValidPhone } from '@brazilian-utils/brazilian-utils'; isValidPhone('11900000000'); // true +isValidPhone('11712345678', { version: 2 }); // false (v2 exige 9 como primeiro dígito do celular) isValidPhone('+55 11 98765-4321'); // true (código de país aceito) isValidPhone('08001234567'); // false (números de serviço não são aceitos por padrão) isValidPhone('08001234567', { accept: ['service'] }); // true @@ -541,12 +542,12 @@ const address = await getAddressInfoByCep('01310100'); // { cep: '01310100', state: 'SP', city: 'São Paulo', neighborhood: 'Bela Vista', street: 'Avenida Paulista' } // Usando provedores específicos -const address = await getAddressInfoByCep('01310-100', { +const addressFromProviders = await getAddressInfoByCep('01310-100', { providers: ['viacep', 'brasilapi'] }); // Usando número como entrada (será preenchido automaticamente com zeros à esquerda) -const address = await getAddressInfoByCep(1310100); +const addressFromNumber = await getAddressInfoByCep(1310100); ``` ## isValidProcessoJuridico @@ -832,7 +833,7 @@ capitalize(' josé maria '); // José Maria (toda sequência de espaço em b ## formatCurrency -Formata um número inteiro ou float para uma string no padrão BRL. Um `number` é formatado como está (sinal e decimais preservados). Uma entrada em `string` é lida pela mesma regra do `parseCurrency`, com a diferença de que um valor escrito sem nenhum separador permanece em unidades inteiras: o último `,` ou `.` seguido de 1 ou 2 dígitos (ou de até `precision` dígitos, quando esse valor for maior) é o separador decimal, todo outro `,` ou `.` é separador de milhar, e um `-` escrito antes do primeiro dígito é preservado. Assim `'1.234,56'` vira `1.234,56`, `'-10.5'` vira `-10,50` e `'1234'` vira `1.234,00`. `precision` é limitado ao intervalo `0..20` (o limite do pacote, o que o Node 20 ainda impõe ao `Intl.NumberFormat`), o padrão é 2 e volta a 2 quando não é um número finito. Um valor que não seja um número finito (`NaN`, `Infinity`, `-Infinity`) vira string vazia, e um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo) também; `null`, arrays e booleanos passam por `Number()` como no 2.3.0. As opções são tipadas como `FormatCurrencyOptions`. +Formata um número inteiro ou float para uma string no padrão BRL. Um `number` é formatado como está (sinal e decimais preservados). Uma entrada em `string` é lida pela mesma regra do `parseCurrency`, com a diferença de que um valor escrito sem nenhum separador permanece em unidades inteiras: o último `,` ou `.` seguido de 1 ou 2 dígitos (ou de até `precision` dígitos, quando esse valor for maior) é o separador decimal, todo outro `,` ou `.` é separador de milhar, e um `-` escrito antes do primeiro dígito é preservado. Assim `'1.234,56'` vira `1.234,56`, `'-10.5'` vira `-10,50` e `'1234'` vira `1.234,00`. `precision` é limitado ao intervalo `0..20` (o limite do pacote, o que o Node 20 ainda impõe ao `Intl.NumberFormat`), o padrão é 2 e volta a 2 quando não é um número finito. Um valor que não seja um número finito (`NaN`, `Infinity`, `-Infinity`) vira string vazia, e um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo) também; `null`, arrays e booleanos passam por `Number()` como no 2.3.0. `options.symbol` prefixa o resultado com o símbolo monetário `R$` (padrão `false`). As opções são tipadas como `FormatCurrencyOptions`. ```javascript import { formatCurrency } from '@brazilian-utils/brazilian-utils'; @@ -1171,25 +1172,32 @@ parseCnh('026503064-61'); // '02650306461' ## getCepInfoByAddress -Busca CEPs a partir de um endereço usando a ViaCEP. Lança `GetCepInfoByAddressValidationError` quando a UF, a cidade ou a rua estão ausentes/inválidas — inclusive quando o argumento não é um objeto (omitido, `null`, uma string) e quando `federalUnit` não é uma string, casos em que nenhum `TypeError` cru escapa — `GetCepInfoByAddressNotFoundError` quando nenhum endereço corresponde à busca, e `GetCepInfoByAddressError` quando a própria ViaCEP responde com um status de erro HTTP. Uma requisição que não pode ser realizada (falha de transporte) rejeita com o erro original do `fetch`. +Busca CEPs a partir de um endereço usando a ViaCEP. Lança `GetCepInfoByAddressValidationError` quando a UF, a cidade ou a rua estão ausentes/inválidas — inclusive quando o argumento não é um objeto (omitido, `null`, uma string) e quando `federalUnit` não é uma string, casos em que nenhum `TypeError` cru escapa — `GetCepInfoByAddressNotFoundError` quando nenhum endereço corresponde à busca, e `GetCepInfoByAddressError` quando a própria ViaCEP responde com um status de erro HTTP. Uma requisição que não pode ser realizada (falha de transporte) rejeita com o erro original do `fetch`. Cada item é tipado como `CepAddressInfo` e traz a resposta da ViaCEP sem alterações, com os nomes de campo da própria ViaCEP: `cep`, `logradouro`, `complemento`, `unidade`, `bairro`, `localidade`, `uf`, `estado`, `regiao`, `ibge`, `gia`, `ddd` e `siafi`. Um nome de rua abrangente corresponde a muitos CEPs, então busque de forma tão específica quanto o endereço permitir. ```javascript import { getCepInfoByAddress } from '@brazilian-utils/brazilian-utils'; const ceps = await getCepInfoByAddress({ - federalUnit: 'SP', - city: 'Sao Paulo', - street: 'Avenida Paulista' + federalUnit: 'MG', + city: 'Ouro Preto', + street: 'Rua Direita' }); // [ // { -// cep: '01310-100', -// logradouro: 'Avenida Paulista', -// complemento: 'de 612 a 1510 - lado par', -// bairro: 'Bela Vista', -// localidade: 'São Paulo', -// uf: 'SP' +// cep: '35411-152', +// logradouro: 'Rua Direita', +// complemento: '', +// unidade: '', +// bairro: 'Riacho (Amarantina)', +// localidade: 'Ouro Preto', +// uf: 'MG', +// estado: 'Minas Gerais', +// regiao: 'Sudeste', +// ibge: '3146107', +// gia: '', +// ddd: '31', +// siafi: '4921' // } // ] ``` @@ -1501,7 +1509,7 @@ addBusinessDays(new Date(2024, 0, 2), 1.5); // null (não é um número inteiro) ## subBusinessDays -Subtrai um número de dias úteis brasileiros de uma data: `subBusinessDays(date, amount, options?)` é `addBusinessDays(date, -amount, options)`, e é exatamente assim que a função é implementada, então tudo o que vale acima vale aqui (o horário preservado, a entrada intacta, um `amount` igual a `0` devolvendo a data sem alterações, o intervalo de 1900 a 2099 e os casos de `null`). Um `amount` negativo anda para frente. +Subtrai um número de dias úteis brasileiros de uma data: `subBusinessDays(date, amount, options?)` é `addBusinessDays(date, -amount, options)`, e é exatamente assim que a função é implementada, então tudo o que vale acima vale aqui (o horário preservado, a entrada intacta, um `amount` igual a `0` devolvendo a data sem alterações, o intervalo de 1900 a 2099 e os casos de `null`), inclusive o `options.stateCode`. Um `amount` negativo anda para frente. ```javascript import { subBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1613,7 +1621,7 @@ isValidCns('abc123456789010000'); // false (não escrito como um CNS) ## formatCns -Formata um número de CNS (Cartão Nacional de Saúde) nos grupos de exibição usuais de 3-4-4-4 dígitos separados por espaço. As opções são tipadas como `FormatCnsOptions`. +Formata um número de CNS (Cartão Nacional de Saúde) nos grupos de exibição usuais de 3-4-4-4 dígitos separados por espaço. `options.pad` (parte de `FormatCnsOptions`) preenche o valor com zeros à esquerda até as 15 posições do padrão antes de aplicar a máscara (padrão `false`). ```javascript import { formatCns } from '@brazilian-utils/brazilian-utils'; @@ -1625,7 +1633,7 @@ formatCns('89010001', { pad: true }); // '000 0000 8901 0001' ## isValidCertidao -Verifica se a matrícula de uma certidão de registro civil (nascimento, casamento, óbito e os demais atos mantidos por uma serventia de registro civil das pessoas naturais) é válida. A matrícula tem 32 dígitos distribuídos em 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), e os dois dígitos verificadores usam módulo 11 com pesos ciclando de 2 a 10 e voltando por 0. Aceita os caracteres de máscara usuais e espaços entre e ao redor dos grupos. O layout é o publicado atualmente no [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023), com o inciso II e os §§ 1º a 5º na redação do Provimento CN nº 237/2026 e o restante do artigo na do Provimento CN nº 182/2024; a própria matrícula foi instituída pelo já revogado [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311). Os dígitos verificadores estão detalhados em [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e implementado pelo [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) e pelo [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). +Verifica se a matrícula de uma certidão de registro civil (nascimento, casamento, óbito e os demais atos mantidos por uma serventia de registro civil das pessoas naturais) é válida. A matrícula tem 32 dígitos distribuídos em 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), e os dois dígitos verificadores usam módulo 11 com pesos ciclando de 2 a 10 e voltando por 0. Aceita os caracteres de máscara usuais e espaços entre e ao redor dos grupos. O layout é o publicado atualmente no [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023), com o inciso II e os §§ 1º e 3º a 5º na redação do Provimento CN nº 237/2026 e o restante do artigo, inclusive o § 2º, na do Provimento CN nº 182/2024; a própria matrícula foi instituída pelo já revogado [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311) e ganhou sua estrutura de dígitos no também revogado [Provimento CNJ nº 3/2009, art. 7º](https://atos.cnj.jus.br/atos/detalhar/1310). Os dígitos verificadores estão detalhados em [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e implementado pelo [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) e pelo [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). Os dígitos do serviço são fixos em `55`, o código que o [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) atribui ao registro civil das pessoas naturais, então uma matrícula com qualquer outro par na nona e décima posições é rejeitada por mais que os dígitos verificadores confiram. O dígito do tipo de livro sempre precisa nomear um dos nove tipos de livro (o mesmo `CertidaoType` retornado por `parseCertidao`), então uma matrícula cujo dígito é `0` é rejeitada por mais que os dígitos verificadores confiram, do mesmo jeito que `parseCertidao` devolve `null` para ela. `options.accept` (parte de `IsValidCertidaoOptions`) restringe ainda mais aos tipos listados; o padrão é aceitar todos os tipos, e um valor que não seja um array volta para esse padrão. Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. @@ -1720,7 +1728,7 @@ formatCei('249', { pad: true }); // 00.000.00002/49 ## isValidCno -Verifica se um número de CNO (Cadastro Nacional de Obras) é válido. O CNO substituiu o CEI para obras e manteve a mesma numeração, então uma obra registrada sob um CEI antigo conserva o número e os dois cadastros são validados do mesmo jeito: 12 dígitos impressos como `00.000.00000/00`, com o dígito verificador calculado sobre os 11 dígitos da base. A Receita Federal não publica a regra do dígito verificador; ela foi confirmada contra os [dados abertos do Cadastro Nacional de Obras (CNO)](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) da Receita Federal: todas as 38432 obras registradas em Minas Gerais passam nesta verificação. +Verifica se um número de CNO (Cadastro Nacional de Obras) é válido. O CNO substituiu o CEI para obras e manteve a mesma numeração, então uma obra registrada sob um CEI antigo conserva o número e os dois cadastros são validados do mesmo jeito: 12 dígitos impressos como `00.000.00000/00`, com o dígito verificador calculado sobre os 11 dígitos da base. A Receita Federal não publica a regra do dígito verificador; ela foi confirmada contra os [dados abertos do Cadastro Nacional de Obras (CNO)](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) da Receita Federal: todas as obras do recorte de Minas Gerais desse conjunto passam nesta verificação. A página do catálogo publica apenas a descrição e os links de download do conjunto, não esse resultado. ```javascript import { isValidCno } from '@brazilian-utils/brazilian-utils'; @@ -1788,7 +1796,7 @@ isValidRegistroProfissional('SP-123456/T-3', { council: 'CRC' }); // false ("T" ## isValidVin -Valida se um VIN (Vehicle Identification Number / chassi) é válido. Verifica o tamanho (17 caracteres), as letras excluídas (`I`, `O`, `Q` nunca são válidas; estrutura da [ISO 3779:2009](https://www.iso.org/standard/52200.html)) e o dígito verificador na 9ª posição, calculado e transliterado conforme o [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15). Esse dígito verificador é uma exigência norte-americana (49 CFR 565.15 / SAE J853): a Resolução CONTRAN nº 24/1998 e a ABNT NBR 6066 definem a estrutura do VIN brasileiro, mas não o exigem, então muitos VINs fabricados no Brasil não possuem um dígito verificador correspondente. Esta função é, portanto, uma verificação estrutural no padrão norte-americano, não um validador universal de VINs brasileiros. Não diferencia maiúsculas de minúsculas e remove espaços nas extremidades. +Valida se um VIN (Vehicle Identification Number / chassi) é válido. Verifica o tamanho (17 caracteres), as letras excluídas (`I`, `O`, `Q` nunca são válidas; estrutura da [ISO 3779:2009](https://www.iso.org/standard/52200.html)) e o dígito verificador na 9ª posição, calculado e transliterado conforme o [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15). Esse dígito verificador é uma exigência norte-americana (49 CFR 565.15 / SAE J853): a [Resolução CONTRAN nº 968/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf) (que revogou a Resolução CONTRAN nº 24/1998 a partir de 1º de janeiro de 2025) e a ABNT NBR 6066 definem a estrutura do VIN brasileiro, mas não o exigem, então muitos VINs fabricados no Brasil não possuem um dígito verificador correspondente. Esta função é, portanto, uma verificação estrutural no padrão norte-americano, não um validador universal de VINs brasileiros. Não diferencia maiúsculas de minúsculas e remove espaços nas extremidades. ```javascript import { isValidVin } from '@brazilian-utils/brazilian-utils'; @@ -1832,7 +1840,7 @@ Os títulos das ocupações vêm da [tabela oficial de ocupações da CBO 2002 p ## isValidCnae -Valida se um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas) existe na tabela CNAE 2.3 publicada pelo IBGE. Aceita o código com ou sem a máscara `NNNN-N/NN`, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 7 dígitos, ou a máscara, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. +Valida se um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas) existe na [tabela CNAE-Subclasses 2.3 publicada pelo IBGE](https://concla.ibge.gov.br/busca-online-cnae.html), a revisão de subclasses atual da CNAE 2.0. Aceita o código com ou sem a máscara `NNNN-N/NN`, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 7 dígitos, ou a máscara, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. ```javascript import { isValidCnae } from '@brazilian-utils/brazilian-utils'; @@ -1946,7 +1954,7 @@ Valida um código de CST (Código de Situação Tributária) para um tributo. In `options.tax` (parte de `IsValidCstOptions`) é opcional: omita-o para aceitar um código que exista em qualquer uma das quatro tabelas acima. -A Tabela B do ICMS é a vigente: o [Anexo I consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), cuja redação atual veio do [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (efeitos a partir de 01.12.23) e que o [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) alterou suprimindo os itens 12, 13, 52, 72 e 74 (efeitos a partir de 09.07.24) antes que eles chegassem a produzir efeitos: o 39/23 os havia acrescentado "sem efeitos", então esses códigos nunca estiveram em vigor. `02`, `15`, `53` e `61` são seus códigos de monofasia de combustíveis. +A Tabela B do ICMS é a vigente: o [Anexo I consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), cuja redação atual veio do [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (efeitos a partir de 01.12.23) e que o [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) alterou suprimindo os itens 12, 13, 52, 72 e 74 (efeitos a partir de 09.07.24) antes que eles chegassem a produzir efeitos: o 39/23 havia adiado a produção de efeitos deles para 1º de outubro de 2024, então a revogação os alcançou antes e esses códigos nunca estiveram em vigor. `02`, `15`, `53` e `61` são seus códigos de monofasia de combustíveis. Uma string só é lida como código quando está escrita em uma das formas documentadas (os 2 dígitos de um código da Tabela B, ou os 3 dígitos da forma do ICMS com um único separador opcional depois do dígito de origem, além de espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. O dígito de origem é a única fronteira que um CST impresso tem, então `'0 10'` e `'1-10'` são lidos, mas `'0-0'`, `'11-0'` e `'00-'` não. diff --git a/docs/utilities.md b/docs/utilities.md index ee082690..f1a741dc 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -2,7 +2,7 @@ Here you will find all the utilities available for use. -> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function returns an empty value of its return type: every `format*` function, `capitalize`, and the string-returning `parse*` functions (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) return `""`; `parseCurrency` returns `0`; the object/tuple parsers — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — return `null`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. +> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `getBoletoInfo` returns `undefined` for an invalid boleto, the one function in the package that returns `undefined`; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function returns an empty value of its return type: every `format*` function, `capitalize`, and the string-returning `parse*` functions (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) return `""`; `parseCurrency` returns `0`; the object/tuple parsers — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — return `null`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. ## isValidCpf @@ -17,7 +17,7 @@ isValidCpf('111 444 777 35'); // true (whitespace mask) ## formatCpf -Format CPF. `options.obfuscate` (part of `FormatCpfOptions`) hides the first 3 digits and the 2 check digits (`***.456.789-**`), the gov.br / Receita Federal display convention, applied after `pad`. It is read for truthiness, the way `pad` is, so any truthy value obfuscates. +Format CPF. `options.pad` (part of `FormatCpfOptions`) left-pads the value with zeros up to the 11 slots of the pattern before masking (default `false`). `options.obfuscate` (same type) hides the first 3 digits and the 2 check digits (`***.456.789-**`), the gov.br / Receita Federal display convention, applied after `pad`. It is read for truthiness, the way `pad` is, so any truthy value obfuscates. ```javascript import { formatCpf } from '@brazilian-utils/brazilian-utils'; @@ -50,7 +50,7 @@ generateCpf('SP'); // the 9th digit is 8, the SP região fiscal code ## isValidCnpj -Check if CNPJ is valid. Supports both the numeric format (`version: 1`, default) and the alphanumeric format (`version: 2`), and accepts the usual mask characters and whitespace. Options are typed as `IsValidCnpjOptions`. +Check if CNPJ is valid. `options.version` (part of `IsValidCnpjOptions`) picks which format is accepted: `1` (default) the numeric-only format, `2` both the numeric and the alphanumeric one; any other value is read as `1`, the way `formatCnpj` and `parseCnpj` read it. The usual mask characters and whitespace are accepted in either version. ```javascript import { isValidCnpj } from '@brazilian-utils/brazilian-utils'; @@ -61,7 +61,7 @@ isValidCnpj('q0slfmbd7vx439', { version: 2 }); // true (lowercase alphanumeric) ## formatCnpj -Format CNPJ. `options.obfuscate` (part of `FormatCnpjOptions`) hides the first 2 digits and the 2 check digits (`**.345.678/0001-**`), the gov.br / Receita Federal display convention. It applies to both versions and comes after `pad`, and is read for truthiness, the way `pad` is, so any truthy value obfuscates. +Format CNPJ. `options.pad` (part of `FormatCnpjOptions`) left-pads the value with zeros up to the 14 slots of the pattern before masking (default `false`). `options.version` (same type) picks which CNPJ format to read: `1` (default) numeric only, `2` alphanumeric. `options.obfuscate` hides the first 2 digits and the 2 check digits (`**.345.678/0001-**`), the gov.br / Receita Federal display convention. It applies to both versions and comes after `pad`, and is read for truthiness, the way `pad` is, so any truthy value obfuscates. ```javascript import { formatCnpj } from '@brazilian-utils/brazilian-utils'; @@ -74,7 +74,7 @@ formatCnpj('12345678000195', { obfuscate: true }); // **.345.678/0001-** ## parseCnpj -Remove CNPJ formatting, return a normalized value, and cap the result to 14 characters. Options are typed as `ParseCnpjOptions`. +Remove CNPJ formatting, return a normalized value, and cap the result to 14 characters. `options.version` (part of `ParseCnpjOptions`) picks which CNPJ format to normalize: `1` (default) keeps digits only, `2` keeps letters and digits, so an alphanumeric CNPJ survives the round trip. ```javascript import { parseCnpj } from '@brazilian-utils/brazilian-utils'; @@ -123,7 +123,7 @@ isValidBoleto('846100000005246100291102005460339004695895061080'); // true (bole ## formatBoleto -Format a boleto number. The arrecadação (convênio/tributos) mask applies only to the 48 digit linha digitável starting with `8`; the 44 digit arrecadação barcode has no display grouping defined by FEBRABAN and keeps the "cobrança bancária" mask instead. +Format a boleto number. `options.pad` (part of `FormatBoletoOptions`) left-pads the value with zeros up to the number of slots in the pattern before masking (default `false`). The arrecadação (convênio/tributos) mask applies only to the 48 digit linha digitável starting with `8`; the 44 digit arrecadação barcode has no display grouping defined by FEBRABAN and keeps the "cobrança bancária" mask instead. ```javascript import { formatBoleto } from '@brazilian-utils/brazilian-utils'; @@ -157,7 +157,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ## getBoletoInfo -Extract information from a boleto (amount, expiration date, bank code). Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. The cycle search never goes below the first cycle, so a `referenceDate` older than the scheme itself still resolves a factor to the oldest date that factor can denote rather than to one before the 07/10/1997 base date. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. +Extract information from a boleto (amount, expiration date, bank code). Returns `undefined` when `value` is not a valid boleto — `isValidBoleto` is checked first — exactly as in 2.3.0, so the result has to be narrowed before it is read. Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. The cycle search never goes below the first cycle, so a `referenceDate` older than the scheme itself still resolves a factor to the oldest date that factor can denote rather than to one before the 07/10/1997 base date. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -272,7 +272,7 @@ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // nu Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e (65), CT-e (57, the Conhecimento de Transporte Eletrônico instituted by the cláusula primeira of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para Outros Serviços instituted by the cláusula primeira of the [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, the CT-e Guia de Transporte de Valores instituted by the cláusula primeira of the [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed differently. Accepts whitespace between digit groups (the common display mask) and the `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the document's XML. -The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` for the CT-e, `{1, 5, 7, 8}` for the CT-e OS, `{1, 2, 7, 8}` for the GTV-e, `{1, 2, 3}` for the MDF-e and `{1, 2}` for the BP-e, the NF3e and the NFCom. Code 8, the authorização pela SVC-SP, is assigned by the [CT-e MOC 4.00](https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais) only, never by the NF-e one; the domains of the [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), the [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) and the [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) come from their own manuals. For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, which forbids the twenty repeated and sequential `cNF` values it lists and a `cNF` equal to the document number. A document number of all zeros is turned down for every model, following the leiaute rather than a choice of this library: `tiposBasico_v4.00.xsd` of the [NF-e schema package](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) types `nNF` as `TNF`, whose pattern is `[1-9]{1}[0-9]{0,8}`, and the Anexo I of every other model repeats the same regex for its own number field. +The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` for the CT-e, `{1, 5, 7, 8}` for the CT-e OS, `{1, 2, 7, 8}` for the GTV-e, `{1, 2, 3}` for the MDF-e and `{1, 2}` for the BP-e, the NF3e and the NFCom. Code 8, the authorização pela SVC-SP, is assigned by the [CT-e MOC 4.00](https://dfe-portal.svrs.rs.gov.br/CTE/Documentos) only, never by the NF-e one; the domains of the [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), the [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) and the [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) come from their own manuals. For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, which forbids the twenty repeated and sequential `cNF` values it lists and a `cNF` equal to the document number. A document number of all zeros is turned down for every model, following the leiaute rather than a choice of this library: `tiposBasico_v4.00.xsd` of the [NF-e schema package](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) types `nNF` as `TNF`, whose pattern is `[1-9]{1}[0-9]{0,8}`, and the Anexo I of every other model repeats the same regex for its own number field. ```javascript import { isValidNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -327,12 +327,13 @@ isValidEmail('john.doe@hotmail.com'); // true ## isValidPhone -Check if phone number (mobile or landline) is valid. A Brazilian country code (`+55`, `0055` or a bare `55`) is accepted and removed before validation, under the rule documented in `parsePhone`. `options.accept` (typed as `PhoneType[]`, part of `IsValidPhoneOptions`) picks which kinds of number count as valid and defaults to `['mobile', 'landline']`; add `'service'` to also accept the non-geographic numbers recognized by `isValidServicePhone`, or pass `[]` to accept none. +Check if phone number (mobile or landline) is valid. A Brazilian country code (`+55`, `0055` or a bare `55`) is accepted and removed before validation, under the rule documented in `parsePhone`. `options.accept` (typed as `PhoneType[]`, part of `IsValidPhoneOptions`) picks which kinds of number count as valid and defaults to `['mobile', 'landline']`; add `'service'` to also accept the non-geographic numbers recognized by `isValidServicePhone`, or pass `[]` to accept none. `options.version` (typed as `PhoneVersion`, part of the same type) is forwarded to `isValidMobilePhone` and picks which mobile numbering rule is enforced: `1` (default) the legacy format, whose first number digit may be 6, 7, 8 or 9, and `2` the current one, which requires 9 and rejects the `700` prefix. It only affects mobile numbers; landline and service numbers are unaffected. ```javascript import { isValidPhone } from '@brazilian-utils/brazilian-utils'; isValidPhone('11900000000'); // true +isValidPhone('11712345678', { version: 2 }); // false (v2 requires 9 as the first mobile digit) isValidPhone('+55 11 98765-4321'); // true (country code accepted) isValidPhone('08001234567'); // false (service numbers rejected by default) isValidPhone('08001234567', { accept: ['service'] }); // true @@ -541,12 +542,12 @@ const address = await getAddressInfoByCep('01310100'); // { cep: '01310100', state: 'SP', city: 'São Paulo', neighborhood: 'Bela Vista', street: 'Avenida Paulista' } // Using specific providers -const address = await getAddressInfoByCep('01310-100', { +const addressFromProviders = await getAddressInfoByCep('01310-100', { providers: ['viacep', 'brasilapi'] }); // Using number input (will be padded automatically) -const address = await getAddressInfoByCep(1310100); +const addressFromNumber = await getAddressInfoByCep(1310100); ``` ## isValidProcessoJuridico @@ -832,7 +833,7 @@ capitalize(' josé maria '); // José Maria (every run of whitespace, tabs a ## formatCurrency -Formats an integer or float to a string in the BRL pattern. A `number` is formatted as-is (sign and decimals preserved). A `string` input is read by the same rule as `parseCurrency`, except that a value written without any separator stays in whole units: the last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator, every other `,` or `.` is a thousands separator, and a `-` written before the first digit is preserved. So `'1.234,56'` formats as `1.234,56`, `'-10.5'` as `-10,50` and `'1234'` as `1.234,00`. `precision` is clamped to `0..20` (the package limit, the bound Node 20 still enforces on `Intl.NumberFormat`), defaults to 2, and falls back to 2 when it is not a finite number. A value that is not a finite number (`NaN`, `Infinity`, `-Infinity`) formats as an empty string, and so does a value that cannot be coerced to a number (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. Options are typed as `FormatCurrencyOptions`. +Formats an integer or float to a string in the BRL pattern. A `number` is formatted as-is (sign and decimals preserved). A `string` input is read by the same rule as `parseCurrency`, except that a value written without any separator stays in whole units: the last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator, every other `,` or `.` is a thousands separator, and a `-` written before the first digit is preserved. So `'1.234,56'` formats as `1.234,56`, `'-10.5'` as `-10,50` and `'1234'` as `1.234,00`. `precision` is clamped to `0..20` (the package limit, the bound Node 20 still enforces on `Intl.NumberFormat`), defaults to 2, and falls back to 2 when it is not a finite number. A value that is not a finite number (`NaN`, `Infinity`, `-Infinity`) formats as an empty string, and so does a value that cannot be coerced to a number (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. `options.symbol` prefixes the result with the `R$` currency symbol (default `false`). Options are typed as `FormatCurrencyOptions`. ```javascript import { formatCurrency } from '@brazilian-utils/brazilian-utils'; @@ -1171,25 +1172,32 @@ parseCnh('026503064-61'); // '02650306461' ## getCepInfoByAddress -Fetch CEPs from an address using ViaCEP. Throws `GetCepInfoByAddressValidationError` when the UF, city or street is missing/invalid — including when the argument is not an object at all (omitted, `null`, a string) and when `federalUnit` is not a string, neither of which leaks a raw `TypeError` — `GetCepInfoByAddressNotFoundError` when no address matches the query, and `GetCepInfoByAddressError` when ViaCEP itself answers with an HTTP error status. A request that cannot be performed at all (a transport failure) rejects with the underlying `fetch` error instead. +Fetch CEPs from an address using ViaCEP. Throws `GetCepInfoByAddressValidationError` when the UF, city or street is missing/invalid — including when the argument is not an object at all (omitted, `null`, a string) and when `federalUnit` is not a string, neither of which leaks a raw `TypeError` — `GetCepInfoByAddressNotFoundError` when no address matches the query, and `GetCepInfoByAddressError` when ViaCEP itself answers with an HTTP error status. A request that cannot be performed at all (a transport failure) rejects with the underlying `fetch` error instead. Each item is typed as `CepAddressInfo` and carries the ViaCEP payload unchanged, under ViaCEP's own field names: `cep`, `logradouro`, `complemento`, `unidade`, `bairro`, `localidade`, `uf`, `estado`, `regiao`, `ibge`, `gia`, `ddd` and `siafi`. A broad street name matches many CEPs, so query as narrowly as the address allows. ```javascript import { getCepInfoByAddress } from '@brazilian-utils/brazilian-utils'; const ceps = await getCepInfoByAddress({ - federalUnit: 'SP', - city: 'Sao Paulo', - street: 'Avenida Paulista' + federalUnit: 'MG', + city: 'Ouro Preto', + street: 'Rua Direita' }); // [ // { -// cep: '01310-100', -// logradouro: 'Avenida Paulista', -// complemento: 'de 612 a 1510 - lado par', -// bairro: 'Bela Vista', -// localidade: 'São Paulo', -// uf: 'SP' +// cep: '35411-152', +// logradouro: 'Rua Direita', +// complemento: '', +// unidade: '', +// bairro: 'Riacho (Amarantina)', +// localidade: 'Ouro Preto', +// uf: 'MG', +// estado: 'Minas Gerais', +// regiao: 'Sudeste', +// ibge: '3146107', +// gia: '', +// ddd: '31', +// siafi: '4921' // } // ] ``` @@ -1501,7 +1509,7 @@ addBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) ## subBusinessDays -Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too. A negative `amount` walks forwards. +Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too, `options.stateCode` included. A negative `amount` walks forwards. ```javascript import { subBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1613,7 +1621,7 @@ isValidCns('abc123456789010000'); // false (not written as a CNS) ## formatCns -Format a CNS (Cartão Nacional de Saúde) number into the common display groups of 3-4-4-4 digits separated by spaces. Options are typed as `FormatCnsOptions`. +Format a CNS (Cartão Nacional de Saúde) number into the common display groups of 3-4-4-4 digits separated by spaces. `options.pad` (part of `FormatCnsOptions`) left-pads the value with zeros up to the 15 slots of the pattern before masking (default `false`). ```javascript import { formatCns } from '@brazilian-utils/brazilian-utils'; @@ -1625,7 +1633,7 @@ formatCns('89010001', { pad: true }); // '000 0000 8901 0001' ## isValidCertidao -Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with weights cycling from 2 to 10 and back through 0. Accepts the usual mask characters and whitespace between/around groups. The layout is the one [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023) currently publishes, with inciso II and §§ 1º to 5º in the redação of the Provimento CN nº 237/2026 and the rest of the article in that of the Provimento CN nº 182/2024; the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). +Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with weights cycling from 2 to 10 and back through 0. Accepts the usual mask characters and whitespace between/around groups. The layout is the one [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023) currently publishes, with inciso II and §§ 1º and 3º to 5º in the redação of the Provimento CN nº 237/2026 and the rest of the article, § 2º included, in that of the Provimento CN nº 182/2024; the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311) and got its digit structure from the also revoked [Provimento CNJ nº 3/2009, art. 7º](https://atos.cnj.jus.br/atos/detalhar/1310). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). The serviço digits are fixed at `55`, the code [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) assigns to the registro civil das pessoas naturais, so a matrícula carrying any other pair in the ninth and tenth positions is rejected however good its check digits are. The book-type digit always has to name one of the nine book types (the same `CertidaoType` returned by `parseCertidao`), so a matrícula whose digit is `0` is rejected however good its check digits are, the same way `parseCertidao` returns `null` for it. `options.accept` (part of `IsValidCertidaoOptions`) narrows that to the listed types; it defaults to every type, and a value that is not an array falls back to that default. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. @@ -1720,7 +1728,7 @@ formatCei('249', { pad: true }); // 00.000.00002/49 ## isValidCno -Check if a CNO (Cadastro Nacional de Obras) number is valid. The CNO replaced the CEI for construction works and kept its numbering, so a work registered under a legacy CEI keeps the same number and both registries validate identically: 12 digits printed as `00.000.00000/00` with a check digit calculated over the 11 base digits. The Receita Federal does not publish the check digit rule; it was confirmed against the [Cadastro Nacional de Obras (CNO) open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) of the Receita Federal: every one of the 38432 works registered in Minas Gerais passes this check. +Check if a CNO (Cadastro Nacional de Obras) number is valid. The CNO replaced the CEI for construction works and kept its numbering, so a work registered under a legacy CEI keeps the same number and both registries validate identically: 12 digits printed as `00.000.00000/00` with a check digit calculated over the 11 base digits. The Receita Federal does not publish the check digit rule; it was confirmed against the [Cadastro Nacional de Obras (CNO) open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) of the Receita Federal: every work in the Minas Gerais extract of that dataset passes this check. The catalogue page itself publishes only the dataset's description and download links, not that result. ```javascript import { isValidCno } from '@brazilian-utils/brazilian-utils'; @@ -1788,7 +1796,7 @@ isValidRegistroProfissional('SP-123456/T-3', { council: 'CRC' }); // false ("T" ## isValidVin -Check if a VIN (Vehicle Identification Number / chassi) is valid. Checks the length (17 characters), the excluded letters (`I`, `O`, `Q` are never valid; [ISO 3779:2009](https://www.iso.org/standard/52200.html) structure) and the check digit at the 9th position, with the check digit and transliteration computed per [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15). That check digit is a North-American requirement (49 CFR 565.15 / SAE J853): Resolução CONTRAN nº 24/1998 and ABNT NBR 6066 define the Brazilian VIN structure but do not mandate it, so many Brazilian-built VINs do not carry a matching check digit. This function is therefore a North-American-style structural check, not a universal validator of Brazilian VINs. Case-insensitive and trims surrounding whitespace. +Check if a VIN (Vehicle Identification Number / chassi) is valid. Checks the length (17 characters), the excluded letters (`I`, `O`, `Q` are never valid; [ISO 3779:2009](https://www.iso.org/standard/52200.html) structure) and the check digit at the 9th position, with the check digit and transliteration computed per [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15). That check digit is a North-American requirement (49 CFR 565.15 / SAE J853): [Resolução CONTRAN nº 968/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf) (which revoked Resolução CONTRAN nº 24/1998 from 1 January 2025) and ABNT NBR 6066 define the Brazilian VIN structure but do not mandate it, so many Brazilian-built VINs do not carry a matching check digit. This function is therefore a North-American-style structural check, not a universal validator of Brazilian VINs. Case-insensitive and trims surrounding whitespace. ```javascript import { isValidVin } from '@brazilian-utils/brazilian-utils'; @@ -1832,7 +1840,7 @@ The occupation titles come from the [official CBO 2002 occupation table publishe ## isValidCnae -Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the CNAE 2.3 table published by IBGE. Accepts the code with or without the `NNNN-N/NN` mask, or as a number. A string is only read as a code when it is written in one of those forms (the 7 digits, or the mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the [CNAE-Subclasses 2.3 table published by IBGE](https://concla.ibge.gov.br/busca-online-cnae.html), the current subclass revision of CNAE 2.0. Accepts the code with or without the `NNNN-N/NN` mask, or as a number. A string is only read as a code when it is written in one of those forms (the 7 digits, or the mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. ```javascript import { isValidCnae } from '@brazilian-utils/brazilian-utils'; @@ -1946,7 +1954,7 @@ Check if a CST (Código de Situação Tributária) code is valid for a given tax `options.tax` (part of `IsValidCstOptions`) is optional: omit it to accept a code that exists in any one of the four tables above. -The ICMS Tabela B is the one in force: the [consolidated Anexo I of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), whose current wording came from [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (effective 01.12.23) and which [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) amended by striking items 12, 13, 52, 72 and 74 (effects from 09.07.24) before they ever took effect: 39/23 had added them "sem efeitos", so those codes were never in force. `02`, `15`, `53` and `61` are its monofasia de combustíveis codes. +The ICMS Tabela B is the one in force: the [consolidated Anexo I of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), whose current wording came from [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (effective 01.12.23) and which [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) amended by striking items 12, 13, 52, 72 and 74 (effects from 09.07.24) before they ever took effect: 39/23 had deferred their effect to 1º de outubro de 2024, so the revocation reached them first and those codes were never in force. `02`, `15`, `53` and `61` are its monofasia de combustíveis codes. A string is only read as a code when it is written in one of the documented forms (the 2 digits of a Tabela B code, or the 3 digits of the ICMS form with an optional single separator after the origin digit, plus optional surrounding whitespace), and a number only when it is a non-negative safe integer. The origin digit is the only boundary a printed CST has, so `'0 10'` and `'1-10'` are read while `'0-0'`, `'11-0'` and `'00-'` are not. diff --git a/scripts/cnae.ts b/scripts/cnae.ts index 864ecd4e..98cbfa6a 100644 --- a/scripts/cnae.ts +++ b/scripts/cnae.ts @@ -46,13 +46,23 @@ const main = async (): Promise => { await writeFile( resolve(scriptsDir, "..", "./src/_internals/constants/cnae.ts"), `/** - * CNAE 2.3 (Classificação Nacional de Atividades Econômicas) subclasses, indexed by the - * raw 7 digit code, mapping to the official subclass description. + * CNAE-Subclasses 2.3 (Classificação Nacional de Atividades Econômicas) subclasses, indexed by + * the raw 7 digit code, mapping to the official subclass description. + * + * 2.3 is the current subclass revision of CNAE 2.0: CONCLA's own CNAE browser lists it as + * "CNAE-Subclasses 2.3" under "CNAE 2.0 (Res 02/2010)" and tells anyone opening an older table + * that the "Versões atuais da CNAE" are "CNAE 2.0 (Res 02/2010)" and "CNAE-Subclasses 2.3". The + * classification's landing page still describes the parent CNAE 2.0 itself ("Base Legal: + * Resolução Concla 01/2006", 1301 subclasses); the 1332 subclasses below are the ones the IBGE + * data service publishes for the 2.3 revision. * * Generated by \`node ./scripts/cnae.ts\`. Do not edit by hand. * * @see Official: https://servicodados.ibge.gov.br/api/v2/cnae/subclasses + * @see Official: https://concla.ibge.gov.br/busca-online-cnae.html + * CONCLA's CNAE search and structure browser, which publishes CNAE-Subclasses 2.3. * @see Official: https://concla.ibge.gov.br/classificacoes/por-tema/atividades-economicas/classificacao-nacional-de-atividades-economicas + * CNAE 2.0, the parent classification the 2.3 subclass revision belongs to. */ export const CNAE_SUBCLASSES: Record = ${JSON.stringify(data)}; diff --git a/src/_internals/calculate-cei-check-digit/calculate-cei-check-digit.ts b/src/_internals/calculate-cei-check-digit/calculate-cei-check-digit.ts index 24950f1f..74800c63 100644 --- a/src/_internals/calculate-cei-check-digit/calculate-cei-check-digit.ts +++ b/src/_internals/calculate-cei-check-digit/calculate-cei-check-digit.ts @@ -26,9 +26,11 @@ import { generateChecksum } from "../generate-checksum/generate-checksum"; * The registry's own page at the Receita Federal, which describes the cadastro but publishes * neither the mask nor the check digit rule. * @see Official: https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno - * Cadastro Nacional de Obras (CNO), dados abertos da Receita Federal: the 38432 works - * registered in Minas Gerais confirm the rule, and their check digits of 0 are what shows - * that a computed 10 maps back to 0, which neither reference implementation does. + * Cadastro Nacional de Obras (CNO), dados abertos da Receita Federal: the catalogue entry for the + * dataset this rule was cross-checked against. The Minas Gerais extract of the downloaded dataset + * confirms the rule, and the works whose check digit is 0 are what shows that a computed 10 maps + * back to 0, which neither reference implementation does; the catalogue page itself publishes only + * the dataset's description and download links (and currently flags it "Desatualizado"). * @see Based on: https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php * PHP reference implementation of the CEI check digit. * @see Based on: https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs diff --git a/src/_internals/constants/area-codes.ts b/src/_internals/constants/area-codes.ts index e2e4b34b..975025a8 100644 --- a/src/_internals/constants/area-codes.ts +++ b/src/_internals/constants/area-codes.ts @@ -116,7 +116,8 @@ export const AREA_CODE_STATES: Record = { * state. * * @see Official: https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais - * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2001/383-resolucao-263 + * @see Based on: https://informacoes.anatel.gov.br/legislacao/resolucoes/2001/383-resolucao-263 + * Anexo of Resolução nº 263/2001 (revoked; still the table Anatel's Códigos Nacionais page links to). */ export const AREA_CODE_SECONDARY_STATES: Record = { 42: ["SC"], diff --git a/src/_internals/constants/cei.ts b/src/_internals/constants/cei.ts index d89d6253..41aac0eb 100644 --- a/src/_internals/constants/cei.ts +++ b/src/_internals/constants/cei.ts @@ -10,9 +10,11 @@ * The registry's own page at the Receita Federal, which describes the cadastro but publishes * neither the mask nor the check digit rule. * @see Official: https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno - * Cadastro Nacional de Obras (CNO), dados abertos da Receita Federal: every one of the 38432 - * works registered in Minas Gerais passes this check, which is what ties the CNO to the CEI - * rule and where the test vectors come from. + * Cadastro Nacional de Obras (CNO), dados abertos da Receita Federal: the catalogue entry for the + * dataset this rule was cross-checked against and where the test vectors come from. The check was + * run over the Minas Gerais extract of the downloaded dataset, which every registered work passed; + * the catalogue page itself publishes only the dataset's description and download links (and + * currently flags it "Desatualizado"), not that result. * @see Based on: https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php * PHP reference implementation of the CEI check digit. * @see Based on: https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs diff --git a/src/_internals/constants/certidao.ts b/src/_internals/constants/certidao.ts index 9c70a70e..544d6406 100644 --- a/src/_internals/constants/certidao.ts +++ b/src/_internals/constants/certidao.ts @@ -6,12 +6,20 @@ * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 * Código Nacional de Normas da Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento * CNJ nº 149/2023), art. 473 as currently published: the in-force layout of the 32 digit - * matrícula. Inciso II and §§ 1º to 5º carry the redação of the Provimento CN nº 237, de - * 13/07/2026; the rest of the article, and the digit layout this library depends on, come from the - * Provimento CN nº 182, de 17/09/2024. + * matrícula. Inciso II and §§ 1º and 3º to 5º carry the redação of the Provimento CN nº 237, de + * 13/07/2026; the rest of the article, § 2º included, and the digit layout this library depends + * on, come from the Provimento CN nº 182, de 17/09/2024. * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 - * Provimento CNJ nº 2, de 27/04/2009, which instituted the modelos únicos de certidão and the - * matrícula (revoked; historical). + * Provimento CNJ nº 2, de 27/04/2009, art. 1º and 2º, which instituted the modelos únicos de + * certidão and ordered that "as certidões passarão a consignar matrícula que identifica o código + * nacional da serventia, o código do acervo, o tipo do serviço prestado, o tipo do livro, o número + * do livro, o número da folha, o número do termo e o digito verificador" (revoked; historical). + * @see Official: https://atos.cnj.jus.br/atos/detalhar/1310 + * Provimento CNJ nº 3, de 17/11/2009, art. 7º, which is where that matrícula first got its digit + * structure: "a matrícula, de inserção obrigatória nas certidões (primeira e demais vias) emitidas + * pelos Cartórios de Registro Civil das Pessoas Naturais a partir de 1º de janeiro de 2010, é + * formada pelos seguintes elementos", incisos I to IX fixing the same 6 + 2 + 2 + 4 + 1 + 5 + 3 + + * 7 + 2 positions art. 473 carries today (revoked; historical). * @see Based on: http://ghiorzi.org/DVnew.htm * Worked example of the two check digits (sums 288 and 309). * @see Based on: https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts diff --git a/src/_internals/constants/cnae.ts b/src/_internals/constants/cnae.ts index 3ae058dd..377b36ac 100644 --- a/src/_internals/constants/cnae.ts +++ b/src/_internals/constants/cnae.ts @@ -1,11 +1,21 @@ /** - * CNAE 2.3 (Classificação Nacional de Atividades Econômicas) subclasses, indexed by the - * raw 7 digit code, mapping to the official subclass description. + * CNAE-Subclasses 2.3 (Classificação Nacional de Atividades Econômicas) subclasses, indexed by + * the raw 7 digit code, mapping to the official subclass description. + * + * 2.3 is the current subclass revision of CNAE 2.0: CONCLA's own CNAE browser lists it as + * "CNAE-Subclasses 2.3" under "CNAE 2.0 (Res 02/2010)" and tells anyone opening an older table + * that the "Versões atuais da CNAE" are "CNAE 2.0 (Res 02/2010)" and "CNAE-Subclasses 2.3". The + * classification's landing page still describes the parent CNAE 2.0 itself ("Base Legal: + * Resolução Concla 01/2006", 1301 subclasses); the 1332 subclasses below are the ones the IBGE + * data service publishes for the 2.3 revision. * * Generated by `node ./scripts/cnae.ts`. Do not edit by hand. * * @see Official: https://servicodados.ibge.gov.br/api/v2/cnae/subclasses + * @see Official: https://concla.ibge.gov.br/busca-online-cnae.html + * CONCLA's CNAE search and structure browser, which publishes CNAE-Subclasses 2.3. * @see Official: https://concla.ibge.gov.br/classificacoes/por-tema/atividades-economicas/classificacao-nacional-de-atividades-economicas + * CNAE 2.0, the parent classification the 2.3 subclass revision belongs to. */ export const CNAE_SUBCLASSES: Record = { "1011201": "FRIGORÍFICO - ABATE DE BOVINOS", diff --git a/src/_internals/constants/number-words.ts b/src/_internals/constants/number-words.ts index 4d6b5b15..0ac7e6ee 100644 --- a/src/_internals/constants/number-words.ts +++ b/src/_internals/constants/number-words.ts @@ -2,8 +2,12 @@ * Portuguese (pt-BR) number-to-words tables, shared by `numberToWords` and by every public * "por extenso" formatter (`convertNumberToWords`, `convertCurrencyToWords`, `convertDateToWords`). * + * @see Based on: https://github.com/savoirfairelinux/num2words/blob/master/num2words/lang_PT.py + * num2words' Portuguese table, which spells 14 "catorze" (not "quatorze"), the spelling used + * here. Its `lang_PT_BR` subclass keeps that table, so `num2words(14, lang="pt_BR")` is + * "catorze". * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/currency.py - * "catorze" (not "quatorze") is used for 14, matching num2words pt_BR and brutils. + * brutils' currency helper, which delegates to num2words and therefore inherits that spelling. */ export const ZERO_WORD = "zero"; diff --git a/src/_internals/constants/service-phone.ts b/src/_internals/constants/service-phone.ts index 72ed5903..f41afcf0 100644 --- a/src/_internals/constants/service-phone.ts +++ b/src/_internals/constants/service-phone.ts @@ -27,10 +27,11 @@ * `2`-`6` as the first digit of a fixed-line number) whose 4-digit prefix a carrier licenses * in many DDDs at once and points at a single customer, marketed as "Número Único". Anatel * withdrew the 4-digit special service codes instead of allocating them: Resolução nº 86/1998 - * art. 43 I, in the wording of Resolução nº 229/2000, ordered the prestadoras de STFC to - * release every 4-character code in use, and Ato nº 43.151/2004 art. 2º II repeated the order - * with a 180-day deadline. So the roots below are the conventional ones the market settled on, - * not an official allocation. + * art. 43 I, in its last wording (Resolução nº 241, de 30 de novembro de 2000, which superseded + * the Resolução nº 229/2000 one), ordered the prestadoras de STFC to release "até 30 de julho de + * 2001, os códigos de serviços especiais com 4 caracteres que estejam em uso", and Ato nº + * 43.151/2004 art. 2º II repeated the order with a 180-day deadline. So the roots below are the + * conventional ones the market settled on, not an official allocation. * * Display formatting is convention too: no Anatel document specifies one. `0800 123 4567` (4-3-4) * is the grouping used on gov.br, and `4004-1234` the one carriers print. @@ -39,10 +40,13 @@ * Resolução Anatel nº 749/2022, the Regulamento de Numeração in force. * @see Official: https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151 * Ato Anatel nº 43.151/2004, whose Anexo is the consolidated SUP designation table. - * @see Official: https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2024/1953-ato-12712 - * Ato Anatel nº 12.712/2024, the CNG designation table (items 10.6 and 12.1). + * @see Official: https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2140-ato-12712 + * Ato Anatel nº 12.712, de 04/09/2024, art. 1º: the Procedimento para a Atribuição e Designação + * de Recursos de Numeração (Anexo I), in force since 03/12/2024, whose items 10.6 and 12.1 carry + * the `500` donation-amount rule and the `900` reserva técnica. * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86 - * Resolução Anatel nº 86/1998 (revoked), art. 43 I: the release of the 4-character codes. + * Resolução Anatel nº 86/1998 (revoked), art. 43 I: the release of the 4-character codes, in the + * redação dada pela Resolução nº 241, de 30 de novembro de 2000, the last one the page carries. */ export const SERVICE_PHONE_NON_GEOGRAPHIC_PREFIXES = [ diff --git a/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.ts b/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.ts index f5d74564..b2e1c24d 100644 --- a/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.ts +++ b/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.ts @@ -37,9 +37,11 @@ import { sanitizeToDigits } from "../sanitize-to-digits/sanitize-to-digits"; * The registry's own page at the Receita Federal, which describes the cadastro but publishes * neither the mask nor the check digit rule. * @see Official: https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno - * Cadastro Nacional de Obras (CNO), dados abertos da Receita Federal: every one of the 38432 - * works registered in Minas Gerais passes this check, which is what ties the CNO to the CEI - * rule and where the test vectors come from. + * Cadastro Nacional de Obras (CNO), dados abertos da Receita Federal: the catalogue entry for the + * dataset this rule was cross-checked against and where the test vectors come from. The check was + * run over the Minas Gerais extract of the downloaded dataset, which every registered work passed; + * the catalogue page itself publishes only the dataset's description and download links (and + * currently flags it "Desatualizado"), not that result. * @see Based on: https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php * PHP reference implementation of the CEI check digit. * @see Based on: https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs diff --git a/src/capitalize/capitalize.ts b/src/capitalize/capitalize.ts index ec9ed319..f0a7ef9e 100644 --- a/src/capitalize/capitalize.ts +++ b/src/capitalize/capitalize.ts @@ -73,7 +73,11 @@ const toWordSet = ( * Redação da Presidência da República keeps in lower case inside a proper name, and the default * `upperCaseWords` list is sourced in `constants.ts` from the laws that create each designation. * - * @see Based on: https://www4.planalto.gov.br/centrodeestudos/assuntos/manual-de-redacao-da-presidencia-da-republica + * @see Official: https://www4.planalto.gov.br/centrodeestudos/assuntos/manual-de-redacao-da-presidencia-da-republica/manual-de-redacao.pdf + * Manual de Redação da Presidência da República, 3ª edição (Portaria nº 1.369/2018), item 5.1.8 + * b) and item 10.2 a). + * @see Official: https://www4.planalto.gov.br/centrodeestudos/assuntos/manual-de-redacao-da-presidencia-da-republica + * The Presidência page that publishes it. * * @example * ```typescript diff --git a/src/capitalize/constants.ts b/src/capitalize/constants.ts index d1d9fff4..524a7533 100644 --- a/src/capitalize/constants.ts +++ b/src/capitalize/constants.ts @@ -3,11 +3,20 @@ import { type StateCode } from "../_internals/constants/states"; /** * Prepositions, articles and conjunctions that stay in lower case inside a proper name, the * default `lowerCaseWords` of `capitalize`. The Manual de Redação da Presidência da República - * writes personal and institutional names with every word capitalized except the connective - * words ("Ministério da Justiça", "José da Silva"), and the same convention is used by the IBGE - * for the names of municipalities ("Mogi das Cruzes", "Santa Bárbara d'Oeste"). + * states the convention twice: a cargo is "redigido apenas com as iniciais maiúsculas. As + * preposições que liguem as palavras do cargo devem ser grafadas em minúsculas" (item 5.1.8 b), + * and a title is written "com inicial maiúscula em todas as palavras, exceto nas de ligação" + * (item 10.2 a). The same convention is used by the IBGE for the names of municipalities + * ("Mogi das Cruzes", "Santa Bárbara d'Oeste"). Applying it to personal and institutional names + * ("Ministério da Justiça", "José da Silva") is this library's extension of that rule; the + * Manual does not spell those two cases out. * + * @see Official: https://www4.planalto.gov.br/centrodeestudos/assuntos/manual-de-redacao-da-presidencia-da-republica/manual-de-redacao.pdf + * Manual de Redação da Presidência da República, 3ª edição (Portaria nº 1.369, de 27/12/2018), + * items 5.1.8 b) and 10.2 a). The landing page below only recounts the editions and links to + * this PDF; the rule itself is in the PDF. * @see Official: https://www4.planalto.gov.br/centrodeestudos/assuntos/manual-de-redacao-da-presidencia-da-republica + * The Presidência page that publishes it ("Acesse aqui a íntegra da última edição publicada"). * @see Official: https://servicodados.ibge.gov.br/api/docs/localidades */ export const PREPOSITIONS = [ @@ -46,9 +55,14 @@ export const PREPOSITIONS = [ * Lei Complementar nº 123/2006, art. 72, revoked by the Lei Complementar nº 155/2016, added * "Microempresa ou Empresa de Pequeno Porte, ou suas respectivas abreviações, ME ou EPP" to the * name; art. 18-A defines the Microempreendedor Individual (MEI). - * @see Based on: https://www.gov.br/empresas-e-negocios/pt-br/drei/legislacao/instrucoes-normativas - * Instruções normativas of the DREI, which the Juntas Comerciais follow to register a nome - * empresarial and the source of the S/S spelling of the sociedade simples. + * @see Official: https://www.gov.br/empresas-e-negocios/pt-br/drei/legislacao/instrucoes-normativas/arquivos-instrucoes-normativas-em-vigor/anexo-iv-limitada_link.pdf + * IN DREI nº 81/2020, Anexo IV (Manual de Registro de Sociedade Limitada), the rules the Juntas + * Comerciais follow for the nome empresarial and for the "conversão de sociedade simples ou + * associação do cartório de registro de pessoas jurídicas para a Junta Comercial". The S/S + * abbreviation itself is registry practice: no DREI norm spells it out, and it is kept in this + * list only because registered names carry it. + * @see Official: https://www.gov.br/empresas-e-negocios/pt-br/drei/legislacao/instrucoes-normativas + * The DREI index of instruções normativas in force, where that Anexo is published. */ const BUSINESS_ABBREVIATIONS = [ "CEP", diff --git a/src/format-certidao/format-certidao.ts b/src/format-certidao/format-certidao.ts index 873fa517..d7823ad5 100644 --- a/src/format-certidao/format-certidao.ts +++ b/src/format-certidao/format-certidao.ts @@ -35,12 +35,20 @@ export type FormatCertidaoOptions = { * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 * Código Nacional de Normas da Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento * CNJ nº 149/2023), art. 473 as currently published: the in-force layout of the 32 digit - * matrícula. Inciso II and §§ 1º to 5º carry the redação of the Provimento CN nº 237, de - * 13/07/2026; the rest of the article, and the digit layout this library depends on, come from the - * Provimento CN nº 182, de 17/09/2024. + * matrícula. Inciso II and §§ 1º and 3º to 5º carry the redação of the Provimento CN nº 237, de + * 13/07/2026; the rest of the article, § 2º included, and the digit layout this library depends + * on, come from the Provimento CN nº 182, de 17/09/2024. * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 - * Provimento CNJ nº 2, de 27/04/2009, which instituted the modelos únicos de certidão and the - * matrícula (revoked; historical). + * Provimento CNJ nº 2, de 27/04/2009, art. 1º and 2º, which instituted the modelos únicos de + * certidão and ordered that "as certidões passarão a consignar matrícula que identifica o código + * nacional da serventia, o código do acervo, o tipo do serviço prestado, o tipo do livro, o número + * do livro, o número da folha, o número do termo e o digito verificador" (revoked; historical). + * @see Official: https://atos.cnj.jus.br/atos/detalhar/1310 + * Provimento CNJ nº 3, de 17/11/2009, art. 7º, which is where that matrícula first got its digit + * structure: "a matrícula, de inserção obrigatória nas certidões (primeira e demais vias) emitidas + * pelos Cartórios de Registro Civil das Pessoas Naturais a partir de 1º de janeiro de 2010, é + * formada pelos seguintes elementos", incisos I to IX fixing the same 6 + 2 + 2 + 4 + 1 + 5 + 3 + + * 7 + 2 positions art. 473 carries today (revoked; historical). * @see Based on: http://ghiorzi.org/DVnew.htm * Worked example of the two check digits (sums 288 and 309). * @see Based on: https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts diff --git a/src/generate-processo-juridico/generate-processo-juridico.ts b/src/generate-processo-juridico/generate-processo-juridico.ts index 45050b77..b79d0646 100644 --- a/src/generate-processo-juridico/generate-processo-juridico.ts +++ b/src/generate-processo-juridico/generate-processo-juridico.ts @@ -33,8 +33,8 @@ const calculateCheckDigits = (base: string): string => { * * @example * ```typescript - * generateProcessoJuridico(); // "00020802520265150049" - * generateProcessoJuridico({ year: 2030, court: 5 }); // "12345672820305120049" + * generateProcessoJuridico(); // "00020803420265150049" + * generateProcessoJuridico({ year: 2030, court: 5 }); // "12345679820305120049" * generateProcessoJuridico({ year: 10000 }); // null * ``` * diff --git a/src/get-address-info-by-cep/get-address-info-by-cep.ts b/src/get-address-info-by-cep/get-address-info-by-cep.ts index f3b1f6b3..39489e34 100644 --- a/src/get-address-info-by-cep/get-address-info-by-cep.ts +++ b/src/get-address-info-by-cep/get-address-info-by-cep.ts @@ -53,7 +53,11 @@ export type CepProvider = "viacep" | "widenet" | "brasilapi"; /** Options of `getAddressInfoByCep`. */ export type GetAddressInfoByCepOptions = { - /** Which CEP services to race, in the order given (default: all of them). */ + /** + * Which CEP services to race, in the order given (default: `["viacep", "brasilapi"]`; the + * deprecated `"widenet"` provider is excluded from the default list, but can still be + * requested explicitly). + */ providers?: CepProvider[]; }; diff --git a/src/get-cnae/get-cnae.ts b/src/get-cnae/get-cnae.ts index ac822352..045b7752 100644 --- a/src/get-cnae/get-cnae.ts +++ b/src/get-cnae/get-cnae.ts @@ -17,7 +17,7 @@ export type Cnae = { /** * Looks a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up in the - * official CNAE 2.3 table. + * official CNAE-Subclasses 2.3 table, the current subclass revision of CNAE 2.0. * * A string is only read as a code when it is written in one of the documented forms: the 7 * digits, or the `NNNN-N/NN` mask, with a single separator (space, `.`, `-` or `/`) between the groups and optional @@ -40,6 +40,8 @@ export type Cnae = { * ``` * * @see Official: https://servicodados.ibge.gov.br/api/v2/cnae/subclasses + * @see Official: https://concla.ibge.gov.br/busca-online-cnae.html + * CONCLA's CNAE search and structure browser, which publishes CNAE-Subclasses 2.3. */ export const getCnae = (value: string | number): Cnae | null => { if (!isLookupCode(value)) return null; diff --git a/src/get-holidays/constants.ts b/src/get-holidays/constants.ts index b3eb0e6b..b2e26c6f 100644 --- a/src/get-holidays/constants.ts +++ b/src/get-holidays/constants.ts @@ -165,19 +165,31 @@ export const SC_NEXT_SUNDAY_TRANSFER_SINCE_YEAR = 2005; * @see Official: https://sapl.al.pi.leg.br/norma/5849 * Lei PI nº 176/1937, Dia do Piauí (19/10) * @see Official: http://alerjln1.alerj.rj.gov.br/CONTLEI.NSF/c8aa0900025feef6032564ec0060dfff/1baf90ca125ff96f8325740a00776600 - * Lei RJ nº 5.198/2008, São Jorge (23/04), upheld by STF ADI 4092 (Plenário, sessão virtual de 18 - * a 25/08/2023, trânsito em julgado 28/10/2023): "O Tribunal, por maioria, declarou a - * constitucionalidade da Lei do Estado do Rio de Janeiro n. 5.198, de 5 de março de 2008". + * Lei RJ nº 5.198/2008, São Jorge (23/04): the ALERJ text of the law. Its Ficha Técnica records + * no ação de inconstitucionalidade; the STF case is cited separately below. + * @see Official: https://portal.stf.jus.br/processos/detalhe.asp?incidente=2624787 + * STF ADI 4092, which upheld that law. Decisão de julgamento of 28/08/2023, Tribunal Pleno, + * sessão virtual: "O Tribunal, por maioria, declarou a constitucionalidade da Lei do Estado do Rio + * de Janeiro n. 5.198, de 5 de março de 2008, e, por conseguinte, julgou improcedente a presente + * ação direta … Plenário, Sessão Virtual de 18.8.2023 a 25.8.2023"; trânsito em julgado 28/10/2023. * @see Official: http://alerjln1.alerj.rj.gov.br/CONTLEI.NSF/69d90307244602bb032567e800668618/80a541c3a5a9d63183256c7d0057bf25 * Lei RJ nº 4.007/2002, Dia da Consciência Negra (state holiday until it became national in 2024). - * STF ADI 4131, cited here before as pending against it, in fact challenged Lei RJ nº 5.243/2008 - * and was não conhecida on 21/09/2018 (trânsito em julgado 25/10/2018). + * Its Ficha Técnica records no ação de inconstitucionalidade either. + * @see Official: https://portal.stf.jus.br/processos/detalhe.asp?incidente=2636281 + * STF ADI 4131, cited here before as pending against Lei RJ nº 4.007/2002, in fact sought "a + * declaração de inconstitucionalidade da Lei n. 5.243, do Estado do Rio de Janeiro, de 14 de maio + * de 2008" and was não conhecida on 21/09/2018 (trânsito em julgado 25/10/2018). * @see Official: http://www.al.rn.leg.br/storage/legislacao//arq5064574f632ec.pdf * Lei RN nº 8.913/2006, Mártires de Cunhaú e Uruaçu (03/10) + * @see Official: https://ww2.al.rs.gov.br/dal/LinkClick.aspx?fileticket=WQdIfqNoXO4%3d&tabid=3683&mid=5359 + * Constituição Estadual do RS compilada (the "Veja em HTML" document of the Assembleia's + * Constituição Estadual page, linked below), art. 6º § 1º, Revolução Farroupilha (20/09): "O dia + * 20 de setembro é a data magna, sendo considerado feriado no Estado. (Redação dada pela Emenda + * Constitucional n.º 11, de 03/10/95) … (Renumerado pela Emenda Constitucional n.º 83, de + * 28/09/23)". * @see Official: https://ww2.al.rs.gov.br/dal/Legisla%C3%A7%C3%A3o/Constitui%C3%A7%C3%A3oEstadual/tabid/3683/Default.aspx - * Constituição Estadual do RS, art. 6º § 1º (EC nº 11/1995, renumbered by EC nº 83/2023), - * Revolução Farroupilha (20/09): "O dia 20 de setembro é a data magna, sendo considerado feriado - * no Estado". + * The Assembleia Legislativa do RS page that publishes that compiled text; it is a link hub and + * carries no article text of its own. * @see Official: https://sapl.al.ro.leg.br/norma/4958 * Lei RO nº 2.291, de 22/04/2010, Criação do Estado de Rondônia (04/01): "DECLARA O DIA 4 DE * JANEIRO DATA MAGNA E FERIADO CIVIL ESTADUAL". Lei RO nº 3.170/2013, cited here before, is a diff --git a/src/get-holidays/get-holidays.test.ts b/src/get-holidays/get-holidays.test.ts index 813a69b5..b718bc54 100644 --- a/src/get-holidays/get-holidays.test.ts +++ b/src/get-holidays/get-holidays.test.ts @@ -458,7 +458,7 @@ describe("getHolidays", () => { }); }); - test("should list DF's Fundação de Brasília (Lei distrital nº 10.633/1989) next to the national Tiradentes, which falls on the same 21 April under a different name", () => { + test("should list DF's Fundação de Brasília (Lei distrital nº 72/1989, art. 1º, I) next to the national Tiradentes, which falls on the same 21 April under a different name", () => { const dfHolidays = getHolidays({ year: 2024, stateCode: "DF" }); expect(dfHolidays).toContainEqual({ diff --git a/src/get-holidays/get-holidays.ts b/src/get-holidays/get-holidays.ts index 65144f81..bc8313e8 100644 --- a/src/get-holidays/get-holidays.ts +++ b/src/get-holidays/get-holidays.ts @@ -163,6 +163,9 @@ const computeHolidays = (year: number, stateCode: StateCode | undefined): Holida * const spHolidays = getHolidays({ year: 2024, stateCode: 'SP' }); * ``` * + * The national holiday laws are cited below; the state holiday laws are cited individually, one + * `@see` per holiday, in `src/get-holidays/constants.ts`. + * * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l0662.htm * Lei 662/1949, the base national holidays law (Ano novo, Dia do trabalhador, Independência do * Brasil, Proclamação da República, Natal). @@ -198,8 +201,6 @@ const computeHolidays = (year: number, stateCode: StateCode | undefined): Holida * with the Meeus/Jones/Butcher algorithm. It is a convenience entry, listed because callers * computing a liturgical calendar expect it, not because it is a holiday anyone observes as a day * off. - * @see Official: state holiday laws are cited individually, one `@see` per holiday, in - * `src/get-holidays/constants.ts`. */ export function getHolidays(year: number): Holiday[]; /** diff --git a/src/is-valid-bank-account/is-valid-bank-account.ts b/src/is-valid-bank-account/is-valid-bank-account.ts index bbfd2c8b..5d50e66c 100644 --- a/src/is-valid-bank-account/is-valid-bank-account.ts +++ b/src/is-valid-bank-account/is-valid-bank-account.ts @@ -23,7 +23,11 @@ export type IsValidBankAccountOptions = { agency: string; /** Account number, digits only, without the check digit. */ account: string; - /** The account check digit, one character. */ + /** + * The account check digit: one or two characters, or "X" for Banco do Brasil and "P" for + * Bradesco. Banks with a published rule take a single character; the generic fallback also + * accepts two, chaining mod10 and mod11 over the account. + */ digit: string; }; diff --git a/src/is-valid-cei/is-valid-cei.ts b/src/is-valid-cei/is-valid-cei.ts index 9d2130da..2f88fe50 100644 --- a/src/is-valid-cei/is-valid-cei.ts +++ b/src/is-valid-cei/is-valid-cei.ts @@ -35,9 +35,11 @@ import { isValidCeiCnoNumber } from "../_internals/is-valid-cei-cno-number/is-va * The registry's own page at the Receita Federal, which describes the cadastro but publishes * neither the mask nor the check digit rule. * @see Official: https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno - * Cadastro Nacional de Obras (CNO), dados abertos da Receita Federal: every one of the 38432 - * works registered in Minas Gerais passes this check, which is what ties the CNO to the CEI - * rule and where the test vectors come from. + * Cadastro Nacional de Obras (CNO), dados abertos da Receita Federal: the catalogue entry for the + * dataset this rule was cross-checked against and where the test vectors come from. The check was + * run over the Minas Gerais extract of the downloaded dataset, which every registered work passed; + * the catalogue page itself publishes only the dataset's description and download links (and + * currently flags it "Desatualizado"), not that result. * @see Based on: https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php * PHP reference implementation of the CEI check digit. * @see Based on: https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs diff --git a/src/is-valid-certidao/is-valid-certidao.ts b/src/is-valid-certidao/is-valid-certidao.ts index cfc4898c..47db7124 100644 --- a/src/is-valid-certidao/is-valid-certidao.ts +++ b/src/is-valid-certidao/is-valid-certidao.ts @@ -70,12 +70,20 @@ const getCheckDigit = (value: string): number => { * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 * Código Nacional de Normas da Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento * CNJ nº 149/2023), art. 473 as currently published: the in-force layout of the 32 digit - * matrícula. Inciso II and §§ 1º to 5º carry the redação of the Provimento CN nº 237, de - * 13/07/2026; the rest of the article, and the digit layout this library depends on, come from the - * Provimento CN nº 182, de 17/09/2024. + * matrícula. Inciso II and §§ 1º and 3º to 5º carry the redação of the Provimento CN nº 237, de + * 13/07/2026; the rest of the article, § 2º included, and the digit layout this library depends + * on, come from the Provimento CN nº 182, de 17/09/2024. * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 - * Provimento CNJ nº 2, de 27/04/2009, which instituted the modelos únicos de certidão and the - * matrícula (revoked; historical). + * Provimento CNJ nº 2, de 27/04/2009, art. 1º and 2º, which instituted the modelos únicos de + * certidão and ordered that "as certidões passarão a consignar matrícula que identifica o código + * nacional da serventia, o código do acervo, o tipo do serviço prestado, o tipo do livro, o número + * do livro, o número da folha, o número do termo e o digito verificador" (revoked; historical). + * @see Official: https://atos.cnj.jus.br/atos/detalhar/1310 + * Provimento CNJ nº 3, de 17/11/2009, art. 7º, which is where that matrícula first got its digit + * structure: "a matrícula, de inserção obrigatória nas certidões (primeira e demais vias) emitidas + * pelos Cartórios de Registro Civil das Pessoas Naturais a partir de 1º de janeiro de 2010, é + * formada pelos seguintes elementos", incisos I to IX fixing the same 6 + 2 + 2 + 4 + 1 + 5 + 3 + + * 7 + 2 positions art. 473 carries today (revoked; historical). * @see Based on: http://ghiorzi.org/DVnew.htm * Worked example of the two check digits (sums 288 and 309). * @see Based on: https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts diff --git a/src/is-valid-cnae/is-valid-cnae.ts b/src/is-valid-cnae/is-valid-cnae.ts index 2257c1f6..614d6060 100644 --- a/src/is-valid-cnae/is-valid-cnae.ts +++ b/src/is-valid-cnae/is-valid-cnae.ts @@ -2,7 +2,7 @@ import { getCnae } from "../get-cnae/get-cnae"; /** * Validates if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code - * exists in the official CNAE 2.3 table. + * exists in the official CNAE-Subclasses 2.3 table, the current subclass revision of CNAE 2.0. * * A string is only read as a code when it is written in one of the documented forms: the 7 * digits, or the `NNNN-N/NN` mask, with a single separator (space, `.`, `-` or `/`) between the groups and optional @@ -25,5 +25,7 @@ import { getCnae } from "../get-cnae/get-cnae"; * ``` * * @see Official: https://servicodados.ibge.gov.br/api/v2/cnae/subclasses + * @see Official: https://concla.ibge.gov.br/busca-online-cnae.html + * CONCLA's CNAE search and structure browser, which publishes CNAE-Subclasses 2.3. */ export const isValidCnae = (value: string | number): boolean => getCnae(value) !== null; diff --git a/src/is-valid-cno/is-valid-cno.ts b/src/is-valid-cno/is-valid-cno.ts index ea760e12..9eb6cc86 100644 --- a/src/is-valid-cno/is-valid-cno.ts +++ b/src/is-valid-cno/is-valid-cno.ts @@ -34,9 +34,11 @@ import { isValidCeiCnoNumber } from "../_internals/is-valid-cei-cno-number/is-va * The registry's own page at the Receita Federal, which describes the cadastro but publishes * neither the mask nor the check digit rule. * @see Official: https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno - * Cadastro Nacional de Obras (CNO), dados abertos da Receita Federal: every one of the 38432 - * works registered in Minas Gerais passes this check, which is what ties the CNO to the CEI - * rule and where the test vectors come from. + * Cadastro Nacional de Obras (CNO), dados abertos da Receita Federal: the catalogue entry for the + * dataset this rule was cross-checked against and where the test vectors come from. The check was + * run over the Minas Gerais extract of the downloaded dataset, which every registered work passed; + * the catalogue page itself publishes only the dataset's description and download links (and + * currently flags it "Desatualizado"), not that result. * @see Based on: https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php * PHP reference implementation of the CEI check digit. */ diff --git a/src/is-valid-cpf/is-valid-cpf.ts b/src/is-valid-cpf/is-valid-cpf.ts index 98d5ac10..6c33bfb9 100644 --- a/src/is-valid-cpf/is-valid-cpf.ts +++ b/src/is-valid-cpf/is-valid-cpf.ts @@ -40,8 +40,8 @@ const isValidChecksum = (cpf: string): boolean => { * * The check digit rule (`REGRA_VALIDA_CPF`) is specified, with the worked example * `280012389-38`, in the Receita Federal's Manual de Preenchimento da e-Financeira, Anexo II — - * Leiautes Gerais, approved by the Ato Declaratório Executivo Cofis nº 10, de 25 de maio de - * 2026. The manual states the rule in its mirror form, weights 9 down to 1 "a partir da + * Leiautes Gerais, approved by the Ato Declaratório Executivo Cofis nº 10, de 19 de maio de + * 2026 (DOU de 25/05/2026). The manual states the rule in its mirror form, weights 9 down to 1 "a partir da * unidade" with "o resto 10 é considerado 0", which is algebraically the same digit as the * weights 10 down to 2 with `11 - resto` implemented above. The manual's own file used to be * served from `sped.rfb.gov.br`, a host that no longer answers at all, so the approving act is diff --git a/src/is-valid-cst/constants.ts b/src/is-valid-cst/constants.ts index 6b091391..1d448951 100644 --- a/src/is-valid-cst/constants.ts +++ b/src/is-valid-cst/constants.ts @@ -10,8 +10,11 @@ * Ajuste SINIEF 39/23, which gave Tabela B its current wording with effect from 01.12.23. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24 * Ajuste SINIEF 20/24, which struck items 12, 13, 52, 72 and 74 from Tabela B (effects from - * 09.07.24) before they ever took effect: Ajuste SINIEF 39/23 had added them "sem efeitos", - * so those codes were never in force. + * 09.07.24) before they ever took effect: those items sat in the inciso III of its cláusula + * segunda, whose effect the alínea "b" of the inciso I of the cláusula terceira of Ajuste SINIEF + * 39/23 had deferred to 1º de outubro de 2024, so the revocation reached them first and the codes + * were never in force. Neither ajuste uses the phrase "sem efeitos"; this is the reading of the + * two clauses, not a quotation. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/1994/aj_003_94 * Ajuste SINIEF 03/1994, which instituted the ICMS CST as the two digit code AB. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2000/AJ_006_00 diff --git a/src/is-valid-cst/is-valid-cst.test.ts b/src/is-valid-cst/is-valid-cst.test.ts index 6ce2e24e..1f4aa632 100644 --- a/src/is-valid-cst/is-valid-cst.test.ts +++ b/src/is-valid-cst/is-valid-cst.test.ts @@ -33,7 +33,7 @@ describe("isValidCst", () => { expect(isValidCst("061", { tax: "icms" })).toBe(true); }); - it('should return false for the codes Ajuste SINIEF 39/23 added "sem efeitos" and Ajuste SINIEF 20/24 struck before they took effect (12, 13, 52, 72 and 74)', () => { + it("should return false for the codes Ajuste SINIEF 39/23 added with deferred effect and Ajuste SINIEF 20/24 struck before they took effect (12, 13, 52, 72 and 74)", () => { expect(isValidCst("012", { tax: "icms" })).toBe(false); expect(isValidCst("013", { tax: "icms" })).toBe(false); expect(isValidCst("052", { tax: "icms" })).toBe(false); diff --git a/src/is-valid-cst/is-valid-cst.ts b/src/is-valid-cst/is-valid-cst.ts index e947db9c..c96f895e 100644 --- a/src/is-valid-cst/is-valid-cst.ts +++ b/src/is-valid-cst/is-valid-cst.ts @@ -65,8 +65,11 @@ const isValidForTax = (digits: string, tax: "icms" | "ipi" | "pis" | "cofins"): * Ajuste SINIEF 39/23, which gave Tabela B its current wording with effect from 01.12.23. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24 * Ajuste SINIEF 20/24, which struck items 12, 13, 52, 72 and 74 from Tabela B (effects from - * 09.07.24) before they ever took effect: Ajuste SINIEF 39/23 had added them "sem efeitos", - * so those codes were never in force. + * 09.07.24) before they ever took effect: those items sat in the inciso III of its cláusula + * segunda, whose effect the alínea "b" of the inciso I of the cláusula terceira of Ajuste SINIEF + * 39/23 had deferred to 1º de outubro de 2024, so the revocation reached them first and the codes + * were never in force. Neither ajuste uses the phrase "sem efeitos"; this is the reading of the + * two clauses, not a quotation. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/1994/aj_003_94 * Ajuste SINIEF 03/1994, which instituted the ICMS CST as the two digit code AB. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2000/AJ_006_00 diff --git a/src/is-valid-email/is-valid-email.ts b/src/is-valid-email/is-valid-email.ts index 63783372..34b72112 100644 --- a/src/is-valid-email/is-valid-email.ts +++ b/src/is-valid-email/is-valid-email.ts @@ -15,9 +15,9 @@ const EMAIL_REGEX = * ``` * * The WHATWG HTML "valid e-mail address" definition is narrowed further: the local part is - * limited to letters, digits and `_'+-.`, it may not start with a dot or contain two dots in a - * row, and the domain must carry at least one dot and end in an alphabetic label of 2 to 63 - * letters. Each dotted label follows the WHATWG production `[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?`, + * limited to letters, digits and `_'+-.`, it may not start with a dot, end with a dot or an + * apostrophe, or contain two dots in a row, and the domain must carry at least one dot and end + * in an alphabetic label of 2 to 63 letters. Each dotted label follows the WHATWG production `[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?`, * so a label may neither start nor end with a hyphen nor exceed 63 characters, and the final * label is capped at the same 63 characters. It is a practical * subset of that WHATWG definition, not of IETF RFC 5322: quoted local parts and address diff --git a/src/is-valid-nfe-key/is-valid-nfe-key.ts b/src/is-valid-nfe-key/is-valid-nfe-key.ts index 262d6c1c..4557a411 100644 --- a/src/is-valid-nfe-key/is-valid-nfe-key.ts +++ b/src/is-valid-nfe-key/is-valid-nfe-key.ts @@ -36,8 +36,11 @@ import { parseNfeKey } from "../parse-nfe-key/parse-nfe-key"; * Ajuste SINIEF 36/19, cláusula primeira: the CT-e OS, modelo 67. * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20 * Ajuste SINIEF 03/20, cláusula primeira: the GTV-e, modelo 64. - * @see Official: https://www.cte.fazenda.gov.br/portal/listaManuais.aspx?tipoConteudo=manuais - * CT-e MOC 4.00, Anexo I: the `tpEmis` domains D19, D27 and D15. + * @see Official: https://dfe-portal.svrs.rs.gov.br/CTE/Documentos + * CT-e MOC 4.00, Anexo I ("MOC CTe 4.00 Anexo I - Leiaute e Regras de Validação"): the `tpEmis` + * domains D19, D27 and D15. Published by the SVRS dfe-portal, like the BP-e, NF3e and NFCom + * manuals below; the cte.fazenda.gov.br manual index answers "Sistema temporariamente + * indisponível" permanently. * @see Official: https://dfe-portal.svrs.rs.gov.br/BPE/Documentos * BP-e MOC 1.00b, Visão Geral and Anexo I: modelo 63. * @see Official: https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos diff --git a/src/is-valid-vin/constants.ts b/src/is-valid-vin/constants.ts index 121f0639..83de7205 100644 --- a/src/is-valid-vin/constants.ts +++ b/src/is-valid-vin/constants.ts @@ -3,15 +3,17 @@ * `I`, `O` and `Q` (dropped to avoid confusion with `1` and `0`), per ISO 3779:2009 structure. * The check digit at the 9th position, its transliteration table and its weighted MOD 11 * algorithm are a North-American requirement (49 CFR 565.15 / SAE J853), not something - * Resolução CONTRAN nº 24/1998 or ABNT NBR 6066 — which define the Brazilian VIN structure — - * mandate; many Brazilian-built VINs do not carry a matching check digit. + * Resolução CONTRAN nº 968/2022 (which replaced Resolução CONTRAN nº 24/1998 from 1 January 2025, + * art. 50, II) or ABNT NBR 6066, which define the Brazilian VIN structure, mandate; many Brazilian-built VINs do not carry a matching check digit. * The ISO catalogue page sits behind a bot filter and answers HTTP 403 to every non-browser * client, so it has to be opened in a browser, where it renders the standard's paywalled * abstract rather than its text. * @see Official: https://www.iso.org/standard/52200.html * @see Official: https://www.ecfr.gov/current/title-49/section-565.15 - * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-Senatran/resolucoes-contran - * @see Based on: https://vpic.nhtsa.dot.gov/api/ + * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf + * Resolução CONTRAN nº 968, de 20 de junho de 2022, art. 2º, I (VIN of 17 characters in three sections) + * and art. 50, II (revocation of Resolução nº 24/1998 from 1 January 2025). + * @see Official: https://vpic.nhtsa.dot.gov/api/ */ export const VIN_LENGTH = 17; diff --git a/src/is-valid-vin/is-valid-vin.ts b/src/is-valid-vin/is-valid-vin.ts index daf56033..274f3441 100644 --- a/src/is-valid-vin/is-valid-vin.ts +++ b/src/is-valid-vin/is-valid-vin.ts @@ -12,7 +12,8 @@ import { * Checks the length (17 characters), the excluded letters (`I`, `O`, `Q` are never valid; ISO * 3779:2009 structure) and the check digit at the 9th position, with the check digit and * transliteration computed per 49 CFR 565.15. That 9th-position check digit is a North-American - * requirement (49 CFR 565.15 / SAE J853): Resolução CONTRAN nº 24/1998 and ABNT NBR 6066 define + * requirement (49 CFR 565.15 / SAE J853): Resolução CONTRAN nº 968/2022 (in force since 1 July 2022, revoking Resolução CONTRAN nº 24/1998 from + * 1 January 2025 by its art. 50, II) and ABNT NBR 6066 define * the Brazilian VIN structure but do not mandate it, so many Brazilian-built VINs do not carry * a matching check digit. This function is therefore a North-American-style structural check, * not a universal validator of Brazilian VINs. Case-insensitive and trims surrounding whitespace. @@ -36,8 +37,10 @@ import { * * @see Official: https://www.iso.org/standard/52200.html * @see Official: https://www.ecfr.gov/current/title-49/section-565.15 - * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-Senatran/resolucoes-contran - * @see Based on: https://vpic.nhtsa.dot.gov/api/ + * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf + * Resolução CONTRAN nº 968, de 20 de junho de 2022, art. 2º, I (VIN of 17 characters in three sections) + * and art. 50, II (revocation of Resolução nº 24/1998 from 1 January 2025). + * @see Official: https://vpic.nhtsa.dot.gov/api/ */ export const isValidVin = (value: string): boolean => { if (typeof value !== "string") return false; diff --git a/src/parse-certidao/constants.ts b/src/parse-certidao/constants.ts index 0663a06c..6ebaf08b 100644 --- a/src/parse-certidao/constants.ts +++ b/src/parse-certidao/constants.ts @@ -15,12 +15,20 @@ * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 * Código Nacional de Normas da Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento * CNJ nº 149/2023), art. 473 as currently published: the in-force layout of the 32 digit - * matrícula. Inciso II and §§ 1º to 5º carry the redação of the Provimento CN nº 237, de - * 13/07/2026; the rest of the article, and the digit layout this library depends on, come from the - * Provimento CN nº 182, de 17/09/2024. + * matrícula. Inciso II and §§ 1º and 3º to 5º carry the redação of the Provimento CN nº 237, de + * 13/07/2026; the rest of the article, § 2º included, and the digit layout this library depends + * on, come from the Provimento CN nº 182, de 17/09/2024. * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 - * Provimento CNJ nº 2, de 27/04/2009, which instituted the modelos únicos de certidão and the - * matrícula (revoked; historical). + * Provimento CNJ nº 2, de 27/04/2009, art. 1º and 2º, which instituted the modelos únicos de + * certidão and ordered that "as certidões passarão a consignar matrícula que identifica o código + * nacional da serventia, o código do acervo, o tipo do serviço prestado, o tipo do livro, o número + * do livro, o número da folha, o número do termo e o digito verificador" (revoked; historical). + * @see Official: https://atos.cnj.jus.br/atos/detalhar/1310 + * Provimento CNJ nº 3, de 17/11/2009, art. 7º, which is where that matrícula first got its digit + * structure: "a matrícula, de inserção obrigatória nas certidões (primeira e demais vias) emitidas + * pelos Cartórios de Registro Civil das Pessoas Naturais a partir de 1º de janeiro de 2010, é + * formada pelos seguintes elementos", incisos I to IX fixing the same 6 + 2 + 2 + 4 + 1 + 5 + 3 + + * 7 + 2 positions art. 473 carries today (revoked; historical). * @see Based on: http://ghiorzi.org/DVnew.htm * Description of the nine books and their codes. * @see Based on: https://github.com/Casilhero/brazilian-validators/blob/main/src/Support/CertidaoInfo.php diff --git a/src/parse-certidao/parse-certidao.ts b/src/parse-certidao/parse-certidao.ts index 38b28172..31802de5 100644 --- a/src/parse-certidao/parse-certidao.ts +++ b/src/parse-certidao/parse-certidao.ts @@ -85,12 +85,20 @@ export type Certidao = { * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 * Código Nacional de Normas da Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento * CNJ nº 149/2023), art. 473 as currently published: the in-force layout of the 32 digit - * matrícula. Inciso II and §§ 1º to 5º carry the redação of the Provimento CN nº 237, de - * 13/07/2026; the rest of the article, and the digit layout this library depends on, come from the - * Provimento CN nº 182, de 17/09/2024. + * matrícula. Inciso II and §§ 1º and 3º to 5º carry the redação of the Provimento CN nº 237, de + * 13/07/2026; the rest of the article, § 2º included, and the digit layout this library depends + * on, come from the Provimento CN nº 182, de 17/09/2024. * @see Official: https://atos.cnj.jus.br/atos/detalhar/1311 - * Provimento CNJ nº 2, de 27/04/2009, which instituted the modelos únicos de certidão and the - * matrícula (revoked; historical). + * Provimento CNJ nº 2, de 27/04/2009, art. 1º and 2º, which instituted the modelos únicos de + * certidão and ordered that "as certidões passarão a consignar matrícula que identifica o código + * nacional da serventia, o código do acervo, o tipo do serviço prestado, o tipo do livro, o número + * do livro, o número da folha, o número do termo e o digito verificador" (revoked; historical). + * @see Official: https://atos.cnj.jus.br/atos/detalhar/1310 + * Provimento CNJ nº 3, de 17/11/2009, art. 7º, which is where that matrícula first got its digit + * structure: "a matrícula, de inserção obrigatória nas certidões (primeira e demais vias) emitidas + * pelos Cartórios de Registro Civil das Pessoas Naturais a partir de 1º de janeiro de 2010, é + * formada pelos seguintes elementos", incisos I to IX fixing the same 6 + 2 + 2 + 4 + 1 + 5 + 3 + + * 7 + 2 positions art. 473 carries today (revoked; historical). * @see Based on: http://ghiorzi.org/DVnew.htm * Worked example of the two check digits (sums 288 and 309). * @see Based on: https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts diff --git a/src/parse-processo-juridico/parse-processo-juridico.ts b/src/parse-processo-juridico/parse-processo-juridico.ts index 7642be59..d50760e5 100644 --- a/src/parse-processo-juridico/parse-processo-juridico.ts +++ b/src/parse-processo-juridico/parse-processo-juridico.ts @@ -10,7 +10,7 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * * @example * ```typescript - * parseProcessoJuridico("0002080-25.2026.5.15.0049"); // "00020802520265150049" + * parseProcessoJuridico("0002080-34.2026.5.15.0049"); // "00020803420265150049" * ``` * * Resolução CNJ nº 65/2008 defines this Número Único de Processo layout and its check digits. From ddf63fcf8d9a98d8a5c60699eff6ce40e4df6e40 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 04:13:37 -0300 Subject: [PATCH 40/75] ci(tree-shaking): mark a missing build as a comparison failure and default the step output to it - `scripts/tree-shaking.ts` exited 1 when `dist/` was missing, which the `tree-shaking: accepted` label could swallow as a regression; it now exits with the comparison-failure code - the compare step writes `code=2` before running Node, so a step that dies before reporting its exit code cannot leave the output empty and pass both gates --- .github/workflows/build.yml | 2 ++ scripts/tree-shaking.ts | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6cee777..6cf52571 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,6 +83,8 @@ jobs: continue-on-error: true run: | if [ "${{ steps.base.outputs.measured }}" = "true" ]; then + code=2 + echo "code=$code" >> "$GITHUB_OUTPUT" set +e node scripts/tree-shaking.ts --compare base.json --markdown tree-shaking.md code=$? diff --git a/scripts/tree-shaking.ts b/scripts/tree-shaking.ts index cb8e2a58..ccf615e0 100644 --- a/scripts/tree-shaking.ts +++ b/scripts/tree-shaking.ts @@ -47,6 +47,9 @@ import { build } from "esbuild"; const rootDir = resolve(import.meta.dirname, ".."); const packageName = "@brazilian-utils/brazilian-utils"; +/** Exit code of a comparison that could not be carried out (missing build, unreadable base). */ +const COMPARISON_ERROR_EXIT_CODE = 2; + const CONCURRENCY = 16; const FULL_IMPORT_KEY = "__full__"; @@ -250,7 +253,7 @@ const measureExports = async ( const distEntry = resolve(packageRoot, "dist/brazilian-utils.js"); if (!existsSync(distEntry)) { console.error(`Missing ${distEntry}. Run \`npm run build\` first.`); - process.exit(1); + process.exit(COMPARISON_ERROR_EXIT_CODE); } const { testable, aliasOf } = await loadExports(distEntry); @@ -579,8 +582,6 @@ const readSnapshot = async (path: string): Promise => { return parsed; }; -const COMPARISON_ERROR_EXIT_CODE = 2; - const main = async (): Promise => { const args = parseArgs(process.argv.slice(2)); const packageRoot = From 68bc902db03fa0aad12544a801c04d2daba93140 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 04:13:37 -0300 Subject: [PATCH 41/75] docs: put every cited URL on its own citation line and fill the thin sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 32 `@see` lines carried the description after the URL; the URL now stands alone and the description follows, as CONTRIBUTING prescribes - `isValidEmail` documents its acceptance rules, `getHolidays` its 1900 to 2099 range, `getAddressInfoByCep` its base error class, `isValidCnh` the repeated-digit rejection, `formatLicensePlate` the empty return and the partial mask, `isValidNcm` the leading-zero string trap, the eight `format*` sections the `pad` default, the certidão section the second-pass weight - README links that only worked on GitHub (bundle size, license, contributing anchor) point at the site or the repository; the OpenSSF badge keeps the documented `api.scorecard.dev` host - `scripts/llms.ts` derives the getting-started TOC and the dataset-backed util list instead of hard-coding them; `formatCei` no longer calls its mask official; the CAEPF repeated-base example is labelled as such; Portuguese wording fixes (fixo, subunidades monetárias, uma de três formas) --- README.md | 10 +-- docs/llms-full.txt | 45 +++++------ docs/llms.txt | 6 +- docs/pt-br/utilities.md | 70 ++++++++--------- docs/utilities.md | 44 +++++------ scripts/llms.ts | 76 +++++++++++++++++-- src/_internals/constants/iban.ts | 6 +- src/add-business-days/add-business-days.ts | 3 +- .../convert-number-to-words.ts | 3 +- .../difference-in-business-days.ts | 6 +- src/format-cei/format-cei.ts | 4 +- src/format-iban/format-iban.ts | 6 +- .../generate-pix-payload.ts | 3 +- .../get-state-by-ibge-code.ts | 3 +- .../get-state-code-by-name.ts | 3 +- .../get-state-name-by-code.ts | 3 +- src/get-timezone-by-state/constants.ts | 6 +- .../get-timezone-by-state.ts | 6 +- src/is-valid-caepf/constants.ts | 3 +- src/is-valid-caepf/is-valid-caepf.ts | 5 +- src/is-valid-iban/is-valid-iban.ts | 15 ++-- src/is-valid-pix-key/is-valid-pix-key.ts | 3 +- .../is-valid-pix-payload.ts | 3 +- src/parse-iban/parse-iban.ts | 12 ++- src/parse-pix-key/parse-pix-key.ts | 3 +- src/parse-pix-payload/parse-pix-payload.ts | 3 +- src/sub-business-days/sub-business-days.ts | 3 +- 27 files changed, 225 insertions(+), 128 deletions(-) diff --git a/README.md b/README.md index 35135c07..6e4d7743 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ [📖 Documentation](https://brazilian-utils.com.br/#/getting-started) -[![npm version](https://img.shields.io/npm/v/@brazilian-utils/brazilian-utils.svg)](https://www.npmjs.com/package/@brazilian-utils/brazilian-utils) [![Downloads per month](https://img.shields.io/npm/dm/@brazilian-utils/brazilian-utils.svg)](https://www.npmjs.com/package/@brazilian-utils/brazilian-utils) [![License: MIT](https://img.shields.io/github/license/brazilian-utils/javascript.svg)](LICENSE) -[![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](CONTRIBUTING.md#zero-runtime-dependencies) [![Bundle size](https://img.shields.io/bundlephobia/minzip/@brazilian-utils/brazilian-utils?label=isValidCpf%20import%20%3C%201%20KB&color=brightgreen)](docs/getting-started.md#bundle-size) [![Tree-shakeable](https://badgen.net/bundlephobia/tree-shaking/@brazilian-utils/brazilian-utils)](docs/getting-started.md#bundle-size) [![TypeScript](https://img.shields.io/npm/types/@brazilian-utils/brazilian-utils)](https://www.npmjs.com/package/@brazilian-utils/brazilian-utils) -[![Build Status](https://github.com/brazilian-utils/javascript/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/brazilian-utils/javascript/actions/workflows/build.yml?query=branch%3Amain) [![Tests](https://github.com/brazilian-utils/javascript/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/brazilian-utils/javascript/actions/workflows/tests.yml?query=branch%3Amain) [![codecov](https://codecov.io/gh/brazilian-utils/javascript/branch/main/graph/badge.svg)](https://codecov.io/gh/brazilian-utils/javascript) [![Mutation tests](https://github.com/brazilian-utils/javascript/actions/workflows/mutation.yml/badge.svg?branch=main)](https://github.com/brazilian-utils/javascript/actions/workflows/mutation.yml?query=branch%3Amain) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/brazilian-utils/javascript/badge)](https://scorecard.dev/viewer/?uri=github.com/brazilian-utils/javascript) +[![npm version](https://img.shields.io/npm/v/@brazilian-utils/brazilian-utils.svg)](https://www.npmjs.com/package/@brazilian-utils/brazilian-utils) [![Downloads per month](https://img.shields.io/npm/dm/@brazilian-utils/brazilian-utils.svg)](https://www.npmjs.com/package/@brazilian-utils/brazilian-utils) [![License: MIT](https://img.shields.io/github/license/brazilian-utils/javascript.svg)](https://github.com/brazilian-utils/javascript/blob/main/LICENSE) +[![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](https://github.com/brazilian-utils/javascript/blob/main/CONTRIBUTING.md#zero-runtime-dependencies) [![Bundle size](https://img.shields.io/bundlephobia/minzip/@brazilian-utils/brazilian-utils?label=isValidCpf%20import%20%3C%201%20KB&color=brightgreen)](https://brazilian-utils.com.br/#/getting-started?id=bundle-size) [![Tree-shakeable](https://badgen.net/bundlephobia/tree-shaking/@brazilian-utils/brazilian-utils)](https://brazilian-utils.com.br/#/getting-started?id=bundle-size) [![TypeScript](https://img.shields.io/npm/types/@brazilian-utils/brazilian-utils)](https://www.npmjs.com/package/@brazilian-utils/brazilian-utils) +[![Build Status](https://github.com/brazilian-utils/javascript/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/brazilian-utils/javascript/actions/workflows/build.yml?query=branch%3Amain) [![Tests](https://github.com/brazilian-utils/javascript/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/brazilian-utils/javascript/actions/workflows/tests.yml?query=branch%3Amain) [![codecov](https://codecov.io/gh/brazilian-utils/javascript/branch/main/graph/badge.svg)](https://codecov.io/gh/brazilian-utils/javascript) [![Mutation tests](https://github.com/brazilian-utils/javascript/actions/workflows/mutation.yml/badge.svg?branch=main)](https://github.com/brazilian-utils/javascript/actions/workflows/mutation.yml?query=branch%3Amain) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/brazilian-utils/javascript/badge)](https://scorecard.dev/viewer/?uri=github.com/brazilian-utils/javascript) @@ -88,7 +88,7 @@ isValidCpf("1232454233345"); // false You can check a list of utilities [by clicking here](https://brazilian-utils.com.br/#/utilities). -- The package is tree-shakeable. Every util is also available as its own subpath (e.g. `@brazilian-utils/brazilian-utils/get-cities`) so you can lazy-load the few heavy ones. See [Bundle size](docs/getting-started.md#bundle-size). +- The package is tree-shakeable. Every util is also available as its own subpath (e.g. `@brazilian-utils/brazilian-utils/get-cities`) so you can lazy-load the few heavy ones. See [Bundle size](https://brazilian-utils.com.br/#/getting-started?id=bundle-size). ## Development @@ -174,4 +174,4 @@ This project follows the [all-contributors](https://github.com/kentcdodds/all-co ## License -[MIT](LICENSE) +[MIT](https://github.com/brazilian-utils/javascript/blob/main/LICENSE) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 1b27d4ce..435b249e 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -5,6 +5,7 @@ ## Table of contents - [Getting Started](#getting-started) + - [Why Brazilian Utils](#why-brazilian-utils) - [Installation](#installation) - [Runtime support](#runtime-support) - [Usage](#usage) @@ -289,7 +290,7 @@ generateCpf('SP'); // the 9th digit is 8, the SP região fiscal code ### isValidCnpj -Check if CNPJ is valid. `options.version` (part of `IsValidCnpjOptions`) picks which format is accepted: `1` (default) the numeric-only format, `2` both the numeric and the alphanumeric one; any other value is read as `1`, the way `formatCnpj` and `parseCnpj` read it. The usual mask characters and whitespace are accepted in either version. +Check if CNPJ is valid. `options.version` (part of `IsValidCnpjOptions`) picks which format is accepted: `1` (default) the numeric-only format, `2` both the numeric and the alphanumeric one; any other value is read as `1`, the way `formatCnpj` and `parseCnpj` read it. The usual mask characters and whitespace are accepted in either version. Version `2` has no reserved-value list, because the Receita Federal manual defines none for the alphanumeric format: a repeated-character alphanumeric base (all `A`s, say) that passes the checksum is accepted, while the numeric reserved numbers are rejected under version `1`. ```javascript import { isValidCnpj } from '@brazilian-utils/brazilian-utils'; @@ -556,7 +557,7 @@ parseNfeKey('invalid'); // null ### isValidEmail -Check if email is valid. +Check if email is valid. The accepted set is a practical subset of the WHATWG HTML [valid e-mail address](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) definition, not of [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322). The local part is limited to letters, digits and `_'+-.`, and may not start with a dot, end with a dot or an apostrophe, or contain two dots in a row. The domain must carry at least one dot, and each dotted label follows the WHATWG production `[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?`, so a label may neither start nor end with a hyphen nor exceed 63 characters; the final label is alphabetic and 2 to 63 letters long, so `user@example.c1` is rejected. Quoted local parts (`"john doe"@example.com`) and address literals (`john@[127.0.0.1]`) are rejected. ```javascript import { isValidEmail } from '@brazilian-utils/brazilian-utils'; @@ -636,7 +637,7 @@ isValidLandlinePhone('1130000000'); // true ### isValidServicePhone -Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`), whose consolidated table is the Anexo of [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151). `112` and `911` are rejected: Anatel designates neither, and `911` is not even inside the `1N₂N₁` range art. 13 of [Resolução nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749) destines to public utility services, so the way handsets route them is a GSM convention rather than a numbering designation. Only the structure is checked, the number does not have to be assigned to anyone. Anatel withdrew the 4-digit codes instead of allocating them (art. 43 I of [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86) and art. 2º II of the Ato above both ordered them released), so only the conventional `300X` and `400X` roots are recognised: other "Número Único" carrier prefixes in market use, such as `4020` and `4062`, are out of scope and are rejected. +Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total, so the shorter, extinct `0800` + 6 digit form is rejected), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`), whose consolidated table is the Anexo of [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151). `112` and `911` are rejected: Anatel designates neither, and `911` is not even inside the `1N₂N₁` range art. 13 of [Resolução nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749) destines to public utility services, so the way handsets route them is a GSM convention rather than a numbering designation. Only the structure is checked: the number does not have to be assigned to anyone, and the `0500` rule that encodes a donation amount in the last two digits is not enforced. Anatel withdrew the 4-digit codes instead of allocating them (art. 43 I of [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86) and art. 2º II of the Ato above both ordered them released), so only the conventional `300X` and `400X` roots are recognised: other "Número Único" carrier prefixes in market use, such as `4020` and `4062`, are out of scope and are rejected. ```javascript import { isValidServicePhone } from '@brazilian-utils/brazilian-utils'; @@ -729,7 +730,7 @@ isValidPis('12056412547'); // false ### formatPis -Format PIS number. `options.pad` (part of `FormatPisOptions`) left-pads the value with zeros to the full 11 digits before masking. +Format PIS number. `options.pad` (part of `FormatPisOptions`) left-pads the value with zeros to the full 11 digits before masking (default `false`). ```javascript import { formatPis } from '@brazilian-utils/brazilian-utils'; @@ -750,7 +751,7 @@ parsePis('123.45678.90-1'); // 12345678901 ### formatCep -Format CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (part of `FormatCepOptions`) left-pads the value with zeros to the full 8 digits before masking. +Format CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (part of `FormatCepOptions`) left-pads the value with zeros to the full 8 digits before masking (default `false`). ```javascript import { formatCep } from '@brazilian-utils/brazilian-utils'; @@ -771,7 +772,7 @@ parseCep('92500-000'); // 92500000 ### getAddressInfoByCep -Fetch address information for a given CEP using multiple providers. Defaults to `['viacep', 'brasilapi']`. The `'widenet'` provider is deprecated (its endpoint no longer responds) and excluded from the default list, but it can still be requested explicitly via `options.providers` (typed as `CepProvider[]`). The resolved address is typed as `AddressInfo`. A transient network failure is retried twice per provider, with a 250 ms linear backoff (250 ms, then 500 ms), so a provider that keeps failing is tried up to 3 times and adds about 750 ms before its own failure lands; an HTTP error status or a non-retryable failure is not retried. The providers are started together and raced with `Promise.any`, not queried one after the other, so those retries delay nothing for the other providers, only the moment an all-failed rejection can surface. An `options.providers` that names no known provider rejects with `GetAddressInfoByCepValidationError` ("Nenhum provedor válido especificado"): an empty array, an array of unknown names, and a value that is not an array at all, `null` included. With `providers: ['brasilapi']`, a CEP BrasilAPI does not know rejects with `GetAddressInfoByCepNotFoundError`, since BrasilAPI signals a miss with HTTP 404; any other error status is still a `GetAddressInfoByCepServiceError`. +Fetch address information for a given CEP using multiple providers. Defaults to `['viacep', 'brasilapi']`. The `'widenet'` provider is deprecated (its endpoint no longer responds) and excluded from the default list, but it can still be requested explicitly via `options.providers` (typed as `CepProvider[]`). The resolved address is typed as `AddressInfo`. A transient network failure is retried twice per provider, with a 250 ms linear backoff (250 ms, then 500 ms), so a provider that keeps failing is tried up to 3 times and adds about 750 ms before its own failure lands; an HTTP error status or a non-retryable failure is not retried. The providers are started together and raced with `Promise.any`, not queried one after the other, so those retries delay nothing for the other providers, only the moment an all-failed rejection can surface. An `options.providers` that names no known provider rejects with `GetAddressInfoByCepValidationError` ("Nenhum provedor válido especificado"): an empty array, an array of unknown names, and a value that is not an array at all, `null` included. With `providers: ['brasilapi']`, a CEP BrasilAPI does not know rejects with `GetAddressInfoByCepNotFoundError`, since BrasilAPI signals a miss with HTTP 404; any other error status is still a `GetAddressInfoByCepServiceError`. All three extend `GetAddressInfoByCepError`, the base class of every error this util rejects with, so a single `catch` on it covers all of them. ```javascript import { getAddressInfoByCep } from '@brazilian-utils/brazilian-utils'; @@ -803,7 +804,7 @@ isValidProcessoJuridico('ab00020802520125150049'); // false (letters are rejecte ### formatProcessoJuridico -Format the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119) (mask `NNNNNNN-DD.AAAA.J.TR.OOOO`). `options.pad` (part of `FormatProcessoJuridicoOptions`) left-pads the value with zeros to the full 20 digits before masking. +Format the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119) (mask `NNNNNNN-DD.AAAA.J.TR.OOOO`). `options.pad` (part of `FormatProcessoJuridicoOptions`) left-pads the value with zeros to the full 20 digits before masking (default `false`). ```javascript import { formatProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -966,7 +967,7 @@ getBankByCode('999'); // null ### getBankByIspb -Look a Brazilian bank up by its ISPB (Identificador do Sistema de Pagamentos Brasileiro), the 8 digit code published by Banco Central do Brasil in the [STR participants list](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). Every SPB participant has an ISPB, but this dataset only carries the institutions that also have a COMPE code, so an ISPB whose institution has no COMPE code of its own returns `null`. Accepts both `string` and `number` input, with or without leading zeros. Returns a fresh copy (typed as `Bank`) of the matching bank, or `null` when no bank has that ISPB. +Look a Brazilian bank up by its ISPB (Identificador do Sistema de Pagamentos Brasileiro), the 8 digit code published by Banco Central do Brasil in the [STR participants list](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). Every SPB participant has an ISPB, but this dataset only carries the institutions that also have a COMPE code, so an ISPB whose institution has no COMPE code of its own returns `null`. Accepts both `string` and `number` input, with or without leading zeros, so `getBankByIspb(0)` finds the same bank as `getBankByIspb('00000000')`. The dataset is generated from that CSV, falling back to [BrasilAPI](https://brasilapi.com.br/api/banks/v1) when the Bacen request fails. Returns a fresh copy (typed as `Bank`) of the matching bank, or `null` when no bank has that ISPB. ```javascript import { getBankByIspb } from '@brazilian-utils/brazilian-utils'; @@ -1197,7 +1198,7 @@ getStateByIbgeCode(3.5); // null ### getStateCodeByName -Get the two-letter code (sigla) of a Brazilian state given its full name. The match is accent-insensitive, case-insensitive and ignores leading/trailing whitespace, so `'sao paulo'`, `'SÃO PAULO'` and `' São Paulo '` all resolve to `'SP'`. Exports the `StateCode` type. +Get the two-letter code (sigla) of a Brazilian state given its full name. The match is accent-insensitive, case-insensitive and ignores leading/trailing whitespace, so `'sao paulo'`, `'SÃO PAULO'` and `' São Paulo '` all resolve to `'SP'`. Every run of internal whitespace collapses into a single space too, so `'Rio de Janeiro'` resolves to `'RJ'`, while a name written without the space matches nothing (`'saopaulo'` is not `'São Paulo'`). Exports the `StateCode` type. ```javascript import { getStateCodeByName } from '@brazilian-utils/brazilian-utils'; @@ -1279,7 +1280,7 @@ getCities('SP'); ### getHolidays -Get Brazilian holidays for a given year. Returns national holidays and optionally state-specific holidays. Each holiday (typed as `Holiday`) has a `type` field (`HolidayType`: `"national"`, `"state"`, `"optional"` or `"religious"`). "Dia da Consciência Negra" (Nov 20) is a national holiday from 2024 onward (Lei nº 14.759/2023). Before that, MT and RJ still carry their own state-level entry named `"Consciência Negra"` on the same date. Results are memoized per `year`/`stateCode`, but each call still returns a fresh copy. An unknown/invalid `stateCode` is ignored, returning national holidays only; the lookup reads own properties only, so `"__proto__"`, `"constructor"` and the like are unknown state codes rather than a crash. +Get Brazilian holidays for a given year. Returns national holidays and optionally state-specific holidays. Each holiday (typed as `Holiday`) has a `type` field (`HolidayType`: `"national"`, `"state"`, `"optional"` or `"religious"`). "Dia da Consciência Negra" (Nov 20) is a national holiday from 2024 onward (Lei nº 14.759/2023). Before that, several states still carry a state-level entry of their own on the same date: `"Consciência Negra"` in MT and RJ, `"Dia Estadual da Consciência Negra"` in AP and `"Dia da Consciência Negra"` in AM and SP. Results are memoized per `year`/`stateCode`, but each call still returns a fresh copy. An unknown/invalid `stateCode` is ignored, returning national holidays only; the lookup reads own properties only, so `"__proto__"`, `"constructor"` and the like are unknown state codes rather than a crash. Only the years 1900 through 2099 are supported, the range the business day utilities inherit; a year outside it returns `[]`. Only one state holiday per UF is a feriado civil under [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm), art. 1º, II, which authorises "a data magna do Estado fixada em lei estadual" in the singular; the other entries rest on ordinary state laws and are reported because they are observed in practice. Notable per-state rules: @@ -1368,7 +1369,7 @@ generateCep(); // '92500000' ### formatCnh -Format CNH. `options.pad` (part of `FormatCnhOptions`) left-pads the value with zeros to the full 11 digits before masking. +Format CNH. `options.pad` (part of `FormatCnhOptions`) left-pads the value with zeros to the full 11 digits before masking (default `false`). ```javascript import { formatCnh } from '@brazilian-utils/brazilian-utils'; @@ -1379,7 +1380,7 @@ formatCnh('2650306461', { pad: true }); // 026503064-61 ### isValidCnh -Check if CNH is valid. Spaces, dots and hyphens around/between the digits are ignored, but any other character, a letter in particular, makes the value invalid. +Check if CNH is valid. Spaces, dots and hyphens around/between the digits are ignored, but any other character, a letter in particular, makes the value invalid. A value whose 11 digits are all the same is rejected before the check digits are computed, so `'11111111111'` is invalid. ```javascript import { isValidCnh } from '@brazilian-utils/brazilian-utils'; @@ -1401,7 +1402,7 @@ generateCnh(); // '02650306461' ### parseCnh -Remove CNH formatting, keep only digits, and cap the result to 11 digits. +Remove CNH formatting, keep only digits, and cap the result to 11 digits. Returns `''` when there is no digit at all. ```javascript import { parseCnh } from '@brazilian-utils/brazilian-utils'; @@ -1534,7 +1535,7 @@ generatePhone('service'); // '08001234567' or '40041234' ### formatLicensePlate -Format a license plate. Old Brazilian plates (`LLLNNNN`) are returned with a hyphen and Mercosul plates (`LLLNLNN`) stay normalized. +Format a license plate. Old Brazilian plates (`LLLNNNN`) are returned with a hyphen and Mercosul plates (`LLLNLNN`) stay normalized. Partial values are formatted as far as they go, so it can also be used as an input mask, and a value that cannot start a valid plate gives `''`. ```javascript import { formatLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -1606,7 +1607,7 @@ generatePis(); // '91077906857' ### getMunicipality -Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. A single function handles both directions, based on whether `options` has a `code` or a `municipalityName`/`uf`. `code` accepts both `string` and `number` input and must be exactly 7 digits, otherwise the function resolves to `null`. A `code` given as a number must be a non-negative integer: a sign and a decimal point are not digits, so `-3550308` and `355030.8` resolve to `null` instead of being read as `3550308`. Resolution is entirely offline, from a bundled IBGE dataset: no network request is made. The municipality name match ignores accents and casing. An unknown municipality, an unknown UF or invalid input all resolve to `null`. The `[name, uf]` pair is a fresh array on every call, so mutating the result never affects subsequent lookups. +Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. A single function handles both directions, based on whether `options` has a `code` or a `municipalityName`/`uf`. `code` accepts both `string` and `number` input and must be exactly 7 digits, otherwise the function resolves to `null`. A `code` given as a number must be a non-negative integer: a sign and a decimal point are not digits, so `-3550308` and `355030.8` resolve to `null` instead of being read as `3550308`. Resolution is entirely offline, from a bundled IBGE dataset: no network request is made. The municipality name match ignores accents and casing, and every run of whitespace collapses into a single space, so `'sao paulo'` matches `'São Paulo'` while a name written without the space does not; the casing is folded to upper case, the direction Unicode expands `'ß'` to `'SS'` in, so `'Paßos'` matches `'Passos'`. An unknown municipality, an unknown UF or invalid input all resolve to `null`. The `[name, uf]` pair is a fresh array on every call, so mutating the result never affects subsequent lookups. ```javascript import { getMunicipality } from '@brazilian-utils/brazilian-utils'; @@ -1872,7 +1873,7 @@ formatCns('89010001', { pad: true }); // '000 0000 8901 0001' ### isValidCertidao -Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with weights cycling from 2 to 10 and back through 0. Accepts the usual mask characters and whitespace between/around groups. The layout is the one [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023) currently publishes, with inciso II and §§ 1º and 3º to 5º in the redação of the Provimento CN nº 237/2026 and the rest of the article, § 2º included, in that of the Provimento CN nº 182/2024; the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311) and got its digit structure from the also revoked [Provimento CNJ nº 3/2009, art. 7º](https://atos.cnj.jus.br/atos/detalhar/1310). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). +Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with the weights cycling from 2 to 10 and back through 0: the first pass starts at 2 over the 30 base digits, the second at 1 over the 31 digits that include the first check digit, and in both a remainder of 10 is read as 1. Accepts the usual mask characters and whitespace between/around groups. The layout is the one [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023) currently publishes, with inciso II and §§ 1º and 3º to 5º in the redação of the Provimento CN nº 237/2026 and the rest of the article, § 2º included, in that of the Provimento CN nº 182/2024; the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311) and got its digit structure from the also revoked [Provimento CNJ nº 3/2009, art. 7º](https://atos.cnj.jus.br/atos/detalhar/1310). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). The serviço digits are fixed at `55`, the code [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) assigns to the registro civil das pessoas naturais, so a matrícula carrying any other pair in the ninth and tenth positions is rejected however good its check digits are. The book-type digit always has to name one of the nine book types (the same `CertidaoType` returned by `parseCertidao`), so a matrícula whose digit is `0` is rejected however good its check digits are, the same way `parseCertidao` returns `null` for it. `options.accept` (part of `IsValidCertidaoOptions`) narrows that to the listed types; it defaults to every type, and a value that is not an array falls back to that default. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. @@ -1929,7 +1930,7 @@ The `Certidao` result carries: ### formatCertidao -Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits. The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. +Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. ```javascript import { formatCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1955,7 +1956,7 @@ isValidCei('000000000000'); // false (repeated digits) ### formatCei -Format a CEI (Cadastro Específico do INSS) number according to the usual `00.000.00000/00` mask, the one the reference implementations of the check digit agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCeiOptions`) left pads the value with zeros up to 12 digits. +Format a CEI (Cadastro Específico do INSS) number according to the usual `00.000.00000/00` mask, the one the reference implementations of the check digit agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCeiOptions`) left pads the value with zeros up to 12 digits (default `false`). ```javascript import { formatCei } from '@brazilian-utils/brazilian-utils'; @@ -1981,7 +1982,7 @@ isValidCno('000000000000'); // false (repeated digits) ### formatCno -Format a CNO (Cadastro Nacional de Obras) number. The CNO kept the CEI's numbering, so both share the same 12 digit, `00.000.00000/00` mask, the one the reference implementations of the check digit agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCnoOptions`) left pads the value with zeros up to 12 digits. +Format a CNO (Cadastro Nacional de Obras) number. The CNO kept the CEI's numbering, so both share the same 12 digit, `00.000.00000/00` mask, the one the reference implementations of the check digit agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCnoOptions`) left pads the value with zeros up to 12 digits (default `false`). ```javascript import { formatCno } from '@brazilian-utils/brazilian-utils'; @@ -2002,13 +2003,13 @@ isValidCaepf('293.118.610/001-84'); // true isValidCaepf('41142260000101'); // true isValidCaepf(29311861000184); // true isValidCaepf('29311861000185'); // false (invalid check digits) -isValidCaepf('00000000000000'); // false (invalid check digits) +isValidCaepf('00000000000000'); // false (repeated base digits) isValidCaepf('00000000000012'); // false (repeated base digits) ``` ### formatCaepf -Format a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number according to the usual `000.000.000/000-00` mask, the one the sources of the check digit rule agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCaepfOptions`) left pads the value with zeros up to 14 digits. +Format a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number according to the usual `000.000.000/000-00` mask, the one the sources of the check digit rule agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCaepfOptions`) left pads the value with zeros up to 14 digits (default `false`). ```javascript import { formatCaepf } from '@brazilian-utils/brazilian-utils'; @@ -2121,7 +2122,7 @@ getCnae('0111abc301'); // null (not a documented form) ### isValidNcm -Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. Accepts the code with or without the dotted mask, or as a number. A string is only read as a code when it is written in one of those forms (the 8 digits, or the `NNNN.NN.NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. Accepts the code with or without the dotted mask, or as a number. A string is only read as a code when it is written in one of those forms (the 8 digits, or the `NNNN.NN.NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. A bare number cannot carry a leading zero, so a code starting with `0` has to be passed as a string: `isValidNcm(1012100)` is `false` while `isValidNcm('01012100')` is `true`. ```javascript import { isValidNcm } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/llms.txt b/docs/llms.txt index d700f79e..4b909811 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -10,7 +10,7 @@ Install with `npm install --save @brazilian-utils/brazilian-utils` (also availab import { isValidCpf } from '@brazilian-utils/brazilian-utils'; ``` -Every util is also available as its own subpath for lazy-loading/code-splitting, `@brazilian-utils/brazilian-utils/` (kebab-case of the function name, e.g. `isValidCpf` maps to `is-valid-cpf`) - most useful for `getCities`, the one util that embeds a large dataset: +Every util is also available as its own subpath for lazy-loading/code-splitting, `@brazilian-utils/brazilian-utils/` (kebab-case of the function name, e.g. `isValidCpf` maps to `is-valid-cpf`) - most useful for the utils that embed an official dataset (`getMunicipalities`, `getMunicipalityByCode`, `getMunicipality`, `getCities`, `isValidNcm`, `isValidCbo`, `getCbo`, `isValidCnae`, `getCnae`, `isValidCfop`, `getCfop`, `getBanks` and `getBankByCode`): ```javascript const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities'); @@ -20,7 +20,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [Getting started](https://brazilian-utils.com.br/getting-started.md): installation, runtime support, usage and bundle size/subpath imports - [Utilities](https://brazilian-utils.com.br/utilities.md): full English reference, one section per function, with signatures and examples -- [Bundle size](https://brazilian-utils.com.br/getting-started.md#bundle-size): tree-shaking behavior and the `getCities`/subpath-import exception +- [Bundle size](https://brazilian-utils.com.br/getting-started.md#bundle-size): tree-shaking behavior and the dataset-backed utils that are worth a subpath import ## Validators (isValid*) @@ -35,7 +35,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [isValidPhone](https://brazilian-utils.com.br/utilities.md#isvalidphone): Check if phone number (mobile or landline) is valid. - [isValidMobilePhone](https://brazilian-utils.com.br/utilities.md#isvalidmobilephone): Check if mobile phone number is valid. - [isValidLandlinePhone](https://brazilian-utils.com.br/utilities.md#isvalidlandlinephone): Check if landline phone number is valid. -- [isValidServicePhone](https://brazilian-utils.com.br/utilities.md#isvalidservicephone): Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`), whose consolidated table is the Anexo of Ato Anatel nº 43.151/2004. +- [isValidServicePhone](https://brazilian-utils.com.br/utilities.md#isvalidservicephone): Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total, so the shorter, extinct `0800` + 6 digit form is rejected), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`), whose consolidated table is the Anexo of Ato Anatel nº 43.151/2004. - [isValidLicensePlate](https://brazilian-utils.com.br/utilities.md#isvalidlicenseplate): Check if license plate is valid. - [isValidRenavam](https://brazilian-utils.com.br/utilities.md#isvalidrenavam): Check if RENAVAM (Registro Nacional de Veículos Automotores) is valid. - [isValidPis](https://brazilian-utils.com.br/utilities.md#isvalidpis): Check if PIS is valid. diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index c7b94268..bfc15aa8 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -50,7 +50,7 @@ generateCpf('SP'); // o 9º dígito é 8, o código da região fiscal de SP ## isValidCnpj -Valida se o CNPJ é válido. `options.version` (parte de `IsValidCnpjOptions`) escolhe qual formato é aceito: `1` (padrão) apenas o formato numérico, `2` tanto o numérico quanto o alfanumérico; qualquer outro valor é lido como `1`, do mesmo jeito que `formatCnpj` e `parseCnpj` o leem. Os caracteres de máscara usuais e espaços em branco são aceitos nas duas versões. +Valida se o CNPJ é válido. `options.version` (parte de `IsValidCnpjOptions`) escolhe qual formato é aceito: `1` (padrão) apenas o formato numérico, `2` tanto o numérico quanto o alfanumérico; qualquer outro valor é lido como `1`, do mesmo jeito que `formatCnpj` e `parseCnpj` o leem. Os caracteres de máscara usuais e espaços em branco são aceitos nas duas versões. A versão `2` não tem lista de valores reservados, porque o manual da Receita Federal não define nenhuma para o formato alfanumérico: uma base alfanumérica de caracteres repetidos (todos `A`, por exemplo) que passe no dígito verificador é aceita, enquanto os números reservados numéricos são rejeitados na versão `1`. ```javascript import { isValidCnpj } from '@brazilian-utils/brazilian-utils'; @@ -93,7 +93,7 @@ import { isValidCep } from '@brazilian-utils/brazilian-utils'; isValidCep('01310100'); // true isValidCep('92500-000'); // true (hífen entre os grupos) isValidCep('92.500-000'); // true (ponto e hífen) -isValidCep('013 10 100'); // true (espaços entre os dígitos) +isValidCep('013 10 100'); // true (espaços em qualquer posição entre os dígitos) isValidCep(20040020); // true (entrada numérica) isValidCep('9250000A'); // false (letras são rejeitadas) isValidCep('12345'); // false (tamanho inválido) @@ -146,7 +146,7 @@ parseBoleto('00190.00009 01149.718601 68524.522114 6 75860000102656'); // 001900 ## generateBoleto -Gera um boleto válido aleatório. Informe `{ type: "arrecadacao" }` (tipado como `GenerateBoletoOptions`) para gerar um boleto de arrecadação em vez do tipo padrão "bancario" (cobrança bancária). Um boleto de arrecadação sorteia o segmento entre 1 e 7 (o segmento 9 é de uso dos próprios bancos) e o identificador de valor entre os quatro valores possíveis, `6` e `8` para valor efetivo e `7` e `9` para quantidade de moeda, de modo que os dois ramos de `hasEffectiveValue` do `getBoletoInfo` sejam alcançáveis. +Gera um boleto válido aleatório. Informe `{ type: "arrecadacao" }` (tipado como `GenerateBoletoOptions`) para gerar um boleto de arrecadação em vez do tipo padrão "bancario" (cobrança bancária). Um boleto de arrecadação sorteia o segmento entre 1 e 7 (o segmento 9 é de uso dos próprios bancos) e o identificador de valor entre os quatro valores possíveis, `6` e `8` para valor efetivo e `7` e `9` para quantidade de referência, de modo que os dois ramos de `hasEffectiveValue` do `getBoletoInfo` sejam alcançáveis. ```javascript import { generateBoleto } from '@brazilian-utils/brazilian-utils'; @@ -157,7 +157,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ## getBoletoInfo -Extrai informações de um boleto (valor, data de vencimento, código do banco). Retorna `undefined` quando `value` não é um boleto válido — o `isValidBoleto` é verificado antes —, exatamente como na 2.3.0, então o resultado precisa ser estreitado antes de ser lido. Aceita opcionalmente `{ referenceDate }` (tipado como `GetBoletoInfoOptions`) para resolver o ciclo do "fator de vencimento" a partir de uma data específica em vez de agora (o ciclo do fator reiniciou em 22/02/2025, segundo a FEBRABAN). Nem a FEBRABAN nem o Banco Central publicam uma forma de distinguir um fator do ciclo antigo de um do ciclo novo, então todo fator resolve para uma de duas datas separadas por 9000 dias e o `referenceDate` escolhe entre elas por meio das janelas de segurança da própria biblioteca: o mesmo boleto pode passar a resolver para a outra candidata com o tempo, então informe `referenceDate` explicitamente sempre que a resposta precisar ser estável. A busca de ciclo nunca desce abaixo do primeiro ciclo, então um `referenceDate` anterior ao próprio esquema ainda resolve um fator para a data mais antiga que aquele fator consegue representar, em vez de uma anterior à data-base de 07/10/1997. Para um boleto de arrecadação, o resultado, tipado como `BoletoInfo`, continua trazendo as duas chaves, porém vazias, `bankCode: ''` e `expirationDate: null`, já que o boleto não tem código de banco nem fator de vencimento, e acrescenta `type: "arrecadacao"`, `segment`, `value` e `hasEffectiveValue`. +Extrai informações de um boleto (valor, data de vencimento, código do banco). Retorna `undefined` quando `value` não é um boleto válido — o `isValidBoleto` é verificado antes —, exatamente como na 2.3.0, então o resultado precisa ser estreitado antes de ser lido. Aceita opcionalmente `{ referenceDate }` (tipado como `GetBoletoInfoOptions`) para resolver o ciclo do "fator de vencimento" a partir de uma data específica em vez de agora (o ciclo de data-base do fator reiniciou em 22/02/2025, segundo a FEBRABAN). Nem a FEBRABAN nem o Banco Central publicam uma forma de distinguir um fator do ciclo antigo de um do ciclo novo, então todo fator resolve para uma de duas datas separadas por 9000 dias e o `referenceDate` escolhe entre elas por meio das janelas de segurança da própria biblioteca: o mesmo boleto pode passar a resolver para a outra candidata com o tempo, então informe `referenceDate` explicitamente sempre que a resposta precisar ser estável. A busca de ciclo nunca desce abaixo do primeiro ciclo, então um `referenceDate` anterior ao próprio esquema ainda resolve um fator para a data mais antiga que aquele fator consegue representar, em vez de uma anterior à data-base de 07/10/1997. Para um boleto de arrecadação, o resultado, tipado como `BoletoInfo`, continua trazendo as duas chaves, porém vazias, `bankCode: ''` e `expirationDate: null`, já que o boleto não tem código de banco nem fator de vencimento, e acrescenta `type: "arrecadacao"`, `segment`, `value` e `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -209,7 +209,7 @@ parsePixKey('+5551998259765'); // { type: 'phone', value: '+5551998259765' } ## isValidPixPayload -Valida se um payload de BR Code Pix (a string por trás de um QR Code Pix e do "Pix copia e cola") é válido: estrutura TLV bem formada, objetos obrigatórios presentes, um dos templates "Merchant Account Information" carregando o GUI `br.gov.bcb.pix` junto com uma chave ou uma URL, e um CRC-16 que confere. O objeto "Point of Initiation Method" (`01`) é informativo: o Manual do BR Code o marca como opcional e só atribui significado ao valor `"12"` ("só pode ser utilizado uma vez"), então ele pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` torna o payload inválido. Quando um payload construído em torno de uma chave traz um valor (`54`), esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque, ou seja, a menos que traga o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`) como prescreve o §2.6 do manual do Pix; rejeitar `"0"`/`"0.00"` sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. Um `fss` escrito ao lado de uma localização de PSP torna o payload inválido: o §2.7 do Manual de Padrões para Iniciação do Pix mapeia o QR Code dinâmico para exatamente dois subobjetos, `00` (GUI) e `25` (URL), e o `fss` pertence ao template estático do §2.6. A chave em si não é validada contra os formatos do DICT, use `isValidPixKey` para isso. Os Unreserved Templates (IDs 80 a 99) são ignorados: o "QR Code composto" do Pix Automático (Pix recorrente) grava neles a localização de recorrência e, quando esse payload também traz uma localização de pagamento em 26-25, como no exemplo composto do manual do Pix, ele é aceito e lido como um payload dinâmico comum, com a localização de recorrência descartada. Só um payload sem nenhum template Pix nos IDs 26 a 51 é considerado inválido. +Valida se um payload de BR Code Pix (a string por trás de um QR Code Pix e do "Pix copia e cola") é válido: estrutura TLV bem formada, objetos obrigatórios presentes, um dos templates "Merchant Account Information" carregando o GUI `br.gov.bcb.pix` junto com uma chave ou uma URL, e um CRC-16 que confere. O objeto "Point of Initiation Method" (`01`) é informativo: o Manual do BR Code o marca como opcional e só atribui significado ao valor `"12"` ("só pode ser utilizado uma vez"), então ele pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` torna o payload inválido. Quando um payload construído em torno de uma chave traz um valor (`54`), esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque, ou seja, a menos que traga o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`) como prescreve o §2.6 do manual do Pix; rejeitar `"0"`/`"0.00"` sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. Um `fss` escrito ao lado de uma localização de PSP torna o payload inválido: o §2.7 do Manual de Padrões para Iniciação do Pix mapeia o QR Code dinâmico para exatamente dois subobjetos, `00` (GUI) e `25` (URL), e o `fss` pertence ao template estático do §2.6. A chave em si não é validada contra os formatos do DICT, use `isValidPixKey` para isso. Os Unreserved Templates (IDs 80 a 99) são ignorados: o "QR Code composto" do Pix Automático (Pix recorrente) grava em um deles a localização de recorrência e, quando esse payload também traz uma localização de pagamento em 26-25, como no exemplo composto do manual do Pix, ele é aceito e lido como um payload dinâmico comum, com a localização de recorrência descartada. Só um payload sem nenhum template Pix nos IDs 26 a 51 é considerado inválido. ```javascript import { isValidPixPayload } from '@brazilian-utils/brazilian-utils'; @@ -224,7 +224,7 @@ isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (CRC quebrado) ## parsePixPayload -Interpreta um payload de BR Code Pix e retorna seus campos. O payload é validado pelo `isValidPixPayload` primeiro, então uma estrutura malformada, um CRC quebrado ou um objeto obrigatório ausente retornam `null` em vez de um resultado parcial. Um payload estático vem com `key`, um dinâmico com `url`. O resultado é tipado como `PixPayload`; `pointOfInitiation` está sempre presente e é tipado como `PixPointOfInitiation`, `"dynamic"` quando o payload traz uma localização de PSP ou quando o objeto "Point of Initiation Method" (`01`) é `"12"`, e `"static"` nos demais casos. As informações da conta do recebedor devem trazer exatamente uma chave ou uma `url` (verificada com a mesma regra de localização de PSP do `generatePixPayload`); o próprio `01` é informativo, então pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` retorna `null`. Quando um payload construído em torno de uma chave traz um valor, esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque: o §2.6 do manual do Pix coloca o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`), devolvido como `withdrawalFacilitator`, e `54` igual a `"0"` ou `"0.00"` é aceito junto dele. Rejeitar um valor zero sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. Um `fss` escrito ao lado de uma localização de PSP retorna `null`: o §2.7 do Manual de Padrões para Iniciação do Pix mapeia o QR Code dinâmico para exatamente dois subobjetos, `00` (GUI) e `25` (URL), e o `fss` pertence ao template estático do §2.6. Quando o payload traz uma localização de PSP, o valor e o `txid` são ignorados, como o manual determina. Os Unreserved Templates (IDs 80 a 99) são ignorados: um "QR Code composto" do Pix Automático que também traga uma localização de pagamento em 26-25 é interpretado como um payload dinâmico comum e sua localização de recorrência é descartada, então quem precisa distinguir os dois não pode se apoiar neste parser. Só um payload sem nenhum template Pix nos IDs 26 a 51 retorna `null`. +Interpreta um payload de BR Code Pix e retorna seus campos. O payload é validado pelo `isValidPixPayload` primeiro, então uma estrutura malformada, um CRC quebrado ou um objeto obrigatório ausente retornam `null` em vez de um resultado parcial. Um payload estático vem com `key`, um dinâmico com `url`. O resultado é tipado como `PixPayload`; `pointOfInitiation` está sempre presente e é tipado como `PixPointOfInitiation`, `"dynamic"` quando o payload traz uma localização de PSP ou quando o objeto "Point of Initiation Method" (`01`) é `"12"`, e `"static"` nos demais casos. As informações da conta do recebedor devem trazer exatamente um entre uma chave e uma `url` (verificada com a mesma regra de localização de PSP do `generatePixPayload`); o próprio `01` é informativo, então pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` retorna `null`. Quando um payload construído em torno de uma chave traz um valor, esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque: o §2.6 do manual do Pix coloca o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`), devolvido como `withdrawalFacilitator`, e `54` igual a `"0"` ou `"0.00"` é aceito junto dele. Rejeitar um valor zero sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. Um `fss` escrito ao lado de uma localização de PSP retorna `null`: o §2.7 do Manual de Padrões para Iniciação do Pix mapeia o QR Code dinâmico para exatamente dois subobjetos, `00` (GUI) e `25` (URL), e o `fss` pertence ao template estático do §2.6. Quando o payload traz uma localização de PSP, o valor e o `txid` são ignorados, como o manual determina. Os Unreserved Templates (IDs 80 a 99) são ignorados: um "QR Code composto" do Pix Automático que também traga uma localização de pagamento em 26-25 é interpretado como um payload dinâmico comum e sua localização de recorrência é descartada, então quem precisa distinguir os dois não pode se apoiar neste parser. Só um payload sem nenhum template Pix nos IDs 26 a 51 retorna `null`. ```javascript import { parsePixPayload } from '@brazilian-utils/brazilian-utils'; @@ -272,7 +272,7 @@ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // nu Valida se uma chave de acesso de DF-e (Documento Fiscal eletrônico) é válida. Cobre todos os documentos cuja chave de acesso é a mesma string de 44 dígitos: NF-e (modelo 55), NFC-e (65), CT-e (57, o Conhecimento de Transporte Eletrônico instituído pela cláusula primeira do [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, o Conhecimento de Transporte Eletrônico para Outros Serviços instituído pela cláusula primeira do [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, o CT-e Guia de Transporte de Valores instituído pela cláusula primeira do [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) e NFCom (62). O CF-e-SAT (59) fica de fora: sua "chave de consulta" de 44 posições é composta de outro jeito. Aceita espaços entre os grupos de dígitos (a máscara de exibição usual) e os prefixos `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` e `NFCom` encontrados no atributo `Id` do XML do documento. -A forma de emissão (`tpEmis`) é conferida contra os códigos que o MOC daquele modelo atribui, então o conjunto aceito muda com o modelo: de 1 a 7 e 9 para NF-e e NFC-e, `{1, 3, 4, 5, 7, 8}` para o CT-e, `{1, 5, 7, 8}` para o CT-e OS, `{1, 2, 7, 8}` para a GTV-e, `{1, 2, 3}` para o MDF-e e `{1, 2}` para o BP-e, a NF3e e a NFCom. O código 8, a autorização pela SVC-SP, é atribuído somente pelo [MOC do CT-e 4.00](https://dfe-portal.svrs.rs.gov.br/CTE/Documentos), nunca pelo da NF-e; os domínios do [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), da [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) e da [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) vêm dos manuais deles. Para NF-e e NFC-e o código numérico também é conferido contra a regra B03-10 do MOC da NF-e, que proíbe os vinte valores repetidos e sequenciais de `cNF` que ela lista e um `cNF` igual ao número do documento. Já um número de documento todo zerado é recusado em todos os modelos seguindo o leiaute, não por escolha desta biblioteca: o `tiposBasico_v4.00.xsd` do [pacote de schemas da NF-e](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) tipa o `nNF` como `TNF`, cujo pattern é `[1-9]{1}[0-9]{0,8}`, e o Anexo I de cada um dos outros modelos repete o mesmo regex no seu próprio campo de número. +O tipo de emissão (`tpEmis`) é conferido contra os códigos que o MOC daquele modelo atribui, então o conjunto aceito muda com o modelo: de 1 a 7 e 9 para NF-e e NFC-e, `{1, 3, 4, 5, 7, 8}` para o CT-e, `{1, 5, 7, 8}` para o CT-e OS, `{1, 2, 7, 8}` para a GTV-e, `{1, 2, 3}` para o MDF-e e `{1, 2}` para o BP-e, a NF3e e a NFCom. O código 8, a autorização pela SVC-SP, é atribuído somente pelo [MOC do CT-e 4.00](https://dfe-portal.svrs.rs.gov.br/CTE/Documentos), nunca pelo da NF-e; os domínios do [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), da [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) e da [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) vêm dos manuais deles. Para NF-e e NFC-e o código numérico também é conferido contra a regra B03-10 do MOC da NF-e, que proíbe os vinte valores repetidos e sequenciais de `cNF` que ela lista e um `cNF` igual ao número do documento. Já um número de documento todo zerado é recusado em todos os modelos seguindo o leiaute, não por escolha desta biblioteca: o `tiposBasico_v4.00.xsd` do [pacote de schemas da NF-e](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) tipa o `nNF` como `TNF`, cujo pattern é `[1-9]{1}[0-9]{0,8}`, e o Anexo I de cada um dos outros modelos repete o mesmo regex no seu próprio campo de número. ```javascript import { isValidNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -317,7 +317,7 @@ parseNfeKey('invalid'); // null ## isValidEmail -Valida se email é válido. +Valida se email é válido. O conjunto aceito é um subconjunto prático da definição de [endereço de e-mail válido](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) do HTML da WHATWG, e não da [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322). A parte local é limitada a letras, dígitos e `_'+-.`, e não pode começar com ponto, terminar com ponto ou apóstrofo, nem conter dois pontos seguidos. O domínio precisa ter pelo menos um ponto, e cada rótulo separado por ponto segue a produção `[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?` da WHATWG, então um rótulo não pode começar nem terminar com hífen nem passar de 63 caracteres; o rótulo final é alfabético e tem de 2 a 63 letras, então `user@example.c1` é rejeitado. Partes locais entre aspas (`"john doe"@example.com`) e literais de endereço (`john@[127.0.0.1]`) são rejeitadas. ```javascript import { isValidEmail } from '@brazilian-utils/brazilian-utils'; @@ -327,7 +327,7 @@ isValidEmail('john.doe@hotmail.com'); // true ## isValidPhone -Valida se o número de telefone (celular ou residencial) é válido. Um código de país brasileiro (`+55`, `0055` ou um `55` isolado) é aceito e removido antes da validação, seguindo a regra documentada em `parsePhone`. `options.accept` (tipado como `PhoneType[]`, parte de `IsValidPhoneOptions`) define quais tipos de número são aceitos e tem como padrão `['mobile', 'landline']`; adicione `'service'` para também aceitar os números não geográficos reconhecidos por `isValidServicePhone`, ou informe `[]` para não aceitar nenhum. `options.version` (tipado como `PhoneVersion`, parte do mesmo tipo) é repassado ao `isValidMobilePhone` e escolhe qual regra de numeração celular é aplicada: `1` (padrão) o formato antigo, cujo primeiro dígito do número pode ser 6, 7, 8 ou 9, e `2` o atual, que exige 9 e rejeita o prefixo `700`. Vale apenas para celulares; números residenciais e de serviço não são afetados. +Valida se o número de telefone (celular ou fixo) é válido. Um código de país brasileiro (`+55`, `0055` ou um `55` isolado) é aceito e removido antes da validação, seguindo a regra documentada em `parsePhone`. `options.accept` (tipado como `PhoneType[]`, parte de `IsValidPhoneOptions`) define quais tipos de número são aceitos e tem como padrão `['mobile', 'landline']`; adicione `'service'` para também aceitar os números não geográficos reconhecidos por `isValidServicePhone`, ou informe `[]` para não aceitar nenhum. `options.version` (tipado como `PhoneVersion`, parte do mesmo tipo) é repassado ao `isValidMobilePhone` e escolhe qual regra de numeração celular é aplicada: `1` (padrão) o formato antigo, cujo primeiro dígito do número pode ser 6, 7, 8 ou 9, e `2` o atual, que exige 9 e rejeita o prefixo `700`. Vale apenas para celulares; números fixos e de serviço não são afetados. ```javascript import { isValidPhone } from '@brazilian-utils/brazilian-utils'; @@ -387,7 +387,7 @@ isValidMobilePhone('11712345678', { version: 2 }); // false (v2 exige 9 como pri ## isValidLandlinePhone -Valida se o número de telefone residencial é válido. +Valida se o número de telefone fixo é válido. ```javascript import { isValidLandlinePhone } from '@brazilian-utils/brazilian-utils'; @@ -397,7 +397,7 @@ isValidLandlinePhone('1130000000'); // true ## isValidServicePhone -Valida se um número de telefone é um número de serviço brasileiro válido, discado sem DDD: os Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` e `0900` (11 dígitos no total), os números abreviados `300X`/`400X` (8 dígitos), e os códigos de 3 dígitos dos Códigos de Acesso a Serviços de Utilidade Pública designados pela Anatel (ex.: `190`, `192`), cuja tabela consolidada é o Anexo do [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151). O `112` e o `911` são rejeitados: a Anatel não designa nenhum dos dois, e o `911` sequer está dentro da faixa `1N₂N₁` que o art. 13 da [Resolução nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749) destina aos serviços de utilidade pública, então o encaminhamento deles nos aparelhos é uma convenção GSM, não uma designação de numeração. Apenas a estrutura é verificada, o número não precisa estar atribuído a ninguém. A Anatel retirou os códigos de 4 caracteres em vez de alocá-los (o art. 43 I da [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86) e o art. 2º II do Ato acima mandaram liberá-los), então apenas as raízes convencionais `300X` e `400X` são reconhecidas: outros prefixos de "Número Único" usados no mercado, como `4020` e `4062`, estão fora de escopo e são rejeitados. +Valida se um número de telefone é um número de serviço brasileiro válido, discado sem DDD: os Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` e `0900` (11 dígitos no total, então a forma curta e extinta de `0800` + 6 dígitos é rejeitada), os números abreviados `300X`/`400X` (8 dígitos), e os códigos de 3 dígitos dos Códigos de Acesso a Serviços de Utilidade Pública designados pela Anatel (ex.: `190`, `192`), cuja tabela consolidada é o Anexo do [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151). O `112` e o `911` são rejeitados: a Anatel não designa nenhum dos dois, e o `911` sequer está dentro da faixa `1N₂N₁` que o art. 13 da [Resolução nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749) destina aos serviços de utilidade pública, então o encaminhamento deles nos aparelhos é uma convenção GSM, não uma designação de numeração. Apenas a estrutura é verificada: o número não precisa estar atribuído a ninguém, e a regra do `0500` que codifica o valor da doação nos dois últimos dígitos não é aplicada. A Anatel retirou os códigos de 4 dígitos em vez de alocá-los (o art. 43 I da [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86) e o art. 2º II do Ato acima mandaram liberá-los), então apenas as raízes convencionais `300X` e `400X` são reconhecidas: outros prefixos de "Número Único" usados no mercado, como `4020` e `4062`, estão fora de escopo e são rejeitados. ```javascript import { isValidServicePhone } from '@brazilian-utils/brazilian-utils'; @@ -490,7 +490,7 @@ isValidPis('12056412547'); // false ## formatPis -Formata número de PIS. `options.pad` (parte de `FormatPisOptions`) completa o valor com zeros à esquerda até os 11 dígitos antes de aplicar a máscara. +Formata número de PIS. `options.pad` (parte de `FormatPisOptions`) completa o valor com zeros à esquerda até os 11 dígitos antes de aplicar a máscara (padrão `false`). ```javascript import { formatPis } from '@brazilian-utils/brazilian-utils'; @@ -511,7 +511,7 @@ parsePis('123.45678.90-1'); // 12345678901 ## formatCep -Formata o CEP ([código de endereçamento postal](https://pt.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (parte de `FormatCepOptions`) completa o valor com zeros à esquerda até os 8 dígitos antes de aplicar a máscara. +Formata o CEP ([código de endereçamento postal](https://pt.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (parte de `FormatCepOptions`) completa o valor com zeros à esquerda até os 8 dígitos antes de aplicar a máscara (padrão `false`). ```javascript import { formatCep } from '@brazilian-utils/brazilian-utils'; @@ -532,7 +532,7 @@ parseCep('92500-000'); // 92500000 ## getAddressInfoByCep -Busca informações de endereço para um CEP usando múltiplos provedores. O padrão é `['viacep', 'brasilapi']`. O provedor `'widenet'` está descontinuado (seu endpoint não responde mais) e foi excluído da lista padrão, mas ainda pode ser solicitado explicitamente via `options.providers` (tipado como `CepProvider[]`). O endereço retornado é tipado como `AddressInfo`. Uma falha transitória de rede é repetida duas vezes por provedor, com backoff linear de 250 ms (250 ms e depois 500 ms), então um provedor que continua falhando é tentado até 3 vezes e acrescenta cerca de 750 ms antes de a sua própria falha se concretizar; um status de erro HTTP ou uma falha não recuperável não é repetida. Os provedores são disparados juntos e disputados com `Promise.any`, não consultados um após o outro, então essas tentativas não atrasam nada para os demais provedores, apenas o momento em que uma rejeição por falha de todos pode aparecer. Um `options.providers` que não nomeia nenhum provedor conhecido rejeita com `GetAddressInfoByCepValidationError` ("Nenhum provedor válido especificado"): um array vazio, um array de nomes desconhecidos e um valor que não é um array, incluindo `null`. Com `providers: ['brasilapi']`, um CEP que a BrasilAPI não conhece rejeita com `GetAddressInfoByCepNotFoundError`, já que a BrasilAPI sinaliza a ausência com HTTP 404; qualquer outro status de erro continua sendo um `GetAddressInfoByCepServiceError`. +Busca informações de endereço para um CEP usando múltiplos provedores. O padrão é `['viacep', 'brasilapi']`. O provedor `'widenet'` está descontinuado (seu endpoint não responde mais) e foi excluído da lista padrão, mas ainda pode ser solicitado explicitamente via `options.providers` (tipado como `CepProvider[]`). O endereço retornado é tipado como `AddressInfo`. Uma falha transitória de rede é repetida duas vezes por provedor, com backoff linear de 250 ms (250 ms e depois 500 ms), então um provedor que continua falhando é tentado até 3 vezes e acrescenta cerca de 750 ms antes de a sua própria falha se concretizar; um status de erro HTTP ou uma falha não recuperável não é repetida. Os provedores são disparados juntos e disputados com `Promise.any`, não consultados um após o outro, então essas tentativas não atrasam nada para os demais provedores, apenas o momento em que uma rejeição por falha de todos pode aparecer. Um `options.providers` que não nomeia nenhum provedor conhecido rejeita com `GetAddressInfoByCepValidationError` ("Nenhum provedor válido especificado"): um array vazio, um array de nomes desconhecidos e um valor que não é um array, incluindo `null`. Com `providers: ['brasilapi']`, um CEP que a BrasilAPI não conhece rejeita com `GetAddressInfoByCepNotFoundError`, já que a BrasilAPI sinaliza a ausência com HTTP 404; qualquer outro status de erro continua sendo um `GetAddressInfoByCepServiceError`. Os três estendem `GetAddressInfoByCepError`, a classe base de todos os erros com que este utilitário rejeita, então um único `catch` nela cobre todos. ```javascript import { getAddressInfoByCep } from '@brazilian-utils/brazilian-utils'; @@ -564,7 +564,7 @@ isValidProcessoJuridico('ab00020802520125150049'); // false (letras são rejeita ## formatProcessoJuridico -Formata um número no formato definido pelo [CNJ](https://atos.cnj.jus.br/atos/detalhar/119) (máscara `NNNNNNN-DD.AAAA.J.TR.OOOO`). `options.pad` (parte de `FormatProcessoJuridicoOptions`) completa o valor com zeros à esquerda até os 20 dígitos antes de aplicar a máscara. +Formata um número no formato definido pelo [CNJ](https://atos.cnj.jus.br/atos/detalhar/119) (máscara `NNNNNNN-DD.AAAA.J.TR.OOOO`). `options.pad` (parte de `FormatProcessoJuridicoOptions`) completa o valor com zeros à esquerda até os 20 dígitos antes de aplicar a máscara (padrão `false`). ```javascript import { formatProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -596,7 +596,7 @@ isValidIe('go', '109161793'); // true (case-insensitive) ## isValidBankAccount -Verifica se uma conta bancária brasileira é válida. O `bankCode` precisa estar na lista de participantes do STR publicada pelo Banco Central do Brasil (o mesmo dataset usado por `getBankByCode`), então um código não atribuído como `'999'` é sempre inválido. A partir daí o banco é validado de três formas: pelo algoritmo de dígito verificador publicado, apenas pela estrutura (o banco existe e a agência/conta respeitam a quantidade de dígitos documentada, para bancos que não publicam regra de dígito) ou pela verificação genérica mod10/mod11, que continua sendo o fallback para os demais bancos da lista. +Verifica se uma conta bancária brasileira é válida. O `bankCode` precisa estar na lista de participantes do STR publicada pelo Banco Central do Brasil (o mesmo dataset usado por `getBankByCode`), então um código não atribuído como `'999'` é sempre inválido. A partir daí o banco é validado de uma de três formas: pelo algoritmo de dígito verificador publicado, apenas pela estrutura (o banco existe e a agência/conta respeitam a quantidade de dígitos documentada, para bancos que não publicam regra de dígito) ou pela verificação genérica mod10/mod11, que continua sendo o fallback para os demais bancos da lista. Bancos validados pelo algoritmo de dígito verificador publicado: @@ -727,7 +727,7 @@ getBankByCode('999'); // null ## getBankByIspb -Busca um banco brasileiro pelo seu ISPB (Identificador do Sistema de Pagamentos Brasileiro), o código de 8 dígitos publicado pelo Banco Central do Brasil na [lista de participantes do STR](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). Todo participante do SPB tem um ISPB, mas este conjunto de dados só traz as instituições que também têm código COMPE, então um ISPB cuja instituição não tem código COMPE próprio retorna `null`. Aceita tanto `string` quanto `number`, com ou sem zeros à esquerda. Retorna uma nova cópia (tipada como `Bank`) do banco correspondente, ou `null` quando nenhum banco tem esse ISPB. +Busca um banco brasileiro pelo seu ISPB (Identificador do Sistema de Pagamentos Brasileiro), o código de 8 dígitos publicado pelo Banco Central do Brasil na [lista de participantes do STR](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). Todo participante do SPB tem um ISPB, mas este conjunto de dados só traz as instituições que também têm código COMPE, então um ISPB cuja instituição não tem código COMPE próprio retorna `null`. Aceita tanto `string` quanto `number`, com ou sem zeros à esquerda, então `getBankByIspb(0)` encontra o mesmo banco que `getBankByIspb('00000000')`. O conjunto de dados é gerado a partir desse CSV, recorrendo à [BrasilAPI](https://brasilapi.com.br/api/banks/v1) quando a requisição ao Bacen falha. Retorna uma nova cópia (tipada como `Bank`) do banco correspondente, ou `null` quando nenhum banco tem esse ISPB. ```javascript import { getBankByIspb } from '@brazilian-utils/brazilian-utils'; @@ -851,7 +851,7 @@ formatCurrency(Number.NaN); // "" (números não finitos viram string vazia) ## parseCurrency -Transforma uma string para o formato de inteiro ou float. O último `,` ou `.` seguido de 1 ou 2 dígitos (ou de até `precision` dígitos, quando esse valor for maior) é o separador decimal; todo outro `,` ou `.` é separador de milhar. Assim `'R$ 1.234,56'` vira `1234.56`, `'R$ 1.234'` vira `1234`, `'1,5'` vira `1.5` e `'12.34'` vira `12.34`. Um valor escrito sem nenhum separador mantém a convenção de centavos e é dividido por `10 ** precision`, então `'1234'` vira `12.34`. Um `-` escrito antes do primeiro dígito é preservado, então `'-R$ 1,00'` vira `-1`. `precision` (padrão 2, limitado a `0..20`, e voltando a 2 quando não é um número finito) controla quantos dígitos são tratados como centavos. As opções são tipadas como `ParseCurrencyOptions`. +Transforma uma string para o formato de inteiro ou float. O último `,` ou `.` seguido de 1 ou 2 dígitos (ou de até `precision` dígitos, quando esse valor for maior) é o separador decimal; todo outro `,` ou `.` é separador de milhar. Assim `'R$ 1.234,56'` vira `1234.56`, `'R$ 1.234'` vira `1234`, `'1,5'` vira `1.5` e `'12.34'` vira `12.34`. Um valor escrito sem nenhum separador mantém a convenção de centavos e é dividido por `10 ** precision`, então `'1234'` vira `12.34`. Um `-` escrito antes do primeiro dígito é preservado, então `'-R$ 1,00'` vira `-1`. `precision` (padrão 2, limitado a `0..20`, e voltando a 2 quando não é um número finito) controla quantos dígitos são tratados como subunidades monetárias. As opções são tipadas como `ParseCurrencyOptions`. ```javascript import { parseCurrency } from '@brazilian-utils/brazilian-utils'; @@ -958,7 +958,7 @@ getStateByIbgeCode(3.5); // null ## getStateCodeByName -Retorna a sigla de um estado brasileiro a partir do nome completo. A comparação ignora acentos, maiúsculas/minúsculas e espaços nas pontas, então `'sao paulo'`, `'SÃO PAULO'` e `' São Paulo '` resolvem para `'SP'`. Exporta o tipo `StateCode`. +Retorna a sigla de um estado brasileiro a partir do nome completo. A comparação ignora acentos, maiúsculas/minúsculas e espaços nas pontas, então `'sao paulo'`, `'SÃO PAULO'` e `' São Paulo '` resolvem para `'SP'`. Toda sequência de espaços internos também vira um único espaço, então `'Rio de Janeiro'` resolve para `'RJ'`, enquanto um nome escrito sem o espaço não corresponde a nada (`'saopaulo'` não é `'São Paulo'`). Exporta o tipo `StateCode`. ```javascript import { getStateCodeByName } from '@brazilian-utils/brazilian-utils'; @@ -1040,7 +1040,7 @@ getCities('SP'); ## getHolidays -Retorna feriados brasileiros para um determinado ano. Retorna feriados nacionais e opcionalmente feriados estaduais. Cada feriado (tipado como `Holiday`) tem um campo `type` (`HolidayType`: `"national"`, `"state"`, `"optional"` ou `"religious"`). O "Dia da Consciência Negra" (20 de novembro) é feriado nacional a partir de 2024 (Lei nº 14.759/2023). Antes disso, MT e RJ ainda trazem seu próprio feriado estadual chamado `"Consciência Negra"` na mesma data. Os resultados são memoizados por `year`/`stateCode`, mas cada chamada ainda retorna uma cópia nova. Um `stateCode` desconhecido/inválido é ignorado, retornando apenas os feriados nacionais; a busca lê apenas propriedades próprias, então `"__proto__"`, `"constructor"` e afins são códigos desconhecidos como qualquer outro, e não uma exceção. +Retorna feriados brasileiros para um determinado ano. Retorna feriados nacionais e opcionalmente feriados estaduais. Cada feriado (tipado como `Holiday`) tem um campo `type` (`HolidayType`: `"national"`, `"state"`, `"optional"` ou `"religious"`). O "Dia da Consciência Negra" (20 de novembro) é feriado nacional a partir de 2024 (Lei nº 14.759/2023). Antes disso, vários estados ainda trazem um feriado estadual próprio na mesma data: `"Consciência Negra"` em MT e RJ, `"Dia Estadual da Consciência Negra"` no AP e `"Dia da Consciência Negra"` no AM e em SP. Os resultados são memoizados por `year`/`stateCode`, mas cada chamada ainda retorna uma cópia nova. Um `stateCode` desconhecido/inválido é ignorado, retornando apenas os feriados nacionais; a busca lê apenas propriedades próprias, então `"__proto__"`, `"constructor"` e afins são códigos desconhecidos como qualquer outro, e não uma exceção. Só os anos de 1900 a 2099 são suportados, o intervalo que os utilitários de dias úteis herdam; um ano fora dele retorna `[]`. Apenas um feriado estadual por UF é feriado civil pela [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm), art. 1º, II, que autoriza "a data magna do Estado fixada em lei estadual", no singular; as demais entradas se apoiam em leis estaduais ordinárias e são reportadas por serem observadas na prática. Regras notáveis por estado: @@ -1129,7 +1129,7 @@ generateCep(); // '92500000' ## formatCnh -Formata a CNH. `options.pad` (parte de `FormatCnhOptions`) completa o valor com zeros à esquerda até os 11 dígitos antes de aplicar a máscara. +Formata a CNH. `options.pad` (parte de `FormatCnhOptions`) completa o valor com zeros à esquerda até os 11 dígitos antes de aplicar a máscara (padrão `false`). ```javascript import { formatCnh } from '@brazilian-utils/brazilian-utils'; @@ -1140,7 +1140,7 @@ formatCnh('2650306461', { pad: true }); // 026503064-61 ## isValidCnh -Valida se a CNH é válida. Espaços, pontos e hífens ao redor/entre os dígitos são ignorados, mas qualquer outro caractere, uma letra em especial, invalida o valor. +Valida se a CNH é válida. Espaços, pontos e hífens ao redor/entre os dígitos são ignorados, mas qualquer outro caractere, uma letra em especial, invalida o valor. Um valor cujos 11 dígitos são todos iguais é rejeitado antes do cálculo dos dígitos verificadores, então `'11111111111'` é inválido. ```javascript import { isValidCnh } from '@brazilian-utils/brazilian-utils'; @@ -1162,7 +1162,7 @@ generateCnh(); // '02650306461' ## parseCnh -Remove a formatação da CNH, mantém apenas os dígitos e limita o resultado a 11 dígitos. +Remove a formatação da CNH, mantém apenas os dígitos e limita o resultado a 11 dígitos. Retorna `''` quando não há nenhum dígito. ```javascript import { parseCnh } from '@brazilian-utils/brazilian-utils'; @@ -1295,7 +1295,7 @@ generatePhone('service'); // '08001234567' ou '40041234' ## formatLicensePlate -Formata uma placa. Placas antigas brasileiras (`LLLNNNN`) são retornadas com hífen e placas Mercosul (`LLLNLNN`) permanecem normalizadas. +Formata uma placa. Placas antigas brasileiras (`LLLNNNN`) são retornadas com hífen e placas Mercosul (`LLLNLNN`) permanecem normalizadas. Valores parciais são formatados até onde os caracteres informados alcançarem, então também pode ser usada como máscara de digitação, e um valor que não pode iniciar uma placa válida retorna `''`. ```javascript import { formatLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -1367,7 +1367,7 @@ generatePis(); // '91077906857' ## getMunicipality -Busca informações de município por código IBGE, ou obtém o código IBGE a partir do nome do município e UF. Uma única função cobre as duas direções, dependendo se `options` tem `code` ou `municipalityName`/`uf`. `code` aceita tanto `string` quanto `number` e deve ter exatamente 7 dígitos, caso contrário a função resolve para `null`. Um `code` informado como número precisa ser um inteiro não negativo: sinal e ponto decimal não são dígitos, então `-3550308` e `355030.8` resolvem para `null` em vez de serem lidos como `3550308`. A resolução é totalmente offline, a partir de um dataset do IBGE embutido na biblioteca: nenhuma requisição de rede é feita. A comparação do nome do município ignora acentos e diferenças entre maiúsculas/minúsculas. Um município desconhecido, uma UF desconhecida ou uma entrada inválida resolvem para `null`. O par `[name, uf]` é um array novo a cada chamada, então alterar o resultado nunca afeta as buscas seguintes. +Busca informações de município por código IBGE, ou obtém o código IBGE a partir do nome do município e UF. Uma única função cobre as duas direções, dependendo se `options` tem `code` ou `municipalityName`/`uf`. `code` aceita tanto `string` quanto `number` e deve ter exatamente 7 dígitos, caso contrário a função resolve para `null`. Um `code` informado como número precisa ser um inteiro não negativo: sinal e ponto decimal não são dígitos, então `-3550308` e `355030.8` resolvem para `null` em vez de serem lidos como `3550308`. A resolução é totalmente offline, a partir de um dataset do IBGE embutido na biblioteca: nenhuma requisição de rede é feita. A comparação do nome do município ignora acentos e diferenças entre maiúsculas/minúsculas, e toda sequência de espaços vira um único espaço, então `'sao paulo'` corresponde a `'São Paulo'`, enquanto um nome escrito sem o espaço não; a caixa é convertida para maiúsculas, a direção em que o Unicode expande `'ß'` para `'SS'`, então `'Paßos'` corresponde a `'Passos'`. Um município desconhecido, uma UF desconhecida ou uma entrada inválida resolvem para `null`. O par `[name, uf]` é um array novo a cada chamada, então alterar o resultado nunca afeta as buscas seguintes. ```javascript import { getMunicipality } from '@brazilian-utils/brazilian-utils'; @@ -1476,7 +1476,7 @@ isHoliday(); // false ## isBusinessDay -Verifica se uma data é um dia útil no Brasil. Retorna `false` para sábados, domingos e feriados brasileiros retornados por `getHolidays` para a data local de `value` (ano/mês/dia lidos localmente), a mesma convenção usada por `isHoliday`. `options.includeOptional` (parte de `BusinessDayOptions`, o tipo de opções que todos os utilitários de dias úteis compartilham) tem valor padrão `true`, então feriados do tipo opcional (`Holiday.type === "optional"`, ou seja, Carnaval e Corpus Christi) também contam como dias não úteis; passe `false` para considerar apenas os feriados estatutários. `options.stateCode` também considera os feriados daquele estado; uma string que não é um código de estado conhecido é ignorada, retornando apenas os feriados nacionais, enquanto um `stateCode` presente que não é uma string (um número, `null`, um objeto) é rejeitado e faz a chamada retornar `false` mesmo em um dia de semana comum — a mesma distinção que `isHoliday` faz, e o valor que `addBusinessDays`, `subBusinessDays` e `differenceInBusinessDays` rejeitam com `null`. Um `value` que não é um `Date` válido retorna `false`. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele retorna `false`. +Verifica se uma data é um dia útil no Brasil. Retorna `false` para sábados, domingos e feriados brasileiros retornados por `getHolidays` para a data local de `value` (ano/mês/dia lidos localmente), a mesma convenção usada por `isHoliday`. `options.includeOptional` (parte de `BusinessDayOptions`, o tipo de opções que todos os utilitários de dias úteis compartilham) tem valor padrão `true`, então feriados do tipo opcional (`Holiday.type === "optional"`, ou seja, Carnaval e Corpus Christi) também contam como dias não úteis; passe `false` para considerar apenas os feriados estatutários. `options.stateCode` também considera os feriados daquele estado; uma string que não é um código de estado conhecido é ignorada, considerando apenas os feriados nacionais, enquanto um `stateCode` presente que não é uma string (um número, `null`, um objeto) é rejeitado e faz a chamada retornar `false` mesmo em um dia de semana comum — a mesma distinção que `isHoliday` faz, e o valor que `addBusinessDays`, `subBusinessDays` e `differenceInBusinessDays` rejeitam com `null`. Um `value` que não é um `Date` válido retorna `false`. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele retorna `false`. ```javascript import { isBusinessDay } from '@brazilian-utils/brazilian-utils'; @@ -1501,7 +1501,7 @@ import { addBusinessDays } from '@brazilian-utils/brazilian-utils'; addBusinessDays(new Date(2024, 0, 2, 12), 1); // Date, 2024-01-03 12:00 (o dia seguinte já é útil) addBusinessDays(new Date(2024, 11, 31, 12), 1); // Date, 2025-01-02 12:00 (2025-01-01 é Ano novo, pulado) addBusinessDays(new Date(2024, 0, 5, 12), -1); // Date, 2024-01-04 12:00 (anda para trás) -addBusinessDays(new Date(2024, 0, 6, 12), 0); // Date, 2024-01-06 12:00 (sem alteração, mesmo sendo sábado) +addBusinessDays(new Date(2024, 0, 6, 12), 0); // Date, 2024-01-06 12:00 (sem alteração, mesmo o sábado não sendo dia útil) addBusinessDays(new Date(2024, 6, 8, 12), 1, { stateCode: 'SP' }); // Date, 2024-07-10 12:00 (2024-07-09 é a Revolução Constitucionalista em SP, pulado) addBusinessDays(new Date('not a date'), 1); // null addBusinessDays(new Date(2024, 0, 2), 1.5); // null (não é um número inteiro) @@ -1518,7 +1518,7 @@ subBusinessDays(new Date(2024, 0, 5, 12), 1); // Date, 2024-01-04 12:00 (o dia a subBusinessDays(new Date(2024, 0, 8, 12), 1); // Date, 2024-01-05 12:00 (anda para trás passando pelo fim de semana) subBusinessDays(new Date(2025, 0, 2, 12), 1); // Date, 2024-12-31 12:00 (2025-01-01 é Ano novo, pulado) subBusinessDays(new Date(2024, 0, 5, 12), -1); // Date, 2024-01-08 12:00 (anda para frente) -subBusinessDays(new Date(2024, 0, 6, 12), 0); // Date, 2024-01-06 12:00 (sem alteração, mesmo sendo sábado) +subBusinessDays(new Date(2024, 0, 6, 12), 0); // Date, 2024-01-06 12:00 (sem alteração, mesmo o sábado não sendo dia útil) subBusinessDays(new Date(2024, 6, 10, 12), 1, { stateCode: 'SP' }); // Date, 2024-07-08 12:00 (2024-07-09 é a Revolução Constitucionalista em SP, pulado) subBusinessDays(new Date('not a date'), 1); // null subBusinessDays(new Date(2024, 0, 2), 1.5); // null (não é um número inteiro) @@ -1633,7 +1633,7 @@ formatCns('89010001', { pad: true }); // '000 0000 8901 0001' ## isValidCertidao -Verifica se a matrícula de uma certidão de registro civil (nascimento, casamento, óbito e os demais atos mantidos por uma serventia de registro civil das pessoas naturais) é válida. A matrícula tem 32 dígitos distribuídos em 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), e os dois dígitos verificadores usam módulo 11 com pesos ciclando de 2 a 10 e voltando por 0. Aceita os caracteres de máscara usuais e espaços entre e ao redor dos grupos. O layout é o publicado atualmente no [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023), com o inciso II e os §§ 1º e 3º a 5º na redação do Provimento CN nº 237/2026 e o restante do artigo, inclusive o § 2º, na do Provimento CN nº 182/2024; a própria matrícula foi instituída pelo já revogado [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311) e ganhou sua estrutura de dígitos no também revogado [Provimento CNJ nº 3/2009, art. 7º](https://atos.cnj.jus.br/atos/detalhar/1310). Os dígitos verificadores estão detalhados em [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e implementado pelo [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) e pelo [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). +Verifica se a matrícula de uma certidão de registro civil (nascimento, casamento, óbito e os demais atos mantidos por uma serventia de registro civil das pessoas naturais) é válida. A matrícula tem 32 dígitos distribuídos em 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), e os dois dígitos verificadores usam módulo 11 com os pesos ciclando de 2 a 10 e voltando por 0: o primeiro cálculo começa em 2 sobre os 30 dígitos da base, o segundo em 1 sobre os 31 dígitos que incluem o primeiro dígito verificador, e nos dois um resto 10 é lido como 1. Aceita os caracteres de máscara usuais e espaços entre e ao redor dos grupos. O layout é o publicado atualmente no [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023), com o inciso II e os §§ 1º e 3º a 5º na redação do Provimento CN nº 237/2026 e o restante do artigo, inclusive o § 2º, na do Provimento CN nº 182/2024; a própria matrícula foi instituída pelo já revogado [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311) e ganhou sua estrutura de dígitos no também revogado [Provimento CNJ nº 3/2009, art. 7º](https://atos.cnj.jus.br/atos/detalhar/1310). Os dígitos verificadores estão detalhados em [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e implementado pelo [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) e pelo [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). Os dígitos do serviço são fixos em `55`, o código que o [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) atribui ao registro civil das pessoas naturais, então uma matrícula com qualquer outro par na nona e décima posições é rejeitada por mais que os dígitos verificadores confiram. O dígito do tipo de livro sempre precisa nomear um dos nove tipos de livro (o mesmo `CertidaoType` retornado por `parseCertidao`), então uma matrícula cujo dígito é `0` é rejeitada por mais que os dígitos verificadores confiram, do mesmo jeito que `parseCertidao` devolve `null` para ela. `options.accept` (parte de `IsValidCertidaoOptions`) restringe ainda mais aos tipos listados; o padrão é aceitar todos os tipos, e um valor que não seja um array volta para esse padrão. Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. @@ -1690,7 +1690,7 @@ O resultado `Certidao` traz: ## formatCertidao -Formata a matrícula de uma certidão de registro civil na máscara impressa do Provimento, os 32 dígitos agrupados em 6 2 2 4 1 5 3 7 2 e separados por espaços. `options.pad` (parte de `FormatCertidaoOptions`) preenche o valor com zeros à esquerda até 32 dígitos. A máscara é a do [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. +Formata a matrícula de uma certidão de registro civil na máscara impressa do Provimento, os 32 dígitos agrupados em 6 2 2 4 1 5 3 7 2 e separados por espaços. `options.pad` (parte de `FormatCertidaoOptions`) preenche o valor com zeros à esquerda até 32 dígitos (padrão `false`). A máscara é a do [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. ```javascript import { formatCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1716,7 +1716,7 @@ isValidCei('000000000000'); // false (dígitos repetidos) ## formatCei -Formata um número de CEI (Cadastro Específico do INSS) na máscara usual `00.000.00000/00`, a mesma em que as implementações de referência do dígito verificador concordam (a Receita Federal não a publica). Formata progressivamente, até onde os dígitos informados alcançarem, então também pode ser usada como máscara de digitação. `options.pad` (parte de `FormatCeiOptions`) preenche a esquerda com zeros até 12 dígitos. +Formata um número de CEI (Cadastro Específico do INSS) na máscara usual `00.000.00000/00`, a mesma em que as implementações de referência do dígito verificador concordam (a Receita Federal não a publica). Formata progressivamente, até onde os dígitos informados alcançarem, então também pode ser usada como máscara de digitação. `options.pad` (parte de `FormatCeiOptions`) preenche à esquerda com zeros até 12 dígitos (padrão `false`). ```javascript import { formatCei } from '@brazilian-utils/brazilian-utils'; @@ -1742,7 +1742,7 @@ isValidCno('000000000000'); // false (dígitos repetidos) ## formatCno -Formata um número de CNO (Cadastro Nacional de Obras). O CNO manteve a numeração do CEI, então os dois compartilham a mesma máscara de 12 dígitos, `00.000.00000/00`, a mesma em que as implementações de referência do dígito verificador concordam (a Receita Federal não a publica). Formata progressivamente, até onde os dígitos informados alcançarem, então também pode ser usada como máscara de digitação. `options.pad` (parte de `FormatCnoOptions`) preenche a esquerda com zeros até 12 dígitos. +Formata um número de CNO (Cadastro Nacional de Obras). O CNO manteve a numeração do CEI, então os dois compartilham a mesma máscara de 12 dígitos, `00.000.00000/00`, a mesma em que as implementações de referência do dígito verificador concordam (a Receita Federal não a publica). Formata progressivamente, até onde os dígitos informados alcançarem, então também pode ser usada como máscara de digitação. `options.pad` (parte de `FormatCnoOptions`) preenche à esquerda com zeros até 12 dígitos (padrão `false`). ```javascript import { formatCno } from '@brazilian-utils/brazilian-utils'; @@ -1763,13 +1763,13 @@ isValidCaepf('293.118.610/001-84'); // true isValidCaepf('41142260000101'); // true isValidCaepf(29311861000184); // true isValidCaepf('29311861000185'); // false (dígitos verificadores inválidos) -isValidCaepf('00000000000000'); // false (dígitos verificadores inválidos) +isValidCaepf('00000000000000'); // false (dígitos da base repetidos) isValidCaepf('00000000000012'); // false (dígitos da base repetidos) ``` ## formatCaepf -Formata um número de CAEPF (Cadastro de Atividade Econômica da Pessoa Física) na máscara usual `000.000.000/000-00`, a mesma em que as fontes da regra do dígito verificador concordam (a Receita Federal não a publica). Formata progressivamente, até onde os dígitos informados alcançarem, então também pode ser usada como máscara de digitação. `options.pad` (parte de `FormatCaepfOptions`) preenche a esquerda com zeros até 14 dígitos. +Formata um número de CAEPF (Cadastro de Atividade Econômica da Pessoa Física) na máscara usual `000.000.000/000-00`, a mesma em que as fontes da regra do dígito verificador concordam (a Receita Federal não a publica). Formata progressivamente, até onde os dígitos informados alcançarem, então também pode ser usada como máscara de digitação. `options.pad` (parte de `FormatCaepfOptions`) preenche à esquerda com zeros até 14 dígitos (padrão `false`). ```javascript import { formatCaepf } from '@brazilian-utils/brazilian-utils'; @@ -1882,7 +1882,7 @@ getCnae('0111abc301'); // null (não é uma forma documentada) ## isValidNcm -Valida se um código NCM (Nomenclatura Comum do Mercosul) existe na tabela vigente publicada pelo Siscomex/MDIC. Aceita o código com ou sem a máscara de pontos, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 8 dígitos, ou a máscara `NNNN.NN.NN`, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. +Valida se um código NCM (Nomenclatura Comum do Mercosul) existe na tabela vigente publicada pelo Siscomex/MDIC. Aceita o código com ou sem a máscara de pontos, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 8 dígitos, ou a máscara `NNNN.NN.NN`, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. Um número não carrega zero à esquerda, então um código iniciado por `0` precisa ser informado como string: `isValidNcm(1012100)` é `false`, enquanto `isValidNcm('01012100')` é `true`. ```javascript import { isValidNcm } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/utilities.md b/docs/utilities.md index f1a741dc..076e9748 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -50,7 +50,7 @@ generateCpf('SP'); // the 9th digit is 8, the SP região fiscal code ## isValidCnpj -Check if CNPJ is valid. `options.version` (part of `IsValidCnpjOptions`) picks which format is accepted: `1` (default) the numeric-only format, `2` both the numeric and the alphanumeric one; any other value is read as `1`, the way `formatCnpj` and `parseCnpj` read it. The usual mask characters and whitespace are accepted in either version. +Check if CNPJ is valid. `options.version` (part of `IsValidCnpjOptions`) picks which format is accepted: `1` (default) the numeric-only format, `2` both the numeric and the alphanumeric one; any other value is read as `1`, the way `formatCnpj` and `parseCnpj` read it. The usual mask characters and whitespace are accepted in either version. Version `2` has no reserved-value list, because the Receita Federal manual defines none for the alphanumeric format: a repeated-character alphanumeric base (all `A`s, say) that passes the checksum is accepted, while the numeric reserved numbers are rejected under version `1`. ```javascript import { isValidCnpj } from '@brazilian-utils/brazilian-utils'; @@ -317,7 +317,7 @@ parseNfeKey('invalid'); // null ## isValidEmail -Check if email is valid. +Check if email is valid. The accepted set is a practical subset of the WHATWG HTML [valid e-mail address](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) definition, not of [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322). The local part is limited to letters, digits and `_'+-.`, and may not start with a dot, end with a dot or an apostrophe, or contain two dots in a row. The domain must carry at least one dot, and each dotted label follows the WHATWG production `[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?`, so a label may neither start nor end with a hyphen nor exceed 63 characters; the final label is alphabetic and 2 to 63 letters long, so `user@example.c1` is rejected. Quoted local parts (`"john doe"@example.com`) and address literals (`john@[127.0.0.1]`) are rejected. ```javascript import { isValidEmail } from '@brazilian-utils/brazilian-utils'; @@ -397,7 +397,7 @@ isValidLandlinePhone('1130000000'); // true ## isValidServicePhone -Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`), whose consolidated table is the Anexo of [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151). `112` and `911` are rejected: Anatel designates neither, and `911` is not even inside the `1N₂N₁` range art. 13 of [Resolução nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749) destines to public utility services, so the way handsets route them is a GSM convention rather than a numbering designation. Only the structure is checked, the number does not have to be assigned to anyone. Anatel withdrew the 4-digit codes instead of allocating them (art. 43 I of [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86) and art. 2º II of the Ato above both ordered them released), so only the conventional `300X` and `400X` roots are recognised: other "Número Único" carrier prefixes in market use, such as `4020` and `4062`, are out of scope and are rejected. +Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total, so the shorter, extinct `0800` + 6 digit form is rejected), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`), whose consolidated table is the Anexo of [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151). `112` and `911` are rejected: Anatel designates neither, and `911` is not even inside the `1N₂N₁` range art. 13 of [Resolução nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749) destines to public utility services, so the way handsets route them is a GSM convention rather than a numbering designation. Only the structure is checked: the number does not have to be assigned to anyone, and the `0500` rule that encodes a donation amount in the last two digits is not enforced. Anatel withdrew the 4-digit codes instead of allocating them (art. 43 I of [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86) and art. 2º II of the Ato above both ordered them released), so only the conventional `300X` and `400X` roots are recognised: other "Número Único" carrier prefixes in market use, such as `4020` and `4062`, are out of scope and are rejected. ```javascript import { isValidServicePhone } from '@brazilian-utils/brazilian-utils'; @@ -490,7 +490,7 @@ isValidPis('12056412547'); // false ## formatPis -Format PIS number. `options.pad` (part of `FormatPisOptions`) left-pads the value with zeros to the full 11 digits before masking. +Format PIS number. `options.pad` (part of `FormatPisOptions`) left-pads the value with zeros to the full 11 digits before masking (default `false`). ```javascript import { formatPis } from '@brazilian-utils/brazilian-utils'; @@ -511,7 +511,7 @@ parsePis('123.45678.90-1'); // 12345678901 ## formatCep -Format CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (part of `FormatCepOptions`) left-pads the value with zeros to the full 8 digits before masking. +Format CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (part of `FormatCepOptions`) left-pads the value with zeros to the full 8 digits before masking (default `false`). ```javascript import { formatCep } from '@brazilian-utils/brazilian-utils'; @@ -532,7 +532,7 @@ parseCep('92500-000'); // 92500000 ## getAddressInfoByCep -Fetch address information for a given CEP using multiple providers. Defaults to `['viacep', 'brasilapi']`. The `'widenet'` provider is deprecated (its endpoint no longer responds) and excluded from the default list, but it can still be requested explicitly via `options.providers` (typed as `CepProvider[]`). The resolved address is typed as `AddressInfo`. A transient network failure is retried twice per provider, with a 250 ms linear backoff (250 ms, then 500 ms), so a provider that keeps failing is tried up to 3 times and adds about 750 ms before its own failure lands; an HTTP error status or a non-retryable failure is not retried. The providers are started together and raced with `Promise.any`, not queried one after the other, so those retries delay nothing for the other providers, only the moment an all-failed rejection can surface. An `options.providers` that names no known provider rejects with `GetAddressInfoByCepValidationError` ("Nenhum provedor válido especificado"): an empty array, an array of unknown names, and a value that is not an array at all, `null` included. With `providers: ['brasilapi']`, a CEP BrasilAPI does not know rejects with `GetAddressInfoByCepNotFoundError`, since BrasilAPI signals a miss with HTTP 404; any other error status is still a `GetAddressInfoByCepServiceError`. +Fetch address information for a given CEP using multiple providers. Defaults to `['viacep', 'brasilapi']`. The `'widenet'` provider is deprecated (its endpoint no longer responds) and excluded from the default list, but it can still be requested explicitly via `options.providers` (typed as `CepProvider[]`). The resolved address is typed as `AddressInfo`. A transient network failure is retried twice per provider, with a 250 ms linear backoff (250 ms, then 500 ms), so a provider that keeps failing is tried up to 3 times and adds about 750 ms before its own failure lands; an HTTP error status or a non-retryable failure is not retried. The providers are started together and raced with `Promise.any`, not queried one after the other, so those retries delay nothing for the other providers, only the moment an all-failed rejection can surface. An `options.providers` that names no known provider rejects with `GetAddressInfoByCepValidationError` ("Nenhum provedor válido especificado"): an empty array, an array of unknown names, and a value that is not an array at all, `null` included. With `providers: ['brasilapi']`, a CEP BrasilAPI does not know rejects with `GetAddressInfoByCepNotFoundError`, since BrasilAPI signals a miss with HTTP 404; any other error status is still a `GetAddressInfoByCepServiceError`. All three extend `GetAddressInfoByCepError`, the base class of every error this util rejects with, so a single `catch` on it covers all of them. ```javascript import { getAddressInfoByCep } from '@brazilian-utils/brazilian-utils'; @@ -564,7 +564,7 @@ isValidProcessoJuridico('ab00020802520125150049'); // false (letters are rejecte ## formatProcessoJuridico -Format the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119) (mask `NNNNNNN-DD.AAAA.J.TR.OOOO`). `options.pad` (part of `FormatProcessoJuridicoOptions`) left-pads the value with zeros to the full 20 digits before masking. +Format the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119) (mask `NNNNNNN-DD.AAAA.J.TR.OOOO`). `options.pad` (part of `FormatProcessoJuridicoOptions`) left-pads the value with zeros to the full 20 digits before masking (default `false`). ```javascript import { formatProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -727,7 +727,7 @@ getBankByCode('999'); // null ## getBankByIspb -Look a Brazilian bank up by its ISPB (Identificador do Sistema de Pagamentos Brasileiro), the 8 digit code published by Banco Central do Brasil in the [STR participants list](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). Every SPB participant has an ISPB, but this dataset only carries the institutions that also have a COMPE code, so an ISPB whose institution has no COMPE code of its own returns `null`. Accepts both `string` and `number` input, with or without leading zeros. Returns a fresh copy (typed as `Bank`) of the matching bank, or `null` when no bank has that ISPB. +Look a Brazilian bank up by its ISPB (Identificador do Sistema de Pagamentos Brasileiro), the 8 digit code published by Banco Central do Brasil in the [STR participants list](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). Every SPB participant has an ISPB, but this dataset only carries the institutions that also have a COMPE code, so an ISPB whose institution has no COMPE code of its own returns `null`. Accepts both `string` and `number` input, with or without leading zeros, so `getBankByIspb(0)` finds the same bank as `getBankByIspb('00000000')`. The dataset is generated from that CSV, falling back to [BrasilAPI](https://brasilapi.com.br/api/banks/v1) when the Bacen request fails. Returns a fresh copy (typed as `Bank`) of the matching bank, or `null` when no bank has that ISPB. ```javascript import { getBankByIspb } from '@brazilian-utils/brazilian-utils'; @@ -958,7 +958,7 @@ getStateByIbgeCode(3.5); // null ## getStateCodeByName -Get the two-letter code (sigla) of a Brazilian state given its full name. The match is accent-insensitive, case-insensitive and ignores leading/trailing whitespace, so `'sao paulo'`, `'SÃO PAULO'` and `' São Paulo '` all resolve to `'SP'`. Exports the `StateCode` type. +Get the two-letter code (sigla) of a Brazilian state given its full name. The match is accent-insensitive, case-insensitive and ignores leading/trailing whitespace, so `'sao paulo'`, `'SÃO PAULO'` and `' São Paulo '` all resolve to `'SP'`. Every run of internal whitespace collapses into a single space too, so `'Rio de Janeiro'` resolves to `'RJ'`, while a name written without the space matches nothing (`'saopaulo'` is not `'São Paulo'`). Exports the `StateCode` type. ```javascript import { getStateCodeByName } from '@brazilian-utils/brazilian-utils'; @@ -1040,7 +1040,7 @@ getCities('SP'); ## getHolidays -Get Brazilian holidays for a given year. Returns national holidays and optionally state-specific holidays. Each holiday (typed as `Holiday`) has a `type` field (`HolidayType`: `"national"`, `"state"`, `"optional"` or `"religious"`). "Dia da Consciência Negra" (Nov 20) is a national holiday from 2024 onward (Lei nº 14.759/2023). Before that, MT and RJ still carry their own state-level entry named `"Consciência Negra"` on the same date. Results are memoized per `year`/`stateCode`, but each call still returns a fresh copy. An unknown/invalid `stateCode` is ignored, returning national holidays only; the lookup reads own properties only, so `"__proto__"`, `"constructor"` and the like are unknown state codes rather than a crash. +Get Brazilian holidays for a given year. Returns national holidays and optionally state-specific holidays. Each holiday (typed as `Holiday`) has a `type` field (`HolidayType`: `"national"`, `"state"`, `"optional"` or `"religious"`). "Dia da Consciência Negra" (Nov 20) is a national holiday from 2024 onward (Lei nº 14.759/2023). Before that, several states still carry a state-level entry of their own on the same date: `"Consciência Negra"` in MT and RJ, `"Dia Estadual da Consciência Negra"` in AP and `"Dia da Consciência Negra"` in AM and SP. Results are memoized per `year`/`stateCode`, but each call still returns a fresh copy. An unknown/invalid `stateCode` is ignored, returning national holidays only; the lookup reads own properties only, so `"__proto__"`, `"constructor"` and the like are unknown state codes rather than a crash. Only the years 1900 through 2099 are supported, the range the business day utilities inherit; a year outside it returns `[]`. Only one state holiday per UF is a feriado civil under [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm), art. 1º, II, which authorises "a data magna do Estado fixada em lei estadual" in the singular; the other entries rest on ordinary state laws and are reported because they are observed in practice. Notable per-state rules: @@ -1129,7 +1129,7 @@ generateCep(); // '92500000' ## formatCnh -Format CNH. `options.pad` (part of `FormatCnhOptions`) left-pads the value with zeros to the full 11 digits before masking. +Format CNH. `options.pad` (part of `FormatCnhOptions`) left-pads the value with zeros to the full 11 digits before masking (default `false`). ```javascript import { formatCnh } from '@brazilian-utils/brazilian-utils'; @@ -1140,7 +1140,7 @@ formatCnh('2650306461', { pad: true }); // 026503064-61 ## isValidCnh -Check if CNH is valid. Spaces, dots and hyphens around/between the digits are ignored, but any other character, a letter in particular, makes the value invalid. +Check if CNH is valid. Spaces, dots and hyphens around/between the digits are ignored, but any other character, a letter in particular, makes the value invalid. A value whose 11 digits are all the same is rejected before the check digits are computed, so `'11111111111'` is invalid. ```javascript import { isValidCnh } from '@brazilian-utils/brazilian-utils'; @@ -1162,7 +1162,7 @@ generateCnh(); // '02650306461' ## parseCnh -Remove CNH formatting, keep only digits, and cap the result to 11 digits. +Remove CNH formatting, keep only digits, and cap the result to 11 digits. Returns `''` when there is no digit at all. ```javascript import { parseCnh } from '@brazilian-utils/brazilian-utils'; @@ -1295,7 +1295,7 @@ generatePhone('service'); // '08001234567' or '40041234' ## formatLicensePlate -Format a license plate. Old Brazilian plates (`LLLNNNN`) are returned with a hyphen and Mercosul plates (`LLLNLNN`) stay normalized. +Format a license plate. Old Brazilian plates (`LLLNNNN`) are returned with a hyphen and Mercosul plates (`LLLNLNN`) stay normalized. Partial values are formatted as far as they go, so it can also be used as an input mask, and a value that cannot start a valid plate gives `''`. ```javascript import { formatLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -1367,7 +1367,7 @@ generatePis(); // '91077906857' ## getMunicipality -Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. A single function handles both directions, based on whether `options` has a `code` or a `municipalityName`/`uf`. `code` accepts both `string` and `number` input and must be exactly 7 digits, otherwise the function resolves to `null`. A `code` given as a number must be a non-negative integer: a sign and a decimal point are not digits, so `-3550308` and `355030.8` resolve to `null` instead of being read as `3550308`. Resolution is entirely offline, from a bundled IBGE dataset: no network request is made. The municipality name match ignores accents and casing. An unknown municipality, an unknown UF or invalid input all resolve to `null`. The `[name, uf]` pair is a fresh array on every call, so mutating the result never affects subsequent lookups. +Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. A single function handles both directions, based on whether `options` has a `code` or a `municipalityName`/`uf`. `code` accepts both `string` and `number` input and must be exactly 7 digits, otherwise the function resolves to `null`. A `code` given as a number must be a non-negative integer: a sign and a decimal point are not digits, so `-3550308` and `355030.8` resolve to `null` instead of being read as `3550308`. Resolution is entirely offline, from a bundled IBGE dataset: no network request is made. The municipality name match ignores accents and casing, and every run of whitespace collapses into a single space, so `'sao paulo'` matches `'São Paulo'` while a name written without the space does not; the casing is folded to upper case, the direction Unicode expands `'ß'` to `'SS'` in, so `'Paßos'` matches `'Passos'`. An unknown municipality, an unknown UF or invalid input all resolve to `null`. The `[name, uf]` pair is a fresh array on every call, so mutating the result never affects subsequent lookups. ```javascript import { getMunicipality } from '@brazilian-utils/brazilian-utils'; @@ -1633,7 +1633,7 @@ formatCns('89010001', { pad: true }); // '000 0000 8901 0001' ## isValidCertidao -Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with weights cycling from 2 to 10 and back through 0. Accepts the usual mask characters and whitespace between/around groups. The layout is the one [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023) currently publishes, with inciso II and §§ 1º and 3º to 5º in the redação of the Provimento CN nº 237/2026 and the rest of the article, § 2º included, in that of the Provimento CN nº 182/2024; the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311) and got its digit structure from the also revoked [Provimento CNJ nº 3/2009, art. 7º](https://atos.cnj.jus.br/atos/detalhar/1310). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). +Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with the weights cycling from 2 to 10 and back through 0: the first pass starts at 2 over the 30 base digits, the second at 1 over the 31 digits that include the first check digit, and in both a remainder of 10 is read as 1. Accepts the usual mask characters and whitespace between/around groups. The layout is the one [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023) currently publishes, with inciso II and §§ 1º and 3º to 5º in the redação of the Provimento CN nº 237/2026 and the rest of the article, § 2º included, in that of the Provimento CN nº 182/2024; the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311) and got its digit structure from the also revoked [Provimento CNJ nº 3/2009, art. 7º](https://atos.cnj.jus.br/atos/detalhar/1310). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). The serviço digits are fixed at `55`, the code [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) assigns to the registro civil das pessoas naturais, so a matrícula carrying any other pair in the ninth and tenth positions is rejected however good its check digits are. The book-type digit always has to name one of the nine book types (the same `CertidaoType` returned by `parseCertidao`), so a matrícula whose digit is `0` is rejected however good its check digits are, the same way `parseCertidao` returns `null` for it. `options.accept` (part of `IsValidCertidaoOptions`) narrows that to the listed types; it defaults to every type, and a value that is not an array falls back to that default. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. @@ -1690,7 +1690,7 @@ The `Certidao` result carries: ## formatCertidao -Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits. The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. +Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. ```javascript import { formatCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1716,7 +1716,7 @@ isValidCei('000000000000'); // false (repeated digits) ## formatCei -Format a CEI (Cadastro Específico do INSS) number according to the usual `00.000.00000/00` mask, the one the reference implementations of the check digit agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCeiOptions`) left pads the value with zeros up to 12 digits. +Format a CEI (Cadastro Específico do INSS) number according to the usual `00.000.00000/00` mask, the one the reference implementations of the check digit agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCeiOptions`) left pads the value with zeros up to 12 digits (default `false`). ```javascript import { formatCei } from '@brazilian-utils/brazilian-utils'; @@ -1742,7 +1742,7 @@ isValidCno('000000000000'); // false (repeated digits) ## formatCno -Format a CNO (Cadastro Nacional de Obras) number. The CNO kept the CEI's numbering, so both share the same 12 digit, `00.000.00000/00` mask, the one the reference implementations of the check digit agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCnoOptions`) left pads the value with zeros up to 12 digits. +Format a CNO (Cadastro Nacional de Obras) number. The CNO kept the CEI's numbering, so both share the same 12 digit, `00.000.00000/00` mask, the one the reference implementations of the check digit agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCnoOptions`) left pads the value with zeros up to 12 digits (default `false`). ```javascript import { formatCno } from '@brazilian-utils/brazilian-utils'; @@ -1763,13 +1763,13 @@ isValidCaepf('293.118.610/001-84'); // true isValidCaepf('41142260000101'); // true isValidCaepf(29311861000184); // true isValidCaepf('29311861000185'); // false (invalid check digits) -isValidCaepf('00000000000000'); // false (invalid check digits) +isValidCaepf('00000000000000'); // false (repeated base digits) isValidCaepf('00000000000012'); // false (repeated base digits) ``` ## formatCaepf -Format a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number according to the usual `000.000.000/000-00` mask, the one the sources of the check digit rule agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCaepfOptions`) left pads the value with zeros up to 14 digits. +Format a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number according to the usual `000.000.000/000-00` mask, the one the sources of the check digit rule agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCaepfOptions`) left pads the value with zeros up to 14 digits (default `false`). ```javascript import { formatCaepf } from '@brazilian-utils/brazilian-utils'; @@ -1882,7 +1882,7 @@ getCnae('0111abc301'); // null (not a documented form) ## isValidNcm -Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. Accepts the code with or without the dotted mask, or as a number. A string is only read as a code when it is written in one of those forms (the 8 digits, or the `NNNN.NN.NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. Accepts the code with or without the dotted mask, or as a number. A string is only read as a code when it is written in one of those forms (the 8 digits, or the `NNNN.NN.NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. A bare number cannot carry a leading zero, so a code starting with `0` has to be passed as a string: `isValidNcm(1012100)` is `false` while `isValidNcm('01012100')` is `true`. ```javascript import { isValidNcm } from '@brazilian-utils/brazilian-utils'; diff --git a/scripts/llms.ts b/scripts/llms.ts index 22214905..3b154366 100644 --- a/scripts/llms.ts +++ b/scripts/llms.ts @@ -95,6 +95,67 @@ function parseUtilities(utilitiesMd: string): UtilSection[] { }); } +const FENCE_MARKER = "```"; +const SUB_HEADING_PATTERN = /^#{2,3} (.+)$/; +const BACKTICKED_PATTERN = /`([^`]+)`/g; + +/** + * Collects the `##` and `###` headings of a page, in document order and outside code fences, so a + * generated table of contents cannot drift from the page it indexes. + * @param {string} markdown - The Markdown page to read the headings of. + * @returns {string[]} The heading texts, in document order. + */ +function subHeadings(markdown: string): string[] { + let insideFence = false; + + return markdown.split("\n").flatMap((line) => { + if (line.startsWith(FENCE_MARKER)) { + insideFence = !insideFence; + return []; + } + + if (insideFence) return []; + + const heading = SUB_HEADING_PATTERN.exec(line)?.[1]; + + return heading === undefined ? [] : [heading.trim()]; + }); +} + +/** + * Reads the util names listed in the "Bundle size" table of `getting-started.md`, so the summary + * of the dataset-backed utils cannot drift from the table it summarizes. + * @param {string} gettingStartedMd - The full contents of `getting-started.md`. + * @returns {string[]} The util names of the table, in document order. + */ +function parseDatasetUtils(gettingStartedMd: string): string[] { + const section = /\n## Bundle size\n([\s\S]*?)(?=\n## |$)/.exec(gettingStartedMd)?.[1] ?? ""; + + const names: string[] = []; + + for (const row of section.split("\n")) { + if (!row.startsWith("| `")) continue; + + for (const [, name] of (row.split("|")[1] ?? "").matchAll(BACKTICKED_PATTERN)) { + if (name !== undefined) names.push(name); + } + } + + return names; +} + +/** + * Joins names into an English list, e.g. "`a`, `b` and `c`". + * @param {string[]} names - The names to join, in order. + * @returns {string} The names, backticked and comma-separated, with "and" before the last one. + */ +function joinNames(names: string[]): string { + const quoted = names.map((name) => `\`${name}\``); + const last = quoted.at(-1) ?? ""; + + return quoted.length < 2 ? last : `${quoted.slice(0, -1).join(", ")} and ${last}`; +} + const PREFIX_GROUPS: { title: string; test: (name: string) => boolean }[] = [ { title: "Validators (isValid*)", test: (name) => name.startsWith("isValid") }, { title: "Formatters (format*)", test: (name) => name.startsWith("format") }, @@ -132,7 +193,7 @@ function utilLink(util: UtilSection): string { return `- [${util.name}](${SITE}/utilities.md#${util.slug}): ${util.description}`; } -function buildLlmsTxt(utils: UtilSection[]): string { +function buildLlmsTxt(utils: UtilSection[], datasetUtils: string[]): string { const groups = groupUtilities(utils); const groupSections = groups .map((group) => `## ${group.title}\n\n${group.utils.map(utilLink).join("\n")}`) @@ -150,7 +211,7 @@ Install with \`npm install --save @brazilian-utils/brazilian-utils\` (also avail import { isValidCpf } from '@brazilian-utils/brazilian-utils'; \`\`\` -Every util is also available as its own subpath for lazy-loading/code-splitting, \`@brazilian-utils/brazilian-utils/\` (kebab-case of the function name, e.g. \`isValidCpf\` maps to \`is-valid-cpf\`) - most useful for \`getCities\`, the one util that embeds a large dataset: +Every util is also available as its own subpath for lazy-loading/code-splitting, \`@brazilian-utils/brazilian-utils/\` (kebab-case of the function name, e.g. \`isValidCpf\` maps to \`is-valid-cpf\`) - most useful for the utils that embed an official dataset (${joinNames(datasetUtils)}): \`\`\`javascript const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities'); @@ -160,7 +221,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [Getting started](${SITE}/getting-started.md): installation, runtime support, usage and bundle size/subpath imports - [Utilities](${SITE}/utilities.md): full English reference, one section per function, with signatures and examples -- [Bundle size](${SITE}/getting-started.md#bundle-size): tree-shaking behavior and the \`getCities\`/subpath-import exception +- [Bundle size](${SITE}/getting-started.md#bundle-size): tree-shaking behavior and the dataset-backed utils that are worth a subpath import ${groupSections} @@ -210,9 +271,7 @@ function buildLlmsFullTxt( ): string { const toc = [ "- [Getting Started](#getting-started)", - ...["Installation", "Runtime support", "Usage", "Bundle size"].map( - (heading) => ` - [${heading}](#${slugify(heading)})`, - ), + ...subHeadings(gettingStartedMd).map((heading) => ` - [${heading}](#${slugify(heading)})`), "- [Utilities](#utilities)", ...utils.map((util) => ` - [${util.name}](#${util.slug})`), ].join("\n"); @@ -239,7 +298,10 @@ function main(): void { const utilitiesMd = readFileSync(join(DOCS_DIR, "utilities.md"), "utf8"); const utils = parseUtilities(utilitiesMd); - writeFileSync(join(DOCS_DIR, "llms.txt"), buildLlmsTxt(utils)); + writeFileSync( + join(DOCS_DIR, "llms.txt"), + buildLlmsTxt(utils, parseDatasetUtils(gettingStartedMd)), + ); writeFileSync( join(DOCS_DIR, "llms-full.txt"), buildLlmsFullTxt(gettingStartedMd, utilitiesMd, utils), diff --git a/src/_internals/constants/iban.ts b/src/_internals/constants/iban.ts index b82cfc01..2d54100e 100644 --- a/src/_internals/constants/iban.ts +++ b/src/_internals/constants/iban.ts @@ -12,8 +12,10 @@ * caractere alfanumérico", while the ISO 13616 registry pattern `1!a` makes it a letter, and the * registry is the form followed here, so a digit in that position is deliberately rejected. * Only Brazilian IBANs follow this layout; every other ISO 13616 country has its own. - * @see Official: https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf Circular BCB nº 3.625/2013 - * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf Diretrizes de Implementação do IBAN no Brasil + * @see Official: https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf + * Circular BCB nº 3.625/2013 + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf + * Diretrizes de Implementação do IBAN no Brasil */ export const BR_IBAN_LENGTH = 29; diff --git a/src/add-business-days/add-business-days.ts b/src/add-business-days/add-business-days.ts index 4d5fe37c..9847b11b 100644 --- a/src/add-business-days/add-business-days.ts +++ b/src/add-business-days/add-business-days.ts @@ -52,7 +52,8 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * addBusinessDays(null, 1); // null * ``` * - * @see Based on: https://date-fns.org/docs/addBusinessDays Reference behavior for `amount: 0`, + * @see Based on: https://date-fns.org/docs/addBusinessDays + * Reference behavior for `amount: 0`, * for the positional `(date, amount)` argument order and for walking backwards on a negative * `amount`. The underlying holiday determination's official sources are cited in * `isBusinessDay`/`getHolidays`. diff --git a/src/convert-number-to-words/convert-number-to-words.ts b/src/convert-number-to-words/convert-number-to-words.ts index 60c8ef6c..1e21e769 100644 --- a/src/convert-number-to-words/convert-number-to-words.ts +++ b/src/convert-number-to-words/convert-number-to-words.ts @@ -39,7 +39,8 @@ export type ConvertNumberToWordsOptions = { * convertNumberToWords(NaN); // "" * ``` * - * @see Based on: https://github.com/savoirfairelinux/num2words `brutils` itself has no dedicated + * @see Based on: https://github.com/savoirfairelinux/num2words + * `brutils` itself has no dedicated * number-to-words module (its `currency.py` delegates the Portuguese numeral text to this * library's `pt_BR` locale); this is the reference for the numeral-word tables reproduced here. */ diff --git a/src/difference-in-business-days/difference-in-business-days.ts b/src/difference-in-business-days/difference-in-business-days.ts index cd7050d2..01cdb633 100644 --- a/src/difference-in-business-days/difference-in-business-days.ts +++ b/src/difference-in-business-days/difference-in-business-days.ts @@ -51,9 +51,11 @@ const toLocalDayTimestamp = (date: Date): number => * differenceInBusinessDays(new Date(2100, 0, 5), new Date(2100, 0, 4)); // null (outside the supported years) * ``` * - * @see Based on: https://date-fns.org/docs/differenceInBusinessDays Documented behavior and the + * @see Based on: https://date-fns.org/docs/differenceInBusinessDays + * Documented behavior and the * positional `(laterDate, earlierDate)` argument order. - * @see Based on: https://unpkg.com/date-fns@4.1.0/differenceInBusinessDays.js Source used to + * @see Based on: https://unpkg.com/date-fns@4.1.0/differenceInBusinessDays.js + * Source used to * verify the exact boundary treatment (`earlierDate` counted, `laterDate` excluded) and the sign * convention. The underlying holiday determination's official sources are cited in * `isBusinessDay`/`getHolidays`. diff --git a/src/format-cei/format-cei.ts b/src/format-cei/format-cei.ts index a4fc7575..7df7bbf5 100644 --- a/src/format-cei/format-cei.ts +++ b/src/format-cei/format-cei.ts @@ -10,7 +10,9 @@ export type FormatCeiOptions = { }; /** - * Formats a CEI (Cadastro Específico do INSS) number according to the official mask. + * Formats a CEI (Cadastro Específico do INSS) number according to the usual "00.000.00000/00" + * mask, the one the reference implementations of the check digit agree on (the Receita Federal + * does not print it). * * Formats progressively, as far as the digits given go, so it can also be used as an input * mask while the user is still typing. diff --git a/src/format-iban/format-iban.ts b/src/format-iban/format-iban.ts index 2a4b6d70..72cfef51 100644 --- a/src/format-iban/format-iban.ts +++ b/src/format-iban/format-iban.ts @@ -31,8 +31,10 @@ import { GROUP_SIZE } from "./constants"; * formatIban("BR1500000000000010932840814P-2"); // "" (hyphens are not part of an IBAN) * ``` * - * @see Official: https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf Circular BCB nº 3.625/2013 - * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf Diretrizes de Implementação do IBAN no Brasil + * @see Official: https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf + * Circular BCB nº 3.625/2013 + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf + * Diretrizes de Implementação do IBAN no Brasil */ export const formatIban = (value: string): string => { if (typeof value !== "string") return ""; diff --git a/src/generate-pix-payload/generate-pix-payload.ts b/src/generate-pix-payload/generate-pix-payload.ts index 111a6cd7..93fc90ae 100644 --- a/src/generate-pix-payload/generate-pix-payload.ts +++ b/src/generate-pix-payload/generate-pix-payload.ts @@ -203,7 +203,8 @@ const resolveFormattedAmount = ( * * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/spb_docs/ManualBRCode.pdf * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf - * @see Official: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. + * @see Official: https://github.com/bacen/pix-api + * Pix (SPI) OpenAPI spec. * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html * DICT (Diretório de Identificadores de Contas Transacionais) API specification. */ diff --git a/src/get-state-by-ibge-code/get-state-by-ibge-code.ts b/src/get-state-by-ibge-code/get-state-by-ibge-code.ts index 6c038d6c..266b41d9 100644 --- a/src/get-state-by-ibge-code/get-state-by-ibge-code.ts +++ b/src/get-state-by-ibge-code/get-state-by-ibge-code.ts @@ -20,7 +20,8 @@ export type { State } from "../_internals/constants/states"; * @returns {State|null} The matching `State` object, or `null` when `code` is not a known * IBGE UF code. * - * @see Official: https://servicodados.ibge.gov.br/api/v1/localidades/estados (IBGE Localidades API, field `id`) + * @see Official: https://servicodados.ibge.gov.br/api/v1/localidades/estados + * (IBGE Localidades API, field `id`) * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf * (Manual de Orientação do Contribuinte, "chave de acesso" / "Tabela do IBGE") * diff --git a/src/get-state-code-by-name/get-state-code-by-name.ts b/src/get-state-code-by-name/get-state-code-by-name.ts index e2d72980..28fc41a6 100644 --- a/src/get-state-code-by-name/get-state-code-by-name.ts +++ b/src/get-state-code-by-name/get-state-code-by-name.ts @@ -21,7 +21,8 @@ const normalizeName = (value: string): string => * @returns {StateCode|null} The two-letter state code, or `null` when `name` does not match * any Brazilian state. * - * @see Official: https://servicodados.ibge.gov.br/api/v1/localidades/estados (IBGE Localidades API) + * @see Official: https://servicodados.ibge.gov.br/api/v1/localidades/estados + * (IBGE Localidades API) * * @example * ```typescript diff --git a/src/get-state-name-by-code/get-state-name-by-code.ts b/src/get-state-name-by-code/get-state-name-by-code.ts index 5970c7ca..a9eda4ac 100644 --- a/src/get-state-name-by-code/get-state-name-by-code.ts +++ b/src/get-state-name-by-code/get-state-name-by-code.ts @@ -12,7 +12,8 @@ export type { StateName } from "../_internals/constants/states"; * @returns {StateName|null} The full state name, or `null` when `code` does not match any * Brazilian state. * - * @see Official: https://servicodados.ibge.gov.br/api/v1/localidades/estados (IBGE Localidades API) + * @see Official: https://servicodados.ibge.gov.br/api/v1/localidades/estados + * (IBGE Localidades API) * * @example * ```typescript diff --git a/src/get-timezone-by-state/constants.ts b/src/get-timezone-by-state/constants.ts index 90bdf88d..99291586 100644 --- a/src/get-timezone-by-state/constants.ts +++ b/src/get-timezone-by-state/constants.ts @@ -10,9 +10,11 @@ * UTC-02:00 offset is out of scope here. * * @see Official: https://www.iana.org/time-zones - * @see Based on: https://raw.githubusercontent.com/eggert/tz/main/zone1970.tab (IANA tz + * @see Based on: https://raw.githubusercontent.com/eggert/tz/main/zone1970.tab + * (IANA tz * database data file, `BR` rows) - * @see Based on: https://en.wikipedia.org/wiki/Time_in_Brazil Used to confirm the state + * @see Based on: https://en.wikipedia.org/wiki/Time_in_Brazil + * Used to confirm the state * coverage of each zone. */ export const STATE_TIMEZONES: Record = { diff --git a/src/get-timezone-by-state/get-timezone-by-state.ts b/src/get-timezone-by-state/get-timezone-by-state.ts index c9c3d5b2..5e31a910 100644 --- a/src/get-timezone-by-state/get-timezone-by-state.ts +++ b/src/get-timezone-by-state/get-timezone-by-state.ts @@ -18,9 +18,11 @@ import { STATE_TIMEZONES } from "./constants"; * any Brazilian state. * * @see Official: https://www.iana.org/time-zones - * @see Based on: https://raw.githubusercontent.com/eggert/tz/main/zone1970.tab (IANA tz + * @see Based on: https://raw.githubusercontent.com/eggert/tz/main/zone1970.tab + * (IANA tz * database data file, `BR` rows) - * @see Based on: https://en.wikipedia.org/wiki/Time_in_Brazil Used to confirm the state + * @see Based on: https://en.wikipedia.org/wiki/Time_in_Brazil + * Used to confirm the state * coverage of each zone. * * @example diff --git a/src/is-valid-caepf/constants.ts b/src/is-valid-caepf/constants.ts index 83dd992c..66bef577 100644 --- a/src/is-valid-caepf/constants.ts +++ b/src/is-valid-caepf/constants.ts @@ -14,7 +14,8 @@ * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/caepf * The registry's own page at the Receita Federal, which describes the cadastro but publishes * neither the 14 digit layout nor the check digit rule. - * @see Based on: http://ghiorzi.org/DVnew.htm Description of the CAEPF layout and of the + * @see Based on: http://ghiorzi.org/DVnew.htm + * Description of the CAEPF layout and of the * shift of 12 applied to the check digit pair. * @see Based on: https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts * Reference implementation agreeing on the weights and on the shift. diff --git a/src/is-valid-caepf/is-valid-caepf.ts b/src/is-valid-caepf/is-valid-caepf.ts index 6fdf3102..ac8b4996 100644 --- a/src/is-valid-caepf/is-valid-caepf.ts +++ b/src/is-valid-caepf/is-valid-caepf.ts @@ -41,14 +41,15 @@ const getCheckDigit = (base: string, weights: number[]): number => * isValidCaepf("41142260000101"); // true * isValidCaepf(29311861000184); // true * isValidCaepf("29311861000185"); // false (invalid check digits) - * isValidCaepf("00000000000000"); // false (invalid check digits) + * isValidCaepf("00000000000000"); // false (repeated base digits) * isValidCaepf("00000000000012"); // false (repeated base digits) * ``` * * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/caepf * The registry's own page at the Receita Federal, which describes the cadastro but publishes * neither the 14 digit layout nor the check digit rule. - * @see Based on: http://ghiorzi.org/DVnew.htm Description of the CAEPF layout and of the + * @see Based on: http://ghiorzi.org/DVnew.htm + * Description of the CAEPF layout and of the * shift of 12 applied to the check digit pair. * @see Based on: https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts * Reference implementation agreeing on the weights and on the shift. diff --git a/src/is-valid-iban/is-valid-iban.ts b/src/is-valid-iban/is-valid-iban.ts index dfbbc033..6302d447 100644 --- a/src/is-valid-iban/is-valid-iban.ts +++ b/src/is-valid-iban/is-valid-iban.ts @@ -49,11 +49,16 @@ const hasValidCheckDigits = (iban: string): boolean => { * isValidIban("DE89370400440532013000"); // false (non Brazilian IBAN) * ``` * - * @see Official: https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf Circular BCB nº 3.625/2013 - * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf Diretrizes de Implementação do IBAN no Brasil - * @see Official: https://www.iso.org/standard/81090.html ISO 13616-1:2020 (IBAN structure) - * @see Official: https://www.iso.org/standard/31531.html ISO/IEC 7064:2003 (MOD 97-10 check digit algorithm) - * @see Based on: https://www.iban.com/structure Used to cross check the Brazil IBAN example. + * @see Official: https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf + * Circular BCB nº 3.625/2013 + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf + * Diretrizes de Implementação do IBAN no Brasil + * @see Official: https://www.iso.org/standard/81090.html + * ISO 13616-1:2020 (IBAN structure) + * @see Official: https://www.iso.org/standard/31531.html + * ISO/IEC 7064:2003 (MOD 97-10 check digit algorithm) + * @see Based on: https://www.iban.com/structure + * Used to cross check the Brazil IBAN example. */ export const isValidIban = (value: string): boolean => { if (typeof value !== "string") return false; diff --git a/src/is-valid-pix-key/is-valid-pix-key.ts b/src/is-valid-pix-key/is-valid-pix-key.ts index 1cdb5ec3..cd6ab992 100644 --- a/src/is-valid-pix-key/is-valid-pix-key.ts +++ b/src/is-valid-pix-key/is-valid-pix-key.ts @@ -33,7 +33,8 @@ export type IsValidPixKeyOptions = { * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html * DICT (Diretório de Identificadores de Contas Transacionais) API specification, key format * reference. - * @see Official: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. + * @see Official: https://github.com/bacen/pix-api + * Pix (SPI) OpenAPI spec. */ export const isValidPixKey = (value: string, options?: IsValidPixKeyOptions): boolean => { const key = parsePixKey(value); diff --git a/src/is-valid-pix-payload/is-valid-pix-payload.ts b/src/is-valid-pix-payload/is-valid-pix-payload.ts index 92f47c6e..8d22f7da 100644 --- a/src/is-valid-pix-payload/is-valid-pix-payload.ts +++ b/src/is-valid-pix-payload/is-valid-pix-payload.ts @@ -46,7 +46,8 @@ import { parsePixPayload } from "../parse-pix-payload/parse-pix-payload"; * * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/spb_docs/ManualBRCode.pdf * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf - * @see Official: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. + * @see Official: https://github.com/bacen/pix-api + * Pix (SPI) OpenAPI spec. * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html * DICT (Diretório de Identificadores de Contas Transacionais) API specification. */ diff --git a/src/parse-iban/parse-iban.ts b/src/parse-iban/parse-iban.ts index a6646406..da9d1513 100644 --- a/src/parse-iban/parse-iban.ts +++ b/src/parse-iban/parse-iban.ts @@ -77,10 +77,14 @@ const ACCOUNT_TYPE_END = ACCOUNT_END + ACCOUNT_TYPE_LENGTH; * parseIban("BR1500000000000010932840814P-2"); // null (hyphens are not part of an IBAN) * ``` * - * @see Official: https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf Circular BCB nº 3.625/2013 - * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf Diretrizes de Implementação do IBAN no Brasil - * @see Official: https://www.iso.org/standard/81090.html ISO 13616-1:2020 (IBAN structure) - * @see Official: https://www.iso.org/standard/31531.html ISO/IEC 7064:2003 (MOD 97-10 check digit algorithm) + * @see Official: https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf + * Circular BCB nº 3.625/2013 + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf + * Diretrizes de Implementação do IBAN no Brasil + * @see Official: https://www.iso.org/standard/81090.html + * ISO 13616-1:2020 (IBAN structure) + * @see Official: https://www.iso.org/standard/31531.html + * ISO/IEC 7064:2003 (MOD 97-10 check digit algorithm) */ export const parseIban = (value: string): Iban | null => { if (!isValidIban(value)) return null; diff --git a/src/parse-pix-key/parse-pix-key.ts b/src/parse-pix-key/parse-pix-key.ts index 7ffb1329..27bfbeb9 100644 --- a/src/parse-pix-key/parse-pix-key.ts +++ b/src/parse-pix-key/parse-pix-key.ts @@ -84,7 +84,8 @@ const resolvePhoneKey = (trimmed: string): PixKey | null => { * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html * DICT (Diretório de Identificadores de Contas Transacionais) API specification, key format * reference. - * @see Official: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. + * @see Official: https://github.com/bacen/pix-api + * Pix (SPI) OpenAPI spec. */ export const parsePixKey = (value: string): PixKey | null => { if (typeof value !== "string") return null; diff --git a/src/parse-pix-payload/parse-pix-payload.ts b/src/parse-pix-payload/parse-pix-payload.ts index a5959f58..8b5ce942 100644 --- a/src/parse-pix-payload/parse-pix-payload.ts +++ b/src/parse-pix-payload/parse-pix-payload.ts @@ -268,7 +268,8 @@ const buildPixPayload = ( * * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/spb_docs/ManualBRCode.pdf * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf - * @see Official: https://github.com/bacen/pix-api Pix (SPI) OpenAPI spec. + * @see Official: https://github.com/bacen/pix-api + * Pix (SPI) OpenAPI spec. * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html * DICT (Diretório de Identificadores de Contas Transacionais) API specification. */ diff --git a/src/sub-business-days/sub-business-days.ts b/src/sub-business-days/sub-business-days.ts index 02e0eec8..664c6fbd 100644 --- a/src/sub-business-days/sub-business-days.ts +++ b/src/sub-business-days/sub-business-days.ts @@ -49,7 +49,8 @@ export type { BusinessDayOptions } from "../is-business-day/is-business-day"; * subBusinessDays(new Date(1900, 0, 2), 1); // null (the walk leaves the supported years) * ``` * - * @see Based on: https://date-fns.org/docs/subBusinessDays Reference behavior and the positional + * @see Based on: https://date-fns.org/docs/subBusinessDays + * Reference behavior and the positional * `(date, amount)` argument order. The underlying holiday determination's official sources are * cited in `isBusinessDay`/`getHolidays`. */ From 13971717c1f90fd13b6683d3d2685799e33eb460 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 04:50:39 -0300 Subject: [PATCH 42/75] ci(datasets): decode entities and require a complete CFOP annex before writing the table - the annex parser dropped any paragraph whose markup differed from the one exact opening tag and only refused an empty result, so a markup change could have written a partial table; the run now fails below 600 operable codes (the annex holds 619) and decodes HTML entities in the text --- scripts/cfop.ts | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/scripts/cfop.ts b/scripts/cfop.ts index 95cec93e..aec80377 100644 --- a/scripts/cfop.ts +++ b/scripts/cfop.ts @@ -15,6 +15,25 @@ const scriptsDir = import.meta.dirname; */ const CURRENT_TEXT_PARAGRAPH_REGEX = /

([^<]*)<\/p>/g; +/** + * Smallest number of operable codes a complete annex yields. The consolidated text carries 619 + * today and CONFAZ only adds or replaces codes, so a result far below it means the markup changed + * and the paragraph pattern above stopped matching, not that codes were revoked. + */ +const MINIMUM_OPERABLE_CODES = 600; + +const HTML_ENTITIES: Record = { + "&": "&", + "<": "<", + ">": ">", + """: '"', + "'": "'", + " ": " ", +}; + +const decodeEntities = (text: string): string => + text.replaceAll(/&(?:amp|lt|gt|quot|#39|nbsp);/g, (entity) => HTML_ENTITIES[entity] ?? entity); + /** A code line, e.g. `1.101 - Compra para industrialização ou produção rural.`. */ const CODE_LINE_REGEX = /^(\d)\.(\d{3})\s*[-–]\s*(.+)$/; @@ -39,7 +58,9 @@ const TRAILING_PUNCTUATION_REGEX = /[.\s]+$/; */ const parseAnnex = (html: string): Record => { const paragraphs = [...html.matchAll(CURRENT_TEXT_PARAGRAPH_REGEX)].map((match) => - (match[1] ?? "").replaceAll(/\s+/g, " ").trim(), + decodeEntities(match[1] ?? "") + .replaceAll(/\s+/g, " ") + .trim(), ); const data: Record = {}; @@ -75,8 +96,12 @@ const main = async (): Promise => { async (response) => { const data = parseAnnex(await response.text()); - if (Object.keys(data).length === 0) { - throw new Error("CFOP annex page holds no operable code"); + const count = Object.keys(data).length; + + if (count < MINIMUM_OPERABLE_CODES) { + throw new Error( + `CFOP annex page yielded ${count} operable codes, below the ${MINIMUM_OPERABLE_CODES} a complete annex holds; the markup probably changed`, + ); } return data; From 1882ed8efc1146026249da1627178db9c0ce808c Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 12:31:47 -0300 Subject: [PATCH 43/75] test(cep): skip the live Widenet check while the service is offline - the weekly live run asked Widenet for a real CEP and failed every week since the service started answering HTTP 502, the same outage that removed it from the default provider list; the check is skipped with that reason and comes back when the service does --- .../get-address-info-by-cep.test.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/get-address-info-by-cep/get-address-info-by-cep.test.ts b/src/get-address-info-by-cep/get-address-info-by-cep.test.ts index 1ad2b8c8..14e7396a 100644 --- a/src/get-address-info-by-cep/get-address-info-by-cep.test.ts +++ b/src/get-address-info-by-cep/get-address-info-by-cep.test.ts @@ -757,17 +757,19 @@ describe("getAddressInfoByCep", () => { LIVE_TEST_TIMEOUT, ); - it( - "should fetch live address from Widenet", - async () => { - const result = await getAddressInfoByCep(VALID_CEP, { - providers: ["widenet"], - }); - - expectAddressFound(result); - }, - LIVE_TEST_TIMEOUT, - ); + describe.skip("Widenet, skipped while the service answers HTTP 502 (since September 2026, the reason it left the default provider list)", () => { + it( + "should fetch live address from Widenet", + async () => { + const result = await getAddressInfoByCep(VALID_CEP, { + providers: ["widenet"], + }); + + expectAddressFound(result); + }, + LIVE_TEST_TIMEOUT, + ); + }); it( "should fetch live address from BrasilAPI", From 882cf9554ad01595a794eb734a47df256a18e50f Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 14:49:54 -0300 Subject: [PATCH 44/75] docs(cep): say a CEP that starts with 0 has to be a string when given as a number - `isValidCep(1310100)` is `false` and `formatCep(1310100)` gives `13101-00` because a number cannot keep the leading zero; both sections now say so and point at the string form or `pad` --- docs/llms-full.txt | 4 ++-- docs/pt-br/utilities.md | 4 ++-- docs/utilities.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 435b249e..aecb5cf1 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -325,7 +325,7 @@ parseCnpj('12.OUT.345/0001-99', { version: 2 }); // 12OUT345000199 ### isValidCep -Check if CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)) is valid. Accepts both `string` and `number` input; any spaces, dots and hyphens around/between the 8 digits are ignored, but any other character, a letter in particular, makes the value invalid. +Check if CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)) is valid. Accepts both `string` and `number` input, but a CEP that starts with `0` has to be passed as a string, since a number cannot keep the leading zero (`isValidCep(1310100)` is `false`, `isValidCep('01310100')` is `true`); any spaces, dots and hyphens around/between the 8 digits are ignored, but any other character, a letter in particular, makes the value invalid. ```javascript import { isValidCep } from '@brazilian-utils/brazilian-utils'; @@ -751,7 +751,7 @@ parsePis('123.45678.90-1'); // 12345678901 ### formatCep -Format CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (part of `FormatCepOptions`) left-pads the value with zeros to the full 8 digits before masking (default `false`). +Format CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (part of `FormatCepOptions`) left-pads the value with zeros to the full 8 digits before masking (default `false`); a CEP that starts with `0` given as a number loses that zero, so pass it as a string or use `pad`. ```javascript import { formatCep } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index bfc15aa8..97f0d974 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -85,7 +85,7 @@ parseCnpj('12.OUT.345/0001-99', { version: 2 }); // 12OUT345000199 ## isValidCep -Valida se o CEP ([código de endereçamento postal](https://pt.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)) é válido. Aceita entrada como `string` ou `number`; espaços, pontos e hífens ao redor/entre os 8 dígitos são ignorados, mas qualquer outro caractere, uma letra em especial, invalida o valor. +Valida se o CEP ([código de endereçamento postal](https://pt.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)) é válido. Aceita entrada como `string` ou `number`, mas um CEP que começa com `0` precisa ser passado como string, já que um número não preserva o zero à esquerda (`isValidCep(1310100)` é `false`, `isValidCep('01310100')` é `true`); espaços, pontos e hífens ao redor/entre os 8 dígitos são ignorados, mas qualquer outro caractere, uma letra em especial, invalida o valor. ```javascript import { isValidCep } from '@brazilian-utils/brazilian-utils'; @@ -511,7 +511,7 @@ parsePis('123.45678.90-1'); // 12345678901 ## formatCep -Formata o CEP ([código de endereçamento postal](https://pt.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (parte de `FormatCepOptions`) completa o valor com zeros à esquerda até os 8 dígitos antes de aplicar a máscara (padrão `false`). +Formata o CEP ([código de endereçamento postal](https://pt.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (parte de `FormatCepOptions`) completa o valor com zeros à esquerda até os 8 dígitos antes de aplicar a máscara (padrão `false`); um CEP que começa com `0` passado como número perde esse zero, então passe-o como string ou use `pad`. ```javascript import { formatCep } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/utilities.md b/docs/utilities.md index 076e9748..cac7c782 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -85,7 +85,7 @@ parseCnpj('12.OUT.345/0001-99', { version: 2 }); // 12OUT345000199 ## isValidCep -Check if CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)) is valid. Accepts both `string` and `number` input; any spaces, dots and hyphens around/between the 8 digits are ignored, but any other character, a letter in particular, makes the value invalid. +Check if CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)) is valid. Accepts both `string` and `number` input, but a CEP that starts with `0` has to be passed as a string, since a number cannot keep the leading zero (`isValidCep(1310100)` is `false`, `isValidCep('01310100')` is `true`); any spaces, dots and hyphens around/between the 8 digits are ignored, but any other character, a letter in particular, makes the value invalid. ```javascript import { isValidCep } from '@brazilian-utils/brazilian-utils'; @@ -511,7 +511,7 @@ parsePis('123.45678.90-1'); // 12345678901 ## formatCep -Format CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (part of `FormatCepOptions`) left-pads the value with zeros to the full 8 digits before masking (default `false`). +Format CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (part of `FormatCepOptions`) left-pads the value with zeros to the full 8 digits before masking (default `false`); a CEP that starts with `0` given as a number loses that zero, so pass it as a string or use `pad`. ```javascript import { formatCep } from '@brazilian-utils/brazilian-utils'; From 6d47c14d7083da83f4bccc726af8fd0210888927 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 15:03:57 -0300 Subject: [PATCH 45/75] fix(words): write the groups the way the official texts do, without commas, and spell 14 quatorze MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `convertNumberToWords(1235)` gave "mil, duzentos e trinta e cinco", the num2words convention; the Lei Orçamentária Anual (Lei 14.822/2024, art. 1º), the salário mínimo decrees (Decreto 12.342/2024) and the Manual de Redação da Presidência write "mil duzentos e trinta e cinco" and "cinco trilhões quinhentos e sessenta e seis bilhões duzentos e oitenta e quatro milhões oitocentos e dez mil trezentos e setenta e três reais": groups joined by a space, "e" only inside a group and before a final round hundred or a final group below 100 - 14 is "quatorze", the form of the same texts; num2words' "catorze" is also admitted by the VOLP - `convertCurrencyToWords` and the year of `convertDateToWords` follow, tests and docs updated --- docs/llms-full.txt | 8 ++-- docs/llms.txt | 4 +- docs/pt-br/utilities.md | 8 ++-- docs/utilities.md | 8 ++-- src/_internals/constants/number-words.ts | 13 +++--- .../number-to-words/number-to-words.test.ts | 18 ++++----- .../number-to-words/number-to-words.ts | 32 +++++++++------ .../convert-currency-to-words.test.ts | 38 +++++++++--------- .../convert-currency-to-words.ts | 4 +- .../convert-date-to-words.test.ts | 2 +- .../convert-date-to-words.ts | 7 ++-- .../convert-number-to-words.test.ts | 40 +++++++++---------- .../convert-number-to-words.ts | 2 +- 13 files changed, 95 insertions(+), 89 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index aecb5cf1..203dc084 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -1109,7 +1109,7 @@ parseCurrency(''); // 0 ### convertNumberToWords -Formats an integer as its Brazilian Portuguese cardinal number words ("por extenso"), e.g. `1235` becomes `"mil, duzentos e trinta e cinco"`. Only integers from `-999999999999999` to `999999999999999` (999 trillion in absolute value) are supported; anything outside that range, `NaN` or a non-finite value returns `""`. A non-integer `value` is truncated toward zero before conversion. `options.gender` (part of `ConvertNumberToWordsOptions`) agrees "um/dois" and the hundreds group ("duzentos/duzentas", etc.) with the noun the number qualifies, defaulting to `"masculine"`. An invalid `gender` value is ignored and the default is used. The result is always lowercase; apply any other casing to it yourself. +Formats an integer as its Brazilian Portuguese cardinal number words ("por extenso"), e.g. `1235` becomes `"mil duzentos e trinta e cinco"`. Only integers from `-999999999999999` to `999999999999999` (999 trillion in absolute value) are supported; anything outside that range, `NaN` or a non-finite value returns `""`. A non-integer `value` is truncated toward zero before conversion. `options.gender` (part of `ConvertNumberToWordsOptions`) agrees "um/dois" and the hundreds group ("duzentos/duzentas", etc.) with the noun the number qualifies, defaulting to `"masculine"`. An invalid `gender` value is ignored and the default is used. The result is always lowercase; apply any other casing to it yourself. ```javascript import { convertNumberToWords } from '@brazilian-utils/brazilian-utils'; @@ -1125,12 +1125,12 @@ convertNumberToWords(NaN); // "" ### convertCurrencyToWords -Formats a monetary amount in Brazilian Reais as its "por extenso" textual representation, the style used to write out the amount by hand on cheques and contracts, e.g. `1523.45` becomes `"mil, quinhentos e vinte e três reais e quarenta e cinco centavos"`. `value` is truncated (not rounded) to 2 decimal places. The singular noun is used for exactly 1 ("um real", "um centavo") and "de" is inserted before "reais" when the amount is a round million, billion or trillion of reais. An amount that truncates to nothing becomes `"zero reais"` with no "menos" prefix, any other negative amount is prefixed with "menos", and invalid input returns `""`. Above `Number.MAX_SAFE_INTEGER / 100` reais (about 90 trillion) a double cannot carry cents, so the amount is read as a whole number of reais. It takes no options: the result is always lowercase; apply any other casing to it yourself. +Formats a monetary amount in Brazilian Reais as its "por extenso" textual representation, the style used to write out the amount by hand on cheques and contracts, e.g. `1523.45` becomes `"mil quinhentos e vinte e três reais e quarenta e cinco centavos"`. `value` is truncated (not rounded) to 2 decimal places. The singular noun is used for exactly 1 ("um real", "um centavo") and "de" is inserted before "reais" when the amount is a round million, billion or trillion of reais. An amount that truncates to nothing becomes `"zero reais"` with no "menos" prefix, any other negative amount is prefixed with "menos", and invalid input returns `""`. Above `Number.MAX_SAFE_INTEGER / 100` reais (about 90 trillion) a double cannot carry cents, so the amount is read as a whole number of reais. It takes no options: the result is always lowercase; apply any other casing to it yourself. ```javascript import { convertCurrencyToWords } from '@brazilian-utils/brazilian-utils'; -convertCurrencyToWords(1523.45); // "mil, quinhentos e vinte e três reais e quarenta e cinco centavos" +convertCurrencyToWords(1523.45); // "mil quinhentos e vinte e três reais e quarenta e cinco centavos" convertCurrencyToWords(1); // "um real" convertCurrencyToWords(0.01); // "um centavo" convertCurrencyToWords(1000000); // "um milhão de reais" @@ -1781,7 +1781,7 @@ differenceInBusinessDays(new Date(), new Date('not a date')); // null ### convertDateToWords -Formats a date as its Brazilian Portuguese "por extenso" textual representation, e.g. `"01/01/2024"` becomes `"primeiro de janeiro de dois mil e vinte e quatro"`. Accepts a `Date` (read by its local calendar date, the same convention used by `isHoliday`) or a string in `"dd/mm/yyyy"` or ISO `"yyyy-mm-dd"` format. With the default `options.style` of `"full"`, day 1 is written as "primeiro" and every other day uses the cardinal number; with `"month"`, only the month name is spelled out and the day/year are left as digits (day 1 as `"1º"`, e.g. `"2 de março de 2024"`, `"1º de janeiro de 2024"`). Month names are lowercase. In `"full"` style the year is written out as a cardinal number without the thousands comma that `convertNumberToWords`/`convertCurrencyToWords` use (`1999` reads as `"mil novecentos e noventa e nove"`, not `"mil, novecentos e noventa e nove"`), matching how a date is read aloud. `options.weekday` (default `false`) prefixes the pt-BR weekday name in lowercase followed by a comma (`"sábado, dois de março de dois mil e vinte e quatro"`), computed from the resolved calendar date. An invalid `style` value is ignored and the default is used. The result is always lowercase; apply any other casing to it yourself. February 29th is accepted on the leap years of the proleptic Gregorian calendar (divisible by 4, except centuries not divisible by 400). Returns `""` for an invalid `Date`, a malformed string, a day/month that does not exist, or a date before year 1. +Formats a date as its Brazilian Portuguese "por extenso" textual representation, e.g. `"01/01/2024"` becomes `"primeiro de janeiro de dois mil e vinte e quatro"`. Accepts a `Date` (read by its local calendar date, the same convention used by `isHoliday`) or a string in `"dd/mm/yyyy"` or ISO `"yyyy-mm-dd"` format. With the default `options.style` of `"full"`, day 1 is written as "primeiro" and every other day uses the cardinal number; with `"month"`, only the month name is spelled out and the day/year are left as digits (day 1 as `"1º"`, e.g. `"2 de março de 2024"`, `"1º de janeiro de 2024"`). Month names are lowercase. In `"full"` style the year is written out as a cardinal number without the thousands comma that `convertNumberToWords`/`convertCurrencyToWords` use (`1999` reads as `"mil novecentos e noventa e nove"`, not `"mil novecentos e noventa e nove"`), matching how a date is read aloud. `options.weekday` (default `false`) prefixes the pt-BR weekday name in lowercase followed by a comma (`"sábado, dois de março de dois mil e vinte e quatro"`), computed from the resolved calendar date. An invalid `style` value is ignored and the default is used. The result is always lowercase; apply any other casing to it yourself. February 29th is accepted on the leap years of the proleptic Gregorian calendar (divisible by 4, except centuries not divisible by 400). Returns `""` for an invalid `Date`, a malformed string, a day/month that does not exist, or a date before year 1. ```javascript import { convertDateToWords } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/llms.txt b/docs/llms.txt index 4b909811..2ecc7825 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -154,8 +154,8 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' ## Other utilities - [capitalize](https://brazilian-utils.com.br/utilities.md#capitalize): Transforms the first letter into a capital one of each word, the way a Brazilian name, company name or address is written, with no options needed. -- [convertNumberToWords](https://brazilian-utils.com.br/utilities.md#convertnumbertowords): Formats an integer as its Brazilian Portuguese cardinal number words ("por extenso"), e.g. `1235` becomes `"mil, duzentos e trinta e cinco"`. -- [convertCurrencyToWords](https://brazilian-utils.com.br/utilities.md#convertcurrencytowords): Formats a monetary amount in Brazilian Reais as its "por extenso" textual representation, the style used to write out the amount by hand on cheques and contracts, e.g. `1523.45` becomes `"mil, quinhentos e vinte e três reais e quarenta e cinco centavos"`. +- [convertNumberToWords](https://brazilian-utils.com.br/utilities.md#convertnumbertowords): Formats an integer as its Brazilian Portuguese cardinal number words ("por extenso"), e.g. `1235` becomes `"mil duzentos e trinta e cinco"`. +- [convertCurrencyToWords](https://brazilian-utils.com.br/utilities.md#convertcurrencytowords): Formats a monetary amount in Brazilian Reais as its "por extenso" textual representation, the style used to write out the amount by hand on cheques and contracts, e.g. `1523.45` becomes `"mil quinhentos e vinte e três reais e quarenta e cinco centavos"`. - [convertLicensePlateToMercosul](https://brazilian-utils.com.br/utilities.md#convertlicenseplatetomercosul): Convert an old format Brazilian license plate (`LLLNNNN`) to the Mercosul format (`LLLNLNN`), following the official conversion table: the digit in the 5th position becomes a letter (`0` through `9` mapping to `A` through `J`). - [isHoliday](https://brazilian-utils.com.br/utilities.md#isholiday): Check if a specific date is a Brazilian holiday. - [isBusinessDay](https://brazilian-utils.com.br/utilities.md#isbusinessday): Check if a date is a Brazilian business day (dia útil). diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 97f0d974..d66b8bd9 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -869,7 +869,7 @@ parseCurrency(''); // 0 ## convertNumberToWords -Formata um número inteiro por extenso em português do Brasil, ex.: `1235` vira `"mil, duzentos e trinta e cinco"`. Só são suportados inteiros de `-999999999999999` a `999999999999999` (999 trilhões em valor absoluto); fora desse intervalo, `NaN` ou um valor não finito retornam `""`. Um `value` não inteiro é truncado em direção a zero antes da conversão. `options.gender` (parte de `ConvertNumberToWordsOptions`) concorda "um/dois" e a centena ("duzentos/duzentas" etc.) com o substantivo que o número qualifica, com padrão `"masculine"`. Um valor inválido de `gender` é ignorado e o padrão é usado. O resultado sai sempre em minúsculas; aplique qualquer outra caixa por conta própria. +Formata um número inteiro por extenso em português do Brasil, ex.: `1235` vira `"mil duzentos e trinta e cinco"`. Só são suportados inteiros de `-999999999999999` a `999999999999999` (999 trilhões em valor absoluto); fora desse intervalo, `NaN` ou um valor não finito retornam `""`. Um `value` não inteiro é truncado em direção a zero antes da conversão. `options.gender` (parte de `ConvertNumberToWordsOptions`) concorda "um/dois" e a centena ("duzentos/duzentas" etc.) com o substantivo que o número qualifica, com padrão `"masculine"`. Um valor inválido de `gender` é ignorado e o padrão é usado. O resultado sai sempre em minúsculas; aplique qualquer outra caixa por conta própria. ```javascript import { convertNumberToWords } from '@brazilian-utils/brazilian-utils'; @@ -885,12 +885,12 @@ convertNumberToWords(NaN); // "" ## convertCurrencyToWords -Formata um valor monetário em Reais por extenso, no estilo usado para escrever o valor à mão em cheques e contratos, ex.: `1523.45` vira `"mil, quinhentos e vinte e três reais e quarenta e cinco centavos"`. O `value` é truncado (não arredondado) para 2 casas decimais. O substantivo no singular é usado para exatamente 1 ("um real", "um centavo") e "de" é inserido antes de "reais" quando o valor é um milhão, bilhão ou trilhão de reais redondo. Um valor que trunca para nada vira `"zero reais"`, sem o prefixo "menos"; qualquer outro valor negativo recebe o prefixo "menos", e uma entrada inválida retorna `""`. Acima de `Number.MAX_SAFE_INTEGER / 100` reais (cerca de 90 trilhões) um double não consegue carregar centavos, então o valor é lido como um número inteiro de reais. Não recebe opções: o resultado sai sempre em minúsculas; aplique qualquer outra caixa por conta própria. +Formata um valor monetário em Reais por extenso, no estilo usado para escrever o valor à mão em cheques e contratos, ex.: `1523.45` vira `"mil quinhentos e vinte e três reais e quarenta e cinco centavos"`. O `value` é truncado (não arredondado) para 2 casas decimais. O substantivo no singular é usado para exatamente 1 ("um real", "um centavo") e "de" é inserido antes de "reais" quando o valor é um milhão bilhão ou trilhão de reais redondo. Um valor que trunca para nada vira `"zero reais"`, sem o prefixo "menos"; qualquer outro valor negativo recebe o prefixo "menos", e uma entrada inválida retorna `""`. Acima de `Number.MAX_SAFE_INTEGER / 100` reais (cerca de 90 trilhões) um double não consegue carregar centavos, então o valor é lido como um número inteiro de reais. Não recebe opções: o resultado sai sempre em minúsculas; aplique qualquer outra caixa por conta própria. ```javascript import { convertCurrencyToWords } from '@brazilian-utils/brazilian-utils'; -convertCurrencyToWords(1523.45); // "mil, quinhentos e vinte e três reais e quarenta e cinco centavos" +convertCurrencyToWords(1523.45); // "mil quinhentos e vinte e três reais e quarenta e cinco centavos" convertCurrencyToWords(1); // "um real" convertCurrencyToWords(0.01); // "um centavo" convertCurrencyToWords(1000000); // "um milhão de reais" @@ -1541,7 +1541,7 @@ differenceInBusinessDays(new Date(), new Date('not a date')); // null ## convertDateToWords -Formata uma data por extenso em português do Brasil, ex.: `"01/01/2024"` vira `"primeiro de janeiro de dois mil e vinte e quatro"`. Aceita um `Date` (lido pela sua data de calendário local, a mesma convenção usada por `isHoliday`) ou uma string no formato `"dd/mm/yyyy"` ou ISO `"yyyy-mm-dd"`. Com o `options.style` padrão `"full"`, o dia 1 é escrito como "primeiro" e os demais dias usam o número cardinal; com `"month"`, só o nome do mês é escrito por extenso e o dia/ano ficam em dígitos (o dia 1 como `"1º"`, ex.: `"2 de março de 2024"`, `"1º de janeiro de 2024"`). Os nomes dos meses ficam em minúsculo. No estilo `"full"` o ano é escrito por extenso sem a vírgula de milhar que `convertNumberToWords`/`convertCurrencyToWords` usam (`1999` vira `"mil novecentos e noventa e nove"`, não `"mil, novecentos e noventa e nove"`), do jeito que uma data é lida em voz alta. `options.weekday` (padrão `false`) prefixa o nome do dia da semana em pt-BR minúsculo seguido de vírgula (`"sábado, dois de março de dois mil e vinte e quatro"`), calculado a partir da data de calendário resolvida. Um valor inválido de `style` é ignorado e o padrão é usado. O resultado sai sempre em minúsculas; aplique qualquer outra caixa por conta própria. O dia 29 de fevereiro é aceito nos anos bissextos do calendário gregoriano proléptico (divisíveis por 4, exceto séculos não divisíveis por 400). Retorna `""` para um `Date` inválido, uma string malformada, um dia/mês que não existe ou uma data anterior ao ano 1. +Formata uma data por extenso em português do Brasil, ex.: `"01/01/2024"` vira `"primeiro de janeiro de dois mil e vinte e quatro"`. Aceita um `Date` (lido pela sua data de calendário local, a mesma convenção usada por `isHoliday`) ou uma string no formato `"dd/mm/yyyy"` ou ISO `"yyyy-mm-dd"`. Com o `options.style` padrão `"full"`, o dia 1 é escrito como "primeiro" e os demais dias usam o número cardinal; com `"month"`, só o nome do mês é escrito por extenso e o dia/ano ficam em dígitos (o dia 1 como `"1º"`, ex.: `"2 de março de 2024"`, `"1º de janeiro de 2024"`). Os nomes dos meses ficam em minúsculo. No estilo `"full"` o ano é escrito por extenso sem a vírgula de milhar que `convertNumberToWords`/`convertCurrencyToWords` usam (`1999` vira `"mil novecentos e noventa e nove"`, não `"mil novecentos e noventa e nove"`), do jeito que uma data é lida em voz alta. `options.weekday` (padrão `false`) prefixa o nome do dia da semana em pt-BR minúsculo seguido de vírgula (`"sábado, dois de março de dois mil e vinte e quatro"`), calculado a partir da data de calendário resolvida. Um valor inválido de `style` é ignorado e o padrão é usado. O resultado sai sempre em minúsculas; aplique qualquer outra caixa por conta própria. O dia 29 de fevereiro é aceito nos anos bissextos do calendário gregoriano proléptico (divisíveis por 4, exceto séculos não divisíveis por 400). Retorna `""` para um `Date` inválido, uma string malformada, um dia/mês que não existe ou uma data anterior ao ano 1. ```javascript import { convertDateToWords } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/utilities.md b/docs/utilities.md index cac7c782..bb0dbd5a 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -869,7 +869,7 @@ parseCurrency(''); // 0 ## convertNumberToWords -Formats an integer as its Brazilian Portuguese cardinal number words ("por extenso"), e.g. `1235` becomes `"mil, duzentos e trinta e cinco"`. Only integers from `-999999999999999` to `999999999999999` (999 trillion in absolute value) are supported; anything outside that range, `NaN` or a non-finite value returns `""`. A non-integer `value` is truncated toward zero before conversion. `options.gender` (part of `ConvertNumberToWordsOptions`) agrees "um/dois" and the hundreds group ("duzentos/duzentas", etc.) with the noun the number qualifies, defaulting to `"masculine"`. An invalid `gender` value is ignored and the default is used. The result is always lowercase; apply any other casing to it yourself. +Formats an integer as its Brazilian Portuguese cardinal number words ("por extenso"), e.g. `1235` becomes `"mil duzentos e trinta e cinco"`. Only integers from `-999999999999999` to `999999999999999` (999 trillion in absolute value) are supported; anything outside that range, `NaN` or a non-finite value returns `""`. A non-integer `value` is truncated toward zero before conversion. `options.gender` (part of `ConvertNumberToWordsOptions`) agrees "um/dois" and the hundreds group ("duzentos/duzentas", etc.) with the noun the number qualifies, defaulting to `"masculine"`. An invalid `gender` value is ignored and the default is used. The result is always lowercase; apply any other casing to it yourself. ```javascript import { convertNumberToWords } from '@brazilian-utils/brazilian-utils'; @@ -885,12 +885,12 @@ convertNumberToWords(NaN); // "" ## convertCurrencyToWords -Formats a monetary amount in Brazilian Reais as its "por extenso" textual representation, the style used to write out the amount by hand on cheques and contracts, e.g. `1523.45` becomes `"mil, quinhentos e vinte e três reais e quarenta e cinco centavos"`. `value` is truncated (not rounded) to 2 decimal places. The singular noun is used for exactly 1 ("um real", "um centavo") and "de" is inserted before "reais" when the amount is a round million, billion or trillion of reais. An amount that truncates to nothing becomes `"zero reais"` with no "menos" prefix, any other negative amount is prefixed with "menos", and invalid input returns `""`. Above `Number.MAX_SAFE_INTEGER / 100` reais (about 90 trillion) a double cannot carry cents, so the amount is read as a whole number of reais. It takes no options: the result is always lowercase; apply any other casing to it yourself. +Formats a monetary amount in Brazilian Reais as its "por extenso" textual representation, the style used to write out the amount by hand on cheques and contracts, e.g. `1523.45` becomes `"mil quinhentos e vinte e três reais e quarenta e cinco centavos"`. `value` is truncated (not rounded) to 2 decimal places. The singular noun is used for exactly 1 ("um real", "um centavo") and "de" is inserted before "reais" when the amount is a round million, billion or trillion of reais. An amount that truncates to nothing becomes `"zero reais"` with no "menos" prefix, any other negative amount is prefixed with "menos", and invalid input returns `""`. Above `Number.MAX_SAFE_INTEGER / 100` reais (about 90 trillion) a double cannot carry cents, so the amount is read as a whole number of reais. It takes no options: the result is always lowercase; apply any other casing to it yourself. ```javascript import { convertCurrencyToWords } from '@brazilian-utils/brazilian-utils'; -convertCurrencyToWords(1523.45); // "mil, quinhentos e vinte e três reais e quarenta e cinco centavos" +convertCurrencyToWords(1523.45); // "mil quinhentos e vinte e três reais e quarenta e cinco centavos" convertCurrencyToWords(1); // "um real" convertCurrencyToWords(0.01); // "um centavo" convertCurrencyToWords(1000000); // "um milhão de reais" @@ -1541,7 +1541,7 @@ differenceInBusinessDays(new Date(), new Date('not a date')); // null ## convertDateToWords -Formats a date as its Brazilian Portuguese "por extenso" textual representation, e.g. `"01/01/2024"` becomes `"primeiro de janeiro de dois mil e vinte e quatro"`. Accepts a `Date` (read by its local calendar date, the same convention used by `isHoliday`) or a string in `"dd/mm/yyyy"` or ISO `"yyyy-mm-dd"` format. With the default `options.style` of `"full"`, day 1 is written as "primeiro" and every other day uses the cardinal number; with `"month"`, only the month name is spelled out and the day/year are left as digits (day 1 as `"1º"`, e.g. `"2 de março de 2024"`, `"1º de janeiro de 2024"`). Month names are lowercase. In `"full"` style the year is written out as a cardinal number without the thousands comma that `convertNumberToWords`/`convertCurrencyToWords` use (`1999` reads as `"mil novecentos e noventa e nove"`, not `"mil, novecentos e noventa e nove"`), matching how a date is read aloud. `options.weekday` (default `false`) prefixes the pt-BR weekday name in lowercase followed by a comma (`"sábado, dois de março de dois mil e vinte e quatro"`), computed from the resolved calendar date. An invalid `style` value is ignored and the default is used. The result is always lowercase; apply any other casing to it yourself. February 29th is accepted on the leap years of the proleptic Gregorian calendar (divisible by 4, except centuries not divisible by 400). Returns `""` for an invalid `Date`, a malformed string, a day/month that does not exist, or a date before year 1. +Formats a date as its Brazilian Portuguese "por extenso" textual representation, e.g. `"01/01/2024"` becomes `"primeiro de janeiro de dois mil e vinte e quatro"`. Accepts a `Date` (read by its local calendar date, the same convention used by `isHoliday`) or a string in `"dd/mm/yyyy"` or ISO `"yyyy-mm-dd"` format. With the default `options.style` of `"full"`, day 1 is written as "primeiro" and every other day uses the cardinal number; with `"month"`, only the month name is spelled out and the day/year are left as digits (day 1 as `"1º"`, e.g. `"2 de março de 2024"`, `"1º de janeiro de 2024"`). Month names are lowercase. In `"full"` style the year is written out as a cardinal number without the thousands comma that `convertNumberToWords`/`convertCurrencyToWords` use (`1999` reads as `"mil novecentos e noventa e nove"`, not `"mil novecentos e noventa e nove"`), matching how a date is read aloud. `options.weekday` (default `false`) prefixes the pt-BR weekday name in lowercase followed by a comma (`"sábado, dois de março de dois mil e vinte e quatro"`), computed from the resolved calendar date. An invalid `style` value is ignored and the default is used. The result is always lowercase; apply any other casing to it yourself. February 29th is accepted on the leap years of the proleptic Gregorian calendar (divisible by 4, except centuries not divisible by 400). Returns `""` for an invalid `Date`, a malformed string, a day/month that does not exist, or a date before year 1. ```javascript import { convertDateToWords } from '@brazilian-utils/brazilian-utils'; diff --git a/src/_internals/constants/number-words.ts b/src/_internals/constants/number-words.ts index 0ac7e6ee..dd0b9ad9 100644 --- a/src/_internals/constants/number-words.ts +++ b/src/_internals/constants/number-words.ts @@ -2,12 +2,13 @@ * Portuguese (pt-BR) number-to-words tables, shared by `numberToWords` and by every public * "por extenso" formatter (`convertNumberToWords`, `convertCurrencyToWords`, `convertDateToWords`). * + * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2024/lei/L14822.htm + * Lei nº 14.822/2024 (Lei Orçamentária Anual de 2024), art. 1º, which spells 14 "quatorze" + * ("quatrocentos e quatorze bilhões"), the form the official Brazilian texts use; the Vocabulário + * Ortográfico admits both "quatorze" and "quatorze", and num2words' Portuguese table (below) picks + * "quatorze". * @see Based on: https://github.com/savoirfairelinux/num2words/blob/master/num2words/lang_PT.py - * num2words' Portuguese table, which spells 14 "catorze" (not "quatorze"), the spelling used - * here. Its `lang_PT_BR` subclass keeps that table, so `num2words(14, lang="pt_BR")` is - * "catorze". - * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/currency.py - * brutils' currency helper, which delegates to num2words and therefore inherits that spelling. + * num2words' Portuguese table, the source of every other word of this file. */ export const ZERO_WORD = "zero"; @@ -27,7 +28,7 @@ export const UNITS: readonly string[] = [ "onze", "doze", "treze", - "catorze", + "quatorze", "quinze", "dezesseis", "dezessete", diff --git a/src/_internals/number-to-words/number-to-words.test.ts b/src/_internals/number-to-words/number-to-words.test.ts index 1337bdf4..4fe34a81 100644 --- a/src/_internals/number-to-words/number-to-words.test.ts +++ b/src/_internals/number-to-words/number-to-words.test.ts @@ -15,7 +15,7 @@ describe("numberToWords", () => { expect(numberToWords(11)).toBe("onze"); expect(numberToWords(12)).toBe("doze"); expect(numberToWords(13)).toBe("treze"); - expect(numberToWords(14)).toBe("catorze"); + expect(numberToWords(14)).toBe("quatorze"); expect(numberToWords(15)).toBe("quinze"); expect(numberToWords(16)).toBe("dezesseis"); expect(numberToWords(17)).toBe("dezessete"); @@ -51,8 +51,8 @@ describe("numberToWords", () => { expect(numberToWords(1100)).toBe("mil e cem"); }); - test("should separate 'mil' from a non round last group with a comma (1235 -> num2words pt_BR 'mil, duzentos e trinta e cinco')", () => { - expect(numberToWords(1235)).toBe("mil, duzentos e trinta e cinco"); + test("should separate 'mil' from a non round last group with a comma (1235 -> num2words pt_BR 'mil duzentos e trinta e cinco')", () => { + expect(numberToWords(1235)).toBe("mil duzentos e trinta e cinco"); }); test("should return 'dois mil' for 2000 (masculine default)", () => { @@ -73,26 +73,26 @@ describe("numberToWords", () => { test("should convert the maximum supported value (999 trillion, num2words pt_BR)", () => { expect(numberToWords(NUMBER_TO_WORDS_MAX_VALUE)).toBe( - "novecentos e noventa e nove trilhões, novecentos e noventa e nove bilhões, " + - "novecentos e noventa e nove milhões, novecentos e noventa e nove mil, " + + "novecentos e noventa e nove trilhões novecentos e noventa e nove bilhões " + + "novecentos e noventa e nove milhões novecentos e noventa e nove mil " + "novecentos e noventa e nove", ); }); test("should convert a value spanning billions, millions and thousands (999999999999, num2words pt_BR)", () => { expect(numberToWords(999_999_999_999)).toBe( - "novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, " + - "novecentos e noventa e nove mil, novecentos e noventa e nove", + "novecentos e noventa e nove bilhões novecentos e noventa e nove milhões " + + "novecentos e noventa e nove mil novecentos e noventa e nove", ); }); test("should skip a zero intermediate group (1000230 -> no 'zero mil')", () => { - expect(numberToWords(1_000_230)).toBe("um milhão, duzentos e trinta"); + expect(numberToWords(1_000_230)).toBe("um milhão duzentos e trinta"); }); test("should separate an intermediate group below 100 with a comma, reserving 'e' for the last group (1045678; num2words pt_BR differs here only because its post-processing rewrites ' e ' before a hundreds word)", () => { expect(numberToWords(1_045_678)).toBe( - "um milhão, quarenta e cinco mil, seiscentos e setenta e oito", + "um milhão quarenta e cinco mil seiscentos e setenta e oito", ); }); diff --git a/src/_internals/number-to-words/number-to-words.ts b/src/_internals/number-to-words/number-to-words.ts index de359086..d7097d20 100644 --- a/src/_internals/number-to-words/number-to-words.ts +++ b/src/_internals/number-to-words/number-to-words.ts @@ -71,21 +71,22 @@ const isRoundHundred = (value: number): boolean => value % 100 === 0; /** * Converts a non-negative integer into its Brazilian Portuguese cardinal number words - * ("por extenso"), e.g. `1235` becomes `"mil, duzentos e trinta e cinco"`. + * ("por extenso"), e.g. `1235` becomes `"mil duzentos e trinta e cinco"`. * * This is the shared engine behind every "por extenso" formatter of this library * (`convertNumberToWords`, `convertCurrencyToWords`, `convertDateToWords`): it only converts, it * never validates or sanitizes its input, so callers must pass a finite, non-negative integer - * within `[0, NUMBER_TO_WORDS_MAX_VALUE]`. Grouping uses commas between groups and "e" is used - * instead of a comma right before the last group when that group is below 100 or is a round - * hundred (100, 200, ..., 900), matching how the value would be written by hand - * (e.g. `1200` -> `"mil e duzentos"`, `1235` -> `"mil, duzentos e trinta e cinco"`). The "e" - * connector is therefore reserved for the last group: an intermediate group below 100 still takes - * a comma (`1045678` -> `"um milhão, quarenta e cinco mil, seiscentos e setenta e oito"`). This is - * the one place where the output deviates from `num2words`' pt_BR locale, which writes - * `"um milhão e quarenta e cinco mil, ..."` there because its post-processing only rewrites " e " - * into "," when the next word is a hundreds word, making an intermediate group's punctuation - * depend on the group that follows it. Every published `brutils` example is reproduced exactly. + * within `[0, NUMBER_TO_WORDS_MAX_VALUE]`. Groups are joined by a space, and "e" is used right + * before the last group when that group is below 100 or is a round hundred (100, 200, ..., 900), + * the way the official texts write amounts out: `1200` -> `"mil e duzentos"`, `1001` -> `"mil e + * um"`, `1235` -> `"mil duzentos e trinta e cinco"`, `1045678` -> `"um milhão quarenta e cinco mil + * seiscentos e setenta e oito"`. This is the spelling of the Lei Orçamentária Anual ("cinco + * trilhões quinhentos e sessenta e seis bilhões duzentos e oitenta e quatro milhões oitocentos e + * dez mil trezentos e setenta e três reais", Lei 14.822/2024, art. 1º), of the salário mínimo + * decrees ("mil quinhentos e dezoito reais", Decreto 12.342/2024) and of the examples in the Manual + * de Redação da Presidência da República ("mil duzentos e cinquenta reais", "mil e quatrocentos + * reais"). It deviates from `num2words`' pt_BR locale, which separates the groups with commas + * ("mil duzentos e trinta e cinco") and writes "e" before an intermediate group below 100. * * @param {number} value - A non-negative integer in `[0, NUMBER_TO_WORDS_MAX_VALUE]`. * @param {NumberToWordsOptions} [options] - Optional conversion options. @@ -98,12 +99,17 @@ const isRoundHundred = (value: number): boolean => value % 100 === 0; * numberToWords(21); // "vinte e um" * numberToWords(100); // "cem" * numberToWords(1100); // "mil e cem" - * numberToWords(1235); // "mil, duzentos e trinta e cinco" + * numberToWords(1235); // "mil duzentos e trinta e cinco" * numberToWords(2000000); // "dois milhões" * numberToWords(2, { gender: "feminine" }); // "duas" * numberToWords(2000, { gender: "feminine" }); // "duas mil" * ``` * + * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2024/lei/L14822.htm + * Lei nº 14.822, de 22 de janeiro de 2024 (Lei Orçamentária Anual de 2024), art. 1º: amounts written + * out with the groups separated by spaces, "e" only inside a group, and "quatorze". + * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2024/decreto/D12342.htm + * Decreto nº 12.342, de 30 de dezembro de 2024, art. 1º: "R$ 1.518,00 (mil quinhentos e dezoito reais)". * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/currency.py */ export const numberToWords = (value: number, options?: NumberToWordsOptions): string => { @@ -136,7 +142,7 @@ export const numberToWords = (value: number, options?: NumberToWordsOptions): st const connector = // Stryker disable next-line EqualityOperator: equivalent, groupValue === 100 already satisfies isRoundHundred(groupValue) - index === lastNonZeroIndex && (groupValue < 100 || isRoundHundred(groupValue)) ? " e " : ", "; + index === lastNonZeroIndex && (groupValue < 100 || isRoundHundred(groupValue)) ? " e " : " "; result += connector + groupText; } diff --git a/src/convert-currency-to-words/convert-currency-to-words.test.ts b/src/convert-currency-to-words/convert-currency-to-words.test.ts index b12b5e79..91ad2562 100644 --- a/src/convert-currency-to-words/convert-currency-to-words.test.ts +++ b/src/convert-currency-to-words/convert-currency-to-words.test.ts @@ -39,12 +39,12 @@ describe("convertCurrencyToWords", () => { test("should join reais and centavos with 'e' (1523.45, brutils 'convert_real_to_text' example)", () => { expect(convertCurrencyToWords(1523.45)).toBe( - "mil, quinhentos e vinte e três reais e quarenta e cinco centavos", + "mil quinhentos e vinte e três reais e quarenta e cinco centavos", ); }); test("should not insert 'de' when a mil/hundred group follows the million group", () => { - expect(convertCurrencyToWords(1_000_230)).toBe("um milhão, duzentos e trinta reais"); + expect(convertCurrencyToWords(1_000_230)).toBe("um milhão duzentos e trinta reais"); }); test("should return only the centavos when the reais part is zero", () => { @@ -114,7 +114,7 @@ describe("convertCurrencyToWords", () => { test("should still report cents exactly at the Number.MAX_SAFE_INTEGER cents boundary, reading the 90 cents the double holds (90071992547409.9 is exactly 90071992547409.90625, the 91st cent only shows up when 9007199254740990.625 is scaled and rounded to Number.MAX_SAFE_INTEGER)", () => { expect(convertCurrencyToWords(90_071_992_547_409.9)).toBe( - "noventa trilhões, setenta e um bilhões, novecentos e noventa e dois milhões, quinhentos e quarenta e sete mil, quatrocentos e nove reais e noventa centavos", + "noventa trilhões setenta e um bilhões novecentos e noventa e dois milhões quinhentos e quarenta e sete mil quatrocentos e nove reais e noventa centavos", ); }); }); @@ -144,19 +144,19 @@ describe("convertCurrencyToWords", () => { [1_000_000_000_000.0199, "um trilhão de reais e um centavo"], [ 123_456_789_012.345, - "cento e vinte e três bilhões, quatrocentos e cinquenta e seis milhões, setecentos e oitenta e nove mil e doze reais e trinta e quatro centavos", + "cento e vinte e três bilhões quatrocentos e cinquenta e seis milhões setecentos e oitenta e nove mil e doze reais e trinta e quatro centavos", ], [ 87_654_321_098.7654, - "oitenta e sete bilhões, seiscentos e cinquenta e quatro milhões, trezentos e vinte e um mil e noventa e oito reais e setenta e seis centavos", + "oitenta e sete bilhões seiscentos e cinquenta e quatro milhões trezentos e vinte e um mil e noventa e oito reais e setenta e seis centavos", ], [ 999_999_999_999.999, - "novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais e noventa e nove centavos", + "novecentos e noventa e nove bilhões novecentos e noventa e nove milhões novecentos e noventa e nove mil novecentos e noventa e nove reais e noventa e nove centavos", ], [ 9_007_199_254_740.99, - "nove trilhões, sete bilhões, cento e noventa e nove milhões, duzentos e cinquenta e quatro mil, setecentos e quarenta reais e noventa e nove centavos", + "nove trilhões sete bilhões cento e noventa e nove milhões duzentos e cinquenta e quatro mil setecentos e quarenta reais e noventa e nove centavos", ], ]; @@ -204,7 +204,7 @@ describe("convertCurrencyToWords", () => { expect(convertCurrencyToWords(0)).toBe("zero reais"); expect(convertCurrencyToWords(-5.5)).toBe("menos cinco reais e cinquenta centavos"); expect(convertCurrencyToWords(1523.45)).toBe( - "mil, quinhentos e vinte e três reais e quarenta e cinco centavos", + "mil quinhentos e vinte e três reais e quarenta e cinco centavos", ); }); }); @@ -226,7 +226,7 @@ describe("convertCurrencyToWords", () => { [11, "onze centavos"], [12, "doze centavos"], [13, "treze centavos"], - [14, "catorze centavos"], + [14, "quatorze centavos"], [15, "quinze centavos"], [16, "dezesseis centavos"], [17, "dezessete centavos"], @@ -326,7 +326,7 @@ describe("convertCurrencyToWords", () => { [111, "um real e onze centavos"], [112, "um real e doze centavos"], [113, "um real e treze centavos"], - [114, "um real e catorze centavos"], + [114, "um real e quatorze centavos"], [115, "um real e quinze centavos"], [116, "um real e dezesseis centavos"], [117, "um real e dezessete centavos"], @@ -371,22 +371,22 @@ describe("convertCurrencyToWords", () => { const cases: [number, string][] = [ [1000, "mil reais"], [1000.01, "mil reais e um centavo"], - [1101, "mil, cento e um reais"], - [1101.01, "mil, cento e um reais e um centavo"], - [1523.45, "mil, quinhentos e vinte e três reais e quarenta e cinco centavos"], + [1101, "mil cento e um reais"], + [1101.01, "mil cento e um reais e um centavo"], + [1523.45, "mil quinhentos e vinte e três reais e quarenta e cinco centavos"], [1_000_000, "um milhão de reais"], [1_000_000.01, "um milhão de reais e um centavo"], [2_000_000, "dois milhões de reais"], [1_000_001, "um milhão e um reais"], [ 999_999_999_999_999, - "novecentos e noventa e nove trilhões, novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais", + "novecentos e noventa e nove trilhões novecentos e noventa e nove bilhões novecentos e noventa e nove milhões novecentos e noventa e nove mil novecentos e noventa e nove reais", ], [1.999, "um real e noventa e nove centavos"], [100.5, "cem reais e cinquenta centavos"], [2, "dois reais"], [10.5, "dez reais e cinquenta centavos"], - [999_999, "novecentos e noventa e nove mil, novecentos e noventa e nove reais"], + [999_999, "novecentos e noventa e nove mil novecentos e noventa e nove reais"], [100, "cem reais"], [1_000_000_000, "um bilhão de reais"], [2_000_000_000, "dois bilhões de reais"], @@ -403,7 +403,7 @@ describe("convertCurrencyToWords", () => { [0.5, "cinquenta centavos"], [1, "um real"], [-50.25, "menos cinquenta reais e vinte e cinco centavos"], - [1523.45, "mil, quinhentos e vinte e três reais e quarenta e cinco centavos"], + [1523.45, "mil quinhentos e vinte e três reais e quarenta e cinco centavos"], [1_000_000, "um milhão de reais"], [2_000_000, "dois milhões de reais"], [1_000_000_000, "um bilhão de reais"], @@ -414,7 +414,7 @@ describe("convertCurrencyToWords", () => { [2_000_000_000.99, "dois bilhões de reais e noventa e nove centavos"], [ 1_234_567_890.5, - "um bilhão, duzentos e trinta e quatro milhões, quinhentos e sessenta e sete mil, oitocentos e noventa reais e cinquenta centavos", + "um bilhão duzentos e trinta e quatro milhões quinhentos e sessenta e sete mil oitocentos e noventa reais e cinquenta centavos", ], [0.001, "zero reais"], [0.009, "zero reais"], @@ -424,13 +424,13 @@ describe("convertCurrencyToWords", () => { [1_000_000_000.99, "um bilhão de reais e noventa e nove centavos"], [ 999_999_999_999.99, - "novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais e noventa e nove centavos", + "novecentos e noventa e nove bilhões novecentos e noventa e nove milhões novecentos e noventa e nove mil novecentos e noventa e nove reais e noventa e nove centavos", ], [1_000_000_000_000.01, "um trilhão de reais e um centavo"], [1_000_000_000_000.99, "um trilhão de reais e noventa e nove centavos"], [ 9_999_999_999_999.99, - "nove trilhões, novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove reais e noventa e nove centavos", + "nove trilhões novecentos e noventa e nove bilhões novecentos e noventa e nove milhões novecentos e noventa e nove mil novecentos e noventa e nove reais e noventa e nove centavos", ], ]; expectAmounts(cases); diff --git a/src/convert-currency-to-words/convert-currency-to-words.ts b/src/convert-currency-to-words/convert-currency-to-words.ts index c9aeadb1..153ec7b2 100644 --- a/src/convert-currency-to-words/convert-currency-to-words.ts +++ b/src/convert-currency-to-words/convert-currency-to-words.ts @@ -38,7 +38,7 @@ const endsInMillionScale = (words: string): boolean => /** * Formats a monetary amount in Brazilian Reais as its "por extenso" textual representation, * the style used to write out the amount by hand on cheques and contracts, e.g. `1523.45` - * becomes `"mil, quinhentos e vinte e três reais e quarenta e cinco centavos"`. + * becomes `"mil quinhentos e vinte e três reais e quarenta e cinco centavos"`. * * `value` is truncated (not rounded) to 2 decimal places before conversion, matching * `brutils`' `convert_real_to_text`. The singular noun is used for exactly 1 ("um real", @@ -58,7 +58,7 @@ const endsInMillionScale = (words: string): boolean => * * @example * ```typescript - * convertCurrencyToWords(1523.45); // "mil, quinhentos e vinte e três reais e quarenta e cinco centavos" + * convertCurrencyToWords(1523.45); // "mil quinhentos e vinte e três reais e quarenta e cinco centavos" * convertCurrencyToWords(1); // "um real" * convertCurrencyToWords(0.01); // "um centavo" * convertCurrencyToWords(1000000); // "um milhão de reais" diff --git a/src/convert-date-to-words/convert-date-to-words.test.ts b/src/convert-date-to-words/convert-date-to-words.test.ts index c6e80d87..9b1f08f4 100644 --- a/src/convert-date-to-words/convert-date-to-words.test.ts +++ b/src/convert-date-to-words/convert-date-to-words.test.ts @@ -327,7 +327,7 @@ describe("convertDateToWords", () => { ["11/03/2024", "onze de março de dois mil e vinte e quatro"], ["12/03/2024", "doze de março de dois mil e vinte e quatro"], ["13/03/2024", "treze de março de dois mil e vinte e quatro"], - ["14/03/2024", "catorze de março de dois mil e vinte e quatro"], + ["14/03/2024", "quatorze de março de dois mil e vinte e quatro"], ["15/03/2024", "quinze de março de dois mil e vinte e quatro"], ["16/03/2024", "dezesseis de março de dois mil e vinte e quatro"], ["17/03/2024", "dezessete de março de dois mil e vinte e quatro"], diff --git a/src/convert-date-to-words/convert-date-to-words.ts b/src/convert-date-to-words/convert-date-to-words.ts index 9a46902a..a908c96c 100644 --- a/src/convert-date-to-words/convert-date-to-words.ts +++ b/src/convert-date-to-words/convert-date-to-words.ts @@ -41,9 +41,8 @@ const dayToWords = (day: number, monthStyle: boolean): string => { * with no timezone conversion. With the default `"full"` `options.style`, day 1 is written as * "primeiro" and every other day uses the cardinal number; with `"month"`, only the month name * is spelled out and the day/year are written as digits (day 1 as `"1º"`). Month names are - * lowercase. In `"full"` style the year is written out as a cardinal number without the - * thousands comma that `convertNumberToWords`/`convertCurrencyToWords` use (`1999` reads as - * `"mil novecentos e noventa e nove"`, not `"mil, novecentos e noventa e nove"`), matching how a + * lowercase. In `"full"` style the year is written out as a cardinal number the way + * `convertNumberToWords` writes it (`1999` reads as `"mil novecentos e noventa e nove"`), matching how a * date is read aloud. `options.weekday` prefixes the pt-BR weekday name (lowercase) followed by * a comma. The result is always lowercase; apply any other casing to it yourself. * February 29th is accepted on the leap years of the proleptic Gregorian calendar @@ -112,7 +111,7 @@ export const convertDateToWords = ( const monthName = MONTH_NAMES[month - 1]; const isMonthStyle = options?.style === "month"; - const yearWords = isMonthStyle ? String(year) : numberToWords(year).replaceAll(", ", " "); + const yearWords = isMonthStyle ? String(year) : numberToWords(year); const dateWords = `${dayToWords(day, isMonthStyle)} de ${monthName} de ${yearWords}`; return options?.weekday === true diff --git a/src/convert-number-to-words/convert-number-to-words.test.ts b/src/convert-number-to-words/convert-number-to-words.test.ts index 7ba46944..ee24e807 100644 --- a/src/convert-number-to-words/convert-number-to-words.test.ts +++ b/src/convert-number-to-words/convert-number-to-words.test.ts @@ -42,8 +42,8 @@ describe("convertNumberToWords", () => { test("should convert the maximum supported value (999999999999999, 999 trillion)", () => { expect(convertNumberToWords(NUMBER_TO_WORDS_MAX_VALUE)).toBe( - "novecentos e noventa e nove trilhões, novecentos e noventa e nove bilhões, " + - "novecentos e noventa e nove milhões, novecentos e noventa e nove mil, " + + "novecentos e noventa e nove trilhões novecentos e noventa e nove bilhões " + + "novecentos e noventa e nove milhões novecentos e noventa e nove mil " + "novecentos e noventa e nove", ); }); @@ -181,7 +181,7 @@ describe("convertNumberToWords", () => { [111, "cento e onze"], [112, "cento e doze"], [113, "cento e treze"], - [114, "cento e catorze"], + [114, "cento e quatorze"], [115, "cento e quinze"], [116, "cento e dezesseis"], [117, "cento e dezessete"], @@ -304,48 +304,48 @@ describe("convertNumberToWords", () => { [1001, "mil e um"], [1021, "mil e vinte e um"], [1100, "mil e cem"], - [1101, "mil, cento e um"], + [1101, "mil cento e um"], [1200, "mil e duzentos"], - [1235, "mil, duzentos e trinta e cinco"], - [1999, "mil, novecentos e noventa e nove"], + [1235, "mil duzentos e trinta e cinco"], + [1999, "mil novecentos e noventa e nove"], [2000, "dois mil"], [2001, "dois mil e um"], [5000, "cinco mil"], - [9999, "nove mil, novecentos e noventa e nove"], + [9999, "nove mil novecentos e noventa e nove"], [10_000, "dez mil"], [21_000, "vinte e um mil"], [100_000, "cem mil"], [101_000, "cento e um mil"], [200_000, "duzentos mil"], [300_000, "trezentos mil"], - [999_999, "novecentos e noventa e nove mil, novecentos e noventa e nove"], + [999_999, "novecentos e noventa e nove mil novecentos e noventa e nove"], [1_000_000, "um milhão"], [1_000_001, "um milhão e um"], [1_000_100, "um milhão e cem"], - [1_000_230, "um milhão, duzentos e trinta"], - [1_045_678, "um milhão, quarenta e cinco mil, seiscentos e setenta e oito"], + [1_000_230, "um milhão duzentos e trinta"], + [1_045_678, "um milhão quarenta e cinco mil seiscentos e setenta e oito"], [1_100_000, "um milhão e cem mil"], [1_200_000, "um milhão e duzentos mil"], - [1_230_000, "um milhão, duzentos e trinta mil"], - [1_230_045, "um milhão, duzentos e trinta mil e quarenta e cinco"], - [1_230_456, "um milhão, duzentos e trinta mil, quatrocentos e cinquenta e seis"], + [1_230_000, "um milhão duzentos e trinta mil"], + [1_230_045, "um milhão duzentos e trinta mil e quarenta e cinco"], + [1_230_456, "um milhão duzentos e trinta mil quatrocentos e cinquenta e seis"], [2_000_000, "dois milhões"], [1_000_000_000, "um bilhão"], [1_000_000_001, "um bilhão e um"], [2_000_000_000, "dois bilhões"], [ 1_234_567_890, - "um bilhão, duzentos e trinta e quatro milhões, quinhentos e sessenta e sete mil, oitocentos e noventa", + "um bilhão duzentos e trinta e quatro milhões quinhentos e sessenta e sete mil oitocentos e noventa", ], [ 999_999_999_999, - "novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove", + "novecentos e noventa e nove bilhões novecentos e noventa e nove milhões novecentos e noventa e nove mil novecentos e noventa e nove", ], [1_000_000_000_000, "um trilhão"], [2_000_000_000_000, "dois trilhões"], [ 999_999_999_999_999, - "novecentos e noventa e nove trilhões, novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove", + "novecentos e noventa e nove trilhões novecentos e noventa e nove bilhões novecentos e noventa e nove milhões novecentos e noventa e nove mil novecentos e noventa e nove", ], ]; expectWords(cases); @@ -366,7 +366,7 @@ describe("convertNumberToWords", () => { [-11, "menos onze"], [-12, "menos doze"], [-13, "menos treze"], - [-14, "menos catorze"], + [-14, "menos quatorze"], [-15, "menos quinze"], [-16, "menos dezesseis"], [-17, "menos dezessete"], @@ -467,7 +467,7 @@ describe("convertNumberToWords", () => { [-1_000_000, "menos um milhão"], [ -999_999_999_999_999, - "menos novecentos e noventa e nove trilhões, novecentos e noventa e nove bilhões, novecentos e noventa e nove milhões, novecentos e noventa e nove mil, novecentos e noventa e nove", + "menos novecentos e noventa e nove trilhões novecentos e noventa e nove bilhões novecentos e noventa e nove milhões novecentos e noventa e nove mil novecentos e noventa e nove", ], ]; expectWords(cases); @@ -489,7 +489,7 @@ describe("convertNumberToWords", () => { [11, "onze"], [12, "doze"], [13, "treze"], - [14, "catorze"], + [14, "quatorze"], [15, "quinze"], [16, "dezesseis"], [17, "dezessete"], @@ -527,7 +527,7 @@ describe("convertNumberToWords", () => { [1000, "mil"], [1001, "mil e uma"], [1100, "mil e cem"], - [1101, "mil, cento e uma"], + [1101, "mil cento e uma"], [2000, "duas mil"], [2002, "duas mil e duas"], [3000, "três mil"], diff --git a/src/convert-number-to-words/convert-number-to-words.ts b/src/convert-number-to-words/convert-number-to-words.ts index 1e21e769..3ba3e4ae 100644 --- a/src/convert-number-to-words/convert-number-to-words.ts +++ b/src/convert-number-to-words/convert-number-to-words.ts @@ -12,7 +12,7 @@ export type ConvertNumberToWordsOptions = { /** * Formats an integer as its Brazilian Portuguese cardinal number words ("por extenso"), - * e.g. `1235` becomes `"mil, duzentos e trinta e cinco"`. + * e.g. `1235` becomes `"mil duzentos e trinta e cinco"`. * * Only integers from `-999999999999999` to `999999999999999` (999 trillion in absolute value, * the highest value expressible with the "trilhão" scale word) are supported; anything outside From 214da0d9ea028caf5ef24d33513ebadc35259577 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 17:51:11 -0300 Subject: [PATCH 46/75] fix(csosn): read only the bare 3 digits, the code has no printed grouping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `isValidCsosn("1-01")` and `("1-0-1")` were accepted through a separator allowance copied from the ICMS CST; a CSOSN is printed as three plain digits everywhere (the NF-e carries the origin in its own `orig` field), so the separator forms are rejected and only surrounding whitespace is tolerated - the Portuguese `convertCurrencyToWords` sentence regains its comma ("um milhão, bilhão ou trilhão") --- docs/llms-full.txt | 2 +- docs/pt-br/utilities.md | 4 ++-- docs/utilities.md | 2 +- src/is-valid-csosn/constants.ts | 7 ++++--- src/is-valid-csosn/is-valid-csosn.test.ts | 8 ++++++++ src/is-valid-csosn/is-valid-csosn.ts | 7 ++++--- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 203dc084..92eef8d1 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -2216,7 +2216,7 @@ isValidCst(-110); // false (not a non-negative safe integer) Check if a CSOSN (Código de Situação da Operação no Simples Nacional) code is one of the 10 codes of the [consolidated Anexo III-A of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), the table Ajuste SINIEF 03/2010 instituted: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` or `900`. -A string is only read as a code when it is written in one of the documented forms (the 3 digits, with a single separator between them and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +A string is only read as a code when it is written as the bare 3 digits with optional surrounding whitespace: a CSOSN has no printed grouping (the NF-e carries the origin digit in its own `orig` field), so `'1-01'` is rejected; a number is read only when it is a non-negative safe integer. ```javascript import { isValidCsosn } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index d66b8bd9..a592d0f6 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -885,7 +885,7 @@ convertNumberToWords(NaN); // "" ## convertCurrencyToWords -Formata um valor monetário em Reais por extenso, no estilo usado para escrever o valor à mão em cheques e contratos, ex.: `1523.45` vira `"mil quinhentos e vinte e três reais e quarenta e cinco centavos"`. O `value` é truncado (não arredondado) para 2 casas decimais. O substantivo no singular é usado para exatamente 1 ("um real", "um centavo") e "de" é inserido antes de "reais" quando o valor é um milhão bilhão ou trilhão de reais redondo. Um valor que trunca para nada vira `"zero reais"`, sem o prefixo "menos"; qualquer outro valor negativo recebe o prefixo "menos", e uma entrada inválida retorna `""`. Acima de `Number.MAX_SAFE_INTEGER / 100` reais (cerca de 90 trilhões) um double não consegue carregar centavos, então o valor é lido como um número inteiro de reais. Não recebe opções: o resultado sai sempre em minúsculas; aplique qualquer outra caixa por conta própria. +Formata um valor monetário em Reais por extenso, no estilo usado para escrever o valor à mão em cheques e contratos, ex.: `1523.45` vira `"mil quinhentos e vinte e três reais e quarenta e cinco centavos"`. O `value` é truncado (não arredondado) para 2 casas decimais. O substantivo no singular é usado para exatamente 1 ("um real", "um centavo") e "de" é inserido antes de "reais" quando o valor é um milhão, bilhão ou trilhão de reais redondo. Um valor que trunca para nada vira `"zero reais"`, sem o prefixo "menos"; qualquer outro valor negativo recebe o prefixo "menos", e uma entrada inválida retorna `""`. Acima de `Number.MAX_SAFE_INTEGER / 100` reais (cerca de 90 trilhões) um double não consegue carregar centavos, então o valor é lido como um número inteiro de reais. Não recebe opções: o resultado sai sempre em minúsculas; aplique qualquer outra caixa por conta própria. ```javascript import { convertCurrencyToWords } from '@brazilian-utils/brazilian-utils'; @@ -1976,7 +1976,7 @@ isValidCst(-110); // false (não é um inteiro seguro não negativo) Valida se um código de CSOSN (Código de Situação da Operação no Simples Nacional) é um dos 10 códigos do [Anexo III-A consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), a tabela instituída pelo Ajuste SINIEF 03/2010: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` ou `900`. -Uma string só é lida como código quando está escrita em uma das formas documentadas (os 3 dígitos, com um único separador entre eles e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. +Uma string só é lida como código quando está escrita como os 3 dígitos puros, com espaços em branco opcionais no início e no fim: um CSOSN não tem agrupamento impresso (a NF-e leva o dígito de origem no seu próprio campo `orig`), então `'1-01'` é rejeitado; um número só é lido quando é um inteiro seguro não negativo. ```javascript import { isValidCsosn } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/utilities.md b/docs/utilities.md index bb0dbd5a..d33127bb 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -1976,7 +1976,7 @@ isValidCst(-110); // false (not a non-negative safe integer) Check if a CSOSN (Código de Situação da Operação no Simples Nacional) code is one of the 10 codes of the [consolidated Anexo III-A of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), the table Ajuste SINIEF 03/2010 instituted: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` or `900`. -A string is only read as a code when it is written in one of the documented forms (the 3 digits, with a single separator between them and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +A string is only read as a code when it is written as the bare 3 digits with optional surrounding whitespace: a CSOSN has no printed grouping (the NF-e carries the origin digit in its own `orig` field), so `'1-01'` is rejected; a number is read only when it is a non-negative safe integer. ```javascript import { isValidCsosn } from '@brazilian-utils/brazilian-utils'; diff --git a/src/is-valid-csosn/constants.ts b/src/is-valid-csosn/constants.ts index 565ac087..88a8d67b 100644 --- a/src/is-valid-csosn/constants.ts +++ b/src/is-valid-csosn/constants.ts @@ -23,7 +23,8 @@ export const CSOSN_CODES = [ ] as const; /** - * Shape a CSOSN code has to be written in: the 3 digits, optionally split by a single - * whitespace or mask character. + * Shape a CSOSN code has to be written in: the bare 3 digits. Unlike the ICMS CST, whose origin + * digit is printed apart from the Tabela B pair, a CSOSN has no internal grouping anywhere it is + * printed (the NF-e carries the origin in its own `orig` field), so no separator is accepted. */ -export const CSOSN_FORMAT_REGEX = /^\d[\s.\-/]?\d[\s.\-/]?\d$/; +export const CSOSN_FORMAT_REGEX = /^\d{3}$/; diff --git a/src/is-valid-csosn/is-valid-csosn.test.ts b/src/is-valid-csosn/is-valid-csosn.test.ts index c699ebb2..3c29b543 100644 --- a/src/is-valid-csosn/is-valid-csosn.test.ts +++ b/src/is-valid-csosn/is-valid-csosn.test.ts @@ -53,6 +53,14 @@ describe("isValidCsosn", () => { expect(isValidCsosn("1--01")).toBe(false); }); + it("should return false for a code split by a separator, since a CSOSN has no printed grouping", () => { + expect(isValidCsosn("1-01")).toBe(false); + expect(isValidCsosn("1 01")).toBe(false); + expect(isValidCsosn("10.1")).toBe(false); + expect(isValidCsosn("1-0-1")).toBe(false); + expect(isValidCsosn(" 101 ")).toBe(true); + }); + it("should return false for a number that is not a non-negative safe integer", () => { expect(isValidCsosn(-101)).toBe(false); expect(isValidCsosn(10.1)).toBe(false); diff --git a/src/is-valid-csosn/is-valid-csosn.ts b/src/is-valid-csosn/is-valid-csosn.ts index a983076d..91f1fc7a 100644 --- a/src/is-valid-csosn/is-valid-csosn.ts +++ b/src/is-valid-csosn/is-valid-csosn.ts @@ -8,9 +8,10 @@ import { CSOSN_CODES, CSOSN_FORMAT_REGEX } from "./constants"; * Accepted codes are `101, 102, 103, 201, 202, 203, 300, 400, 500, 900`, the table the * consolidated Anexo III-A of Convênio SINIEF s/nº 1970 carries. * - * A string is only read as a code when it is written in one of the documented forms: the 3 - * digits, with a single separator between them and optional surrounding whitespace. Anything - * else (`"abc101"`) is rejected instead of having its digits picked out. A number is only read + * A string is only read as a code when it is written as the bare 3 digits with optional + * surrounding whitespace. A CSOSN has no printed grouping (the NF-e carries the origin digit in + * its own `orig` field), so a separator inside it (`"1-01"`) is rejected, and so is anything + * else (`"abc101"`) instead of having its digits picked out. A number is only read * as a code when it is a non-negative safe integer, since a sign, a decimal point or a rounded * magnitude would otherwise be read as a code the caller never wrote. * From 3600310f61a730be9e7211dbbf4697a0e2d5ceb0 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 17:58:30 -0300 Subject: [PATCH 47/75] docs(business-days): name the includeOptional and stateCode options in the three walkers - `addBusinessDays`, `subBusinessDays` and `differenceInBusinessDays` only pointed at `BusinessDayOptions`; the sections now name `options.includeOptional` (default `true`) and `options.stateCode` and say they behave as in `isBusinessDay`, in both languages --- docs/llms-full.txt | 6 +++--- docs/llms.txt | 4 ++-- docs/pt-br/utilities.md | 6 +++--- docs/utilities.md | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 92eef8d1..722b0244 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -1733,7 +1733,7 @@ isBusinessDay(new Date('not a date')); // false ### addBusinessDays -Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and Brazilian holidays exactly as `isBusinessDay` defines them (same `BusinessDayOptions`). The signature is date-fns': `addBusinessDays(date, amount, options?)`. Returns a new `Date`; the input `date` is never mutated, and its time-of-day is preserved in the result. An `amount` of `0` returns a new `Date` equal to `date`, unchanged, even when `date` itself falls on a weekend or holiday, this mirrors the verified behavior of [date-fns' `addBusinessDays(date, 0)`](https://date-fns.org/docs/addBusinessDays), which also does not roll the input to the next business day. A negative `amount` walks backwards, one business day at a time, also like date-fns. Returns `null` on bad input: a `date` that is not a valid `Date`, an `amount` that is not a finite integer, or a `stateCode` that is not a string; an `options` that is not an object at all is ignored, exactly as `isBusinessDay` ignores it. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it, or a walk that leaves it, returns `null`. +Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and Brazilian holidays exactly as `isBusinessDay` defines them (same `BusinessDayOptions`: `options.includeOptional`, default `true`, and `options.stateCode` work exactly as they do there). The signature is date-fns': `addBusinessDays(date, amount, options?)`. Returns a new `Date`; the input `date` is never mutated, and its time-of-day is preserved in the result. An `amount` of `0` returns a new `Date` equal to `date`, unchanged, even when `date` itself falls on a weekend or holiday, this mirrors the verified behavior of [date-fns' `addBusinessDays(date, 0)`](https://date-fns.org/docs/addBusinessDays), which also does not roll the input to the next business day. A negative `amount` walks backwards, one business day at a time, also like date-fns. Returns `null` on bad input: a `date` that is not a valid `Date`, an `amount` that is not a finite integer, or a `stateCode` that is not a string; an `options` that is not an object at all is ignored, exactly as `isBusinessDay` ignores it. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it, or a walk that leaves it, returns `null`. ```javascript import { addBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1749,7 +1749,7 @@ addBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) ### subBusinessDays -Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too, `options.stateCode` included. A negative `amount` walks forwards. +Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too, `options.stateCode` and `options.includeOptional` included. A negative `amount` walks forwards. ```javascript import { subBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1766,7 +1766,7 @@ subBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) ### differenceInBusinessDays -Count the number of Brazilian business days (dias úteis) between two dates, mirroring the semantics of [date-fns' `differenceInBusinessDays`](https://date-fns.org/docs/differenceInBusinessDays) (verified against its source), argument order included: `differenceInBusinessDays(laterDate, earlierDate, options?)`. The walk starts at `earlierDate` and stops just before `laterDate`, so `earlierDate` is counted when it is itself a business day, `laterDate` is never counted, and every business day strictly in between is counted once. Only the calendar day of each `Date` matters, the time of day is ignored. Business days are determined exactly like `isBusinessDay` (same `BusinessDayOptions`). The result is positive when `laterDate` is after `earlierDate` and negative when it is before it; two dates on the same calendar day return `0`. Returns `null` on bad input: a date that is not a valid `Date`, or a `stateCode` that is not a string; an `options` that is not an object at all is ignored. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `null`. +Count the number of Brazilian business days (dias úteis) between two dates, mirroring the semantics of [date-fns' `differenceInBusinessDays`](https://date-fns.org/docs/differenceInBusinessDays) (verified against its source), argument order included: `differenceInBusinessDays(laterDate, earlierDate, options?)`. The walk starts at `earlierDate` and stops just before `laterDate`, so `earlierDate` is counted when it is itself a business day, `laterDate` is never counted, and every business day strictly in between is counted once. Only the calendar day of each `Date` matters, the time of day is ignored. Business days are determined exactly like `isBusinessDay` (same `BusinessDayOptions`), `options.includeOptional` (default `true`) and `options.stateCode` included. The result is positive when `laterDate` is after `earlierDate` and negative when it is before it; two dates on the same calendar day return `0`. Returns `null` on bad input: a date that is not a valid `Date`, or a `stateCode` that is not a string; an `options` that is not an object at all is ignored. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `null`. ```javascript import { differenceInBusinessDays } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/llms.txt b/docs/llms.txt index 2ecc7825..efddb7aa 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -159,8 +159,8 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [convertLicensePlateToMercosul](https://brazilian-utils.com.br/utilities.md#convertlicenseplatetomercosul): Convert an old format Brazilian license plate (`LLLNNNN`) to the Mercosul format (`LLLNLNN`), following the official conversion table: the digit in the 5th position becomes a letter (`0` through `9` mapping to `A` through `J`). - [isHoliday](https://brazilian-utils.com.br/utilities.md#isholiday): Check if a specific date is a Brazilian holiday. - [isBusinessDay](https://brazilian-utils.com.br/utilities.md#isbusinessday): Check if a date is a Brazilian business day (dia útil). -- [addBusinessDays](https://brazilian-utils.com.br/utilities.md#addbusinessdays): Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and Brazilian holidays exactly as `isBusinessDay` defines them (same `BusinessDayOptions`). -- [subBusinessDays](https://brazilian-utils.com.br/utilities.md#subbusinessdays): Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too, `options.stateCode` included. +- [addBusinessDays](https://brazilian-utils.com.br/utilities.md#addbusinessdays): Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and Brazilian holidays exactly as `isBusinessDay` defines them (same `BusinessDayOptions`: `options.includeOptional`, default `true`, and `options.stateCode` work exactly as they do there). +- [subBusinessDays](https://brazilian-utils.com.br/utilities.md#subbusinessdays): Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too, `options.stateCode` and `options.includeOptional` included. - [differenceInBusinessDays](https://brazilian-utils.com.br/utilities.md#differenceinbusinessdays): Count the number of Brazilian business days (dias úteis) between two dates, mirroring the semantics of date-fns' `differenceInBusinessDays` (verified against its source), argument order included: `differenceInBusinessDays(laterDate, earlierDate, options?)`. - [convertDateToWords](https://brazilian-utils.com.br/utilities.md#convertdatetowords): Formats a date as its Brazilian Portuguese "por extenso" textual representation, e.g. `"01/01/2024"` becomes `"primeiro de janeiro de dois mil e vinte e quatro"`. - [removeAccents](https://brazilian-utils.com.br/utilities.md#removeaccents): Remove diacritical marks (accents, tildes, cedillas) from a string, decomposing every accented character into its base letter plus combining marks (Unicode NFD) and dropping the combining marks. diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index a592d0f6..2ea3f3d9 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -1493,7 +1493,7 @@ isBusinessDay(new Date('not a date')); // false ## addBusinessDays -Adiciona um número de dias úteis brasileiros a uma data, pulando sábados, domingos e feriados brasileiros exatamente como `isBusinessDay` os define (as mesmas `BusinessDayOptions`). A assinatura é a do date-fns: `addBusinessDays(date, amount, options?)`. Retorna um novo `Date`; a `date` de entrada nunca é alterada, e seu horário é preservado no resultado. Um `amount` igual a `0` retorna um novo `Date` igual a `date`, sem alterações, mesmo quando `date` cai em um fim de semana ou feriado, isso reflete o comportamento verificado de [`addBusinessDays(date, 0)` do date-fns](https://date-fns.org/docs/addBusinessDays), que também não avança a entrada para o próximo dia útil. Um `amount` negativo anda para trás, um dia útil por vez, também como no date-fns. Retorna `null` em caso de entrada inválida: uma `date` que não é um `Date` válido, um `amount` que não é um número inteiro finito, ou um `stateCode` que não é uma string; um `options` que não é um objeto é ignorado, exatamente como o `isBusinessDay` o ignora. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele, ou um percurso que sai dele, retorna `null`. +Adiciona um número de dias úteis brasileiros a uma data, pulando sábados, domingos e feriados brasileiros exatamente como `isBusinessDay` os define (as mesmas `BusinessDayOptions`: `options.includeOptional`, padrão `true`, e `options.stateCode` funcionam exatamente como lá). A assinatura é a do date-fns: `addBusinessDays(date, amount, options?)`. Retorna um novo `Date`; a `date` de entrada nunca é alterada, e seu horário é preservado no resultado. Um `amount` igual a `0` retorna um novo `Date` igual a `date`, sem alterações, mesmo quando `date` cai em um fim de semana ou feriado, isso reflete o comportamento verificado de [`addBusinessDays(date, 0)` do date-fns](https://date-fns.org/docs/addBusinessDays), que também não avança a entrada para o próximo dia útil. Um `amount` negativo anda para trás, um dia útil por vez, também como no date-fns. Retorna `null` em caso de entrada inválida: uma `date` que não é um `Date` válido, um `amount` que não é um número inteiro finito, ou um `stateCode` que não é uma string; um `options` que não é um objeto é ignorado, exatamente como o `isBusinessDay` o ignora. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele, ou um percurso que sai dele, retorna `null`. ```javascript import { addBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1509,7 +1509,7 @@ addBusinessDays(new Date(2024, 0, 2), 1.5); // null (não é um número inteiro) ## subBusinessDays -Subtrai um número de dias úteis brasileiros de uma data: `subBusinessDays(date, amount, options?)` é `addBusinessDays(date, -amount, options)`, e é exatamente assim que a função é implementada, então tudo o que vale acima vale aqui (o horário preservado, a entrada intacta, um `amount` igual a `0` devolvendo a data sem alterações, o intervalo de 1900 a 2099 e os casos de `null`), inclusive o `options.stateCode`. Um `amount` negativo anda para frente. +Subtrai um número de dias úteis brasileiros de uma data: `subBusinessDays(date, amount, options?)` é `addBusinessDays(date, -amount, options)`, e é exatamente assim que a função é implementada, então tudo o que vale acima vale aqui (o horário preservado, a entrada intacta, um `amount` igual a `0` devolvendo a data sem alterações, o intervalo de 1900 a 2099 e os casos de `null`), inclusive o `options.stateCode` e o `options.includeOptional`. Um `amount` negativo anda para frente. ```javascript import { subBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1526,7 +1526,7 @@ subBusinessDays(new Date(2024, 0, 2), 1.5); // null (não é um número inteiro) ## differenceInBusinessDays -Conta o número de dias úteis brasileiros entre duas datas, refletindo a semântica de [`differenceInBusinessDays` do date-fns](https://date-fns.org/docs/differenceInBusinessDays) (verificada em seu código-fonte), inclusive a ordem dos argumentos: `differenceInBusinessDays(laterDate, earlierDate, options?)`. O percurso começa em `earlierDate` e para logo antes de `laterDate`, então `earlierDate` é contado quando ele próprio é um dia útil, `laterDate` nunca é contado, e cada dia útil estritamente entre os dois é contado uma vez. Só a data de calendário de cada `Date` importa, o horário é ignorado. Os dias úteis são determinados exatamente como em `isBusinessDay` (as mesmas `BusinessDayOptions`). O resultado é positivo quando `laterDate` é posterior a `earlierDate` e negativo quando é anterior; duas datas no mesmo dia de calendário retornam `0`. Retorna `null` em caso de entrada inválida: uma data que não é um `Date` válido, ou um `stateCode` que não é uma string; um `options` que não é um objeto é ignorado. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele retorna `null`. +Conta o número de dias úteis brasileiros entre duas datas, refletindo a semântica de [`differenceInBusinessDays` do date-fns](https://date-fns.org/docs/differenceInBusinessDays) (verificada em seu código-fonte), inclusive a ordem dos argumentos: `differenceInBusinessDays(laterDate, earlierDate, options?)`. O percurso começa em `earlierDate` e para logo antes de `laterDate`, então `earlierDate` é contado quando ele próprio é um dia útil, `laterDate` nunca é contado, e cada dia útil estritamente entre os dois é contado uma vez. Só a data de calendário de cada `Date` importa, o horário é ignorado. Os dias úteis são determinados exatamente como em `isBusinessDay` (as mesmas `BusinessDayOptions`), inclusive o `options.includeOptional` (padrão `true`) e o `options.stateCode`. O resultado é positivo quando `laterDate` é posterior a `earlierDate` e negativo quando é anterior; duas datas no mesmo dia de calendário retornam `0`. Retorna `null` em caso de entrada inválida: uma data que não é um `Date` válido, ou um `stateCode` que não é uma string; um `options` que não é um objeto é ignorado. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele retorna `null`. ```javascript import { differenceInBusinessDays } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/utilities.md b/docs/utilities.md index d33127bb..75e8e23f 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -1493,7 +1493,7 @@ isBusinessDay(new Date('not a date')); // false ## addBusinessDays -Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and Brazilian holidays exactly as `isBusinessDay` defines them (same `BusinessDayOptions`). The signature is date-fns': `addBusinessDays(date, amount, options?)`. Returns a new `Date`; the input `date` is never mutated, and its time-of-day is preserved in the result. An `amount` of `0` returns a new `Date` equal to `date`, unchanged, even when `date` itself falls on a weekend or holiday, this mirrors the verified behavior of [date-fns' `addBusinessDays(date, 0)`](https://date-fns.org/docs/addBusinessDays), which also does not roll the input to the next business day. A negative `amount` walks backwards, one business day at a time, also like date-fns. Returns `null` on bad input: a `date` that is not a valid `Date`, an `amount` that is not a finite integer, or a `stateCode` that is not a string; an `options` that is not an object at all is ignored, exactly as `isBusinessDay` ignores it. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it, or a walk that leaves it, returns `null`. +Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and Brazilian holidays exactly as `isBusinessDay` defines them (same `BusinessDayOptions`: `options.includeOptional`, default `true`, and `options.stateCode` work exactly as they do there). The signature is date-fns': `addBusinessDays(date, amount, options?)`. Returns a new `Date`; the input `date` is never mutated, and its time-of-day is preserved in the result. An `amount` of `0` returns a new `Date` equal to `date`, unchanged, even when `date` itself falls on a weekend or holiday, this mirrors the verified behavior of [date-fns' `addBusinessDays(date, 0)`](https://date-fns.org/docs/addBusinessDays), which also does not roll the input to the next business day. A negative `amount` walks backwards, one business day at a time, also like date-fns. Returns `null` on bad input: a `date` that is not a valid `Date`, an `amount` that is not a finite integer, or a `stateCode` that is not a string; an `options` that is not an object at all is ignored, exactly as `isBusinessDay` ignores it. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it, or a walk that leaves it, returns `null`. ```javascript import { addBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1509,7 +1509,7 @@ addBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) ## subBusinessDays -Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too, `options.stateCode` included. A negative `amount` walks forwards. +Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too, `options.stateCode` and `options.includeOptional` included. A negative `amount` walks forwards. ```javascript import { subBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1526,7 +1526,7 @@ subBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) ## differenceInBusinessDays -Count the number of Brazilian business days (dias úteis) between two dates, mirroring the semantics of [date-fns' `differenceInBusinessDays`](https://date-fns.org/docs/differenceInBusinessDays) (verified against its source), argument order included: `differenceInBusinessDays(laterDate, earlierDate, options?)`. The walk starts at `earlierDate` and stops just before `laterDate`, so `earlierDate` is counted when it is itself a business day, `laterDate` is never counted, and every business day strictly in between is counted once. Only the calendar day of each `Date` matters, the time of day is ignored. Business days are determined exactly like `isBusinessDay` (same `BusinessDayOptions`). The result is positive when `laterDate` is after `earlierDate` and negative when it is before it; two dates on the same calendar day return `0`. Returns `null` on bad input: a date that is not a valid `Date`, or a `stateCode` that is not a string; an `options` that is not an object at all is ignored. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `null`. +Count the number of Brazilian business days (dias úteis) between two dates, mirroring the semantics of [date-fns' `differenceInBusinessDays`](https://date-fns.org/docs/differenceInBusinessDays) (verified against its source), argument order included: `differenceInBusinessDays(laterDate, earlierDate, options?)`. The walk starts at `earlierDate` and stops just before `laterDate`, so `earlierDate` is counted when it is itself a business day, `laterDate` is never counted, and every business day strictly in between is counted once. Only the calendar day of each `Date` matters, the time of day is ignored. Business days are determined exactly like `isBusinessDay` (same `BusinessDayOptions`), `options.includeOptional` (default `true`) and `options.stateCode` included. The result is positive when `laterDate` is after `earlierDate` and negative when it is before it; two dates on the same calendar day return `0`. Returns `null` on bad input: a date that is not a valid `Date`, or a `stateCode` that is not a string; an `options` that is not an object at all is ignored. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `null`. ```javascript import { differenceInBusinessDays } from '@brazilian-utils/brazilian-utils'; From 33e27be3e7ce35462d6c7cd70bab1308622f4746 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:13:14 -0300 Subject: [PATCH 48/75] feat(cnpj): accept a branch number in generateCnpj MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first argument now takes either the version, as before, or an object with the same version plus branch, the "número de ordem" block in positions 9 to 12. The branch is an integer from 1 to 9999, written zero padded to four characters; an invalid one is ignored and a random block is used, so the call never throws. Passing a plain 1 or 2 keeps working. --- docs/llms-full.txt | 4 +- docs/pt-br/utilities.md | 4 +- docs/utilities.md | 4 +- src/generate-cnpj/generate-cnpj.test.ts | 120 +++++++++++++++++++++++- src/generate-cnpj/generate-cnpj.ts | 96 +++++++++++++++---- src/index.test.ts | 2 + src/index.ts | 2 +- 7 files changed, 208 insertions(+), 24 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 722b0244..5b571504 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -341,13 +341,15 @@ isValidCep('12345'); // false (invalid length) ### generateCnpj -Generate a valid random CNPJ. Uses `Math.random()` internally, so it is not cryptographically secure. +Generate a valid random CNPJ. Uses `Math.random()` internally, so it is not cryptographically secure. The first argument is either the version, as before, or a `GenerateCnpjOptions` object with the same `version` plus `branch`, the "número de ordem" (filial) block in positions 9 to 12: an integer from 1 to 9999 written zero padded to four characters, random by default. An invalid `branch` is ignored and a random block is used, and the block stays numeric on the alphanumeric version. ```javascript import { generateCnpj } from '@brazilian-utils/brazilian-utils' generateCnpj(); generateCnpj(2); // alphanumeric CNPJ, e.g. 'Q0SLFMBD7VX439' +generateCnpj({ branch: 3 }); // ordem block '0003', e.g. '12345678000372' +generateCnpj({ version: 2, branch: 1 }); // alphanumeric CNPJ whose ordem block is '0001' ``` ### isValidBoleto diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 2ea3f3d9..58112ee8 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -101,13 +101,15 @@ isValidCep('12345'); // false (tamanho inválido) ## generateCnpj -Gera um CNPJ válido aleatório. Usa `Math.random()` internamente, então não é criptograficamente seguro. +Gera um CNPJ válido aleatório. Usa `Math.random()` internamente, então não é criptograficamente seguro. O primeiro argumento é a versão, como antes, ou um objeto `GenerateCnpjOptions` com a mesma `version` mais `branch`, o bloco do "número de ordem" (filial) nas posições 9 a 12: um inteiro de 1 a 9999 escrito com zeros à esquerda em quatro caracteres, aleatório por padrão. Um `branch` inválido é ignorado e um bloco aleatório é usado, e o bloco continua numérico na versão alfanumérica. ```javascript import { generateCnpj } from '@brazilian-utils/brazilian-utils' generateCnpj(); generateCnpj(2); // CNPJ alfanumérico, ex. 'Q0SLFMBD7VX439' +generateCnpj({ branch: 3 }); // bloco de ordem '0003', ex. '12345678000372' +generateCnpj({ version: 2, branch: 1 }); // CNPJ alfanumérico cujo bloco de ordem é '0001' ``` ## isValidBoleto diff --git a/docs/utilities.md b/docs/utilities.md index 75e8e23f..f5e7fb83 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -101,13 +101,15 @@ isValidCep('12345'); // false (invalid length) ## generateCnpj -Generate a valid random CNPJ. Uses `Math.random()` internally, so it is not cryptographically secure. +Generate a valid random CNPJ. Uses `Math.random()` internally, so it is not cryptographically secure. The first argument is either the version, as before, or a `GenerateCnpjOptions` object with the same `version` plus `branch`, the "número de ordem" (filial) block in positions 9 to 12: an integer from 1 to 9999 written zero padded to four characters, random by default. An invalid `branch` is ignored and a random block is used, and the block stays numeric on the alphanumeric version. ```javascript import { generateCnpj } from '@brazilian-utils/brazilian-utils' generateCnpj(); generateCnpj(2); // alphanumeric CNPJ, e.g. 'Q0SLFMBD7VX439' +generateCnpj({ branch: 3 }); // ordem block '0003', e.g. '12345678000372' +generateCnpj({ version: 2, branch: 1 }); // alphanumeric CNPJ whose ordem block is '0001' ``` ## isValidBoleto diff --git a/src/generate-cnpj/generate-cnpj.test.ts b/src/generate-cnpj/generate-cnpj.test.ts index c82a5782..54ba0d23 100644 --- a/src/generate-cnpj/generate-cnpj.test.ts +++ b/src/generate-cnpj/generate-cnpj.test.ts @@ -3,10 +3,20 @@ import * as fc from "fast-check"; import { CNPJ_LENGTH } from "../_internals/constants/cnpj"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidCnpj } from "../is-valid-cnpj/is-valid-cnpj"; -import { generateCnpj } from "./generate-cnpj"; +import { type GenerateCnpjOptions, generateCnpj } from "./generate-cnpj"; const REMAINDER_TWO_DRAWS = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; +const BRANCH_FALLBACK_DRAWS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2]; + +const INVALID_BRANCHES: [string, number][] = [ + ["0, below the first ordem", 0], + ["10000, past the last ordem", 10_000], + ["1.5, not an integer", 1.5], + ["-1, a negative ordem", -1], + ["NaN", Number.NaN], +]; + const generateWithForcedDraws = ( draws: number[], alphabetSize: number, @@ -159,6 +169,92 @@ describe("generateCnpj", () => { }); }); + describe("options object", () => { + test("should generate a numeric CNPJ for an empty options object", () => { + const cnpj = generateCnpj({}); + + expect(cnpj).toHaveLength(CNPJ_LENGTH); + expect(/^\d+$/.test(cnpj)).toBe(true); + expect(isValidCnpj(cnpj)).toBe(true); + }); + + test("should write the branch as the ordem block in positions 9 to 12", () => { + const cnpj = generateCnpj({ branch: 1 }); + + expect(cnpj.slice(8, 12)).toBe("0001"); + expect(isValidCnpj(cnpj)).toBe(true); + }); + + test("should zero pad a branch shorter than the four character ordem block", () => { + expect(generateCnpj({ branch: 3 }).slice(8, 12)).toBe("0003"); + expect(generateCnpj({ branch: 42 }).slice(8, 12)).toBe("0042"); + expect(generateCnpj({ branch: 500 }).slice(8, 12)).toBe("0500"); + }); + + test("should keep the ordem block numeric on the alphanumeric version, with letters in the raiz", () => { + const raizChars = new Set(); + + for (let index = 0; index < 100; index++) { + const cnpj = generateCnpj({ version: 2, branch: 9999 }); + + expect(cnpj).toHaveLength(CNPJ_LENGTH); + expect(cnpj.slice(8, 12)).toBe("9999"); + expect(isValidCnpj(cnpj, { version: 2 })).toBe(true); + + for (const char of cnpj.slice(0, 8)) { + raizChars.add(char); + } + } + + expect([...raizChars].some((char) => /[A-Z]/.test(char))).toBe(true); + }); + + test("should generate a numeric CNPJ with a branch when the version is 1", () => { + const cnpj = generateCnpj({ version: 1, branch: 1234 }); + + expect(/^\d+$/.test(cnpj)).toBe(true); + expect(cnpj.slice(8, 12)).toBe("1234"); + expect(isValidCnpj(cnpj)).toBe(true); + }); + + for (const [label, branch] of INVALID_BRANCHES) { + test(`should draw a random ordem block when the branch is ${label}`, () => { + const cnpj = generateWithForcedDraws(BRANCH_FALLBACK_DRAWS, 10, () => + generateCnpj({ branch }), + ); + + expect(cnpj).toBe("12345678901230"); + expect(isValidCnpj(cnpj)).toBe(true); + }); + } + + test("should draw a random ordem block when the branch is a string", () => { + const cnpj = generateWithForcedDraws(BRANCH_FALLBACK_DRAWS, 10, () => + // @ts-expect-error: intentionally invalid input + generateCnpj({ branch: "3" }), + ); + + expect(cnpj).toBe("12345678901230"); + }); + + test("should draw a random ordem block when the branch is null", () => { + const cnpj = generateWithForcedDraws(BRANCH_FALLBACK_DRAWS, 10, () => + // @ts-expect-error: intentionally invalid input + generateCnpj({ branch: null }), + ); + + expect(cnpj).toBe("12345678901230"); + }); + + test("should ignore an unknown version in the options object and generate a numeric CNPJ", () => { + // @ts-expect-error: intentionally invalid input + const cnpj = generateCnpj({ version: 3 }); + + expect(/^\d+$/.test(cnpj)).toBe(true); + expect(isValidCnpj(cnpj)).toBe(true); + }); + }); + describe("properties", () => { const batchSize = fc.integer({ min: 1, max: 10 }); @@ -189,12 +285,30 @@ describe("generateCnpj", () => { }), ); }); + + test("should write any ordem from 1 to 9999 into positions 9 to 12 of both versions", () => { + fc.assert( + fc.property(fc.integer({ min: 1, max: 9999 }), (branch) => { + const padded = `000${branch}`.slice(-4); + + expect(generateCnpj({ branch }).slice(8, 12)).toBe(padded); + expect(generateCnpj({ version: 2, branch }).slice(8, 12)).toBe(padded); + }), + ); + }); }); }); describe("generateCnpj types", () => { - test("should take an optional version and return a string", () => { - expectTypeOf(generateCnpj).parameter(0).toEqualTypeOf<1 | 2 | undefined>(); + test("should take an optional version or options object and return a string", () => { + expectTypeOf(generateCnpj) + .parameter(0) + .toEqualTypeOf<1 | 2 | GenerateCnpjOptions | undefined>(); expectTypeOf(generateCnpj).returns.toEqualTypeOf(); }); + + test("should take an optional version and branch in the options object", () => { + expectTypeOf().toEqualTypeOf<1 | 2 | undefined>(); + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/src/generate-cnpj/generate-cnpj.ts b/src/generate-cnpj/generate-cnpj.ts index ac05a2e0..2cf05815 100644 --- a/src/generate-cnpj/generate-cnpj.ts +++ b/src/generate-cnpj/generate-cnpj.ts @@ -3,21 +3,58 @@ import { generateChecksum } from "../_internals/generate-checksum/generate-check import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; -const BASE_LENGTH = 12; +const ROOT_LENGTH = 8; + +const BRANCH_LENGTH = 4; + +const MIN_BRANCH = 1; + +const MAX_BRANCH = 9999; const VALID_CNPJ_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; -const generateRandomCnpjChar = (): string => - VALID_CNPJ_CHARS.charAt(Math.floor(Math.random() * VALID_CNPJ_CHARS.length)); +/** + * The options `generateCnpj` accepts, an alternative to passing the version positionally. + */ +export type GenerateCnpjOptions = { + /** + * The version of the CNPJ to be generated: `1` for the numeric CNPJ and `2` for the + * alphanumeric one. Defaults to `1`, and any other runtime value also generates a version 1 + * (numeric) CNPJ. + */ + version?: 1 | 2; + /** + * The "número de ordem" (filial) block, positions 9 to 12 of the CNPJ: an integer from 1 to + * 9999, written zero padded to four characters (`3` becomes `"0003"`). Defaults to a random + * block, and an integer outside that range, a fractional number or any other runtime value is + * ignored, so a random block is used for those as well. The block stays numeric on the + * alphanumeric version, which the IN RFB nº 2.229/2024 layout allows. + */ + branch?: number; +}; -const generateAlphanumericCnpjBase = (): string => { - let base = ""; - for (let i = 0; i < BASE_LENGTH; i++) { - base += generateRandomCnpjChar(); +const generateRandomCnpjChars = (length: number): string => { + let chars = ""; + for (let i = 0; i < length; i++) { + chars += VALID_CNPJ_CHARS.charAt(Math.floor(Math.random() * VALID_CNPJ_CHARS.length)); } - return base; + return chars; }; +const isInteger = (value: unknown): value is number => Number.isInteger(value); + +const isBranchInRange = (branch: number | undefined): branch is number => + isInteger(branch) && branch >= MIN_BRANCH && branch <= MAX_BRANCH; + +const generateBase = ( + branch: number | undefined, + generatePart: (length: number) => string, +): string => + generatePart(ROOT_LENGTH) + + (isBranchInRange(branch) + ? branch.toString().padStart(BRANCH_LENGTH, "0") + : generatePart(BRANCH_LENGTH)); + const generateNonRepeatedBase = (generate: () => string): string => { let base = generate(); while (isRepeatedDigits(base)) { @@ -41,15 +78,15 @@ const calculateAlphanumericCheckDigit = (base: string, weights: number[]): strin return (mod < 2 ? 0 : 11 - mod).toString(); }; -const generateNumericCnpj = (): string => { - const base = generateNonRepeatedBase(() => generateRandomNumber(BASE_LENGTH)); +const generateNumericCnpj = (branch: number | undefined): string => { + const base = generateNonRepeatedBase(() => generateBase(branch, generateRandomNumber)); const firstCheckDigit = calculateCheckDigit(base, CNPJ_FIRST_DIGIT_WEIGHTS); const secondCheckDigit = calculateCheckDigit(base + firstCheckDigit, CNPJ_SECOND_DIGIT_WEIGHTS); return base + firstCheckDigit + secondCheckDigit; }; -const generateAlphanumericCnpj = (): string => { - const base = generateNonRepeatedBase(generateAlphanumericCnpjBase); +const generateAlphanumericCnpj = (branch: number | undefined): string => { + const base = generateNonRepeatedBase(() => generateBase(branch, generateRandomCnpjChars)); const firstCheckDigit = calculateAlphanumericCheckDigit(base, CNPJ_FIRST_DIGIT_WEIGHTS); const secondCheckDigit = calculateAlphanumericCheckDigit( base + firstCheckDigit, @@ -58,25 +95,50 @@ const generateAlphanumericCnpj = (): string => { return base + firstCheckDigit + secondCheckDigit; }; +const isGenerateCnpjOptions = ( + versionOrOptions: 1 | 2 | GenerateCnpjOptions, +): versionOrOptions is GenerateCnpjOptions => + typeof versionOrOptions === "object" && versionOrOptions !== null; + /** * Generates a valid random CNPJ (Cadastro Nacional da Pessoa Jurídica). * * Uses `Math.random()` internally, so it is not cryptographically secure, do not use for security purposes. * - * @param {1 | 2} [version] - The version of the CNPJ to be generated: `1` for the numeric CNPJ and - * `2` for the alphanumeric one. Defaults to `1`, and never throws: `null`, `undefined` and any - * other runtime value that is not `2` also generate a version 1 (numeric) CNPJ. + * The first argument is either the version, as it has always been, or a `GenerateCnpjOptions` + * object carrying that same version plus the "número de ordem" (filial) block to write in + * positions 9 to 12. + * + * @param {1 | 2 | GenerateCnpjOptions} [versionOrOptions] - The version of the CNPJ to be + * generated: `1` for the numeric CNPJ and `2` for the alphanumeric one, or an options object. + * Defaults to `1`, and never throws: `null`, `undefined` and any other runtime value that is + * neither `2` nor an object also generate a version 1 (numeric) CNPJ. + * @param {1 | 2} [versionOrOptions.version] - The version of the CNPJ to be generated, as above. + * @param {number} [versionOrOptions.branch] - The "número de ordem" (filial) block, an integer + * from 1 to 9999 written zero padded to four characters. Defaults to a random block, and an + * invalid branch is ignored rather than reported, so a random block is used for it too. * @returns {string} A valid 14-digit CNPJ string without formatting. * * @example * ```typescript * generateCnpj(); // "12345678000195" * generateCnpj(2); // "Q0SLFMBD7VX439" + * generateCnpj({ version: 2 }); // "Q0SLFMBD7VX439" + * generateCnpj({ branch: 3 }); // "12345678000372", the ordem block is "0003" + * generateCnpj({ version: 2, branch: 1 }); // "Q0SLFMBD000148", the ordem block is "0001" + * generateCnpj({ branch: 0 }); // "12345678472695", an out of range branch draws a random block * ``` * * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/cnpj * @see Official: https://www.gov.br/receitafederal/pt-br/centrais-de-conteudo/publicacoes/documentos-tecnicos/cnpj/manual-dv-cnpj.pdf * @see Official: https://www.gov.br/receitafederal/pt-br/acesso-a-informacao/acoes-e-programas/programas-e-atividades/cnpj-alfanumerico */ -export const generateCnpj = (version: 1 | 2 = 1): string => - version === 2 ? generateAlphanumericCnpj() : generateNumericCnpj(); +export const generateCnpj = (versionOrOptions: 1 | 2 | GenerateCnpjOptions = 1): string => { + const options: GenerateCnpjOptions = isGenerateCnpjOptions(versionOrOptions) + ? versionOrOptions + : { version: versionOrOptions }; + + return options.version === 2 + ? generateAlphanumericCnpj(options.branch) + : generateNumericCnpj(options.branch); +}; diff --git a/src/index.test.ts b/src/index.test.ts index 8fff53af..dd91fdb1 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -32,6 +32,7 @@ import { type FormatPisOptions, type FormatProcessoJuridicoOptions, type GenerateBoletoOptions, + type GenerateCnpjOptions, type GenerateLicensePlateFormat, type GeneratePhoneType, type GeneratePixPayloadParams, @@ -282,6 +283,7 @@ describe("Public API", () => { FormatPisOptions: FormatPisOptions; FormatProcessoJuridicoOptions: FormatProcessoJuridicoOptions; GenerateBoletoOptions: GenerateBoletoOptions; + GenerateCnpjOptions: GenerateCnpjOptions; GenerateLicensePlateFormat: GenerateLicensePlateFormat; GeneratePhoneType: GeneratePhoneType; GeneratePixPayloadParams: GeneratePixPayloadParams; diff --git a/src/index.ts b/src/index.ts index c64f074c..957bfa6b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,7 +43,7 @@ export { formatVoterId } from "./format-voter-id/format-voter-id"; export { type GenerateBoletoOptions, generateBoleto } from "./generate-boleto/generate-boleto"; export { generateCep } from "./generate-cep/generate-cep"; export { generateCnh } from "./generate-cnh/generate-cnh"; -export { generateCnpj } from "./generate-cnpj/generate-cnpj"; +export { type GenerateCnpjOptions, generateCnpj } from "./generate-cnpj/generate-cnpj"; export { generateCpf } from "./generate-cpf/generate-cpf"; export { generateLegalNature } from "./generate-legal-nature/generate-legal-nature"; export { From e46aa21d6910ba0179301d92a17fe6e30099d1a1 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:13:41 -0300 Subject: [PATCH 49/75] feat(renavam): add generateRenavam Generates a valid 11 digit RENAVAM with the check digit computed by the same modulus 11 rule isValidRenavam applies, now shared through an internal calculateRenavamCheckDigit helper. --- docs/llms-full.txt | 11 +++ docs/llms.txt | 1 + docs/pt-br/utilities.md | 10 ++ docs/utilities.md | 10 ++ .../calculate-renavam-check-digit.test.ts | 28 ++++++ .../calculate-renavam-check-digit.ts | 49 ++++++++++ src/generate-renavam/generate-renavam.test.ts | 93 +++++++++++++++++++ src/generate-renavam/generate-renavam.ts | 38 ++++++++ src/index.test.ts | 1 + src/index.ts | 1 + src/is-valid-renavam/is-valid-renavam.ts | 26 +----- 11 files changed, 247 insertions(+), 21 deletions(-) create mode 100644 src/_internals/calculate-renavam-check-digit/calculate-renavam-check-digit.test.ts create mode 100644 src/_internals/calculate-renavam-check-digit/calculate-renavam-check-digit.ts create mode 100644 src/generate-renavam/generate-renavam.test.ts create mode 100644 src/generate-renavam/generate-renavam.ts diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 5b571504..af2dd7a8 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -44,6 +44,7 @@ - [getAreaCodesByState](#getareacodesbystate) - [isValidLicensePlate](#isvalidlicenseplate) - [isValidRenavam](#isvalidrenavam) + - [generateRenavam](#generaterenavam) - [isValidPis](#isvalidpis) - [formatPis](#formatpis) - [parsePis](#parsepis) @@ -720,6 +721,16 @@ isValidRenavam('00000000000'); // false (repeated digits) isValidRenavam('ab00639884962'); // false (letters are rejected) ``` +### generateRenavam + +Generate a valid random RENAVAM: the 11 digit form, ten base digits plus the check digit. A base whose digits are all the same is drawn again, since `isValidRenavam` rejects those. Uses `Math.random()` internally, so it is not cryptographically secure. + +```javascript +import { generateRenavam } from '@brazilian-utils/brazilian-utils'; + +generateRenavam(); // '12345678900' +``` + ### isValidPis Check if PIS is valid. Accepts the usual mask characters (`.`, `-`, `/`, `(`, `)`, `,`, `*`) and whitespace. diff --git a/docs/llms.txt b/docs/llms.txt index efddb7aa..d04625bc 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -114,6 +114,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [generateCnpj](https://brazilian-utils.com.br/utilities.md#generatecnpj): Generate a valid random CNPJ. - [generateBoleto](https://brazilian-utils.com.br/utilities.md#generateboleto): Generate a valid random boleto. - [generatePixPayload](https://brazilian-utils.com.br/utilities.md#generatepixpayload): Generates the payload of a Pix BR Code. +- [generateRenavam](https://brazilian-utils.com.br/utilities.md#generaterenavam): Generate a valid random RENAVAM: the 11 digit form, ten base digits plus the check digit. - [generatePassport](https://brazilian-utils.com.br/utilities.md#generatepassport): Generate a random valid Brazilian passport number. - [generateCep](https://brazilian-utils.com.br/utilities.md#generatecep): Generate a random CEP. - [generateCnh](https://brazilian-utils.com.br/utilities.md#generatecnh): Generate a valid random CNH. diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 58112ee8..9724bc63 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -480,6 +480,16 @@ isValidRenavam('00000000000'); // false (dígitos repetidos) isValidRenavam('ab00639884962'); // false (letras são rejeitadas) ``` +## generateRenavam + +Gera um RENAVAM válido aleatório: o formato de 11 dígitos, dez dígitos de base mais o dígito verificador. Uma base com todos os dígitos iguais é sorteada de novo, já que `isValidRenavam` rejeita essas. Usa `Math.random()` internamente, então não é criptograficamente seguro. + +```javascript +import { generateRenavam } from '@brazilian-utils/brazilian-utils'; + +generateRenavam(); // '12345678900' +``` + ## isValidPis Valida se o PIS é válido. Aceita os caracteres de máscara usuais (`.`, `-`, `/`, `(`, `)`, `,`, `*`) e espaços em branco. diff --git a/docs/utilities.md b/docs/utilities.md index f5e7fb83..a93e9387 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -480,6 +480,16 @@ isValidRenavam('00000000000'); // false (repeated digits) isValidRenavam('ab00639884962'); // false (letters are rejected) ``` +## generateRenavam + +Generate a valid random RENAVAM: the 11 digit form, ten base digits plus the check digit. A base whose digits are all the same is drawn again, since `isValidRenavam` rejects those. Uses `Math.random()` internally, so it is not cryptographically secure. + +```javascript +import { generateRenavam } from '@brazilian-utils/brazilian-utils'; + +generateRenavam(); // '12345678900' +``` + ## isValidPis Check if PIS is valid. Accepts the usual mask characters (`.`, `-`, `/`, `(`, `)`, `,`, `*`) and whitespace. diff --git a/src/_internals/calculate-renavam-check-digit/calculate-renavam-check-digit.test.ts b/src/_internals/calculate-renavam-check-digit/calculate-renavam-check-digit.test.ts new file mode 100644 index 00000000..cd3a82b6 --- /dev/null +++ b/src/_internals/calculate-renavam-check-digit/calculate-renavam-check-digit.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from "../test/runtime"; +import { calculateRenavamCheckDigit } from "./calculate-renavam-check-digit"; + +describe("calculateRenavamCheckDigit", () => { + test("should return 2 for the base of 00639884962 (klawdyo/validation-br renavam fixture)", () => { + expect(calculateRenavamCheckDigit("0063988496")).toBe(2); + }); + + test("should return 0 for the base of 12345678900, where the multiplier wraps from 9 back to 2", () => { + expect(calculateRenavamCheckDigit("1234567890")).toBe(0); + }); + + test("should return 0 when the product leaves a remainder of 10 (base of 00000000060)", () => { + expect(calculateRenavamCheckDigit("0000000006")).toBe(0); + }); + + test("should return 1 for the base of 00000000051, where only the rightmost digit weighs", () => { + expect(calculateRenavamCheckDigit("0000000005")).toBe(1); + }); + + test("should return 6 for the base of 90000000006, where only the leftmost digit weighs", () => { + expect(calculateRenavamCheckDigit("9000000000")).toBe(6); + }); + + test("should return 0 for a base of only zeros", () => { + expect(calculateRenavamCheckDigit("0000000000")).toBe(0); + }); +}); diff --git a/src/_internals/calculate-renavam-check-digit/calculate-renavam-check-digit.ts b/src/_internals/calculate-renavam-check-digit/calculate-renavam-check-digit.ts new file mode 100644 index 00000000..6b0224db --- /dev/null +++ b/src/_internals/calculate-renavam-check-digit/calculate-renavam-check-digit.ts @@ -0,0 +1,49 @@ +const FIRST_MULTIPLIER = 2; + +const LAST_MULTIPLIER = 9; + +const MODULUS = 11; + +const SUM_SCALE = 10; + +const OVERFLOW_DIGIT = 10; + +/** + * Calculates the check digit of a RENAVAM (Registro Nacional de Veículos Automotores) base, the + * eleventh digit of the registration. + * + * The ten base digits are read from right to left and multiplied by 2, 3, 4, 5, 6, 7, 8, 9 and + * then 2 again, cycling back whenever the multiplier passes 9. The weighted sum is multiplied by + * ten and the check digit is the remainder of that product by eleven, with a remainder of ten + * mapped back to 0. + * + * The Código de Trânsito Brasileiro creates the RENAVAM registry but does not define its check + * digit, so the calculation follows the two community references cited below. + * + * @param {string} base - The ten digits that precede the check digit. + * @returns {number} The check digit, 0 to 9. + * + * @example + * ```typescript + * calculateRenavamCheckDigit("0063988496"); // 2 + * calculateRenavamCheckDigit("1234567890"); // 0 + * ``` + * + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l9503compilado.htm + * @see Based on: https://github.com/klawdyo/validation-br/blob/main/src/renavam.ts + * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/renavam.py + */ +export const calculateRenavamCheckDigit = (base: string): number => { + let sum = 0; + let multiplier = FIRST_MULTIPLIER; + + for (let index = base.length - 1; index >= 0; index--) { + sum += Number.parseInt(base.charAt(index), 10) * multiplier; + + multiplier = multiplier >= LAST_MULTIPLIER ? FIRST_MULTIPLIER : multiplier + 1; + } + + const digit = (sum * SUM_SCALE) % MODULUS; + + return digit === OVERFLOW_DIGIT ? 0 : digit; +}; diff --git a/src/generate-renavam/generate-renavam.test.ts b/src/generate-renavam/generate-renavam.test.ts new file mode 100644 index 00000000..081c3ebb --- /dev/null +++ b/src/generate-renavam/generate-renavam.test.ts @@ -0,0 +1,93 @@ +import * as fc from "fast-check"; + +import { bench, describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { isValidRenavam } from "../is-valid-renavam/is-valid-renavam"; +import { generateRenavam } from "./generate-renavam"; + +const generateWithDrawnDigits = (drawn: string): string => { + const realRandom = Math.random; + let position = 0; + + Math.random = (): number => { + const drawnDigit = Number(drawn.charAt(position)); + position += 1; + + return (drawnDigit + 0.5) / 10; + }; + + try { + return generateRenavam(); + } finally { + Math.random = realRandom; + } +}; + +describe("generateRenavam", () => { + test("should have the right length without mask (11)", () => { + expect(generateRenavam()).toHaveLength(11); + expect(/^\d{11}$/.test(generateRenavam())).toBe(true); + }); + + test("should always generate a valid RENAVAM", () => { + for (let i = 0; i < 1000; i++) { + expect(isValidRenavam(generateRenavam())).toBe(true); + } + }); + + test("should regenerate the base when it comes out with repeated digits", () => { + expect(generateWithDrawnDigits("00000000001234567890")).toBe("12345678900"); + }); + + test("should keep a check digit of 0 when the weighted product leaves a remainder of 10", () => { + expect(generateWithDrawnDigits("0000000006")).toBe("00000000060"); + }); + + test("should append the check digit the validator expects for a drawn base", () => { + expect(generateWithDrawnDigits("0063988496")).toBe("00639884962"); + expect(generateWithDrawnDigits("9000000000")).toBe("90000000006"); + }); + + describe("properties", () => { + const batchSize = fc.integer({ min: 1, max: 20 }); + + test("should generate 11 digit RENAVAM numbers its own validator accepts", () => { + fc.assert( + fc.property(batchSize, (size) => { + for (let index = 0; index < size; index++) { + const renavam = generateRenavam(); + + expect(renavam).toMatch(/^\d{11}$/); + expect(/^(\d)\1{9}/.test(renavam)).toBe(false); + expect(isValidRenavam(renavam)).toBe(true); + } + }), + ); + }); + + test("should generate registrations the usual mask characters do not change", () => { + fc.assert( + fc.property(batchSize, (size) => { + for (let index = 0; index < size; index++) { + const renavam = generateRenavam(); + + expect(isValidRenavam(`${renavam.slice(0, 7)}.${renavam.slice(7)}`)).toBe(true); + expect(isValidRenavam(` ${renavam.slice(0, 10)}-${renavam.slice(10)} `)).toBe(true); + } + }), + ); + }); + }); +}); + +describe("generateRenavam types", () => { + test("should take no parameters and return a string", () => { + expectTypeOf(generateRenavam).parameters.toEqualTypeOf<[]>(); + expectTypeOf(generateRenavam).returns.toEqualTypeOf(); + }); +}); + +describe("generateRenavam benchmarks", () => { + bench("generate a RENAVAM", () => { + generateRenavam(); + }); +}); diff --git a/src/generate-renavam/generate-renavam.ts b/src/generate-renavam/generate-renavam.ts new file mode 100644 index 00000000..1fd23071 --- /dev/null +++ b/src/generate-renavam/generate-renavam.ts @@ -0,0 +1,38 @@ +import { calculateRenavamCheckDigit } from "../_internals/calculate-renavam-check-digit/calculate-renavam-check-digit"; +import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; +import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; + +const BASE_LENGTH = 10; + +/** + * Generates a valid random RENAVAM (Registro Nacional de Veículos Automotores) number. + * + * The result is always the eleven digit form: ten base digits followed by the check digit. A base + * whose digits are all the same is drawn again, since `isValidRenavam` rejects a registration like + * `"00000000000"`. + * + * Uses `Math.random()` internally, so it is not cryptographically secure, do not use for security purposes. + * + * @returns {string} A valid 11-digit RENAVAM string without formatting. + * + * @example + * ```typescript + * generateRenavam(); // "12345678900" + * ``` + * + * The Código de Trânsito Brasileiro creates the RENAVAM registry but does not define its check + * digit, so the algorithm follows the two community references cited as `Based on:`. + * + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/l9503compilado.htm + * @see Based on: https://github.com/klawdyo/validation-br/blob/main/src/renavam.ts + * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/renavam.py + */ +export const generateRenavam = (): string => { + let base = generateRandomNumber(BASE_LENGTH); + + while (isRepeatedDigits(base)) { + base = generateRandomNumber(BASE_LENGTH); + } + + return `${base}${calculateRenavamCheckDigit(base)}`; +}; diff --git a/src/index.test.ts b/src/index.test.ts index dd91fdb1..0f309637 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -133,6 +133,7 @@ const PUBLIC = [ "generatePis", "generatePixPayload", "generateProcessoJuridico", + "generateRenavam", "generateVoterId", "getAddressInfoByCep", "getAreaCodeInfo", diff --git a/src/index.ts b/src/index.ts index 957bfa6b..db2e01a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,6 +61,7 @@ export { type GenerateProcessoJuridicoOptions, generateProcessoJuridico, } from "./generate-processo-juridico/generate-processo-juridico"; +export { generateRenavam } from "./generate-renavam/generate-renavam"; export { generateVoterId } from "./generate-voter-id/generate-voter-id"; export { type AddressInfo, diff --git a/src/is-valid-renavam/is-valid-renavam.ts b/src/is-valid-renavam/is-valid-renavam.ts index 277b55ba..28633133 100644 --- a/src/is-valid-renavam/is-valid-renavam.ts +++ b/src/is-valid-renavam/is-valid-renavam.ts @@ -1,7 +1,10 @@ +import { calculateRenavamCheckDigit } from "../_internals/calculate-renavam-check-digit/calculate-renavam-check-digit"; import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; const RENAVAM_LENGTH = 11; +const BASE_LENGTH = 10; + const SEPARATORS_REGEX = /[\s.-]/g; const FORMAT_REGEX = /^\d{9}$|^\d{11}$/; @@ -53,28 +56,9 @@ export const isValidRenavam = (renavam: string | number): boolean => { if (isRepeatedDigits(paddedDigits)) return false; - const renavamWithoutDigit = paddedDigits.slice(0, 10); - - let reversedRenavam = ""; - - for (const char of renavamWithoutDigit) { - reversedRenavam = char + reversedRenavam; - } - - let sum = 0; - let multiplier = 2; - for (const char of reversedRenavam) { - const digit = Number.parseInt(char, 10); - sum += digit * multiplier; - - multiplier = multiplier >= 9 ? 2 : multiplier + 1; - } - - const mod11 = sum % 11; - - const expectedDigit = mod11 <= 1 ? 0 : 11 - mod11; + const expectedDigit = calculateRenavamCheckDigit(paddedDigits.slice(0, BASE_LENGTH)); - const actualDigit = Number.parseInt(paddedDigits.charAt(10), 10); + const actualDigit = Number.parseInt(paddedDigits.charAt(BASE_LENGTH), 10); return expectedDigit === actualDigit; }; From defbdf83b8688f6a443b1f6e92d4769b039cbc8f Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:13:42 -0300 Subject: [PATCH 50/75] feat(legal-nature): expose the CONCLA category and add getLegalNaturesByCategory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getLegalNature now returns a category field with the code (1 to 5) and the description of the CONCLA group the legal nature belongs to, read from the first digit as the Tabela de Natureza Jurídica 2021 defines it. getLegalNaturesByCategory lists every code of one group, in ascending order, and returns an empty list for an unknown category. --- docs/llms-full.txt | 34 +- docs/llms.txt | 1 + docs/pt-br/utilities.md | 33 +- docs/utilities.md | 33 +- .../constants/legal-nature-categories.ts | 31 ++ src/get-legal-nature/get-legal-nature.test.ts | 75 +++-- src/get-legal-nature/get-legal-nature.ts | 30 +- .../get-legal-natures-by-category.test.ts | 292 ++++++++++++++++++ .../get-legal-natures-by-category.ts | 59 ++++ .../get-legal-natures.test.ts | 2 +- src/index.test.ts | 3 + src/index.ts | 7 +- 12 files changed, 558 insertions(+), 42 deletions(-) create mode 100644 src/_internals/constants/legal-nature-categories.ts create mode 100644 src/get-legal-natures-by-category/get-legal-natures-by-category.test.ts create mode 100644 src/get-legal-natures-by-category/get-legal-natures-by-category.ts diff --git a/docs/llms-full.txt b/docs/llms-full.txt index af2dd7a8..3283b069 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -91,6 +91,7 @@ - [generateLegalNature](#generatelegalnature) - [parseLegalNature](#parselegalnature) - [getLegalNatures](#getlegalnatures) + - [getLegalNaturesByCategory](#getlegalnaturesbycategory) - [getLegalNature](#getlegalnature) - [generatePhone](#generatephone) - [formatLicensePlate](#formatlicenseplate) @@ -1520,22 +1521,45 @@ const legalNatures = getLegalNatures(); legalNatures['2062']; // 'Sociedade Empresária Limitada' ``` +### getLegalNaturesByCategory + +Get every legal nature of a CONCLA category, the group given by the first digit of the code: `1` Administração Pública, `2` Entidades Empresariais, `3` Entidades sem Fins Lucrativos, `4` Pessoas Físicas and `5` Organizações Internacionais e Outras Instituições Extraterritoriais. The category is accepted as a string or as a number, the entries come back sorted by code, and an unknown category gives `[]`. + +```javascript +import { getLegalNaturesByCategory } from '@brazilian-utils/brazilian-utils'; + +getLegalNaturesByCategory('4')[0]; +// { +// code: '4014', +// description: 'Empresa Individual Imobiliária', +// category: { code: '4', description: 'Pessoas Físicas' }, +// } +getLegalNaturesByCategory(4).length; // 6 +getLegalNaturesByCategory('2').length; // 33 +getLegalNaturesByCategory('9'); // [] +``` + ### getLegalNature -Look a legal nature code up in the official IBGE/CONCLA table. +Look a legal nature code up in the official IBGE/CONCLA table. The entry also carries the CONCLA category the code is listed under, taken from its first digit. ```javascript import { getLegalNature } from '@brazilian-utils/brazilian-utils'; -getLegalNature('2062'); // { code: '2062', description: 'Sociedade Empresária Limitada' } -getLegalNature('206-2'); // { code: '2062', description: 'Sociedade Empresária Limitada' } -getLegalNature(206.2); // { code: '2062', description: 'Sociedade Empresária Limitada' } +getLegalNature('2062'); +// { +// code: '2062', +// description: 'Sociedade Empresária Limitada', +// category: { code: '2', description: 'Entidades Empresariais' }, +// } +getLegalNature('206-2')?.code; // '2062' +getLegalNature(206.2)?.category.description; // 'Entidades Empresariais' getLegalNature('0000'); // null ``` ### generatePhone -Generate a random Brazilian phone number. Accepts `'mobile'`, `'landline'` or `'service'` (typed as `GeneratePhoneType`); a service number has no DDD. Omitted, it randomly generates a mobile or a landline, never a service number. +Generate a random Brazilian phone number. Accepts `'mobile'`, `'landline'` or `'service'` (typed as `GeneratePhoneType`); a service number has no DDD. Omitted, it randomly generates a mobile or a landline, never a service number. A generated mobile number always starts with 9, so it passes both `isValidMobilePhone` numbering rules. ```javascript import { generatePhone } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/llms.txt b/docs/llms.txt index d04625bc..a0acf1e0 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -143,6 +143,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [getHolidays](https://brazilian-utils.com.br/utilities.md#getholidays): Get Brazilian holidays for a given year. - [getCepInfoByAddress](https://brazilian-utils.com.br/utilities.md#getcepinfobyaddress): Fetch CEPs from an address using ViaCEP. - [getLegalNatures](https://brazilian-utils.com.br/utilities.md#getlegalnatures): Get the legal nature map keyed by code. +- [getLegalNaturesByCategory](https://brazilian-utils.com.br/utilities.md#getlegalnaturesbycategory): Get every legal nature of a CONCLA category, the group given by the first digit of the code: `1` Administração Pública, `2` Entidades Empresariais, `3` Entidades sem Fins Lucrativos, `4` Pessoas Físicas and `5` Organizações Internacionais e Outras Instituições Extraterritoriais. - [getLegalNature](https://brazilian-utils.com.br/utilities.md#getlegalnature): Look a legal nature code up in the official IBGE/CONCLA table. - [getFormatLicensePlate](https://brazilian-utils.com.br/utilities.md#getformatlicenseplate): Detect the normalized format of a license plate. - [getMunicipality](https://brazilian-utils.com.br/utilities.md#getmunicipality): Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 9724bc63..2f23cb23 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -1279,22 +1279,45 @@ const legalNatures = getLegalNatures(); legalNatures['2062']; // 'Sociedade Empresária Limitada' ``` +## getLegalNaturesByCategory + +Retorna todas as naturezas jurídicas de uma categoria do CONCLA, o grupo dado pelo primeiro dígito do código: `1` Administração Pública, `2` Entidades Empresariais, `3` Entidades sem Fins Lucrativos, `4` Pessoas Físicas e `5` Organizações Internacionais e Outras Instituições Extraterritoriais. A categoria é aceita como string ou como número, as entradas voltam ordenadas por código e uma categoria desconhecida devolve `[]`. + +```javascript +import { getLegalNaturesByCategory } from '@brazilian-utils/brazilian-utils'; + +getLegalNaturesByCategory('4')[0]; +// { +// code: '4014', +// description: 'Empresa Individual Imobiliária', +// category: { code: '4', description: 'Pessoas Físicas' }, +// } +getLegalNaturesByCategory(4).length; // 6 +getLegalNaturesByCategory('2').length; // 33 +getLegalNaturesByCategory('9'); // [] +``` + ## getLegalNature -Busca um código de natureza jurídica na tabela oficial do IBGE/CONCLA. +Busca um código de natureza jurídica na tabela oficial do IBGE/CONCLA. A entrada também traz a categoria do CONCLA em que o código está listado, dada pelo seu primeiro dígito. ```javascript import { getLegalNature } from '@brazilian-utils/brazilian-utils'; -getLegalNature('2062'); // { code: '2062', description: 'Sociedade Empresária Limitada' } -getLegalNature('206-2'); // { code: '2062', description: 'Sociedade Empresária Limitada' } -getLegalNature(206.2); // { code: '2062', description: 'Sociedade Empresária Limitada' } +getLegalNature('2062'); +// { +// code: '2062', +// description: 'Sociedade Empresária Limitada', +// category: { code: '2', description: 'Entidades Empresariais' }, +// } +getLegalNature('206-2')?.code; // '2062' +getLegalNature(206.2)?.category.description; // 'Entidades Empresariais' getLegalNature('0000'); // null ``` ## generatePhone -Gera um telefone brasileiro aleatório. Aceita `'mobile'`, `'landline'` ou `'service'` (tipado como `GeneratePhoneType`); um número de serviço não tem DDD. Se omitido, gera aleatoriamente um celular ou um fixo, nunca um número de serviço. +Gera um telefone brasileiro aleatório. Aceita `'mobile'`, `'landline'` ou `'service'` (tipado como `GeneratePhoneType`); um número de serviço não tem DDD. Se omitido, gera aleatoriamente um celular ou um fixo, nunca um número de serviço. Um celular gerado sempre começa com 9, então passa nas duas regras de numeração do `isValidMobilePhone`. ```javascript import { generatePhone } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/utilities.md b/docs/utilities.md index a93e9387..082328ef 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -1279,22 +1279,45 @@ const legalNatures = getLegalNatures(); legalNatures['2062']; // 'Sociedade Empresária Limitada' ``` +## getLegalNaturesByCategory + +Get every legal nature of a CONCLA category, the group given by the first digit of the code: `1` Administração Pública, `2` Entidades Empresariais, `3` Entidades sem Fins Lucrativos, `4` Pessoas Físicas and `5` Organizações Internacionais e Outras Instituições Extraterritoriais. The category is accepted as a string or as a number, the entries come back sorted by code, and an unknown category gives `[]`. + +```javascript +import { getLegalNaturesByCategory } from '@brazilian-utils/brazilian-utils'; + +getLegalNaturesByCategory('4')[0]; +// { +// code: '4014', +// description: 'Empresa Individual Imobiliária', +// category: { code: '4', description: 'Pessoas Físicas' }, +// } +getLegalNaturesByCategory(4).length; // 6 +getLegalNaturesByCategory('2').length; // 33 +getLegalNaturesByCategory('9'); // [] +``` + ## getLegalNature -Look a legal nature code up in the official IBGE/CONCLA table. +Look a legal nature code up in the official IBGE/CONCLA table. The entry also carries the CONCLA category the code is listed under, taken from its first digit. ```javascript import { getLegalNature } from '@brazilian-utils/brazilian-utils'; -getLegalNature('2062'); // { code: '2062', description: 'Sociedade Empresária Limitada' } -getLegalNature('206-2'); // { code: '2062', description: 'Sociedade Empresária Limitada' } -getLegalNature(206.2); // { code: '2062', description: 'Sociedade Empresária Limitada' } +getLegalNature('2062'); +// { +// code: '2062', +// description: 'Sociedade Empresária Limitada', +// category: { code: '2', description: 'Entidades Empresariais' }, +// } +getLegalNature('206-2')?.code; // '2062' +getLegalNature(206.2)?.category.description; // 'Entidades Empresariais' getLegalNature('0000'); // null ``` ## generatePhone -Generate a random Brazilian phone number. Accepts `'mobile'`, `'landline'` or `'service'` (typed as `GeneratePhoneType`); a service number has no DDD. Omitted, it randomly generates a mobile or a landline, never a service number. +Generate a random Brazilian phone number. Accepts `'mobile'`, `'landline'` or `'service'` (typed as `GeneratePhoneType`); a service number has no DDD. Omitted, it randomly generates a mobile or a landline, never a service number. A generated mobile number always starts with 9, so it passes both `isValidMobilePhone` numbering rules. ```javascript import { generatePhone } from '@brazilian-utils/brazilian-utils'; diff --git a/src/_internals/constants/legal-nature-categories.ts b/src/_internals/constants/legal-nature-categories.ts new file mode 100644 index 00000000..1b483ffd --- /dev/null +++ b/src/_internals/constants/legal-nature-categories.ts @@ -0,0 +1,31 @@ +/** The CONCLA category (natureza jurídica group) a legal nature code belongs to. */ +export type LegalNatureCategory = { + /** The category code, the first digit shared by every legal nature code in the group. */ + code: "1" | "2" | "3" | "4" | "5"; + /** The official category title in Portuguese, per IBGE/CONCLA. */ + description: string; +}; + +/** + * The five categories of the Tabela de Natureza Jurídica 2021 (IBGE/CONCLA), indexed by the + * first digit of the four digit code: the table groups its codes under these headings, so + * "2062" (Sociedade Empresária Limitada) belongs to "2" (Entidades Empresariais). The legacy + * codes kept in `LEGAL_NATURE` for 2.3.0 compatibility follow the same rule. + * + * The CONCLA table page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser; the detailed structure PDF next to it is served + * normally and prints the same five headings. + * + * @see Official: https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 + * @see Official: https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf + */ +export const LEGAL_NATURE_CATEGORIES: Record = { + "1": { code: "1", description: "Administração Pública" }, + "2": { code: "2", description: "Entidades Empresariais" }, + "3": { code: "3", description: "Entidades sem Fins Lucrativos" }, + "4": { code: "4", description: "Pessoas Físicas" }, + "5": { + code: "5", + description: "Organizações Internacionais e Outras Instituições Extraterritoriais", + }, +}; diff --git a/src/get-legal-nature/get-legal-nature.test.ts b/src/get-legal-nature/get-legal-nature.test.ts index d68d0213..107c3763 100644 --- a/src/get-legal-nature/get-legal-nature.test.ts +++ b/src/get-legal-nature/get-legal-nature.test.ts @@ -1,47 +1,69 @@ import * as fc from "fast-check"; +import { LEGAL_NATURE_CATEGORIES } from "../_internals/constants/legal-nature-categories"; import { anyValue, digitsUpTo } from "../_internals/test/arbitraries"; import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { LEGAL_NATURE } from "../is-valid-legal-nature/constants"; import { isValidLegalNature } from "../is-valid-legal-nature/is-valid-legal-nature"; -import { getLegalNature, type LegalNature } from "./get-legal-nature"; +import { getLegalNature, type LegalNature, type LegalNatureCategory } from "./get-legal-nature"; + +const SOCIEDADE_EMPRESARIA_LIMITADA: LegalNature = { + code: "2062", + description: "Sociedade Empresária Limitada", + category: { code: "2", description: "Entidades Empresariais" }, +}; describe("getLegalNature", () => { it("should reject a code with letters attached, like isValidLegalNature does", () => { expect(getLegalNature("2062a")).toBeNull(); expect(getLegalNature("a2062")).toBeNull(); - expect(getLegalNature("206-2")).toEqual({ - code: "2062", - description: getLegalNature("2062")?.description, - }); + expect(getLegalNature("206-2")).toEqual(SOCIEDADE_EMPRESARIA_LIMITADA); }); it("should return the legal nature entry for a known code as a string", () => { - expect(getLegalNature("2062")).toEqual({ - code: "2062", - description: "Sociedade Empresária Limitada", - }); + expect(getLegalNature("2062")).toEqual(SOCIEDADE_EMPRESARIA_LIMITADA); }); it("should return the legal nature entry for a known code as a number", () => { - expect(getLegalNature(2062)).toEqual({ - code: "2062", - description: "Sociedade Empresária Limitada", - }); + expect(getLegalNature(2062)).toEqual(SOCIEDADE_EMPRESARIA_LIMITADA); }); it("should strip the mask of a number just like the mask of a string", () => { - expect(getLegalNature(206.2)).toEqual({ - code: "2062", - description: "Sociedade Empresária Limitada", - }); + expect(getLegalNature(206.2)).toEqual(SOCIEDADE_EMPRESARIA_LIMITADA); expect(getLegalNature(206.2)).toEqual(getLegalNature("206.2")); }); it("should return the legal nature entry for a masked code (206-2)", () => { - expect(getLegalNature("206-2")).toEqual({ - code: "2062", - description: "Sociedade Empresária Limitada", + expect(getLegalNature("206-2")).toEqual(SOCIEDADE_EMPRESARIA_LIMITADA); + }); + + it("should carry the CONCLA category of the first digit of the code", () => { + expect(getLegalNature("1015")?.category).toEqual({ + code: "1", + description: "Administração Pública", + }); + expect(getLegalNature("3034")?.category).toEqual({ + code: "3", + description: "Entidades sem Fins Lucrativos", + }); + expect(getLegalNature("4014")?.category).toEqual({ + code: "4", + description: "Pessoas Físicas", + }); + expect(getLegalNature("5010")?.category).toEqual({ + code: "5", + description: "Organizações Internacionais e Outras Instituições Extraterritoriais", + }); + }); + + it("should carry the category of the first digit for a legacy code too", () => { + expect(getLegalNature("2208")?.category).toEqual({ + code: "2", + description: "Entidades Empresariais", + }); + expect(getLegalNature("5002")?.category).toEqual({ + code: "5", + description: "Organizações Internacionais e Outras Instituições Extraterritoriais", }); }); @@ -49,6 +71,7 @@ describe("getLegalNature", () => { const first = getLegalNature("2062"); const second = getLegalNature("2062"); expect(first).not.toBe(second); + expect(first?.category).not.toBe(second?.category); }); it("should return null for an unknown 4 digit code", () => { @@ -83,7 +106,11 @@ describe("getLegalNature", () => { test("should look every code of the table up, masked, plain or numeric", () => { fc.assert( fc.property(knownCode, (code) => { - const entry = { code, description: LEGAL_NATURE[code] }; + const entry = { + code, + description: LEGAL_NATURE[code], + category: LEGAL_NATURE_CATEGORIES[code[0]], + }; expect(getLegalNature(code)).toEqual(entry); expect(getLegalNature(`${code.slice(0, 3)}-${code.slice(3)}`)).toEqual(entry); @@ -122,4 +149,10 @@ describe("getLegalNature types", () => { expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); }); + + test("should type the category as a code of the five CONCLA groups and a description", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<"1" | "2" | "3" | "4" | "5">(); + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/src/get-legal-nature/get-legal-nature.ts b/src/get-legal-nature/get-legal-nature.ts index 88dfdb4d..156858e7 100644 --- a/src/get-legal-nature/get-legal-nature.ts +++ b/src/get-legal-nature/get-legal-nature.ts @@ -1,5 +1,11 @@ +import { + LEGAL_NATURE_CATEGORIES, + type LegalNatureCategory, +} from "../_internals/constants/legal-nature-categories"; import { LEGAL_NATURE, MASK_REGEX } from "../is-valid-legal-nature/constants"; +export type { LegalNatureCategory } from "../_internals/constants/legal-nature-categories"; + /** * A Brazilian legal nature (natureza jurídica) entry. */ @@ -8,12 +14,18 @@ export type LegalNature = { code: string; /** The official description in Portuguese, per IBGE/CONCLA. */ description: string; + /** The CONCLA category the code belongs to, given by its first digit. */ + category: LegalNatureCategory; }; const lookUp = (code: string): LegalNature | null => { if (!Object.hasOwn(LEGAL_NATURE, code)) return null; - return { code, description: LEGAL_NATURE[code] }; + return { + code, + description: LEGAL_NATURE[code], + category: { ...LEGAL_NATURE_CATEGORIES[code[0]] }, + }; }; /** @@ -22,6 +34,11 @@ const lookUp = (code: string): LegalNature | null => { * The usual mask characters (hyphens, dots, whitespace) are stripped before the lookup, from a * number as well as from a string, so `getLegalNature(206.2)` resolves like `getLegalNature("206.2")`. * + * The entry also carries the CONCLA category of the code, the group the table lists it under, + * taken from its first digit: 1 Administração Pública, 2 Entidades Empresariais, 3 Entidades + * sem Fins Lucrativos, 4 Pessoas Físicas and 5 Organizações Internacionais e Outras + * Instituições Extraterritoriais. + * * @param {string|number} value - The legal nature code to look up, with or without formatting. * @returns {LegalNature|null} The matching legal nature entry, or null when the code is unknown * or invalid. @@ -35,9 +52,14 @@ const lookUp = (code: string): LegalNature | null => { * * @example * ```typescript - * getLegalNature("2062"); // { code: "2062", description: "Sociedade Empresária Limitada" } - * getLegalNature("206-2"); // { code: "2062", description: "Sociedade Empresária Limitada" } - * getLegalNature(206.2); // { code: "2062", description: "Sociedade Empresária Limitada" } + * getLegalNature("2062"); + * // { + * // code: "2062", + * // description: "Sociedade Empresária Limitada", + * // category: { code: "2", description: "Entidades Empresariais" }, + * // } + * getLegalNature("206-2")?.code; // "2062" + * getLegalNature(206.2)?.category.description; // "Entidades Empresariais" * getLegalNature("0000"); // null * ``` */ diff --git a/src/get-legal-natures-by-category/get-legal-natures-by-category.test.ts b/src/get-legal-natures-by-category/get-legal-natures-by-category.test.ts new file mode 100644 index 00000000..e82b1da6 --- /dev/null +++ b/src/get-legal-natures-by-category/get-legal-natures-by-category.test.ts @@ -0,0 +1,292 @@ +import * as fc from "fast-check"; + +import { LEGAL_NATURE_CATEGORIES } from "../_internals/constants/legal-nature-categories"; +import { anyValue } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { getLegalNature, type LegalNature } from "../get-legal-nature/get-legal-nature"; +import { LEGAL_NATURE } from "../is-valid-legal-nature/constants"; +import { getLegalNaturesByCategory } from "./get-legal-natures-by-category"; + +const PESSOAS_FISICAS: LegalNature[] = [ + { + code: "4014", + description: "Empresa Individual Imobiliária", + category: { code: "4", description: "Pessoas Físicas" }, + }, + { + code: "4022", + description: "Segurado Especial", + category: { code: "4", description: "Pessoas Físicas" }, + }, + { + code: "4081", + description: "Contribuinte individual", + category: { code: "4", description: "Pessoas Físicas" }, + }, + { + code: "4090", + description: "Candidato a Cargo Político Eletivo", + category: { code: "4", description: "Pessoas Físicas" }, + }, + { + code: "4111", + description: "Leiloeiro", + category: { code: "4", description: "Pessoas Físicas" }, + }, + { + code: "4120", + description: "Produtor Rural (Pessoa Física)", + category: { code: "4", description: "Pessoas Físicas" }, + }, +]; + +const EXTRATERRITORIAIS: LegalNature[] = [ + { + code: "5002", + description: "Organização Internacional e Outras Instituições Extraterritoriais", + category: { + code: "5", + description: "Organizações Internacionais e Outras Instituições Extraterritoriais", + }, + }, + { + code: "5010", + description: "Organização Internacional", + category: { + code: "5", + description: "Organizações Internacionais e Outras Instituições Extraterritoriais", + }, + }, + { + code: "5029", + description: "Representação Diplomática Estrangeira", + category: { + code: "5", + description: "Organizações Internacionais e Outras Instituições Extraterritoriais", + }, + }, + { + code: "5037", + description: "Outras Instituições Extraterritoriais", + category: { + code: "5", + description: "Organizações Internacionais e Outras Instituições Extraterritoriais", + }, + }, +]; + +const codesOf = (category: string | number): string[] => + getLegalNaturesByCategory(category).map((legalNature) => legalNature.code); + +describe("getLegalNaturesByCategory", () => { + test("should return the whole category as entries, ascending by code", () => { + expect(getLegalNaturesByCategory("4")).toEqual(PESSOAS_FISICAS); + }); + + test("should list the legacy codes of the category too, in code order", () => { + expect(getLegalNaturesByCategory("5")).toEqual(EXTRATERRITORIAIS); + }); + + test("should accept the category code as a number", () => { + expect(getLegalNaturesByCategory(4)).toEqual(PESSOAS_FISICAS); + expect(getLegalNaturesByCategory(5)).toEqual(EXTRATERRITORIAIS); + }); + + test("should list the 32 codes of Administração Pública", () => { + expect(codesOf("1")).toEqual([ + "1015", + "1023", + "1031", + "1040", + "1058", + "1066", + "1074", + "1082", + "1104", + "1112", + "1120", + "1139", + "1147", + "1155", + "1163", + "1171", + "1180", + "1198", + "1210", + "1228", + "1236", + "1244", + "1252", + "1260", + "1279", + "1287", + "1295", + "1309", + "1317", + "1325", + "1333", + "1341", + ]); + }); + + test("should list the 33 codes of Entidades Empresariais, legacy ones in place", () => { + expect(codesOf("2")).toEqual([ + "2011", + "2038", + "2046", + "2054", + "2062", + "2070", + "2076", + "2089", + "2097", + "2100", + "2127", + "2135", + "2143", + "2151", + "2160", + "2178", + "2194", + "2208", + "2216", + "2224", + "2232", + "2240", + "2259", + "2267", + "2275", + "2283", + "2291", + "2305", + "2313", + "2321", + "2330", + "2348", + "2356", + ]); + }); + + test("should list the 25 codes of Entidades sem Fins Lucrativos", () => { + expect(codesOf("3")).toEqual([ + "3034", + "3042", + "3050", + "3069", + "3077", + "3085", + "3093", + "3107", + "3115", + "3123", + "3131", + "3204", + "3212", + "3220", + "3239", + "3247", + "3255", + "3263", + "3271", + "3280", + "3298", + "3301", + "3310", + "3328", + "3999", + ]); + }); + + test("should return a fresh array of fresh entries on every call", () => { + const first = getLegalNaturesByCategory("5"); + const second = getLegalNaturesByCategory("5"); + + expect(first).not.toBe(second); + expect(first[0]).not.toBe(second[0]); + expect(first[0].category).not.toBe(second[0].category); + }); + + test("should return an empty array for a category outside 1 to 5", () => { + expect(getLegalNaturesByCategory("0")).toEqual([]); + expect(getLegalNaturesByCategory("6")).toEqual([]); + expect(getLegalNaturesByCategory("9")).toEqual([]); + expect(getLegalNaturesByCategory(9)).toEqual([]); + }); + + test("should return an empty array for a prefix that is not a category on its own", () => { + expect(getLegalNaturesByCategory("20")).toEqual([]); + expect(getLegalNaturesByCategory("2062")).toEqual([]); + expect(getLegalNaturesByCategory(20)).toEqual([]); + }); + + test("should return an empty array for an empty or padded category code", () => { + expect(getLegalNaturesByCategory("")).toEqual([]); + expect(getLegalNaturesByCategory(" 2")).toEqual([]); + expect(getLegalNaturesByCategory("02")).toEqual([]); + }); + + test("should return an empty array for a value that is not a string or a number", () => { + // @ts-expect-error not a string or number + expect(getLegalNaturesByCategory(null)).toEqual([]); + // @ts-expect-error not a string or number + expect(getLegalNaturesByCategory()).toEqual([]); + // @ts-expect-error not a string or number + expect(getLegalNaturesByCategory(["2"])).toEqual([]); + expect(getLegalNaturesByCategory(Object.create(null))).toEqual([]); + }); + + test("should partition the whole table across the five categories", () => { + const codes = Object.keys(LEGAL_NATURE_CATEGORIES).flatMap((category) => codesOf(category)); + + expect(codes.length).toBe(100); + expect(new Set(codes).size).toBe(100); + expect(codes.every((code) => Object.hasOwn(LEGAL_NATURE, code))).toBe(true); + }); + + describe("properties", () => { + const categoryCodes = fc.constantFrom(...Object.keys(LEGAL_NATURE_CATEGORIES)); + + test("should return the same entry getLegalNature returns for every code it lists", () => { + fc.assert( + fc.property(categoryCodes, (category) => { + for (const legalNature of getLegalNaturesByCategory(category)) { + expect(legalNature).toEqual(getLegalNature(legalNature.code)); + expect(legalNature.code.startsWith(category)).toBe(true); + } + }), + ); + }); + + test("should return the codes of a category in ascending order", () => { + fc.assert( + fc.property(categoryCodes, (category) => { + const codes = codesOf(category); + + expect(codes).toEqual([...codes].sort((a, b) => Number(a) - Number(b))); + }), + ); + }); + + test("should read a number category exactly like its string form", () => { + fc.assert( + fc.property(categoryCodes, (category) => { + expect(getLegalNaturesByCategory(Number(category))).toEqual( + getLegalNaturesByCategory(category), + ); + }), + ); + }); + + test("should never throw, whatever it is given", () => { + expectNeverThrows(getLegalNaturesByCategory, anyValue); + }); + }); +}); + +describe("getLegalNaturesByCategory types", () => { + test("should take a string or number category and return an array of legal natures", () => { + expectTypeOf(getLegalNaturesByCategory).parameter(0).toEqualTypeOf(); + expectTypeOf(getLegalNaturesByCategory).returns.toEqualTypeOf(); + }); +}); diff --git a/src/get-legal-natures-by-category/get-legal-natures-by-category.ts b/src/get-legal-natures-by-category/get-legal-natures-by-category.ts new file mode 100644 index 00000000..f4209cf8 --- /dev/null +++ b/src/get-legal-natures-by-category/get-legal-natures-by-category.ts @@ -0,0 +1,59 @@ +import { LEGAL_NATURE_CATEGORIES } from "../_internals/constants/legal-nature-categories"; +import { type LegalNature } from "../get-legal-nature/get-legal-nature"; +import { LEGAL_NATURE } from "../is-valid-legal-nature/constants"; + +/** + * Retrieves every Brazilian legal nature (natureza jurídica) of a CONCLA category. + * + * The category is the first digit of the four digit code, the heading the table lists the code + * under: 1 Administração Pública, 2 Entidades Empresariais, 3 Entidades sem Fins Lucrativos, + * 4 Pessoas Físicas and 5 Organizações Internacionais e Outras Instituições Extraterritoriais. + * It is accepted as a string or as a number, so `"2"` and `2` return the same list. + * + * The legacy codes `LEGAL_NATURE` keeps for 2.3.0 compatibility (2076, 2100, 2208, 3042, 3050, + * 3093, 3123 and 5002) are listed under the category of their first digit as well. The result + * is in ascending code order, since the table is keyed by the codes themselves, and is a fresh + * array of fresh entries on every call. + * + * @param {string|number} category - The category code, `"1"` through `"5"` or 1 through 5. + * @returns {LegalNature[]} The legal natures of the category, sorted by code, or an empty array + * when the category is unknown or the input is invalid. + * + * The CONCLA table page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser; the detailed structure PDF next to it is served + * normally. + * + * @see Official: https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 + * @see Official: https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf + * + * @example + * ```typescript + * getLegalNaturesByCategory("4")[0]; + * // { + * // code: "4014", + * // description: "Empresa Individual Imobiliária", + * // category: { code: "4", description: "Pessoas Físicas" }, + * // } + * getLegalNaturesByCategory(4).length; // 6 + * getLegalNaturesByCategory("2").length; // 33 + * getLegalNaturesByCategory("9"); // [] + * ``` + */ +export const getLegalNaturesByCategory = (category: string | number): LegalNature[] => { + if (typeof category !== "string" && typeof category !== "number") return []; + + const categoryCode = String(category); + + if (!Object.hasOwn(LEGAL_NATURE_CATEGORIES, categoryCode)) return []; + + const entry = LEGAL_NATURE_CATEGORIES[categoryCode]; + const legalNatures: LegalNature[] = []; + + for (const [code, description] of Object.entries(LEGAL_NATURE)) { + if (code.startsWith(categoryCode)) { + legalNatures.push({ code, description, category: { ...entry } }); + } + } + + return legalNatures; +}; diff --git a/src/get-legal-natures/get-legal-natures.test.ts b/src/get-legal-natures/get-legal-natures.test.ts index 7cf617ac..76db888e 100644 --- a/src/get-legal-natures/get-legal-natures.test.ts +++ b/src/get-legal-natures/get-legal-natures.test.ts @@ -35,7 +35,7 @@ describe("getLegalNatures", () => { expect(code).toMatch(/^\d{4}$/); expect(isValidLegalNature(code)).toBe(true); - expect(getLegalNature(code)).toEqual(entry); + expect(getLegalNature(code)).toMatchObject(entry); }), ); }); diff --git a/src/index.test.ts b/src/index.test.ts index 0f309637..265589b3 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -58,6 +58,7 @@ import { type IsValidPixKeyOptions, type IsValidRegistroProfissionalOptions, type LegalNature, + type LegalNatureCategory, type LicensePlateFormat, type Municipality, type NfeKey, @@ -151,6 +152,7 @@ const PUBLIC = [ "getHolidays", "getLegalNature", "getLegalNatures", + "getLegalNaturesByCategory", "getMunicipalities", "getMunicipality", "getMunicipalityByCode", @@ -310,6 +312,7 @@ describe("Public API", () => { IsValidPixKeyOptions: IsValidPixKeyOptions; IsValidRegistroProfissionalOptions: IsValidRegistroProfissionalOptions; LegalNature: LegalNature; + LegalNatureCategory: LegalNatureCategory; LicensePlateFormat: LicensePlateFormat; Municipality: Municipality; NfeKey: NfeKey; diff --git a/src/index.ts b/src/index.ts index db2e01a3..641fe43a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -105,8 +105,13 @@ export { type HolidayType, getHolidays, } from "./get-holidays/get-holidays"; -export { type LegalNature, getLegalNature } from "./get-legal-nature/get-legal-nature"; +export { + type LegalNature, + type LegalNatureCategory, + getLegalNature, +} from "./get-legal-nature/get-legal-nature"; export { getLegalNatures } from "./get-legal-natures/get-legal-natures"; +export { getLegalNaturesByCategory } from "./get-legal-natures-by-category/get-legal-natures-by-category"; export { getMunicipalities } from "./get-municipalities/get-municipalities"; export { type GetMunicipalityByCodeOptions, From af21b776f54ef94c727e867d809c4339e4d8b5a6 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:13:42 -0300 Subject: [PATCH 51/75] feat(phone): accept 7 and 8 as mobile first digits under version 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolução Anatel 749/2022, art. 12, I, "a" places 7, 8 and 9 in the Serviço Móvel Pessoal, so version 2 of isValidMobilePhone and isValidPhone now accepts the three digits instead of 9 only, and rejects 6, which art. 12, I, "b" leaves as Reserva Técnica. The 700 series, reserved by art. 12, II for the satellite service, is now rejected explicitly under version 2; version 1 is unchanged. --- docs/llms-full.txt | 11 ++++--- docs/pt-br/utilities.md | 11 ++++--- docs/utilities.md | 11 ++++--- src/is-valid-mobile-phone/constants.ts | 14 ++++++++- .../is-valid-mobile-phone.test.ts | 22 ++++++++++++-- .../is-valid-mobile-phone.ts | 29 ++++++++++++------- src/is-valid-phone/is-valid-phone.test.ts | 7 +++++ src/is-valid-phone/is-valid-phone.ts | 8 ++++- 8 files changed, 86 insertions(+), 27 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 3283b069..d9e6e4b4 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -571,13 +571,14 @@ isValidEmail('john.doe@hotmail.com'); // true ### isValidPhone -Check if phone number (mobile or landline) is valid. A Brazilian country code (`+55`, `0055` or a bare `55`) is accepted and removed before validation, under the rule documented in `parsePhone`. `options.accept` (typed as `PhoneType[]`, part of `IsValidPhoneOptions`) picks which kinds of number count as valid and defaults to `['mobile', 'landline']`; add `'service'` to also accept the non-geographic numbers recognized by `isValidServicePhone`, or pass `[]` to accept none. `options.version` (typed as `PhoneVersion`, part of the same type) is forwarded to `isValidMobilePhone` and picks which mobile numbering rule is enforced: `1` (default) the legacy format, whose first number digit may be 6, 7, 8 or 9, and `2` the current one, which requires 9 and rejects the `700` prefix. It only affects mobile numbers; landline and service numbers are unaffected. +Check if phone number (mobile or landline) is valid. A Brazilian country code (`+55`, `0055` or a bare `55`) is accepted and removed before validation, under the rule documented in `parsePhone`. `options.accept` (typed as `PhoneType[]`, part of `IsValidPhoneOptions`) picks which kinds of number count as valid and defaults to `['mobile', 'landline']`; add `'service'` to also accept the non-geographic numbers recognized by `isValidServicePhone`, or pass `[]` to accept none. `options.version` (typed as `PhoneVersion`, part of the same type) is forwarded to `isValidMobilePhone` and picks which mobile numbering rule is enforced: `1` (default) the legacy format, whose first number digit may be 6, 7, 8 or 9, and `2` the current one of Resolução Anatel 749/2022, art. 12, I, "a", which accepts 7, 8 or 9 and rejects the `700` prefix. It only affects mobile numbers; landline and service numbers are unaffected. ```javascript import { isValidPhone } from '@brazilian-utils/brazilian-utils'; isValidPhone('11900000000'); // true -isValidPhone('11712345678', { version: 2 }); // false (v2 requires 9 as the first mobile digit) +isValidPhone('11712345678', { version: 2 }); // true (7, 8 and 9 are all SMP) +isValidPhone('11700123456', { version: 2 }); // false (the 700 series is satellite) isValidPhone('+55 11 98765-4321'); // true (country code accepted) isValidPhone('08001234567'); // false (service numbers rejected by default) isValidPhone('08001234567', { accept: ['service'] }); // true @@ -619,14 +620,16 @@ parsePhone('55987654321'); // 55987654321 (area code 55, not mistaken for the +5 ### isValidMobilePhone -Check if mobile phone number is valid. `options.version` (typed as `PhoneVersion`) controls which mobile numbering rule is enforced: `1` (default) is the pre-Resolução Anatel 749/2022 format, kept for 2.3.0 compatibility, whose first number digit (after the DDD) may be 6, 7, 8 or 9; `2` enforces only 9, a stricter subset of the resolution's art. 12 I (Serviço Móvel Pessoal). Version `1` also does not carve out the `700` prefix, which art. 12 II reserves for the Serviço Móvel Global por Satélite rather than SMP, so `isValidMobilePhone('11700123456')` is `true` for a number outside SMP; version `2` rejects it. +Check if mobile phone number is valid. `options.version` (typed as `PhoneVersion`) controls which mobile numbering rule is enforced: `1` (default) is the pre-Resolução Anatel 749/2022 format, kept for 2.3.0 compatibility, whose first number digit (after the DDD) may be 6, 7, 8 or 9; `2` enforces the resolution's art. 12, I, "a", which places 7, 8 and 9 in the Serviço Móvel Pessoal (SMP), so a leading 6 is Reserva Técnica and is rejected. Version `2` also carves out the `700` prefix, which art. 12, II reserves for the Serviço Móvel Global por Satélite rather than SMP, so `isValidMobilePhone('11700123456', { version: 2 })` is `false`; version `1` does not carve it out and accepts it. ```javascript import { isValidMobilePhone } from '@brazilian-utils/brazilian-utils'; isValidMobilePhone('11900000000'); // true isValidMobilePhone('11712345678', { version: 1 }); // true (legacy format) -isValidMobilePhone('11712345678', { version: 2 }); // false (v2 requires 9 as the first digit) +isValidMobilePhone('11712345678', { version: 2 }); // true (7 is SMP as well) +isValidMobilePhone('11612345678', { version: 2 }); // false (6 is Reserva Técnica) +isValidMobilePhone('11700123456', { version: 2 }); // false (the 700 series is satellite) ``` ### isValidLandlinePhone diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 2f23cb23..75ceca1d 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -329,13 +329,14 @@ isValidEmail('john.doe@hotmail.com'); // true ## isValidPhone -Valida se o número de telefone (celular ou fixo) é válido. Um código de país brasileiro (`+55`, `0055` ou um `55` isolado) é aceito e removido antes da validação, seguindo a regra documentada em `parsePhone`. `options.accept` (tipado como `PhoneType[]`, parte de `IsValidPhoneOptions`) define quais tipos de número são aceitos e tem como padrão `['mobile', 'landline']`; adicione `'service'` para também aceitar os números não geográficos reconhecidos por `isValidServicePhone`, ou informe `[]` para não aceitar nenhum. `options.version` (tipado como `PhoneVersion`, parte do mesmo tipo) é repassado ao `isValidMobilePhone` e escolhe qual regra de numeração celular é aplicada: `1` (padrão) o formato antigo, cujo primeiro dígito do número pode ser 6, 7, 8 ou 9, e `2` o atual, que exige 9 e rejeita o prefixo `700`. Vale apenas para celulares; números fixos e de serviço não são afetados. +Valida se o número de telefone (celular ou fixo) é válido. Um código de país brasileiro (`+55`, `0055` ou um `55` isolado) é aceito e removido antes da validação, seguindo a regra documentada em `parsePhone`. `options.accept` (tipado como `PhoneType[]`, parte de `IsValidPhoneOptions`) define quais tipos de número são aceitos e tem como padrão `['mobile', 'landline']`; adicione `'service'` para também aceitar os números não geográficos reconhecidos por `isValidServicePhone`, ou informe `[]` para não aceitar nenhum. `options.version` (tipado como `PhoneVersion`, parte do mesmo tipo) é repassado ao `isValidMobilePhone` e escolhe qual regra de numeração celular é aplicada: `1` (padrão) o formato antigo, cujo primeiro dígito do número pode ser 6, 7, 8 ou 9, e `2` o atual, da Resolução Anatel 749/2022, art. 12, I, "a", que aceita 7, 8 ou 9 e rejeita o prefixo `700`. Vale apenas para celulares; números fixos e de serviço não são afetados. ```javascript import { isValidPhone } from '@brazilian-utils/brazilian-utils'; isValidPhone('11900000000'); // true -isValidPhone('11712345678', { version: 2 }); // false (v2 exige 9 como primeiro dígito do celular) +isValidPhone('11712345678', { version: 2 }); // true (7, 8 e 9 são todos SMP) +isValidPhone('11700123456', { version: 2 }); // false (a série 700 é de satélite) isValidPhone('+55 11 98765-4321'); // true (código de país aceito) isValidPhone('08001234567'); // false (números de serviço não são aceitos por padrão) isValidPhone('08001234567', { accept: ['service'] }); // true @@ -377,14 +378,16 @@ parsePhone('55987654321'); // 55987654321 (DDD 55, não confundido com o código ## isValidMobilePhone -Valida se o número de telefone celular é válido. `options.version` (tipado como `PhoneVersion`) controla qual regra de numeração celular é aplicada: `1` (padrão) é o formato anterior à Resolução Anatel 749/2022, mantido por compatibilidade com a 2.3.0, cujo primeiro dígito do número (após o DDD) pode ser 6, 7, 8 ou 9; `2` exige apenas 9, um subconjunto mais restrito do art. 12 I da resolução (Serviço Móvel Pessoal). A versão `1` também não exclui o prefixo `700`, que o art. 12 II reserva ao Serviço Móvel Global por Satélite e não ao SMP, então `isValidMobilePhone('11700123456')` é `true` para um número fora do SMP; a versão `2` o rejeita. +Valida se o número de telefone celular é válido. `options.version` (tipado como `PhoneVersion`) controla qual regra de numeração celular é aplicada: `1` (padrão) é o formato anterior à Resolução Anatel 749/2022, mantido por compatibilidade com a 2.3.0, cujo primeiro dígito do número (após o DDD) pode ser 6, 7, 8 ou 9; `2` aplica o art. 12, I, "a" da resolução, que coloca 7, 8 e 9 no Serviço Móvel Pessoal (SMP), então um 6 inicial é Reserva Técnica e é rejeitado. A versão `2` também exclui o prefixo `700`, que o art. 12, II reserva ao Serviço Móvel Global por Satélite e não ao SMP, então `isValidMobilePhone('11700123456', { version: 2 })` é `false`; a versão `1` não o exclui e o aceita. ```javascript import { isValidMobilePhone } from '@brazilian-utils/brazilian-utils'; isValidMobilePhone('11900000000'); // true isValidMobilePhone('11712345678', { version: 1 }); // true (formato antigo) -isValidMobilePhone('11712345678', { version: 2 }); // false (v2 exige 9 como primeiro dígito) +isValidMobilePhone('11712345678', { version: 2 }); // true (7 também é SMP) +isValidMobilePhone('11612345678', { version: 2 }); // false (6 é Reserva Técnica) +isValidMobilePhone('11700123456', { version: 2 }); // false (a série 700 é de satélite) ``` ## isValidLandlinePhone diff --git a/docs/utilities.md b/docs/utilities.md index 082328ef..5805081e 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -329,13 +329,14 @@ isValidEmail('john.doe@hotmail.com'); // true ## isValidPhone -Check if phone number (mobile or landline) is valid. A Brazilian country code (`+55`, `0055` or a bare `55`) is accepted and removed before validation, under the rule documented in `parsePhone`. `options.accept` (typed as `PhoneType[]`, part of `IsValidPhoneOptions`) picks which kinds of number count as valid and defaults to `['mobile', 'landline']`; add `'service'` to also accept the non-geographic numbers recognized by `isValidServicePhone`, or pass `[]` to accept none. `options.version` (typed as `PhoneVersion`, part of the same type) is forwarded to `isValidMobilePhone` and picks which mobile numbering rule is enforced: `1` (default) the legacy format, whose first number digit may be 6, 7, 8 or 9, and `2` the current one, which requires 9 and rejects the `700` prefix. It only affects mobile numbers; landline and service numbers are unaffected. +Check if phone number (mobile or landline) is valid. A Brazilian country code (`+55`, `0055` or a bare `55`) is accepted and removed before validation, under the rule documented in `parsePhone`. `options.accept` (typed as `PhoneType[]`, part of `IsValidPhoneOptions`) picks which kinds of number count as valid and defaults to `['mobile', 'landline']`; add `'service'` to also accept the non-geographic numbers recognized by `isValidServicePhone`, or pass `[]` to accept none. `options.version` (typed as `PhoneVersion`, part of the same type) is forwarded to `isValidMobilePhone` and picks which mobile numbering rule is enforced: `1` (default) the legacy format, whose first number digit may be 6, 7, 8 or 9, and `2` the current one of Resolução Anatel 749/2022, art. 12, I, "a", which accepts 7, 8 or 9 and rejects the `700` prefix. It only affects mobile numbers; landline and service numbers are unaffected. ```javascript import { isValidPhone } from '@brazilian-utils/brazilian-utils'; isValidPhone('11900000000'); // true -isValidPhone('11712345678', { version: 2 }); // false (v2 requires 9 as the first mobile digit) +isValidPhone('11712345678', { version: 2 }); // true (7, 8 and 9 are all SMP) +isValidPhone('11700123456', { version: 2 }); // false (the 700 series is satellite) isValidPhone('+55 11 98765-4321'); // true (country code accepted) isValidPhone('08001234567'); // false (service numbers rejected by default) isValidPhone('08001234567', { accept: ['service'] }); // true @@ -377,14 +378,16 @@ parsePhone('55987654321'); // 55987654321 (area code 55, not mistaken for the +5 ## isValidMobilePhone -Check if mobile phone number is valid. `options.version` (typed as `PhoneVersion`) controls which mobile numbering rule is enforced: `1` (default) is the pre-Resolução Anatel 749/2022 format, kept for 2.3.0 compatibility, whose first number digit (after the DDD) may be 6, 7, 8 or 9; `2` enforces only 9, a stricter subset of the resolution's art. 12 I (Serviço Móvel Pessoal). Version `1` also does not carve out the `700` prefix, which art. 12 II reserves for the Serviço Móvel Global por Satélite rather than SMP, so `isValidMobilePhone('11700123456')` is `true` for a number outside SMP; version `2` rejects it. +Check if mobile phone number is valid. `options.version` (typed as `PhoneVersion`) controls which mobile numbering rule is enforced: `1` (default) is the pre-Resolução Anatel 749/2022 format, kept for 2.3.0 compatibility, whose first number digit (after the DDD) may be 6, 7, 8 or 9; `2` enforces the resolution's art. 12, I, "a", which places 7, 8 and 9 in the Serviço Móvel Pessoal (SMP), so a leading 6 is Reserva Técnica and is rejected. Version `2` also carves out the `700` prefix, which art. 12, II reserves for the Serviço Móvel Global por Satélite rather than SMP, so `isValidMobilePhone('11700123456', { version: 2 })` is `false`; version `1` does not carve it out and accepts it. ```javascript import { isValidMobilePhone } from '@brazilian-utils/brazilian-utils'; isValidMobilePhone('11900000000'); // true isValidMobilePhone('11712345678', { version: 1 }); // true (legacy format) -isValidMobilePhone('11712345678', { version: 2 }); // false (v2 requires 9 as the first digit) +isValidMobilePhone('11712345678', { version: 2 }); // true (7 is SMP as well) +isValidMobilePhone('11612345678', { version: 2 }); // false (6 is Reserva Técnica) +isValidMobilePhone('11700123456', { version: 2 }); // false (the 700 series is satellite) ``` ## isValidLandlinePhone diff --git a/src/is-valid-mobile-phone/constants.ts b/src/is-valid-mobile-phone/constants.ts index 8e85f36f..63835132 100644 --- a/src/is-valid-mobile-phone/constants.ts +++ b/src/is-valid-mobile-phone/constants.ts @@ -1,2 +1,14 @@ +/** + * The first digit (N9) a Brazilian mobile access code may carry, per numbering rule. + * + * Version 1 is the pre-Resolução 749/2022 set kept for 2.3.0 compatibility. Version 2 is the + * set of art. 12, I, "a" of the resolution: `“7”, "8" e “9”: Serviço Móvel Pessoal (SMP), + * ressalvado o disposto no inciso II deste artigo`, the ressalva being art. 12, II, "a", + * `“700”: Serviço Móvel Global por Satélite (SMGS)`, a series outside the SMP. + * + * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749 + */ export const MOBILE_VALID_FIRST_NUMBERS_V1 = [6, 7, 8, 9]; -export const MOBILE_VALID_FIRST_NUMBERS_V2 = [9]; +export const MOBILE_VALID_FIRST_NUMBERS_V2 = [7, 8, 9]; + +export const MOBILE_SATELLITE_PREFIX = "700"; diff --git a/src/is-valid-mobile-phone/is-valid-mobile-phone.test.ts b/src/is-valid-mobile-phone/is-valid-mobile-phone.test.ts index 02c09361..f2a6e1a8 100644 --- a/src/is-valid-mobile-phone/is-valid-mobile-phone.test.ts +++ b/src/is-valid-mobile-phone/is-valid-mobile-phone.test.ts @@ -36,10 +36,14 @@ describe("isValidMobilePhone", () => { expect(isValidMobilePhone("+1 415 555 2671")).toBe(false); }); - test("when version 2 is requested but the first number digit is a version-1-only value (6, 7 or 8)", () => { - expect(isValidMobilePhone("11712345678", { version: 2 })).toBe(false); + test("when version 2 is requested but the first number digit is the version-1-only 6", () => { expect(isValidMobilePhone("11612345678", { version: 2 })).toBe(false); - expect(isValidMobilePhone("11812345678", { version: 2 })).toBe(false); + }); + + test("when version 2 is requested and the number is in the 700 satellite series", () => { + expect(isValidMobilePhone("11700123456", { version: 2 })).toBe(false); + expect(isValidMobilePhone("(11) 70012-3456", { version: 2 })).toBe(false); + expect(isValidMobilePhone("+55 11 70012-3456", { version: 2 })).toBe(false); }); }); @@ -49,8 +53,20 @@ describe("isValidMobilePhone", () => { expect(isValidMobilePhone("11987654321", { version: 2 })).toBe(true); }); + test("when version 2 is requested and the first number digit is 7 or 8", () => { + expect(isValidMobilePhone("11712345678", { version: 2 })).toBe(true); + expect(isValidMobilePhone("11812345678", { version: 2 })).toBe(true); + }); + + test("when version 2 is requested and the number only starts like the 700 series", () => { + expect(isValidMobilePhone("11701234567", { version: 2 })).toBe(true); + expect(isValidMobilePhone("11770012345", { version: 2 })).toBe(true); + }); + test("when is a valid mobile phone version 1", () => { expect(isValidMobilePhone("11712345678", { version: 1 })).toBe(true); + expect(isValidMobilePhone("11612345678", { version: 1 })).toBe(true); + expect(isValidMobilePhone("11700123456", { version: 1 })).toBe(true); }); test("when it carries the country code", () => { diff --git a/src/is-valid-mobile-phone/is-valid-mobile-phone.ts b/src/is-valid-mobile-phone/is-valid-mobile-phone.ts index 3ed92c84..d6afbdf7 100644 --- a/src/is-valid-mobile-phone/is-valid-mobile-phone.ts +++ b/src/is-valid-mobile-phone/is-valid-mobile-phone.ts @@ -2,13 +2,17 @@ import { PHONE_NATIONAL_MAX_LENGTH } from "../_internals/constants/phone"; import { isValidDDD } from "../_internals/is-valid-ddd/is-valid-ddd"; import { normalizePhone } from "../_internals/normalize-phone/normalize-phone"; import { type PhoneVersion } from "../is-valid-phone/is-valid-phone"; -import { MOBILE_VALID_FIRST_NUMBERS_V1, MOBILE_VALID_FIRST_NUMBERS_V2 } from "./constants"; +import { + MOBILE_SATELLITE_PREFIX, + MOBILE_VALID_FIRST_NUMBERS_V1, + MOBILE_VALID_FIRST_NUMBERS_V2, +} from "./constants"; export type { PhoneVersion } from "../is-valid-phone/is-valid-phone"; /** Options of `isValidMobilePhone`. */ export type IsValidMobilePhoneOptions = { - /** Numbering rule to enforce over the 11 digit number: `1` (default) accepts 6, 7, 8 or 9 as the first number digit, `2` requires 9. */ + /** Numbering rule to enforce over the 11 digit number: `1` (default) accepts 6, 7, 8 or 9 as the first number digit, `2` accepts 7, 8 or 9 and rejects the `700` series. */ version?: PhoneVersion; }; @@ -19,6 +23,8 @@ const isValidMobileFirstNumber = (value: string, version?: PhoneVersion): boolea return MOBILE_VALID_FIRST_NUMBERS_V1.includes(firstDigit); } + if (value.startsWith(MOBILE_SATELLITE_PREFIX, 2)) return false; + return MOBILE_VALID_FIRST_NUMBERS_V2.includes(firstDigit); }; @@ -31,7 +37,8 @@ const isValidMobileFirstNumber = (value: string, version?: PhoneVersion): boolea * The `version` option controls which mobile numbering rule is enforced: * - `1` (default): accepts the legacy 11-digit format, whose first number digit * (right after the DDD) may be 6, 7, 8 or 9. - * - `2`: enforces the current format, whose first number digit must be 9. + * - `2`: enforces the current format, whose first number digit must be 7, 8 or 9 and whose + * `700` series is left out. * * @param {string} value - The phone number to validate. * @param {IsValidMobilePhoneOptions} options - Optional validation options. @@ -43,18 +50,20 @@ const isValidMobileFirstNumber = (value: string, version?: PhoneVersion): boolea * isValidMobilePhone("(11) 98765-4321"); // true (accepts both v1 and v2) * isValidMobilePhone("11987654321", { version: 2 }); // true * isValidMobilePhone("11712345678", { version: 1 }); // true - * isValidMobilePhone("11712345678", { version: 2 }); // false (v2 requires 9 as the first digit) + * isValidMobilePhone("11712345678", { version: 2 }); // true (7 is SMP as well) + * isValidMobilePhone("11612345678", { version: 2 }); // false (6 is Reserva Técnica) + * isValidMobilePhone("11700123456", { version: 2 }); // false (the 700 series is satellite) * isValidMobilePhone("+55 11 98765-4321"); // true * ``` * * `version: 1` (the default) is the pre-Resolução 749/2022 rule, which also accepts a leading - * 6, kept for 2.3.0 compatibility. `version: 2` enforces only 9, a stricter subset of the - * resolution's art. 12 I, which places 7, 8 and 9 in Serviço Móvel Pessoal (SMP). + * 6, kept for 2.3.0 compatibility. `version: 2` enforces art. 12, I, "a" of the resolution, + * `“7”, "8" e “9”: Serviço Móvel Pessoal (SMP), ressalvado o disposto no inciso II deste + * artigo`, so 6 is Reserva Técnica and is rejected. * - * `version: 1` also does not carve out the `700` prefix, which art. 12 II reserves for the - * Serviço Móvel Global por Satélite rather than SMP, so `isValidMobilePhone("11700123456")` is - * `true` for a number outside SMP. `version: 2` rejects it, along with every other first digit - * that is not 9. + * That ressalva is art. 12, II, "a", `“700”: Serviço Móvel Global por Satélite (SMGS)`: the + * `700` series is not SMP, so `version: 2` rejects `isValidMobilePhone("11700123456")`. + * `version: 1` does not carve the series out and accepts it, for 2.3.0 compatibility. * * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749 */ diff --git a/src/is-valid-phone/is-valid-phone.test.ts b/src/is-valid-phone/is-valid-phone.test.ts index c6b70085..d43a11ab 100644 --- a/src/is-valid-phone/is-valid-phone.test.ts +++ b/src/is-valid-phone/is-valid-phone.test.ts @@ -60,6 +60,11 @@ describe("isValidPhone", () => { expect(isValidPhone("08001234567", { accept: [] })).toBe(false); }); + test("when version 2 rejects the mobile number", () => { + expect(isValidPhone("11612345678", { version: 2 })).toBe(false); + expect(isValidPhone("11700123456", { version: 2 })).toBe(false); + }); + test("when the kind is not accepted", () => { expect(isValidPhone("11987654321", { accept: ["landline"] })).toBe(false); expect(isValidPhone("1130000000", { accept: ["mobile"] })).toBe(false); @@ -72,6 +77,8 @@ describe("isValidPhone", () => { test("when is a valid mobile phone version 2", () => { expect(isValidPhone("(11) 98765-4321")).toBe(true); expect(isValidPhone("11987654321", { version: 2 })).toBe(true); + expect(isValidPhone("11712345678", { version: 2 })).toBe(true); + expect(isValidPhone("11812345678", { version: 2 })).toBe(true); }); test("when is a valid landline phone", () => { diff --git a/src/is-valid-phone/is-valid-phone.ts b/src/is-valid-phone/is-valid-phone.ts index a0ae3ea3..a4c72321 100644 --- a/src/is-valid-phone/is-valid-phone.ts +++ b/src/is-valid-phone/is-valid-phone.ts @@ -9,7 +9,7 @@ import { isValidMobilePhone } from "../is-valid-mobile-phone/is-valid-mobile-pho import { isValidServicePhone } from "../is-valid-service-phone/is-valid-service-phone"; import { DEFAULT_ACCEPT } from "./constants"; -/** The Brazilian mobile numbering rule to enforce over the 11 digit number: `1` the legacy one, `2` the current one. */ +/** The Brazilian mobile numbering rule to enforce over the 11 digit number: `1` the legacy one (6, 7, 8 or 9), `2` the current one (7, 8 or 9, without the `700` series). */ export type PhoneVersion = 1 | 2; /** The kinds of Brazilian phone number `isValidPhone` can accept. */ @@ -33,6 +33,10 @@ export type IsValidPhoneOptions = { * `["mobile", "landline"]`, i.e. geographic numbers only. Add `"service"` to also accept the * non-geographic numbers recognized by `isValidServicePhone`; pass `[]` to accept none. * + * `options.version` is forwarded to `isValidMobilePhone` and only affects mobile numbers: + * `1` (the default) accepts a first number digit of 6, 7, 8 or 9, and `2` the 7, 8 and 9 of + * Resolução Anatel nº 749/2022, art. 12, I, "a", minus its `700` satellite series. + * * @param {string} value - The phone number to validate. * @param {IsValidPhoneOptions} options - Optional validation options. * @param {1|2} options.version - The mobile numbering rule to enforce, see `isValidMobilePhone`. @@ -43,6 +47,8 @@ export type IsValidPhoneOptions = { * ```typescript * isValidPhone("(11) 98765-4321"); // true * isValidPhone("11987654321", { version: 2 }); // true + * isValidPhone("11712345678", { version: 2 }); // true (7 is SMP as well) + * isValidPhone("11700123456", { version: 2 }); // false (the 700 series is satellite) * isValidPhone("1130000000"); // true (landline) * isValidPhone("+55 11 98765-4321"); // true * isValidPhone("08001234567"); // false (service numbers are not accepted by default) From 77cf05b8879c20ddb2590927e7903e554302ca9c Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:19:26 -0300 Subject: [PATCH 52/75] ci: reference the setup action with the self-repository syntax GitHub now recommends `uses: $/path` over `./path` for actions in the same repository: it resolves to the running commit and does not depend on what a previous step left in the workspace, which is what the zizmor self-repository audit flags on every workflow. actionlint does not know the syntax yet (rhysd/actionlint#711), so a config ignores only that message. The `npm install -g npm@12.0.2` in the release job is pinned to an exact version and npm is not a package.json dependency, so the adhoc-packages finding is ignored inline with that reason. --- .github/actionlint.yaml | 7 +++++++ .github/workflows/build.yml | 4 ++-- .github/workflows/check.yml | 2 +- .github/workflows/datasets.yml | 2 +- .github/workflows/live-tests.yml | 2 +- .github/workflows/mutation.yml | 2 +- .github/workflows/release.yml | 5 +++-- .github/workflows/tests.yml | 10 +++++----- 8 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 .github/actionlint.yaml diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 00000000..d7b9d94e --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,7 @@ +# actionlint 1.7.12 does not know GitHub's self-repository `uses: $/...` syntax yet +# (https://github.com/rhysd/actionlint/issues/711); zizmor's self-repository audit and the GitHub +# docs recommend it over the workspace-relative `./...` form. Drop this once actionlint supports it. +paths: + .github/workflows/**/*.yml: + ignore: + - 'specifying action "\$/\.github/actions/setup" in invalid format because ref is missing' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6cf52571..4306ce53 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: persist-credentials: false - name: Setup - uses: ./.github/actions/setup + uses: $/.github/actions/setup - name: Run build run: vp run build @@ -52,7 +52,7 @@ jobs: persist-credentials: false - name: Setup - uses: ./.github/actions/setup + uses: $/.github/actions/setup - name: Checkout base if: ${{ github.event_name == 'pull_request' }} diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 80466e04..18b1480a 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -27,7 +27,7 @@ jobs: persist-credentials: false - name: Setup - uses: ./.github/actions/setup + uses: $/.github/actions/setup - name: Run checks run: vp check diff --git a/.github/workflows/datasets.yml b/.github/workflows/datasets.yml index 16197652..d261f644 100644 --- a/.github/workflows/datasets.yml +++ b/.github/workflows/datasets.yml @@ -23,7 +23,7 @@ jobs: persist-credentials: false - name: Setup - uses: ./.github/actions/setup + uses: $/.github/actions/setup - name: Rebuild datasets run: npm run build:data diff --git a/.github/workflows/live-tests.yml b/.github/workflows/live-tests.yml index 83458c49..d5041bb1 100644 --- a/.github/workflows/live-tests.yml +++ b/.github/workflows/live-tests.yml @@ -22,7 +22,7 @@ jobs: persist-credentials: false - name: Setup - uses: ./.github/actions/setup + uses: $/.github/actions/setup - name: Run live CEP tests run: vp run test:live diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 1d2e29c9..0058ea56 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -26,7 +26,7 @@ jobs: persist-credentials: false - name: Setup - uses: ./.github/actions/setup + uses: $/.github/actions/setup - name: Mutation test every file run: npm run test:mutation diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a816df8b..058f5416 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,7 +83,7 @@ jobs: persist-credentials: false - name: Setup - uses: ./.github/actions/setup + uses: $/.github/actions/setup with: node-version: 24 @@ -104,6 +104,7 @@ jobs: cat tree-shaking.md >> "$GITHUB_STEP_SUMMARY" - name: Ensure npm supports staged publishing and OIDC (npm >= 11.15) + # zizmor: ignore[adhoc-packages] npm is pinned to an exact version and is not a package.json dependency run: npm install -g npm@12.0.2 - name: Stage on npm @@ -126,7 +127,7 @@ jobs: persist-credentials: false - name: Setup - uses: ./.github/actions/setup + uses: $/.github/actions/setup - name: Generate the CycloneDX SBOM of the published package # The package has no runtime dependencies, so the SBOM describes the package itself; diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 67a5b57b..b16427c5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,7 +29,7 @@ jobs: persist-credentials: false - name: Setup - uses: ./.github/actions/setup + uses: $/.github/actions/setup with: node-version: ${{ matrix.node-version }} @@ -65,7 +65,7 @@ jobs: persist-credentials: false - name: Setup - uses: ./.github/actions/setup + uses: $/.github/actions/setup - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 @@ -85,7 +85,7 @@ jobs: persist-credentials: false - name: Setup - uses: ./.github/actions/setup + uses: $/.github/actions/setup - name: Setup Deno uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2.0.5 @@ -110,7 +110,7 @@ jobs: persist-credentials: false - name: Setup - uses: ./.github/actions/setup + uses: $/.github/actions/setup - name: Run tests in ${{ matrix.browser }} run: vp test --browser.enabled --browser.name=${{ matrix.browser }} @@ -127,7 +127,7 @@ jobs: persist-credentials: false - name: Setup - uses: ./.github/actions/setup + uses: $/.github/actions/setup - name: Run tests in Safari run: vp test --browser.enabled --browser.name=safari --browser.headless=false From e879cb4c4226131a10b0c02ad81216dcb91a353b Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:41:12 -0300 Subject: [PATCH 53/75] docs: state the measured isValidCpf size and fix two comments in the words tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README and both getting-started pages said `isValidCpf` costs under 1 KB while the tree-shaking section of the same page measures 1.2 KB minified and 0.6 KB gzipped; the three now carry the measured pair. The number-words table cited the Vocabulário Ortográfico as admitting "quatorze" and "quatorze" (one of them is "catorze"), and the num2words comparison in numberToWords quoted the comma-separated spelling without its comma. --- README.md | 2 +- docs/getting-started.md | 2 +- docs/llms-full.txt | 2 +- docs/pt-br/getting-started.md | 4 ++-- src/_internals/constants/number-words.ts | 2 +- src/_internals/number-to-words/number-to-words.ts | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6e4d7743..1ba59330 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Brazilian Utils is a library focused on solving problems that we face daily in t ## Why Brazilian Utils - **Zero runtime dependencies.** Nothing else lands in your `node_modules` or in your bundle. -- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs under 1 KB; every util is also its own subpath entry (`@brazilian-utils/brazilian-utils/get-cities`) for the heavy ones. +- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs about 1.2 KB minified (0.6 KB gzipped); every util is also its own subpath entry (`@brazilian-utils/brazilian-utils/get-cities`) for the heavy ones. - **Runs everywhere.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno and evergreen browsers, tested in CI on every one of them. - **Written in TypeScript.** Types ship with the package; the public API is tracked by an API report so nothing changes silently. - **Validated against the official rules.** Every validator cites the specification, law or dataset it implements (`@see` in the docs), and the test suite is mutation-tested, not just covered. diff --git a/docs/getting-started.md b/docs/getting-started.md index 55f67f79..72fe7d09 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,7 +5,7 @@ Brazilian Utils is a library focused on solving problems that we face daily in t ## Why Brazilian Utils - **Zero runtime dependencies.** Nothing else lands in your `node_modules` or in your bundle. -- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs under 1 KB; every util is also its own subpath entry (`@brazilian-utils/brazilian-utils/get-cities`) for the heavy ones. +- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs about 1.2 KB minified (0.6 KB gzipped); every util is also its own subpath entry (`@brazilian-utils/brazilian-utils/get-cities`) for the heavy ones. - **Runs everywhere.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno and evergreen browsers, tested in CI on every one of them. - **Written in TypeScript.** Types ship with the package; the public API is tracked by an API report so nothing changes silently. - **Validated against the official rules.** Every validator cites the specification, law or dataset it implements (`@see` in the docs), and the test suite is mutation-tested, not just covered. diff --git a/docs/llms-full.txt b/docs/llms-full.txt index d9e6e4b4..d8e88250 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -146,7 +146,7 @@ Brazilian Utils is a library focused on solving problems that we face daily in t ### Why Brazilian Utils - **Zero runtime dependencies.** Nothing else lands in your `node_modules` or in your bundle. -- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs under 1 KB; every util is also its own subpath entry (`@brazilian-utils/brazilian-utils/get-cities`) for the heavy ones. +- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs about 1.2 KB minified (0.6 KB gzipped); every util is also its own subpath entry (`@brazilian-utils/brazilian-utils/get-cities`) for the heavy ones. - **Runs everywhere.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno and evergreen browsers, tested in CI on every one of them. - **Written in TypeScript.** Types ship with the package; the public API is tracked by an API report so nothing changes silently. - **Validated against the official rules.** Every validator cites the specification, law or dataset it implements (`@see` in the docs), and the test suite is mutation-tested, not just covered. diff --git a/docs/pt-br/getting-started.md b/docs/pt-br/getting-started.md index 20f16f76..3ce59e3f 100644 --- a/docs/pt-br/getting-started.md +++ b/docs/pt-br/getting-started.md @@ -5,7 +5,7 @@ Brazilian Utils é uma biblioteca com foco na resolução de problemas que enfre ## Por que Brazilian Utils - **Zero dependências de runtime.** Nada além da lib entra no seu `node_modules` ou no seu bundle. -- **Tree-shakeable até a função.** `import { isValidCpf }` custa menos de 1 KB; cada utilitário também é um subpath próprio (`@brazilian-utils/brazilian-utils/get-cities`) para os mais pesados. +- **Tree-shakeable até a função.** `import { isValidCpf }` custa cerca de 1,2 KB minificado (0,6 KB com gzip); cada utilitário também é um subpath próprio (`@brazilian-utils/brazilian-utils/get-cities`) para os mais pesados. - **Roda em qualquer lugar.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno e navegadores modernos, testados no CI em todos eles. - **Escrita em TypeScript.** Os tipos vêm no pacote; a API pública é acompanhada por um relatório de API, então nada muda em silêncio. - **Validada contra as regras oficiais.** Cada validador cita a especificação, lei ou base de dados que implementa (`@see` na documentação), e a suíte de testes passa por mutation testing, não só por cobertura. @@ -63,7 +63,7 @@ Você pode conferir a lista de utilitários [clicando aqui](utilities.md). ## Tamanho do bundle -O pacote é tree-shakeable: importar um utilitário da raiz traz apenas o código daquele utilitário, não o resto da biblioteca. `isValidCpf`, por exemplo, adiciona cerca de 1,2 KB minificado (0,6 KB com gzip) ao seu bundle. Um bundler com suporte a tree-shaking (webpack, Rollup, esbuild, Vite etc.) descarta todos os outros utilitários. +O pacote é tree-shakeable: importar um utilitário da raiz traz apenas o código daquele utilitário, não o resto da biblioteca. `isValidCpf`, por exemplo, adiciona cerca de 1,2 KB minificado (0,6 KB com gzip) ao seu bundle. Um bundler com suporte a tree-shaking (webpack, Rollup, esbuild, Vite, etc.) descarta todos os outros utilitários. Alguns utilitários são a exceção: cada um embute um dataset oficial e pesa muito mais que todos os outros utilitários somados. Estes são os tamanhos de um import isolado, minificado e com gzip: diff --git a/src/_internals/constants/number-words.ts b/src/_internals/constants/number-words.ts index dd0b9ad9..e4efd314 100644 --- a/src/_internals/constants/number-words.ts +++ b/src/_internals/constants/number-words.ts @@ -5,7 +5,7 @@ * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2024/lei/L14822.htm * Lei nº 14.822/2024 (Lei Orçamentária Anual de 2024), art. 1º, which spells 14 "quatorze" * ("quatrocentos e quatorze bilhões"), the form the official Brazilian texts use; the Vocabulário - * Ortográfico admits both "quatorze" and "quatorze", and num2words' Portuguese table (below) picks + * Ortográfico admits both "catorze" and "quatorze", and num2words' Portuguese table (below) picks * "quatorze". * @see Based on: https://github.com/savoirfairelinux/num2words/blob/master/num2words/lang_PT.py * num2words' Portuguese table, the source of every other word of this file. diff --git a/src/_internals/number-to-words/number-to-words.ts b/src/_internals/number-to-words/number-to-words.ts index d7097d20..d732de32 100644 --- a/src/_internals/number-to-words/number-to-words.ts +++ b/src/_internals/number-to-words/number-to-words.ts @@ -86,7 +86,7 @@ const isRoundHundred = (value: number): boolean => value % 100 === 0; * decrees ("mil quinhentos e dezoito reais", Decreto 12.342/2024) and of the examples in the Manual * de Redação da Presidência da República ("mil duzentos e cinquenta reais", "mil e quatrocentos * reais"). It deviates from `num2words`' pt_BR locale, which separates the groups with commas - * ("mil duzentos e trinta e cinco") and writes "e" before an intermediate group below 100. + * ("mil, duzentos e trinta e cinco") and writes "e" before an intermediate group below 100. * * @param {number} value - A non-negative integer in `[0, NUMBER_TO_WORDS_MAX_VALUE]`. * @param {NumberToWordsOptions} [options] - Optional conversion options. From b928819171d20883cdf9707d5d9c0b35e89db7ac Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:27:57 -0300 Subject: [PATCH 54/75] fix(format): never throw on a value without a string conversion `formatCpf(Object.create(null))`, and every other formatter or parser that reads its digits or letters through the shared sanitizers, threw a TypeError because the sanitizer called `value.toString()` on an object that has none. The sanitizers now read the value through `String(value)` inside a guard that turns a failed conversion into an empty string, so hostile input gets the empty result of the family, as `null` and `undefined` already did. --- .../sanitize-to-alphanumeric.ts | 6 ++- .../sanitize-to-digits/sanitize-to-digits.ts | 7 +++- .../to-string-safe/to-string-safe.test.ts | 41 +++++++++++++++++++ .../to-string-safe/to-string-safe.ts | 22 ++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 src/_internals/to-string-safe/to-string-safe.test.ts create mode 100644 src/_internals/to-string-safe/to-string-safe.ts diff --git a/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts b/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts index ccf97d62..af016eb6 100644 --- a/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts +++ b/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts @@ -1,5 +1,8 @@ +import { toStringSafe } from "../to-string-safe/to-string-safe"; + /** * Sanitizes the input value by removing all non-alphanumeric characters and uppercasing the result. + * A value with no string conversion (an object with a null prototype) reads as `""` instead of throwing. * * @param {string|number} value - The input value to be sanitized. It can be a string or a number. * @returns {string} A string containing only uppercase alphanumeric characters from the input value. @@ -12,7 +15,6 @@ * ``` */ export const sanitizeToAlphanumeric = (value: string | number): string => - value - .toString() + toStringSafe(value) .replaceAll(/[^A-Za-z0-9]/g, "") .toUpperCase(); diff --git a/src/_internals/sanitize-to-digits/sanitize-to-digits.ts b/src/_internals/sanitize-to-digits/sanitize-to-digits.ts index 4bb380c2..57f854fa 100644 --- a/src/_internals/sanitize-to-digits/sanitize-to-digits.ts +++ b/src/_internals/sanitize-to-digits/sanitize-to-digits.ts @@ -1,5 +1,8 @@ +import { toStringSafe } from "../to-string-safe/to-string-safe"; + /** - * Sanitizes the input value by removing all non-digit characters. + * Sanitizes the input value by removing all non-digit characters. A value with no string + * conversion (an object with a null prototype) reads as `""` instead of throwing. * * @param {string|number} value - The input value to be sanitized. It can be a string or a number. * @returns {string} A string containing only the digit characters from the input value. @@ -13,4 +16,4 @@ * ``` */ export const sanitizeToDigits = (value: string | number): string => - value.toString().replaceAll(/\D/g, ""); + toStringSafe(value).replaceAll(/\D/g, ""); diff --git a/src/_internals/to-string-safe/to-string-safe.test.ts b/src/_internals/to-string-safe/to-string-safe.test.ts new file mode 100644 index 00000000..9a85898a --- /dev/null +++ b/src/_internals/to-string-safe/to-string-safe.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, expectTypeOf, it, test } from "../test/runtime"; +import { toStringSafe } from "./to-string-safe"; + +describe("toStringSafe", () => { + it("should read strings and numbers as String does", () => { + expect(toStringSafe("abc")).toBe("abc"); + expect(toStringSafe(123)).toBe("123"); + expect(toStringSafe(1.5)).toBe("1.5"); + expect(toStringSafe(12n)).toBe("12"); + }); + + it("should read arrays, booleans and plain objects as String does", () => { + expect(toStringSafe([1, 2])).toBe("1,2"); + expect(toStringSafe(true)).toBe("true"); + expect(toStringSafe({})).toBe("[object Object]"); + expect(toStringSafe(null)).toBe("null"); + // @ts-expect-error: intentionally missing argument + expect(toStringSafe()).toBe("undefined"); + }); + + it("should return an empty string for an object with a null prototype, which has no toString", () => { + expect(toStringSafe(Object.create(null))).toBe(""); + }); + + it("should return an empty string for an object whose toString throws", () => { + const hostile = { + toString: (): string => { + throw new Error("no"); + }, + }; + + expect(toStringSafe(hostile)).toBe(""); + }); +}); + +describe("toStringSafe types", () => { + test("should take unknown and return a string", () => { + expectTypeOf(toStringSafe).parameter(0).toEqualTypeOf(); + expectTypeOf(toStringSafe).returns.toEqualTypeOf(); + }); +}); diff --git a/src/_internals/to-string-safe/to-string-safe.ts b/src/_internals/to-string-safe/to-string-safe.ts new file mode 100644 index 00000000..ab911592 --- /dev/null +++ b/src/_internals/to-string-safe/to-string-safe.ts @@ -0,0 +1,22 @@ +/** + * Reads a value as a string the way `String(value)` does, but returns `""` when the value has no + * string conversion (an object with a null prototype, an object whose `toString` throws) instead + * of throwing, so a formatter handed hostile input never throws. + * + * @param {unknown} value - The value to read. + * @returns {string} `String(value)`, or `""` when that conversion throws. + * + * @example + * ```typescript + * toStringSafe(123) // "123" + * toStringSafe([1, 2]) // "1,2" + * toStringSafe(Object.create(null)) // "" + * ``` + */ +export const toStringSafe = (value: unknown): string => { + try { + return String(value); + } catch { + return ""; + } +}; From e676688b326b18165d160e94a45de3e6e8f1e344 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:27:57 -0300 Subject: [PATCH 55/75] feat(format): mask the new formatters progressively like formatCpf and add pad to formatLegalNature `formatCnae`, `formatNcm`, `formatNfeKey`, `formatCertidao` and `formatIban` returned `""` for a value with characters outside the mask, for a negative or fractional number, or for a number at all, while every formatter of 2.3.0 reads the value for its digits (or letters) and masks it as far as they go, which is what an input mask needs. The five now follow that contract: only the digits or letters are read, a partial value is masked progressively and a null or undefined value gives `""`. `formatLegalNature` gains the same `pad` option `formatCnae` and `formatCpf` have, exported as `FormatLegalNatureOptions`. --- docs/llms-full.txt | 25 +++++++++------- docs/pt-br/utilities.md | 25 +++++++++------- docs/utilities.md | 25 +++++++++------- src/format-certidao/format-certidao.test.ts | 8 ++--- src/format-certidao/format-certidao.ts | 15 ++++++---- src/format-cnae/format-cnae.test.ts | 20 +++++++++---- src/format-cnae/format-cnae.ts | 29 +++++++----------- src/format-iban/format-iban.test.ts | 12 +++++--- src/format-iban/format-iban.ts | 21 +++++-------- .../format-legal-nature.test.ts | 18 ++++++++++- .../format-legal-nature.ts | 21 ++++++++++++- src/format-ncm/format-ncm.test.ts | 20 +++++++++---- src/format-ncm/format-ncm.ts | 30 +++++++------------ src/format-nfe-key/format-nfe-key.test.ts | 4 +-- src/format-nfe-key/format-nfe-key.ts | 11 +++---- src/index.test.ts | 2 ++ src/index.ts | 5 +++- 17 files changed, 170 insertions(+), 121 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index d8e88250..6584c764 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -532,7 +532,7 @@ isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 000 ### formatNfeKey -Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. A value that is not a string is only read when it is a non-negative safe integer, so anything with no usable digit representation (a negative or fractional number, an object, an object created with `Object.create(null)`) gives `''`. +Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. Like every formatter of this package, the value is read for its digits and grouped as far as they go, so a masked or partial key still being typed is grouped progressively, and anything without a digit (an object, `true`, an object created with `Object.create(null)`) gives `''` instead of throwing. Use `isValidNfeKey` to check a key. ```javascript import { formatNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -1010,7 +1010,7 @@ isValidIban('DE89370400440532013000'); // false (non Brazilian IBAN) ### formatIban -Format an IBAN in the ISO 13616 print grouping, blocks of 4 characters, the presentation used on statements and bank forms. Does not validate the check digits or the field layout; formats whatever is given, up to the 29 character length of a Brazilian IBAN, as far as it goes, so the function can also be used as an input mask, and an IBAN of another country is grouped the same way up to that length. Use `isValidIban` to check validity. The value may be compact (`'BR1500000000000010932840814P2'`), already in the ISO 13616 print format (letters and digits in groups separated by a single space) or a partial value still being typed (`'BR15'`), in every case with optional surrounding whitespace; only a character outside letters and digits, or a separator other than a single space, returns an empty string instead of being quietly dropped. +Format an IBAN in the ISO 13616 print grouping, blocks of 4 characters, the presentation used on statements and bank forms. Does not validate the check digits or the field layout; formats whatever is given, up to the 29 character length of a Brazilian IBAN, as far as it goes, so the function can also be used as an input mask, and an IBAN of another country is grouped the same way up to that length. Use `isValidIban` to check validity. The value may be compact (`'BR1500000000000010932840814P2'`), already in the ISO 13616 print format or a partial value still being typed (`'BR15'`); like every formatter of this package, it is read for its letters and digits and grouped as far as they go, any other character (a hyphen, a dot, extra whitespace) is dropped and the letters are uppercased. Only a value that is not a string gives an empty string. ```javascript import { formatIban } from '@brazilian-utils/brazilian-utils'; @@ -1018,7 +1018,7 @@ import { formatIban } from '@brazilian-utils/brazilian-utils'; formatIban('BR1500000000000010932840814P2'); // 'BR15 0000 0000 0000 1093 2840 814P 2' formatIban('br1500000000000010932840814p2'); // 'BR15 0000 0000 0000 1093 2840 814P 2' formatIban('BR15'); // 'BR15' -formatIban('BR1500000000000010932840814P-2'); // '' (hyphens are not part of an IBAN) +formatIban('BR15 0000-0000.0000/1093 2840 814P-2'); // 'BR15 0000 0000 0000 1093 2840 814P 2' (only letters and digits are read) ``` ### parseIban @@ -1473,12 +1473,15 @@ generateProcessoJuridico({ year: 10000 }); // null (year out of range) ### formatLegalNature -Format a legal nature code. +Format a legal nature code. `options.pad` (part of `FormatLegalNatureOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes; with `true` the value is first left padded with zeros to the 4 digits of a complete code. Use `isValidLegalNature` to check a code. ```javascript import { formatLegalNature } from '@brazilian-utils/brazilian-utils'; formatLegalNature('2062'); // 206-2 +formatLegalNature(2062); // 206-2 +formatLegalNature('206'); // 206 (masked as far as it goes) +formatLegalNature('62', { pad: true }); // 006-2 (padded to 4 digits first) ``` ### isValidLegalNature @@ -1970,7 +1973,7 @@ The `Certidao` result carries: ### formatCertidao -Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. +Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). The parameter is typed as a string because the 32 digits of a matrícula are more than a JavaScript number can hold exactly; at runtime the value is read for its digits and masked as far as they go, like in every formatter of this package, so a partial matrícula still being typed is masked progressively. ```javascript import { formatCertidao } from '@brazilian-utils/brazilian-utils'; @@ -2134,7 +2137,7 @@ isValidCnae(-111301); // false (not a non-negative safe integer) ### formatCnae -Format a CNAE (Classificação Nacional de Atividades Econômicas) subclass code. `options.pad` (part of `FormatCnaeOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes, which is what an input being typed into needs; with `true` the value is first left padded with zeros to the 7 digits of a complete subclass code, so it always comes back fully masked. A number is treated exactly like the string of its digits, so it is only padded under `pad: true`. Only digits and the mask characters are accepted; anything else gives `''`, and so does a number that is not a non-negative safe integer. +Format a CNAE (Classificação Nacional de Atividades Econômicas) subclass code. `options.pad` (part of `FormatCnaeOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes, which is what an input being typed into needs; with `true` the value is first left padded with zeros to the 7 digits of a complete subclass code, so it always comes back fully masked. A number is treated exactly like the string of its digits, so it is only padded under `pad: true`. Like every formatter of this package, the value is read for its digits and masked as far as they go: characters outside the mask are dropped and a number is read as the string of its digits, sign and decimal point included. Use `isValidCnae` to check a code. ```javascript import { formatCnae } from '@brazilian-utils/brazilian-utils'; @@ -2144,8 +2147,8 @@ formatCnae('62'); // 62 (masked as far as it goes) formatCnae('62015'); // 6201-5 formatCnae('62', { pad: true }); // 0000-0/62 (padded to 7 digits first) formatCnae(111301, { pad: true }); // 0111-3/01 -formatCnae('abc6201501'); // '' (not a documented form) -formatCnae(-6201501); // '' (not a non-negative safe integer) +formatCnae('abc6201501'); // 6201-5/01 (only the digits are read) +formatCnae(-6201501); // 6201-5/01 ``` ### getCnae @@ -2176,7 +2179,7 @@ isValidNcm(-84713012); // false (not a non-negative safe integer) ### formatNcm -Format an NCM (Nomenclatura Comum do Mercosul) code. `options.pad` (part of `FormatNcmOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes, which is what an input being typed into needs; with `true` the value is first left padded with zeros to the 8 digits of a complete code, so it always comes back fully masked. A number is treated exactly like the string of its digits, so it is only padded under `pad: true`. Only digits and the mask characters are accepted; anything else gives `''`, and so does a number that is not a non-negative safe integer. +Format an NCM (Nomenclatura Comum do Mercosul) code. `options.pad` (part of `FormatNcmOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes, which is what an input being typed into needs; with `true` the value is first left padded with zeros to the 8 digits of a complete code, so it always comes back fully masked. A number is treated exactly like the string of its digits, so it is only padded under `pad: true`. Like every formatter of this package, the value is read for its digits and masked as far as they go: characters outside the mask are dropped and a number is read as the string of its digits, sign and decimal point included. Use `isValidNcm` to check a code. ```javascript import { formatNcm } from '@brazilian-utils/brazilian-utils'; @@ -2185,8 +2188,8 @@ formatNcm('84713012'); // 8471.30.12 formatNcm('8471'); // 8471 (masked as far as it goes) formatNcm('847130'); // 8471.30 formatNcm('8471', { pad: true }); // 0000.84.71 (padded to 8 digits first) -formatNcm('abc8471'); // '' (not a documented form) -formatNcm(-84713012); // '' (not a non-negative safe integer) +formatNcm('abc8471'); // 8471 (only the digits are read) +formatNcm(-84713012); // 8471.30.12 ``` ### isValidCfop diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 75ceca1d..938e2d74 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -290,7 +290,7 @@ isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 000 ## formatNfeKey -Formata uma chave de acesso de DF-e (Documento Fiscal eletrônico) em grupos de 4 dígitos separados por espaço, a forma em que todo documento auxiliar a imprime: o DANFE da NF-e e da NFC-e, o DACTE do CT-e, do CT-e OS e da GTV-e, o DAMDFE do MDF-e, o DABPE do BP-e, o DANF3E da NF3e e o DANFE-COM da NFCom. Um valor que não seja string só é lido quando é um inteiro seguro não negativo, então qualquer coisa sem representação utilizável em dígitos (um número negativo ou fracionário, um objeto, um objeto criado com `Object.create(null)`) devolve `''`. +Formata uma chave de acesso de DF-e (Documento Fiscal eletrônico) em grupos de 4 dígitos separados por espaço, a forma em que todo documento auxiliar a imprime: o DANFE da NF-e e da NFC-e, o DACTE do CT-e, do CT-e OS e da GTV-e, o DAMDFE do MDF-e, o DABPE do BP-e, o DANF3E da NF3e e o DANFE-COM da NFCom. Como todo formatador deste pacote, o valor é lido pelos seus dígitos e agrupado até onde eles vão, então uma chave com máscara ou parcial, ainda sendo digitada, é agrupada progressivamente, e qualquer coisa sem dígito (um objeto, `true`, um objeto criado com `Object.create(null)`) devolve `''` em vez de lançar. Use `isValidNfeKey` para verificar uma chave. ```javascript import { formatNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -768,7 +768,7 @@ isValidIban('DE89370400440532013000'); // false (IBAN não brasileiro) ## formatIban -Formata um IBAN no agrupamento impresso da ISO 13616, blocos de 4 caracteres, a apresentação usada em extratos e formulários bancários. Não valida os dígitos verificadores nem o layout dos campos; formata o que for passado, até o limite de 29 caracteres de um IBAN brasileiro, até onde for possível, então a função também pode ser usada como máscara de digitação, e um IBAN de outro país é agrupado do mesmo jeito até esse limite. Use `isValidIban` para verificar a validade. O valor pode ser compacto (`'BR1500000000000010932840814P2'`), já estar no formato impresso da ISO 13616 (letras e dígitos em grupos separados por um único espaço) ou ser um valor parcial ainda sendo digitado (`'BR15'`), em todos os casos com espaços em branco opcionais no início e no fim; apenas um caractere fora de letras e dígitos, ou um separador diferente de um único espaço, resulta em uma string vazia, em vez de ser descartado silenciosamente. +Formata um IBAN no agrupamento impresso da ISO 13616, blocos de 4 caracteres, a apresentação usada em extratos e formulários bancários. Não valida os dígitos verificadores nem o layout dos campos; formata o que for passado, até o limite de 29 caracteres de um IBAN brasileiro, até onde for possível, então a função também pode ser usada como máscara de digitação, e um IBAN de outro país é agrupado do mesmo jeito até esse limite. Use `isValidIban` para verificar a validade. O valor pode ser compacto (`'BR1500000000000010932840814P2'`), já estar no formato impresso da ISO 13616 ou ser um valor parcial ainda sendo digitado (`'BR15'`); como todo formatador deste pacote, ele é lido pelas suas letras e dígitos e agrupado até onde eles vão, qualquer outro caractere (hífen, ponto, espaço a mais) é descartado e as letras viram maiúsculas. Só um valor que não seja string resulta em uma string vazia. ```javascript import { formatIban } from '@brazilian-utils/brazilian-utils'; @@ -776,7 +776,7 @@ import { formatIban } from '@brazilian-utils/brazilian-utils'; formatIban('BR1500000000000010932840814P2'); // 'BR15 0000 0000 0000 1093 2840 814P 2' formatIban('br1500000000000010932840814p2'); // 'BR15 0000 0000 0000 1093 2840 814P 2' formatIban('BR15'); // 'BR15' -formatIban('BR1500000000000010932840814P-2'); // '' (hífen não faz parte de um IBAN) +formatIban('BR15 0000-0000.0000/1093 2840 814P-2'); // 'BR15 0000 0000 0000 1093 2840 814P 2' (só letras e dígitos são lidos) ``` ## parseIban @@ -1231,12 +1231,15 @@ generateProcessoJuridico({ year: 10000 }); // null (ano fora do intervalo) ## formatLegalNature -Formata um código de natureza jurídica. +Formata um código de natureza jurídica. `options.pad` (parte de `FormatLegalNatureOptions`) funciona exatamente como em `formatCpf`/`formatCep`: com o padrão `false` a máscara é aplicada progressivamente, até onde o valor vai; com `true` o valor é primeiro completado com zeros à esquerda até os 4 dígitos de um código completo. Use `isValidLegalNature` para verificar um código. ```javascript import { formatLegalNature } from '@brazilian-utils/brazilian-utils'; formatLegalNature('2062'); // 206-2 +formatLegalNature(2062); // 206-2 +formatLegalNature('206'); // 206 (máscara aplicada até onde o valor vai) +formatLegalNature('62', { pad: true }); // 006-2 (completado até 4 dígitos antes) ``` ## isValidLegalNature @@ -1728,7 +1731,7 @@ O resultado `Certidao` traz: ## formatCertidao -Formata a matrícula de uma certidão de registro civil na máscara impressa do Provimento, os 32 dígitos agrupados em 6 2 2 4 1 5 3 7 2 e separados por espaços. `options.pad` (parte de `FormatCertidaoOptions`) preenche o valor com zeros à esquerda até 32 dígitos (padrão `false`). A máscara é a do [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. +Formata a matrícula de uma certidão de registro civil na máscara impressa do Provimento, os 32 dígitos agrupados em 6 2 2 4 1 5 3 7 2 e separados por espaços. `options.pad` (parte de `FormatCertidaoOptions`) preenche o valor com zeros à esquerda até 32 dígitos (padrão `false`). A máscara é a do [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). O parâmetro é tipado como string porque os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta com exatidão; em tempo de execução o valor é lido pelos seus dígitos e a máscara é aplicada até onde eles vão, como em todo formatador deste pacote, então uma matrícula parcial ainda sendo digitada é mascarada progressivamente. ```javascript import { formatCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1892,7 +1895,7 @@ isValidCnae(-111301); // false (não é um inteiro seguro não negativo) ## formatCnae -Formata um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas). `options.pad` (parte de `FormatCnaeOptions`) funciona exatamente como em `formatCpf`/`formatCep`: com o padrão `false` a máscara é aplicada progressivamente, até onde o valor vai, que é o que um campo sendo digitado precisa; com `true` o valor é primeiro completado com zeros à esquerda até os 7 dígitos de uma subclasse completa, então ele sempre volta com a máscara inteira. Um número é tratado exatamente como a string dos seus dígitos, ou seja, só é completado com `pad: true`. Só dígitos e os caracteres de máscara são aceitos; qualquer outra coisa retorna `''`, assim como um número que não seja um inteiro seguro não negativo. +Formata um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas). `options.pad` (parte de `FormatCnaeOptions`) funciona exatamente como em `formatCpf`/`formatCep`: com o padrão `false` a máscara é aplicada progressivamente, até onde o valor vai, que é o que um campo sendo digitado precisa; com `true` o valor é primeiro completado com zeros à esquerda até os 7 dígitos de uma subclasse completa, então ele sempre volta com a máscara inteira. Um número é tratado exatamente como a string dos seus dígitos, ou seja, só é completado com `pad: true`. Como todo formatador deste pacote, o valor é lido pelos seus dígitos e a máscara é aplicada até onde eles vão: caracteres fora da máscara são descartados e um número é lido como a string dos seus dígitos, sinal e ponto decimal inclusos. Use `isValidCnae` para verificar um código. ```javascript import { formatCnae } from '@brazilian-utils/brazilian-utils'; @@ -1902,8 +1905,8 @@ formatCnae('62'); // 62 (máscara aplicada até onde o valor vai) formatCnae('62015'); // 6201-5 formatCnae('62', { pad: true }); // 0000-0/62 (completado até 7 dígitos antes) formatCnae(111301, { pad: true }); // 0111-3/01 -formatCnae('abc6201501'); // '' (não é uma forma documentada) -formatCnae(-6201501); // '' (não é um inteiro seguro não negativo) +formatCnae('abc6201501'); // 6201-5/01 (só os dígitos são lidos) +formatCnae(-6201501); // 6201-5/01 ``` ## getCnae @@ -1934,7 +1937,7 @@ isValidNcm(-84713012); // false (não é um inteiro seguro não negativo) ## formatNcm -Formata um código NCM (Nomenclatura Comum do Mercosul). `options.pad` (parte de `FormatNcmOptions`) funciona exatamente como em `formatCpf`/`formatCep`: com o padrão `false` a máscara é aplicada progressivamente, até onde o valor vai, que é o que um campo sendo digitado precisa; com `true` o valor é primeiro completado com zeros à esquerda até os 8 dígitos de um código completo, então ele sempre volta com a máscara inteira. Um número é tratado exatamente como a string dos seus dígitos, ou seja, só é completado com `pad: true`. Só dígitos e os caracteres de máscara são aceitos; qualquer outra coisa retorna `''`, assim como um número que não seja um inteiro seguro não negativo. +Formata um código NCM (Nomenclatura Comum do Mercosul). `options.pad` (parte de `FormatNcmOptions`) funciona exatamente como em `formatCpf`/`formatCep`: com o padrão `false` a máscara é aplicada progressivamente, até onde o valor vai, que é o que um campo sendo digitado precisa; com `true` o valor é primeiro completado com zeros à esquerda até os 8 dígitos de um código completo, então ele sempre volta com a máscara inteira. Um número é tratado exatamente como a string dos seus dígitos, ou seja, só é completado com `pad: true`. Como todo formatador deste pacote, o valor é lido pelos seus dígitos e a máscara é aplicada até onde eles vão: caracteres fora da máscara são descartados e um número é lido como a string dos seus dígitos, sinal e ponto decimal inclusos. Use `isValidNcm` para verificar um código. ```javascript import { formatNcm } from '@brazilian-utils/brazilian-utils'; @@ -1943,8 +1946,8 @@ formatNcm('84713012'); // 8471.30.12 formatNcm('8471'); // 8471 (máscara aplicada até onde o valor vai) formatNcm('847130'); // 8471.30 formatNcm('8471', { pad: true }); // 0000.84.71 (completado até 8 dígitos antes) -formatNcm('abc8471'); // '' (não é uma forma documentada) -formatNcm(-84713012); // '' (não é um inteiro seguro não negativo) +formatNcm('abc8471'); // 8471 (só os dígitos são lidos) +formatNcm(-84713012); // 8471.30.12 ``` ## isValidCfop diff --git a/docs/utilities.md b/docs/utilities.md index 5805081e..3e73102f 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -290,7 +290,7 @@ isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 000 ## formatNfeKey -Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. A value that is not a string is only read when it is a non-negative safe integer, so anything with no usable digit representation (a negative or fractional number, an object, an object created with `Object.create(null)`) gives `''`. +Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. Like every formatter of this package, the value is read for its digits and grouped as far as they go, so a masked or partial key still being typed is grouped progressively, and anything without a digit (an object, `true`, an object created with `Object.create(null)`) gives `''` instead of throwing. Use `isValidNfeKey` to check a key. ```javascript import { formatNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -768,7 +768,7 @@ isValidIban('DE89370400440532013000'); // false (non Brazilian IBAN) ## formatIban -Format an IBAN in the ISO 13616 print grouping, blocks of 4 characters, the presentation used on statements and bank forms. Does not validate the check digits or the field layout; formats whatever is given, up to the 29 character length of a Brazilian IBAN, as far as it goes, so the function can also be used as an input mask, and an IBAN of another country is grouped the same way up to that length. Use `isValidIban` to check validity. The value may be compact (`'BR1500000000000010932840814P2'`), already in the ISO 13616 print format (letters and digits in groups separated by a single space) or a partial value still being typed (`'BR15'`), in every case with optional surrounding whitespace; only a character outside letters and digits, or a separator other than a single space, returns an empty string instead of being quietly dropped. +Format an IBAN in the ISO 13616 print grouping, blocks of 4 characters, the presentation used on statements and bank forms. Does not validate the check digits or the field layout; formats whatever is given, up to the 29 character length of a Brazilian IBAN, as far as it goes, so the function can also be used as an input mask, and an IBAN of another country is grouped the same way up to that length. Use `isValidIban` to check validity. The value may be compact (`'BR1500000000000010932840814P2'`), already in the ISO 13616 print format or a partial value still being typed (`'BR15'`); like every formatter of this package, it is read for its letters and digits and grouped as far as they go, any other character (a hyphen, a dot, extra whitespace) is dropped and the letters are uppercased. Only a value that is not a string gives an empty string. ```javascript import { formatIban } from '@brazilian-utils/brazilian-utils'; @@ -776,7 +776,7 @@ import { formatIban } from '@brazilian-utils/brazilian-utils'; formatIban('BR1500000000000010932840814P2'); // 'BR15 0000 0000 0000 1093 2840 814P 2' formatIban('br1500000000000010932840814p2'); // 'BR15 0000 0000 0000 1093 2840 814P 2' formatIban('BR15'); // 'BR15' -formatIban('BR1500000000000010932840814P-2'); // '' (hyphens are not part of an IBAN) +formatIban('BR15 0000-0000.0000/1093 2840 814P-2'); // 'BR15 0000 0000 0000 1093 2840 814P 2' (only letters and digits are read) ``` ## parseIban @@ -1231,12 +1231,15 @@ generateProcessoJuridico({ year: 10000 }); // null (year out of range) ## formatLegalNature -Format a legal nature code. +Format a legal nature code. `options.pad` (part of `FormatLegalNatureOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes; with `true` the value is first left padded with zeros to the 4 digits of a complete code. Use `isValidLegalNature` to check a code. ```javascript import { formatLegalNature } from '@brazilian-utils/brazilian-utils'; formatLegalNature('2062'); // 206-2 +formatLegalNature(2062); // 206-2 +formatLegalNature('206'); // 206 (masked as far as it goes) +formatLegalNature('62', { pad: true }); // 006-2 (padded to 4 digits first) ``` ## isValidLegalNature @@ -1728,7 +1731,7 @@ The `Certidao` result carries: ## formatCertidao -Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. +Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). The parameter is typed as a string because the 32 digits of a matrícula are more than a JavaScript number can hold exactly; at runtime the value is read for its digits and masked as far as they go, like in every formatter of this package, so a partial matrícula still being typed is masked progressively. ```javascript import { formatCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1892,7 +1895,7 @@ isValidCnae(-111301); // false (not a non-negative safe integer) ## formatCnae -Format a CNAE (Classificação Nacional de Atividades Econômicas) subclass code. `options.pad` (part of `FormatCnaeOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes, which is what an input being typed into needs; with `true` the value is first left padded with zeros to the 7 digits of a complete subclass code, so it always comes back fully masked. A number is treated exactly like the string of its digits, so it is only padded under `pad: true`. Only digits and the mask characters are accepted; anything else gives `''`, and so does a number that is not a non-negative safe integer. +Format a CNAE (Classificação Nacional de Atividades Econômicas) subclass code. `options.pad` (part of `FormatCnaeOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes, which is what an input being typed into needs; with `true` the value is first left padded with zeros to the 7 digits of a complete subclass code, so it always comes back fully masked. A number is treated exactly like the string of its digits, so it is only padded under `pad: true`. Like every formatter of this package, the value is read for its digits and masked as far as they go: characters outside the mask are dropped and a number is read as the string of its digits, sign and decimal point included. Use `isValidCnae` to check a code. ```javascript import { formatCnae } from '@brazilian-utils/brazilian-utils'; @@ -1902,8 +1905,8 @@ formatCnae('62'); // 62 (masked as far as it goes) formatCnae('62015'); // 6201-5 formatCnae('62', { pad: true }); // 0000-0/62 (padded to 7 digits first) formatCnae(111301, { pad: true }); // 0111-3/01 -formatCnae('abc6201501'); // '' (not a documented form) -formatCnae(-6201501); // '' (not a non-negative safe integer) +formatCnae('abc6201501'); // 6201-5/01 (only the digits are read) +formatCnae(-6201501); // 6201-5/01 ``` ## getCnae @@ -1934,7 +1937,7 @@ isValidNcm(-84713012); // false (not a non-negative safe integer) ## formatNcm -Format an NCM (Nomenclatura Comum do Mercosul) code. `options.pad` (part of `FormatNcmOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes, which is what an input being typed into needs; with `true` the value is first left padded with zeros to the 8 digits of a complete code, so it always comes back fully masked. A number is treated exactly like the string of its digits, so it is only padded under `pad: true`. Only digits and the mask characters are accepted; anything else gives `''`, and so does a number that is not a non-negative safe integer. +Format an NCM (Nomenclatura Comum do Mercosul) code. `options.pad` (part of `FormatNcmOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes, which is what an input being typed into needs; with `true` the value is first left padded with zeros to the 8 digits of a complete code, so it always comes back fully masked. A number is treated exactly like the string of its digits, so it is only padded under `pad: true`. Like every formatter of this package, the value is read for its digits and masked as far as they go: characters outside the mask are dropped and a number is read as the string of its digits, sign and decimal point included. Use `isValidNcm` to check a code. ```javascript import { formatNcm } from '@brazilian-utils/brazilian-utils'; @@ -1943,8 +1946,8 @@ formatNcm('84713012'); // 8471.30.12 formatNcm('8471'); // 8471 (masked as far as it goes) formatNcm('847130'); // 8471.30 formatNcm('8471', { pad: true }); // 0000.84.71 (padded to 8 digits first) -formatNcm('abc8471'); // '' (not a documented form) -formatNcm(-84713012); // '' (not a non-negative safe integer) +formatNcm('abc8471'); // 8471 (only the digits are read) +formatNcm(-84713012); // 8471.30.12 ``` ## isValidCfop diff --git a/src/format-certidao/format-certidao.test.ts b/src/format-certidao/format-certidao.test.ts index 38ae15e0..fc86c88f 100644 --- a/src/format-certidao/format-certidao.test.ts +++ b/src/format-certidao/format-certidao.test.ts @@ -60,10 +60,10 @@ describe("formatCertidao", () => { }); }); - describe("should refuse a number", () => { - test("because the 32 digits of a matrícula do not fit in a JavaScript number", () => { + describe("should read a number as the string of its digits, like formatCpf", () => { + test("masking it as far as it goes; the parameter is typed as a string only because 32 digits do not fit a number", () => { // @ts-expect-error: intentionally invalid input - expect(formatCertidao(104_539_015_520)).toBe(""); + expect(formatCertidao(104_539_015_520)).toBe("104539 01 55 20"); }); }); @@ -103,7 +103,7 @@ describe("formatCertidao", () => { fc.property(fc.string({ unit: "grapheme" }), fc.integer(), (text, number) => { expect(typeof formatCertidao(text)).toBe("string"); // @ts-expect-error: intentionally invalid input - expect(formatCertidao(number)).toBe(""); + expect(typeof formatCertidao(number)).toBe("string"); }), ); }); diff --git a/src/format-certidao/format-certidao.ts b/src/format-certidao/format-certidao.ts index d7823ad5..72a6e462 100644 --- a/src/format-certidao/format-certidao.ts +++ b/src/format-certidao/format-certidao.ts @@ -1,5 +1,6 @@ import { CERTIDAO_PATTERN } from "../_internals/constants/certidao"; import { format } from "../_internals/format/format"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; /** Options of `formatCertidao`. */ @@ -12,8 +13,10 @@ export type FormatCertidaoOptions = { * Formats the matrícula of a certidão de registro civil into the printed mask of the norm, the * 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. * - * Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can - * hold, so a numeric argument gives an empty string instead of the digits of a rounded value. + * The parameter is typed as a string because the 32 digits of a matrícula are more than a + * JavaScript number can hold exactly. At runtime the value is read for its digits and masked as + * far as they go, like in every formatter of this package, so a partial matrícula still being + * typed is masked progressively and a number is read as the string of its digits. * * @param {string} value - The matrícula value to be formatted. * @param {FormatCertidaoOptions} [options] - Optional formatting options. @@ -57,10 +60,10 @@ export type FormatCertidaoOptions = { * Third reference implementation agreeing on the weights and on the remainder of 10 read as 1. */ export const formatCertidao = (value: string, options?: FormatCertidaoOptions): string => - typeof value === "string" - ? format({ + isNullish(value) + ? "" + : format({ pad: options?.pad, value: sanitizeToDigits(value), pattern: CERTIDAO_PATTERN, - }) - : ""; + }); diff --git a/src/format-cnae/format-cnae.test.ts b/src/format-cnae/format-cnae.test.ts index e5b6f353..45add4ab 100644 --- a/src/format-cnae/format-cnae.test.ts +++ b/src/format-cnae/format-cnae.test.ts @@ -74,14 +74,22 @@ describe("formatCnae", () => { expect(formatCnae()).toBe(""); }); - it("should return an empty string for a value that is not digits and mask characters", () => { - expect(formatCnae("abc6201501")).toBe(""); + it("should return an empty string for null and undefined even under pad, instead of a zero-filled code", () => { + // @ts-expect-error not a string or number + expect(formatCnae(null, { pad: true })).toBe(""); + // @ts-expect-error not a string or number + expect(formatCnae(undefined, { pad: true })).toBe(""); + }); + + it("should read only the digits of a value with other characters, like formatCpf", () => { + expect(formatCnae("abc6201501")).toBe("6201-5/01"); + expect(formatCnae("62.01-5/01")).toBe("6201-5/01"); }); - it("should return an empty string for a number that is not a non-negative safe integer", () => { - expect(formatCnae(-6_201_501)).toBe(""); - expect(formatCnae(620_150.1)).toBe(""); - expect(formatCnae(2 ** 53)).toBe(""); + it("should read a signed or fractional number as the string of its digits, like formatCpf", () => { + expect(formatCnae(-6_201_501)).toBe("6201-5/01"); + expect(formatCnae(620_150.1)).toBe("6201-5/01"); + expect(formatCnae(2 ** 53)).toBe("9007-1/99"); }); it("should return an empty string for a null-prototype object", () => { diff --git a/src/format-cnae/format-cnae.ts b/src/format-cnae/format-cnae.ts index 11b55bb3..72d6b3f5 100644 --- a/src/format-cnae/format-cnae.ts +++ b/src/format-cnae/format-cnae.ts @@ -1,13 +1,7 @@ import { format } from "../_internals/format/format"; -import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -/** - * Shape a value may be written in while a CNAE code is being typed: digits and the mask - * characters of the `NNNN-N/NN` presentation, nothing else. - */ -const CNAE_MASK_REGEX = /^[\d\s.\-/]*$/; - /** Options of `formatCnae`. */ export type FormatCnaeOptions = { /** Whether to left pad the value with zeros up to the 7 digits of a complete subclass code (default: `false`). */ @@ -28,10 +22,11 @@ export type FormatCnaeOptions = { * `pad: true`, so `formatCnae(111301)` gives `"1113-0/1"` and `formatCnae(111301, { pad: true })` * gives `"0111-3/01"`. * - * A string is only formatted when it holds nothing but digits and the mask characters; - * anything else (`"abc6201501"`) gives `""` instead of having its digits picked out. A number - * is only formatted when it is a non-negative safe integer, since a sign, a decimal point or a - * rounded magnitude would otherwise be read as a code the caller never wrote. + * Like every formatter of this package, the value is read for its digits and masked as far as + * they go: characters outside the mask are dropped (`formatCnae("abc6201501")` gives + * `"6201-5/01"`) and a number is read as the string of its digits, sign and decimal point + * included (`formatCnae(-6201501)` gives `"6201-5/01"`). This is the input-mask contract of + * `formatCpf`; use `isValidCnae` to check a code. * * @param {string|number} value - The CNAE code to be formatted. * @param {FormatCnaeOptions} [options] - Optional formatting options. @@ -46,22 +41,18 @@ export type FormatCnaeOptions = { * formatCnae("62"); // "62" (partial values are masked as far as they go) * formatCnae("62015"); // "6201-5" * formatCnae("62", { pad: true }); // "0000-0/62" (padded to 7 digits first) - * formatCnae("abc6201501"); // "" (not a documented form) - * formatCnae(-6201501); // "" (not a non-negative safe integer) + * formatCnae("abc6201501"); // "6201-5/01" (only the digits are read) + * formatCnae(-6201501); // "6201-5/01" * ``` * * @see Official: https://servicodados.ibge.gov.br/api/v2/cnae/subclasses */ export const formatCnae = (value: string | number, options?: FormatCnaeOptions): string => { - if (!isLookupCode(value)) return ""; - - const code = String(value); - - if (!CNAE_MASK_REGEX.test(code)) return ""; + if (isNullish(value)) return ""; return format({ pad: options?.pad, - value: sanitizeToDigits(code), + value: sanitizeToDigits(value), pattern: "0000-0/00", }); }; diff --git a/src/format-iban/format-iban.test.ts b/src/format-iban/format-iban.test.ts index c64d8fc6..d584a453 100644 --- a/src/format-iban/format-iban.test.ts +++ b/src/format-iban/format-iban.test.ts @@ -38,10 +38,14 @@ describe("formatIban", () => { ); }); - it("should return an empty string when a character outside the print format is present", () => { - expect(formatIban("BR1500000000000010932840814P-2")).toBe(""); - expect(formatIban("BR15 0000-0000.0000/1093 2840 814P 2")).toBe(""); - expect(formatIban("BR15 0000")).toBe(""); + it("should read only the letters and digits of a value with other characters, like formatCpf", () => { + expect(formatIban("BR1500000000000010932840814P-2")).toBe( + "BR15 0000 0000 0000 1093 2840 814P 2", + ); + expect(formatIban("BR15 0000-0000.0000/1093 2840 814P 2")).toBe( + "BR15 0000 0000 0000 1093 2840 814P 2", + ); + expect(formatIban("BR15 0000")).toBe("BR15 0000"); }); it("should cap the result to 29 characters", () => { diff --git a/src/format-iban/format-iban.ts b/src/format-iban/format-iban.ts index 72cfef51..c41fedec 100644 --- a/src/format-iban/format-iban.ts +++ b/src/format-iban/format-iban.ts @@ -1,4 +1,4 @@ -import { BR_IBAN_LENGTH, IBAN_FORMAT_REGEX } from "../_internals/constants/iban"; +import { BR_IBAN_LENGTH } from "../_internals/constants/iban"; import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; import { GROUP_SIZE } from "./constants"; @@ -12,15 +12,14 @@ import { GROUP_SIZE } from "./constants"; * length. Use `isValidIban` to check validity. * * The value may be compact (`"BR1500000000000010932840814P2"`), already in the ISO 13616 print - * format (letters and digits in groups separated by a single space) or a partial value still - * being typed, in every case with optional surrounding whitespace. Only a character outside - * letters and digits, or a separator other than a single space, makes the value something - * other than an IBAN, and then the function returns an empty string instead of quietly - * dropping the character and presenting the rest as an IBAN. + * format, or a partial value still being typed. Like every formatter of this package, it is read + * for its letters and digits and grouped as far as they go: any other character (a hyphen, a + * dot, extra whitespace) is dropped and the letters are uppercased. Only a value that is not a + * string gives an empty string. * * @param {string} value - The IBAN to be formatted. * @returns {string} The IBAN uppercased and grouped in blocks of 4 characters, or an empty - * string when `value` is not a string written in the print format. + * string when `value` is not a string. * * @example * ```typescript @@ -28,7 +27,7 @@ import { GROUP_SIZE } from "./constants"; * formatIban("br1500000000000010932840814p2"); // "BR15 0000 0000 0000 1093 2840 814P 2" * formatIban("BR15"); // "BR15" * formatIban("BR1500000000000010932840814P2EXTRA"); // "BR15 0000 0000 0000 1093 2840 814P 2" - * formatIban("BR1500000000000010932840814P-2"); // "" (hyphens are not part of an IBAN) + * formatIban("BR15 0000-0000.0000/1093 2840 814P-2"); // "BR15 0000 0000 0000 1093 2840 814P 2" * ``` * * @see Official: https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf @@ -39,11 +38,7 @@ import { GROUP_SIZE } from "./constants"; export const formatIban = (value: string): string => { if (typeof value !== "string") return ""; - const printed = value.trim(); - - if (!IBAN_FORMAT_REGEX.test(printed)) return ""; - - const sanitized = sanitizeToAlphanumeric(printed).slice(0, BR_IBAN_LENGTH); + const sanitized = sanitizeToAlphanumeric(value).slice(0, BR_IBAN_LENGTH); let formatted = ""; diff --git a/src/format-legal-nature/format-legal-nature.test.ts b/src/format-legal-nature/format-legal-nature.test.ts index d13ed4e2..929c8680 100644 --- a/src/format-legal-nature/format-legal-nature.test.ts +++ b/src/format-legal-nature/format-legal-nature.test.ts @@ -6,7 +6,7 @@ import { } from "../_internals/test/properties"; import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { parseLegalNature } from "../parse-legal-nature/parse-legal-nature"; -import { formatLegalNature } from "./format-legal-nature"; +import { type FormatLegalNatureOptions, formatLegalNature } from "./format-legal-nature"; describe("formatLegalNature", () => { it("should format legal nature values", () => { @@ -17,6 +17,19 @@ describe("formatLegalNature", () => { expect(formatLegalNature("2062")).toBe("206-2"); }); + it("should format a number and read only the digits of a masked value, like formatCpf", () => { + expect(formatLegalNature(2062)).toBe("206-2"); + expect(formatLegalNature("206-2")).toBe("206-2"); + expect(formatLegalNature("abc2062")).toBe("206-2"); + }); + + it("should left pad with zeros to 4 digits when pad is true", () => { + expect(formatLegalNature("62", { pad: true })).toBe("006-2"); + expect(formatLegalNature(62, { pad: true })).toBe("006-2"); + expect(formatLegalNature("2062", { pad: true })).toBe("206-2"); + expect(formatLegalNature("62", { pad: false })).toBe("62"); + }); + it("should return an empty string for null or undefined", () => { // @ts-expect-error: intentionally invalid input expect(formatLegalNature(null)).toBe(""); @@ -44,6 +57,9 @@ describe("formatLegalNature", () => { describe("formatLegalNature types", () => { test("should take a string or number value and return a string", () => { expectTypeOf(formatLegalNature).parameter(0).toEqualTypeOf(); + expectTypeOf(formatLegalNature) + .parameter(1) + .toEqualTypeOf(); expectTypeOf(formatLegalNature).returns.toEqualTypeOf(); }); }); diff --git a/src/format-legal-nature/format-legal-nature.ts b/src/format-legal-nature/format-legal-nature.ts index 2dc782ae..fd072811 100644 --- a/src/format-legal-nature/format-legal-nature.ts +++ b/src/format-legal-nature/format-legal-nature.ts @@ -2,15 +2,30 @@ import { format } from "../_internals/format/format"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +/** Options of `formatLegalNature`. */ +export type FormatLegalNatureOptions = { + /** Whether to left pad the value with zeros up to the 4 digits of a complete code (default: `false`). */ + pad?: boolean; +}; + /** * Formats a Brazilian legal nature (natureza jurídica) code. * + * Like every formatter of this package, the value is read for its digits and masked as far as + * they go (`"206"` stays `"206"`, `"2062"` becomes `"206-2"`); with `pad: true` it is first left + * padded with zeros to the 4 digits of a complete code. Use `isValidLegalNature` to check a code. + * * @param {string|number} value - The legal nature code to be formatted. + * @param {FormatLegalNatureOptions} [options] - Optional formatting options. + * @param {boolean} [options.pad] - Whether to pad the value with leading zeros. Defaults to `false`. * @returns {string} The formatted code, or an empty string when there is nothing to format. * * @example * ```typescript * formatLegalNature("2062"); // "206-2" + * formatLegalNature(2062); // "206-2" + * formatLegalNature("206"); // "206" (partial values are masked as far as they go) + * formatLegalNature("62", { pad: true }); // "006-2" * ``` * * The CONCLA table page sits behind a bot filter and answers HTTP 403 to every non-browser @@ -20,10 +35,14 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * @see Official: https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021 * @see Official: https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf */ -export const formatLegalNature = (value: string | number): string => +export const formatLegalNature = ( + value: string | number, + options?: FormatLegalNatureOptions, +): string => isNullish(value) ? "" : format({ + pad: options?.pad, value: sanitizeToDigits(value), pattern: "000-0", }); diff --git a/src/format-ncm/format-ncm.test.ts b/src/format-ncm/format-ncm.test.ts index 67e3b3b1..47fcbf02 100644 --- a/src/format-ncm/format-ncm.test.ts +++ b/src/format-ncm/format-ncm.test.ts @@ -73,14 +73,22 @@ describe("formatNcm", () => { expect(formatNcm()).toBe(""); }); - it("should return an empty string for a value that is not digits and mask characters", () => { - expect(formatNcm("abc8471")).toBe(""); + it("should return an empty string for null and undefined even under pad, instead of a zero-filled code", () => { + // @ts-expect-error not a string or number + expect(formatNcm(null, { pad: true })).toBe(""); + // @ts-expect-error not a string or number + expect(formatNcm(undefined, { pad: true })).toBe(""); + }); + + it("should read only the digits of a value with other characters, like formatCpf", () => { + expect(formatNcm("abc8471")).toBe("8471"); + expect(formatNcm("8471.30-12")).toBe("8471.30.12"); }); - it("should return an empty string for a number that is not a non-negative safe integer", () => { - expect(formatNcm(-84_713_012)).toBe(""); - expect(formatNcm(8_471_301.2)).toBe(""); - expect(formatNcm(2 ** 53)).toBe(""); + it("should read a signed or fractional number as the string of its digits, like formatCpf", () => { + expect(formatNcm(-84_713_012)).toBe("8471.30.12"); + expect(formatNcm(8_471_301.2)).toBe("8471.30.12"); + expect(formatNcm(2 ** 53)).toBe("9007.19.92"); }); it("should return an empty string for a null-prototype object", () => { diff --git a/src/format-ncm/format-ncm.ts b/src/format-ncm/format-ncm.ts index 3b7c2686..c78ccbd0 100644 --- a/src/format-ncm/format-ncm.ts +++ b/src/format-ncm/format-ncm.ts @@ -1,14 +1,7 @@ import { format } from "../_internals/format/format"; -import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -/** - * Shape a value may be written in while an NCM code is being typed: digits and the mask - * characters of the `NNNN.NN.NN` presentation, nothing else. Wider than the complete-code - * shape `isValidNcm` demands, because this formatter masks progressively. - */ -const NCM_MASK_REGEX = /^[\d\s.\-/]*$/; - /** Options of `formatNcm`. */ export type FormatNcmOptions = { /** Whether to left pad the value with zeros up to the 8 digits of a complete NCM code (default: `false`). */ @@ -29,10 +22,11 @@ export type FormatNcmOptions = { * `pad: true`, so `formatNcm(8471)` gives `"8471"` and `formatNcm(8471, { pad: true })` gives * `"0000.84.71"`. * - * A string is only formatted when it holds nothing but digits and the mask characters; - * anything else (`"abc8471"`) gives `""` instead of having its digits picked out. A number is - * only formatted when it is a non-negative safe integer, since a sign, a decimal point or a - * rounded magnitude would otherwise be read as a code the caller never wrote. + * Like every formatter of this package, the value is read for its digits and masked as far as + * they go: characters outside the mask are dropped (`formatNcm("abc8471")` gives + * `"8471"`) and a number is read as the string of its digits, sign and decimal point + * included (`formatNcm(-84713012)` gives `"8471.30.12"`). This is the input-mask contract of + * `formatCpf`; use `isValidNcm` to check a code. * * @param {string|number} value - The NCM code to be formatted. * @param {FormatNcmOptions} [options] - Optional formatting options. @@ -47,22 +41,18 @@ export type FormatNcmOptions = { * formatNcm("8471"); // "8471" (partial values are masked as far as they go) * formatNcm("847130"); // "8471.30" * formatNcm("8471", { pad: true }); // "0000.84.71" (padded to 8 digits first) - * formatNcm("abc8471"); // "" (not a documented form) - * formatNcm(-84713012); // "" (not a non-negative safe integer) + * formatNcm("abc8471"); // "8471" (only the digits are read) + * formatNcm(-84713012); // "8471.30.12" * ``` * * @see Official: https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json */ export const formatNcm = (value: string | number, options?: FormatNcmOptions): string => { - if (!isLookupCode(value)) return ""; - - const code = String(value); - - if (!NCM_MASK_REGEX.test(code)) return ""; + if (isNullish(value)) return ""; return format({ pad: options?.pad, - value: sanitizeToDigits(code), + value: sanitizeToDigits(value), pattern: "0000.00.00", }); }; diff --git a/src/format-nfe-key/format-nfe-key.test.ts b/src/format-nfe-key/format-nfe-key.test.ts index 75ad072e..c5b34aaf 100644 --- a/src/format-nfe-key/format-nfe-key.test.ts +++ b/src/format-nfe-key/format-nfe-key.test.ts @@ -47,9 +47,9 @@ describe("formatNfeKey", () => { // @ts-expect-error: intentionally invalid input expect(formatNfeKey(true)).toBe(""); // @ts-expect-error: intentionally invalid input - expect(formatNfeKey(-11)).toBe(""); + expect(formatNfeKey(-11)).toBe("11"); // @ts-expect-error: intentionally invalid input - expect(formatNfeKey(1.1)).toBe(""); + expect(formatNfeKey(1.1)).toBe("11"); }); test("should return an empty string for an object with a null prototype, which has no toString", () => { diff --git a/src/format-nfe-key/format-nfe-key.ts b/src/format-nfe-key/format-nfe-key.ts index dba794d9..fb76dbf5 100644 --- a/src/format-nfe-key/format-nfe-key.ts +++ b/src/format-nfe-key/format-nfe-key.ts @@ -1,5 +1,5 @@ import { format } from "../_internals/format/format"; -import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { PATTERN } from "./constants"; @@ -9,9 +9,10 @@ import { PATTERN } from "./constants"; * NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the * MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. * - * Anything that is not a string is only read when it is a non-negative safe integer, so a value - * with no usable digit representation (a negative or fractional number, an object, a value with - * a null prototype) gives `""` instead of throwing. + * Like every formatter of this package, the value is read for its digits and grouped as far as + * they go, so a masked or partial key still being typed is grouped progressively and anything + * without a digit (an object, `true`, an object with a null prototype) gives `""` instead of + * throwing. Use `isValidNfeKey` to check a key. * * @param {string} value - The access key value to be formatted. * @returns {string} The formatted access key, e.g. "3520 0612 3456 ...". @@ -26,4 +27,4 @@ import { PATTERN } from "./constants"; * Manual de Orientação do Contribuinte (MOC) NF-e, "chave de acesso". */ export const formatNfeKey = (value: string): string => - isLookupCode(value) ? format({ value: sanitizeToDigits(value), pattern: PATTERN }) : ""; + isNullish(value) ? "" : format({ value: sanitizeToDigits(value), pattern: PATTERN }); diff --git a/src/index.test.ts b/src/index.test.ts index 265589b3..1ecaba62 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -21,6 +21,7 @@ import { type FormatCepOptions, type FormatCertidaoOptions, type FormatCnaeOptions, + type FormatLegalNatureOptions, type FormatCnhOptions, type FormatCnoOptions, type FormatCnpjOptions, @@ -275,6 +276,7 @@ describe("Public API", () => { FormatCepOptions: FormatCepOptions; FormatCertidaoOptions: FormatCertidaoOptions; FormatCnaeOptions: FormatCnaeOptions; + FormatLegalNatureOptions: FormatLegalNatureOptions; FormatCnhOptions: FormatCnhOptions; FormatCnoOptions: FormatCnoOptions; FormatCnpjOptions: FormatCnpjOptions; diff --git a/src/index.ts b/src/index.ts index 641fe43a..3e63f722 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,7 +28,10 @@ export { type FormatCnsOptions, formatCns } from "./format-cns/format-cns"; export { type FormatCpfOptions, formatCpf } from "./format-cpf/format-cpf"; export { type FormatCurrencyOptions, formatCurrency } from "./format-currency/format-currency"; export { formatIban } from "./format-iban/format-iban"; -export { formatLegalNature } from "./format-legal-nature/format-legal-nature"; +export { + type FormatLegalNatureOptions, + formatLegalNature, +} from "./format-legal-nature/format-legal-nature"; export { formatLicensePlate } from "./format-license-plate/format-license-plate"; export { type FormatNcmOptions, formatNcm } from "./format-ncm/format-ncm"; export { formatNfeKey } from "./format-nfe-key/format-nfe-key"; From 5ef48fd19be264d756257984d0dd6ccdb776086a Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 10:04:56 -0300 Subject: [PATCH 56/75] fix(lookups): read a code the same way whether it comes as a string or a number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `getCbo(10205)` found the code but `getCbo("10205")` did not, because only a number was zero padded to the width of the table; `getCnae`, `isValidCbo`, `isValidCnae` and `isValidNcm` had the same split. A value of bare digits is now padded the same way in both types (CBO to 6, CNAE to 7, NCM to 8), a masked value is read as written, and a single digit CST is read as the 3 digit ICMS form, so `isValidCst(0)` and `isValidCst("000")` agree. CFOP, natureza jurídica and CSOSN carry a significant first digit and are documented as never padded. An unknown `tax` in `isValidCst` falls back to the default like every other scalar option. `Cbo.title` is now `description`, the field every other record uses, and `getCnae(...).code` is the bare 7 digits, with `formatCnae` adding the mask, as the other getters do. --- docs/llms-full.txt | 39 ++++++++--- docs/llms.txt | 4 +- docs/pt-br/utilities.md | 39 ++++++++--- docs/utilities.md | 39 ++++++++--- .../pad-lookup-code/pad-lookup-code.test.ts | 46 ++++++++++++ .../pad-lookup-code/pad-lookup-code.ts | 34 +++++++++ src/get-cbo/get-cbo.test.ts | 45 ++++++++---- src/get-cbo/get-cbo.ts | 23 +++--- src/get-cfop/get-cfop.ts | 4 ++ src/get-cnae/get-cnae.test.ts | 37 +++++++--- src/get-cnae/get-cnae.ts | 23 ++++-- src/get-legal-nature/get-legal-nature.ts | 4 ++ src/is-valid-cbo/is-valid-cbo.test.ts | 11 ++- src/is-valid-cbo/is-valid-cbo.ts | 7 +- src/is-valid-cfop/is-valid-cfop.ts | 4 ++ src/is-valid-cnae/is-valid-cnae.test.ts | 11 ++- src/is-valid-cnae/is-valid-cnae.ts | 7 +- src/is-valid-csosn/is-valid-csosn.ts | 4 ++ src/is-valid-cst/is-valid-cst.test.ts | 70 +++++++++++++++++-- src/is-valid-cst/is-valid-cst.ts | 55 +++++++++++---- src/is-valid-ncm/is-valid-ncm.test.ts | 22 +++--- src/is-valid-ncm/is-valid-ncm.ts | 14 ++-- 22 files changed, 429 insertions(+), 113 deletions(-) create mode 100644 src/_internals/pad-lookup-code/pad-lookup-code.test.ts create mode 100644 src/_internals/pad-lookup-code/pad-lookup-code.ts diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 6584c764..33fbbdc8 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -2087,12 +2087,13 @@ import { isValidVin } from '@brazilian-utils/brazilian-utils'; isValidVin('1HGCM82633A004352'); // true isValidVin('1m8gdm9axkp042788'); // true (check digit X, lowercase) isValidVin('1HGCM82633A004353'); // false (bad check digit) +isValidVin('00000000000000000'); // false (every character the same, though the check digit matches) isValidVin('1HGCM8263IA004352'); // false (contains the excluded letter I) ``` ### isValidCbo -Check if a CBO (Classificação Brasileira de Ocupações) code exists in the MTE occupation table. Accepts the code with or without the hyphen mask, or as a number. A string is only read as a code when it is written in one of those forms (the 6 digits, or the `NNNN-NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +Check if a CBO (Classificação Brasileira de Ocupações) code exists in the MTE occupation table. Accepts the code with or without the hyphen mask, or as a number. A string is only read as a code when it is written in one of those forms (the 6 digits, or the `NNNN-NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. A CBO code is always 6 digits and its leading zeros are part of it, so a value written as bare digits is left padded with zeros to 6 whether it comes as a string or as a number, exactly like `getBankByCode` pads a bank code: `10205`, `'10205'` and `'010205'` are the same code. A masked value already carries its separators and is read as written. ```javascript import { isValidCbo } from '@brazilian-utils/brazilian-utils'; @@ -2100,6 +2101,8 @@ import { isValidCbo } from '@brazilian-utils/brazilian-utils'; isValidCbo('2124-05'); // true isValidCbo('212405'); // true isValidCbo(212405); // true +isValidCbo(10205); // true (padded to 6 digits, so this is '010205') +isValidCbo('10205'); // true (padded the same way a number is) isValidCbo('000000'); // false isValidCbo('2124abc05'); // false (not a documented form) isValidCbo(-212405); // false (not a non-negative safe integer) @@ -2109,12 +2112,14 @@ The occupation titles come from the [official CBO 2002 occupation table publishe ### getCbo -Look a CBO (Classificação Brasileira de Ocupações) code up and get its official occupation title. A `number` keeps its implied leading zeros: `getCbo(10205)` is read as `010205`. Same input rules as `isValidCbo`: a string has to be written as the 6 digits or with the `NNNN-NN` mask, and a number has to be a non-negative safe integer. +Look a CBO (Classificação Brasileira de Ocupações) code up and get its official occupation title, in the `{ code, description }` record every lookup of this library returns. A value written as bare digits keeps its implied leading zeros, as a string as much as a number: `getCbo(10205)` and `getCbo('10205')` are both read as `010205`. Same input rules as `isValidCbo`: a string has to be written as the 6 digits or with the `NNNN-NN` mask, and a number has to be a non-negative safe integer. ```javascript import { getCbo } from '@brazilian-utils/brazilian-utils'; -getCbo('2124-05'); // { code: '212405', title: 'Analista de desenvolvimento de sistemas' } +getCbo('2124-05'); // { code: '212405', description: 'Analista de desenvolvimento de sistemas' } +getCbo(10205); // { code: '010205', description: 'Oficial da aeronáutica' } (padded to 6 digits) +getCbo('10205'); // { code: '010205', description: 'Oficial da aeronáutica' } (padded the same way) getCbo('000000'); // null getCbo('2124abc05'); // null (not a documented form) ``` @@ -2123,13 +2128,15 @@ The occupation titles come from the [official CBO 2002 occupation table publishe ### isValidCnae -Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the [CNAE-Subclasses 2.3 table published by IBGE](https://concla.ibge.gov.br/busca-online-cnae.html), the current subclass revision of CNAE 2.0. Accepts the code with or without the `NNNN-N/NN` mask, or as a number. A string is only read as a code when it is written in one of those forms (the 7 digits, or the mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the [CNAE-Subclasses 2.3 table published by IBGE](https://concla.ibge.gov.br/busca-online-cnae.html), the current subclass revision of CNAE 2.0. Accepts the code with or without the `NNNN-N/NN` mask, or as a number. A string is only read as a code when it is written in one of those forms (the 7 digits, or the mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. A CNAE subclass code is always 7 digits and its leading zeros are part of it, so a value written as bare digits is left padded with zeros to 7 whether it comes as a string or as a number: `111301`, `'111301'` and `'0111301'` are the same code. A masked value already carries its separators and is read as written. ```javascript import { isValidCnae } from '@brazilian-utils/brazilian-utils'; isValidCnae('6201-5/01'); // true isValidCnae('6201501'); // true +isValidCnae(111301); // true (padded to 7 digits, so this is '0111301') +isValidCnae('111301'); // true (padded the same way a number is) isValidCnae('0000000'); // false isValidCnae('0111abc301'); // false (not a documented form) isValidCnae(-111301); // false (not a non-negative safe integer) @@ -2153,25 +2160,30 @@ formatCnae(-6201501); // 6201-5/01 ### getCnae -Look a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up and get its formatted code and official description. A `number` keeps its implied leading zeros: `getCnae(111301)` is read as `0111301`. Same input rules as `isValidCnae`: a string has to be written as the 7 digits or with the `NNNN-N/NN` mask, and a number has to be a non-negative safe integer. +Look a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up and get its code and official description. `code` comes back as the 7 bare digits, like every other lookup of this library; pass it to `formatCnae` for the `NNNN-N/NN` form. A value written as bare digits keeps its implied leading zeros, as a string as much as a number: `getCnae(111301)` and `getCnae('111301')` are both read as `0111301`. Same input rules as `isValidCnae`: a string has to be written as the 7 digits or with the `NNNN-N/NN` mask, and a number has to be a non-negative safe integer. ```javascript -import { getCnae } from '@brazilian-utils/brazilian-utils'; +import { formatCnae, getCnae } from '@brazilian-utils/brazilian-utils'; -getCnae('6201501'); // { code: '6201-5/01', description: 'DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA' } +getCnae('6201-5/01'); // { code: '6201501', description: 'DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA' } +getCnae(111301); // { code: '0111301', description: 'CULTIVO DE ARROZ' } (padded to 7 digits) +getCnae('111301'); // { code: '0111301', description: 'CULTIVO DE ARROZ' } (padded the same way) getCnae('0000000'); // null getCnae('0111abc301'); // null (not a documented form) +formatCnae(getCnae('6201501')?.code); // 6201-5/01 (the mask is the formatter's job) ``` ### isValidNcm -Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. Accepts the code with or without the dotted mask, or as a number. A string is only read as a code when it is written in one of those forms (the 8 digits, or the `NNNN.NN.NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. A bare number cannot carry a leading zero, so a code starting with `0` has to be passed as a string: `isValidNcm(1012100)` is `false` while `isValidNcm('01012100')` is `true`. +Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. Accepts the code with or without the dotted mask, or as a number. A string is only read as a code when it is written in one of those forms (the 8 digits, or the `NNNN.NN.NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. An NCM code is always 8 digits and its leading zeros are part of it, so a value written as bare digits is left padded with zeros to 8 whether it comes as a string or as a number: `1012100`, `'1012100'` and `'01012100'` are the same code. A masked value already carries its separators and is read as written. ```javascript import { isValidNcm } from '@brazilian-utils/brazilian-utils'; isValidNcm('8471.30.12'); // true isValidNcm('84713012'); // true +isValidNcm(1012100); // true (padded to 8 digits, so this is '01012100') +isValidNcm('1012100'); // true (padded the same way a number is) isValidNcm('00000000'); // false isValidNcm('abc01012100'); // false (not a documented form) isValidNcm(-84713012); // false (not a non-negative safe integer) @@ -2196,7 +2208,7 @@ formatNcm(-84713012); // 8471.30.12 Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table. The table is the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), the text in force (current wording given by Ajuste SINIEF 03/24, last amended by [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25)), not the frozen 2001 text of Ajuste SINIEF 07/01. Only operable codes count: the group and subgroup headings of the official nomenclature, the codes ending in `00` and `50` (1000, 1100, 1150, 5350, ...), are section titles rather than codes a document can carry, so they are rejected. -A string is only read as a code when it is written in one of the documented forms (the 4 digits, or the `N.NNN` form the annex prints, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +A string is only read as a code when it is written in one of the documented forms (the 4 digits, or the `N.NNN` form the annex prints, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. No CFOP code starts with a zero, its first digit is the operation group (1 to 7), so nothing is ever padded here: a number and the string of the same digits are read identically. ```javascript import { isValidCfop } from '@brazilian-utils/brazilian-utils'; @@ -2235,21 +2247,26 @@ Check if a CST (Código de Situação Tributária) code is valid for a given tax | `pis` | 2 digits | `01`-`09`, `49`, `50`-`56`, `60`-`67`, `70`-`75`, `98`, `99` | | `cofins` | 2 digits | same table as `pis` | -`options.tax` (part of `IsValidCstOptions`) is optional: omit it to accept a code that exists in any one of the four tables above. +`options.tax` (part of `IsValidCstOptions`) is optional: omit it to accept a code that exists in any one of the four tables above. A `tax` outside those four values falls back to that same default at runtime, the way every other scalar option of this library treats a value it does not know. The ICMS Tabela B is the one in force: the [consolidated Anexo I of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), whose current wording came from [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (effective 01.12.23) and which [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) amended by striking items 12, 13, 52, 72 and 74 (effects from 09.07.24) before they ever took effect: 39/23 had deferred their effect to 1º de outubro de 2024, so the revocation reached them first and those codes were never in force. `02`, `15`, `53` and `61` are its monofasia de combustíveis codes. A string is only read as a code when it is written in one of the documented forms (the 2 digits of a Tabela B code, or the 3 digits of the ICMS form with an optional single separator after the origin digit, plus optional surrounding whitespace), and a number only when it is a non-negative safe integer. The origin digit is the only boundary a printed CST has, so `'0 10'` and `'1-10'` are read while `'0-0'`, `'11-0'` and `'00-'` are not. +A single digit is narrower than either documented form, so it is left padded with zeros to the 3 digits of the ICMS form, whether it comes as a string or as a number: `0`, `'0'` and `'000'` are all the ICMS code `000`. A 2 digit value is already a documented form, a Tabela B code, and is read as written, so a Tabela B code keeps its own two digits: `'07'`, not `7`, which is the ICMS code `007`. + ```javascript import { isValidCst } from '@brazilian-utils/brazilian-utils'; isValidCst('000', { tax: 'icms' }); // true +isValidCst(0, { tax: 'icms' }); // true (a single digit is padded to the 3 digit form, '000') +isValidCst('0', { tax: 'icms' }); // true (padded the same way a number is) isValidCst('110', { tax: 'icms' }); // true isValidCst('002', { tax: 'icms' }); // true (monofasia de combustíveis) isValidCst('06', { tax: 'pis' }); // true isValidCst('99', { tax: 'ipi' }); // true isValidCst('110'); // true (found in the icms table, tax omitted) +isValidCst('000', { tax: 'nope' }); // true (an unknown tax falls back to every table) isValidCst('999'); // false (not in any table) isValidCst('abc110'); // false (not a documented form) isValidCst(-110); // false (not a non-negative safe integer) @@ -2259,7 +2276,7 @@ isValidCst(-110); // false (not a non-negative safe integer) Check if a CSOSN (Código de Situação da Operação no Simples Nacional) code is one of the 10 codes of the [consolidated Anexo III-A of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), the table Ajuste SINIEF 03/2010 instituted: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` or `900`. -A string is only read as a code when it is written as the bare 3 digits with optional surrounding whitespace: a CSOSN has no printed grouping (the NF-e carries the origin digit in its own `orig` field), so `'1-01'` is rejected; a number is read only when it is a non-negative safe integer. +A string is only read as a code when it is written as the bare 3 digits with optional surrounding whitespace: a CSOSN has no printed grouping (the NF-e carries the origin digit in its own `orig` field), so `'1-01'` is rejected; a number is read only when it is a non-negative safe integer. No CSOSN code starts with a zero, the table runs from `101` to `900`, so nothing is ever padded here: a number and the string of the same digits are read identically. ```javascript import { isValidCsosn } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/llms.txt b/docs/llms.txt index a0acf1e0..01614852 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -149,8 +149,8 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [getMunicipality](https://brazilian-utils.com.br/utilities.md#getmunicipality): Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. - [getMunicipalities](https://brazilian-utils.com.br/utilities.md#getmunicipalities): Get Brazilian municipalities published by the IBGE. - [getMunicipalityByCode](https://brazilian-utils.com.br/utilities.md#getmunicipalitybycode): Look up a Brazilian municipality by its 7-digit IBGE code. -- [getCbo](https://brazilian-utils.com.br/utilities.md#getcbo): Look a CBO (Classificação Brasileira de Ocupações) code up and get its official occupation title. -- [getCnae](https://brazilian-utils.com.br/utilities.md#getcnae): Look a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up and get its formatted code and official description. +- [getCbo](https://brazilian-utils.com.br/utilities.md#getcbo): Look a CBO (Classificação Brasileira de Ocupações) code up and get its official occupation title, in the `{ code, description }` record every lookup of this library returns. +- [getCnae](https://brazilian-utils.com.br/utilities.md#getcnae): Look a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up and get its code and official description. - [getCfop](https://brazilian-utils.com.br/utilities.md#getcfop): Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description, as the consolidated Anexo II of Convênio SINIEF s/nº 1970 words it, in the text in force, last amended by Ajuste SINIEF 39/25. ## Other utilities diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 938e2d74..d2aad681 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -1845,12 +1845,13 @@ import { isValidVin } from '@brazilian-utils/brazilian-utils'; isValidVin('1HGCM82633A004352'); // true isValidVin('1m8gdm9axkp042788'); // true (dígito verificador X, minúsculo) isValidVin('1HGCM82633A004353'); // false (dígito verificador inválido) +isValidVin('00000000000000000'); // false (todos os caracteres iguais, ainda que o dígito feche) isValidVin('1HGCM8263IA004352'); // false (contém a letra excluída I) ``` ## isValidCbo -Valida se um código CBO (Classificação Brasileira de Ocupações) existe na tabela de ocupações do MTE. Aceita o código com ou sem a máscara de hífen, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 6 dígitos, ou a máscara `NNNN-NN`, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. +Valida se um código CBO (Classificação Brasileira de Ocupações) existe na tabela de ocupações do MTE. Aceita o código com ou sem a máscara de hífen, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 6 dígitos, ou a máscara `NNNN-NN`, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. Um código CBO sempre tem 6 dígitos e os zeros à esquerda fazem parte dele, então um valor escrito apenas com dígitos é completado com zeros à esquerda até 6, seja ele string ou número, exatamente como `getBankByCode` completa um código de banco: `10205`, `'10205'` e `'010205'` são o mesmo código. Um valor mascarado já carrega os seus separadores e é lido como foi escrito. ```javascript import { isValidCbo } from '@brazilian-utils/brazilian-utils'; @@ -1858,6 +1859,8 @@ import { isValidCbo } from '@brazilian-utils/brazilian-utils'; isValidCbo('2124-05'); // true isValidCbo('212405'); // true isValidCbo(212405); // true +isValidCbo(10205); // true (completado para 6 dígitos, ou seja, '010205') +isValidCbo('10205'); // true (completado do mesmo jeito que um número) isValidCbo('000000'); // false isValidCbo('2124abc05'); // false (não é uma forma documentada) isValidCbo(-212405); // false (não é um inteiro seguro não negativo) @@ -1867,12 +1870,14 @@ Os títulos das ocupações vêm da [tabela oficial de ocupações da CBO 2002 p ## getCbo -Consulta um código CBO (Classificação Brasileira de Ocupações) e retorna o título oficial da ocupação. Um `number` mantém os zeros à esquerda implícitos: `getCbo(10205)` é lido como `010205`. Valem as mesmas regras de entrada de `isValidCbo`: uma string precisa estar escrita com os 6 dígitos ou com a máscara `NNNN-NN`, e um número precisa ser um inteiro seguro não negativo. +Consulta um código CBO (Classificação Brasileira de Ocupações) e retorna o título oficial da ocupação, no registro `{ code, description }` que toda consulta desta biblioteca devolve. Um valor escrito apenas com dígitos mantém os zeros à esquerda implícitos, tanto como string quanto como número: `getCbo(10205)` e `getCbo('10205')` são lidos como `010205`. Valem as mesmas regras de entrada de `isValidCbo`: uma string precisa estar escrita com os 6 dígitos ou com a máscara `NNNN-NN`, e um número precisa ser um inteiro seguro não negativo. ```javascript import { getCbo } from '@brazilian-utils/brazilian-utils'; -getCbo('2124-05'); // { code: '212405', title: 'Analista de desenvolvimento de sistemas' } +getCbo('2124-05'); // { code: '212405', description: 'Analista de desenvolvimento de sistemas' } +getCbo(10205); // { code: '010205', description: 'Oficial da aeronáutica' } (completado para 6 dígitos) +getCbo('10205'); // { code: '010205', description: 'Oficial da aeronáutica' } (completado do mesmo jeito) getCbo('000000'); // null getCbo('2124abc05'); // null (não é uma forma documentada) ``` @@ -1881,13 +1886,15 @@ Os títulos das ocupações vêm da [tabela oficial de ocupações da CBO 2002 p ## isValidCnae -Valida se um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas) existe na [tabela CNAE-Subclasses 2.3 publicada pelo IBGE](https://concla.ibge.gov.br/busca-online-cnae.html), a revisão de subclasses atual da CNAE 2.0. Aceita o código com ou sem a máscara `NNNN-N/NN`, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 7 dígitos, ou a máscara, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. +Valida se um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas) existe na [tabela CNAE-Subclasses 2.3 publicada pelo IBGE](https://concla.ibge.gov.br/busca-online-cnae.html), a revisão de subclasses atual da CNAE 2.0. Aceita o código com ou sem a máscara `NNNN-N/NN`, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 7 dígitos, ou a máscara, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. Um código de subclasse CNAE sempre tem 7 dígitos e os zeros à esquerda fazem parte dele, então um valor escrito apenas com dígitos é completado com zeros à esquerda até 7, seja ele string ou número: `111301`, `'111301'` e `'0111301'` são o mesmo código. Um valor mascarado já carrega os seus separadores e é lido como foi escrito. ```javascript import { isValidCnae } from '@brazilian-utils/brazilian-utils'; isValidCnae('6201-5/01'); // true isValidCnae('6201501'); // true +isValidCnae(111301); // true (completado para 7 dígitos, ou seja, '0111301') +isValidCnae('111301'); // true (completado do mesmo jeito que um número) isValidCnae('0000000'); // false isValidCnae('0111abc301'); // false (não é uma forma documentada) isValidCnae(-111301); // false (não é um inteiro seguro não negativo) @@ -1911,25 +1918,30 @@ formatCnae(-6201501); // 6201-5/01 ## getCnae -Busca um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas) e retorna seu código formatado e a descrição oficial. Um `number` mantém os zeros à esquerda implícitos: `getCnae(111301)` é lido como `0111301`. Valem as mesmas regras de entrada de `isValidCnae`: uma string precisa estar escrita com os 7 dígitos ou com a máscara `NNNN-N/NN`, e um número precisa ser um inteiro seguro não negativo. +Busca um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas) e retorna seu código e a descrição oficial. O `code` volta com os 7 dígitos crus, como em toda consulta desta biblioteca; passe-o para `formatCnae` para obter a forma `NNNN-N/NN`. Um valor escrito apenas com dígitos mantém os zeros à esquerda implícitos, tanto como string quanto como número: `getCnae(111301)` e `getCnae('111301')` são lidos como `0111301`. Valem as mesmas regras de entrada de `isValidCnae`: uma string precisa estar escrita com os 7 dígitos ou com a máscara `NNNN-N/NN`, e um número precisa ser um inteiro seguro não negativo. ```javascript -import { getCnae } from '@brazilian-utils/brazilian-utils'; +import { formatCnae, getCnae } from '@brazilian-utils/brazilian-utils'; -getCnae('6201501'); // { code: '6201-5/01', description: 'DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA' } +getCnae('6201-5/01'); // { code: '6201501', description: 'DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA' } +getCnae(111301); // { code: '0111301', description: 'CULTIVO DE ARROZ' } (completado para 7 dígitos) +getCnae('111301'); // { code: '0111301', description: 'CULTIVO DE ARROZ' } (completado do mesmo jeito) getCnae('0000000'); // null getCnae('0111abc301'); // null (não é uma forma documentada) +formatCnae(getCnae('6201501')?.code); // 6201-5/01 (aplicar a máscara é trabalho do formatador) ``` ## isValidNcm -Valida se um código NCM (Nomenclatura Comum do Mercosul) existe na tabela vigente publicada pelo Siscomex/MDIC. Aceita o código com ou sem a máscara de pontos, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 8 dígitos, ou a máscara `NNNN.NN.NN`, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. Um número não carrega zero à esquerda, então um código iniciado por `0` precisa ser informado como string: `isValidNcm(1012100)` é `false`, enquanto `isValidNcm('01012100')` é `true`. +Valida se um código NCM (Nomenclatura Comum do Mercosul) existe na tabela vigente publicada pelo Siscomex/MDIC. Aceita o código com ou sem a máscara de pontos, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 8 dígitos, ou a máscara `NNNN.NN.NN`, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. Um código NCM sempre tem 8 dígitos e os zeros à esquerda fazem parte dele, então um valor escrito apenas com dígitos é completado com zeros à esquerda até 8, seja ele string ou número: `1012100`, `'1012100'` e `'01012100'` são o mesmo código. Um valor mascarado já carrega os seus separadores e é lido como foi escrito. ```javascript import { isValidNcm } from '@brazilian-utils/brazilian-utils'; isValidNcm('8471.30.12'); // true isValidNcm('84713012'); // true +isValidNcm(1012100); // true (completado para 8 dígitos, ou seja, '01012100') +isValidNcm('1012100'); // true (completado do mesmo jeito que um número) isValidNcm('00000000'); // false isValidNcm('abc01012100'); // false (não é uma forma documentada) isValidNcm(-84713012); // false (não é um inteiro seguro não negativo) @@ -1954,7 +1966,7 @@ formatNcm(-84713012); // 8471.30.12 Valida se um código CFOP (Código Fiscal de Operações e Prestações) existe na tabela oficial. A tabela é o [Anexo II consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), o texto vigente (redação atual dada pelo Ajuste SINIEF 03/24, última alteração pelo [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25)), e não o texto congelado de 2001 do Ajuste SINIEF 07/01. Só os códigos operáveis contam: os títulos de grupo e subgrupo da nomenclatura oficial, os códigos terminados em `00` e `50` (1000, 1100, 1150, 5350, ...), são títulos de seção e não códigos que um documento pode carregar, então são rejeitados. -Uma string só é lida como código quando está escrita em uma das formas documentadas (os 4 dígitos, ou a forma `N.NNN` impressa no anexo, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. +Uma string só é lida como código quando está escrita em uma das formas documentadas (os 4 dígitos, ou a forma `N.NNN` impressa no anexo, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. Nenhum código CFOP começa com zero, o seu primeiro dígito é o grupo da operação (1 a 7), então aqui nada é completado: um número e a string dos mesmos dígitos são lidos de forma idêntica. ```javascript import { isValidCfop } from '@brazilian-utils/brazilian-utils'; @@ -1993,21 +2005,26 @@ Valida um código de CST (Código de Situação Tributária) para um tributo. In | `pis` | 2 dígitos | `01`-`09`, `49`, `50`-`56`, `60`-`67`, `70`-`75`, `98`, `99` | | `cofins` | 2 dígitos | mesma tabela do `pis` | -`options.tax` (parte de `IsValidCstOptions`) é opcional: omita-o para aceitar um código que exista em qualquer uma das quatro tabelas acima. +`options.tax` (parte de `IsValidCstOptions`) é opcional: omita-o para aceitar um código que exista em qualquer uma das quatro tabelas acima. Um `tax` fora desses quatro valores cai nesse mesmo padrão em tempo de execução, do jeito que toda outra opção escalar desta biblioteca trata um valor que não conhece. A Tabela B do ICMS é a vigente: o [Anexo I consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), cuja redação atual veio do [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (efeitos a partir de 01.12.23) e que o [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) alterou suprimindo os itens 12, 13, 52, 72 e 74 (efeitos a partir de 09.07.24) antes que eles chegassem a produzir efeitos: o 39/23 havia adiado a produção de efeitos deles para 1º de outubro de 2024, então a revogação os alcançou antes e esses códigos nunca estiveram em vigor. `02`, `15`, `53` e `61` são seus códigos de monofasia de combustíveis. Uma string só é lida como código quando está escrita em uma das formas documentadas (os 2 dígitos de um código da Tabela B, ou os 3 dígitos da forma do ICMS com um único separador opcional depois do dígito de origem, além de espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. O dígito de origem é a única fronteira que um CST impresso tem, então `'0 10'` e `'1-10'` são lidos, mas `'0-0'`, `'11-0'` e `'00-'` não. +Um único dígito é mais estreito que qualquer uma das formas documentadas, então ele é completado com zeros à esquerda até os 3 dígitos da forma do ICMS, seja ele string ou número: `0`, `'0'` e `'000'` são todos o código ICMS `000`. Um valor de 2 dígitos já é uma forma documentada, um código da Tabela B, e é lido como foi escrito, ou seja, um código da Tabela B mantém os seus dois dígitos: `'07'`, não `7`, que é o código ICMS `007`. + ```javascript import { isValidCst } from '@brazilian-utils/brazilian-utils'; isValidCst('000', { tax: 'icms' }); // true +isValidCst(0, { tax: 'icms' }); // true (um único dígito é completado até a forma de 3 dígitos, '000') +isValidCst('0', { tax: 'icms' }); // true (completado do mesmo jeito que um número) isValidCst('110', { tax: 'icms' }); // true isValidCst('002', { tax: 'icms' }); // true (monofasia de combustíveis) isValidCst('06', { tax: 'pis' }); // true isValidCst('99', { tax: 'ipi' }); // true isValidCst('110'); // true (encontrado na tabela icms, tax omitido) +isValidCst('000', { tax: 'nope' }); // true (um tax desconhecido cai em todas as tabelas) isValidCst('999'); // false (não existe em nenhuma tabela) isValidCst('abc110'); // false (não é uma forma documentada) isValidCst(-110); // false (não é um inteiro seguro não negativo) @@ -2017,7 +2034,7 @@ isValidCst(-110); // false (não é um inteiro seguro não negativo) Valida se um código de CSOSN (Código de Situação da Operação no Simples Nacional) é um dos 10 códigos do [Anexo III-A consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), a tabela instituída pelo Ajuste SINIEF 03/2010: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` ou `900`. -Uma string só é lida como código quando está escrita como os 3 dígitos puros, com espaços em branco opcionais no início e no fim: um CSOSN não tem agrupamento impresso (a NF-e leva o dígito de origem no seu próprio campo `orig`), então `'1-01'` é rejeitado; um número só é lido quando é um inteiro seguro não negativo. +Uma string só é lida como código quando está escrita como os 3 dígitos puros, com espaços em branco opcionais no início e no fim: um CSOSN não tem agrupamento impresso (a NF-e leva o dígito de origem no seu próprio campo `orig`), então `'1-01'` é rejeitado; um número só é lido quando é um inteiro seguro não negativo. Nenhum código CSOSN começa com zero, a tabela vai de `101` a `900`, então aqui nada é completado: um número e a string dos mesmos dígitos são lidos de forma idêntica. ```javascript import { isValidCsosn } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/utilities.md b/docs/utilities.md index 3e73102f..c7d941f5 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -1845,12 +1845,13 @@ import { isValidVin } from '@brazilian-utils/brazilian-utils'; isValidVin('1HGCM82633A004352'); // true isValidVin('1m8gdm9axkp042788'); // true (check digit X, lowercase) isValidVin('1HGCM82633A004353'); // false (bad check digit) +isValidVin('00000000000000000'); // false (every character the same, though the check digit matches) isValidVin('1HGCM8263IA004352'); // false (contains the excluded letter I) ``` ## isValidCbo -Check if a CBO (Classificação Brasileira de Ocupações) code exists in the MTE occupation table. Accepts the code with or without the hyphen mask, or as a number. A string is only read as a code when it is written in one of those forms (the 6 digits, or the `NNNN-NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +Check if a CBO (Classificação Brasileira de Ocupações) code exists in the MTE occupation table. Accepts the code with or without the hyphen mask, or as a number. A string is only read as a code when it is written in one of those forms (the 6 digits, or the `NNNN-NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. A CBO code is always 6 digits and its leading zeros are part of it, so a value written as bare digits is left padded with zeros to 6 whether it comes as a string or as a number, exactly like `getBankByCode` pads a bank code: `10205`, `'10205'` and `'010205'` are the same code. A masked value already carries its separators and is read as written. ```javascript import { isValidCbo } from '@brazilian-utils/brazilian-utils'; @@ -1858,6 +1859,8 @@ import { isValidCbo } from '@brazilian-utils/brazilian-utils'; isValidCbo('2124-05'); // true isValidCbo('212405'); // true isValidCbo(212405); // true +isValidCbo(10205); // true (padded to 6 digits, so this is '010205') +isValidCbo('10205'); // true (padded the same way a number is) isValidCbo('000000'); // false isValidCbo('2124abc05'); // false (not a documented form) isValidCbo(-212405); // false (not a non-negative safe integer) @@ -1867,12 +1870,14 @@ The occupation titles come from the [official CBO 2002 occupation table publishe ## getCbo -Look a CBO (Classificação Brasileira de Ocupações) code up and get its official occupation title. A `number` keeps its implied leading zeros: `getCbo(10205)` is read as `010205`. Same input rules as `isValidCbo`: a string has to be written as the 6 digits or with the `NNNN-NN` mask, and a number has to be a non-negative safe integer. +Look a CBO (Classificação Brasileira de Ocupações) code up and get its official occupation title, in the `{ code, description }` record every lookup of this library returns. A value written as bare digits keeps its implied leading zeros, as a string as much as a number: `getCbo(10205)` and `getCbo('10205')` are both read as `010205`. Same input rules as `isValidCbo`: a string has to be written as the 6 digits or with the `NNNN-NN` mask, and a number has to be a non-negative safe integer. ```javascript import { getCbo } from '@brazilian-utils/brazilian-utils'; -getCbo('2124-05'); // { code: '212405', title: 'Analista de desenvolvimento de sistemas' } +getCbo('2124-05'); // { code: '212405', description: 'Analista de desenvolvimento de sistemas' } +getCbo(10205); // { code: '010205', description: 'Oficial da aeronáutica' } (padded to 6 digits) +getCbo('10205'); // { code: '010205', description: 'Oficial da aeronáutica' } (padded the same way) getCbo('000000'); // null getCbo('2124abc05'); // null (not a documented form) ``` @@ -1881,13 +1886,15 @@ The occupation titles come from the [official CBO 2002 occupation table publishe ## isValidCnae -Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the [CNAE-Subclasses 2.3 table published by IBGE](https://concla.ibge.gov.br/busca-online-cnae.html), the current subclass revision of CNAE 2.0. Accepts the code with or without the `NNNN-N/NN` mask, or as a number. A string is only read as a code when it is written in one of those forms (the 7 digits, or the mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the [CNAE-Subclasses 2.3 table published by IBGE](https://concla.ibge.gov.br/busca-online-cnae.html), the current subclass revision of CNAE 2.0. Accepts the code with or without the `NNNN-N/NN` mask, or as a number. A string is only read as a code when it is written in one of those forms (the 7 digits, or the mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. A CNAE subclass code is always 7 digits and its leading zeros are part of it, so a value written as bare digits is left padded with zeros to 7 whether it comes as a string or as a number: `111301`, `'111301'` and `'0111301'` are the same code. A masked value already carries its separators and is read as written. ```javascript import { isValidCnae } from '@brazilian-utils/brazilian-utils'; isValidCnae('6201-5/01'); // true isValidCnae('6201501'); // true +isValidCnae(111301); // true (padded to 7 digits, so this is '0111301') +isValidCnae('111301'); // true (padded the same way a number is) isValidCnae('0000000'); // false isValidCnae('0111abc301'); // false (not a documented form) isValidCnae(-111301); // false (not a non-negative safe integer) @@ -1911,25 +1918,30 @@ formatCnae(-6201501); // 6201-5/01 ## getCnae -Look a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up and get its formatted code and official description. A `number` keeps its implied leading zeros: `getCnae(111301)` is read as `0111301`. Same input rules as `isValidCnae`: a string has to be written as the 7 digits or with the `NNNN-N/NN` mask, and a number has to be a non-negative safe integer. +Look a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up and get its code and official description. `code` comes back as the 7 bare digits, like every other lookup of this library; pass it to `formatCnae` for the `NNNN-N/NN` form. A value written as bare digits keeps its implied leading zeros, as a string as much as a number: `getCnae(111301)` and `getCnae('111301')` are both read as `0111301`. Same input rules as `isValidCnae`: a string has to be written as the 7 digits or with the `NNNN-N/NN` mask, and a number has to be a non-negative safe integer. ```javascript -import { getCnae } from '@brazilian-utils/brazilian-utils'; +import { formatCnae, getCnae } from '@brazilian-utils/brazilian-utils'; -getCnae('6201501'); // { code: '6201-5/01', description: 'DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA' } +getCnae('6201-5/01'); // { code: '6201501', description: 'DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA' } +getCnae(111301); // { code: '0111301', description: 'CULTIVO DE ARROZ' } (padded to 7 digits) +getCnae('111301'); // { code: '0111301', description: 'CULTIVO DE ARROZ' } (padded the same way) getCnae('0000000'); // null getCnae('0111abc301'); // null (not a documented form) +formatCnae(getCnae('6201501')?.code); // 6201-5/01 (the mask is the formatter's job) ``` ## isValidNcm -Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. Accepts the code with or without the dotted mask, or as a number. A string is only read as a code when it is written in one of those forms (the 8 digits, or the `NNNN.NN.NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. A bare number cannot carry a leading zero, so a code starting with `0` has to be passed as a string: `isValidNcm(1012100)` is `false` while `isValidNcm('01012100')` is `true`. +Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. Accepts the code with or without the dotted mask, or as a number. A string is only read as a code when it is written in one of those forms (the 8 digits, or the `NNNN.NN.NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. An NCM code is always 8 digits and its leading zeros are part of it, so a value written as bare digits is left padded with zeros to 8 whether it comes as a string or as a number: `1012100`, `'1012100'` and `'01012100'` are the same code. A masked value already carries its separators and is read as written. ```javascript import { isValidNcm } from '@brazilian-utils/brazilian-utils'; isValidNcm('8471.30.12'); // true isValidNcm('84713012'); // true +isValidNcm(1012100); // true (padded to 8 digits, so this is '01012100') +isValidNcm('1012100'); // true (padded the same way a number is) isValidNcm('00000000'); // false isValidNcm('abc01012100'); // false (not a documented form) isValidNcm(-84713012); // false (not a non-negative safe integer) @@ -1954,7 +1966,7 @@ formatNcm(-84713012); // 8471.30.12 Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table. The table is the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), the text in force (current wording given by Ajuste SINIEF 03/24, last amended by [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25)), not the frozen 2001 text of Ajuste SINIEF 07/01. Only operable codes count: the group and subgroup headings of the official nomenclature, the codes ending in `00` and `50` (1000, 1100, 1150, 5350, ...), are section titles rather than codes a document can carry, so they are rejected. -A string is only read as a code when it is written in one of the documented forms (the 4 digits, or the `N.NNN` form the annex prints, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. +A string is only read as a code when it is written in one of the documented forms (the 4 digits, or the `N.NNN` form the annex prints, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. No CFOP code starts with a zero, its first digit is the operation group (1 to 7), so nothing is ever padded here: a number and the string of the same digits are read identically. ```javascript import { isValidCfop } from '@brazilian-utils/brazilian-utils'; @@ -1993,21 +2005,26 @@ Check if a CST (Código de Situação Tributária) code is valid for a given tax | `pis` | 2 digits | `01`-`09`, `49`, `50`-`56`, `60`-`67`, `70`-`75`, `98`, `99` | | `cofins` | 2 digits | same table as `pis` | -`options.tax` (part of `IsValidCstOptions`) is optional: omit it to accept a code that exists in any one of the four tables above. +`options.tax` (part of `IsValidCstOptions`) is optional: omit it to accept a code that exists in any one of the four tables above. A `tax` outside those four values falls back to that same default at runtime, the way every other scalar option of this library treats a value it does not know. The ICMS Tabela B is the one in force: the [consolidated Anexo I of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), whose current wording came from [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (effective 01.12.23) and which [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) amended by striking items 12, 13, 52, 72 and 74 (effects from 09.07.24) before they ever took effect: 39/23 had deferred their effect to 1º de outubro de 2024, so the revocation reached them first and those codes were never in force. `02`, `15`, `53` and `61` are its monofasia de combustíveis codes. A string is only read as a code when it is written in one of the documented forms (the 2 digits of a Tabela B code, or the 3 digits of the ICMS form with an optional single separator after the origin digit, plus optional surrounding whitespace), and a number only when it is a non-negative safe integer. The origin digit is the only boundary a printed CST has, so `'0 10'` and `'1-10'` are read while `'0-0'`, `'11-0'` and `'00-'` are not. +A single digit is narrower than either documented form, so it is left padded with zeros to the 3 digits of the ICMS form, whether it comes as a string or as a number: `0`, `'0'` and `'000'` are all the ICMS code `000`. A 2 digit value is already a documented form, a Tabela B code, and is read as written, so a Tabela B code keeps its own two digits: `'07'`, not `7`, which is the ICMS code `007`. + ```javascript import { isValidCst } from '@brazilian-utils/brazilian-utils'; isValidCst('000', { tax: 'icms' }); // true +isValidCst(0, { tax: 'icms' }); // true (a single digit is padded to the 3 digit form, '000') +isValidCst('0', { tax: 'icms' }); // true (padded the same way a number is) isValidCst('110', { tax: 'icms' }); // true isValidCst('002', { tax: 'icms' }); // true (monofasia de combustíveis) isValidCst('06', { tax: 'pis' }); // true isValidCst('99', { tax: 'ipi' }); // true isValidCst('110'); // true (found in the icms table, tax omitted) +isValidCst('000', { tax: 'nope' }); // true (an unknown tax falls back to every table) isValidCst('999'); // false (not in any table) isValidCst('abc110'); // false (not a documented form) isValidCst(-110); // false (not a non-negative safe integer) @@ -2017,7 +2034,7 @@ isValidCst(-110); // false (not a non-negative safe integer) Check if a CSOSN (Código de Situação da Operação no Simples Nacional) code is one of the 10 codes of the [consolidated Anexo III-A of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), the table Ajuste SINIEF 03/2010 instituted: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` or `900`. -A string is only read as a code when it is written as the bare 3 digits with optional surrounding whitespace: a CSOSN has no printed grouping (the NF-e carries the origin digit in its own `orig` field), so `'1-01'` is rejected; a number is read only when it is a non-negative safe integer. +A string is only read as a code when it is written as the bare 3 digits with optional surrounding whitespace: a CSOSN has no printed grouping (the NF-e carries the origin digit in its own `orig` field), so `'1-01'` is rejected; a number is read only when it is a non-negative safe integer. No CSOSN code starts with a zero, the table runs from `101` to `900`, so nothing is ever padded here: a number and the string of the same digits are read identically. ```javascript import { isValidCsosn } from '@brazilian-utils/brazilian-utils'; diff --git a/src/_internals/pad-lookup-code/pad-lookup-code.test.ts b/src/_internals/pad-lookup-code/pad-lookup-code.test.ts new file mode 100644 index 00000000..2f2eda57 --- /dev/null +++ b/src/_internals/pad-lookup-code/pad-lookup-code.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from "../test/runtime"; +import { padLookupCode } from "./pad-lookup-code"; + +describe("padLookupCode", () => { + test("should left pad a number with zeros up to the given width", () => { + expect(padLookupCode(10_205, 6)).toBe("010205"); + expect(padLookupCode(0, 3)).toBe("000"); + expect(padLookupCode(5, 3)).toBe("005"); + }); + + test("should left pad a string of bare digits exactly like the number it spells", () => { + expect(padLookupCode("10205", 6)).toBe("010205"); + expect(padLookupCode("0", 3)).toBe("000"); + }); + + test("should return a value already as wide as the table unchanged", () => { + expect(padLookupCode("212405", 6)).toBe("212405"); + expect(padLookupCode(212_405, 6)).toBe("212405"); + }); + + test("should never shorten a value wider than the table", () => { + expect(padLookupCode("2124055", 6)).toBe("2124055"); + }); + + test("should trim surrounding whitespace before padding", () => { + expect(padLookupCode(" 10205 ", 6)).toBe("010205"); + expect(padLookupCode(" 212405 ", 6)).toBe("212405"); + }); + + test("should hand a masked value back untouched, even when it is narrower than the table", () => { + expect(padLookupCode("6201-5/01", 7)).toBe("6201-5/01"); + expect(padLookupCode("12-3", 6)).toBe("12-3"); + expect(padLookupCode("2124 05", 6)).toBe("2124 05"); + }); + + test("should hand a value that is not digits back untouched", () => { + expect(padLookupCode("abc", 6)).toBe("abc"); + expect(padLookupCode("2124abc05", 6)).toBe("2124abc05"); + expect(padLookupCode("+212405", 6)).toBe("+212405"); + }); + + test("should never turn an empty value into a code of zeros", () => { + expect(padLookupCode("", 6)).toBe(""); + expect(padLookupCode(" ", 6)).toBe(""); + }); +}); diff --git a/src/_internals/pad-lookup-code/pad-lookup-code.ts b/src/_internals/pad-lookup-code/pad-lookup-code.ts new file mode 100644 index 00000000..0768a5f0 --- /dev/null +++ b/src/_internals/pad-lookup-code/pad-lookup-code.ts @@ -0,0 +1,34 @@ +const BARE_DIGITS_REGEX = /^\d+$/; + +/** + * Left pads a lookup code with zeros up to the fixed width of its table, so the leading zeros a + * table's codes carry never depend on how the caller wrote the value. + * + * A table whose codes all have the same width and may start with a zero (CBO with 6 digits, + * CNAE with 7, NCM with 8) is looked up by that padded form, so `10205`, `"10205"` and + * `"010205"` are all the CBO code `010205`, the same way `getBankByCode(1)` and + * `getBankByCode("1")` are both the bank `"001"`. + * + * Only a value written as bare digits is padded: a masked value (`"6201-5/01"`) already carries + * its separators and is handed back untouched, and so is anything that is not digits at all + * (`"abc"`), which the caller's own format check then turns down. Surrounding whitespace is + * trimmed either way, and an empty value is never turned into a code of zeros. + * + * @param {string|number} value - The value to normalize, a string or a number. + * @param {number} length - The fixed digit width of the table's codes. + * @returns {string} The trimmed value, left padded with zeros when it is written as bare digits. + * + * @example + * ```typescript + * padLookupCode(10205, 6); // "010205" + * padLookupCode("10205", 6); // "010205" + * padLookupCode(" 212405 ", 6); // "212405" + * padLookupCode("6201-5/01", 7); // "6201-5/01" + * padLookupCode("", 6); // "" + * ``` + */ +export const padLookupCode = (value: string | number, length: number): string => { + const code = String(value).trim(); + + return BARE_DIGITS_REGEX.test(code) ? code.padStart(length, "0") : code; +}; diff --git a/src/get-cbo/get-cbo.test.ts b/src/get-cbo/get-cbo.test.ts index d0c2b070..4d9c21c6 100644 --- a/src/get-cbo/get-cbo.test.ts +++ b/src/get-cbo/get-cbo.test.ts @@ -11,33 +11,46 @@ describe("getCbo", () => { it("should return the occupation for a code without a mask", () => { expect(getCbo("212405")).toEqual({ code: "212405", - title: "Analista de desenvolvimento de sistemas", + description: "Analista de desenvolvimento de sistemas", }); }); it("should return the occupation for a code with the hyphen mask", () => { expect(getCbo("2124-05")).toEqual({ code: "212405", - title: "Analista de desenvolvimento de sistemas", + description: "Analista de desenvolvimento de sistemas", }); }); it("should return the occupation for a code given as a number", () => { expect(getCbo(212_405)).toEqual({ code: "212405", - title: "Analista de desenvolvimento de sistemas", + description: "Analista de desenvolvimento de sistemas", }); }); - it("should pad a number to six digits so codes starting with zero resolve (0102-05, Oficial da aeronáutica)", () => { - expect(getCbo(10_205)).toEqual({ code: "010205", title: "Oficial da aeronáutica" }); - expect(getCbo("10205")).toBeNull(); + it("should pad to six digits so codes starting with zero resolve (0102-05, Oficial da aeronáutica)", () => { + const oficial = { code: "010205", description: "Oficial da aeronáutica" }; + + expect(getCbo(10_205)).toEqual(oficial); + expect(getCbo("10205")).toEqual(oficial); + expect(getCbo("010205")).toEqual(oficial); + }); + + it("should pad a string of bare digits exactly like the number it spells", () => { + expect(getCbo("10205")).toEqual(getCbo(10_205)); + expect(getCbo(" 10205 ")).toEqual(getCbo(10_205)); + }); + + it("should not pad a masked value, which already carries its separators", () => { + expect(getCbo("102-05")).toBeNull(); + expect(getCbo("0102-05")).toEqual({ code: "010205", description: "Oficial da aeronáutica" }); }); it("should resolve a code the official CSV carries and the community mirror did not (142135)", () => { expect(getCbo("142135")).toEqual({ code: "142135", - title: "Oficial de proteção de dados pessoais (dpo)", + description: "Oficial de proteção de dados pessoais (dpo)", }); }); @@ -49,11 +62,11 @@ describe("getCbo", () => { expect(getCbo("2124--05")).toBeNull(); expect(getCbo("2124-05")).toEqual({ code: "212405", - title: "Analista de desenvolvimento de sistemas", + description: "Analista de desenvolvimento de sistemas", }); expect(getCbo("2124 05")).toEqual({ code: "212405", - title: "Analista de desenvolvimento de sistemas", + description: "Analista de desenvolvimento de sistemas", }); }); @@ -67,7 +80,7 @@ describe("getCbo", () => { expect(getCbo("000000")).toBeNull(); }); - it("should return null when the digit count is not six", () => { + it("should return null for a padded short value no occupation carries and for a wider value", () => { expect(getCbo("21240")).toBeNull(); expect(getCbo("2124055")).toBeNull(); }); @@ -108,8 +121,12 @@ describe("getCbo", () => { test("should resolve every known code, as a string or a number, and agree with isValidCbo", () => { fc.assert( fc.property(codeArbitrary, (code) => { - expect(getCbo(code)).toEqual({ code, title: CBO_TITLES[code] }); - expect(getCbo(Number(code))).toEqual({ code, title: CBO_TITLES[code] }); + const expected = { code, description: CBO_TITLES[code] }; + const unpadded = String(Number(code)); + + expect(getCbo(code)).toEqual(expected); + expect(getCbo(Number(code))).toEqual(expected); + expect(getCbo(unpadded)).toEqual(expected); expect(isValidCbo(code)).toBe(true); }), ); @@ -120,7 +137,7 @@ describe("getCbo", () => { fc.property(codeArbitrary, (code) => { const masked = `${code.slice(0, 4)}-${code.slice(4)}`; - expect(getCbo(masked)).toEqual({ code, title: CBO_TITLES[code] }); + expect(getCbo(masked)).toEqual({ code, description: CBO_TITLES[code] }); }), ); }); @@ -131,6 +148,6 @@ describe("getCbo types", () => { test("should take a string or number and return a Cbo or null", () => { expectTypeOf(getCbo).parameter(0).toEqualTypeOf(); expectTypeOf(getCbo).returns.toEqualTypeOf(); - expectTypeOf().toEqualTypeOf<{ code: string; title: string }>(); + expectTypeOf().toEqualTypeOf<{ code: string; description: string }>(); }); }); diff --git a/src/get-cbo/get-cbo.ts b/src/get-cbo/get-cbo.ts index 9e7cfade..54025c04 100644 --- a/src/get-cbo/get-cbo.ts +++ b/src/get-cbo/get-cbo.ts @@ -1,5 +1,6 @@ import { CBO_FORMAT_REGEX, CBO_TITLES } from "../_internals/constants/cbo"; import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; +import { padLookupCode } from "../_internals/pad-lookup-code/pad-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; const CBO_LENGTH = 6; @@ -10,8 +11,8 @@ const CBO_LENGTH = 6; export type Cbo = { /** The 6 digit occupation code, without the hyphen mask. */ code: string; - /** The official occupation title. */ - title: string; + /** The official occupation description, the title the MTE table prints. */ + description: string; }; /** @@ -25,14 +26,20 @@ export type Cbo = { * since a sign, a decimal point or a rounded magnitude would otherwise be read as a code the * caller never wrote. * + * A CBO code is always 6 digits and its leading zeros are part of it, so a value written as + * bare digits is left padded with zeros to 6 whether it comes as a string or as a number: + * `10205`, `"10205"` and `"010205"` are the same code. A masked value already carries its + * separators and is read as written. + * * @param {string|number} value - The CBO code to look up, with or without the hyphen * mask, e.g. `"2124-05"`, `"212405"` or `212405`. * @returns {Cbo|null} The matching occupation, or null when the code is unknown or invalid. * * @example * ```typescript - * getCbo("2124-05"); // { code: "212405", title: "Analista de desenvolvimento de sistemas" } - * getCbo(10205); // { code: "010205", title: "Oficial da aeronáutica" } (a number is padded to 6 digits) + * getCbo("2124-05"); // { code: "212405", description: "Analista de desenvolvimento de sistemas" } + * getCbo(10205); // { code: "010205", description: "Oficial da aeronáutica" } (padded to 6 digits) + * getCbo("10205"); // { code: "010205", description: "Oficial da aeronáutica" } (padded to 6 digits) * getCbo("999999"); // null * getCbo("2124abc05"); // null (not a documented form) * getCbo(-212405); // null (not a non-negative safe integer) @@ -47,14 +54,14 @@ export type Cbo = { export const getCbo = (value: string | number): Cbo | null => { if (!isLookupCode(value)) return null; - const code = typeof value === "number" ? String(value).padStart(CBO_LENGTH, "0") : value.trim(); + const code = padLookupCode(value, CBO_LENGTH); if (!CBO_FORMAT_REGEX.test(code)) return null; const digits = sanitizeToDigits(code); - const title = CBO_TITLES[digits]; + const description = CBO_TITLES[digits]; - if (title === undefined) return null; + if (description === undefined) return null; - return { code: digits, title }; + return { code: digits, description }; }; diff --git a/src/get-cfop/get-cfop.ts b/src/get-cfop/get-cfop.ts index e256b0dd..608a4d5f 100644 --- a/src/get-cfop/get-cfop.ts +++ b/src/get-cfop/get-cfop.ts @@ -29,6 +29,10 @@ export type Cfop = { * safe integer, since a sign, a decimal point or a rounded magnitude would otherwise be read * as a code the caller never wrote. * + * No CFOP code starts with a zero, its first digit is the operation group (1 to 7), so nothing + * is ever padded here: a number and the string of the same digits are read identically, and a + * value narrower than 4 digits is not a code at all. + * * @param {string|number} value - The CFOP code to look up, with or without the `N.NNN` mask, * e.g. `"1.101"`, `"1101"` or `1101`. * @returns {Cfop|null} The matching CFOP entry, or null when the code is unknown or diff --git a/src/get-cnae/get-cnae.test.ts b/src/get-cnae/get-cnae.test.ts index cf8c4af6..e286c7f3 100644 --- a/src/get-cnae/get-cnae.test.ts +++ b/src/get-cnae/get-cnae.test.ts @@ -11,28 +11,46 @@ import { getCnae, type Cnae } from "./get-cnae"; describe("getCnae", () => { it("should return the CNAE entry for a known code as a string", () => { expect(getCnae("6201501")).toEqual({ - code: "6201-5/01", + code: "6201501", description: "DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA", }); }); it("should return the CNAE entry for a known code as a number", () => { expect(getCnae(6_201_501)).toEqual({ - code: "6201-5/01", + code: "6201501", description: "DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA", }); }); it("should return the CNAE entry for a masked code", () => { expect(getCnae("6201-5/01")).toEqual({ - code: "6201-5/01", + code: "6201501", description: "DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA", }); }); - it("should pad a number to seven digits so codes starting with zero resolve (0111-3/01, cultivo de arroz)", () => { - expect(getCnae(111_301)).toEqual({ code: "0111-3/01", description: "CULTIVO DE ARROZ" }); - expect(getCnae("111301")).toBeNull(); + it("should pad to seven digits so codes starting with zero resolve (0111-3/01, cultivo de arroz)", () => { + const arroz = { code: "0111301", description: "CULTIVO DE ARROZ" }; + + expect(getCnae(111_301)).toEqual(arroz); + expect(getCnae("111301")).toEqual(arroz); + expect(getCnae("0111301")).toEqual(arroz); + }); + + it("should pad a string of bare digits exactly like the number it spells", () => { + expect(getCnae("111301")).toEqual(getCnae(111_301)); + expect(getCnae(" 111301 ")).toEqual(getCnae(111_301)); + }); + + it("should not pad a masked value, which already carries its separators", () => { + expect(getCnae("111-3/01")).toBeNull(); + expect(getCnae("0111-3/01")).toEqual({ code: "0111301", description: "CULTIVO DE ARROZ" }); + }); + + it("should return the bare digits as the code and leave the mask to formatCnae", () => { + expect(getCnae("6201-5/01")?.code).toBe("6201501"); + expect(formatCnae(getCnae("6201-5/01")?.code ?? "")).toBe("6201-5/01"); }); it("should return a fresh object on every call", () => { @@ -45,7 +63,7 @@ describe("getCnae", () => { expect(getCnae("0000000")).toBeNull(); }); - it("should return null for a code with a digit count different from seven", () => { + it("should return null for a padded short value no subclass carries", () => { expect(getCnae("620150")).toBeNull(); }); @@ -81,10 +99,13 @@ describe("getCnae", () => { test("should resolve every known code, as a string or a number, and agree with formatCnae and isValidCnae", () => { fc.assert( fc.property(codeArbitrary, (code) => { - const expected = { code: formatCnae(code), description: CNAE_SUBCLASSES[code] }; + const expected = { code, description: CNAE_SUBCLASSES[code] }; + const unpadded = String(Number(code)); expect(getCnae(code)).toEqual(expected); expect(getCnae(Number(code))).toEqual(expected); + expect(getCnae(unpadded)).toEqual(expected); + expect(formatCnae(getCnae(code)?.code ?? "")).toBe(formatCnae(code)); expect(isValidCnae(code)).toBe(true); }), ); diff --git a/src/get-cnae/get-cnae.ts b/src/get-cnae/get-cnae.ts index 045b7752..aa28ad74 100644 --- a/src/get-cnae/get-cnae.ts +++ b/src/get-cnae/get-cnae.ts @@ -1,7 +1,7 @@ import { CNAE_FORMAT_REGEX, CNAE_SUBCLASSES } from "../_internals/constants/cnae"; import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; +import { padLookupCode } from "../_internals/pad-lookup-code/pad-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -import { formatCnae } from "../format-cnae/format-cnae"; const CNAE_LENGTH = 7; @@ -9,7 +9,7 @@ const CNAE_LENGTH = 7; * A CNAE (Classificação Nacional de Atividades Econômicas) subclass. */ export type Cnae = { - /** The subclass code formatted as `NNNN-N/NN`. */ + /** The 7 digit subclass code, without the mask. Use `formatCnae` for the `NNNN-N/NN` form. */ code: string; /** The official subclass description. */ description: string; @@ -26,16 +26,26 @@ export type Cnae = { * since a sign, a decimal point or a rounded magnitude would otherwise be read as a code the * caller never wrote. * + * A CNAE subclass code is always 7 digits and its leading zeros are part of it, so a value + * written as bare digits is left padded with zeros to 7 whether it comes as a string or as a + * number: `111301`, `"111301"` and `"0111301"` are the same code. A masked value already + * carries its separators and is read as written. + * + * `code` comes back as those 7 bare digits, like every other lookup of this library; pass it to + * `formatCnae` for the `NNNN-N/NN` form. + * * @param {string|number} value - The CNAE code to look up, with or without the * `NNNN-N/NN` mask. * @returns {Cnae|null} The matching subclass, or null when the code is unknown or invalid. * * @example * ```typescript - * getCnae("6201501"); // { code: "6201-5/01", description: "DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA" } - * getCnae(111301); // { code: "0111-3/01", description: "CULTIVO DE ARROZ" } (a number is padded to 7 digits) + * getCnae("6201-5/01"); // { code: "6201501", description: "DESENVOLVIMENTO DE PROGRAMAS DE COMPUTADOR SOB ENCOMENDA" } + * getCnae(111301); // { code: "0111301", description: "CULTIVO DE ARROZ" } (padded to 7 digits) + * getCnae("111301"); // { code: "0111301", description: "CULTIVO DE ARROZ" } (padded to 7 digits) * getCnae("0000000"); // null * getCnae("0111abc301"); // null (not a documented form) + * formatCnae(getCnae("6201501")?.code); // "6201-5/01" (the mask is the formatter's job) * getCnae(-111301); // null (not a non-negative safe integer) * ``` * @@ -46,8 +56,7 @@ export type Cnae = { export const getCnae = (value: string | number): Cnae | null => { if (!isLookupCode(value)) return null; - const subclass = - typeof value === "number" ? String(value).padStart(CNAE_LENGTH, "0") : value.trim(); + const subclass = padLookupCode(value, CNAE_LENGTH); if (!CNAE_FORMAT_REGEX.test(subclass)) return null; @@ -56,5 +65,5 @@ export const getCnae = (value: string | number): Cnae | null => { if (description === undefined) return null; - return { code: formatCnae(digits), description }; + return { code: digits, description }; }; diff --git a/src/get-legal-nature/get-legal-nature.ts b/src/get-legal-nature/get-legal-nature.ts index 156858e7..d008998e 100644 --- a/src/get-legal-nature/get-legal-nature.ts +++ b/src/get-legal-nature/get-legal-nature.ts @@ -34,6 +34,10 @@ const lookUp = (code: string): LegalNature | null => { * The usual mask characters (hyphens, dots, whitespace) are stripped before the lookup, from a * number as well as from a string, so `getLegalNature(206.2)` resolves like `getLegalNature("206.2")`. * + * No legal nature code starts with a zero, its first digit is the CONCLA category (1 to 5), so + * nothing is ever padded here: a number and the string of the same digits are read identically, + * and a value narrower than 4 digits is not a code at all. + * * The entry also carries the CONCLA category of the code, the group the table lists it under, * taken from its first digit: 1 Administração Pública, 2 Entidades Empresariais, 3 Entidades * sem Fins Lucrativos, 4 Pessoas Físicas and 5 Organizações Internacionais e Outras diff --git a/src/is-valid-cbo/is-valid-cbo.test.ts b/src/is-valid-cbo/is-valid-cbo.test.ts index 1fb0f7f3..266d0b27 100644 --- a/src/is-valid-cbo/is-valid-cbo.test.ts +++ b/src/is-valid-cbo/is-valid-cbo.test.ts @@ -19,10 +19,15 @@ describe("isValidCbo", () => { expect(isValidCbo(212_405)).toBe(true); }); - it("should pad a number with leading zeros before looking it up", () => { + it("should pad a value with leading zeros before looking it up, as a number or as a string", () => { expect(isValidCbo(10_205)).toBe(true); expect(isValidCbo("010205")).toBe(true); - expect(isValidCbo("10205")).toBe(false); + expect(isValidCbo("10205")).toBe(true); + }); + + it("should not pad a masked value, which already carries its separators", () => { + expect(isValidCbo("102-05")).toBe(false); + expect(isValidCbo("0102-05")).toBe(true); }); it("should validate a CBO code with surrounding whitespace", () => { @@ -47,7 +52,7 @@ describe("isValidCbo", () => { expect(isValidCbo("000000")).toBe(false); }); - it("should return false when the digit count is not six", () => { + it("should return false for a padded short value no occupation carries and for a wider value", () => { expect(isValidCbo("21240")).toBe(false); expect(isValidCbo("2124055")).toBe(false); }); diff --git a/src/is-valid-cbo/is-valid-cbo.ts b/src/is-valid-cbo/is-valid-cbo.ts index 676b56b3..2064d844 100644 --- a/src/is-valid-cbo/is-valid-cbo.ts +++ b/src/is-valid-cbo/is-valid-cbo.ts @@ -9,6 +9,10 @@ import { getCbo } from "../get-cbo/get-cbo"; * surrounding whitespace. A number is only read as a code when it is a non-negative safe * integer. * + * A CBO code is always 6 digits and its leading zeros are part of it, so a value written as + * bare digits is left padded with zeros to 6 whether it comes as a string or as a number: + * `10205`, `"10205"` and `"010205"` are the same code. + * * @param {string|number} value - The CBO code to be validated, with or without the hyphen * mask, e.g. `"2124-05"`, `"212405"` or `212405`. * @returns {boolean} True when the code is a known 6 digit occupation code, false otherwise. @@ -18,7 +22,8 @@ import { getCbo } from "../get-cbo/get-cbo"; * isValidCbo("2124-05"); // true * isValidCbo("212405"); // true * isValidCbo(212405); // true - * isValidCbo(10205); // true (a number is padded to 6 digits, so this is "010205") + * isValidCbo(10205); // true (padded to 6 digits, so this is "010205") + * isValidCbo("10205"); // true (padded to 6 digits, so this is "010205") * isValidCbo("999999"); // false * isValidCbo("2124abc05"); // false (not a documented form) * isValidCbo(-212405); // false (not a non-negative safe integer) diff --git a/src/is-valid-cfop/is-valid-cfop.ts b/src/is-valid-cfop/is-valid-cfop.ts index 06eac7a5..e50bb6b4 100644 --- a/src/is-valid-cfop/is-valid-cfop.ts +++ b/src/is-valid-cfop/is-valid-cfop.ts @@ -20,6 +20,10 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * safe integer, since a sign, a decimal point or a rounded magnitude would otherwise be read * as a code the caller never wrote. * + * No CFOP code starts with a zero, its first digit is the operation group (1 to 7), so nothing + * is ever padded here: a number and the string of the same digits are read identically, and a + * value narrower than 4 digits is not a code at all. + * * @param {string|number} value - The CFOP code to be validated, with or without the `N.NNN` * mask, e.g. `"1.101"`, `"1101"` or `1101`. * @returns {boolean} True when the code is a known 4 digit CFOP code, false otherwise. diff --git a/src/is-valid-cnae/is-valid-cnae.test.ts b/src/is-valid-cnae/is-valid-cnae.test.ts index 4ca593ea..bc655973 100644 --- a/src/is-valid-cnae/is-valid-cnae.test.ts +++ b/src/is-valid-cnae/is-valid-cnae.test.ts @@ -19,10 +19,15 @@ describe("isValidCnae", () => { expect(isValidCnae(6_201_501)).toBe(true); }); - it("should pad a number with leading zeros before looking it up", () => { + it("should pad a value with leading zeros before looking it up, as a number or as a string", () => { expect(isValidCnae(111_301)).toBe(true); expect(isValidCnae("0111301")).toBe(true); - expect(isValidCnae("111301")).toBe(false); + expect(isValidCnae("111301")).toBe(true); + }); + + it("should not pad a masked value, which already carries its separators", () => { + expect(isValidCnae("111-3/01")).toBe(false); + expect(isValidCnae("0111-3/01")).toBe(true); }); it("should validate a CNAE code with surrounding whitespace", () => { @@ -38,7 +43,7 @@ describe("isValidCnae", () => { expect(isValidCnae("0000000")).toBe(false); }); - it("should return false when the digit count is not seven", () => { + it("should return false for a padded short value no subclass carries and for a wider value", () => { expect(isValidCnae("620150")).toBe(false); expect(isValidCnae("62015011")).toBe(false); }); diff --git a/src/is-valid-cnae/is-valid-cnae.ts b/src/is-valid-cnae/is-valid-cnae.ts index 614d6060..6a481bdd 100644 --- a/src/is-valid-cnae/is-valid-cnae.ts +++ b/src/is-valid-cnae/is-valid-cnae.ts @@ -9,6 +9,10 @@ import { getCnae } from "../get-cnae/get-cnae"; * surrounding whitespace. A number is only read as a code when it is a non-negative safe * integer. * + * A CNAE subclass code is always 7 digits and its leading zeros are part of it, so a value + * written as bare digits is left padded with zeros to 7 whether it comes as a string or as a + * number: `111301`, `"111301"` and `"0111301"` are the same code. + * * @param {string|number} value - The CNAE code to be validated, with or without the * `NNNN-N/NN` mask, e.g. `"6201-5/01"`, `"6201501"` or `6201501`. * @returns {boolean} True when the code is a known 7 digit subclass, false otherwise. @@ -18,7 +22,8 @@ import { getCnae } from "../get-cnae/get-cnae"; * isValidCnae("6201-5/01"); // true * isValidCnae("6201501"); // true * isValidCnae(6201501); // true - * isValidCnae(111301); // true (a number is padded to 7 digits, so this is "0111301") + * isValidCnae(111301); // true (padded to 7 digits, so this is "0111301") + * isValidCnae("111301"); // true (padded to 7 digits, so this is "0111301") * isValidCnae("0000000"); // false * isValidCnae("0111abc301"); // false (not a documented form) * isValidCnae(-111301); // false (not a non-negative safe integer) diff --git a/src/is-valid-csosn/is-valid-csosn.ts b/src/is-valid-csosn/is-valid-csosn.ts index 91f1fc7a..d8ccdbcb 100644 --- a/src/is-valid-csosn/is-valid-csosn.ts +++ b/src/is-valid-csosn/is-valid-csosn.ts @@ -15,6 +15,10 @@ import { CSOSN_CODES, CSOSN_FORMAT_REGEX } from "./constants"; * as a code when it is a non-negative safe integer, since a sign, a decimal point or a rounded * magnitude would otherwise be read as a code the caller never wrote. * + * No CSOSN code starts with a zero, the table runs from `101` to `900`, so nothing is ever + * padded here: a number and the string of the same digits are read identically, and a value + * narrower than 3 digits is not a code at all. + * * @param {string|number} value - The CSOSN code to be validated, e.g. `"101"` or `101`. * @returns {boolean} True when the code is a known CSOSN code, false otherwise. * diff --git a/src/is-valid-cst/is-valid-cst.test.ts b/src/is-valid-cst/is-valid-cst.test.ts index 1f4aa632..d6ac8d99 100644 --- a/src/is-valid-cst/is-valid-cst.test.ts +++ b/src/is-valid-cst/is-valid-cst.test.ts @@ -80,14 +80,43 @@ describe("isValidCst", () => { }); }); - it("should return false for an unknown tax", () => { - // @ts-expect-error not a valid tax - expect(isValidCst("00", { tax: "iss" })).toBe(false); + describe("unknown tax", () => { + it("should fall back to the default and check every table, as every other scalar option does", () => { + // @ts-expect-error not a valid tax + expect(isValidCst("000", { tax: "nope" })).toBe(true); + // @ts-expect-error not a valid tax + expect(isValidCst("00", { tax: "iss" })).toBe(true); + // @ts-expect-error not a valid tax + expect(isValidCst("07", { tax: "iss" })).toBe(true); + }); + + it("should still reject a code that exists in no table", () => { + // @ts-expect-error not a valid tax + expect(isValidCst("999", { tax: "iss" })).toBe(false); + }); + + it("should fall back to the default when the tax is not a string", () => { + // @ts-expect-error not a valid tax + expect(isValidCst("07", { tax: 1 })).toBe(true); + // @ts-expect-error not a valid tax + expect(isValidCst("07", { tax: null })).toBe(true); + }); + + it("should fall back to the default for a prototype chain key", () => { + // @ts-expect-error not a valid tax + expect(isValidCst("07", { tax: "__proto__" })).toBe(true); + // @ts-expect-error not a valid tax + expect(isValidCst("07", { tax: "toString" })).toBe(true); + }); }); - it("should return false for an unknown tax even when the code is a valid pis/cofins code", () => { - // @ts-expect-error not a valid tax - expect(isValidCst("07", { tax: "iss" })).toBe(false); + it("should consult only the given table, never the other three", () => { + expect(isValidCst("00", { tax: "icms" })).toBe(false); + expect(isValidCst("06", { tax: "ipi" })).toBe(false); + expect(isValidCst("00", { tax: "pis" })).toBe(false); + expect(isValidCst("00", { tax: "cofins" })).toBe(false); + expect(isValidCst("00")).toBe(true); + expect(isValidCst("06")).toBe(true); }); describe("without options (tax omitted)", () => { @@ -130,6 +159,35 @@ describe("isValidCst", () => { expect(isValidCst("00", "foo")).toBe(false); }); + describe("padding", () => { + it("should read a single digit as the three digit icms form, as a number or as a string", () => { + expect(isValidCst(0, { tax: "icms" })).toBe(true); + expect(isValidCst("0", { tax: "icms" })).toBe(true); + expect(isValidCst("000", { tax: "icms" })).toBe(true); + expect(isValidCst(2, { tax: "icms" })).toBe(true); + expect(isValidCst("2", { tax: "icms" })).toBe(true); + }); + + it("should agree between a number and a string when the tax is omitted", () => { + expect(isValidCst(0)).toBe(true); + expect(isValidCst("0")).toBe(true); + expect(isValidCst(9)).toBe(false); + expect(isValidCst("9")).toBe(false); + }); + + it("should leave a two digit Tabela B code as written, never padding it to three", () => { + expect(isValidCst(49, { tax: "ipi" })).toBe(true); + expect(isValidCst("49", { tax: "ipi" })).toBe(true); + expect(isValidCst(49, { tax: "icms" })).toBe(false); + expect(isValidCst("00", { tax: "ipi" })).toBe(true); + }); + + it("should trim a single digit before padding it", () => { + expect(isValidCst(" 0 ", { tax: "icms" })).toBe(true); + expect(isValidCst(" 0 ")).toBe(true); + }); + }); + it("should return false for an empty string", () => { expect(isValidCst("", { tax: "icms" })).toBe(false); }); diff --git a/src/is-valid-cst/is-valid-cst.ts b/src/is-valid-cst/is-valid-cst.ts index c96f895e..9b8226b7 100644 --- a/src/is-valid-cst/is-valid-cst.ts +++ b/src/is-valid-cst/is-valid-cst.ts @@ -1,7 +1,14 @@ import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; +import { padLookupCode } from "../_internals/pad-lookup-code/pad-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { CST_FORMAT_REGEX, ICMS_CST_CODES, IPI_CST_CODES, PIS_COFINS_CST_CODES } from "./constants"; +/** Width of the ICMS form, the widest a CST is printed with: 1 origin digit plus a Tabela B code. */ +const CST_LENGTH = 3; + +/** Width of a bare Tabela B code, the narrowest documented form a CST is written in. */ +const TABELA_B_LENGTH = 2; + /** * Options for `isValidCst`. */ @@ -9,23 +16,30 @@ export type IsValidCstOptions = { /** * The tax whose CST (Código de Situação Tributária) table the value is checked against. * Omit it to accept a code that exists in any of the four tables (`icms`, `ipi`, `pis`, - * `cofins`). + * `cofins`); a value outside those four falls back to that same default at runtime. */ tax?: "icms" | "ipi" | "pis" | "cofins"; }; +type CstTax = NonNullable; + const isValidIcmsCst = (digits: string): boolean => digits.charAt(0) <= "8" && (ICMS_CST_CODES as readonly string[]).includes(digits.slice(1)); -const isValidForTax = (digits: string, tax: "icms" | "ipi" | "pis" | "cofins"): boolean => { - if (tax === "icms") return isValidIcmsCst(digits); - if (tax === "ipi") return (IPI_CST_CODES as readonly string[]).includes(digits); +const isValidIpiCst = (digits: string): boolean => + (IPI_CST_CODES as readonly string[]).includes(digits); - if (tax === "pis" || tax === "cofins") { - return (PIS_COFINS_CST_CODES as readonly string[]).includes(digits); - } +const isValidPisCofinsCst = (digits: string): boolean => + (PIS_COFINS_CST_CODES as readonly string[]).includes(digits); - return false; +const isKnownTax = (tax: unknown): tax is CstTax => + tax === "icms" || tax === "ipi" || tax === "pis" || tax === "cofins"; + +const isValidForTax = (digits: string, tax: CstTax): boolean => { + if (tax === "icms") return isValidIcmsCst(digits); + if (tax === "ipi") return isValidIpiCst(digits); + + return isValidPisCofinsCst(digits); }; /** @@ -42,7 +56,10 @@ const isValidForTax = (digits: string, tax: "icms" | "ipi" | "pis" | "cofins"): * 52, 53, 54, 55, 56, 60, 61, 62, 63, 64, 65, 66, 67, 70, 71, 72, 73, 74, 75, 98, 99`. * * `options.tax` is optional. When it is omitted, the code is valid as long as it exists in any - * one of the four tables above; when it is given, only that table is consulted. + * one of the four tables above; when it is given, only that table is consulted. A `tax` outside + * the four documented values falls back to that default instead of turning the code down, the + * way every other scalar option of this library (`version`, `type`, `style`) treats a value it + * does not know. * * A string is only read as a code when it is written in one of the documented forms: the 2 * digits of a Tabela B code, or the 3 digits of the ICMS form with an optional single @@ -53,9 +70,15 @@ const isValidForTax = (digits: string, tax: "icms" | "ipi" | "pis" | "cofins"): * only read as a code when it is a non-negative safe integer, since a sign, a decimal point or * a rounded magnitude would otherwise be read as a code the caller never wrote. * + * A single digit is narrower than either documented form, so it is left padded with zeros to + * the 3 digits of the ICMS form, whether it comes as a string or as a number: `0`, `"0"` and + * `"000"` are all the ICMS code `000`. A 2 digit value is already a documented form, a Tabela B + * code, and is read as written, so `isValidCst("00", { tax: "ipi" })` stays a CST-IPI check and + * a Tabela B code keeps its own two digits: `"07"`, not `7`, which is the ICMS code `007`. + * * @param {string|number} value - The CST code to be validated, e.g. `"110"`, `"0 10"` or `110`. * @param {IsValidCstOptions} [options] - The tax whose table the value is checked against. - * Checks every table when omitted. + * Checks every table when omitted or when the tax is not one of the four documented values. * @returns {boolean} True when the code is valid for the given tax (or for any tax, when * `options.tax` is omitted), false otherwise. * @@ -89,8 +112,11 @@ const isValidForTax = (digits: string, tax: "icms" | "ipi" | "pis" | "cofins"): * isValidCst("49", { tax: "pis" }); // true * isValidCst("07", { tax: "cofins" }); // true * isValidCst("99", { tax: "icms" }); // false + * isValidCst(0, { tax: "icms" }); // true (a single digit is padded to the 3 digit form, "000") + * isValidCst("0", { tax: "icms" }); // true (padded the same way a number is) * isValidCst("110"); // true (found in the icms table) * isValidCst("49"); // true (found in the ipi table) + * isValidCst("000", { tax: "nope" }); // true (an unknown tax falls back to checking every table) * isValidCst("999"); // false (not in any table) * isValidCst("abc110"); // false (not a documented form) * isValidCst(-110); // false (not a non-negative safe integer) @@ -100,16 +126,15 @@ export const isValidCst = (value: string | number, options?: IsValidCstOptions): if (!isLookupCode(value)) return false; if (options !== undefined && (options === null || typeof options !== "object")) return false; - const code = typeof value === "number" ? String(value) : value.trim(); + const trimmed = String(value).trim(); + const code = trimmed.length < TABELA_B_LENGTH ? padLookupCode(trimmed, CST_LENGTH) : trimmed; if (!CST_FORMAT_REGEX.test(code)) return false; const digits = sanitizeToDigits(code); const tax = options?.tax; - if (tax !== undefined) return isValidForTax(digits, tax); + if (isKnownTax(tax)) return isValidForTax(digits, tax); - return ( - isValidForTax(digits, "icms") || isValidForTax(digits, "ipi") || isValidForTax(digits, "pis") - ); + return isValidIcmsCst(digits) || isValidIpiCst(digits) || isValidPisCofinsCst(digits); }; diff --git a/src/is-valid-ncm/is-valid-ncm.test.ts b/src/is-valid-ncm/is-valid-ncm.test.ts index e443f845..2c044444 100644 --- a/src/is-valid-ncm/is-valid-ncm.test.ts +++ b/src/is-valid-ncm/is-valid-ncm.test.ts @@ -24,8 +24,14 @@ describe("isValidNcm", () => { expect(isValidNcm("0101.21.00")).toBe(true); }); - it("should return false for a number that lost a leading zero (1012100 is not 01012100)", () => { - expect(isValidNcm(1_012_100)).toBe(false); + it("should pad a value to eight digits, as a number or as a string (1012100 is 01012100)", () => { + expect(isValidNcm(1_012_100)).toBe(true); + expect(isValidNcm("1012100")).toBe(true); + }); + + it("should not pad a masked value, which already carries its separators", () => { + expect(isValidNcm("101.21.00")).toBe(false); + expect(isValidNcm("0101.21.00")).toBe(true); }); it("should validate an NCM code with surrounding whitespace", () => { @@ -36,7 +42,7 @@ describe("isValidNcm", () => { expect(isValidNcm("12345678")).toBe(false); }); - it("should return false when the digit count is not eight", () => { + it("should return false for a padded short value no code carries and for a wider value", () => { expect(isValidNcm("2203000")).toBe(false); expect(isValidNcm("220300000")).toBe(false); }); @@ -77,9 +83,6 @@ describe("isValidNcm", () => { describe("properties", () => { const codeArbitrary = fc.constantFrom(...NCM_CODES); - const nonZeroLeadingCodeArbitrary = fc.constantFrom( - ...NCM_CODES.filter((code) => !code.startsWith("0")), - ); test("should never throw, regardless of the input", () => { expectNeverThrows(isValidNcm, anyGarbage); @@ -96,10 +99,13 @@ describe("isValidNcm", () => { ); }); - test("should validate every known code without a leading zero when given as a number", () => { + test("should validate every known code written without its leading zeros", () => { fc.assert( - fc.property(nonZeroLeadingCodeArbitrary, (code) => { + fc.property(codeArbitrary, (code) => { + const unpadded = String(Number(code)); + expect(isValidNcm(Number(code))).toBe(true); + expect(isValidNcm(unpadded)).toBe(true); }), ); }); diff --git a/src/is-valid-ncm/is-valid-ncm.ts b/src/is-valid-ncm/is-valid-ncm.ts index 9a8859f7..ab08009c 100644 --- a/src/is-valid-ncm/is-valid-ncm.ts +++ b/src/is-valid-ncm/is-valid-ncm.ts @@ -1,7 +1,10 @@ import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; +import { padLookupCode } from "../_internals/pad-lookup-code/pad-lookup-code"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { NCM_CODES, NCM_FORMAT_REGEX } from "./constants"; +const NCM_LENGTH = 8; + let cache: Set | undefined; const getCache = (): Set => { @@ -19,9 +22,10 @@ const getCache = (): Set => { * since a sign, a decimal point or a rounded magnitude would otherwise be read as a code the * caller never wrote. * - * A bare `number` input cannot represent a code that starts with `0` (the leading zero is - * lost), so a numeric NCM code starting with `0` must be passed as a string to validate - * correctly. + * An NCM code is always 8 digits and its leading zeros are part of it, so a value written as + * bare digits is left padded with zeros to 8 whether it comes as a string or as a number: + * `1012100`, `"1012100"` and `"01012100"` are the same code. A masked value already carries its + * separators and is read as written. * * @param {string|number} value - The NCM code to be validated, with or without the * `NNNN.NN.NN` mask. @@ -31,6 +35,8 @@ const getCache = (): Set => { * ```typescript * isValidNcm("0101.21.00"); // true * isValidNcm("01012100"); // true + * isValidNcm(1012100); // true (padded to 8 digits, so this is "01012100") + * isValidNcm("1012100"); // true (padded to 8 digits, so this is "01012100") * isValidNcm("00000000"); // false * isValidNcm("abc01012100"); // false (not a documented form) * isValidNcm(-84713012); // false (not a non-negative safe integer) @@ -41,7 +47,7 @@ const getCache = (): Set => { export const isValidNcm = (value: string | number): boolean => { if (!isLookupCode(value)) return false; - const code = typeof value === "number" ? String(value) : value.trim(); + const code = padLookupCode(value, NCM_LENGTH); if (!NCM_FORMAT_REGEX.test(code)) return false; From 0e38f5e89a3f539fee8e4fe3b8cf3c3954cebef0 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 10:04:57 -0300 Subject: [PATCH 57/75] fix(validators): accept the CPF mask separators at the printed groups of the NF-e key, IBAN and card `isValidCpf` and `isValidCnpj` accept a space, a dot, a hyphen or a slash, optional and interchangeable, only between the printed groups of the document. The new validators each had their own alphabet: the NF-e key took any whitespace anywhere, the IBAN a single space only, the credit card a space or a hyphen. They now follow the CPF rule: the key at its eleven groups of 4 digits (the XML id prefix still accepted), the IBAN at the ISO 13616 groups of 4 characters, the card between any two digits since its printed grouping depends on the brand. The VIN, which has no printed grouping, accepts none and says so. `isValidCreditCard` and `isValidVin` now reject a value made of one repeated character, as every other validator does. `NfeKey.state` is renamed `stateCode`, the name the library uses for a UF code. --- docs/llms-full.txt | 49 +++++++++------ docs/llms.txt | 2 +- docs/pt-br/utilities.md | 49 +++++++++------ docs/utilities.md | 49 +++++++++------ src/_internals/constants/iban.ts | 12 ++-- src/index.test.ts | 2 + src/index.ts | 2 +- src/is-valid-cns/is-valid-cns.test.ts | 12 ++++ src/is-valid-cns/is-valid-cns.ts | 8 ++- .../is-valid-credit-card.test.ts | 43 +++++++++++++- .../is-valid-credit-card.ts | 22 +++++-- src/is-valid-iban/is-valid-iban.test.ts | 51 +++++++++++++--- src/is-valid-iban/is-valid-iban.ts | 18 +++--- src/is-valid-nfe-key/is-valid-nfe-key.test.ts | 59 +++++++++++++++++-- src/is-valid-nfe-key/is-valid-nfe-key.ts | 11 +++- src/is-valid-vin/is-valid-vin.test.ts | 18 ++++++ src/is-valid-vin/is-valid-vin.ts | 15 +++++ src/parse-iban/parse-iban.test.ts | 8 ++- src/parse-iban/parse-iban.ts | 12 ++-- src/parse-nfe-key/constants.ts | 10 +++- src/parse-nfe-key/parse-nfe-key.test.ts | 12 ++-- src/parse-nfe-key/parse-nfe-key.ts | 20 +++---- 22 files changed, 363 insertions(+), 121 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 33fbbdc8..448eceb0 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -244,7 +244,7 @@ Pick one style per util in a given app: a bundler treats the root import and the Here you will find all the utilities available for use. -> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `getBoletoInfo` returns `undefined` for an invalid boleto, the one function in the package that returns `undefined`; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function returns an empty value of its return type: every `format*` function, `capitalize`, and the string-returning `parse*` functions (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) return `""`; `parseCurrency` returns `0`; the object/tuple parsers — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — return `null`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. +> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `getBoletoInfo` returns `null` for an invalid boleto; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function returns an empty value of its return type: every `format*` function, `capitalize`, and the string-returning `parse*` functions (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) return `""`; `parseCurrency` returns `0`; the object/tuple parsers — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — return `null`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. ### isValidCpf @@ -525,6 +525,8 @@ isValidNfeKey('35170458716523000119550010000000121000123458'); // true (NF-e, SP isValidNfeKey('NFe35170458716523000119550010000000121000123458'); // true (XML Id prefix) isValidNfeKey('CTe35170458716523000119570010000000128000123452'); // true (CT-e authorised by the SVC-SP) isValidNfeKey('3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458'); // true (masked) +isValidNfeKey('3517.0458.7165.2300.0119.5500.1000.0000.1210.0012.3458'); // true (any of the mask characters) +isValidNfeKey('351 70458716523000119550010000000121000123458'); // false (a separator inside a group of 4) isValidNfeKey('99170458716523000119550010000000121000123458'); // false (invalid cUF) isValidNfeKey('35170458716523000119550010000000128000123455'); // false (the NF-e MOC does not assign tpEmis 8) isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 00000000, rule B03-10) @@ -532,28 +534,33 @@ isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 000 ### formatNfeKey -Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. Like every formatter of this package, the value is read for its digits and grouped as far as they go, so a masked or partial key still being typed is grouped progressively, and anything without a digit (an object, `true`, an object created with `Object.create(null)`) gives `''` instead of throwing. Use `isValidNfeKey` to check a key. +Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. Like every formatter of this package, the value is read for its digits and grouped as far as they go, so a masked or partial key still being typed is grouped progressively, and anything without a digit (an object, `true`, an object created with `Object.create(null)`) gives `''` instead of throwing. Use `isValidNfeKey` to check a key. `options.pad` (part of `FormatNfeKeyOptions`) left pads the value with zeros up to the 44 digits of a complete access key (default `false`). The parameter is typed as a string because 44 digits are more than a JavaScript number can hold exactly; at runtime a number is read as the string of its digits, like in every formatter of this package. ```javascript import { formatNfeKey } from '@brazilian-utils/brazilian-utils'; formatNfeKey('35170458716523000119550010000000121000123458'); // '3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458' + +formatNfeKey('12345'); // '1234 5' + +formatNfeKey('12345', { pad: true }); +// '0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 2345' ``` ### parseNfeKey -Parses a DF-e access key into its fields (state, year, month, taxId, model, series, number, emissionType, code, checkDigit). Accepts the same input forms as `isValidNfeKey` and returns `null` when the key is not valid. The result is typed as `NfeKey`, whose `model` is an `NfeKeyModel`. NFCom (`'62'`) and NF3e (`'66'`) spend position 36 of the key on `nSiteAutoriz`, the site of the authorizer that received the document, so for those two models the result also carries `authorizationSite` and `code` is 7 digits instead of 8. +Parses a DF-e access key into its fields (stateCode, year, month, taxId, model, series, number, emissionType, code, checkDigit). Accepts the same input forms as `isValidNfeKey` and returns `null` when the key is not valid. The result is typed as `NfeKey`, whose `model` is an `NfeKeyModel`. NFCom (`'62'`) and NF3e (`'66'`) spend position 36 of the key on `nSiteAutoriz`, the site of the authorizer that received the document, so for those two models the result also carries `authorizationSite` and `code` is 7 digits instead of 8. ```javascript import { parseNfeKey } from '@brazilian-utils/brazilian-utils'; parseNfeKey('35170458716523000119550010000000121000123458'); -// { state: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '55', +// { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '55', // series: 1, number: 12, emissionType: 1, code: '00012345', checkDigit: 8 } parseNfeKey('35170458716523000119620010000000121000123450'); -// { state: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', +// { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', // series: 1, number: 12, emissionType: 1, authorizationSite: 0, code: '0012345', checkDigit: 0 } parseNfeKey('invalid'); // null @@ -996,15 +1003,16 @@ getBankByIspb('99999999'); // null ### isValidIban -Check if a Brazilian IBAN (International Bank Account Number) is valid, per Bacen's [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf) (Circular BCB nº 3.625/2013): `BR` + 2 ISO 7064 MOD 97-10 check digits + 8 digit ISPB + 5 digit branch + 10 digit account + 1 letter account type (any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 owner indicator (`1` for the first or only holder up to `9` for the ninth, then `A` to `Z` from the tenth, so `0` is rejected), 29 characters total. Only Brazilian IBANs (country code `BR`) are recognized; any other country returns `false`, since this package does not carry the field layout of the other 90+ ISO 13616 countries. Is case-insensitive and accepts both forms an IBAN is written in: compact (`'BR1500000000000010932840814P2'`) or in the ISO 13616 print format, letters and digits in groups separated by a single space, with optional surrounding whitespace either way. Only a character outside letters and digits, or a separator other than a single space, makes the value something other than an IBAN, so it is rejected instead of being stripped. +Check if a Brazilian IBAN (International Bank Account Number) is valid, per Bacen's [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf) (Circular BCB nº 3.625/2013): `BR` + 2 ISO 7064 MOD 97-10 check digits + 8 digit ISPB + 5 digit branch + 10 digit account + 1 letter account type (any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 owner indicator (`1` for the first or only holder up to `9` for the ninth, then `A` to `Z` from the tenth, so `0` is rejected), 29 characters total. Only Brazilian IBANs (country code `BR`) are recognized; any other country returns `false`, since this package does not carry the field layout of the other 90+ ISO 13616 countries. Is case-insensitive and accepts both forms an IBAN is written in: compact (`'BR1500000000000010932840814P2'`) or in the ISO 13616 print format, letters and digits in groups of 4 (the last one shorter), with optional surrounding whitespace either way. The groups may be split by whitespace, `.`, `-` or `/`, the interchangeable mask characters `isValidCpf` and `isValidCnpj` accept. Only a separator away from a group boundary, a run of separators (ISO 13616 prints a single one) or a character outside letters and digits makes the value something other than an IBAN, so it is rejected instead of being stripped. ```javascript import { isValidIban } from '@brazilian-utils/brazilian-utils'; isValidIban('BR1500000000000010932840814P2'); // true isValidIban('BR15 0000 0000 0000 1093 2840 814P 2'); // true (grouping spaces) +isValidIban('BR15-0000-0000-0000-1093-2840-814P-2'); // true (any of the mask characters) isValidIban('BR1500000000000010932840814P3'); // false (bad check digits) -isValidIban('BR1500000000000010932840814P-2'); // false (hyphens are not part of an IBAN) +isValidIban('BR15 000 00000 0000 1093 2840 814P 2'); // false (a separator inside a group) isValidIban('DE89370400440532013000'); // false (non Brazilian IBAN) ``` @@ -1023,7 +1031,7 @@ formatIban('BR15 0000-0000.0000/1093 2840 814P-2'); // 'BR15 0000 0000 0000 1093 ### parseIban -Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups separated by a single space), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying any character other than letters, digits and those single grouping spaces. The result is typed as `Iban`, whose `accountType` is a `string`. +Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups of 4 split by a single whitespace, `.`, `-` or `/`), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying a separator away from a group boundary, a run of separators or any character other than letters and digits. The result is typed as `Iban`, whose `accountType` is a `string`. ```javascript import { parseIban } from '@brazilian-utils/brazilian-utils'; @@ -1040,12 +1048,12 @@ parseIban('BR1500000000000010932840814P2'); // } parseIban('DE89370400440532013000'); // null (non Brazilian IBAN) -parseIban('BR1500000000000010932840814P-2'); // null (hyphens are not part of an IBAN) +parseIban('BR15 000 00000 0000 1093 2840 814P 2'); // null (a separator inside a group) ``` ### isValidCreditCard -Check if a payment card number is valid using the Luhn algorithm ([ISO/IEC 7812-1](https://www.iso.org/standard/70484.html)). Accepts the usual mask characters (spaces, hyphens) between digits and whitespace around the value; any other character makes the value invalid. Performs no brand detection (Visa, Mastercard, Amex...), issuer range lookup or expiration/CVV checks, only the digit count (12 to 19) and the Luhn check digit. A `number` is only accepted when it is a non-negative safe integer: anything above `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 digits) has already been rounded to a different number before the function sees it, so pass a longer PAN as a string. +Check if a payment card number is valid using the Luhn algorithm ([ISO/IEC 7812-1](https://www.iso.org/standard/70484.html)). Accepts the usual mask characters (whitespace, `.`, `-` and `/`, the interchangeable set `isValidCpf` and `isValidCnpj` accept) between any two digits and whitespace around the value; any other character makes the value invalid. They are accepted between any two digits rather than at fixed positions because the printed grouping of a PAN changes with the brand (4-4-4-4 for Visa and Mastercard, 4-6-5 for American Express, 4-6-4 for Diners Club), so there is no single layout to pin them to. Performs no brand detection (Visa, Mastercard, Amex...), issuer range lookup or expiration/CVV checks, only the digit count (12 to 19) and the Luhn check digit. A `number` is only accepted when it is a non-negative safe integer: anything above `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 digits) has already been rounded to a different number before the function sees it, so pass a longer PAN as a string. A value whose digits are all the same (`'0000000000000000'`) is rejected even when it passes the Luhn check, the way every other validator of this package rejects a repeated-digit document (`isValidCpf('00000000000')`, `isValidCns`, `isValidCaepf`, `isValidCei`). ```javascript import { isValidCreditCard } from '@brazilian-utils/brazilian-utils'; @@ -1054,7 +1062,9 @@ isValidCreditCard('4111111111111111'); // true (Visa test number) isValidCreditCard('5555555555554444'); // true (Mastercard test number) isValidCreditCard('378282246310005'); // true (American Express test number) isValidCreditCard('4111 1111 1111 1111'); // true (spaced mask) +isValidCreditCard('4111.1111/1111-1111'); // true (any of the mask characters) isValidCreditCard('4111111111111112'); // false (bad check digit) +isValidCreditCard('0000000000000000'); // false (every digit the same, though the Luhn check passes) isValidCreditCard('4111a1111b1111c1111'); // false (letters between the digits) isValidCreditCard(4111111111111111111); // false (above 2^53 - 1, pass it as a string) ``` @@ -1889,7 +1899,7 @@ parseVoterId('1234 5678 8 01 91'); // '1234567880191' (13-digit SP/MG voter id) ### isValidCns -Check if a CNS (Cartão Nacional de Saúde) number is valid, the unique SUS (Sistema Único de Saúde) user identifier. Definitive cards (starting with 1 or 2) are validated over an embedded 11 digit PIS/PASEP/NIS derived base weighted 15 down to 5; when the raw digit computes to 10, DATASUS raises the weighted sum by 2, recomputes the digit and marks the card with the suffix `001` instead of `000`. Provisional cards (starting with 7, 8 or 9) are validated instead by a single weighted sum (weights 15 down to 1) that must be a multiple of 11. The value has to be written as the 15 digits, optionally split into the printed groups of 3-4-4-4 by whitespace or the usual mask characters, a run of them between two groups included; letters among the digits are rejected instead of being read past. +Check if a CNS (Cartão Nacional de Saúde) number is valid, the unique SUS (Sistema Único de Saúde) user identifier. Definitive cards (starting with 1 or 2) are validated over an embedded 11 digit PIS/PASEP/NIS derived base weighted 15 down to 5; when the raw digit computes to 10, DATASUS raises the weighted sum by 2, recomputes the digit and marks the card with the suffix `001` instead of `000`. Provisional cards (starting with 7, 8 or 9) are validated instead by a single weighted sum (weights 15 down to 1) that must be a multiple of 11. The value has to be written as the 15 digits, optionally split into the printed groups of 3-4-4-4 by whitespace, `.`, `-` or `/`, the interchangeable mask characters `isValidCpf` and `isValidCnpj` accept, a run of them between two groups included; letters among the digits, or a separator inside a group, are rejected instead of being read past. The two routines come from the [ANVISA CNS validation page](https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/), which sits behind a bot filter and answers HTTP 403 to non-browser clients. The [e-SUS APS page](https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html) documents the same algorithm and is reachable without a browser, but applies the provisional routine to numbers starting with 5, 7, 8 or 9; this implementation follows ANVISA and rejects a 5-prefixed number even when its weighted sum checks out. @@ -1898,6 +1908,7 @@ import { isValidCns } from '@brazilian-utils/brazilian-utils'; isValidCns('123456789010000'); // true (definitive) isValidCns('700000000000005'); // true (provisional) +isValidCns('123.4567-8901/0000'); // true (any of the mask characters) isValidCns('12345678901'); // false (wrong length) isValidCns('abc123456789010000'); // false (not written as a CNS) ``` @@ -2064,22 +2075,22 @@ formatCaepf('184', { pad: true }); // 000.000.000/001-84 ### isValidRegistroProfissional -Check the structure of a professional council registration number (registro/inscrição profissional). Options are typed as `IsValidRegistroProfissionalOptions`: `options.council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `options.stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits, the tipo de registro (`"O"` Originário or `"P"` Provisório, which says nothing about the professional category) and the check digit, as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). A Registro Transferido or Secundário appends `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, per that same item and [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: the Manual's own examples are `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` and `PI-111222/O-5 S-AC`. Both UFs must be real state codes, and `options.stateCode` is compared against the originating one. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. Only the CRC shape and those CRP regional codes rest on a published source: the CFP page publishes no length for the inscription number itself, and the OAB, the CFM and the CFO publish no format at all, so the digit ranges accepted for `"CRP"`, `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative (the OAB/SP public search field is `maxlength="7"`, and the CFM documents `300`-prefixed and `P`-suffixed CRMs, none of which these shapes express). CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). +Check the structure of a professional council registration number (registro/inscrição profissional). It takes a single object, typed as `IsValidRegistroProfissionalOptions`, the shape `isValidBankAccount` takes: `value` is the registration number, `council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). Anything that is not an object, and an object missing `value` or `council`, is `false`. This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits, the tipo de registro (`"O"` Originário or `"P"` Provisório, which says nothing about the professional category) and the check digit, as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). A Registro Transferido or Secundário appends `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, per that same item and [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: the Manual's own examples are `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` and `PI-111222/O-5 S-AC`. Both UFs must be real state codes, and `stateCode` is compared against the originating one. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. Only the CRC shape and those CRP regional codes rest on a published source: the CFP page publishes no length for the inscription number itself, and the OAB, the CFM and the CFO publish no format at all, so the digit ranges accepted for `"CRP"`, `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative (the OAB/SP public search field is `maxlength="7"`, and the CFM documents `300`-prefixed and `P`-suffixed CRMs, none of which these shapes express). CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). ```javascript import { isValidRegistroProfissional } from '@brazilian-utils/brazilian-utils'; -isValidRegistroProfissional('123456/SP', { council: 'OAB' }); // true -isValidRegistroProfissional('123456-RJ', { council: 'OAB', stateCode: 'SP' }); // false (UF mismatch) -isValidRegistroProfissional('06/12345', { council: 'CRP' }); // true -isValidRegistroProfissional('SP-123456/O-3', { council: 'CRC' }); // true -isValidRegistroProfissional('SP-123456/O-3 T-MG', { council: 'CRC' }); // true (registro transferido) -isValidRegistroProfissional('SP-123456/T-3', { council: 'CRC' }); // false ("T" is not a tipo de registro) +isValidRegistroProfissional({ value: '123456/SP', council: 'OAB' }); // true +isValidRegistroProfissional({ value: '123456-RJ', council: 'OAB', stateCode: 'SP' }); // false (UF mismatch) +isValidRegistroProfissional({ value: '06/12345', council: 'CRP' }); // true +isValidRegistroProfissional({ value: 'SP-123456/O-3', council: 'CRC' }); // true +isValidRegistroProfissional({ value: 'SP-123456/O-3 T-MG', council: 'CRC' }); // true (registro transferido) +isValidRegistroProfissional({ value: 'SP-123456/T-3', council: 'CRC' }); // false ("T" is not a tipo de registro) ``` ### isValidVin -Check if a VIN (Vehicle Identification Number / chassi) is valid. Checks the length (17 characters), the excluded letters (`I`, `O`, `Q` are never valid; [ISO 3779:2009](https://www.iso.org/standard/52200.html) structure) and the check digit at the 9th position, with the check digit and transliteration computed per [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15). That check digit is a North-American requirement (49 CFR 565.15 / SAE J853): [Resolução CONTRAN nº 968/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf) (which revoked Resolução CONTRAN nº 24/1998 from 1 January 2025) and ABNT NBR 6066 define the Brazilian VIN structure but do not mandate it, so many Brazilian-built VINs do not carry a matching check digit. This function is therefore a North-American-style structural check, not a universal validator of Brazilian VINs. Case-insensitive and trims surrounding whitespace. +Check if a VIN (Vehicle Identification Number / chassi) is valid. Checks the length (17 characters), the excluded letters (`I`, `O`, `Q` are never valid; [ISO 3779:2009](https://www.iso.org/standard/52200.html) structure) and the check digit at the 9th position, with the check digit and transliteration computed per [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15). That check digit is a North-American requirement (49 CFR 565.15 / SAE J853): [Resolução CONTRAN nº 968/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf) (which revoked Resolução CONTRAN nº 24/1998 from 1 January 2025) and ABNT NBR 6066 define the Brazilian VIN structure but do not mandate it, so many Brazilian-built VINs do not carry a matching check digit. This function is therefore a North-American-style structural check, not a universal validator of Brazilian VINs. Case-insensitive and trims surrounding whitespace. A VIN is printed as one unbroken run of 17 characters, so, unlike the documents this package masks (`isValidCpf`, `isValidCnpj`, `isValidNfeKey`), it has no group boundary to write a separator at and none is accepted: a space, `.`, `-` or `/` among the characters is rejected instead of being stripped. A value whose 17 characters are all the same (`'00000000000000000'`) is rejected even when it carries a matching check digit, the way every other validator of this package rejects a repeated-digit document. ```javascript import { isValidVin } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/llms.txt b/docs/llms.txt index 01614852..ba1a6d1a 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -94,7 +94,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [parseBoleto](https://brazilian-utils.com.br/utilities.md#parseboleto): Remove boleto formatting, keep only digits, and cap the result to 47 digits (48 for boleto de arrecadação). - [parsePixKey](https://brazilian-utils.com.br/utilities.md#parsepixkey): Identifies a Pix key and normalizes it to the canonical form the DICT expects inside a BR Code: 11 digit CPF, 14 character CNPJ, lowercased e-mail, E.164 mobile phone (a landline is not a Pix key) or lowercase UUID EVP. - [parsePixPayload](https://brazilian-utils.com.br/utilities.md#parsepixpayload): Parses a Pix BR Code payload into its fields. -- [parseNfeKey](https://brazilian-utils.com.br/utilities.md#parsenfekey): Parses a DF-e access key into its fields (state, year, month, taxId, model, series, number, emissionType, code, checkDigit). +- [parseNfeKey](https://brazilian-utils.com.br/utilities.md#parsenfekey): Parses a DF-e access key into its fields (stateCode, year, month, taxId, model, series, number, emissionType, code, checkDigit). - [parsePhone](https://brazilian-utils.com.br/utilities.md#parsephone): Remove phone formatting, keep only digits, and cap the result to 11 digits. - [parsePis](https://brazilian-utils.com.br/utilities.md#parsepis): Remove PIS formatting, keep only digits, and cap the result to 11 digits. - [parseCep](https://brazilian-utils.com.br/utilities.md#parsecep): Remove CEP formatting, keep only digits, and cap the result to 8 digits. diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index d2aad681..402fb74f 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -2,7 +2,7 @@ Aqui você encontrará todos os utilitários disponíveis para uso. -> **Tratamento de entrada:** nenhuma função pública síncrona lança exceção com `null`/`undefined` ou um valor de tipo incorreto; as duas funções de rede, `getAddressInfoByCep` e `getCepInfoByAddress`, rejeitam com seus erros tipados (veja as seções delas). Os validadores (`isValid*`) retornam `false`; `isHoliday` retorna `false`; `getHolidays` retorna `[]`; `getBoletoInfo` retorna `undefined` para um boleto inválido, a única função do pacote que retorna `undefined`; `generateProcessoJuridico` retorna `null`; `getMunicipality` retorna `null` para uma busca malformada/sem correspondência. Todas as demais funções `format*`/`parse*` retornam um valor vazio do seu tipo de retorno: toda função `format*`, `capitalize`, e as funções `parse*` que retornam string (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) retornam `""`; `parseCurrency` retorna `0`; os parsers que retornam objeto/tupla — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — retornam `null`. `formatCurrency` retorna `""` para um número não finito e para um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo); `null`, arrays e booleanos passam por `Number()` como no 2.3.0. A única exceção à promessa acima: um objeto criado com `Object.create(null)` não tem `toString`, então as funções `format*`/`parse*` que leem a entrada como texto ainda lançam um `TypeError` para ele, exatamente como na 2.3.0. +> **Tratamento de entrada:** nenhuma função pública síncrona lança exceção com `null`/`undefined` ou um valor de tipo incorreto; as duas funções de rede, `getAddressInfoByCep` e `getCepInfoByAddress`, rejeitam com seus erros tipados (veja as seções delas). Os validadores (`isValid*`) retornam `false`; `isHoliday` retorna `false`; `getHolidays` retorna `[]`; `getBoletoInfo` retorna `null` para um boleto inválido; `generateProcessoJuridico` retorna `null`; `getMunicipality` retorna `null` para uma busca malformada/sem correspondência. Todas as demais funções `format*`/`parse*` retornam um valor vazio do seu tipo de retorno: toda função `format*`, `capitalize`, e as funções `parse*` que retornam string (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) retornam `""`; `parseCurrency` retorna `0`; os parsers que retornam objeto/tupla — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — retornam `null`. `formatCurrency` retorna `""` para um número não finito e para um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo); `null`, arrays e booleanos passam por `Number()` como no 2.3.0. A única exceção à promessa acima: um objeto criado com `Object.create(null)` não tem `toString`, então as funções `format*`/`parse*` que leem a entrada como texto ainda lançam um `TypeError` para ele, exatamente como na 2.3.0. ## isValidCpf @@ -283,6 +283,8 @@ isValidNfeKey('35170458716523000119550010000000121000123458'); // true (NF-e, SP isValidNfeKey('NFe35170458716523000119550010000000121000123458'); // true (prefixo Id do XML) isValidNfeKey('CTe35170458716523000119570010000000128000123452'); // true (CT-e autorizado pela SVC-SP) isValidNfeKey('3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458'); // true (com máscara) +isValidNfeKey('3517.0458.7165.2300.0119.5500.1000.0000.1210.0012.3458'); // true (qualquer um dos caracteres de máscara) +isValidNfeKey('351 70458716523000119550010000000121000123458'); // false (separador dentro de um grupo de 4) isValidNfeKey('99170458716523000119550010000000121000123458'); // false (cUF inválido) isValidNfeKey('35170458716523000119550010000000128000123455'); // false (o MOC da NF-e não atribui tpEmis 8) isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 00000000, regra B03-10) @@ -290,28 +292,33 @@ isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 000 ## formatNfeKey -Formata uma chave de acesso de DF-e (Documento Fiscal eletrônico) em grupos de 4 dígitos separados por espaço, a forma em que todo documento auxiliar a imprime: o DANFE da NF-e e da NFC-e, o DACTE do CT-e, do CT-e OS e da GTV-e, o DAMDFE do MDF-e, o DABPE do BP-e, o DANF3E da NF3e e o DANFE-COM da NFCom. Como todo formatador deste pacote, o valor é lido pelos seus dígitos e agrupado até onde eles vão, então uma chave com máscara ou parcial, ainda sendo digitada, é agrupada progressivamente, e qualquer coisa sem dígito (um objeto, `true`, um objeto criado com `Object.create(null)`) devolve `''` em vez de lançar. Use `isValidNfeKey` para verificar uma chave. +Formata uma chave de acesso de DF-e (Documento Fiscal eletrônico) em grupos de 4 dígitos separados por espaço, a forma em que todo documento auxiliar a imprime: o DANFE da NF-e e da NFC-e, o DACTE do CT-e, do CT-e OS e da GTV-e, o DAMDFE do MDF-e, o DABPE do BP-e, o DANF3E da NF3e e o DANFE-COM da NFCom. Como todo formatador deste pacote, o valor é lido pelos seus dígitos e agrupado até onde eles vão, então uma chave com máscara ou parcial, ainda sendo digitada, é agrupada progressivamente, e qualquer coisa sem dígito (um objeto, `true`, um objeto criado com `Object.create(null)`) devolve `''` em vez de lançar. Use `isValidNfeKey` para verificar uma chave. O `options.pad` (parte de `FormatNfeKeyOptions`) preenche o valor com zeros à esquerda até os 44 dígitos de uma chave de acesso completa (padrão `false`). O parâmetro é tipado como string porque 44 dígitos são mais do que um número JavaScript comporta com exatidão; em tempo de execução um número é lido como a string dos seus dígitos, como em todo formatador deste pacote. ```javascript import { formatNfeKey } from '@brazilian-utils/brazilian-utils'; formatNfeKey('35170458716523000119550010000000121000123458'); // '3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458' + +formatNfeKey('12345'); // '1234 5' + +formatNfeKey('12345', { pad: true }); +// '0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 2345' ``` ## parseNfeKey -Interpreta uma chave de acesso de DF-e e retorna seus campos (state, year, month, taxId, model, series, number, emissionType, code, checkDigit). Aceita as mesmas formas de entrada do `isValidNfeKey` e retorna `null` quando a chave não é válida. O resultado é tipado como `NfeKey`, cujo `model` é um `NfeKeyModel`. A NFCom (`'62'`) e a NF3e (`'66'`) gastam a posição 36 da chave com o `nSiteAutoriz`, o site do autorizador que recebeu o documento, então para esses dois modelos o resultado também traz `authorizationSite` e o `code` tem 7 dígitos em vez de 8. +Interpreta uma chave de acesso de DF-e e retorna seus campos (stateCode, year, month, taxId, model, series, number, emissionType, code, checkDigit). Aceita as mesmas formas de entrada do `isValidNfeKey` e retorna `null` quando a chave não é válida. O resultado é tipado como `NfeKey`, cujo `model` é um `NfeKeyModel`. A NFCom (`'62'`) e a NF3e (`'66'`) gastam a posição 36 da chave com o `nSiteAutoriz`, o site do autorizador que recebeu o documento, então para esses dois modelos o resultado também traz `authorizationSite` e o `code` tem 7 dígitos em vez de 8. ```javascript import { parseNfeKey } from '@brazilian-utils/brazilian-utils'; parseNfeKey('35170458716523000119550010000000121000123458'); -// { state: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '55', +// { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '55', // series: 1, number: 12, emissionType: 1, code: '00012345', checkDigit: 8 } parseNfeKey('35170458716523000119620010000000121000123450'); -// { state: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', +// { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', // series: 1, number: 12, emissionType: 1, authorizationSite: 0, code: '0012345', checkDigit: 0 } parseNfeKey('invalid'); // null @@ -754,15 +761,16 @@ getBankByIspb('99999999'); // null ## isValidIban -Valida se um IBAN (International Bank Account Number) brasileiro é válido, conforme as [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf) do Bacen (Circular BCB nº 3.625/2013): `BR` + 2 dígitos verificadores ISO 7064 MOD 97-10 + 8 dígitos de ISPB + 5 dígitos de agência + 10 dígitos de conta + 1 letra de tipo de conta (qualquer letra, normalmente `C` para conta corrente ou `P` para conta poupança) + 1 indicador de titularidade (`1` para o primeiro ou único titular até `9` para o nono, depois `A` a `Z` a partir do décimo, então `0` é rejeitado), totalizando 29 caracteres. Somente IBANs brasileiros (código de país `BR`) são reconhecidos; qualquer outro país retorna `false`, já que este pacote não conhece o layout de campos dos outros mais de 90 países da ISO 13616. Não diferencia maiúsculas de minúsculas e aceita as duas formas em que um IBAN é escrito: compacta (`'BR1500000000000010932840814P2'`) ou no formato impresso da ISO 13616, letras e dígitos em grupos separados por um único espaço, em ambos os casos com espaços em branco opcionais no início e no fim. Apenas um caractere fora de letras e dígitos, ou um separador diferente de um único espaço, faz do valor algo que não é um IBAN, então ele é rejeitado em vez de removido. +Valida se um IBAN (International Bank Account Number) brasileiro é válido, conforme as [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf) do Bacen (Circular BCB nº 3.625/2013): `BR` + 2 dígitos verificadores ISO 7064 MOD 97-10 + 8 dígitos de ISPB + 5 dígitos de agência + 10 dígitos de conta + 1 letra de tipo de conta (qualquer letra, normalmente `C` para conta corrente ou `P` para conta poupança) + 1 indicador de titularidade (`1` para o primeiro ou único titular até `9` para o nono, depois `A` a `Z` a partir do décimo, então `0` é rejeitado), totalizando 29 caracteres. Somente IBANs brasileiros (código de país `BR`) são reconhecidos; qualquer outro país retorna `false`, já que este pacote não conhece o layout de campos dos outros mais de 90 países da ISO 13616. Não diferencia maiúsculas de minúsculas e aceita as duas formas em que um IBAN é escrito: compacta (`'BR1500000000000010932840814P2'`) ou no formato impresso da ISO 13616, letras e dígitos em grupos de 4 (o último menor), em ambos os casos com espaços em branco opcionais no início e no fim. Os grupos podem ser separados por espaço em branco, `.`, `-` ou `/`, os caracteres de máscara intercambiáveis que `isValidCpf` e `isValidCnpj` aceitam. Apenas um separador fora do limite de um grupo, uma sequência de separadores (a ISO 13616 imprime um único) ou um caractere fora de letras e dígitos faz do valor algo que não é um IBAN, então ele é rejeitado em vez de removido. ```javascript import { isValidIban } from '@brazilian-utils/brazilian-utils'; isValidIban('BR1500000000000010932840814P2'); // true isValidIban('BR15 0000 0000 0000 1093 2840 814P 2'); // true (espaços de agrupamento) +isValidIban('BR15-0000-0000-0000-1093-2840-814P-2'); // true (qualquer um dos caracteres de máscara) isValidIban('BR1500000000000010932840814P3'); // false (dígitos verificadores inválidos) -isValidIban('BR1500000000000010932840814P-2'); // false (hífen não faz parte de um IBAN) +isValidIban('BR15 000 00000 0000 1093 2840 814P 2'); // false (separador dentro de um grupo) isValidIban('DE89370400440532013000'); // false (IBAN não brasileiro) ``` @@ -781,7 +789,7 @@ formatIban('BR15 0000-0000.0000/1093 2840 814P-2'); // 'BR15 0000 0000 0000 1093 ## parseIban -Interpreta um IBAN brasileiro em seus campos: 2 (código do país, sempre `BR`) + 2 (dígitos verificadores ISO 7064 MOD 97-10) + 8 (ISPB) + 5 (agência) + 10 (conta) + 1 (tipo de conta, qualquer letra, normalmente `C` para conta corrente ou `P` para conta poupança) + 1 (indicador do titular, `1` a `9` e depois `A` a `Z`). Aceita as mesmas formas de entrada que `isValidIban`, compacta ou no formato impresso da ISO 13616 (grupos separados por um único espaço), em ambos os casos com espaços em branco opcionais no início e no fim e sem diferenciar maiúsculas de minúsculas, e retorna `null` sempre que `isValidIban` retornaria `false`, inclusive quando o valor carrega qualquer caractere além de letras, dígitos e esses espaços de agrupamento. O resultado é tipado como `Iban`, cujo `accountType` é uma `string`. +Interpreta um IBAN brasileiro em seus campos: 2 (código do país, sempre `BR`) + 2 (dígitos verificadores ISO 7064 MOD 97-10) + 8 (ISPB) + 5 (agência) + 10 (conta) + 1 (tipo de conta, qualquer letra, normalmente `C` para conta corrente ou `P` para conta poupança) + 1 (indicador do titular, `1` a `9` e depois `A` a `Z`). Aceita as mesmas formas de entrada que `isValidIban`, compacta ou no formato impresso da ISO 13616 (grupos de 4 separados por um único espaço em branco, `.`, `-` ou `/`), em ambos os casos com espaços em branco opcionais no início e no fim e sem diferenciar maiúsculas de minúsculas, e retorna `null` sempre que `isValidIban` retornaria `false`, inclusive quando o valor carrega um separador fora do limite de um grupo, uma sequência de separadores ou qualquer caractere além de letras e dígitos. O resultado é tipado como `Iban`, cujo `accountType` é uma `string`. ```javascript import { parseIban } from '@brazilian-utils/brazilian-utils'; @@ -798,12 +806,12 @@ parseIban('BR1500000000000010932840814P2'); // } parseIban('DE89370400440532013000'); // null (IBAN não brasileiro) -parseIban('BR1500000000000010932840814P-2'); // null (hífen não faz parte de um IBAN) +parseIban('BR15 000 00000 0000 1093 2840 814P 2'); // null (separador dentro de um grupo) ``` ## isValidCreditCard -Valida se um número de cartão de pagamento é válido usando o algoritmo de Luhn ([ISO/IEC 7812-1](https://www.iso.org/standard/70484.html)). Aceita os caracteres de máscara usuais (espaços, hifens) entre os dígitos e espaços ao redor do valor; qualquer outro caractere invalida o valor. Não faz detecção de bandeira (Visa, Mastercard, Amex...), consulta de faixa de emissor nem validação de validade/CVV, verifica apenas a quantidade de dígitos (12 a 19) e o dígito verificador de Luhn. Um `number` só é aceito quando é um inteiro seguro não negativo: qualquer valor acima de `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 dígitos) já chega arredondado para outro número, então passe cartões mais longos como string. +Valida se um número de cartão de pagamento é válido usando o algoritmo de Luhn ([ISO/IEC 7812-1](https://www.iso.org/standard/70484.html)). Aceita os caracteres de máscara usuais (espaço em branco, `.`, `-` e `/`, o conjunto intercambiável que `isValidCpf` e `isValidCnpj` aceitam) entre dois dígitos quaisquer e espaços ao redor do valor; qualquer outro caractere invalida o valor. Eles são aceitos entre dois dígitos quaisquer, e não em posições fixas, porque o agrupamento impresso de um PAN muda com a bandeira (4-4-4-4 para Visa e Mastercard, 4-6-5 para American Express, 4-6-4 para Diners Club), então não há um único leiaute ao qual prendê-los. Não faz detecção de bandeira (Visa, Mastercard, Amex...), consulta de faixa de emissor nem validação de validade/CVV, verifica apenas a quantidade de dígitos (12 a 19) e o dígito verificador de Luhn. Um `number` só é aceito quando é um inteiro seguro não negativo: qualquer valor acima de `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 dígitos) já chega arredondado para outro número, então passe cartões mais longos como string. Um valor cujos dígitos são todos iguais (`'0000000000000000'`) é rejeitado mesmo passando no cálculo de Luhn, do jeito que todo outro validador deste pacote rejeita um documento de dígitos repetidos (`isValidCpf('00000000000')`, `isValidCns`, `isValidCaepf`, `isValidCei`). ```javascript import { isValidCreditCard } from '@brazilian-utils/brazilian-utils'; @@ -812,7 +820,9 @@ isValidCreditCard('4111111111111111'); // true (número de teste Visa) isValidCreditCard('5555555555554444'); // true (número de teste Mastercard) isValidCreditCard('378282246310005'); // true (número de teste American Express) isValidCreditCard('4111 1111 1111 1111'); // true (máscara com espaços) +isValidCreditCard('4111.1111/1111-1111'); // true (qualquer um dos caracteres de máscara) isValidCreditCard('4111111111111112'); // false (dígito verificador inválido) +isValidCreditCard('0000000000000000'); // false (todos os dígitos iguais, ainda que o Luhn feche) isValidCreditCard('4111a1111b1111c1111'); // false (letras entre os dígitos) isValidCreditCard(4111111111111111111); // false (acima de 2^53 - 1, passe como string) ``` @@ -1647,7 +1657,7 @@ parseVoterId('1234 5678 8 01 91'); // '1234567880191' (título de 13 dígitos SP ## isValidCns -Verifica se um número de CNS (Cartão Nacional de Saúde) é válido, o identificador único do usuário do SUS (Sistema Único de Saúde). Cartões definitivos (iniciados em 1 ou 2) são validados sobre uma base embutida de 11 dígitos derivada do PIS/PASEP/NIS, ponderada de 15 até 5; quando o dígito bruto resulta em 10, o DATASUS soma 2 à soma ponderada, recalcula o dígito e marca o cartão com o sufixo `001` em vez de `000`. Cartões provisórios (iniciados em 7, 8 ou 9) são validados por uma soma ponderada única (pesos de 15 a 1) que deve ser múltipla de 11. O valor precisa vir escrito como os 15 dígitos, opcionalmente separados nos grupos impressos de 3-4-4-4 por espaços ou pelos caracteres de máscara usuais, inclusive uma sequência deles entre dois grupos; letras no meio dos dígitos são rejeitadas em vez de ignoradas. +Verifica se um número de CNS (Cartão Nacional de Saúde) é válido, o identificador único do usuário do SUS (Sistema Único de Saúde). Cartões definitivos (iniciados em 1 ou 2) são validados sobre uma base embutida de 11 dígitos derivada do PIS/PASEP/NIS, ponderada de 15 até 5; quando o dígito bruto resulta em 10, o DATASUS soma 2 à soma ponderada, recalcula o dígito e marca o cartão com o sufixo `001` em vez de `000`. Cartões provisórios (iniciados em 7, 8 ou 9) são validados por uma soma ponderada única (pesos de 15 a 1) que deve ser múltipla de 11. O valor precisa vir escrito como os 15 dígitos, opcionalmente separados nos grupos impressos de 3-4-4-4 por espaço em branco, `.`, `-` ou `/`, os caracteres de máscara intercambiáveis que `isValidCpf` e `isValidCnpj` aceitam, inclusive uma sequência deles entre dois grupos; letras no meio dos dígitos, ou um separador dentro de um grupo, são rejeitadas em vez de ignoradas. As duas rotinas vêm da [página de validação de CNS da ANVISA](https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/), que fica atrás de um filtro de bots e responde HTTP 403 a clientes que não sejam navegadores. A [página do e-SUS APS](https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html) documenta o mesmo algoritmo e é acessível sem navegador, mas aplica a rotina de provisórios a números iniciados em 5, 7, 8 ou 9; esta implementação segue a ANVISA e rejeita um número iniciado em 5 mesmo quando a soma ponderada fecha. @@ -1656,6 +1666,7 @@ import { isValidCns } from '@brazilian-utils/brazilian-utils'; isValidCns('123456789010000'); // true (definitivo) isValidCns('700000000000005'); // true (provisório) +isValidCns('123.4567-8901/0000'); // true (qualquer um dos caracteres de máscara) isValidCns('12345678901'); // false (tamanho inválido) isValidCns('abc123456789010000'); // false (não escrito como um CNS) ``` @@ -1822,22 +1833,22 @@ formatCaepf('184', { pad: true }); // 000.000.000/001-84 ## isValidRegistroProfissional -Verifica a estrutura de um número de registro/inscrição profissional. As opções são tipadas como `IsValidRegistroProfissionalOptions`: `options.council` escolhe o conselho emissor (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` ou `"CRC"`) e o `options.stateCode` opcional verifica a UF embutida (ignorado para `"CRP"`, cujo prefixo de 2 dígitos é um código regional, não uma UF literal). É apenas uma verificação estrutural: a quantidade de dígitos e a UF são validadas, mas nenhum dígito verificador é calculado, mesmo para o CRC, cujo formato inclui um. Um registro no CRC é a UF, 6 dígitos, o tipo de registro (`"O"` Originário ou `"P"` Provisório, que nada diz sobre a categoria profissional) e o dígito verificador, conforme o [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). Um Registro Transferido ou Secundário acrescenta `"T"` ou `"S"` e a UF do CRC de destino **depois** do dígito verificador, conforme esse mesmo item e a [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: os exemplos do próprio Manual são `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` e `PI-111222/O-5 S-AC`. As duas UFs precisam ser códigos reais, e o `options.stateCode` é comparado com a de origem. O código regional do CRP precisa ser um dos [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) do sistema CFP, de CRP-01 a CRP-24. Só o formato do CRC e esses códigos regionais do CRP se apoiam em fonte publicada: a página do CFP não publica o tamanho do número de inscrição, e a OAB, o CFM e o CFO não publicam formato algum, então as faixas de dígitos aceitas para `"CRP"`, `"OAB"`, `"CRM"` e `"CRO"` são convencionais, não normativas (a busca pública da OAB/SP tem `maxlength="7"`, e o CFM documenta CRMs com prefixo `300` e sufixo `P`, nenhum deles expresso por esses formatos). O CREA não é suportado: seu formato de registro não pôde ser confirmado em uma fonte oficial e publicamente documentada após a unificação nacional de 2016 (RNP). +Verifica a estrutura de um número de registro/inscrição profissional. Recebe um único objeto, tipado como `IsValidRegistroProfissionalOptions`, no mesmo formato do `isValidBankAccount`: `value` é o número do registro, `council` escolhe o conselho emissor (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` ou `"CRC"`) e o `stateCode` opcional verifica a UF embutida (ignorado para `"CRP"`, cujo prefixo de 2 dígitos é um código regional, não uma UF literal). Qualquer coisa que não seja um objeto, e um objeto sem `value` ou sem `council`, é `false`. É apenas uma verificação estrutural: a quantidade de dígitos e a UF são validadas, mas nenhum dígito verificador é calculado, mesmo para o CRC, cujo formato inclui um. Um registro no CRC é a UF, 6 dígitos, o tipo de registro (`"O"` Originário ou `"P"` Provisório, que nada diz sobre a categoria profissional) e o dígito verificador, conforme o [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). Um Registro Transferido ou Secundário acrescenta `"T"` ou `"S"` e a UF do CRC de destino **depois** do dígito verificador, conforme esse mesmo item e a [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: os exemplos do próprio Manual são `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` e `PI-111222/O-5 S-AC`. As duas UFs precisam ser códigos reais, e o `stateCode` é comparado com a de origem. O código regional do CRP precisa ser um dos [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) do sistema CFP, de CRP-01 a CRP-24. Só o formato do CRC e esses códigos regionais do CRP se apoiam em fonte publicada: a página do CFP não publica o tamanho do número de inscrição, e a OAB, o CFM e o CFO não publicam formato algum, então as faixas de dígitos aceitas para `"CRP"`, `"OAB"`, `"CRM"` e `"CRO"` são convencionais, não normativas (a busca pública da OAB/SP tem `maxlength="7"`, e o CFM documenta CRMs com prefixo `300` e sufixo `P`, nenhum deles expresso por esses formatos). O CREA não é suportado: seu formato de registro não pôde ser confirmado em uma fonte oficial e publicamente documentada após a unificação nacional de 2016 (RNP). ```javascript import { isValidRegistroProfissional } from '@brazilian-utils/brazilian-utils'; -isValidRegistroProfissional('123456/SP', { council: 'OAB' }); // true -isValidRegistroProfissional('123456-RJ', { council: 'OAB', stateCode: 'SP' }); // false (UF divergente) -isValidRegistroProfissional('06/12345', { council: 'CRP' }); // true -isValidRegistroProfissional('SP-123456/O-3', { council: 'CRC' }); // true -isValidRegistroProfissional('SP-123456/O-3 T-MG', { council: 'CRC' }); // true (registro transferido) -isValidRegistroProfissional('SP-123456/T-3', { council: 'CRC' }); // false ("T" não é tipo de registro) +isValidRegistroProfissional({ value: '123456/SP', council: 'OAB' }); // true +isValidRegistroProfissional({ value: '123456-RJ', council: 'OAB', stateCode: 'SP' }); // false (UF divergente) +isValidRegistroProfissional({ value: '06/12345', council: 'CRP' }); // true +isValidRegistroProfissional({ value: 'SP-123456/O-3', council: 'CRC' }); // true +isValidRegistroProfissional({ value: 'SP-123456/O-3 T-MG', council: 'CRC' }); // true (registro transferido) +isValidRegistroProfissional({ value: 'SP-123456/T-3', council: 'CRC' }); // false ("T" não é tipo de registro) ``` ## isValidVin -Valida se um VIN (Vehicle Identification Number / chassi) é válido. Verifica o tamanho (17 caracteres), as letras excluídas (`I`, `O`, `Q` nunca são válidas; estrutura da [ISO 3779:2009](https://www.iso.org/standard/52200.html)) e o dígito verificador na 9ª posição, calculado e transliterado conforme o [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15). Esse dígito verificador é uma exigência norte-americana (49 CFR 565.15 / SAE J853): a [Resolução CONTRAN nº 968/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf) (que revogou a Resolução CONTRAN nº 24/1998 a partir de 1º de janeiro de 2025) e a ABNT NBR 6066 definem a estrutura do VIN brasileiro, mas não o exigem, então muitos VINs fabricados no Brasil não possuem um dígito verificador correspondente. Esta função é, portanto, uma verificação estrutural no padrão norte-americano, não um validador universal de VINs brasileiros. Não diferencia maiúsculas de minúsculas e remove espaços nas extremidades. +Valida se um VIN (Vehicle Identification Number / chassi) é válido. Verifica o tamanho (17 caracteres), as letras excluídas (`I`, `O`, `Q` nunca são válidas; estrutura da [ISO 3779:2009](https://www.iso.org/standard/52200.html)) e o dígito verificador na 9ª posição, calculado e transliterado conforme o [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15). Esse dígito verificador é uma exigência norte-americana (49 CFR 565.15 / SAE J853): a [Resolução CONTRAN nº 968/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf) (que revogou a Resolução CONTRAN nº 24/1998 a partir de 1º de janeiro de 2025) e a ABNT NBR 6066 definem a estrutura do VIN brasileiro, mas não o exigem, então muitos VINs fabricados no Brasil não possuem um dígito verificador correspondente. Esta função é, portanto, uma verificação estrutural no padrão norte-americano, não um validador universal de VINs brasileiros. Não diferencia maiúsculas de minúsculas e remove espaços nas extremidades. Um VIN é impresso como uma sequência única de 17 caracteres, então, diferente dos documentos que este pacote mascara (`isValidCpf`, `isValidCnpj`, `isValidNfeKey`), ele não tem limite de grupo onde escrever um separador e nenhum é aceito: um espaço, `.`, `-` ou `/` entre os caracteres é rejeitado em vez de removido. Um valor cujos 17 caracteres são todos iguais (`'00000000000000000'`) é rejeitado mesmo com o dígito verificador correspondente, do jeito que todo outro validador deste pacote rejeita um documento de dígitos repetidos. ```javascript import { isValidVin } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/utilities.md b/docs/utilities.md index c7d941f5..0c6fcc84 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -2,7 +2,7 @@ Here you will find all the utilities available for use. -> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `getBoletoInfo` returns `undefined` for an invalid boleto, the one function in the package that returns `undefined`; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function returns an empty value of its return type: every `format*` function, `capitalize`, and the string-returning `parse*` functions (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) return `""`; `parseCurrency` returns `0`; the object/tuple parsers — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — return `null`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. +> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `getBoletoInfo` returns `null` for an invalid boleto; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function returns an empty value of its return type: every `format*` function, `capitalize`, and the string-returning `parse*` functions (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) return `""`; `parseCurrency` returns `0`; the object/tuple parsers — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — return `null`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. ## isValidCpf @@ -283,6 +283,8 @@ isValidNfeKey('35170458716523000119550010000000121000123458'); // true (NF-e, SP isValidNfeKey('NFe35170458716523000119550010000000121000123458'); // true (XML Id prefix) isValidNfeKey('CTe35170458716523000119570010000000128000123452'); // true (CT-e authorised by the SVC-SP) isValidNfeKey('3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458'); // true (masked) +isValidNfeKey('3517.0458.7165.2300.0119.5500.1000.0000.1210.0012.3458'); // true (any of the mask characters) +isValidNfeKey('351 70458716523000119550010000000121000123458'); // false (a separator inside a group of 4) isValidNfeKey('99170458716523000119550010000000121000123458'); // false (invalid cUF) isValidNfeKey('35170458716523000119550010000000128000123455'); // false (the NF-e MOC does not assign tpEmis 8) isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 00000000, rule B03-10) @@ -290,28 +292,33 @@ isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 000 ## formatNfeKey -Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. Like every formatter of this package, the value is read for its digits and grouped as far as they go, so a masked or partial key still being typed is grouped progressively, and anything without a digit (an object, `true`, an object created with `Object.create(null)`) gives `''` instead of throwing. Use `isValidNfeKey` to check a key. +Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. Like every formatter of this package, the value is read for its digits and grouped as far as they go, so a masked or partial key still being typed is grouped progressively, and anything without a digit (an object, `true`, an object created with `Object.create(null)`) gives `''` instead of throwing. Use `isValidNfeKey` to check a key. `options.pad` (part of `FormatNfeKeyOptions`) left pads the value with zeros up to the 44 digits of a complete access key (default `false`). The parameter is typed as a string because 44 digits are more than a JavaScript number can hold exactly; at runtime a number is read as the string of its digits, like in every formatter of this package. ```javascript import { formatNfeKey } from '@brazilian-utils/brazilian-utils'; formatNfeKey('35170458716523000119550010000000121000123458'); // '3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458' + +formatNfeKey('12345'); // '1234 5' + +formatNfeKey('12345', { pad: true }); +// '0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 2345' ``` ## parseNfeKey -Parses a DF-e access key into its fields (state, year, month, taxId, model, series, number, emissionType, code, checkDigit). Accepts the same input forms as `isValidNfeKey` and returns `null` when the key is not valid. The result is typed as `NfeKey`, whose `model` is an `NfeKeyModel`. NFCom (`'62'`) and NF3e (`'66'`) spend position 36 of the key on `nSiteAutoriz`, the site of the authorizer that received the document, so for those two models the result also carries `authorizationSite` and `code` is 7 digits instead of 8. +Parses a DF-e access key into its fields (stateCode, year, month, taxId, model, series, number, emissionType, code, checkDigit). Accepts the same input forms as `isValidNfeKey` and returns `null` when the key is not valid. The result is typed as `NfeKey`, whose `model` is an `NfeKeyModel`. NFCom (`'62'`) and NF3e (`'66'`) spend position 36 of the key on `nSiteAutoriz`, the site of the authorizer that received the document, so for those two models the result also carries `authorizationSite` and `code` is 7 digits instead of 8. ```javascript import { parseNfeKey } from '@brazilian-utils/brazilian-utils'; parseNfeKey('35170458716523000119550010000000121000123458'); -// { state: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '55', +// { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '55', // series: 1, number: 12, emissionType: 1, code: '00012345', checkDigit: 8 } parseNfeKey('35170458716523000119620010000000121000123450'); -// { state: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', +// { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', // series: 1, number: 12, emissionType: 1, authorizationSite: 0, code: '0012345', checkDigit: 0 } parseNfeKey('invalid'); // null @@ -754,15 +761,16 @@ getBankByIspb('99999999'); // null ## isValidIban -Check if a Brazilian IBAN (International Bank Account Number) is valid, per Bacen's [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf) (Circular BCB nº 3.625/2013): `BR` + 2 ISO 7064 MOD 97-10 check digits + 8 digit ISPB + 5 digit branch + 10 digit account + 1 letter account type (any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 owner indicator (`1` for the first or only holder up to `9` for the ninth, then `A` to `Z` from the tenth, so `0` is rejected), 29 characters total. Only Brazilian IBANs (country code `BR`) are recognized; any other country returns `false`, since this package does not carry the field layout of the other 90+ ISO 13616 countries. Is case-insensitive and accepts both forms an IBAN is written in: compact (`'BR1500000000000010932840814P2'`) or in the ISO 13616 print format, letters and digits in groups separated by a single space, with optional surrounding whitespace either way. Only a character outside letters and digits, or a separator other than a single space, makes the value something other than an IBAN, so it is rejected instead of being stripped. +Check if a Brazilian IBAN (International Bank Account Number) is valid, per Bacen's [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf) (Circular BCB nº 3.625/2013): `BR` + 2 ISO 7064 MOD 97-10 check digits + 8 digit ISPB + 5 digit branch + 10 digit account + 1 letter account type (any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 owner indicator (`1` for the first or only holder up to `9` for the ninth, then `A` to `Z` from the tenth, so `0` is rejected), 29 characters total. Only Brazilian IBANs (country code `BR`) are recognized; any other country returns `false`, since this package does not carry the field layout of the other 90+ ISO 13616 countries. Is case-insensitive and accepts both forms an IBAN is written in: compact (`'BR1500000000000010932840814P2'`) or in the ISO 13616 print format, letters and digits in groups of 4 (the last one shorter), with optional surrounding whitespace either way. The groups may be split by whitespace, `.`, `-` or `/`, the interchangeable mask characters `isValidCpf` and `isValidCnpj` accept. Only a separator away from a group boundary, a run of separators (ISO 13616 prints a single one) or a character outside letters and digits makes the value something other than an IBAN, so it is rejected instead of being stripped. ```javascript import { isValidIban } from '@brazilian-utils/brazilian-utils'; isValidIban('BR1500000000000010932840814P2'); // true isValidIban('BR15 0000 0000 0000 1093 2840 814P 2'); // true (grouping spaces) +isValidIban('BR15-0000-0000-0000-1093-2840-814P-2'); // true (any of the mask characters) isValidIban('BR1500000000000010932840814P3'); // false (bad check digits) -isValidIban('BR1500000000000010932840814P-2'); // false (hyphens are not part of an IBAN) +isValidIban('BR15 000 00000 0000 1093 2840 814P 2'); // false (a separator inside a group) isValidIban('DE89370400440532013000'); // false (non Brazilian IBAN) ``` @@ -781,7 +789,7 @@ formatIban('BR15 0000-0000.0000/1093 2840 814P-2'); // 'BR15 0000 0000 0000 1093 ## parseIban -Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups separated by a single space), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying any character other than letters, digits and those single grouping spaces. The result is typed as `Iban`, whose `accountType` is a `string`. +Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups of 4 split by a single whitespace, `.`, `-` or `/`), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying a separator away from a group boundary, a run of separators or any character other than letters and digits. The result is typed as `Iban`, whose `accountType` is a `string`. ```javascript import { parseIban } from '@brazilian-utils/brazilian-utils'; @@ -798,12 +806,12 @@ parseIban('BR1500000000000010932840814P2'); // } parseIban('DE89370400440532013000'); // null (non Brazilian IBAN) -parseIban('BR1500000000000010932840814P-2'); // null (hyphens are not part of an IBAN) +parseIban('BR15 000 00000 0000 1093 2840 814P 2'); // null (a separator inside a group) ``` ## isValidCreditCard -Check if a payment card number is valid using the Luhn algorithm ([ISO/IEC 7812-1](https://www.iso.org/standard/70484.html)). Accepts the usual mask characters (spaces, hyphens) between digits and whitespace around the value; any other character makes the value invalid. Performs no brand detection (Visa, Mastercard, Amex...), issuer range lookup or expiration/CVV checks, only the digit count (12 to 19) and the Luhn check digit. A `number` is only accepted when it is a non-negative safe integer: anything above `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 digits) has already been rounded to a different number before the function sees it, so pass a longer PAN as a string. +Check if a payment card number is valid using the Luhn algorithm ([ISO/IEC 7812-1](https://www.iso.org/standard/70484.html)). Accepts the usual mask characters (whitespace, `.`, `-` and `/`, the interchangeable set `isValidCpf` and `isValidCnpj` accept) between any two digits and whitespace around the value; any other character makes the value invalid. They are accepted between any two digits rather than at fixed positions because the printed grouping of a PAN changes with the brand (4-4-4-4 for Visa and Mastercard, 4-6-5 for American Express, 4-6-4 for Diners Club), so there is no single layout to pin them to. Performs no brand detection (Visa, Mastercard, Amex...), issuer range lookup or expiration/CVV checks, only the digit count (12 to 19) and the Luhn check digit. A `number` is only accepted when it is a non-negative safe integer: anything above `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 digits) has already been rounded to a different number before the function sees it, so pass a longer PAN as a string. A value whose digits are all the same (`'0000000000000000'`) is rejected even when it passes the Luhn check, the way every other validator of this package rejects a repeated-digit document (`isValidCpf('00000000000')`, `isValidCns`, `isValidCaepf`, `isValidCei`). ```javascript import { isValidCreditCard } from '@brazilian-utils/brazilian-utils'; @@ -812,7 +820,9 @@ isValidCreditCard('4111111111111111'); // true (Visa test number) isValidCreditCard('5555555555554444'); // true (Mastercard test number) isValidCreditCard('378282246310005'); // true (American Express test number) isValidCreditCard('4111 1111 1111 1111'); // true (spaced mask) +isValidCreditCard('4111.1111/1111-1111'); // true (any of the mask characters) isValidCreditCard('4111111111111112'); // false (bad check digit) +isValidCreditCard('0000000000000000'); // false (every digit the same, though the Luhn check passes) isValidCreditCard('4111a1111b1111c1111'); // false (letters between the digits) isValidCreditCard(4111111111111111111); // false (above 2^53 - 1, pass it as a string) ``` @@ -1647,7 +1657,7 @@ parseVoterId('1234 5678 8 01 91'); // '1234567880191' (13-digit SP/MG voter id) ## isValidCns -Check if a CNS (Cartão Nacional de Saúde) number is valid, the unique SUS (Sistema Único de Saúde) user identifier. Definitive cards (starting with 1 or 2) are validated over an embedded 11 digit PIS/PASEP/NIS derived base weighted 15 down to 5; when the raw digit computes to 10, DATASUS raises the weighted sum by 2, recomputes the digit and marks the card with the suffix `001` instead of `000`. Provisional cards (starting with 7, 8 or 9) are validated instead by a single weighted sum (weights 15 down to 1) that must be a multiple of 11. The value has to be written as the 15 digits, optionally split into the printed groups of 3-4-4-4 by whitespace or the usual mask characters, a run of them between two groups included; letters among the digits are rejected instead of being read past. +Check if a CNS (Cartão Nacional de Saúde) number is valid, the unique SUS (Sistema Único de Saúde) user identifier. Definitive cards (starting with 1 or 2) are validated over an embedded 11 digit PIS/PASEP/NIS derived base weighted 15 down to 5; when the raw digit computes to 10, DATASUS raises the weighted sum by 2, recomputes the digit and marks the card with the suffix `001` instead of `000`. Provisional cards (starting with 7, 8 or 9) are validated instead by a single weighted sum (weights 15 down to 1) that must be a multiple of 11. The value has to be written as the 15 digits, optionally split into the printed groups of 3-4-4-4 by whitespace, `.`, `-` or `/`, the interchangeable mask characters `isValidCpf` and `isValidCnpj` accept, a run of them between two groups included; letters among the digits, or a separator inside a group, are rejected instead of being read past. The two routines come from the [ANVISA CNS validation page](https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/), which sits behind a bot filter and answers HTTP 403 to non-browser clients. The [e-SUS APS page](https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html) documents the same algorithm and is reachable without a browser, but applies the provisional routine to numbers starting with 5, 7, 8 or 9; this implementation follows ANVISA and rejects a 5-prefixed number even when its weighted sum checks out. @@ -1656,6 +1666,7 @@ import { isValidCns } from '@brazilian-utils/brazilian-utils'; isValidCns('123456789010000'); // true (definitive) isValidCns('700000000000005'); // true (provisional) +isValidCns('123.4567-8901/0000'); // true (any of the mask characters) isValidCns('12345678901'); // false (wrong length) isValidCns('abc123456789010000'); // false (not written as a CNS) ``` @@ -1822,22 +1833,22 @@ formatCaepf('184', { pad: true }); // 000.000.000/001-84 ## isValidRegistroProfissional -Check the structure of a professional council registration number (registro/inscrição profissional). Options are typed as `IsValidRegistroProfissionalOptions`: `options.council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `options.stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits, the tipo de registro (`"O"` Originário or `"P"` Provisório, which says nothing about the professional category) and the check digit, as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). A Registro Transferido or Secundário appends `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, per that same item and [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: the Manual's own examples are `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` and `PI-111222/O-5 S-AC`. Both UFs must be real state codes, and `options.stateCode` is compared against the originating one. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. Only the CRC shape and those CRP regional codes rest on a published source: the CFP page publishes no length for the inscription number itself, and the OAB, the CFM and the CFO publish no format at all, so the digit ranges accepted for `"CRP"`, `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative (the OAB/SP public search field is `maxlength="7"`, and the CFM documents `300`-prefixed and `P`-suffixed CRMs, none of which these shapes express). CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). +Check the structure of a professional council registration number (registro/inscrição profissional). It takes a single object, typed as `IsValidRegistroProfissionalOptions`, the shape `isValidBankAccount` takes: `value` is the registration number, `council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). Anything that is not an object, and an object missing `value` or `council`, is `false`. This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits, the tipo de registro (`"O"` Originário or `"P"` Provisório, which says nothing about the professional category) and the check digit, as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). A Registro Transferido or Secundário appends `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, per that same item and [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: the Manual's own examples are `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` and `PI-111222/O-5 S-AC`. Both UFs must be real state codes, and `stateCode` is compared against the originating one. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. Only the CRC shape and those CRP regional codes rest on a published source: the CFP page publishes no length for the inscription number itself, and the OAB, the CFM and the CFO publish no format at all, so the digit ranges accepted for `"CRP"`, `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative (the OAB/SP public search field is `maxlength="7"`, and the CFM documents `300`-prefixed and `P`-suffixed CRMs, none of which these shapes express). CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). ```javascript import { isValidRegistroProfissional } from '@brazilian-utils/brazilian-utils'; -isValidRegistroProfissional('123456/SP', { council: 'OAB' }); // true -isValidRegistroProfissional('123456-RJ', { council: 'OAB', stateCode: 'SP' }); // false (UF mismatch) -isValidRegistroProfissional('06/12345', { council: 'CRP' }); // true -isValidRegistroProfissional('SP-123456/O-3', { council: 'CRC' }); // true -isValidRegistroProfissional('SP-123456/O-3 T-MG', { council: 'CRC' }); // true (registro transferido) -isValidRegistroProfissional('SP-123456/T-3', { council: 'CRC' }); // false ("T" is not a tipo de registro) +isValidRegistroProfissional({ value: '123456/SP', council: 'OAB' }); // true +isValidRegistroProfissional({ value: '123456-RJ', council: 'OAB', stateCode: 'SP' }); // false (UF mismatch) +isValidRegistroProfissional({ value: '06/12345', council: 'CRP' }); // true +isValidRegistroProfissional({ value: 'SP-123456/O-3', council: 'CRC' }); // true +isValidRegistroProfissional({ value: 'SP-123456/O-3 T-MG', council: 'CRC' }); // true (registro transferido) +isValidRegistroProfissional({ value: 'SP-123456/T-3', council: 'CRC' }); // false ("T" is not a tipo de registro) ``` ## isValidVin -Check if a VIN (Vehicle Identification Number / chassi) is valid. Checks the length (17 characters), the excluded letters (`I`, `O`, `Q` are never valid; [ISO 3779:2009](https://www.iso.org/standard/52200.html) structure) and the check digit at the 9th position, with the check digit and transliteration computed per [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15). That check digit is a North-American requirement (49 CFR 565.15 / SAE J853): [Resolução CONTRAN nº 968/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf) (which revoked Resolução CONTRAN nº 24/1998 from 1 January 2025) and ABNT NBR 6066 define the Brazilian VIN structure but do not mandate it, so many Brazilian-built VINs do not carry a matching check digit. This function is therefore a North-American-style structural check, not a universal validator of Brazilian VINs. Case-insensitive and trims surrounding whitespace. +Check if a VIN (Vehicle Identification Number / chassi) is valid. Checks the length (17 characters), the excluded letters (`I`, `O`, `Q` are never valid; [ISO 3779:2009](https://www.iso.org/standard/52200.html) structure) and the check digit at the 9th position, with the check digit and transliteration computed per [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15). That check digit is a North-American requirement (49 CFR 565.15 / SAE J853): [Resolução CONTRAN nº 968/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf) (which revoked Resolução CONTRAN nº 24/1998 from 1 January 2025) and ABNT NBR 6066 define the Brazilian VIN structure but do not mandate it, so many Brazilian-built VINs do not carry a matching check digit. This function is therefore a North-American-style structural check, not a universal validator of Brazilian VINs. Case-insensitive and trims surrounding whitespace. A VIN is printed as one unbroken run of 17 characters, so, unlike the documents this package masks (`isValidCpf`, `isValidCnpj`, `isValidNfeKey`), it has no group boundary to write a separator at and none is accepted: a space, `.`, `-` or `/` among the characters is rejected instead of being stripped. A value whose 17 characters are all the same (`'00000000000000000'`) is rejected even when it carries a matching check digit, the way every other validator of this package rejects a repeated-digit document. ```javascript import { isValidVin } from '@brazilian-utils/brazilian-utils'; diff --git a/src/_internals/constants/iban.ts b/src/_internals/constants/iban.ts index 2d54100e..be4ea4b3 100644 --- a/src/_internals/constants/iban.ts +++ b/src/_internals/constants/iban.ts @@ -22,8 +22,12 @@ export const BR_IBAN_LENGTH = 29; export const BR_IBAN_REGEX = /^BR\d{2}\d{8}\d{5}\d{10}[A-Z][A-Z1-9]$/; /** - * Shape an IBAN has to be written in: the ISO 13616 print format, letters and digits in - * groups separated by a single space. Any other character (a hyphen, a dot, a slash) makes - * the value something other than an IBAN, so it is rejected instead of stripped. + * Shape an IBAN has to be written in: letters and digits, optionally split into the ISO 13616 + * print groups of 4 (the last one shorter, 1 to 3 characters, when the length is not a multiple + * of 4) by whitespace, `.`, `-` or `/`, the same interchangeable mask characters `isValidCpf` + * and `isValidCnpj` accept. A separator inside a group, a group of any other size, a run of + * separators between two groups (ISO 13616 prints a single one) or any character outside letters + * and digits makes the value something other than an IBAN, so it is rejected instead of stripped. */ -export const IBAN_FORMAT_REGEX = /^[A-Za-z0-9]+(?: [A-Za-z0-9]+)*$/; +export const IBAN_FORMAT_REGEX = + /^[A-Za-z0-9]{4}(?:[\s.\-/]?[A-Za-z0-9]{4})*(?:[\s.\-/]?[A-Za-z0-9]{1,3})?$/; diff --git a/src/index.test.ts b/src/index.test.ts index 1ecaba62..a64fa4b0 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -29,6 +29,7 @@ import { type FormatCpfOptions, type FormatCurrencyOptions, type FormatNcmOptions, + type FormatNfeKeyOptions, type FormatPhoneOptions, type FormatPisOptions, type FormatProcessoJuridicoOptions, @@ -284,6 +285,7 @@ describe("Public API", () => { FormatCpfOptions: FormatCpfOptions; FormatCurrencyOptions: FormatCurrencyOptions; FormatNcmOptions: FormatNcmOptions; + FormatNfeKeyOptions: FormatNfeKeyOptions; FormatPhoneOptions: FormatPhoneOptions; FormatPisOptions: FormatPisOptions; FormatProcessoJuridicoOptions: FormatProcessoJuridicoOptions; diff --git a/src/index.ts b/src/index.ts index 3e63f722..79c35020 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,7 +34,7 @@ export { } from "./format-legal-nature/format-legal-nature"; export { formatLicensePlate } from "./format-license-plate/format-license-plate"; export { type FormatNcmOptions, formatNcm } from "./format-ncm/format-ncm"; -export { formatNfeKey } from "./format-nfe-key/format-nfe-key"; +export { type FormatNfeKeyOptions, formatNfeKey } from "./format-nfe-key/format-nfe-key"; export { formatPassport } from "./format-passport/format-passport"; export { type FormatPhoneOptions, type PhoneMask, formatPhone } from "./format-phone/format-phone"; export { type FormatPisOptions, formatPis } from "./format-pis/format-pis"; diff --git a/src/is-valid-cns/is-valid-cns.test.ts b/src/is-valid-cns/is-valid-cns.test.ts index 8eee2100..b8c66d14 100644 --- a/src/is-valid-cns/is-valid-cns.test.ts +++ b/src/is-valid-cns/is-valid-cns.test.ts @@ -44,6 +44,11 @@ describe("isValidCns", () => { expect(isValidCns([])).toBe(false); }); + test("when it is an array whose text reads as a valid card", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidCns(["123456789010000"])).toBe(false); + }); + test("when it is an empty string", () => { expect(isValidCns("")).toBe(false); }); @@ -122,6 +127,13 @@ describe("isValidCns", () => { expect(isValidCns("123.4567.8901.0000")).toBe(true); }); + test("for a definitive CNS split by the other interchangeable separators", () => { + expect(isValidCns("123-4567-8901-0000")).toBe(true); + expect(isValidCns("123/4567/8901/0000")).toBe(true); + expect(isValidCns("123.4567-8901/0000")).toBe(true); + expect(isValidCns("123 - 4567 8901 0000")).toBe(true); + }); + test("for a definitive CNS with leading and trailing whitespace", () => { expect(isValidCns(" 123456789010000 ")).toBe(true); }); diff --git a/src/is-valid-cns/is-valid-cns.ts b/src/is-valid-cns/is-valid-cns.ts index 774edac5..50f3a4a1 100644 --- a/src/is-valid-cns/is-valid-cns.ts +++ b/src/is-valid-cns/is-valid-cns.ts @@ -42,9 +42,10 @@ const isValidProvisional = (digits: string): boolean => * be a multiple of 11. * * The value has to be written as the 15 digits, optionally split into the printed groups of 3, - * 4, 4 and 4 by whitespace or the usual mask characters, a run of them between two groups - * included; anything else, a letter among the digits included, is rejected instead of being - * read past. + * 4, 4 and 4 by whitespace, `.`, `-` or `/`, the interchangeable mask characters `isValidCpf` + * and `isValidCnpj` accept, a run of them between two groups included; anything else, a letter + * among the digits or a separator inside a group included, is rejected instead of being read + * past. * * @param {string|number} value - The CNS value to be validated. * @returns {boolean} True if the CNS is valid, false otherwise. @@ -54,6 +55,7 @@ const isValidProvisional = (digits: string): boolean => * isValidCns("123456789010000"); // true (definitive, suffix 000) * isValidCns("100000000060018"); // true (definitive, raw check digit 10, suffix 001) * isValidCns("700000000000005"); // true (provisional) + * isValidCns("123.4567-8901/0000"); // true (any of the mask characters) * isValidCns("123456789010001"); // false (wrong check digit) * isValidCns("12345678901"); // false (wrong length) * ``` diff --git a/src/is-valid-credit-card/is-valid-credit-card.test.ts b/src/is-valid-credit-card/is-valid-credit-card.test.ts index 121d1664..ea04f28c 100644 --- a/src/is-valid-credit-card/is-valid-credit-card.test.ts +++ b/src/is-valid-credit-card/is-valid-credit-card.test.ts @@ -35,6 +35,17 @@ describe("isValidCreditCard", () => { expect(isValidCreditCard("4111-1111-1111-1111")).toBe(true); }); + test("for a value masked with the other interchangeable separators", () => { + expect(isValidCreditCard("4111.1111.1111.1111")).toBe(true); + expect(isValidCreditCard("4111/1111/1111/1111")).toBe(true); + expect(isValidCreditCard("4111.1111/1111-1111")).toBe(true); + }); + + test("for a separator between any two digits, since the grouping changes with the brand", () => { + expect(isValidCreditCard("3782-822463-10005")).toBe(true); + expect(isValidCreditCard("4.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1")).toBe(true); + }); + test("for a value whose digit groups are separated by a run of mask characters", () => { expect(isValidCreditCard("4111 - 1111 - 1111 - 1111")).toBe(true); expect(isValidCreditCard("4111 1111 1111 1111")).toBe(true); @@ -58,6 +69,17 @@ describe("isValidCreditCard", () => { expect(isValidCreditCard("4111111111111112")).toBe(false); }); + test("when every digit is the same, even though the Luhn check passes", () => { + expect(isValidCreditCard("0000000000000000")).toBe(false); + expect(isValidCreditCard("000000000000")).toBe(false); + expect(isValidCreditCard("8888888888888888")).toBe(false); + expect(isValidCreditCard("0000000000000000000")).toBe(false); + }); + + test("when every digit is the same behind a mask, even though the Luhn check passes", () => { + expect(isValidCreditCard("0000 0000 0000 0000")).toBe(false); + }); + test("when it has fewer than 12 digits (11 digits)", () => { expect(isValidCreditCard("60110000000")).toBe(false); }); @@ -66,6 +88,10 @@ describe("isValidCreditCard", () => { expect(isValidCreditCard("12345678901234567850")).toBe(false); }); + test("when it has more than 19 digits and would pass the Luhn check on its own", () => { + expect(isValidCreditCard("12345678901234567852")).toBe(false); + }); + test("when it has more than 19 digits and would still pass the Luhn check on its own", () => { expect(isValidCreditCard("00000000000000000000")).toBe(false); }); @@ -103,8 +129,15 @@ describe("isValidCreditCard", () => { expect(isValidCreditCard("4111a1111b1111c1111")).toBe(false); }); - test("when the mask uses characters other than spaces and hyphens", () => { + test("when the mask uses characters outside the interchangeable set", () => { expect(isValidCreditCard("(41)11-1111 1111 1111")).toBe(false); + expect(isValidCreditCard("4111,1111,1111,1111")).toBe(false); + expect(isValidCreditCard("4111_1111_1111_1111")).toBe(false); + }); + + test("when a mask character is not between two digits", () => { + expect(isValidCreditCard("-4111111111111111")).toBe(false); + expect(isValidCreditCard("4111111111111111.")).toBe(false); }); test("when it is null", () => { @@ -139,6 +172,10 @@ describe("isValidCreditCard", () => { test("should accept exactly one Luhn check digit for any base", () => { fc.assert( fc.property(fc.stringMatching(/^[0-9]{11,18}$/), (base) => { + // A base of a single repeated digit can have its one Luhn candidate rejected as a + // repeated-digit PAN, so it is left to the literal tests above. + fc.pre(new Set(base).size > 1); + const accepted = LUHN_DIGITS.filter((digit) => isValidCreditCard(`${base}${digit}`)); expect(accepted.length).toBe(1); @@ -146,11 +183,11 @@ describe("isValidCreditCard", () => { ); }); - test("should ignore the spaces and hyphens between the digits", () => { + test("should ignore any of the mask characters between the digits", () => { fc.assert( fc.property( fc.stringMatching(/^[0-9]{12,19}$/), - fc.constantFrom(" ", "-"), + fc.constantFrom(" ", "-", ".", "/"), (card, separator) => { const masked = card.replaceAll(/(\d{4})(?=\d)/g, `$1${separator}`); diff --git a/src/is-valid-credit-card/is-valid-credit-card.ts b/src/is-valid-credit-card/is-valid-credit-card.ts index ffdc2a3e..335f871f 100644 --- a/src/is-valid-credit-card/is-valid-credit-card.ts +++ b/src/is-valid-credit-card/is-valid-credit-card.ts @@ -1,21 +1,31 @@ import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; +import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; import { mod10 } from "../_internals/mod10/mod10"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { MAX_LENGTH, MIN_LENGTH } from "./constants"; -const FORMAT_REGEX = /^\d+(?:[ -]+\d+)*$/; +const FORMAT_REGEX = /^\d+(?:[\s.\-/]+\d+)*$/; /** * Validates a payment card number (crédito ou débito) using the Luhn algorithm. * - * Accepts the usual mask characters (spaces and hyphens) between digits, a run of them included, - * so `"4111 - 1111 - 1111 - 1111"` reads as the same PAN, and whitespace around the value; any + * Accepts the usual mask characters (whitespace, `.`, `-` and `/`, the interchangeable set + * `isValidCpf` and `isValidCnpj` accept) between digits, a run of them included, so + * `"4111 - 1111 - 1111 - 1111"` reads as the same PAN, and whitespace around the value; any * other character makes the value invalid, so `"4111a1111b1111c1111"` is rejected instead of - * being read as `"4111111111111111"`. Only checks the digit count (12 to 19: 12 is + * being read as `"4111111111111111"`. They are accepted between any two digits rather than at + * fixed positions: the printed grouping of a PAN changes with the brand (4-4-4-4 for Visa and + * Mastercard, 4-6-5 for American Express, 4-6-4 for Diners Club), so there is no single layout + * to pin them to. Only checks the digit count (12 to 19: 12 is * the de-facto industry minimum PAN length, e.g. Maestro, and ISO/IEC 7812-1 caps the PAN at 19) * and the Luhn check digit; it performs no brand detection (Visa, Mastercard, Amex...), issuer * range lookup or expiration/CVV checks. * + * A value whose digits are all the same (`"0000000000000000"`) is rejected even when it passes + * the Luhn check, as every other validator of this package rejects a repeated-digit document + * (`isValidCpf("00000000000")`, `isValidCns`, `isValidCaepf`, `isValidCei`): no issuer hands out + * such a PAN, and it is what a placeholder or a zero-filled field looks like. + * * A number is only accepted when it is a non-negative safe integer: a card number above * `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 digits) has already been rounded to a different * number by the time it arrives, and a negative one is not a PAN, so both are rejected rather @@ -31,7 +41,9 @@ const FORMAT_REGEX = /^\d+(?:[ -]+\d+)*$/; * isValidCreditCard("378282246310005"); // true (American Express test number) * isValidCreditCard("4111 1111 1111 1111"); // true (spaced mask) * isValidCreditCard("4111 - 1111 - 1111 - 1111"); // true (a run of separators between the digits) + * isValidCreditCard("4111.1111/1111-1111"); // true (any of the mask characters) * isValidCreditCard("4111111111111112"); // false (bad check digit) + * isValidCreditCard("0000000000000000"); // false (every digit the same, though the Luhn check passes) * isValidCreditCard("4111a1111b1111c1111"); // false (letters between the digits) * isValidCreditCard("123456789"); // false (too short) * isValidCreditCard(4111111111111111111); // false (above 2^53 - 1, pass it as a string) @@ -54,6 +66,8 @@ export const isValidCreditCard = (value: string | number): boolean => { if (digits.length < MIN_LENGTH || digits.length > MAX_LENGTH) return false; + if (isRepeatedDigits(digits)) return false; + const checkDigit = digits.charCodeAt(digits.length - 1) - 48; return mod10(digits.slice(0, -1)) === checkDigit; diff --git a/src/is-valid-iban/is-valid-iban.test.ts b/src/is-valid-iban/is-valid-iban.test.ts index e6c2d396..14738974 100644 --- a/src/is-valid-iban/is-valid-iban.test.ts +++ b/src/is-valid-iban/is-valid-iban.test.ts @@ -18,6 +18,17 @@ describe("isValidIban", () => { expect(isValidIban("BR15 0000 0000 0000 1093 2840 814P 2")).toBe(true); }); + test("for a value whose ISO 13616 groups are split by any of the mask characters", () => { + expect(isValidIban("BR15.0000.0000.0000.1093.2840.814P.2")).toBe(true); + expect(isValidIban("BR15-0000-0000-0000-1093-2840-814P-2")).toBe(true); + expect(isValidIban("BR15/0000/0000/0000/1093/2840/814P/2")).toBe(true); + expect(isValidIban("BR15.0000-0000/0000 1093 2840 814P2")).toBe(true); + }); + + test("for a value split at one group boundary only", () => { + expect(isValidIban("BR1500000000000010932840814P-2")).toBe(true); + }); + test("for a lowercase value", () => { expect(isValidIban("br1500000000000010932840814p2")).toBe(true); }); @@ -82,13 +93,20 @@ describe("isValidIban", () => { }); test("when it carries a character outside the print format", () => { - expect(isValidIban("BR1500000000000010932840814P-2")).toBe(false); - expect(isValidIban("BR15.0000.0000.0000.1093.2840.814P2")).toBe(false); - expect(isValidIban("BR1500000000000010932840814P/2")).toBe(false); + expect(isValidIban("BR1500000000000010932840814P_2")).toBe(false); + expect(isValidIban("BR15,0000,0000,0000,1093,2840,814P,2")).toBe(false); + expect(isValidIban("BR1500000000000010932840814P#2")).toBe(false); + }); + + test("when a separator falls inside a group instead of at its boundary", () => { + expect(isValidIban("BR15 000 00000 0000 1093 2840 814P 2")).toBe(false); + expect(isValidIban("BR1 50000000000001093 2840 814P 2")).toBe(false); + expect(isValidIban("BR15 0000 0000 0000 1093 2840 814 P2")).toBe(false); }); - test("when the groups are separated by more than one space", () => { + test("when the groups are separated by more than one separator", () => { expect(isValidIban("BR15 0000 0000 0000 1093 2840 814P 2")).toBe(false); + expect(isValidIban("BR15 0000 0000 0000 1093 2840 .-814P 2")).toBe(false); }); test("when it is an empty string", () => { @@ -145,11 +163,11 @@ describe("isValidIban", () => { ); }); - test("should ignore the grouping spaces and the case of an IBAN", () => { + test("should ignore the grouping separators and the case of an IBAN", () => { fc.assert( - fc.property(bodies, (body) => { + fc.property(bodies, fc.constantFrom(" ", ".", "-", "/"), (body, separator) => { const iban = findIban(body); - const grouped = iban.replaceAll(/(.{4})(?=.)/g, "$1 "); + const grouped = iban.replaceAll(/(.{4})(?=.)/g, `$1${separator}`); expect(isValidIban(grouped)).toBe(true); expect(isValidIban(grouped.toLowerCase())).toBe(true); @@ -157,6 +175,25 @@ describe("isValidIban", () => { ); }); + test("should reject a separator that falls inside an ISO 13616 group", () => { + fc.assert( + fc.property( + bodies, + fc.integer({ min: 1, max: 28 }), + fc.constantFrom(" ", ".", "-", "/"), + (body, index, separator) => { + fc.pre(index % 4 !== 0); + + const iban = findIban(body); + + expect(isValidIban(`${iban.slice(0, index)}${separator}${iban.slice(index)}`)).toBe( + false, + ); + }, + ), + ); + }); + test("should reject an IBAN of any other country", () => { fc.assert( fc.property(bodies, fc.stringMatching(/^[A-Z]{2}$/), (body, countryCode) => { diff --git a/src/is-valid-iban/is-valid-iban.ts b/src/is-valid-iban/is-valid-iban.ts index 6302d447..634c9db1 100644 --- a/src/is-valid-iban/is-valid-iban.ts +++ b/src/is-valid-iban/is-valid-iban.ts @@ -22,14 +22,17 @@ const hasValidCheckDigits = (iban: string): boolean => { * * Only Brazilian IBANs (country code `BR`) are recognized: the field layout of the other 90+ * ISO 13616 countries is out of scope, so any non `BR` IBAN, however well formed, returns - * `false`. Accepts the usual grouping spaces and is case-insensitive. + * `false`. Accepts the usual grouping mask and is case-insensitive. * * Both accepted forms are the ones an IBAN is written in: compact, - * `"BR1500000000000010932840814P2"`, or the ISO 13616 print format, letters and digits in - * groups separated by a single space, with optional surrounding whitespace either way. Only a - * character outside letters and digits, or a separator other than a single space, makes the - * value something other than an IBAN, so `"BR1500000000000010932840814P-2"` and a double space - * are rejected instead of having the offending character stripped. + * `"BR1500000000000010932840814P2"`, or the ISO 13616 print format, letters and digits in groups + * of 4 (the last one shorter), with optional surrounding whitespace either way. The groups may be + * split by whitespace, `.`, `-` or `/`, the interchangeable mask characters `isValidCpf` and + * `isValidCnpj` accept, so `"BR1500000000000010932840814P-2"` reads as the same IBAN. Only a + * separator away from a group boundary, a run of separators (ISO 13616 prints a single one) or a + * character outside letters and digits makes the value something other than an IBAN, so + * `"BR15 0000 0000 0000 1093 2840 814P 2"` and `"BR15 000 00000 0000 1093 2840 814P 2"` are + * rejected instead of having the offending character stripped. * * The last character is the owner indicator, `1` for the first or only holder up to `9` for the * ninth and then `A` to `Z` from the tenth, per Circular BCB nº 3.625/2013 art. 2º § 1º, so a @@ -43,9 +46,10 @@ const hasValidCheckDigits = (iban: string): boolean => { * ```typescript * isValidIban("BR1500000000000010932840814P2"); // true * isValidIban("BR15 0000 0000 0000 1093 2840 814P 2"); // true (grouping spaces) + * isValidIban("BR15-0000-0000-0000-1093-2840-814P-2"); // true (any of the mask characters) * isValidIban("br1500000000000010932840814p2"); // true (case-insensitive) * isValidIban("BR1500000000000010932840814P3"); // false (bad check digits) - * isValidIban("BR1500000000000010932840814P-2"); // false (hyphens are not part of an IBAN) + * isValidIban("BR15 000 00000 0000 1093 2840 814P 2"); // false (a separator inside a group) * isValidIban("DE89370400440532013000"); // false (non Brazilian IBAN) * ``` * diff --git a/src/is-valid-nfe-key/is-valid-nfe-key.test.ts b/src/is-valid-nfe-key/is-valid-nfe-key.test.ts index b1894aff..e2b744ce 100644 --- a/src/is-valid-nfe-key/is-valid-nfe-key.test.ts +++ b/src/is-valid-nfe-key/is-valid-nfe-key.test.ts @@ -56,6 +56,17 @@ describe("isValidNfeKey", () => { expect(isValidNfeKey("3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458")).toBe(true); }); + test("when the printed groups of 4 are split by any of the mask characters", () => { + expect(isValidNfeKey("3517.0458.7165.2300.0119.5500.1000.0000.1210.0012.3458")).toBe(true); + expect(isValidNfeKey("3517-0458-7165-2300-0119-5500-1000-0000-1210-0012-3458")).toBe(true); + expect(isValidNfeKey("3517/0458/7165/2300/0119/5500/1000/0000/1210/0012/3458")).toBe(true); + }); + + test("when the mask characters are mixed and a run of them separates two groups", () => { + expect(isValidNfeKey("3517.0458-7165/2300 0119 5500 1000 0000 1210 0012 3458")).toBe(true); + expect(isValidNfeKey("3517 - 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458")).toBe(true); + }); + test("when it has the NFe prefix and a whitespace mask combined", () => { expect(isValidNfeKey("NFe 3512 0859 5972 4500 0190 5500 0000 0095 8317 1004 0056")).toBe( true, @@ -112,6 +123,21 @@ describe("isValidNfeKey", () => { expect(isValidNfeKey(`${VALID_B}9`)).toBe(false); }); + test("when it has whole groups of 4 digits but not the 44 of a key", () => { + expect(isValidNfeKey(VALID_B.slice(0, 40))).toBe(false); + expect(isValidNfeKey(`${VALID_B}9999`)).toBe(false); + }); + + test("when a separator falls inside a printed group of 4 digits", () => { + expect(isValidNfeKey("351 70458716523000119550010000000121000123458")).toBe(false); + expect(isValidNfeKey("3517 0458 7165 2300 0119 5500 1000 0000 1210 00123 458")).toBe(false); + }); + + test("when the groups are split by a character outside the mask", () => { + expect(isValidNfeKey("3517#0458#7165#2300#0119#5500#1000#0000#1210#0012#3458")).toBe(false); + expect(isValidNfeKey("3517,0458,7165,2300,0119,5500,1000,0000,1210,0012,3458")).toBe(false); + }); + test("when the cUF is not a valid IBGE UF code", () => { expect(isValidNfeKey(`00${VALID_B.slice(2)}`)).toBe(false); }); @@ -224,14 +250,35 @@ describe("isValidNfeKey", () => { ); }); - test("should ignore whitespace anywhere between the digits", () => { + test("should ignore any mask character placed at a printed group boundary", () => { + fc.assert( + fc.property( + fc.integer({ min: 1, max: 10 }), + fc.constantFrom(" ", ".", "-", "/"), + (group, separator) => { + const index = group * 4; + const masked = `${NFE_KEY.slice(0, index)}${separator}${NFE_KEY.slice(index)}`; + + expect(isValidNfeKey(masked)).toBe(true); + expect(isValidNfeKey(`NFe${masked}`)).toBe(true); + }, + ), + ); + }); + + test("should reject a mask character placed anywhere but a printed group boundary", () => { fc.assert( - fc.property(fc.integer({ min: 1, max: 43 }), (index) => { - const masked = `${NFE_KEY.slice(0, index)} ${NFE_KEY.slice(index)}`; + fc.property( + fc.integer({ min: 1, max: 43 }), + fc.constantFrom(" ", ".", "-", "/"), + (index, separator) => { + fc.pre(index % 4 !== 0); - expect(isValidNfeKey(masked)).toBe(true); - expect(isValidNfeKey(`NFe${masked}`)).toBe(true); - }), + const masked = `${NFE_KEY.slice(0, index)}${separator}${NFE_KEY.slice(index)}`; + + expect(isValidNfeKey(masked)).toBe(false); + }, + ), ); }); diff --git a/src/is-valid-nfe-key/is-valid-nfe-key.ts b/src/is-valid-nfe-key/is-valid-nfe-key.ts index 4557a411..dc69ca18 100644 --- a/src/is-valid-nfe-key/is-valid-nfe-key.ts +++ b/src/is-valid-nfe-key/is-valid-nfe-key.ts @@ -7,9 +7,12 @@ import { parseNfeKey } from "../parse-nfe-key/parse-nfe-key"; * (65), CT-e (57), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para * Outros Serviços), GTV-e (64, the CT-e Guia de Transporte de Valores), BP-e (63), NF3e (66) * and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed - * differently. Accepts whitespace between digit groups (the common display mask) and the `NFe`, - * `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the - * document's XML (e.g. `Id="NFe3517...`), which are stripped before validation. + * differently. The 44 digits may be split into the printed groups of 4 by whitespace, `.`, `-` + * or `/`, a run of them between two groups included, the same mask rule `isValidCpf` and + * `isValidCnpj` follow; a separator inside a group of 4, or any other character, is rejected + * instead of being stripped. The `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found + * in the `Id` attribute of the document's XML (e.g. `Id="NFe3517...`) are stripped before that + * check, with any whitespace between the prefix and the first group. * * The key is `cUF(2) AAMM(4) CNPJ/CPF(14) mod(2) serie(3) nNF(9) tpEmis(1) cNF(8) cDV(1)`, with * NFCom and NF3e spending position 36 on `nSiteAutoriz` and leaving 7 digits for `cNF`. @@ -57,6 +60,8 @@ import { parseNfeKey } from "../parse-nfe-key/parse-nfe-key"; * isValidNfeKey("35170458716523000119550010000000121000123458"); // true (NF-e, SP) * isValidNfeKey("NFe35170458716523000119550010000000121000123458"); // true (XML Id prefix) * isValidNfeKey("3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458"); // true (masked) + * isValidNfeKey("3517.0458.7165.2300.0119.5500.1000.0000.1210.0012.3458"); // true (any of the mask characters) + * isValidNfeKey("351 70458716523000119550010000000121000123458"); // false (a separator inside a group of 4) * isValidNfeKey("99170458716523000119550010000000121000123458"); // false (invalid cUF) * isValidNfeKey("35170458716523000119010010000000121000123450"); // false (invalid mod) * ``` diff --git a/src/is-valid-vin/is-valid-vin.test.ts b/src/is-valid-vin/is-valid-vin.test.ts index bfc119a2..30bf44fc 100644 --- a/src/is-valid-vin/is-valid-vin.test.ts +++ b/src/is-valid-vin/is-valid-vin.test.ts @@ -35,6 +35,13 @@ describe("isValidVin", () => { expect(isValidVin("1HGCM82633A004353")).toBe(false); }); + test("when every character is the same, even though the check digit matches", () => { + expect(isValidVin("00000000000000000")).toBe(false); + expect(isValidVin("55555555555555555")).toBe(false); + expect(isValidVin("99999999999999999")).toBe(false); + expect(isValidVin(" 00000000000000000 ")).toBe(false); + }); + test("when it contains the excluded letter I", () => { expect(isValidVin("1HGCM8263IA004352")).toBe(false); }); @@ -71,6 +78,13 @@ describe("isValidVin", () => { expect(isValidVin("1HGCM82633A00435-")).toBe(false); }); + test("when a mask character splits it, since a VIN has no printed grouping", () => { + expect(isValidVin("1HGCM8 2633A004352")).toBe(false); + expect(isValidVin("1HGCM8-2633A004352")).toBe(false); + expect(isValidVin("1HGCM8.2633A004352")).toBe(false); + expect(isValidVin("1HGCM8/2633A004352")).toBe(false); + }); + test("when it is an empty string", () => { expect(isValidVin("")).toBe(false); }); @@ -120,6 +134,10 @@ describe("isValidVin", () => { test("should accept exactly one check character for any body", () => { fc.assert( fc.property(bodies, (body) => { + // A body of a single repeated character can have its one accepted candidate rejected + // as a repeated-character VIN, so it is left to the literal tests above. + fc.pre(new Set(body).size > 1); + const candidates = VIN_CHECK_CHARACTERS.map( (character) => `${body.slice(0, 8)}${character}${body.slice(8)}`, ); diff --git a/src/is-valid-vin/is-valid-vin.ts b/src/is-valid-vin/is-valid-vin.ts index 274f3441..6d7ee416 100644 --- a/src/is-valid-vin/is-valid-vin.ts +++ b/src/is-valid-vin/is-valid-vin.ts @@ -1,4 +1,5 @@ import { generateChecksum } from "../_internals/generate-checksum/generate-checksum"; +import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; import { VIN_CHECK_DIGIT_POSITION, VIN_LENGTH, @@ -18,6 +19,17 @@ import { * a matching check digit. This function is therefore a North-American-style structural check, * not a universal validator of Brazilian VINs. Case-insensitive and trims surrounding whitespace. * + * A VIN is printed as one unbroken run of 17 characters, so, unlike the documents this package + * masks (`isValidCpf`, `isValidCnpj`, `isValidNfeKey`), it has no group boundary to write a + * separator at and none is accepted: a space, `.`, `-` or `/` among the characters is rejected + * instead of being stripped. + * + * A value whose 17 characters are all the same (`"00000000000000000"`) is rejected even when it + * carries a matching check digit, as every other validator of this package rejects a + * repeated-digit document (`isValidCpf("00000000000")`, `isValidCns`, `isValidCaepf`, + * `isValidCei`): no WMI, VDS and VIS are built out of a single repeated character, and it is what + * a placeholder or a zero-filled field looks like. + * * @param {string} value - The VIN to be validated. * @returns {boolean} True when `value` is a 17 character VIN with a matching check digit. * @@ -27,6 +39,7 @@ import { * isValidVin("1m8gdm9axkp042788"); // true (check digit X, lowercase) * isValidVin("JH4TB2H26CC000000"); // true * isValidVin("1HGCM82633A004353"); // false (bad check digit) + * isValidVin("00000000000000000"); // false (every character the same, though the check digit matches) * isValidVin("1HGCM8263IA004352"); // false (contains the excluded letter I) * isValidVin("1HGCM82633A00435"); // false (16 characters) * ``` @@ -49,6 +62,8 @@ export const isValidVin = (value: string): boolean => { if (vin.length !== VIN_LENGTH) return false; + if (isRepeatedDigits(vin)) return false; + // Stryker disable next-line StringLiteral: generateChecksum strips this to digits, so it's inert. let translitDigits = ""; diff --git a/src/parse-iban/parse-iban.test.ts b/src/parse-iban/parse-iban.test.ts index 9c7e554a..393f64d2 100644 --- a/src/parse-iban/parse-iban.test.ts +++ b/src/parse-iban/parse-iban.test.ts @@ -120,8 +120,12 @@ describe("parseIban", () => { }); test("when it carries a character outside the print format", () => { - expect(parseIban("BR1500000000000010932840814P-2")).toBeNull(); - expect(parseIban("BR15.0000.0000.0000.1093.2840.814P2")).toBeNull(); + expect(parseIban("BR1500000000000010932840814P_2")).toBeNull(); + expect(parseIban("BR15,0000,0000,0000,1093,2840,814P2")).toBeNull(); + }); + + test("when a separator falls inside a group instead of at its boundary", () => { + expect(parseIban("BR15 000 00000 0000 1093 2840 814P 2")).toBeNull(); }); test("when it is an empty string", () => { diff --git a/src/parse-iban/parse-iban.ts b/src/parse-iban/parse-iban.ts index da9d1513..451fb29a 100644 --- a/src/parse-iban/parse-iban.ts +++ b/src/parse-iban/parse-iban.ts @@ -50,10 +50,11 @@ const ACCOUNT_TYPE_END = ACCOUNT_END + ACCOUNT_TYPE_LENGTH; * Brazilian IBANs are supported: the field layout of the other ISO 13616 countries is out of * scope, so a well-formed non `BR` IBAN also returns `null`. * - * Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format - * (groups separated by a single space), in either case with optional surrounding whitespace and - * in any case, and returns `null` whenever `isValidIban` would return `false`, including a value - * carrying any character other than letters, digits and those single grouping spaces. + * Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups + * of 4 split by a single whitespace, `.`, `-` or `/`), in either case with optional surrounding + * whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, + * including a value carrying a separator away from a group boundary, a run of separators or any + * character other than letters and digits. * * @param {string} value - The IBAN to be parsed. * @returns {Iban|null} The parsed IBAN, or `null` when it is not a valid Brazilian IBAN. @@ -72,9 +73,10 @@ const ACCOUNT_TYPE_END = ACCOUNT_END + ACCOUNT_TYPE_LENGTH; * // } * * parseIban("BR15 0000 0000 0000 1093 2840 814P 2"); // same result (grouping spaces) + * parseIban("BR15-0000-0000-0000-1093-2840-814P-2"); // same result (any of the mask characters) * parseIban("DE89370400440532013000"); // null (non Brazilian IBAN) * parseIban("BR1500000000000010932840814P3"); // null (bad check digits) - * parseIban("BR1500000000000010932840814P-2"); // null (hyphens are not part of an IBAN) + * parseIban("BR15 000 00000 0000 1093 2840 814P 2"); // null (a separator inside a group) * ``` * * @see Official: https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf diff --git a/src/parse-nfe-key/constants.ts b/src/parse-nfe-key/constants.ts index 923e75d5..6c49c227 100644 --- a/src/parse-nfe-key/constants.ts +++ b/src/parse-nfe-key/constants.ts @@ -96,8 +96,14 @@ export const FORBIDDEN_CODE_MODELS: readonly string[] = ["55", "65"]; */ export const XML_ID_PREFIX_REGEX = /^(?:nfe|cte|mdfe|bpe|nf3e|nfcom)/i; -/** Digits and optional whitespace between groups, what is left once the prefix is stripped. */ -export const FORMAT_REGEX = /^[\d\s]+$/; +/** + * Shape the key has to be written in once the prefix is stripped: the digits, optionally split + * into the printed groups of 4 by whitespace or the usual mask characters, a run of them between + * two groups included, the same rule the CPF, CNPJ, CAEPF and CNS regexes of this library follow. + * A separator inside a group of 4, or any other character, is rejected instead of being stripped. + * The group count is left open so the 44 digit length is still checked where the key is read. + */ +export const FORMAT_REGEX = /^\d{4}(?:[\s.\-/]*\d{4})*$/; /** Start of the document number (nNF) inside the 44 digit key. */ export const NUMBER_START = 25; diff --git a/src/parse-nfe-key/parse-nfe-key.test.ts b/src/parse-nfe-key/parse-nfe-key.test.ts index bad695aa..57104d60 100644 --- a/src/parse-nfe-key/parse-nfe-key.test.ts +++ b/src/parse-nfe-key/parse-nfe-key.test.ts @@ -83,7 +83,7 @@ describe("parseNfeKey", () => { describe("should return the parsed access key", () => { test("for a NF-e access key (SP), the NFePHP `Keys::build` doc example also used in is-valid-nfe-key.test.ts", () => { expect(parseNfeKey(KEY_SP)).toEqual({ - state: "SP", + stateCode: "SP", year: 2017, month: 4, taxId: "58716523000119", @@ -98,7 +98,7 @@ describe("parseNfeKey", () => { test("for a NF-e access key (RS), the NFePHP sped-cte `$infNFe->chave` example (NF-e referenced by a CT-e)", () => { expect(parseNfeKey(KEY_RS)).toEqual({ - state: "RS", + stateCode: "RS", year: 2016, month: 4, taxId: "72202112000136", @@ -161,7 +161,7 @@ describe("parseNfeKey", () => { test("splitting nSiteAutoriz from the 7 digit cNF of an NFCom, per its Visão Geral §2.1.3", () => { expect(parseNfeKey("35170458716523000119620010000000121000123450")).toEqual({ - state: "SP", + stateCode: "SP", year: 2017, month: 4, taxId: "58716523000119", @@ -180,7 +180,7 @@ describe("parseNfeKey", () => { test("splitting nSiteAutoriz from the 7 digit cNF of an NF3e, per its Visão Geral", () => { expect(parseNfeKey("35170458716523000119660010000000121000123454")).toEqual({ - state: "SP", + stateCode: "SP", year: 2017, month: 4, taxId: "58716523000119", @@ -226,7 +226,7 @@ describe("parseNfeKey", () => { const key = buildNfeKey(`${issuer}${numbering}${emissionType}${tail}`); const parsed = parseNfeKey(key); - expect(parsed?.state).toBe(IBGE_UF_CODES[uf]); + expect(parsed?.stateCode).toBe(IBGE_UF_CODES[uf]); expect(parsed?.year).toBe(2000 + Number(year)); expect(parsed?.month).toBe(month); expect(parsed?.taxId).toBe(taxId); @@ -258,7 +258,7 @@ describe("parseNfeKey types", () => { expectTypeOf(parseNfeKey).parameter(0).toEqualTypeOf(); expectTypeOf(parseNfeKey).returns.toEqualTypeOf(); expectTypeOf().toEqualTypeOf<{ - state: StateCode; + stateCode: StateCode; year: number; month: number; taxId: string; diff --git a/src/parse-nfe-key/parse-nfe-key.ts b/src/parse-nfe-key/parse-nfe-key.ts index c20ca85c..ba908715 100644 --- a/src/parse-nfe-key/parse-nfe-key.ts +++ b/src/parse-nfe-key/parse-nfe-key.ts @@ -29,8 +29,8 @@ export type NfeKeyModel = "55" | "57" | "58" | "62" | "63" | "64" | "65" | "66" /** The fields `parseNfeKey` reads out of a DF-e access key (chave de acesso). */ export type NfeKey = { - /** Two letter code of the issuing state, read from the IBGE UF code. */ - state: StateCode; + /** Two letter code of the issuing state (UF), read from the IBGE UF code. */ + stateCode: StateCode; /** Four digit issue year. */ year: number; /** Issue month, 1 to 12. */ @@ -75,9 +75,9 @@ const isForbiddenCode = (model: string, code: string, number: number): boolean = * * Covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e * (65), CT-e (57), MDF-e (58), CT-e OS (67), GTV-e (64), BP-e (63), NF3e (66) and NFCom (62). - * Accepts the same input forms as `isValidNfeKey` (whitespace mask, the `NFe`, `CTe`, `MDFe`, - * `BPe`, `NF3e` and `NFCom` prefixes of the XML `Id` attribute) and returns `null` when the key - * is not valid. + * Accepts the same input forms as `isValidNfeKey` (the printed mask of 4 digit groups, split by + * whitespace, `.`, `-` or `/`, and the `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes + * of the XML `Id` attribute) and returns `null` when the key is not valid. * * The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so * the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` @@ -131,7 +131,7 @@ const isForbiddenCode = (model: string, code: string, number: number): boolean = * @example * ```typescript * parseNfeKey("35170458716523000119550010000000121000123458"); - * // { state: "SP", year: 2017, month: 4, taxId: "58716523000119", model: "55", + * // { stateCode: "SP", year: 2017, month: 4, taxId: "58716523000119", model: "55", * // series: 1, number: 12, emissionType: 1, code: "00012345", checkDigit: 8 } * * parseNfeKey("invalid"); // null @@ -140,7 +140,7 @@ const isForbiddenCode = (model: string, code: string, number: number): boolean = export const parseNfeKey = (value: string): NfeKey | null => { if (typeof value !== "string") return null; - const body = value.trim().replace(XML_ID_PREFIX_REGEX, ""); + const body = value.trim().replace(XML_ID_PREFIX_REGEX, "").trimStart(); if (!FORMAT_REGEX.test(body)) return null; @@ -150,9 +150,9 @@ export const parseNfeKey = (value: string): NfeKey | null => { const uf = digits.slice(0, 2); - const state = IBGE_UF_CODES[uf]; + const stateCode = IBGE_UF_CODES[uf]; - if (state === undefined) return null; + if (stateCode === undefined) return null; const month = Number(digits.slice(4, 6)); @@ -185,7 +185,7 @@ export const parseNfeKey = (value: string): NfeKey | null => { } const parsed: NfeKey = { - state, + stateCode, year: 2000 + Number(digits.slice(2, 4)), month, taxId: digits.slice(6, 20), From 58d561bcb925b8ea67ce5be883338702bbc63d0e Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 10:04:58 -0300 Subject: [PATCH 58/75] fix(api): align the new utils with the library conventions `GeneratePixPayloadParams` is renamed `GeneratePixPayloadOptions`, the suffix every other public options type carries and the one this release moves `IsValidBankAccountParams` to. `formatNfeKey` gains the `pad` option of every fixed width formatter (`FormatNfeKeyOptions`), and `formatCertidao` accepts `string | number` like `formatCpf`, reading a number as the string of its digits. `isValidRegistroProfissional` takes a single object, `{ value, council, stateCode? }`, the shape `isValidBankAccount` uses when a validator needs more than the value. `getBoletoInfo` returns `null` instead of `undefined` for an invalid boleto, as every other getter does; only a strict `=== undefined` comparison is affected. --- docs/llms-full.txt | 13 +- docs/pt-br/utilities.md | 13 +- docs/utilities.md | 13 +- src/format-certidao/format-certidao.test.ts | 8 +- src/format-certidao/format-certidao.ts | 14 +- src/format-nfe-key/format-nfe-key.test.ts | 42 ++++- src/format-nfe-key/format-nfe-key.ts | 27 ++- src/generate-boleto/generate-boleto.test.ts | 2 +- .../generate-pix-payload.test.ts | 8 +- .../generate-pix-payload.ts | 8 +- src/get-boleto-info/get-boleto-info.test.ts | 25 ++- src/get-boleto-info/get-boleto-info.ts | 15 +- src/index.test.ts | 4 +- src/index.ts | 2 +- .../is-valid-registro-profissional.test.ts | 170 +++++++++++------- .../is-valid-registro-profissional.ts | 55 +++--- 16 files changed, 278 insertions(+), 141 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 448eceb0..b8315ea4 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -401,7 +401,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ### getBoletoInfo -Extract information from a boleto (amount, expiration date, bank code). Returns `undefined` when `value` is not a valid boleto — `isValidBoleto` is checked first — exactly as in 2.3.0, so the result has to be narrowed before it is read. Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. The cycle search never goes below the first cycle, so a `referenceDate` older than the scheme itself still resolves a factor to the oldest date that factor can denote rather than to one before the 07/10/1997 base date. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. +Extract information from a boleto (amount, expiration date, bank code). Returns `null` when `value` is not a valid boleto — `isValidBoleto` is checked first — so the result has to be narrowed before it is read. 2.3.0 returned `undefined` here; every getter of the package now answers an unresolved lookup with `null`, so only a strict `=== undefined` comparison is affected. Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. The cycle search never goes below the first cycle, so a `referenceDate` older than the scheme itself still resolves a factor to the oldest date that factor can denote rather than to one before the 07/10/1997 base date. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -416,6 +416,8 @@ getBoletoInfo('00190000090114971860168524522114675860000102656', { getBoletoInfo('846100000005246100291102005460339004695895061080'); // { amount: 2461, expirationDate: null, bankCode: '', type: 'arrecadacao', segment: 4, value: 24.61, hasEffectiveValue: true } + +getBoletoInfo('invalid'); // null ``` ### isValidPixKey @@ -487,7 +489,7 @@ parsePixPayload( ### generatePixPayload -Generates the payload of a Pix BR Code. Exactly one of `params.key` or `params.url` must be given (part of `GeneratePixPayloadParams`); `null` is returned when both or neither are given. `url` must be a PSP location as the Bacen manual defines it: a host name with a path, without a scheme (`pix.example.com/qr/v2/1234`); a dynamic payload cannot carry `amount` or `txid`, which belong to the PSP location. The amount is written with the two decimal places the BR Code takes, so one that rounds to `0.00` and one that does not survive that round trip (`0.005`, `123.456`) are both rejected rather than written as a different sum. The Pix Saque BR Code, which announces the `fss` of sub-object 26-03, is parsed by `parsePixPayload` but not generated here. +Generates the payload of a Pix BR Code. Exactly one of `params.key` or `params.url` must be given (part of `GeneratePixPayloadOptions`); `null` is returned when both or neither are given. `url` must be a PSP location as the Bacen manual defines it: a host name with a path, without a scheme (`pix.example.com/qr/v2/1234`); a dynamic payload cannot carry `amount` or `txid`, which belong to the PSP location. The amount is written with the two decimal places the BR Code takes, so one that rounds to `0.00` and one that does not survive that round trip (`0.005`, `123.456`) are both rejected rather than written as a different sum. The Pix Saque BR Code, which announces the `fss` of sub-object 26-03, is parsed by `parsePixPayload` but not generated here. When `params.key` is given, it is normalized to its DICT canonical form by `parsePixKey` and the payload is static. When `params.url` is given instead (the PSP location, without a URL scheme, e.g. `"pix.example.com/qr/v2/1234"`), the payload is dynamic per the Manual de Padrões para Iniciação do Pix: the URL takes the key's place in the "Merchant Account Information" template and the "Point of Initiation Method" object is set to dynamic (`12`); `params.url` can be at most 77 characters. `merchantName`, `merchantCity` and `description` are folded to printable ASCII (accents dropped) and truncated to what the BR Code allows. `parsePixPayload` already parses both shapes, so `parsePixPayload(generatePixPayload({ url, ... }))` round-trips. @@ -514,7 +516,7 @@ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // nu ### isValidNfeKey -Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e (65), CT-e (57, the Conhecimento de Transporte Eletrônico instituted by the cláusula primeira of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para Outros Serviços instituted by the cláusula primeira of the [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, the CT-e Guia de Transporte de Valores instituted by the cláusula primeira of the [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed differently. Accepts whitespace between digit groups (the common display mask) and the `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the document's XML. +Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e (65), CT-e (57, the Conhecimento de Transporte Eletrônico instituted by the cláusula primeira of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para Outros Serviços instituted by the cláusula primeira of the [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, the CT-e Guia de Transporte de Valores instituted by the cláusula primeira of the [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed differently. The 44 digits may be split into the printed groups of 4 by whitespace, `.`, `-` or `/`, a run of them between two groups included, the same interchangeable mask `isValidCpf` and `isValidCnpj` accept; a separator inside a group of 4, or any other character, is rejected instead of being stripped. The `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the document's XML are stripped before that check, along with any whitespace between the prefix and the first group. The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` for the CT-e, `{1, 5, 7, 8}` for the CT-e OS, `{1, 2, 7, 8}` for the GTV-e, `{1, 2, 3}` for the MDF-e and `{1, 2}` for the BP-e, the NF3e and the NFCom. Code 8, the authorização pela SVC-SP, is assigned by the [CT-e MOC 4.00](https://dfe-portal.svrs.rs.gov.br/CTE/Documentos) only, never by the NF-e one; the domains of the [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), the [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) and the [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) come from their own manuals. For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, which forbids the twenty repeated and sequential `cNF` values it lists and a `cNF` equal to the document number. A document number of all zeros is turned down for every model, following the leiaute rather than a choice of this library: `tiposBasico_v4.00.xsd` of the [NF-e schema package](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) types `nNF` as `TNF`, whose pattern is `[1-9]{1}[0-9]{0,8}`, and the Anexo I of every other model repeats the same regex for its own number field. @@ -1557,7 +1559,7 @@ getLegalNaturesByCategory('9'); // [] ### getLegalNature -Look a legal nature code up in the official IBGE/CONCLA table. The entry also carries the CONCLA category the code is listed under, taken from its first digit. +Look a legal nature code up in the official IBGE/CONCLA table. The entry also carries the CONCLA category the code is listed under, taken from its first digit. No legal nature code starts with a zero, that first digit is the category (1 to 5), so nothing is ever padded here: a number and the string of the same digits are read identically. ```javascript import { getLegalNature } from '@brazilian-utils/brazilian-utils'; @@ -1984,7 +1986,7 @@ The `Certidao` result carries: ### formatCertidao -Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). The parameter is typed as a string because the 32 digits of a matrícula are more than a JavaScript number can hold exactly; at runtime the value is read for its digits and masked as far as they go, like in every formatter of this package, so a partial matrícula still being typed is masked progressively. +Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). A number is accepted and read as the string of its digits, like in `formatCpf`, but a full 32 digit matrícula has to be a string: that many digits are more than a JavaScript number can hold exactly. At runtime the value is read for its digits and masked as far as they go, like in every formatter of this package, so a partial matrícula still being typed is masked progressively. ```javascript import { formatCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1992,6 +1994,7 @@ import { formatCertidao } from '@brazilian-utils/brazilian-utils'; formatCertidao('10453901552013100012021000012321'); // 104539 01 55 2013 1 00012 021 0000123 21 formatCertidao('104539.01.55.2013.1.00012.021.0000123-21'); // 104539 01 55 2013 1 00012 021 0000123 21 formatCertidao('1552010100020112000012087', { pad: true }); // 000000 01 55 2010 1 00020 112 0000120 87 +formatCertidao(104539015520); // 104539 01 55 20 (a number is read as the string of its digits) ``` ### isValidCei diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 402fb74f..0e0e370e 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -159,7 +159,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ## getBoletoInfo -Extrai informações de um boleto (valor, data de vencimento, código do banco). Retorna `undefined` quando `value` não é um boleto válido — o `isValidBoleto` é verificado antes —, exatamente como na 2.3.0, então o resultado precisa ser estreitado antes de ser lido. Aceita opcionalmente `{ referenceDate }` (tipado como `GetBoletoInfoOptions`) para resolver o ciclo do "fator de vencimento" a partir de uma data específica em vez de agora (o ciclo de data-base do fator reiniciou em 22/02/2025, segundo a FEBRABAN). Nem a FEBRABAN nem o Banco Central publicam uma forma de distinguir um fator do ciclo antigo de um do ciclo novo, então todo fator resolve para uma de duas datas separadas por 9000 dias e o `referenceDate` escolhe entre elas por meio das janelas de segurança da própria biblioteca: o mesmo boleto pode passar a resolver para a outra candidata com o tempo, então informe `referenceDate` explicitamente sempre que a resposta precisar ser estável. A busca de ciclo nunca desce abaixo do primeiro ciclo, então um `referenceDate` anterior ao próprio esquema ainda resolve um fator para a data mais antiga que aquele fator consegue representar, em vez de uma anterior à data-base de 07/10/1997. Para um boleto de arrecadação, o resultado, tipado como `BoletoInfo`, continua trazendo as duas chaves, porém vazias, `bankCode: ''` e `expirationDate: null`, já que o boleto não tem código de banco nem fator de vencimento, e acrescenta `type: "arrecadacao"`, `segment`, `value` e `hasEffectiveValue`. +Extrai informações de um boleto (valor, data de vencimento, código do banco). Retorna `null` quando `value` não é um boleto válido — o `isValidBoleto` é verificado antes —, então o resultado precisa ser estreitado antes de ser lido. A 2.3.0 retornava `undefined` aqui; agora todo getter do pacote responde com `null` a uma busca que não resolve, então só uma comparação estrita `=== undefined` é afetada. Aceita opcionalmente `{ referenceDate }` (tipado como `GetBoletoInfoOptions`) para resolver o ciclo do "fator de vencimento" a partir de uma data específica em vez de agora (o ciclo de data-base do fator reiniciou em 22/02/2025, segundo a FEBRABAN). Nem a FEBRABAN nem o Banco Central publicam uma forma de distinguir um fator do ciclo antigo de um do ciclo novo, então todo fator resolve para uma de duas datas separadas por 9000 dias e o `referenceDate` escolhe entre elas por meio das janelas de segurança da própria biblioteca: o mesmo boleto pode passar a resolver para a outra candidata com o tempo, então informe `referenceDate` explicitamente sempre que a resposta precisar ser estável. A busca de ciclo nunca desce abaixo do primeiro ciclo, então um `referenceDate` anterior ao próprio esquema ainda resolve um fator para a data mais antiga que aquele fator consegue representar, em vez de uma anterior à data-base de 07/10/1997. Para um boleto de arrecadação, o resultado, tipado como `BoletoInfo`, continua trazendo as duas chaves, porém vazias, `bankCode: ''` e `expirationDate: null`, já que o boleto não tem código de banco nem fator de vencimento, e acrescenta `type: "arrecadacao"`, `segment`, `value` e `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -174,6 +174,8 @@ getBoletoInfo('00190000090114971860168524522114675860000102656', { getBoletoInfo('846100000005246100291102005460339004695895061080'); // { amount: 2461, expirationDate: null, bankCode: '', type: 'arrecadacao', segment: 4, value: 24.61, hasEffectiveValue: true } + +getBoletoInfo('invalid'); // null ``` ## isValidPixKey @@ -245,7 +247,7 @@ parsePixPayload( ## generatePixPayload -Gera o payload de um BR Code Pix. Exatamente um entre `params.key` e `params.url` deve ser informado (parte de `GeneratePixPayloadParams`); `null` é retornado quando ambos ou nenhum são informados. `url` deve ser uma localização de PSP como o manual do Bacen define: um host com caminho, sem esquema (`pix.example.com/qr/v2/1234`); um payload dinâmico não pode carregar `amount` nem `txid`, que pertencem à localização do PSP. O valor é escrito com as duas casas decimais que o BR Code aceita, então tanto um que arredonda para `0.00` quanto um que não sobrevive a esse round-trip (`0.005`, `123.456`) são rejeitados, em vez de escritos como uma quantia diferente. O BR Code de Pix Saque, que anuncia o `fss` do subobjeto 26-03, é interpretado pelo `parsePixPayload`, mas não é gerado aqui. +Gera o payload de um BR Code Pix. Exatamente um entre `params.key` e `params.url` deve ser informado (parte de `GeneratePixPayloadOptions`); `null` é retornado quando ambos ou nenhum são informados. `url` deve ser uma localização de PSP como o manual do Bacen define: um host com caminho, sem esquema (`pix.example.com/qr/v2/1234`); um payload dinâmico não pode carregar `amount` nem `txid`, que pertencem à localização do PSP. O valor é escrito com as duas casas decimais que o BR Code aceita, então tanto um que arredonda para `0.00` quanto um que não sobrevive a esse round-trip (`0.005`, `123.456`) são rejeitados, em vez de escritos como uma quantia diferente. O BR Code de Pix Saque, que anuncia o `fss` do subobjeto 26-03, é interpretado pelo `parsePixPayload`, mas não é gerado aqui. Quando `params.key` é informado, ela é normalizada para a forma canônica do DICT pelo `parsePixKey` e o payload é estático. Quando `params.url` é informado no lugar (a localização do PSP, sem o esquema da URL, ex.: `"pix.example.com/qr/v2/1234"`), o payload é dinâmico conforme o Manual de Padrões para Iniciação do Pix: a URL ocupa o lugar da chave no template "Merchant Account Information" e o objeto "Point of Initiation Method" é definido como dinâmico (`12`); `params.url` pode ter no máximo 77 caracteres. `merchantName`, `merchantCity` e `description` são convertidos para ASCII imprimível (acentos removidos) e truncados ao que o BR Code permite. O `parsePixPayload` já interpreta os dois formatos, então `parsePixPayload(generatePixPayload({ url, ... }))` forma um round-trip. @@ -272,7 +274,7 @@ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // nu ## isValidNfeKey -Valida se uma chave de acesso de DF-e (Documento Fiscal eletrônico) é válida. Cobre todos os documentos cuja chave de acesso é a mesma string de 44 dígitos: NF-e (modelo 55), NFC-e (65), CT-e (57, o Conhecimento de Transporte Eletrônico instituído pela cláusula primeira do [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, o Conhecimento de Transporte Eletrônico para Outros Serviços instituído pela cláusula primeira do [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, o CT-e Guia de Transporte de Valores instituído pela cláusula primeira do [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) e NFCom (62). O CF-e-SAT (59) fica de fora: sua "chave de consulta" de 44 posições é composta de outro jeito. Aceita espaços entre os grupos de dígitos (a máscara de exibição usual) e os prefixos `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` e `NFCom` encontrados no atributo `Id` do XML do documento. +Valida se uma chave de acesso de DF-e (Documento Fiscal eletrônico) é válida. Cobre todos os documentos cuja chave de acesso é a mesma string de 44 dígitos: NF-e (modelo 55), NFC-e (65), CT-e (57, o Conhecimento de Transporte Eletrônico instituído pela cláusula primeira do [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, o Conhecimento de Transporte Eletrônico para Outros Serviços instituído pela cláusula primeira do [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, o CT-e Guia de Transporte de Valores instituído pela cláusula primeira do [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) e NFCom (62). O CF-e-SAT (59) fica de fora: sua "chave de consulta" de 44 posições é composta de outro jeito. Os 44 dígitos podem ser separados nos grupos impressos de 4 por espaço em branco, `.`, `-` ou `/`, inclusive uma sequência deles entre dois grupos, a mesma máscara intercambiável que `isValidCpf` e `isValidCnpj` aceitam; um separador dentro de um grupo de 4, ou qualquer outro caractere, é rejeitado em vez de removido. Os prefixos `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` e `NFCom` encontrados no atributo `Id` do XML do documento são removidos antes dessa verificação, junto com qualquer espaço em branco entre o prefixo e o primeiro grupo. O tipo de emissão (`tpEmis`) é conferido contra os códigos que o MOC daquele modelo atribui, então o conjunto aceito muda com o modelo: de 1 a 7 e 9 para NF-e e NFC-e, `{1, 3, 4, 5, 7, 8}` para o CT-e, `{1, 5, 7, 8}` para o CT-e OS, `{1, 2, 7, 8}` para a GTV-e, `{1, 2, 3}` para o MDF-e e `{1, 2}` para o BP-e, a NF3e e a NFCom. O código 8, a autorização pela SVC-SP, é atribuído somente pelo [MOC do CT-e 4.00](https://dfe-portal.svrs.rs.gov.br/CTE/Documentos), nunca pelo da NF-e; os domínios do [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), da [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) e da [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) vêm dos manuais deles. Para NF-e e NFC-e o código numérico também é conferido contra a regra B03-10 do MOC da NF-e, que proíbe os vinte valores repetidos e sequenciais de `cNF` que ela lista e um `cNF` igual ao número do documento. Já um número de documento todo zerado é recusado em todos os modelos seguindo o leiaute, não por escolha desta biblioteca: o `tiposBasico_v4.00.xsd` do [pacote de schemas da NF-e](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) tipa o `nNF` como `TNF`, cujo pattern é `[1-9]{1}[0-9]{0,8}`, e o Anexo I de cada um dos outros modelos repete o mesmo regex no seu próprio campo de número. @@ -1315,7 +1317,7 @@ getLegalNaturesByCategory('9'); // [] ## getLegalNature -Busca um código de natureza jurídica na tabela oficial do IBGE/CONCLA. A entrada também traz a categoria do CONCLA em que o código está listado, dada pelo seu primeiro dígito. +Busca um código de natureza jurídica na tabela oficial do IBGE/CONCLA. A entrada também traz a categoria do CONCLA em que o código está listado, dada pelo seu primeiro dígito. Nenhum código de natureza jurídica começa com zero, esse primeiro dígito é a categoria (1 a 5), então aqui nada é completado: um número e a string dos mesmos dígitos são lidos de forma idêntica. ```javascript import { getLegalNature } from '@brazilian-utils/brazilian-utils'; @@ -1742,7 +1744,7 @@ O resultado `Certidao` traz: ## formatCertidao -Formata a matrícula de uma certidão de registro civil na máscara impressa do Provimento, os 32 dígitos agrupados em 6 2 2 4 1 5 3 7 2 e separados por espaços. `options.pad` (parte de `FormatCertidaoOptions`) preenche o valor com zeros à esquerda até 32 dígitos (padrão `false`). A máscara é a do [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). O parâmetro é tipado como string porque os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta com exatidão; em tempo de execução o valor é lido pelos seus dígitos e a máscara é aplicada até onde eles vão, como em todo formatador deste pacote, então uma matrícula parcial ainda sendo digitada é mascarada progressivamente. +Formata a matrícula de uma certidão de registro civil na máscara impressa do Provimento, os 32 dígitos agrupados em 6 2 2 4 1 5 3 7 2 e separados por espaços. `options.pad` (parte de `FormatCertidaoOptions`) preenche o valor com zeros à esquerda até 32 dígitos (padrão `false`). A máscara é a do [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). Um número é aceito e lido como a string dos seus dígitos, como no `formatCpf`, mas uma matrícula completa de 32 dígitos precisa ser uma string: essa quantidade de dígitos é mais do que um número JavaScript comporta com exatidão. Em tempo de execução o valor é lido pelos seus dígitos e a máscara é aplicada até onde eles vão, como em todo formatador deste pacote, então uma matrícula parcial ainda sendo digitada é mascarada progressivamente. ```javascript import { formatCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1750,6 +1752,7 @@ import { formatCertidao } from '@brazilian-utils/brazilian-utils'; formatCertidao('10453901552013100012021000012321'); // 104539 01 55 2013 1 00012 021 0000123 21 formatCertidao('104539.01.55.2013.1.00012.021.0000123-21'); // 104539 01 55 2013 1 00012 021 0000123 21 formatCertidao('1552010100020112000012087', { pad: true }); // 000000 01 55 2010 1 00020 112 0000120 87 +formatCertidao(104539015520); // 104539 01 55 20 (um número é lido como a string dos seus dígitos) ``` ## isValidCei diff --git a/docs/utilities.md b/docs/utilities.md index 0c6fcc84..fad87fc7 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -159,7 +159,7 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ## getBoletoInfo -Extract information from a boleto (amount, expiration date, bank code). Returns `undefined` when `value` is not a valid boleto — `isValidBoleto` is checked first — exactly as in 2.3.0, so the result has to be narrowed before it is read. Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. The cycle search never goes below the first cycle, so a `referenceDate` older than the scheme itself still resolves a factor to the oldest date that factor can denote rather than to one before the 07/10/1997 base date. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. +Extract information from a boleto (amount, expiration date, bank code). Returns `null` when `value` is not a valid boleto — `isValidBoleto` is checked first — so the result has to be narrowed before it is read. 2.3.0 returned `undefined` here; every getter of the package now answers an unresolved lookup with `null`, so only a strict `=== undefined` comparison is affected. Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. The cycle search never goes below the first cycle, so a `referenceDate` older than the scheme itself still resolves a factor to the oldest date that factor can denote rather than to one before the 07/10/1997 base date. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -174,6 +174,8 @@ getBoletoInfo('00190000090114971860168524522114675860000102656', { getBoletoInfo('846100000005246100291102005460339004695895061080'); // { amount: 2461, expirationDate: null, bankCode: '', type: 'arrecadacao', segment: 4, value: 24.61, hasEffectiveValue: true } + +getBoletoInfo('invalid'); // null ``` ## isValidPixKey @@ -245,7 +247,7 @@ parsePixPayload( ## generatePixPayload -Generates the payload of a Pix BR Code. Exactly one of `params.key` or `params.url` must be given (part of `GeneratePixPayloadParams`); `null` is returned when both or neither are given. `url` must be a PSP location as the Bacen manual defines it: a host name with a path, without a scheme (`pix.example.com/qr/v2/1234`); a dynamic payload cannot carry `amount` or `txid`, which belong to the PSP location. The amount is written with the two decimal places the BR Code takes, so one that rounds to `0.00` and one that does not survive that round trip (`0.005`, `123.456`) are both rejected rather than written as a different sum. The Pix Saque BR Code, which announces the `fss` of sub-object 26-03, is parsed by `parsePixPayload` but not generated here. +Generates the payload of a Pix BR Code. Exactly one of `params.key` or `params.url` must be given (part of `GeneratePixPayloadOptions`); `null` is returned when both or neither are given. `url` must be a PSP location as the Bacen manual defines it: a host name with a path, without a scheme (`pix.example.com/qr/v2/1234`); a dynamic payload cannot carry `amount` or `txid`, which belong to the PSP location. The amount is written with the two decimal places the BR Code takes, so one that rounds to `0.00` and one that does not survive that round trip (`0.005`, `123.456`) are both rejected rather than written as a different sum. The Pix Saque BR Code, which announces the `fss` of sub-object 26-03, is parsed by `parsePixPayload` but not generated here. When `params.key` is given, it is normalized to its DICT canonical form by `parsePixKey` and the payload is static. When `params.url` is given instead (the PSP location, without a URL scheme, e.g. `"pix.example.com/qr/v2/1234"`), the payload is dynamic per the Manual de Padrões para Iniciação do Pix: the URL takes the key's place in the "Merchant Account Information" template and the "Point of Initiation Method" object is set to dynamic (`12`); `params.url` can be at most 77 characters. `merchantName`, `merchantCity` and `description` are folded to printable ASCII (accents dropped) and truncated to what the BR Code allows. `parsePixPayload` already parses both shapes, so `parsePixPayload(generatePixPayload({ url, ... }))` round-trips. @@ -272,7 +274,7 @@ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // nu ## isValidNfeKey -Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e (65), CT-e (57, the Conhecimento de Transporte Eletrônico instituted by the cláusula primeira of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para Outros Serviços instituted by the cláusula primeira of the [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, the CT-e Guia de Transporte de Valores instituted by the cláusula primeira of the [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed differently. Accepts whitespace between digit groups (the common display mask) and the `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the document's XML. +Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e (65), CT-e (57, the Conhecimento de Transporte Eletrônico instituted by the cláusula primeira of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para Outros Serviços instituted by the cláusula primeira of the [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, the CT-e Guia de Transporte de Valores instituted by the cláusula primeira of the [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed differently. The 44 digits may be split into the printed groups of 4 by whitespace, `.`, `-` or `/`, a run of them between two groups included, the same interchangeable mask `isValidCpf` and `isValidCnpj` accept; a separator inside a group of 4, or any other character, is rejected instead of being stripped. The `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the document's XML are stripped before that check, along with any whitespace between the prefix and the first group. The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` for the CT-e, `{1, 5, 7, 8}` for the CT-e OS, `{1, 2, 7, 8}` for the GTV-e, `{1, 2, 3}` for the MDF-e and `{1, 2}` for the BP-e, the NF3e and the NFCom. Code 8, the authorização pela SVC-SP, is assigned by the [CT-e MOC 4.00](https://dfe-portal.svrs.rs.gov.br/CTE/Documentos) only, never by the NF-e one; the domains of the [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), the [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) and the [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) come from their own manuals. For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, which forbids the twenty repeated and sequential `cNF` values it lists and a `cNF` equal to the document number. A document number of all zeros is turned down for every model, following the leiaute rather than a choice of this library: `tiposBasico_v4.00.xsd` of the [NF-e schema package](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) types `nNF` as `TNF`, whose pattern is `[1-9]{1}[0-9]{0,8}`, and the Anexo I of every other model repeats the same regex for its own number field. @@ -1315,7 +1317,7 @@ getLegalNaturesByCategory('9'); // [] ## getLegalNature -Look a legal nature code up in the official IBGE/CONCLA table. The entry also carries the CONCLA category the code is listed under, taken from its first digit. +Look a legal nature code up in the official IBGE/CONCLA table. The entry also carries the CONCLA category the code is listed under, taken from its first digit. No legal nature code starts with a zero, that first digit is the category (1 to 5), so nothing is ever padded here: a number and the string of the same digits are read identically. ```javascript import { getLegalNature } from '@brazilian-utils/brazilian-utils'; @@ -1742,7 +1744,7 @@ The `Certidao` result carries: ## formatCertidao -Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). The parameter is typed as a string because the 32 digits of a matrícula are more than a JavaScript number can hold exactly; at runtime the value is read for its digits and masked as far as they go, like in every formatter of this package, so a partial matrícula still being typed is masked progressively. +Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). A number is accepted and read as the string of its digits, like in `formatCpf`, but a full 32 digit matrícula has to be a string: that many digits are more than a JavaScript number can hold exactly. At runtime the value is read for its digits and masked as far as they go, like in every formatter of this package, so a partial matrícula still being typed is masked progressively. ```javascript import { formatCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1750,6 +1752,7 @@ import { formatCertidao } from '@brazilian-utils/brazilian-utils'; formatCertidao('10453901552013100012021000012321'); // 104539 01 55 2013 1 00012 021 0000123 21 formatCertidao('104539.01.55.2013.1.00012.021.0000123-21'); // 104539 01 55 2013 1 00012 021 0000123 21 formatCertidao('1552010100020112000012087', { pad: true }); // 000000 01 55 2010 1 00020 112 0000120 87 +formatCertidao(104539015520); // 104539 01 55 20 (a number is read as the string of its digits) ``` ## isValidCei diff --git a/src/format-certidao/format-certidao.test.ts b/src/format-certidao/format-certidao.test.ts index fc86c88f..8717f11f 100644 --- a/src/format-certidao/format-certidao.test.ts +++ b/src/format-certidao/format-certidao.test.ts @@ -61,8 +61,7 @@ describe("formatCertidao", () => { }); describe("should read a number as the string of its digits, like formatCpf", () => { - test("masking it as far as it goes; the parameter is typed as a string only because 32 digits do not fit a number", () => { - // @ts-expect-error: intentionally invalid input + test("masking it as far as it goes; a full 32 digit matrícula still has to be a string", () => { expect(formatCertidao(104_539_015_520)).toBe("104539 01 55 20"); }); }); @@ -102,7 +101,6 @@ describe("formatCertidao", () => { fc.assert( fc.property(fc.string({ unit: "grapheme" }), fc.integer(), (text, number) => { expect(typeof formatCertidao(text)).toBe("string"); - // @ts-expect-error: intentionally invalid input expect(typeof formatCertidao(number)).toBe("string"); }), ); @@ -111,8 +109,8 @@ describe("formatCertidao", () => { }); describe("formatCertidao types", () => { - test("should take a string, optional options, and return a string", () => { - expectTypeOf(formatCertidao).parameter(0).toEqualTypeOf(); + test("should take a string or number, optional options, and return a string", () => { + expectTypeOf(formatCertidao).parameter(0).toEqualTypeOf(); expectTypeOf(formatCertidao).parameter(1).toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); expectTypeOf(formatCertidao).returns.toEqualTypeOf(); diff --git a/src/format-certidao/format-certidao.ts b/src/format-certidao/format-certidao.ts index 72a6e462..cfe354a6 100644 --- a/src/format-certidao/format-certidao.ts +++ b/src/format-certidao/format-certidao.ts @@ -13,12 +13,12 @@ export type FormatCertidaoOptions = { * Formats the matrícula of a certidão de registro civil into the printed mask of the norm, the * 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. * - * The parameter is typed as a string because the 32 digits of a matrícula are more than a - * JavaScript number can hold exactly. At runtime the value is read for its digits and masked as - * far as they go, like in every formatter of this package, so a partial matrícula still being - * typed is masked progressively and a number is read as the string of its digits. + * A number is accepted and read as the string of its digits, like in `formatCpf`, but a full 32 + * digit matrícula has to be a string: that many digits are more than a JavaScript number can hold + * exactly. At runtime the value is read for its digits and masked as far as they go, like in every + * formatter of this package, so a partial matrícula still being typed is masked progressively. * - * @param {string} value - The matrícula value to be formatted. + * @param {string|number} value - The matrícula value to be formatted. * @param {FormatCertidaoOptions} [options] - Optional formatting options. * @param {boolean} options.pad - If true, pads the value with leading zeros if necessary. * @returns {string} The formatted matrícula in the pattern "000000 00 00 0000 0 00000 000 0000000 00". @@ -33,6 +33,8 @@ export type FormatCertidaoOptions = { * * formatCertidao("1552010100020112000012087", { pad: true }); * // "000000 01 55 2010 1 00020 112 0000120 87" + * + * formatCertidao(104539015520); // "104539 01 55 20" (a number is read as the string of its digits) * ``` * * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 @@ -59,7 +61,7 @@ export type FormatCertidaoOptions = { * @see Based on: https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php * Third reference implementation agreeing on the weights and on the remainder of 10 read as 1. */ -export const formatCertidao = (value: string, options?: FormatCertidaoOptions): string => +export const formatCertidao = (value: string | number, options?: FormatCertidaoOptions): string => isNullish(value) ? "" : format({ diff --git a/src/format-nfe-key/format-nfe-key.test.ts b/src/format-nfe-key/format-nfe-key.test.ts index c5b34aaf..87e57578 100644 --- a/src/format-nfe-key/format-nfe-key.test.ts +++ b/src/format-nfe-key/format-nfe-key.test.ts @@ -1,9 +1,9 @@ import * as fc from "fast-check"; import { anyGarbage } from "../_internals/test/arbitraries"; -import { expectNeverThrows } from "../_internals/test/properties"; +import { expectNeverThrows, expectPadsToLength } from "../_internals/test/properties"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; -import { formatNfeKey } from "./format-nfe-key"; +import { formatNfeKey, type FormatNfeKeyOptions } from "./format-nfe-key"; const KEY = "35170458716523000119550010000000121000123458"; const FORMATTED = "3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458"; @@ -25,6 +25,31 @@ describe("formatNfeKey", () => { expect(formatNfeKey(`${KEY}999999`)).toBe(FORMATTED); }); + describe("should left pad the value", () => { + test("when options.pad is true", () => { + expect(formatNfeKey("12345", { pad: true })).toBe( + "0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 2345", + ); + }); + + test("keeping a complete access key untouched", () => { + expect(formatNfeKey(KEY, { pad: true })).toBe(FORMATTED); + }); + + test("and nothing else when options.pad is false, undefined or the options object is missing", () => { + expect(formatNfeKey("12345", { pad: false })).toBe("1234 5"); + expect(formatNfeKey("12345", {})).toBe("1234 5"); + expect(formatNfeKey("12345")).toBe("1234 5"); + }); + + test("without throwing when the options object is not one", () => { + // @ts-expect-error: intentionally invalid input + expect(formatNfeKey("12345", null)).toBe("1234 5"); + // @ts-expect-error: intentionally invalid input + expect(formatNfeKey("12345", "pad")).toBe("1234 5"); + }); + }); + test("should remove all non numeric characters, including the NFe prefix", () => { expect(formatNfeKey(`NFe${KEY}`)).toBe(FORMATTED); expect(formatNfeKey(FORMATTED)).toBe(FORMATTED); @@ -91,6 +116,15 @@ describe("formatNfeKey", () => { ); }); + test("should left pad a shorter value up to the access key length", () => { + expectPadsToLength( + formatNfeKey, + (value) => value.replaceAll(/\D/g, ""), + fc.stringMatching(/^[0-9]{0,44}$/), + 44, + ); + }); + test("should never throw for any garbage input", () => { expectNeverThrows(formatNfeKey, anyGarbage); }); @@ -98,8 +132,10 @@ describe("formatNfeKey", () => { }); describe("formatNfeKey types", () => { - test("should take a string and return a string", () => { + test("should take a string, optional options, and return a string", () => { expectTypeOf(formatNfeKey).parameter(0).toEqualTypeOf(); + expectTypeOf(formatNfeKey).parameter(1).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); expectTypeOf(formatNfeKey).returns.toEqualTypeOf(); }); }); diff --git a/src/format-nfe-key/format-nfe-key.ts b/src/format-nfe-key/format-nfe-key.ts index fb76dbf5..ade8d888 100644 --- a/src/format-nfe-key/format-nfe-key.ts +++ b/src/format-nfe-key/format-nfe-key.ts @@ -3,6 +3,12 @@ import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { PATTERN } from "./constants"; +/** Options of `formatNfeKey`. */ +export type FormatNfeKeyOptions = { + /** Whether to left pad the value with zeros up to the 44 digits of a complete access key (default: `false`). */ + pad?: boolean; +}; + /** * Formats a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) into groups of 4 * digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the @@ -14,17 +20,34 @@ import { PATTERN } from "./constants"; * without a digit (an object, `true`, an object with a null prototype) gives `""` instead of * throwing. Use `isValidNfeKey` to check a key. * + * With `pad: true` the value is first left padded with zeros to the 44 digits of a complete + * access key, so it always comes back fully grouped (`"12345"` gives + * `"0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 2345"`). + * + * The parameter is typed as a string because the 44 digits of an access key are more than a + * JavaScript number can hold exactly. At runtime a number is read as the string of its digits, + * like in every formatter of this package. + * * @param {string} value - The access key value to be formatted. + * @param {FormatNfeKeyOptions} [options] - Optional formatting options. + * @param {boolean} [options.pad] - Whether to pad the value with leading zeros. Defaults to `false`. * @returns {string} The formatted access key, e.g. "3520 0612 3456 ...". * * @example * ```typescript * formatNfeKey("35170458716523000119550010000000121000123458"); * // "3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458" + * + * formatNfeKey("12345"); // "1234 5" (partial values are grouped as far as they go) + * + * formatNfeKey("12345", { pad: true }); + * // "0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 2345" * ``` * * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf * Manual de Orientação do Contribuinte (MOC) NF-e, "chave de acesso". */ -export const formatNfeKey = (value: string): string => - isNullish(value) ? "" : format({ value: sanitizeToDigits(value), pattern: PATTERN }); +export const formatNfeKey = (value: string, options?: FormatNfeKeyOptions): string => + isNullish(value) + ? "" + : format({ pad: options?.pad, value: sanitizeToDigits(value), pattern: PATTERN }); diff --git a/src/generate-boleto/generate-boleto.test.ts b/src/generate-boleto/generate-boleto.test.ts index 318e92c9..b733e921 100644 --- a/src/generate-boleto/generate-boleto.test.ts +++ b/src/generate-boleto/generate-boleto.test.ts @@ -165,7 +165,7 @@ describe("generateBoleto", () => { const value = generateBoleto({ type }); const info = getBoletoInfo(value); - expect(info).toBeDefined(); + expect(info).not.toBeNull(); expect(info?.bankCode).toBe(type === "arrecadacao" ? "" : value.slice(0, 3)); }), ); diff --git a/src/generate-pix-payload/generate-pix-payload.test.ts b/src/generate-pix-payload/generate-pix-payload.test.ts index da8b1f81..f20a1ba1 100644 --- a/src/generate-pix-payload/generate-pix-payload.test.ts +++ b/src/generate-pix-payload/generate-pix-payload.test.ts @@ -6,7 +6,7 @@ import { generateCnpj } from "../generate-cnpj/generate-cnpj"; import { generateCpf } from "../generate-cpf/generate-cpf"; import { isValidPixPayload } from "../is-valid-pix-payload/is-valid-pix-payload"; import { type PixPointOfInitiation, parsePixPayload } from "../parse-pix-payload/parse-pix-payload"; -import { type GeneratePixPayloadParams, generatePixPayload } from "./generate-pix-payload"; +import { type GeneratePixPayloadOptions, generatePixPayload } from "./generate-pix-payload"; const BASE = { key: "123e4567-e12b-12d1-a456-426655440000", @@ -381,7 +381,7 @@ describe("generatePixPayload", () => { describe("should round-trip", () => { const ROUND_TRIPS: { name: string; - build: (index: number) => GeneratePixPayloadParams; + build: (index: number) => GeneratePixPayloadOptions; pointOfInitiation: PixPointOfInitiation; }[] = [ { @@ -521,12 +521,12 @@ describe("generatePixPayload", () => { describe("generatePixPayload types", () => { test("should take Pix payload params and return a string or null", () => { - expectTypeOf(generatePixPayload).parameter(0).toEqualTypeOf(); + expectTypeOf(generatePixPayload).parameter(0).toEqualTypeOf(); expectTypeOf(generatePixPayload).returns.toEqualTypeOf(); }); test("should restrict the params to the documented fields", () => { - expectTypeOf().toEqualTypeOf<{ + expectTypeOf().toEqualTypeOf<{ key?: string; url?: string; merchantName: string; diff --git a/src/generate-pix-payload/generate-pix-payload.ts b/src/generate-pix-payload/generate-pix-payload.ts index 93fc90ae..b635f589 100644 --- a/src/generate-pix-payload/generate-pix-payload.ts +++ b/src/generate-pix-payload/generate-pix-payload.ts @@ -43,8 +43,8 @@ import { TXID_REGEX, } from "./constants"; -/** The parameters `generatePixPayload` takes to build a Pix BR Code. */ -export type GeneratePixPayloadParams = { +/** The options `generatePixPayload` takes to build a Pix BR Code. */ +export type GeneratePixPayloadOptions = { /** The Pix key of the receiver, in any accepted form. Required unless `url` is given. */ key?: string; /** @@ -168,7 +168,7 @@ const resolveFormattedAmount = ( * does not survive that round trip (`0.005`, `123.456`) is refused rather than rounded into a * payload that asks the payer for a different sum. * - * @param {GeneratePixPayloadParams} params - The parameters of the payload. + * @param {GeneratePixPayloadOptions} params - The parameters of the payload. * @param {string} [params.key] - The Pix key of the receiver. Required unless `url` is given. * @param {string} [params.url] - The PSP location of a dynamic payload. Required unless `key` * is given. @@ -208,7 +208,7 @@ const resolveFormattedAmount = ( * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html * DICT (Diretório de Identificadores de Contas Transacionais) API specification. */ -export const generatePixPayload = (params: GeneratePixPayloadParams): string | null => { +export const generatePixPayload = (params: GeneratePixPayloadOptions): string | null => { if (isNullish(params) || typeof params !== "object") return null; const { key: keyInput, url: urlInput } = params; diff --git a/src/get-boleto-info/get-boleto-info.test.ts b/src/get-boleto-info/get-boleto-info.test.ts index 2e46991c..b4fca484 100644 --- a/src/get-boleto-info/get-boleto-info.test.ts +++ b/src/get-boleto-info/get-boleto-info.test.ts @@ -31,13 +31,22 @@ const ARRECADACAO_LINE = "846100000005246100291102005460339004695895061080"; const ARRECADACAO_BARCODE = "84610000000246100291100054603390069589506108"; describe("getBoletoInfo", () => { - describe("should return undefined", () => { + describe("should return null", () => { test("when boleto is empty string", () => { - expect(getBoletoInfo("")).toBeUndefined(); + expect(getBoletoInfo("")).toBeNull(); }); test("when boleto is invalid", () => { - expect(getBoletoInfo("00190000090114971860168524522114775860000102656")).toBeUndefined(); + expect(getBoletoInfo("00190000090114971860168524522114775860000102656")).toBeNull(); + }); + + test("when boleto is not a string, never undefined, as every other getter answers", () => { + // @ts-expect-error: intentionally invalid input + expect(getBoletoInfo(null)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(getBoletoInfo()).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(getBoletoInfo(123)).toBeNull(); }); }); @@ -224,17 +233,17 @@ describe("getBoletoInfo", () => { test("should return a value exactly when the bank slip is valid", () => { fc.assert( fc.property(fc.string(), (value) => { - expect(getBoletoInfo(value) !== undefined).toBe(isValidBoleto(value)); + expect(getBoletoInfo(value) !== null).toBe(isValidBoleto(value)); }), ); }); - test("should never throw and always return an object or undefined", () => { + test("should never throw and always return an object or null", () => { fc.assert( fc.property(fc.anything(), (value) => { const info = getBoletoInfo(value as string); - expect(info === undefined || typeof info === "object").toBe(true); + expect(info === null || typeof info === "object").toBe(true); }), ); }); @@ -242,10 +251,10 @@ describe("getBoletoInfo", () => { }); describe("getBoletoInfo types", () => { - test("should take a string, optional options, and return boleto info or undefined", () => { + test("should take a string, optional options, and return boleto info or null", () => { expectTypeOf(getBoletoInfo).parameter(0).toEqualTypeOf(); expectTypeOf(getBoletoInfo).parameter(1).toEqualTypeOf(); - expectTypeOf(getBoletoInfo).returns.toEqualTypeOf(); + expectTypeOf(getBoletoInfo).returns.toEqualTypeOf(); }); test("should restrict referenceDate to a Date", () => { diff --git a/src/get-boleto-info/get-boleto-info.ts b/src/get-boleto-info/get-boleto-info.ts index 16d4024d..e0029686 100644 --- a/src/get-boleto-info/get-boleto-info.ts +++ b/src/get-boleto-info/get-boleto-info.ts @@ -83,6 +83,10 @@ export type GetBoletoInfoOptions = { /** * Extracts information from a Brazilian bank slip (boleto). * + * The value is checked with `isValidBoleto` first, so an invalid bank slip gives `null` rather + * than a partial result, the way every other getter of this package answers a lookup it cannot + * resolve (`getFormatLicensePlate`, `getMunicipality`). + * * Supports the 47 digit "cobrança bancária" linha digitável and, additionally, the * "arrecadação" (convênio/tributos) bank slip: 48 digit linha digitável or 44 digit * barcode, both starting with `8`. Arrecadação bank slips also return `type`, `segment`, @@ -101,7 +105,7 @@ export type GetBoletoInfoOptions = { * @param {string} value - The boleto digitable line (can be with or without mask). * @param {GetBoletoInfoOptions} [options] - Optional options. * @param {Date} options.referenceDate - Date used to resolve the "fator de vencimento" cycle. Defaults to now. - * @returns {BoletoInfo | undefined} An object containing amount (in cents), expirationDate, and bankCode, or undefined if the boleto is invalid. + * @returns {BoletoInfo | null} An object containing amount (in cents), expirationDate, and bankCode, or null if the boleto is invalid. * * @example * ```typescript @@ -112,6 +116,8 @@ export type GetBoletoInfoOptions = { * * getBoletoInfo('846100000005246100291102005460339004695895061080'); * // { amount: 2461, expirationDate: null, bankCode: '', type: 'arrecadacao', segment: 4, value: 24.61, hasEffectiveValue: true } + * + * getBoletoInfo('invalid'); // null * ``` * * Carta-Circular BCB nº 2.926/2000 specifies the linha digitável fields and the módulo 11 @@ -129,11 +135,8 @@ export type GetBoletoInfoOptions = { * Bradesco "Layout da Cobrança" manual: base date 07/10/1997, 03/07/2000 = 1000, 21/02/2025 = 9999 * and a restart at 1000 on 22/02/2025. */ -export const getBoletoInfo = ( - value: string, - options?: GetBoletoInfoOptions, -): BoletoInfo | undefined => { - if (!isValidBoleto(value)) return undefined; +export const getBoletoInfo = (value: string, options?: GetBoletoInfoOptions): BoletoInfo | null => { + if (!isValidBoleto(value)) return null; const sanitized = sanitizeToDigits(value); diff --git a/src/index.test.ts b/src/index.test.ts index a64fa4b0..0288bad5 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -37,7 +37,7 @@ import { type GenerateCnpjOptions, type GenerateLicensePlateFormat, type GeneratePhoneType, - type GeneratePixPayloadParams, + type GeneratePixPayloadOptions, type GenerateProcessoJuridicoOptions, type GetAddressInfoByCepOptions, type GetBoletoInfoOptions, @@ -293,7 +293,7 @@ describe("Public API", () => { GenerateCnpjOptions: GenerateCnpjOptions; GenerateLicensePlateFormat: GenerateLicensePlateFormat; GeneratePhoneType: GeneratePhoneType; - GeneratePixPayloadParams: GeneratePixPayloadParams; + GeneratePixPayloadOptions: GeneratePixPayloadOptions; GenerateProcessoJuridicoOptions: GenerateProcessoJuridicoOptions; GetAddressInfoByCepOptions: GetAddressInfoByCepOptions; GetBoletoInfoOptions: GetBoletoInfoOptions; diff --git a/src/index.ts b/src/index.ts index 79c35020..c30eeae6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,7 +57,7 @@ export { generatePassport } from "./generate-passport/generate-passport"; export { generatePhone, type GeneratePhoneType } from "./generate-phone/generate-phone"; export { generatePis } from "./generate-pis/generate-pis"; export { - type GeneratePixPayloadParams, + type GeneratePixPayloadOptions, generatePixPayload, } from "./generate-pix-payload/generate-pix-payload"; export { diff --git a/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts b/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts index 43a821c7..6eb8bfc5 100644 --- a/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts +++ b/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts @@ -14,153 +14,191 @@ describe("isValidRegistroProfissional", () => { describe("should return false", () => { test("when value is null", () => { // @ts-expect-error: intentionally invalid input - expect(isValidRegistroProfissional(null, { council: "OAB" })).toBe(false); + expect(isValidRegistroProfissional({ value: null, council: "OAB" })).toBe(false); }); test("when value is an empty string", () => { - expect(isValidRegistroProfissional("", { council: "OAB" })).toBe(false); + expect(isValidRegistroProfissional({ value: "", council: "OAB" })).toBe(false); }); - test("when options is null", () => { + test("when the single argument is not an object", () => { // @ts-expect-error: intentionally invalid input - expect(isValidRegistroProfissional("123456/SP", null)).toBe(false); + expect(isValidRegistroProfissional(null)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidRegistroProfissional()).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidRegistroProfissional("123456/SP")).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidRegistroProfissional(123_456)).toBe(false); + }); + + test("when the value is a number, even when its digits would match as a string", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidRegistroProfissional({ value: 2_412_345, council: "CRP" })).toBe(false); + }); + + test("when the object carries no value", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidRegistroProfissional({ council: "OAB" })).toBe(false); + }); + + test("when the object carries no council", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidRegistroProfissional({ value: "123456/SP" })).toBe(false); }); test("when the council is not supported (e.g. CREA)", () => { // @ts-expect-error: intentionally invalid input - expect(isValidRegistroProfissional("1234567890", { council: "CREA" })).toBe(false); + expect(isValidRegistroProfissional({ value: "1234567890", council: "CREA" })).toBe(false); }); test("when an OAB number has no UF", () => { - expect(isValidRegistroProfissional("123456", { council: "OAB" })).toBe(false); + expect(isValidRegistroProfissional({ value: "123456", council: "OAB" })).toBe(false); }); test("when an OAB number has too many digits", () => { - expect(isValidRegistroProfissional("1234567/SP", { council: "OAB" })).toBe(false); + expect(isValidRegistroProfissional({ value: "1234567/SP", council: "OAB" })).toBe(false); }); test("when the UF is not a real Brazilian state code", () => { - expect(isValidRegistroProfissional("123456/ZZ", { council: "OAB" })).toBe(false); + expect(isValidRegistroProfissional({ value: "123456/ZZ", council: "OAB" })).toBe(false); }); - test("when the UF does not match options.stateCode", () => { - expect(isValidRegistroProfissional("123456-RJ", { council: "OAB", stateCode: "SP" })).toBe( - false, - ); + test("when the UF does not match params.stateCode", () => { + expect( + isValidRegistroProfissional({ value: "123456-RJ", council: "OAB", stateCode: "SP" }), + ).toBe(false); }); test("when a CRP number has letters instead of the regional code", () => { - expect(isValidRegistroProfissional("SP/12345", { council: "CRP" })).toBe(false); + expect(isValidRegistroProfissional({ value: "SP/12345", council: "CRP" })).toBe(false); }); test("when a CRC number is missing the category letter", () => { - expect(isValidRegistroProfissional("SP-123456-3", { council: "CRC" })).toBe(false); + expect(isValidRegistroProfissional({ value: "SP-123456-3", council: "CRC" })).toBe(false); }); test("when a CRC number is missing the check digit", () => { - expect(isValidRegistroProfissional("SP-123456/O", { council: "CRC" })).toBe(false); + expect(isValidRegistroProfissional({ value: "SP-123456/O", council: "CRC" })).toBe(false); }); test("when a CRC number of ordem has 5 digits instead of the 6 of the Manual de Registro", () => { - expect(isValidRegistroProfissional("SP-12345/O-3", { council: "CRC" })).toBe(false); + expect(isValidRegistroProfissional({ value: "SP-12345/O-3", council: "CRC" })).toBe(false); }); test("when a CRC number carries a letter that is not a tipo de registro", () => { - expect(isValidRegistroProfissional("SP-123456/X-3", { council: "CRC" })).toBe(false); + expect(isValidRegistroProfissional({ value: "SP-123456/X-3", council: "CRC" })).toBe(false); }); test('when a CRC number puts "T" in the tipo de registro slot, which the Manual de Registro restricts to O and P', () => { - expect(isValidRegistroProfissional("SP-123456/T-3", { council: "CRC" })).toBe(false); + expect(isValidRegistroProfissional({ value: "SP-123456/T-3", council: "CRC" })).toBe(false); }); test('when a CRC number puts "S" in the tipo de registro slot', () => { - expect(isValidRegistroProfissional("SP-123456/S-3", { council: "CRC" })).toBe(false); + expect(isValidRegistroProfissional({ value: "SP-123456/S-3", council: "CRC" })).toBe(false); }); test("when the destination UF of a transferred CRC number is not a real Brazilian state code", () => { - expect(isValidRegistroProfissional("SP-123456/O-3 T-ZZ", { council: "CRC" })).toBe(false); + expect(isValidRegistroProfissional({ value: "SP-123456/O-3 T-ZZ", council: "CRC" })).toBe( + false, + ); }); test("when a transferred CRC number carries no destination UF at all", () => { - expect(isValidRegistroProfissional("SP-123456/O-3 T", { council: "CRC" })).toBe(false); + expect(isValidRegistroProfissional({ value: "SP-123456/O-3 T", council: "CRC" })).toBe(false); }); test("when a CRP regional code is 00, below the CRP-01 of the CFP system", () => { - expect(isValidRegistroProfissional("00/12345", { council: "CRP" })).toBe(false); + expect(isValidRegistroProfissional({ value: "00/12345", council: "CRP" })).toBe(false); }); test("when a CRP regional code is 25, above the CRP-24 of the CFP system", () => { - expect(isValidRegistroProfissional("25/12345", { council: "CRP" })).toBe(false); + expect(isValidRegistroProfissional({ value: "25/12345", council: "CRP" })).toBe(false); }); test("when a CRP regional code is 99, which no Conselho Regional carries", () => { - expect(isValidRegistroProfissional("99/12345", { council: "CRP" })).toBe(false); + expect(isValidRegistroProfissional({ value: "99/12345", council: "CRP" })).toBe(false); }); }); describe("should return true", () => { test("for a valid OAB number", () => { - expect(isValidRegistroProfissional("123456/SP", { council: "OAB" })).toBe(true); + expect(isValidRegistroProfissional({ value: "123456/SP", council: "OAB" })).toBe(true); }); - test("for a valid OAB number matching options.stateCode", () => { - expect(isValidRegistroProfissional("123456-SP", { council: "OAB", stateCode: "SP" })).toBe( - true, - ); + test("for a valid OAB number matching params.stateCode", () => { + expect( + isValidRegistroProfissional({ value: "123456-SP", council: "OAB", stateCode: "SP" }), + ).toBe(true); }); test("for a valid CRM number", () => { - expect(isValidRegistroProfissional("54321/RJ", { council: "CRM" })).toBe(true); + expect(isValidRegistroProfissional({ value: "54321/RJ", council: "CRM" })).toBe(true); }); test("for a valid CRO number", () => { - expect(isValidRegistroProfissional("12345/MG", { council: "CRO" })).toBe(true); + expect(isValidRegistroProfissional({ value: "12345/MG", council: "CRO" })).toBe(true); }); - test("for a valid CRP number, ignoring options.stateCode", () => { - expect(isValidRegistroProfissional("06/12345", { council: "CRP", stateCode: "SP" })).toBe( - true, - ); + test("for a valid CRP number, ignoring params.stateCode", () => { + expect( + isValidRegistroProfissional({ value: "06/12345", council: "CRP", stateCode: "SP" }), + ).toBe(true); }); test("for the first regional code of the CFP system, CRP-01", () => { - expect(isValidRegistroProfissional("01/12345", { council: "CRP" })).toBe(true); + expect(isValidRegistroProfissional({ value: "01/12345", council: "CRP" })).toBe(true); }); test("for the last regional code of the CFP system, CRP-24", () => { - expect(isValidRegistroProfissional("24/12345", { council: "CRP" })).toBe(true); + expect(isValidRegistroProfissional({ value: "24/12345", council: "CRP" })).toBe(true); + expect(isValidRegistroProfissional({ value: "2412345", council: "CRP" })).toBe(true); }); test("for a valid CRC number of a registro originário", () => { - expect(isValidRegistroProfissional("SP-123456/O-3", { council: "CRC" })).toBe(true); + expect(isValidRegistroProfissional({ value: "SP-123456/O-3", council: "CRC" })).toBe(true); }); test("for DF-000001/P-7, the Manual de Registro's own example of a registro provisório", () => { - expect(isValidRegistroProfissional("DF-000001/P-7", { council: "CRC" })).toBe(true); + expect(isValidRegistroProfissional({ value: "DF-000001/P-7", council: "CRC" })).toBe(true); }); test("for DF-000002/O-5, the Manual de Registro's own example of a registro originário", () => { - expect(isValidRegistroProfissional("DF-000002/O-5", { council: "CRC" })).toBe(true); + expect(isValidRegistroProfissional({ value: "DF-000002/O-5", council: "CRC" })).toBe(true); }); test('for "SP-123456/O-3 T-MG", the Manual de Registro\'s own example of a registro definitivo transferido', () => { - expect(isValidRegistroProfissional("SP-123456/O-3 T-MG", { council: "CRC" })).toBe(true); + expect(isValidRegistroProfissional({ value: "SP-123456/O-3 T-MG", council: "CRC" })).toBe( + true, + ); }); test('for "TO-654321/P-8 T-SC", the Manual de Registro\'s own example of a registro provisório transferido', () => { - expect(isValidRegistroProfissional("TO-654321/P-8 T-SC", { council: "CRC" })).toBe(true); + expect(isValidRegistroProfissional({ value: "TO-654321/P-8 T-SC", council: "CRC" })).toBe( + true, + ); }); test('for "PI-111222/O-5 S-AC", the Manual de Registro\'s own example of a registro secundário', () => { - expect(isValidRegistroProfissional("PI-111222/O-5 S-AC", { council: "CRC" })).toBe(true); + expect(isValidRegistroProfissional({ value: "PI-111222/O-5 S-AC", council: "CRC" })).toBe( + true, + ); }); - test("for a transferred CRC number matching options.stateCode, which is the originating UF", () => { + test("for a transferred CRC number matching params.stateCode, which is the originating UF", () => { expect( - isValidRegistroProfissional("SP-123456/O-3 T-MG", { council: "CRC", stateCode: "SP" }), + isValidRegistroProfissional({ + value: "SP-123456/O-3 T-MG", + council: "CRC", + stateCode: "SP", + }), ).toBe(true); expect( - isValidRegistroProfissional("SP-123456/O-3 T-MG", { council: "CRC", stateCode: "MG" }), + isValidRegistroProfissional({ + value: "SP-123456/O-3 T-MG", + council: "CRC", + stateCode: "MG", + }), ).toBe(false); }); }); @@ -174,15 +212,18 @@ describe("isValidRegistroProfissional", () => { fc.assert( fc.property(states, numbers, (stateCode, number) => { for (const council of ["OAB", "CRM", "CRO"] as const) { - expect(isValidRegistroProfissional(`${number}/${stateCode}`, { council })).toBe(true); + expect(isValidRegistroProfissional({ value: `${number}/${stateCode}`, council })).toBe( + true, + ); expect( - isValidRegistroProfissional(`${number}-${stateCode}`, { council, stateCode }), + isValidRegistroProfissional({ value: `${number}-${stateCode}`, council, stateCode }), ).toBe(true); } - expect(isValidRegistroProfissional(`06/${number}`, { council: "CRP" })).toBe(true); + expect(isValidRegistroProfissional({ value: `06/${number}`, council: "CRP" })).toBe(true); expect( - isValidRegistroProfissional(`${stateCode}-${String(number).padStart(6, "0")}/O-3`, { + isValidRegistroProfissional({ + value: `${stateCode}-${String(number).padStart(6, "0")}/O-3`, council: "CRC", }), ).toBe(true); @@ -196,7 +237,7 @@ describe("isValidRegistroProfissional", () => { const value = `${String(region).padStart(2, "0")}/${number}`; const expected = region >= 1 && region <= 24; - expect(isValidRegistroProfissional(value, { council: "CRP" })).toBe(expected); + expect(isValidRegistroProfissional({ value, council: "CRP" })).toBe(expected); }), ); }); @@ -211,7 +252,7 @@ describe("isValidRegistroProfissional", () => { const digits = String(number); const value = `${stateCode}-${digits}/${category}-3`; - expect(isValidRegistroProfissional(value, { council: "CRC" })).toBe( + expect(isValidRegistroProfissional({ value, council: "CRC" })).toBe( digits.length === 6, ); }, @@ -230,12 +271,16 @@ describe("isValidRegistroProfissional", () => { for (const suffix of ["T", "S"]) { expect( - isValidRegistroProfissional(`${number} ${suffix}-${destination}`, { + isValidRegistroProfissional({ + value: `${number} ${suffix}-${destination}`, council: "CRC", }), ).toBe(true); expect( - isValidRegistroProfissional(`${stateCode}-123456/${suffix}-3`, { council: "CRC" }), + isValidRegistroProfissional({ + value: `${stateCode}-123456/${suffix}-3`, + council: "CRC", + }), ).toBe(false); } }, @@ -250,7 +295,7 @@ describe("isValidRegistroProfissional", () => { const value = `${number}/${other}`; - expect(isValidRegistroProfissional(value, { council: "OAB", stateCode })).toBe(false); + expect(isValidRegistroProfissional({ value, council: "OAB", stateCode })).toBe(false); }), ); }); @@ -258,15 +303,15 @@ describe("isValidRegistroProfissional", () => { test("should reject a number that carries no UF at all", () => { fc.assert( fc.property(numbers, (number) => { - expect(isValidRegistroProfissional(`${number}`, { council: "CRM" })).toBe(false); + expect(isValidRegistroProfissional({ value: `${number}`, council: "CRM" })).toBe(false); }), ); }); test("should never throw and always judge a registration with a boolean", () => { fc.assert( - fc.property(fc.anything(), fc.anything(), (value, options) => { - const result = isValidRegistroProfissional(value as string, options as never); + fc.property(fc.anything(), (params) => { + const result = isValidRegistroProfissional(params as IsValidRegistroProfissionalOptions); expect(typeof result).toBe("boolean"); }), @@ -276,11 +321,14 @@ describe("isValidRegistroProfissional", () => { }); describe("isValidRegistroProfissional types", () => { - test("should take a string, required options, and return a boolean", () => { - expectTypeOf(isValidRegistroProfissional).parameter(0).toEqualTypeOf(); + test("should take a single required object and return a boolean", () => { expectTypeOf(isValidRegistroProfissional) - .parameter(1) + .parameter(0) .toEqualTypeOf(); + expectTypeOf(isValidRegistroProfissional).parameters.toEqualTypeOf< + [IsValidRegistroProfissionalOptions] + >(); + expectTypeOf().toEqualTypeOf(); expectTypeOf< IsValidRegistroProfissionalOptions["council"] >().toEqualTypeOf(); diff --git a/src/is-valid-registro-profissional/is-valid-registro-profissional.ts b/src/is-valid-registro-profissional/is-valid-registro-profissional.ts index 47e5c6d3..8f3a8217 100644 --- a/src/is-valid-registro-profissional/is-valid-registro-profissional.ts +++ b/src/is-valid-registro-profissional/is-valid-registro-profissional.ts @@ -1,4 +1,5 @@ import { DATA, type StateCode } from "../_internals/constants/states"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; import { CRC_REGEX, @@ -12,8 +13,10 @@ import { export type { StateCode } from "../_internals/constants/states"; -/** The options `isValidRegistroProfissional` takes: the professional council and, optionally, the UF the registration must belong to. */ +/** The registration `isValidRegistroProfissional` checks: the number, the council that issued it and, optionally, the UF it must belong to. */ export type IsValidRegistroProfissionalOptions = { + /** The registration number to be validated, e.g. `"123456/SP"`. */ + value: string; /** The professional council that issued the registration number. */ council: RegistroProfissionalCouncil; /** The UF the registration is expected to belong to. Ignored for `"CRP"` (see below). */ @@ -42,7 +45,7 @@ const isKnownCrpRegion = (value: string): boolean => { * * This is a structural check only: it validates the digit count and, for the councils whose * number embeds the UF, that the UF is a real Brazilian state code, optionally matching - * `options.stateCode`. It never computes or asserts a check digit, even for CRC, whose format + * `params.stateCode`. It never computes or asserts a check digit, even for CRC, whose format * includes one (the digit is only checked for presence and shape). * * Supported councils and what is validated: @@ -52,7 +55,7 @@ const isKnownCrpRegion = (value: string): boolean => { * - `"CRP"` (Conselho Regional de Psicologia): 2 digit regional code + 4 to 6 digits, e.g. * `"06/12345"`. The regional code must be one of the 24 Conselhos Regionais of the CFP * system, CRP-01 to CRP-24. It is not a literal UF (some regions cover more than one state), - * so `options.stateCode` is ignored for this council. + * so `params.stateCode` is ignored for this council. * - `"CRC"` (Conselho Regional de Contabilidade): UF + 6 digits + the tipo de registro (`"O"` * Originário or `"P"` Provisório) + 1 check digit whose value is not verified, e.g. * `"SP-123456/O-3"`. The letter says nothing about the professional category: the Manual de @@ -61,7 +64,7 @@ const isKnownCrpRegion = (value: string): boolean => { * appending `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, as the * Resolução CFC nº 1.707/2023, art. 5º, parágrafo único, and the Manual's own examples * (`"SP-123456/O-3 T-MG"`, `"TO-654321/P-8 T-SC"`, `"PI-111222/O-5 S-AC"`) put it. Both UFs - * have to be real state codes; `options.stateCode` is compared against the originating one, + * have to be real state codes; `params.stateCode` is compared against the originating one, * the UF the número do Registro Originário belongs to. * * CREA (Conselho Regional de Engenharia e Agronomia) is not supported: since the 2016 national @@ -78,23 +81,28 @@ const isKnownCrpRegion = (value: string): boolean => { * prefixed CRM for foreign-trained physicians and a trailing `P` for inscrição provisória, * neither of which the accepted shape can express. * - * @param {string} value - The registration number to be validated. - * @param {IsValidRegistroProfissionalOptions} options - The validation options. - * @param {RegistroProfissionalCouncil} options.council - The issuing council. - * @param {string} [options.stateCode] - The expected UF, ignored for `"CRP"`. + * Everything it needs travels in a single object, the shape `isValidBankAccount` takes: a + * registration number means nothing without the council that issued it, so the two are read + * together. A value that is not an object, or one missing `value` or `council`, is `false` like + * any other registration it cannot recognise. + * + * @param {IsValidRegistroProfissionalOptions} params - The registration to be validated. + * @param {string} params.value - The registration number, e.g. `"123456/SP"`. + * @param {RegistroProfissionalCouncil} params.council - The issuing council. + * @param {string} [params.stateCode] - The expected UF, ignored for `"CRP"`. * @returns {boolean} True if the value has the structure of a registration number for the * given council, false otherwise. * * @example * ```typescript - * isValidRegistroProfissional("123456/SP", { council: "OAB" }); // true - * isValidRegistroProfissional("123456-SP", { council: "OAB", stateCode: "SP" }); // true - * isValidRegistroProfissional("123456-RJ", { council: "OAB", stateCode: "SP" }); // false (UF mismatch) - * isValidRegistroProfissional("06/12345", { council: "CRP" }); // true - * isValidRegistroProfissional("SP-123456/O-3", { council: "CRC" }); // true - * isValidRegistroProfissional("SP-123456/O-3 T-MG", { council: "CRC" }); // true (transferido) - * isValidRegistroProfissional("SP-123456/T-3", { council: "CRC" }); // false ("T" is not a tipo) - * isValidRegistroProfissional("123456", { council: "OAB" }); // false (no UF) + * isValidRegistroProfissional({ value: "123456/SP", council: "OAB" }); // true + * isValidRegistroProfissional({ value: "123456-SP", council: "OAB", stateCode: "SP" }); // true + * isValidRegistroProfissional({ value: "123456-RJ", council: "OAB", stateCode: "SP" }); // false (UF mismatch) + * isValidRegistroProfissional({ value: "06/12345", council: "CRP" }); // true + * isValidRegistroProfissional({ value: "SP-123456/O-3", council: "CRC" }); // true + * isValidRegistroProfissional({ value: "SP-123456/O-3 T-MG", council: "CRC" }); // true (transferido) + * isValidRegistroProfissional({ value: "SP-123456/T-3", council: "CRC" }); // false ("T" is not a tipo) + * isValidRegistroProfissional({ value: "123456", council: "OAB" }); // false (no UF) * ``` * * @see Official: https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf @@ -122,16 +130,17 @@ const isKnownCrpRegion = (value: string): boolean => { * which publishes no format for the registration number and the UF. */ export const isValidRegistroProfissional = ( - value: string, - options: IsValidRegistroProfissionalOptions, + params: IsValidRegistroProfissionalOptions, ): boolean => { - if (typeof value !== "string") return false; + if (isNullish(params)) return false; - if (typeof options !== "object" || options === null) return false; + const { value, council, stateCode } = params; + + if (typeof value !== "string") return false; - if (!Object.hasOwn(REGEX_BY_COUNCIL, options.council)) return false; + if (!Object.hasOwn(REGEX_BY_COUNCIL, council)) return false; - const regex = REGEX_BY_COUNCIL[options.council]; + const regex = REGEX_BY_COUNCIL[council]; const match = regex.exec(sanitizeToAlphanumeric(value)); @@ -147,5 +156,5 @@ export const isValidRegistroProfissional = ( if (!isKnownStateCode(uf)) return false; - return !options.stateCode || uf === options.stateCode; + return !stateCode || uf === stateCode; }; From 2bc3a09c70cdd3989d81feabd7ea22d7749547a8 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 10:24:25 -0300 Subject: [PATCH 59/75] refactor(api): name the structured readers get*Info, as getBoletoInfo is named Every `parse*` of 2.3.0 strips the mask and returns the digits (`parseCpf("280.012.389-38")` is `"28001238938"`, a partial value passes through, the result is never null), and the name of a function that reads a record out of a value is `getBoletoInfo`. The five new readers that returned an object or `null` under the `parse*` name follow that: `parseCertidao` is `getCertidaoInfo`, `parseIban` is `getIbanInfo`, `parseNfeKey` is `getNfeKeyInfo`, `parsePixKey` is `getPixKeyInfo` and `parsePixPayload` is `getPixPayloadInfo`, with their record types renamed `CertidaoInfo`, `IbanInfo`, `NfeKeyInfo`, `PixKeyInfo` and `PixPayloadInfo`. Behaviour is unchanged. --- docs/llms-full.txt | 126 +++++++++++---- docs/llms.txt | 15 +- docs/pt-br/utilities.md | 117 ++++++++++---- docs/utilities.md | 117 ++++++++++---- src/_internals/constants/nfe-key.ts | 7 + .../generate-pix-payload.test.ts | 51 +++--- .../generate-pix-payload.ts | 14 +- .../constants.ts | 0 .../get-certidao-info.test.ts} | 58 +++---- .../get-certidao-info.ts} | 14 +- .../get-iban-info.test.ts} | 62 ++++---- .../get-iban-info.ts} | 20 +-- .../constants.ts | 11 +- .../get-nfe-key-info.test.ts} | 110 ++++++------- .../get-nfe-key-info.ts} | 19 ++- .../constants.ts | 0 .../get-pix-key-info.test.ts} | 147 +++++++++--------- .../get-pix-key-info.ts} | 26 ++-- .../get-pix-payload-info.test.ts} | 144 +++++++++-------- .../get-pix-payload-info.ts} | 14 +- src/index.test.ts | 25 +-- src/index.ts | 21 +++ .../is-valid-certidao.test.ts | 4 +- src/is-valid-certidao/is-valid-certidao.ts | 8 +- src/is-valid-nfe-key/is-valid-nfe-key.ts | 4 +- src/is-valid-pix-key/is-valid-pix-key.test.ts | 6 +- src/is-valid-pix-key/is-valid-pix-key.ts | 6 +- .../is-valid-pix-payload.ts | 4 +- vite.config.ts | 2 +- 29 files changed, 688 insertions(+), 464 deletions(-) rename src/{parse-certidao => get-certidao-info}/constants.ts (100%) rename src/{parse-certidao/parse-certidao.test.ts => get-certidao-info/get-certidao-info.test.ts} (69%) rename src/{parse-certidao/parse-certidao.ts => get-certidao-info/get-certidao-info.ts} (92%) rename src/{parse-iban/parse-iban.test.ts => get-iban-info/get-iban-info.test.ts} (73%) rename src/{parse-iban/parse-iban.ts => get-iban-info/get-iban-info.ts} (84%) rename src/{parse-nfe-key => get-nfe-key-info}/constants.ts (90%) rename src/{parse-nfe-key/parse-nfe-key.test.ts => get-nfe-key-info/get-nfe-key-info.test.ts} (61%) rename src/{parse-nfe-key/parse-nfe-key.ts => get-nfe-key-info/get-nfe-key-info.ts} (94%) rename src/{parse-pix-key => get-pix-key-info}/constants.ts (100%) rename src/{parse-pix-key/parse-pix-key.test.ts => get-pix-key-info/get-pix-key-info.test.ts} (61%) rename src/{parse-pix-key/parse-pix-key.ts => get-pix-key-info/get-pix-key-info.ts} (82%) rename src/{parse-pix-payload/parse-pix-payload.test.ts => get-pix-payload-info/get-pix-payload-info.test.ts} (79%) rename src/{parse-pix-payload/parse-pix-payload.ts => get-pix-payload-info/get-pix-payload-info.ts} (97%) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index b8315ea4..7ca08b1a 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -26,13 +26,14 @@ - [generateBoleto](#generateboleto) - [getBoletoInfo](#getboletoinfo) - [isValidPixKey](#isvalidpixkey) - - [parsePixKey](#parsepixkey) + - [getPixKeyInfo](#getpixkeyinfo) - [isValidPixPayload](#isvalidpixpayload) - - [parsePixPayload](#parsepixpayload) + - [getPixPayloadInfo](#getpixpayloadinfo) - [generatePixPayload](#generatepixpayload) - [isValidNfeKey](#isvalidnfekey) - [formatNfeKey](#formatnfekey) - [parseNfeKey](#parsenfekey) + - [getNfeKeyInfo](#getnfekeyinfo) - [isValidEmail](#isvalidemail) - [isValidPhone](#isvalidphone) - [formatPhone](#formatphone) @@ -62,6 +63,7 @@ - [isValidIban](#isvalidiban) - [formatIban](#formatiban) - [parseIban](#parseiban) + - [getIbanInfo](#getibaninfo) - [isValidCreditCard](#isvalidcreditcard) - [capitalize](#capitalize) - [formatCurrency](#formatcurrency) @@ -115,8 +117,11 @@ - [parseVoterId](#parsevoterid) - [isValidCns](#isvalidcns) - [formatCns](#formatcns) + - [parseCns](#parsecns) - [isValidCertidao](#isvalidcertidao) + - [formatCertidao](#formatcertidao) - [parseCertidao](#parsecertidao) + - [getCertidaoInfo](#getcertidaoinfo) - [formatCertidao](#formatcertidao) - [isValidCei](#isvalidcei) - [formatCei](#formatcei) @@ -244,7 +249,7 @@ Pick one style per util in a given app: a bundler treats the root import and the Here you will find all the utilities available for use. -> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `getBoletoInfo` returns `null` for an invalid boleto; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function returns an empty value of its return type: every `format*` function, `capitalize`, and the string-returning `parse*` functions (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) return `""`; `parseCurrency` returns `0`; the object/tuple parsers — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — return `null`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. +> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `getBoletoInfo` returns `null` for an invalid boleto; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function returns an empty value of its return type: every `format*` function, `capitalize`, and the string-returning `parse*` functions (`parseBoleto`, `parseCaepf`, `parseCbo`, `parseCei`, `parseCep`, `parseCertidao`, `parseCfop`, `parseCnae`, `parseCnh`, `parseCno`, `parseCnpj`, `parseCns`, `parseCpf`, `parseIban`, `parseLegalNature`, `parseLicensePlate`, `parseNcm`, `parseNfeKey`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) return `""`; `parseCurrency` returns `0`; the structured readers — `getCertidaoInfo`, `getIbanInfo`, `getNfeKeyInfo`, `getPixKeyInfo`, `getPixPayloadInfo` — return `null`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. ### isValidCpf @@ -436,21 +441,21 @@ isValidPixKey('123.456.789-09', { accept: ['email', 'evp'] }); // false isValidPixKey('not a key'); // false ``` -### parsePixKey +### getPixKeyInfo -Identifies a Pix key and normalizes it to the canonical form the DICT expects inside a BR Code: 11 digit CPF, 14 character CNPJ, lowercased e-mail, E.164 mobile phone (a landline is not a Pix key) or lowercase UUID EVP. An 11 digit value that is valid both as a CPF and as a mobile phone is read as a CPF, unless it was written as a phone number (a `+55`/`0055` prefix or a DDD wrapped in parentheses). The CPF and the phone number are recognized by the way they are written, not only by the digits they carry, so surrounding text is not stripped away and `'abc123.456.789-09'` is not a CPF key. Returns `null` when the value is not a valid Pix key. The result is typed as `PixKey`. +Identifies a Pix key and normalizes it to the canonical form the DICT expects inside a BR Code: 11 digit CPF, 14 character CNPJ, lowercased e-mail, E.164 mobile phone (a landline is not a Pix key) or lowercase UUID EVP. An 11 digit value that is valid both as a CPF and as a mobile phone is read as a CPF, unless it was written as a phone number (a `+55`/`0055` prefix or a DDD wrapped in parentheses). The CPF and the phone number are recognized by the way they are written, not only by the digits they carry, so surrounding text is not stripped away and `'abc123.456.789-09'` is not a CPF key. Returns `null` when the value is not a valid Pix key. The result is typed as `PixKeyInfo`. ```javascript -import { parsePixKey } from '@brazilian-utils/brazilian-utils'; +import { getPixKeyInfo } from '@brazilian-utils/brazilian-utils'; -parsePixKey('123.456.789-09'); // { type: 'cpf', value: '12345678909' } -parsePixKey('Fulano@Example.COM '); // { type: 'email', value: 'fulano@example.com' } -parsePixKey('(11) 98765-4321'); // { type: 'phone', value: '+5511987654321' } -parsePixKey('71C7D9BE-4B85-4E43-9F1C-1F3B8B4E9A2D'); +getPixKeyInfo('123.456.789-09'); // { type: 'cpf', value: '12345678909' } +getPixKeyInfo('Fulano@Example.COM '); // { type: 'email', value: 'fulano@example.com' } +getPixKeyInfo('(11) 98765-4321'); // { type: 'phone', value: '+5511987654321' } +getPixKeyInfo('71C7D9BE-4B85-4E43-9F1C-1F3B8B4E9A2D'); // { type: 'evp', value: '71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d' } -parsePixKey('(11) 3000-0000'); // null (a landline is not a Pix key) -parsePixKey('51998259765'); // { type: 'cpf', value: '51998259765' } (also a valid phone) -parsePixKey('+5551998259765'); // { type: 'phone', value: '+5551998259765' } +getPixKeyInfo('(11) 3000-0000'); // null (a landline is not a Pix key) +getPixKeyInfo('51998259765'); // { type: 'cpf', value: '51998259765' } (also a valid phone) +getPixKeyInfo('+5551998259765'); // { type: 'phone', value: '+5551998259765' } ``` ### isValidPixPayload @@ -468,14 +473,14 @@ isValidPixPayload( isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (broken CRC) ``` -### parsePixPayload +### getPixPayloadInfo -Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The result is typed as `PixPayload`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. A `fss` written next to a PSP location returns `null`: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template of §2.6. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Unreserved Templates (IDs 80 to 99) are ignored: a "QR Code composto" of Pix Automático that also carries a payment location in 26-25 is parsed as an ordinary dynamic payload and its recurrence location is dropped, so a consumer that has to tell the two apart cannot rely on this parser. Only a payload with no Pix template at all in IDs 26 to 51 returns `null`. +Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The result is typed as `PixPayloadInfo`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. A `fss` written next to a PSP location returns `null`: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template of §2.6. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Unreserved Templates (IDs 80 to 99) are ignored: a "QR Code composto" of Pix Automático that also carries a payment location in 26-25 is parsed as an ordinary dynamic payload and its recurrence location is dropped, so a consumer that has to tell the two apart cannot rely on this parser. Only a payload with no Pix template at all in IDs 26 to 51 returns `null`. ```javascript -import { parsePixPayload } from '@brazilian-utils/brazilian-utils'; +import { getPixPayloadInfo } from '@brazilian-utils/brazilian-utils'; -parsePixPayload( +getPixPayloadInfo( '00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426655440000' + '5204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041D3D' ); @@ -489,9 +494,9 @@ parsePixPayload( ### generatePixPayload -Generates the payload of a Pix BR Code. Exactly one of `params.key` or `params.url` must be given (part of `GeneratePixPayloadOptions`); `null` is returned when both or neither are given. `url` must be a PSP location as the Bacen manual defines it: a host name with a path, without a scheme (`pix.example.com/qr/v2/1234`); a dynamic payload cannot carry `amount` or `txid`, which belong to the PSP location. The amount is written with the two decimal places the BR Code takes, so one that rounds to `0.00` and one that does not survive that round trip (`0.005`, `123.456`) are both rejected rather than written as a different sum. The Pix Saque BR Code, which announces the `fss` of sub-object 26-03, is parsed by `parsePixPayload` but not generated here. +Generates the payload of a Pix BR Code. Exactly one of `params.key` or `params.url` must be given (part of `GeneratePixPayloadOptions`); `null` is returned when both or neither are given. `url` must be a PSP location as the Bacen manual defines it: a host name with a path, without a scheme (`pix.example.com/qr/v2/1234`); a dynamic payload cannot carry `amount` or `txid`, which belong to the PSP location. The amount is written with the two decimal places the BR Code takes, so one that rounds to `0.00` and one that does not survive that round trip (`0.005`, `123.456`) are both rejected rather than written as a different sum. The Pix Saque BR Code, which announces the `fss` of sub-object 26-03, is parsed by `getPixPayloadInfo` but not generated here. -When `params.key` is given, it is normalized to its DICT canonical form by `parsePixKey` and the payload is static. When `params.url` is given instead (the PSP location, without a URL scheme, e.g. `"pix.example.com/qr/v2/1234"`), the payload is dynamic per the Manual de Padrões para Iniciação do Pix: the URL takes the key's place in the "Merchant Account Information" template and the "Point of Initiation Method" object is set to dynamic (`12`); `params.url` can be at most 77 characters. `merchantName`, `merchantCity` and `description` are folded to printable ASCII (accents dropped) and truncated to what the BR Code allows. `parsePixPayload` already parses both shapes, so `parsePixPayload(generatePixPayload({ url, ... }))` round-trips. +When `params.key` is given, it is normalized to its DICT canonical form by `getPixKeyInfo` and the payload is static. When `params.url` is given instead (the PSP location, without a URL scheme, e.g. `"pix.example.com/qr/v2/1234"`), the payload is dynamic per the Manual de Padrões para Iniciação do Pix: the URL takes the key's place in the "Merchant Account Information" template and the "Point of Initiation Method" object is set to dynamic (`12`); `params.url` can be at most 77 characters. `merchantName`, `merchantCity` and `description` are folded to printable ASCII (accents dropped) and truncated to what the BR Code allows. `getPixPayloadInfo` already parses both shapes, so `getPixPayloadInfo(generatePixPayload({ url, ... }))` round-trips. ```javascript import { generatePixPayload } from '@brazilian-utils/brazilian-utils'; @@ -552,20 +557,34 @@ formatNfeKey('12345', { pad: true }); ### parseNfeKey -Parses a DF-e access key into its fields (stateCode, year, month, taxId, model, series, number, emissionType, code, checkDigit). Accepts the same input forms as `isValidNfeKey` and returns `null` when the key is not valid. The result is typed as `NfeKey`, whose `model` is an `NfeKeyModel`. NFCom (`'62'`) and NF3e (`'66'`) spend position 36 of the key on `nSiteAutoriz`, the site of the authorizer that received the document, so for those two models the result also carries `authorizationSite` and `code` is 7 digits instead of 8. +Remove the formatting of a DF-e access key (chave de acesso), keep only digits, and cap the result to 44 digits. The `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes the `Id` attribute of the document XML puts in front of the key are stripped first, since `NF3e` carries a digit of its own; use `isValidNfeKey` to check the key and `getNfeKeyInfo` to read its fields. ```javascript import { parseNfeKey } from '@brazilian-utils/brazilian-utils'; -parseNfeKey('35170458716523000119550010000000121000123458'); +parseNfeKey('3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458'); +// '35170458716523000119550010000000121000123458' + +parseNfeKey('NFe35170458716523000119550010000000121000123458'); +// '35170458716523000119550010000000121000123458' +``` + +### getNfeKeyInfo + +Parses a DF-e access key into its fields (stateCode, year, month, taxId, model, series, number, emissionType, code, checkDigit). Accepts the same input forms as `isValidNfeKey` and returns `null` when the key is not valid. The result is typed as `NfeKeyInfo`, whose `model` is an `NfeKeyModel`. NFCom (`'62'`) and NF3e (`'66'`) spend position 36 of the key on `nSiteAutoriz`, the site of the authorizer that received the document, so for those two models the result also carries `authorizationSite` and `code` is 7 digits instead of 8. + +```javascript +import { getNfeKeyInfo } from '@brazilian-utils/brazilian-utils'; + +getNfeKeyInfo('35170458716523000119550010000000121000123458'); // { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '55', // series: 1, number: 12, emissionType: 1, code: '00012345', checkDigit: 8 } -parseNfeKey('35170458716523000119620010000000121000123450'); +getNfeKeyInfo('35170458716523000119620010000000121000123450'); // { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', // series: 1, number: 12, emissionType: 1, authorizationSite: 0, code: '0012345', checkDigit: 0 } -parseNfeKey('invalid'); // null +getNfeKeyInfo('invalid'); // null ``` ### isValidEmail @@ -1033,12 +1052,23 @@ formatIban('BR15 0000-0000.0000/1093 2840 814P-2'); // 'BR15 0000 0000 0000 1093 ### parseIban -Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups of 4 split by a single whitespace, `.`, `-` or `/`), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying a separator away from a group boundary, a run of separators or any character other than letters and digits. The result is typed as `Iban`, whose `accountType` is a `string`. +Remove IBAN formatting, keep the letters and digits, uppercase the result, and cap it to the 29 characters of a Brazilian IBAN. An IBAN carries letters as well as digits, so the value is read the way `parsePassport` reads a passport number; use `isValidIban` to check the check digits and `getIbanInfo` to read the fields. ```javascript import { parseIban } from '@brazilian-utils/brazilian-utils'; -parseIban('BR1500000000000010932840814P2'); +parseIban('BR15 0000 0000 0000 1093 2840 814P 2'); // 'BR1500000000000010932840814P2' +parseIban('br15-0000.0000/0000 1093 2840 814p-2'); // 'BR1500000000000010932840814P2' +``` + +### getIbanInfo + +Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups of 4 split by a single whitespace, `.`, `-` or `/`), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying a separator away from a group boundary, a run of separators or any character other than letters and digits. The result is typed as `IbanInfo`, whose `accountType` is a `string`. + +```javascript +import { getIbanInfo } from '@brazilian-utils/brazilian-utils'; + +getIbanInfo('BR1500000000000010932840814P2'); // { // countryCode: 'BR', // checkDigits: '15', @@ -1049,8 +1079,8 @@ parseIban('BR1500000000000010932840814P2'); // owner: '2' // } -parseIban('DE89370400440532013000'); // null (non Brazilian IBAN) -parseIban('BR15 000 00000 0000 1093 2840 814P 2'); // null (a separator inside a group) +getIbanInfo('DE89370400440532013000'); // null (non Brazilian IBAN) +getIbanInfo('BR15 000 00000 0000 1093 2840 814P 2'); // null (a separator inside a group) ``` ### isValidCreditCard @@ -1927,11 +1957,21 @@ formatCns(123456789010000); // '123 4567 8901 0000' formatCns('89010001', { pad: true }); // '000 0000 8901 0001' ``` +### parseCns + +Remove CNS (Cartão Nacional de Saúde) formatting, keep only digits, and cap the result to 15 digits. A partial value passes through as far as it goes, so it can also strip the mask off an input still being typed; use `isValidCns` to check the number itself. + +```javascript +import { parseCns } from '@brazilian-utils/brazilian-utils'; + +parseCns('123 4567 8901 0000'); // '123456789010000' +``` + ### isValidCertidao Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with the weights cycling from 2 to 10 and back through 0: the first pass starts at 2 over the 30 base digits, the second at 1 over the 31 digits that include the first check digit, and in both a remainder of 10 is read as 1. Accepts the usual mask characters and whitespace between/around groups. The layout is the one [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023) currently publishes, with inciso II and §§ 1º and 3º to 5º in the redação of the Provimento CN nº 237/2026 and the rest of the article, § 2º included, in that of the Provimento CN nº 182/2024; the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311) and got its digit structure from the also revoked [Provimento CNJ nº 3/2009, art. 7º](https://atos.cnj.jus.br/atos/detalhar/1310). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). -The serviço digits are fixed at `55`, the code [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) assigns to the registro civil das pessoas naturais, so a matrícula carrying any other pair in the ninth and tenth positions is rejected however good its check digits are. The book-type digit always has to name one of the nine book types (the same `CertidaoType` returned by `parseCertidao`), so a matrícula whose digit is `0` is rejected however good its check digits are, the same way `parseCertidao` returns `null` for it. `options.accept` (part of `IsValidCertidaoOptions`) narrows that to the listed types; it defaults to every type, and a value that is not an array falls back to that default. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. +The serviço digits are fixed at `55`, the code [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) assigns to the registro civil das pessoas naturais, so a matrícula carrying any other pair in the ninth and tenth positions is rejected however good its check digits are. The book-type digit always has to name one of the nine book types (the same `CertidaoType` returned by `getCertidaoInfo`), so a matrícula whose digit is `0` is rejected however good its check digits are, the same way `getCertidaoInfo` returns `null` for it. `options.accept` (part of `IsValidCertidaoOptions`) narrows that to the listed types; it defaults to every type, and a value that is not an array falls back to that default. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. ```javascript import { isValidCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1945,14 +1985,38 @@ isValidCertidao('104539 01 55 2013 1 00012 021 0000123 21', { accept: ['birth'] isValidCertidao('104539 01 55 2013 1 00012 021 0000123 21', { accept: ['death'] }); // false ``` +### formatCertidao + +Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). A number is accepted and read as the string of its digits, like in `formatCpf`, but a full 32 digit matrícula has to be a string: that many digits are more than a JavaScript number can hold exactly. At runtime the value is read for its digits and masked as far as they go, like in every formatter of this package, so a partial matrícula still being typed is masked progressively. + +```javascript +import { formatCertidao } from '@brazilian-utils/brazilian-utils'; + +formatCertidao('10453901552013100012021000012321'); // 104539 01 55 2013 1 00012 021 0000123 21 +formatCertidao('104539.01.55.2013.1.00012.021.0000123-21'); // 104539 01 55 2013 1 00012 021 0000123 21 +formatCertidao('1552010100020112000012087', { pad: true }); // 000000 01 55 2010 1 00020 112 0000120 87 +formatCertidao(104539015520); // 104539 01 55 20 (a number is read as the string of its digits) +``` + ### parseCertidao -Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. [Art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lists the codes 1 to 7; no CNJ primary text reachable today publishes the other two, the Anexo IV of the revoked Provimento CNJ nº 63/2017 included, which lists the same seven. The codes 8 (emancipação) and 9 (interdição) come from the references the check digit rule rests on: [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) both print the nine book list. They are kept because matrículas carrying them circulate. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. +Remove the formatting of the matrícula of a certidão de registro civil, keep only digits, and cap the result to 32 digits. This only takes the mask off: use `isValidCertidao` to check the matrícula and `getCertidaoInfo` to read its fields. ```javascript import { parseCertidao } from '@brazilian-utils/brazilian-utils'; parseCertidao('104539 01 55 2013 1 00012 021 0000123 21'); +// '10453901552013100012021000012321' +``` + +### getCertidaoInfo + +Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. [Art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lists the codes 1 to 7; no CNJ primary text reachable today publishes the other two, the Anexo IV of the revoked Provimento CNJ nº 63/2017 included, which lists the same seven. The codes 8 (emancipação) and 9 (interdição) come from the references the check digit rule rests on: [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) both print the nine book list. They are kept because matrículas carrying them circulate. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. + +```javascript +import { getCertidaoInfo } from '@brazilian-utils/brazilian-utils'; + +getCertidaoInfo('104539 01 55 2013 1 00012 021 0000123 21'); // { // registryCns: '104539', // acervo: '01', @@ -1966,10 +2030,10 @@ parseCertidao('104539 01 55 2013 1 00012 021 0000123 21'); // checkDigits: '21' // } -parseCertidao('invalid'); // null +getCertidaoInfo('invalid'); // null ``` -The `Certidao` result carries: +The `CertidaoInfo` result carries: | Key | Description | | --- | --- | diff --git a/docs/llms.txt b/docs/llms.txt index ba1a6d1a..142f9d1c 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -81,6 +81,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [formatVoterId](https://brazilian-utils.com.br/utilities.md#formatvoterid): Format a voter ID number. - [formatCns](https://brazilian-utils.com.br/utilities.md#formatcns): Format a CNS (Cartão Nacional de Saúde) number into the common display groups of 3-4-4-4 digits separated by spaces. - [formatCertidao](https://brazilian-utils.com.br/utilities.md#formatcertidao): Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. +- [formatCertidao](https://brazilian-utils.com.br/utilities.md#formatcertidao): Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. - [formatCei](https://brazilian-utils.com.br/utilities.md#formatcei): Format a CEI (Cadastro Específico do INSS) number according to the usual `00.000.00000/00` mask, the one the reference implementations of the check digit agree on (the Receita Federal does not print it). - [formatCno](https://brazilian-utils.com.br/utilities.md#formatcno): Format a CNO (Cadastro Nacional de Obras) number. - [formatCaepf](https://brazilian-utils.com.br/utilities.md#formatcaepf): Format a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number according to the usual `000.000.000/000-00` mask, the one the sources of the check digit rule agree on (the Receita Federal does not print it). @@ -92,21 +93,20 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [parseCpf](https://brazilian-utils.com.br/utilities.md#parsecpf): Remove CPF formatting, keep only digits, and cap the result to 11 digits. - [parseCnpj](https://brazilian-utils.com.br/utilities.md#parsecnpj): Remove CNPJ formatting, return a normalized value, and cap the result to 14 characters. - [parseBoleto](https://brazilian-utils.com.br/utilities.md#parseboleto): Remove boleto formatting, keep only digits, and cap the result to 47 digits (48 for boleto de arrecadação). -- [parsePixKey](https://brazilian-utils.com.br/utilities.md#parsepixkey): Identifies a Pix key and normalizes it to the canonical form the DICT expects inside a BR Code: 11 digit CPF, 14 character CNPJ, lowercased e-mail, E.164 mobile phone (a landline is not a Pix key) or lowercase UUID EVP. -- [parsePixPayload](https://brazilian-utils.com.br/utilities.md#parsepixpayload): Parses a Pix BR Code payload into its fields. -- [parseNfeKey](https://brazilian-utils.com.br/utilities.md#parsenfekey): Parses a DF-e access key into its fields (stateCode, year, month, taxId, model, series, number, emissionType, code, checkDigit). +- [parseNfeKey](https://brazilian-utils.com.br/utilities.md#parsenfekey): Remove the formatting of a DF-e access key (chave de acesso), keep only digits, and cap the result to 44 digits. - [parsePhone](https://brazilian-utils.com.br/utilities.md#parsephone): Remove phone formatting, keep only digits, and cap the result to 11 digits. - [parsePis](https://brazilian-utils.com.br/utilities.md#parsepis): Remove PIS formatting, keep only digits, and cap the result to 11 digits. - [parseCep](https://brazilian-utils.com.br/utilities.md#parsecep): Remove CEP formatting, keep only digits, and cap the result to 8 digits. - [parseProcessoJuridico](https://brazilian-utils.com.br/utilities.md#parseprocessojuridico): Remove processo jurídico formatting, keep only digits, and cap the result to 20 digits. -- [parseIban](https://brazilian-utils.com.br/utilities.md#parseiban): Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). +- [parseIban](https://brazilian-utils.com.br/utilities.md#parseiban): Remove IBAN formatting, keep the letters and digits, uppercase the result, and cap it to the 29 characters of a Brazilian IBAN. - [parseCurrency](https://brazilian-utils.com.br/utilities.md#parsecurrency): Transforms a string to an integer or float format. - [parsePassport](https://brazilian-utils.com.br/utilities.md#parsepassport): Remove all non-alphanumeric characters from a passport number, uppercase the result, and cap it to 8 characters. - [parseCnh](https://brazilian-utils.com.br/utilities.md#parsecnh): Remove CNH formatting, keep only digits, and cap the result to 11 digits. - [parseLegalNature](https://brazilian-utils.com.br/utilities.md#parselegalnature): Remove legal nature formatting, keep only digits, and cap the result to 4 digits. - [parseLicensePlate](https://brazilian-utils.com.br/utilities.md#parselicenseplate): Remove separators from a license plate, normalize it to uppercase, and cap it to 7 characters. - [parseVoterId](https://brazilian-utils.com.br/utilities.md#parsevoterid): Remove voter ID formatting, keep only digits, and cap the result to 12 digits (13 when the UF digits identify São Paulo or Minas Gerais). -- [parseCertidao](https://brazilian-utils.com.br/utilities.md#parsecertidao): Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. +- [parseCns](https://brazilian-utils.com.br/utilities.md#parsecns): Remove CNS (Cartão Nacional de Saúde) formatting, keep only digits, and cap the result to 15 digits. +- [parseCertidao](https://brazilian-utils.com.br/utilities.md#parsecertidao): Remove the formatting of the matrícula of a certidão de registro civil, keep only digits, and cap the result to 32 digits. ## Generators (generate*) @@ -128,12 +128,16 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' ## Getters (get*) - [getBoletoInfo](https://brazilian-utils.com.br/utilities.md#getboletoinfo): Extract information from a boleto (amount, expiration date, bank code). +- [getPixKeyInfo](https://brazilian-utils.com.br/utilities.md#getpixkeyinfo): Identifies a Pix key and normalizes it to the canonical form the DICT expects inside a BR Code: 11 digit CPF, 14 character CNPJ, lowercased e-mail, E.164 mobile phone (a landline is not a Pix key) or lowercase UUID EVP. +- [getPixPayloadInfo](https://brazilian-utils.com.br/utilities.md#getpixpayloadinfo): Parses a Pix BR Code payload into its fields. +- [getNfeKeyInfo](https://brazilian-utils.com.br/utilities.md#getnfekeyinfo): Parses a DF-e access key into its fields (stateCode, year, month, taxId, model, series, number, emissionType, code, checkDigit). - [getAreaCodeInfo](https://brazilian-utils.com.br/utilities.md#getareacodeinfo): Get the state (and its region) a Brazilian DDD (area code) belongs to, out of the 67 DDDs in use under the Anatel Plano Geral de Numeração. - [getAreaCodesByState](https://brazilian-utils.com.br/utilities.md#getareacodesbystate): Get every DDD (area code) that serves a given Brazilian state, under the Anatel Plano Geral de Numeração. - [getAddressInfoByCep](https://brazilian-utils.com.br/utilities.md#getaddressinfobycep): Fetch address information for a given CEP using multiple providers. - [getBanks](https://brazilian-utils.com.br/utilities.md#getbanks): Get every Brazilian bank with a compensation code (COMPE), published by Banco Central do Brasil in the STR participants list. - [getBankByCode](https://brazilian-utils.com.br/utilities.md#getbankbycode): Look a Brazilian bank up by its compensation code (COMPE), published by Banco Central do Brasil in the STR participants list. - [getBankByIspb](https://brazilian-utils.com.br/utilities.md#getbankbyispb): Look a Brazilian bank up by its ISPB (Identificador do Sistema de Pagamentos Brasileiro), the 8 digit code published by Banco Central do Brasil in the STR participants list. +- [getIbanInfo](https://brazilian-utils.com.br/utilities.md#getibaninfo): Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). - [getStates](https://brazilian-utils.com.br/utilities.md#getstates): Get all Brazilian states, each with its two-letter code, name, region code, region name and 2-digit IBGE code of the Federative Unit (`cUF`). - [getStateByIbgeCode](https://brazilian-utils.com.br/utilities.md#getstatebyibgecode): Get the Brazilian state whose 2-digit IBGE code ("cUF", the Código da Unidade da Federação) matches the given value. - [getStateCodeByName](https://brazilian-utils.com.br/utilities.md#getstatecodebyname): Get the two-letter code (sigla) of a Brazilian state given its full name. @@ -149,6 +153,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [getMunicipality](https://brazilian-utils.com.br/utilities.md#getmunicipality): Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. - [getMunicipalities](https://brazilian-utils.com.br/utilities.md#getmunicipalities): Get Brazilian municipalities published by the IBGE. - [getMunicipalityByCode](https://brazilian-utils.com.br/utilities.md#getmunicipalitybycode): Look up a Brazilian municipality by its 7-digit IBGE code. +- [getCertidaoInfo](https://brazilian-utils.com.br/utilities.md#getcertidaoinfo): Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. - [getCbo](https://brazilian-utils.com.br/utilities.md#getcbo): Look a CBO (Classificação Brasileira de Ocupações) code up and get its official occupation title, in the `{ code, description }` record every lookup of this library returns. - [getCnae](https://brazilian-utils.com.br/utilities.md#getcnae): Look a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up and get its code and official description. - [getCfop](https://brazilian-utils.com.br/utilities.md#getcfop): Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description, as the consolidated Anexo II of Convênio SINIEF s/nº 1970 words it, in the text in force, last amended by Ajuste SINIEF 39/25. diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 0e0e370e..4c683d7e 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -2,7 +2,7 @@ Aqui você encontrará todos os utilitários disponíveis para uso. -> **Tratamento de entrada:** nenhuma função pública síncrona lança exceção com `null`/`undefined` ou um valor de tipo incorreto; as duas funções de rede, `getAddressInfoByCep` e `getCepInfoByAddress`, rejeitam com seus erros tipados (veja as seções delas). Os validadores (`isValid*`) retornam `false`; `isHoliday` retorna `false`; `getHolidays` retorna `[]`; `getBoletoInfo` retorna `null` para um boleto inválido; `generateProcessoJuridico` retorna `null`; `getMunicipality` retorna `null` para uma busca malformada/sem correspondência. Todas as demais funções `format*`/`parse*` retornam um valor vazio do seu tipo de retorno: toda função `format*`, `capitalize`, e as funções `parse*` que retornam string (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) retornam `""`; `parseCurrency` retorna `0`; os parsers que retornam objeto/tupla — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — retornam `null`. `formatCurrency` retorna `""` para um número não finito e para um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo); `null`, arrays e booleanos passam por `Number()` como no 2.3.0. A única exceção à promessa acima: um objeto criado com `Object.create(null)` não tem `toString`, então as funções `format*`/`parse*` que leem a entrada como texto ainda lançam um `TypeError` para ele, exatamente como na 2.3.0. +> **Tratamento de entrada:** nenhuma função pública síncrona lança exceção com `null`/`undefined` ou um valor de tipo incorreto; as duas funções de rede, `getAddressInfoByCep` e `getCepInfoByAddress`, rejeitam com seus erros tipados (veja as seções delas). Os validadores (`isValid*`) retornam `false`; `isHoliday` retorna `false`; `getHolidays` retorna `[]`; `getBoletoInfo` retorna `null` para um boleto inválido; `generateProcessoJuridico` retorna `null`; `getMunicipality` retorna `null` para uma busca malformada/sem correspondência. Todas as demais funções `format*`/`parse*` retornam um valor vazio do seu tipo de retorno: toda função `format*`, `capitalize`, e as funções `parse*` que retornam string (`parseBoleto`, `parseCaepf`, `parseCbo`, `parseCei`, `parseCep`, `parseCertidao`, `parseCfop`, `parseCnae`, `parseCnh`, `parseCno`, `parseCnpj`, `parseCns`, `parseCpf`, `parseIban`, `parseLegalNature`, `parseLicensePlate`, `parseNcm`, `parseNfeKey`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) retornam `""`; `parseCurrency` retorna `0`; os leitores estruturados — `getCertidaoInfo`, `getIbanInfo`, `getNfeKeyInfo`, `getPixKeyInfo`, `getPixPayloadInfo` — retornam `null`. `formatCurrency` retorna `""` para um número não finito e para um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo); `null`, arrays e booleanos passam por `Number()` como no 2.3.0. A única exceção à promessa acima: um objeto criado com `Object.create(null)` não tem `toString`, então as funções `format*`/`parse*` que leem a entrada como texto ainda lançam um `TypeError` para ele, exatamente como na 2.3.0. ## isValidCpf @@ -194,21 +194,21 @@ isValidPixKey('123.456.789-09', { accept: ['email', 'evp'] }); // false isValidPixKey('not a key'); // false ``` -## parsePixKey +## getPixKeyInfo -Identifica uma chave Pix e a normaliza para a forma canônica que o DICT espera dentro do BR Code: CPF com 11 dígitos, CNPJ com 14 caracteres, e-mail em minúsculas, telefone celular em E.164 (um telefone fixo não é chave Pix) ou UUID em minúsculas (EVP). Um valor de 11 dígitos válido tanto como CPF quanto como celular é lido como CPF, a menos que tenha sido escrito como telefone (prefixo `+55`/`0055` ou DDD entre parênteses). O CPF e o telefone são reconhecidos pela forma como são escritos, não apenas pelos dígitos que carregam, então texto ao redor não é descartado e `'abc123.456.789-09'` não é uma chave CPF. Retorna `null` quando o valor não é uma chave Pix válida. O resultado é tipado como `PixKey`. +Identifica uma chave Pix e a normaliza para a forma canônica que o DICT espera dentro do BR Code: CPF com 11 dígitos, CNPJ com 14 caracteres, e-mail em minúsculas, telefone celular em E.164 (um telefone fixo não é chave Pix) ou UUID em minúsculas (EVP). Um valor de 11 dígitos válido tanto como CPF quanto como celular é lido como CPF, a menos que tenha sido escrito como telefone (prefixo `+55`/`0055` ou DDD entre parênteses). O CPF e o telefone são reconhecidos pela forma como são escritos, não apenas pelos dígitos que carregam, então texto ao redor não é descartado e `'abc123.456.789-09'` não é uma chave CPF. Retorna `null` quando o valor não é uma chave Pix válida. O resultado é tipado como `PixKeyInfo`. ```javascript -import { parsePixKey } from '@brazilian-utils/brazilian-utils'; +import { getPixKeyInfo } from '@brazilian-utils/brazilian-utils'; -parsePixKey('123.456.789-09'); // { type: 'cpf', value: '12345678909' } -parsePixKey('Fulano@Example.COM '); // { type: 'email', value: 'fulano@example.com' } -parsePixKey('(11) 98765-4321'); // { type: 'phone', value: '+5511987654321' } -parsePixKey('71C7D9BE-4B85-4E43-9F1C-1F3B8B4E9A2D'); +getPixKeyInfo('123.456.789-09'); // { type: 'cpf', value: '12345678909' } +getPixKeyInfo('Fulano@Example.COM '); // { type: 'email', value: 'fulano@example.com' } +getPixKeyInfo('(11) 98765-4321'); // { type: 'phone', value: '+5511987654321' } +getPixKeyInfo('71C7D9BE-4B85-4E43-9F1C-1F3B8B4E9A2D'); // { type: 'evp', value: '71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d' } -parsePixKey('(11) 3000-0000'); // null (telefone fixo não é chave Pix) -parsePixKey('51998259765'); // { type: 'cpf', value: '51998259765' } (também é um telefone válido) -parsePixKey('+5551998259765'); // { type: 'phone', value: '+5551998259765' } +getPixKeyInfo('(11) 3000-0000'); // null (telefone fixo não é chave Pix) +getPixKeyInfo('51998259765'); // { type: 'cpf', value: '51998259765' } (também é um telefone válido) +getPixKeyInfo('+5551998259765'); // { type: 'phone', value: '+5551998259765' } ``` ## isValidPixPayload @@ -226,14 +226,14 @@ isValidPixPayload( isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (CRC quebrado) ``` -## parsePixPayload +## getPixPayloadInfo -Interpreta um payload de BR Code Pix e retorna seus campos. O payload é validado pelo `isValidPixPayload` primeiro, então uma estrutura malformada, um CRC quebrado ou um objeto obrigatório ausente retornam `null` em vez de um resultado parcial. Um payload estático vem com `key`, um dinâmico com `url`. O resultado é tipado como `PixPayload`; `pointOfInitiation` está sempre presente e é tipado como `PixPointOfInitiation`, `"dynamic"` quando o payload traz uma localização de PSP ou quando o objeto "Point of Initiation Method" (`01`) é `"12"`, e `"static"` nos demais casos. As informações da conta do recebedor devem trazer exatamente um entre uma chave e uma `url` (verificada com a mesma regra de localização de PSP do `generatePixPayload`); o próprio `01` é informativo, então pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` retorna `null`. Quando um payload construído em torno de uma chave traz um valor, esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque: o §2.6 do manual do Pix coloca o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`), devolvido como `withdrawalFacilitator`, e `54` igual a `"0"` ou `"0.00"` é aceito junto dele. Rejeitar um valor zero sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. Um `fss` escrito ao lado de uma localização de PSP retorna `null`: o §2.7 do Manual de Padrões para Iniciação do Pix mapeia o QR Code dinâmico para exatamente dois subobjetos, `00` (GUI) e `25` (URL), e o `fss` pertence ao template estático do §2.6. Quando o payload traz uma localização de PSP, o valor e o `txid` são ignorados, como o manual determina. Os Unreserved Templates (IDs 80 a 99) são ignorados: um "QR Code composto" do Pix Automático que também traga uma localização de pagamento em 26-25 é interpretado como um payload dinâmico comum e sua localização de recorrência é descartada, então quem precisa distinguir os dois não pode se apoiar neste parser. Só um payload sem nenhum template Pix nos IDs 26 a 51 retorna `null`. +Interpreta um payload de BR Code Pix e retorna seus campos. O payload é validado pelo `isValidPixPayload` primeiro, então uma estrutura malformada, um CRC quebrado ou um objeto obrigatório ausente retornam `null` em vez de um resultado parcial. Um payload estático vem com `key`, um dinâmico com `url`. O resultado é tipado como `PixPayloadInfo`; `pointOfInitiation` está sempre presente e é tipado como `PixPointOfInitiation`, `"dynamic"` quando o payload traz uma localização de PSP ou quando o objeto "Point of Initiation Method" (`01`) é `"12"`, e `"static"` nos demais casos. As informações da conta do recebedor devem trazer exatamente um entre uma chave e uma `url` (verificada com a mesma regra de localização de PSP do `generatePixPayload`); o próprio `01` é informativo, então pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` retorna `null`. Quando um payload construído em torno de uma chave traz um valor, esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque: o §2.6 do manual do Pix coloca o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`), devolvido como `withdrawalFacilitator`, e `54` igual a `"0"` ou `"0.00"` é aceito junto dele. Rejeitar um valor zero sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. Um `fss` escrito ao lado de uma localização de PSP retorna `null`: o §2.7 do Manual de Padrões para Iniciação do Pix mapeia o QR Code dinâmico para exatamente dois subobjetos, `00` (GUI) e `25` (URL), e o `fss` pertence ao template estático do §2.6. Quando o payload traz uma localização de PSP, o valor e o `txid` são ignorados, como o manual determina. Os Unreserved Templates (IDs 80 a 99) são ignorados: um "QR Code composto" do Pix Automático que também traga uma localização de pagamento em 26-25 é interpretado como um payload dinâmico comum e sua localização de recorrência é descartada, então quem precisa distinguir os dois não pode se apoiar neste parser. Só um payload sem nenhum template Pix nos IDs 26 a 51 retorna `null`. ```javascript -import { parsePixPayload } from '@brazilian-utils/brazilian-utils'; +import { getPixPayloadInfo } from '@brazilian-utils/brazilian-utils'; -parsePixPayload( +getPixPayloadInfo( '00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426655440000' + '5204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041D3D' ); @@ -247,9 +247,9 @@ parsePixPayload( ## generatePixPayload -Gera o payload de um BR Code Pix. Exatamente um entre `params.key` e `params.url` deve ser informado (parte de `GeneratePixPayloadOptions`); `null` é retornado quando ambos ou nenhum são informados. `url` deve ser uma localização de PSP como o manual do Bacen define: um host com caminho, sem esquema (`pix.example.com/qr/v2/1234`); um payload dinâmico não pode carregar `amount` nem `txid`, que pertencem à localização do PSP. O valor é escrito com as duas casas decimais que o BR Code aceita, então tanto um que arredonda para `0.00` quanto um que não sobrevive a esse round-trip (`0.005`, `123.456`) são rejeitados, em vez de escritos como uma quantia diferente. O BR Code de Pix Saque, que anuncia o `fss` do subobjeto 26-03, é interpretado pelo `parsePixPayload`, mas não é gerado aqui. +Gera o payload de um BR Code Pix. Exatamente um entre `params.key` e `params.url` deve ser informado (parte de `GeneratePixPayloadOptions`); `null` é retornado quando ambos ou nenhum são informados. `url` deve ser uma localização de PSP como o manual do Bacen define: um host com caminho, sem esquema (`pix.example.com/qr/v2/1234`); um payload dinâmico não pode carregar `amount` nem `txid`, que pertencem à localização do PSP. O valor é escrito com as duas casas decimais que o BR Code aceita, então tanto um que arredonda para `0.00` quanto um que não sobrevive a esse round-trip (`0.005`, `123.456`) são rejeitados, em vez de escritos como uma quantia diferente. O BR Code de Pix Saque, que anuncia o `fss` do subobjeto 26-03, é interpretado pelo `getPixPayloadInfo`, mas não é gerado aqui. -Quando `params.key` é informado, ela é normalizada para a forma canônica do DICT pelo `parsePixKey` e o payload é estático. Quando `params.url` é informado no lugar (a localização do PSP, sem o esquema da URL, ex.: `"pix.example.com/qr/v2/1234"`), o payload é dinâmico conforme o Manual de Padrões para Iniciação do Pix: a URL ocupa o lugar da chave no template "Merchant Account Information" e o objeto "Point of Initiation Method" é definido como dinâmico (`12`); `params.url` pode ter no máximo 77 caracteres. `merchantName`, `merchantCity` e `description` são convertidos para ASCII imprimível (acentos removidos) e truncados ao que o BR Code permite. O `parsePixPayload` já interpreta os dois formatos, então `parsePixPayload(generatePixPayload({ url, ... }))` forma um round-trip. +Quando `params.key` é informado, ela é normalizada para a forma canônica do DICT pelo `getPixKeyInfo` e o payload é estático. Quando `params.url` é informado no lugar (a localização do PSP, sem o esquema da URL, ex.: `"pix.example.com/qr/v2/1234"`), o payload é dinâmico conforme o Manual de Padrões para Iniciação do Pix: a URL ocupa o lugar da chave no template "Merchant Account Information" e o objeto "Point of Initiation Method" é definido como dinâmico (`12`); `params.url` pode ter no máximo 77 caracteres. `merchantName`, `merchantCity` e `description` são convertidos para ASCII imprimível (acentos removidos) e truncados ao que o BR Code permite. O `getPixPayloadInfo` já interpreta os dois formatos, então `getPixPayloadInfo(generatePixPayload({ url, ... }))` forma um round-trip. ```javascript import { generatePixPayload } from '@brazilian-utils/brazilian-utils'; @@ -310,20 +310,34 @@ formatNfeKey('12345', { pad: true }); ## parseNfeKey -Interpreta uma chave de acesso de DF-e e retorna seus campos (stateCode, year, month, taxId, model, series, number, emissionType, code, checkDigit). Aceita as mesmas formas de entrada do `isValidNfeKey` e retorna `null` quando a chave não é válida. O resultado é tipado como `NfeKey`, cujo `model` é um `NfeKeyModel`. A NFCom (`'62'`) e a NF3e (`'66'`) gastam a posição 36 da chave com o `nSiteAutoriz`, o site do autorizador que recebeu o documento, então para esses dois modelos o resultado também traz `authorizationSite` e o `code` tem 7 dígitos em vez de 8. +Remove a formatação de uma chave de acesso de DF-e, mantém apenas os dígitos e limita o resultado a 44 dígitos. Os prefixos `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` e `NFCom` que o atributo `Id` do XML do documento coloca antes da chave são retirados primeiro, já que o `NF3e` carrega um dígito próprio; use `isValidNfeKey` para verificar a chave e `getNfeKeyInfo` para ler os campos dela. ```javascript import { parseNfeKey } from '@brazilian-utils/brazilian-utils'; -parseNfeKey('35170458716523000119550010000000121000123458'); +parseNfeKey('3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458'); +// '35170458716523000119550010000000121000123458' + +parseNfeKey('NFe35170458716523000119550010000000121000123458'); +// '35170458716523000119550010000000121000123458' +``` + +## getNfeKeyInfo + +Interpreta uma chave de acesso de DF-e e retorna seus campos (stateCode, year, month, taxId, model, series, number, emissionType, code, checkDigit). Aceita as mesmas formas de entrada do `isValidNfeKey` e retorna `null` quando a chave não é válida. O resultado é tipado como `NfeKeyInfo`, cujo `model` é um `NfeKeyModel`. A NFCom (`'62'`) e a NF3e (`'66'`) gastam a posição 36 da chave com o `nSiteAutoriz`, o site do autorizador que recebeu o documento, então para esses dois modelos o resultado também traz `authorizationSite` e o `code` tem 7 dígitos em vez de 8. + +```javascript +import { getNfeKeyInfo } from '@brazilian-utils/brazilian-utils'; + +getNfeKeyInfo('35170458716523000119550010000000121000123458'); // { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '55', // series: 1, number: 12, emissionType: 1, code: '00012345', checkDigit: 8 } -parseNfeKey('35170458716523000119620010000000121000123450'); +getNfeKeyInfo('35170458716523000119620010000000121000123450'); // { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', // series: 1, number: 12, emissionType: 1, authorizationSite: 0, code: '0012345', checkDigit: 0 } -parseNfeKey('invalid'); // null +getNfeKeyInfo('invalid'); // null ``` ## isValidEmail @@ -791,12 +805,23 @@ formatIban('BR15 0000-0000.0000/1093 2840 814P-2'); // 'BR15 0000 0000 0000 1093 ## parseIban -Interpreta um IBAN brasileiro em seus campos: 2 (código do país, sempre `BR`) + 2 (dígitos verificadores ISO 7064 MOD 97-10) + 8 (ISPB) + 5 (agência) + 10 (conta) + 1 (tipo de conta, qualquer letra, normalmente `C` para conta corrente ou `P` para conta poupança) + 1 (indicador do titular, `1` a `9` e depois `A` a `Z`). Aceita as mesmas formas de entrada que `isValidIban`, compacta ou no formato impresso da ISO 13616 (grupos de 4 separados por um único espaço em branco, `.`, `-` ou `/`), em ambos os casos com espaços em branco opcionais no início e no fim e sem diferenciar maiúsculas de minúsculas, e retorna `null` sempre que `isValidIban` retornaria `false`, inclusive quando o valor carrega um separador fora do limite de um grupo, uma sequência de separadores ou qualquer caractere além de letras e dígitos. O resultado é tipado como `Iban`, cujo `accountType` é uma `string`. +Remove a formatação do IBAN, mantém as letras e os dígitos, coloca o resultado em maiúsculas e o limita aos 29 caracteres de um IBAN brasileiro. Um IBAN carrega letras além de dígitos, então o valor é lido como o `parsePassport` lê um número de passaporte; use `isValidIban` para verificar os dígitos verificadores e `getIbanInfo` para ler os campos. ```javascript import { parseIban } from '@brazilian-utils/brazilian-utils'; -parseIban('BR1500000000000010932840814P2'); +parseIban('BR15 0000 0000 0000 1093 2840 814P 2'); // 'BR1500000000000010932840814P2' +parseIban('br15-0000.0000/0000 1093 2840 814p-2'); // 'BR1500000000000010932840814P2' +``` + +## getIbanInfo + +Interpreta um IBAN brasileiro em seus campos: 2 (código do país, sempre `BR`) + 2 (dígitos verificadores ISO 7064 MOD 97-10) + 8 (ISPB) + 5 (agência) + 10 (conta) + 1 (tipo de conta, qualquer letra, normalmente `C` para conta corrente ou `P` para conta poupança) + 1 (indicador do titular, `1` a `9` e depois `A` a `Z`). Aceita as mesmas formas de entrada que `isValidIban`, compacta ou no formato impresso da ISO 13616 (grupos de 4 separados por um único espaço em branco, `.`, `-` ou `/`), em ambos os casos com espaços em branco opcionais no início e no fim e sem diferenciar maiúsculas de minúsculas, e retorna `null` sempre que `isValidIban` retornaria `false`, inclusive quando o valor carrega um separador fora do limite de um grupo, uma sequência de separadores ou qualquer caractere além de letras e dígitos. O resultado é tipado como `IbanInfo`, cujo `accountType` é uma `string`. + +```javascript +import { getIbanInfo } from '@brazilian-utils/brazilian-utils'; + +getIbanInfo('BR1500000000000010932840814P2'); // { // countryCode: 'BR', // checkDigits: '15', @@ -807,8 +832,8 @@ parseIban('BR1500000000000010932840814P2'); // owner: '2' // } -parseIban('DE89370400440532013000'); // null (IBAN não brasileiro) -parseIban('BR15 000 00000 0000 1093 2840 814P 2'); // null (separador dentro de um grupo) +getIbanInfo('DE89370400440532013000'); // null (IBAN não brasileiro) +getIbanInfo('BR15 000 00000 0000 1093 2840 814P 2'); // null (separador dentro de um grupo) ``` ## isValidCreditCard @@ -1685,11 +1710,21 @@ formatCns(123456789010000); // '123 4567 8901 0000' formatCns('89010001', { pad: true }); // '000 0000 8901 0001' ``` +## parseCns + +Remove a formatação do CNS (Cartão Nacional de Saúde), mantém apenas os dígitos e limita o resultado a 15 dígitos. Um valor parcial passa adiante até onde vai, então também dá para tirar a máscara de um campo ainda sendo digitado; use `isValidCns` para verificar o número em si. + +```javascript +import { parseCns } from '@brazilian-utils/brazilian-utils'; + +parseCns('123 4567 8901 0000'); // '123456789010000' +``` + ## isValidCertidao Verifica se a matrícula de uma certidão de registro civil (nascimento, casamento, óbito e os demais atos mantidos por uma serventia de registro civil das pessoas naturais) é válida. A matrícula tem 32 dígitos distribuídos em 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), e os dois dígitos verificadores usam módulo 11 com os pesos ciclando de 2 a 10 e voltando por 0: o primeiro cálculo começa em 2 sobre os 30 dígitos da base, o segundo em 1 sobre os 31 dígitos que incluem o primeiro dígito verificador, e nos dois um resto 10 é lido como 1. Aceita os caracteres de máscara usuais e espaços entre e ao redor dos grupos. O layout é o publicado atualmente no [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023), com o inciso II e os §§ 1º e 3º a 5º na redação do Provimento CN nº 237/2026 e o restante do artigo, inclusive o § 2º, na do Provimento CN nº 182/2024; a própria matrícula foi instituída pelo já revogado [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311) e ganhou sua estrutura de dígitos no também revogado [Provimento CNJ nº 3/2009, art. 7º](https://atos.cnj.jus.br/atos/detalhar/1310). Os dígitos verificadores estão detalhados em [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e implementado pelo [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) e pelo [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). -Os dígitos do serviço são fixos em `55`, o código que o [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) atribui ao registro civil das pessoas naturais, então uma matrícula com qualquer outro par na nona e décima posições é rejeitada por mais que os dígitos verificadores confiram. O dígito do tipo de livro sempre precisa nomear um dos nove tipos de livro (o mesmo `CertidaoType` retornado por `parseCertidao`), então uma matrícula cujo dígito é `0` é rejeitada por mais que os dígitos verificadores confiram, do mesmo jeito que `parseCertidao` devolve `null` para ela. `options.accept` (parte de `IsValidCertidaoOptions`) restringe ainda mais aos tipos listados; o padrão é aceitar todos os tipos, e um valor que não seja um array volta para esse padrão. Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. +Os dígitos do serviço são fixos em `55`, o código que o [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) atribui ao registro civil das pessoas naturais, então uma matrícula com qualquer outro par na nona e décima posições é rejeitada por mais que os dígitos verificadores confiram. O dígito do tipo de livro sempre precisa nomear um dos nove tipos de livro (o mesmo `CertidaoType` retornado por `getCertidaoInfo`), então uma matrícula cujo dígito é `0` é rejeitada por mais que os dígitos verificadores confiram, do mesmo jeito que `getCertidaoInfo` devolve `null` para ela. `options.accept` (parte de `IsValidCertidaoOptions`) restringe ainda mais aos tipos listados; o padrão é aceitar todos os tipos, e um valor que não seja um array volta para esse padrão. Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. ```javascript import { isValidCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1703,14 +1738,38 @@ isValidCertidao('104539 01 55 2013 1 00012 021 0000123 21', { accept: ['birth'] isValidCertidao('104539 01 55 2013 1 00012 021 0000123 21', { accept: ['death'] }); // false ``` +## formatCertidao + +Formata a matrícula de uma certidão de registro civil na máscara impressa do Provimento, os 32 dígitos agrupados em 6 2 2 4 1 5 3 7 2 e separados por espaços. `options.pad` (parte de `FormatCertidaoOptions`) preenche o valor com zeros à esquerda até 32 dígitos (padrão `false`). A máscara é a do [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). Um número é aceito e lido como a string dos seus dígitos, como no `formatCpf`, mas uma matrícula completa de 32 dígitos precisa ser uma string: essa quantidade de dígitos é mais do que um número JavaScript comporta com exatidão. Em tempo de execução o valor é lido pelos seus dígitos e a máscara é aplicada até onde eles vão, como em todo formatador deste pacote, então uma matrícula parcial ainda sendo digitada é mascarada progressivamente. + +```javascript +import { formatCertidao } from '@brazilian-utils/brazilian-utils'; + +formatCertidao('10453901552013100012021000012321'); // 104539 01 55 2013 1 00012 021 0000123 21 +formatCertidao('104539.01.55.2013.1.00012.021.0000123-21'); // 104539 01 55 2013 1 00012 021 0000123 21 +formatCertidao('1552010100020112000012087', { pad: true }); // 000000 01 55 2010 1 00020 112 0000120 87 +formatCertidao(104539015520); // 104539 01 55 20 (um número é lido como a string dos seus dígitos) +``` + ## parseCertidao -Extrai os campos da matrícula de uma certidão de registro civil, retornando `null` quando a matrícula é inválida, o que inclui um código de livro que não é um dos nove livros. O [art. 473, V do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lista os códigos de 1 a 7; nenhum texto primário do CNJ acessível hoje publica os outros dois, inclusive o Anexo IV do revogado Provimento CNJ nº 63/2017, que lista os mesmos sete. Os códigos 8 (emancipação) e 9 (interdição) vêm das referências em que a regra do dígito verificador se apoia: o [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e o [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) publicam a lista dos nove livros. Eles são mantidos porque matrículas com eles circulam. Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. +Remove a formatação da matrícula de uma certidão de registro civil, mantém apenas os dígitos e limita o resultado a 32 dígitos. Isso só tira a máscara: use `isValidCertidao` para verificar a matrícula e `getCertidaoInfo` para ler os campos dela. ```javascript import { parseCertidao } from '@brazilian-utils/brazilian-utils'; parseCertidao('104539 01 55 2013 1 00012 021 0000123 21'); +// '10453901552013100012021000012321' +``` + +## getCertidaoInfo + +Extrai os campos da matrícula de uma certidão de registro civil, retornando `null` quando a matrícula é inválida, o que inclui um código de livro que não é um dos nove livros. O [art. 473, V do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lista os códigos de 1 a 7; nenhum texto primário do CNJ acessível hoje publica os outros dois, inclusive o Anexo IV do revogado Provimento CNJ nº 63/2017, que lista os mesmos sete. Os códigos 8 (emancipação) e 9 (interdição) vêm das referências em que a regra do dígito verificador se apoia: o [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e o [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) publicam a lista dos nove livros. Eles são mantidos porque matrículas com eles circulam. Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. + +```javascript +import { getCertidaoInfo } from '@brazilian-utils/brazilian-utils'; + +getCertidaoInfo('104539 01 55 2013 1 00012 021 0000123 21'); // { // registryCns: '104539', // acervo: '01', @@ -1724,10 +1783,10 @@ parseCertidao('104539 01 55 2013 1 00012 021 0000123 21'); // checkDigits: '21' // } -parseCertidao('invalid'); // null +getCertidaoInfo('invalid'); // null ``` -O resultado `Certidao` traz: +O resultado `CertidaoInfo` traz: | Chave | Descrição | | --- | --- | diff --git a/docs/utilities.md b/docs/utilities.md index fad87fc7..5c5f4111 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -2,7 +2,7 @@ Here you will find all the utilities available for use. -> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `getBoletoInfo` returns `null` for an invalid boleto; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function returns an empty value of its return type: every `format*` function, `capitalize`, and the string-returning `parse*` functions (`parseBoleto`, `parseCep`, `parseCnh`, `parseCnpj`, `parseCpf`, `parseLegalNature`, `parseLicensePlate`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) return `""`; `parseCurrency` returns `0`; the object/tuple parsers — `parseCertidao`, `parseIban`, `parseNfeKey`, `parsePixKey`, `parsePixPayload` — return `null`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. +> **Input handling:** no synchronous public function throws on `null`/`undefined` or a wrong-type value; the two network helpers, `getAddressInfoByCep` and `getCepInfoByAddress`, reject with their typed errors (see their sections). `isValid*` predicates return `false`; `isHoliday` returns `false`; `getHolidays` returns `[]`; `getBoletoInfo` returns `null` for an invalid boleto; `generateProcessoJuridico` returns `null`; `getMunicipality` returns `null` for a malformed/unmatched lookup. Every other `format*`/`parse*` function returns an empty value of its return type: every `format*` function, `capitalize`, and the string-returning `parse*` functions (`parseBoleto`, `parseCaepf`, `parseCbo`, `parseCei`, `parseCep`, `parseCertidao`, `parseCfop`, `parseCnae`, `parseCnh`, `parseCno`, `parseCnpj`, `parseCns`, `parseCpf`, `parseIban`, `parseLegalNature`, `parseLicensePlate`, `parseNcm`, `parseNfeKey`, `parsePassport`, `parsePhone`, `parsePis`, `parseProcessoJuridico`, `parseVoterId`) return `""`; `parseCurrency` returns `0`; the structured readers — `getCertidaoInfo`, `getIbanInfo`, `getNfeKeyInfo`, `getPixKeyInfo`, `getPixPayloadInfo` — return `null`. `formatCurrency` returns `""` for a non-finite number and for a value that cannot be coerced to one (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. The one exception to the promise above: an object created with `Object.create(null)` has no `toString`, so the `format*`/`parse*` helpers that read their input as text still throw a `TypeError` for it, exactly as they did in 2.3.0. ## isValidCpf @@ -194,21 +194,21 @@ isValidPixKey('123.456.789-09', { accept: ['email', 'evp'] }); // false isValidPixKey('not a key'); // false ``` -## parsePixKey +## getPixKeyInfo -Identifies a Pix key and normalizes it to the canonical form the DICT expects inside a BR Code: 11 digit CPF, 14 character CNPJ, lowercased e-mail, E.164 mobile phone (a landline is not a Pix key) or lowercase UUID EVP. An 11 digit value that is valid both as a CPF and as a mobile phone is read as a CPF, unless it was written as a phone number (a `+55`/`0055` prefix or a DDD wrapped in parentheses). The CPF and the phone number are recognized by the way they are written, not only by the digits they carry, so surrounding text is not stripped away and `'abc123.456.789-09'` is not a CPF key. Returns `null` when the value is not a valid Pix key. The result is typed as `PixKey`. +Identifies a Pix key and normalizes it to the canonical form the DICT expects inside a BR Code: 11 digit CPF, 14 character CNPJ, lowercased e-mail, E.164 mobile phone (a landline is not a Pix key) or lowercase UUID EVP. An 11 digit value that is valid both as a CPF and as a mobile phone is read as a CPF, unless it was written as a phone number (a `+55`/`0055` prefix or a DDD wrapped in parentheses). The CPF and the phone number are recognized by the way they are written, not only by the digits they carry, so surrounding text is not stripped away and `'abc123.456.789-09'` is not a CPF key. Returns `null` when the value is not a valid Pix key. The result is typed as `PixKeyInfo`. ```javascript -import { parsePixKey } from '@brazilian-utils/brazilian-utils'; +import { getPixKeyInfo } from '@brazilian-utils/brazilian-utils'; -parsePixKey('123.456.789-09'); // { type: 'cpf', value: '12345678909' } -parsePixKey('Fulano@Example.COM '); // { type: 'email', value: 'fulano@example.com' } -parsePixKey('(11) 98765-4321'); // { type: 'phone', value: '+5511987654321' } -parsePixKey('71C7D9BE-4B85-4E43-9F1C-1F3B8B4E9A2D'); +getPixKeyInfo('123.456.789-09'); // { type: 'cpf', value: '12345678909' } +getPixKeyInfo('Fulano@Example.COM '); // { type: 'email', value: 'fulano@example.com' } +getPixKeyInfo('(11) 98765-4321'); // { type: 'phone', value: '+5511987654321' } +getPixKeyInfo('71C7D9BE-4B85-4E43-9F1C-1F3B8B4E9A2D'); // { type: 'evp', value: '71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d' } -parsePixKey('(11) 3000-0000'); // null (a landline is not a Pix key) -parsePixKey('51998259765'); // { type: 'cpf', value: '51998259765' } (also a valid phone) -parsePixKey('+5551998259765'); // { type: 'phone', value: '+5551998259765' } +getPixKeyInfo('(11) 3000-0000'); // null (a landline is not a Pix key) +getPixKeyInfo('51998259765'); // { type: 'cpf', value: '51998259765' } (also a valid phone) +getPixKeyInfo('+5551998259765'); // { type: 'phone', value: '+5551998259765' } ``` ## isValidPixPayload @@ -226,14 +226,14 @@ isValidPixPayload( isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (broken CRC) ``` -## parsePixPayload +## getPixPayloadInfo -Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The result is typed as `PixPayload`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. A `fss` written next to a PSP location returns `null`: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template of §2.6. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Unreserved Templates (IDs 80 to 99) are ignored: a "QR Code composto" of Pix Automático that also carries a payment location in 26-25 is parsed as an ordinary dynamic payload and its recurrence location is dropped, so a consumer that has to tell the two apart cannot rely on this parser. Only a payload with no Pix template at all in IDs 26 to 51 returns `null`. +Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The result is typed as `PixPayloadInfo`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. A `fss` written next to a PSP location returns `null`: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template of §2.6. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Unreserved Templates (IDs 80 to 99) are ignored: a "QR Code composto" of Pix Automático that also carries a payment location in 26-25 is parsed as an ordinary dynamic payload and its recurrence location is dropped, so a consumer that has to tell the two apart cannot rely on this parser. Only a payload with no Pix template at all in IDs 26 to 51 returns `null`. ```javascript -import { parsePixPayload } from '@brazilian-utils/brazilian-utils'; +import { getPixPayloadInfo } from '@brazilian-utils/brazilian-utils'; -parsePixPayload( +getPixPayloadInfo( '00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426655440000' + '5204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041D3D' ); @@ -247,9 +247,9 @@ parsePixPayload( ## generatePixPayload -Generates the payload of a Pix BR Code. Exactly one of `params.key` or `params.url` must be given (part of `GeneratePixPayloadOptions`); `null` is returned when both or neither are given. `url` must be a PSP location as the Bacen manual defines it: a host name with a path, without a scheme (`pix.example.com/qr/v2/1234`); a dynamic payload cannot carry `amount` or `txid`, which belong to the PSP location. The amount is written with the two decimal places the BR Code takes, so one that rounds to `0.00` and one that does not survive that round trip (`0.005`, `123.456`) are both rejected rather than written as a different sum. The Pix Saque BR Code, which announces the `fss` of sub-object 26-03, is parsed by `parsePixPayload` but not generated here. +Generates the payload of a Pix BR Code. Exactly one of `params.key` or `params.url` must be given (part of `GeneratePixPayloadOptions`); `null` is returned when both or neither are given. `url` must be a PSP location as the Bacen manual defines it: a host name with a path, without a scheme (`pix.example.com/qr/v2/1234`); a dynamic payload cannot carry `amount` or `txid`, which belong to the PSP location. The amount is written with the two decimal places the BR Code takes, so one that rounds to `0.00` and one that does not survive that round trip (`0.005`, `123.456`) are both rejected rather than written as a different sum. The Pix Saque BR Code, which announces the `fss` of sub-object 26-03, is parsed by `getPixPayloadInfo` but not generated here. -When `params.key` is given, it is normalized to its DICT canonical form by `parsePixKey` and the payload is static. When `params.url` is given instead (the PSP location, without a URL scheme, e.g. `"pix.example.com/qr/v2/1234"`), the payload is dynamic per the Manual de Padrões para Iniciação do Pix: the URL takes the key's place in the "Merchant Account Information" template and the "Point of Initiation Method" object is set to dynamic (`12`); `params.url` can be at most 77 characters. `merchantName`, `merchantCity` and `description` are folded to printable ASCII (accents dropped) and truncated to what the BR Code allows. `parsePixPayload` already parses both shapes, so `parsePixPayload(generatePixPayload({ url, ... }))` round-trips. +When `params.key` is given, it is normalized to its DICT canonical form by `getPixKeyInfo` and the payload is static. When `params.url` is given instead (the PSP location, without a URL scheme, e.g. `"pix.example.com/qr/v2/1234"`), the payload is dynamic per the Manual de Padrões para Iniciação do Pix: the URL takes the key's place in the "Merchant Account Information" template and the "Point of Initiation Method" object is set to dynamic (`12`); `params.url` can be at most 77 characters. `merchantName`, `merchantCity` and `description` are folded to printable ASCII (accents dropped) and truncated to what the BR Code allows. `getPixPayloadInfo` already parses both shapes, so `getPixPayloadInfo(generatePixPayload({ url, ... }))` round-trips. ```javascript import { generatePixPayload } from '@brazilian-utils/brazilian-utils'; @@ -310,20 +310,34 @@ formatNfeKey('12345', { pad: true }); ## parseNfeKey -Parses a DF-e access key into its fields (stateCode, year, month, taxId, model, series, number, emissionType, code, checkDigit). Accepts the same input forms as `isValidNfeKey` and returns `null` when the key is not valid. The result is typed as `NfeKey`, whose `model` is an `NfeKeyModel`. NFCom (`'62'`) and NF3e (`'66'`) spend position 36 of the key on `nSiteAutoriz`, the site of the authorizer that received the document, so for those two models the result also carries `authorizationSite` and `code` is 7 digits instead of 8. +Remove the formatting of a DF-e access key (chave de acesso), keep only digits, and cap the result to 44 digits. The `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes the `Id` attribute of the document XML puts in front of the key are stripped first, since `NF3e` carries a digit of its own; use `isValidNfeKey` to check the key and `getNfeKeyInfo` to read its fields. ```javascript import { parseNfeKey } from '@brazilian-utils/brazilian-utils'; -parseNfeKey('35170458716523000119550010000000121000123458'); +parseNfeKey('3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458'); +// '35170458716523000119550010000000121000123458' + +parseNfeKey('NFe35170458716523000119550010000000121000123458'); +// '35170458716523000119550010000000121000123458' +``` + +## getNfeKeyInfo + +Parses a DF-e access key into its fields (stateCode, year, month, taxId, model, series, number, emissionType, code, checkDigit). Accepts the same input forms as `isValidNfeKey` and returns `null` when the key is not valid. The result is typed as `NfeKeyInfo`, whose `model` is an `NfeKeyModel`. NFCom (`'62'`) and NF3e (`'66'`) spend position 36 of the key on `nSiteAutoriz`, the site of the authorizer that received the document, so for those two models the result also carries `authorizationSite` and `code` is 7 digits instead of 8. + +```javascript +import { getNfeKeyInfo } from '@brazilian-utils/brazilian-utils'; + +getNfeKeyInfo('35170458716523000119550010000000121000123458'); // { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '55', // series: 1, number: 12, emissionType: 1, code: '00012345', checkDigit: 8 } -parseNfeKey('35170458716523000119620010000000121000123450'); +getNfeKeyInfo('35170458716523000119620010000000121000123450'); // { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', // series: 1, number: 12, emissionType: 1, authorizationSite: 0, code: '0012345', checkDigit: 0 } -parseNfeKey('invalid'); // null +getNfeKeyInfo('invalid'); // null ``` ## isValidEmail @@ -791,12 +805,23 @@ formatIban('BR15 0000-0000.0000/1093 2840 814P-2'); // 'BR15 0000 0000 0000 1093 ## parseIban -Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups of 4 split by a single whitespace, `.`, `-` or `/`), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying a separator away from a group boundary, a run of separators or any character other than letters and digits. The result is typed as `Iban`, whose `accountType` is a `string`. +Remove IBAN formatting, keep the letters and digits, uppercase the result, and cap it to the 29 characters of a Brazilian IBAN. An IBAN carries letters as well as digits, so the value is read the way `parsePassport` reads a passport number; use `isValidIban` to check the check digits and `getIbanInfo` to read the fields. ```javascript import { parseIban } from '@brazilian-utils/brazilian-utils'; -parseIban('BR1500000000000010932840814P2'); +parseIban('BR15 0000 0000 0000 1093 2840 814P 2'); // 'BR1500000000000010932840814P2' +parseIban('br15-0000.0000/0000 1093 2840 814p-2'); // 'BR1500000000000010932840814P2' +``` + +## getIbanInfo + +Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups of 4 split by a single whitespace, `.`, `-` or `/`), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying a separator away from a group boundary, a run of separators or any character other than letters and digits. The result is typed as `IbanInfo`, whose `accountType` is a `string`. + +```javascript +import { getIbanInfo } from '@brazilian-utils/brazilian-utils'; + +getIbanInfo('BR1500000000000010932840814P2'); // { // countryCode: 'BR', // checkDigits: '15', @@ -807,8 +832,8 @@ parseIban('BR1500000000000010932840814P2'); // owner: '2' // } -parseIban('DE89370400440532013000'); // null (non Brazilian IBAN) -parseIban('BR15 000 00000 0000 1093 2840 814P 2'); // null (a separator inside a group) +getIbanInfo('DE89370400440532013000'); // null (non Brazilian IBAN) +getIbanInfo('BR15 000 00000 0000 1093 2840 814P 2'); // null (a separator inside a group) ``` ## isValidCreditCard @@ -1685,11 +1710,21 @@ formatCns(123456789010000); // '123 4567 8901 0000' formatCns('89010001', { pad: true }); // '000 0000 8901 0001' ``` +## parseCns + +Remove CNS (Cartão Nacional de Saúde) formatting, keep only digits, and cap the result to 15 digits. A partial value passes through as far as it goes, so it can also strip the mask off an input still being typed; use `isValidCns` to check the number itself. + +```javascript +import { parseCns } from '@brazilian-utils/brazilian-utils'; + +parseCns('123 4567 8901 0000'); // '123456789010000' +``` + ## isValidCertidao Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with the weights cycling from 2 to 10 and back through 0: the first pass starts at 2 over the 30 base digits, the second at 1 over the 31 digits that include the first check digit, and in both a remainder of 10 is read as 1. Accepts the usual mask characters and whitespace between/around groups. The layout is the one [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023) currently publishes, with inciso II and §§ 1º and 3º to 5º in the redação of the Provimento CN nº 237/2026 and the rest of the article, § 2º included, in that of the Provimento CN nº 182/2024; the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311) and got its digit structure from the also revoked [Provimento CNJ nº 3/2009, art. 7º](https://atos.cnj.jus.br/atos/detalhar/1310). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). -The serviço digits are fixed at `55`, the code [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) assigns to the registro civil das pessoas naturais, so a matrícula carrying any other pair in the ninth and tenth positions is rejected however good its check digits are. The book-type digit always has to name one of the nine book types (the same `CertidaoType` returned by `parseCertidao`), so a matrícula whose digit is `0` is rejected however good its check digits are, the same way `parseCertidao` returns `null` for it. `options.accept` (part of `IsValidCertidaoOptions`) narrows that to the listed types; it defaults to every type, and a value that is not an array falls back to that default. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. +The serviço digits are fixed at `55`, the code [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) assigns to the registro civil das pessoas naturais, so a matrícula carrying any other pair in the ninth and tenth positions is rejected however good its check digits are. The book-type digit always has to name one of the nine book types (the same `CertidaoType` returned by `getCertidaoInfo`), so a matrícula whose digit is `0` is rejected however good its check digits are, the same way `getCertidaoInfo` returns `null` for it. `options.accept` (part of `IsValidCertidaoOptions`) narrows that to the listed types; it defaults to every type, and a value that is not an array falls back to that default. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. ```javascript import { isValidCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1703,14 +1738,38 @@ isValidCertidao('104539 01 55 2013 1 00012 021 0000123 21', { accept: ['birth'] isValidCertidao('104539 01 55 2013 1 00012 021 0000123 21', { accept: ['death'] }); // false ``` +## formatCertidao + +Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). A number is accepted and read as the string of its digits, like in `formatCpf`, but a full 32 digit matrícula has to be a string: that many digits are more than a JavaScript number can hold exactly. At runtime the value is read for its digits and masked as far as they go, like in every formatter of this package, so a partial matrícula still being typed is masked progressively. + +```javascript +import { formatCertidao } from '@brazilian-utils/brazilian-utils'; + +formatCertidao('10453901552013100012021000012321'); // 104539 01 55 2013 1 00012 021 0000123 21 +formatCertidao('104539.01.55.2013.1.00012.021.0000123-21'); // 104539 01 55 2013 1 00012 021 0000123 21 +formatCertidao('1552010100020112000012087', { pad: true }); // 000000 01 55 2010 1 00020 112 0000120 87 +formatCertidao(104539015520); // 104539 01 55 20 (a number is read as the string of its digits) +``` + ## parseCertidao -Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. [Art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lists the codes 1 to 7; no CNJ primary text reachable today publishes the other two, the Anexo IV of the revoked Provimento CNJ nº 63/2017 included, which lists the same seven. The codes 8 (emancipação) and 9 (interdição) come from the references the check digit rule rests on: [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) both print the nine book list. They are kept because matrículas carrying them circulate. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. +Remove the formatting of the matrícula of a certidão de registro civil, keep only digits, and cap the result to 32 digits. This only takes the mask off: use `isValidCertidao` to check the matrícula and `getCertidaoInfo` to read its fields. ```javascript import { parseCertidao } from '@brazilian-utils/brazilian-utils'; parseCertidao('104539 01 55 2013 1 00012 021 0000123 21'); +// '10453901552013100012021000012321' +``` + +## getCertidaoInfo + +Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. [Art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lists the codes 1 to 7; no CNJ primary text reachable today publishes the other two, the Anexo IV of the revoked Provimento CNJ nº 63/2017 included, which lists the same seven. The codes 8 (emancipação) and 9 (interdição) come from the references the check digit rule rests on: [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) both print the nine book list. They are kept because matrículas carrying them circulate. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. + +```javascript +import { getCertidaoInfo } from '@brazilian-utils/brazilian-utils'; + +getCertidaoInfo('104539 01 55 2013 1 00012 021 0000123 21'); // { // registryCns: '104539', // acervo: '01', @@ -1724,10 +1783,10 @@ parseCertidao('104539 01 55 2013 1 00012 021 0000123 21'); // checkDigits: '21' // } -parseCertidao('invalid'); // null +getCertidaoInfo('invalid'); // null ``` -The `Certidao` result carries: +The `CertidaoInfo` result carries: | Key | Description | | --- | --- | diff --git a/src/_internals/constants/nfe-key.ts b/src/_internals/constants/nfe-key.ts index d9b06eb8..f0bc29bc 100644 --- a/src/_internals/constants/nfe-key.ts +++ b/src/_internals/constants/nfe-key.ts @@ -1,2 +1,9 @@ /** Digits of a DF-e (NF-e, NFC-e, CT-e or MDF-e) access key (chave de acesso). */ export const NFE_KEY_LENGTH = 44; + +/** + * The prefixes the `Id` attribute of a DF-e XML puts in front of the 44 digits, one per + * document: `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom`. Stripped before the digits are + * read, since `NF3e` carries a digit of its own. Shared by `getNfeKeyInfo` and `parseNfeKey`. + */ +export const XML_ID_PREFIX_REGEX = /^(?:nfe|cte|mdfe|bpe|nf3e|nfcom)/i; diff --git a/src/generate-pix-payload/generate-pix-payload.test.ts b/src/generate-pix-payload/generate-pix-payload.test.ts index f20a1ba1..0f142d84 100644 --- a/src/generate-pix-payload/generate-pix-payload.test.ts +++ b/src/generate-pix-payload/generate-pix-payload.test.ts @@ -4,8 +4,11 @@ import { crc16Ccitt } from "../_internals/crc16-ccitt/crc16-ccitt"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { generateCnpj } from "../generate-cnpj/generate-cnpj"; import { generateCpf } from "../generate-cpf/generate-cpf"; +import { + type PixPointOfInitiation, + getPixPayloadInfo, +} from "../get-pix-payload-info/get-pix-payload-info"; import { isValidPixPayload } from "../is-valid-pix-payload/is-valid-pix-payload"; -import { type PixPointOfInitiation, parsePixPayload } from "../parse-pix-payload/parse-pix-payload"; import { type GeneratePixPayloadOptions, generatePixPayload } from "./generate-pix-payload"; const BASE = { @@ -267,8 +270,8 @@ describe("generatePixPayload", () => { expect(isValidPixPayload(generatePixPayload(DYNAMIC_BASE) ?? "")).toBe(true); }); - test("that parsePixPayload parses back with pointOfInitiation dynamic and no key", () => { - expect(parsePixPayload(generatePixPayload(DYNAMIC_BASE) ?? "")).toEqual({ + test("that getPixPayloadInfo parses back with pointOfInitiation dynamic and no key", () => { + expect(getPixPayloadInfo(generatePixPayload(DYNAMIC_BASE) ?? "")).toEqual({ url: "pix.example.com/qr/v2/1234", merchantName: "Fulano de Tal", merchantCity: "Brasilia", @@ -284,26 +287,26 @@ describe("generatePixPayload", () => { const payload = generatePixPayload({ ...DYNAMIC_BASE, url }); expect(payload).not.toBeNull(); - expect(parsePixPayload(payload ?? "")?.url).toBe(url); + expect(getPixPayloadInfo(payload ?? "")?.url).toBe(url); }); }); describe("should normalize its parameters", () => { test("folding accents out of the merchant name and city", () => { expect( - parsePixPayload(generatePixPayload({ ...BASE, merchantCity: "Brasília" }) ?? ""), + getPixPayloadInfo(generatePixPayload({ ...BASE, merchantCity: "Brasília" }) ?? ""), ).toMatchObject({ merchantCity: "Brasilia", }); expect( - parsePixPayload(generatePixPayload({ ...BASE, merchantName: "José Antônio" }) ?? ""), + getPixPayloadInfo(generatePixPayload({ ...BASE, merchantName: "José Antônio" }) ?? ""), ).toMatchObject({ merchantName: "Jose Antonio", }); }); test("trimming trailing whitespace introduced by truncating to the maximum length", () => { - const pix = parsePixPayload( + const pix = getPixPayloadInfo( generatePixPayload({ ...BASE, merchantName: `${"A".repeat(24)} B` }) ?? "", ); @@ -311,7 +314,7 @@ describe("generatePixPayload", () => { }); test("truncating the merchant name to 25 characters", () => { - const pix = parsePixPayload( + const pix = getPixPayloadInfo( generatePixPayload({ ...BASE, merchantName: "A".repeat(40) }) ?? "", ); @@ -319,7 +322,7 @@ describe("generatePixPayload", () => { }); test("truncating the merchant city to 15 characters", () => { - const pix = parsePixPayload( + const pix = getPixPayloadInfo( generatePixPayload({ ...BASE, merchantCity: "B".repeat(40) }) ?? "", ); @@ -328,17 +331,17 @@ describe("generatePixPayload", () => { test("normalizing the key to its DICT canonical form", () => { expect( - parsePixPayload(generatePixPayload({ ...BASE, key: "123.456.789-09" }) ?? "")?.key, + getPixPayloadInfo(generatePixPayload({ ...BASE, key: "123.456.789-09" }) ?? "")?.key, ).toBe("12345678909"); expect( - parsePixPayload(generatePixPayload({ ...BASE, key: "(11) 98765-4321" }) ?? "")?.key, + getPixPayloadInfo(generatePixPayload({ ...BASE, key: "(11) 98765-4321" }) ?? "")?.key, ).toBe("+5511987654321"); expect( - parsePixPayload(generatePixPayload({ ...BASE, key: " Fulano@Example.COM " }) ?? "")?.key, + getPixPayloadInfo(generatePixPayload({ ...BASE, key: " Fulano@Example.COM " }) ?? "")?.key, ).toBe("fulano@example.com"); const upperCaseEvp = EVP.toUpperCase(); - expect(parsePixPayload(generatePixPayload({ ...BASE, key: upperCaseEvp }) ?? "")?.key).toBe( + expect(getPixPayloadInfo(generatePixPayload({ ...BASE, key: upperCaseEvp }) ?? "")?.key).toBe( EVP, ); }); @@ -350,7 +353,7 @@ describe("generatePixPayload", () => { description: "y".repeat(90), }); - expect(parsePixPayload(payload ?? "")?.description).toBe("y".repeat(62)); + expect(getPixPayloadInfo(payload ?? "")?.description).toBe("y".repeat(62)); }); test("truncating the description to what a mobile phone key leaves", () => { @@ -360,21 +363,21 @@ describe("generatePixPayload", () => { description: "y".repeat(90), }); - expect(parsePixPayload(payload ?? "")?.description).toBe("y".repeat(59)); + expect(getPixPayloadInfo(payload ?? "")?.description).toBe("y".repeat(59)); }); test("leaving room for the description on a long key", () => { const key = `${"a".repeat(56)}@example.com`; const payload = generatePixPayload({ ...BASE, key, description: "z".repeat(30) }) ?? ""; - expect(parsePixPayload(payload)?.description).toBe("z".repeat(5)); + expect(getPixPayloadInfo(payload)?.description).toBe("z".repeat(5)); }); test("dropping a description that does not fit at all", () => { const key = `${"a".repeat(65)}@example.com`; const payload = generatePixPayload({ ...BASE, key, description: "z".repeat(30) }) ?? ""; - expect(parsePixPayload(payload)).not.toHaveProperty("description"); + expect(getPixPayloadInfo(payload)).not.toHaveProperty("description"); }); }); @@ -416,13 +419,13 @@ describe("generatePixPayload", () => { ]; for (const { name, build, pointOfInitiation } of ROUND_TRIPS) { - test(`through isValidPixPayload and parsePixPayload for ${name}`, () => { + test(`through isValidPixPayload and getPixPayloadInfo for ${name}`, () => { for (let index = 0; index < 200; index++) { const params = build(index); const payload = generatePixPayload(params) ?? ""; expect(isValidPixPayload(payload)).toBe(true); - expect(parsePixPayload(payload)).toEqual({ ...params, pointOfInitiation }); + expect(getPixPayloadInfo(payload)).toEqual({ ...params, pointOfInitiation }); } }); } @@ -439,11 +442,11 @@ describe("generatePixPayload", () => { const cents = fc.integer({ min: 1, max: 9_999_999 }); - test("should round-trip a static payload through parsePixPayload", () => { + test("should round-trip a static payload through getPixPayloadInfo", () => { fc.assert( fc.property(names, cities, (merchantName, merchantCity) => { const payload = generatePixPayload({ key: CPF_KEY, merchantName, merchantCity }); - const parsed = parsePixPayload(payload ?? ""); + const parsed = getPixPayloadInfo(payload ?? ""); expect(isValidPixPayload(payload ?? "")).toBe(true); expect(parsed?.key).toBe(CPF_KEY); @@ -466,7 +469,7 @@ describe("generatePixPayload", () => { amount, txid, }); - const parsed = parsePixPayload(payload ?? ""); + const parsed = getPixPayloadInfo(payload ?? ""); expect(parsed?.amount).toBe(Number(amount.toFixed(2))); expect(parsed?.txid).toBe(txid); @@ -478,7 +481,7 @@ describe("generatePixPayload", () => { fc.assert( fc.property(names, urls, (merchantName, url) => { const payload = generatePixPayload({ url, merchantName, merchantCity: "BRASILIA" }); - const parsed = parsePixPayload(payload ?? ""); + const parsed = getPixPayloadInfo(payload ?? ""); expect(parsed?.url).toBe(url); expect(parsed?.pointOfInitiation).toBe("dynamic"); @@ -508,7 +511,7 @@ describe("generatePixPayload", () => { fc.pre(payload !== null); - const parsed = parsePixPayload(payload ?? ""); + const parsed = getPixPayloadInfo(payload ?? ""); expect(/^[\u0020-\u007E]{1,25}$/.test(parsed?.merchantName ?? "")).toBe(true); expect(/^[\u0020-\u007E]{1,15}$/.test(parsed?.merchantCity ?? "")).toBe(true); diff --git a/src/generate-pix-payload/generate-pix-payload.ts b/src/generate-pix-payload/generate-pix-payload.ts index b635f589..24cffaf1 100644 --- a/src/generate-pix-payload/generate-pix-payload.ts +++ b/src/generate-pix-payload/generate-pix-payload.ts @@ -34,7 +34,7 @@ import { formatTlv } from "../_internals/format-tlv/format-tlv"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { isValidPixUrl } from "../_internals/is-valid-pix-url/is-valid-pix-url"; import { sanitizeToAscii } from "../_internals/sanitize-to-ascii/sanitize-to-ascii"; -import { parsePixKey } from "../parse-pix-key/parse-pix-key"; +import { getPixKeyInfo } from "../get-pix-key-info/get-pix-key-info"; import { AMOUNT_DECIMAL_PLACES, AMOUNT_REGEX, @@ -92,7 +92,7 @@ const resolveIdentifier = ( }; } - const key = parsePixKey(keyInput); + const key = getPixKeyInfo(keyInput); if (!key) return null; @@ -139,7 +139,7 @@ const resolveFormattedAmount = ( * given and when neither is given, since only one of them can occupy the "Merchant Account * Information" template at a time. * - * When `params.key` is given, it is normalized to its DICT canonical form by `parsePixKey` and + * When `params.key` is given, it is normalized to its DICT canonical form by `getPixKeyInfo` and * the payload is static: the "Point of Initiation Method" object is left out, so the payload * may be paid more than once, as in the example of the Bacen manual. * @@ -147,18 +147,18 @@ const resolveFormattedAmount = ( * Iniciação do Pix: the URL takes the key's place in the "Merchant Account Information" * template (sub-object `25` instead of `01`) and the "Point of Initiation Method" object (`01`) * is set to `"12"`. `params.url` must be at most 77 characters, the length that keeps the - * template within its 99 character limit together with the `br.gov.bcb.pix` GUI. `parsePixPayload` - * already parses both shapes, so `parsePixPayload(generatePixPayload({ url, ... }))` round-trips. + * template within its 99 character limit together with the `br.gov.bcb.pix` GUI. `getPixPayloadInfo` + * already parses both shapes, so `getPixPayloadInfo(generatePixPayload({ url, ... }))` round-trips. * * Object `01` is optional in the Manual do BR Code (`Uso: O`), so writing it only for a dynamic - * payload is one of the shapes the manual allows and follows its own examples; `parsePixPayload` + * payload is one of the shapes the manual allows and follows its own examples; `getPixPayloadInfo` * accepts the others too. The Pix Saque BR Code, which announces the ISPB of the "facilitador de * serviço de saque" in sub-object 26-03 (`fss`), is not generated here, only parsed. * * Unreserved Templates (IDs 80 to 99) are never written: the location always goes in the * "Merchant Account Information" template, so the "QR Code composto" of Pix Automático (Pix * recorrente), which puts its recurrence location in one of them, is out of scope here. - * `parsePixPayload` does read a composto, but only as an ordinary dynamic payload. + * `getPixPayloadInfo` does read a composto, but only as an ordinary dynamic payload. * * The merchant name, the merchant city and the description are folded to printable ASCII * (accents are dropped) and truncated to the lengths the BR Code allows, the description to diff --git a/src/parse-certidao/constants.ts b/src/get-certidao-info/constants.ts similarity index 100% rename from src/parse-certidao/constants.ts rename to src/get-certidao-info/constants.ts diff --git a/src/parse-certidao/parse-certidao.test.ts b/src/get-certidao-info/get-certidao-info.test.ts similarity index 69% rename from src/parse-certidao/parse-certidao.test.ts rename to src/get-certidao-info/get-certidao-info.test.ts index 984367f7..136e0bcf 100644 --- a/src/parse-certidao/parse-certidao.test.ts +++ b/src/get-certidao-info/get-certidao-info.test.ts @@ -2,59 +2,59 @@ import * as fc from "fast-check"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { CERTIDAO_TYPES } from "./constants"; -import { parseCertidao, type Certidao, type CertidaoType } from "./parse-certidao"; +import { getCertidaoInfo, type CertidaoInfo, type CertidaoType } from "./get-certidao-info"; const findMatricula = (base: string): string => { for (let pair = 0; pair < 100; pair++) { const value = `${base}${String(pair).padStart(2, "0")}`; - if (parseCertidao(value) !== null) return value; + if (getCertidaoInfo(value) !== null) return value; } return ""; }; -describe("parseCertidao", () => { +describe("getCertidaoInfo", () => { describe("should return null", () => { test("when it is null", () => { // @ts-expect-error: intentionally invalid input - expect(parseCertidao(null)).toBeNull(); + expect(getCertidaoInfo(null)).toBeNull(); }); test("when it is undefined", () => { // @ts-expect-error: intentionally invalid input - expect(parseCertidao()).toBeNull(); + expect(getCertidaoInfo()).toBeNull(); }); test("when it is an empty string", () => { - expect(parseCertidao("")).toBeNull(); + expect(getCertidaoInfo("")).toBeNull(); }); test("when the check digits do not match", () => { - expect(parseCertidao("10453901552013100012021000012322")).toBeNull(); + expect(getCertidaoInfo("10453901552013100012021000012322")).toBeNull(); }); test("when the matrícula is otherwise invalid", () => { - expect(parseCertidao("not-a-matricula")).toBeNull(); + expect(getCertidaoInfo("not-a-matricula")).toBeNull(); }); test("when the book code is 0, outside the nine books of the Provimento", () => { - expect(parseCertidao("10453901552013000012021000012387")).toBeNull(); + expect(getCertidaoInfo("10453901552013000012021000012387")).toBeNull(); }); test("when the serviço is not the 55 of art. 473, III, even with matching check digits", () => { - expect(parseCertidao("09400301542011100110002005191744")).toBeNull(); + expect(getCertidaoInfo("09400301542011100110002005191744")).toBeNull(); }); test("when it is a number, which cannot carry the 32 significant digits of a matrícula", () => { // @ts-expect-error: intentionally invalid input - expect(parseCertidao(1_045_390_155)).toBeNull(); + expect(getCertidaoInfo(1_045_390_155)).toBeNull(); }); }); describe("should return the parsed matrícula", () => { test("for 104539.01.55.2013.1.00012.021.0000123-21, the worked example of ghiorzi.org/DVnew.htm", () => { - expect(parseCertidao("104539 01 55 2013 1 00012 021 0000123 21")).toEqual({ + expect(getCertidaoInfo("104539 01 55 2013 1 00012 021 0000123 21")).toEqual({ registryCns: "104539", acervo: "01", service: "55", @@ -69,7 +69,7 @@ describe("parseCertidao", () => { }); test("for 094300 01 55 2010 1 00020 112 0000120-87 (klawdyo/validation-br certidao.spec.ts)", () => { - expect(parseCertidao("094300 01 55 2010 1 00020 112 0000120-87")).toEqual({ + expect(getCertidaoInfo("094300 01 55 2010 1 00020 112 0000120-87")).toEqual({ registryCns: "094300", acervo: "01", service: "55", @@ -84,39 +84,39 @@ describe("parseCertidao", () => { }); test("for a marriage act, book code 2", () => { - expect(parseCertidao("10453901552013200012021000012376")?.type).toBe("marriage"); + expect(getCertidaoInfo("10453901552013200012021000012376")?.type).toBe("marriage"); }); test("for a religious marriage with civil effect, book code 3", () => { - expect(parseCertidao("10453901552013300012021000012310")?.type).toBe("religious-marriage"); + expect(getCertidaoInfo("10453901552013300012021000012310")?.type).toBe("religious-marriage"); }); test("for a death act, book code 4", () => { - expect(parseCertidao("10453901552013400012021000012365")?.type).toBe("death"); + expect(getCertidaoInfo("10453901552013400012021000012365")?.type).toBe("death"); }); test("for a stillbirth act, book code 5", () => { - expect(parseCertidao("10453901552013500012021000012301")?.type).toBe("stillbirth"); + expect(getCertidaoInfo("10453901552013500012021000012301")?.type).toBe("stillbirth"); }); test("for a proclamas act, book code 6", () => { - expect(parseCertidao("10453901552013600012021000012354")?.type).toBe("banns"); + expect(getCertidaoInfo("10453901552013600012021000012354")?.type).toBe("banns"); }); test("for the other acts of Livro E, book code 7", () => { - expect(parseCertidao("10453901552013700012021000012315")?.type).toBe("other"); + expect(getCertidaoInfo("10453901552013700012021000012315")?.type).toBe("other"); }); test("for an emancipation act, book code 8", () => { - expect(parseCertidao("10453901552013800012021000012343")?.type).toBe("emancipation"); + expect(getCertidaoInfo("10453901552013800012021000012343")?.type).toBe("emancipation"); }); test("for an interdiction act, book code 9", () => { - expect(parseCertidao("10453901552013900012021000012398")?.type).toBe("interdiction"); + expect(getCertidaoInfo("10453901552013900012021000012398")?.type).toBe("interdiction"); }); test("for a matrícula whose first modulus 11 remainder is 10 (826683 01 55 2015 2 09245 842 9990114 18)", () => { - expect(parseCertidao("82668301552015209245842999011418")).toEqual({ + expect(getCertidaoInfo("82668301552015209245842999011418")).toEqual({ registryCns: "826683", acervo: "01", service: "55", @@ -149,7 +149,7 @@ describe("parseCertidao", () => { const [registryCns, acervo, service, year, typeCode, book, page, term] = fields; const registry = `${registryCns}${acervo}${service}${year}${typeCode}`; const value = findMatricula(`${registry}${book}${page}${term}`); - const parsed = parseCertidao(value); + const parsed = getCertidaoInfo(value); expect(parsed?.registryCns).toBe(registryCns); expect(parsed?.acervo).toBe(acervo); @@ -168,7 +168,7 @@ describe("parseCertidao", () => { test("should never throw and always return a matrícula or null", () => { fc.assert( fc.property(fc.anything(), (value) => { - const parsed = parseCertidao(value as string); + const parsed = getCertidaoInfo(value as string); expect(parsed === null || typeof parsed.registryCns === "string").toBe(true); }), @@ -177,11 +177,11 @@ describe("parseCertidao", () => { }); }); -describe("parseCertidao types", () => { - test("should take a string and return a Certidao or null", () => { - expectTypeOf(parseCertidao).parameter(0).toEqualTypeOf(); - expectTypeOf(parseCertidao).returns.toEqualTypeOf(); - expectTypeOf().toEqualTypeOf<{ +describe("getCertidaoInfo types", () => { + test("should take a string and return a CertidaoInfo or null", () => { + expectTypeOf(getCertidaoInfo).parameter(0).toEqualTypeOf(); + expectTypeOf(getCertidaoInfo).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ registryCns: string; acervo: string; service: string; diff --git a/src/parse-certidao/parse-certidao.ts b/src/get-certidao-info/get-certidao-info.ts similarity index 92% rename from src/parse-certidao/parse-certidao.ts rename to src/get-certidao-info/get-certidao-info.ts index 31802de5..6bb9f134 100644 --- a/src/parse-certidao/parse-certidao.ts +++ b/src/get-certidao-info/get-certidao-info.ts @@ -5,7 +5,7 @@ import { CERTIDAO_TYPES } from "./constants"; /** * The nine books (tipo do livro) a matrícula de registro civil can point to, in the order of the - * codes 1 to 9. `parseCertidao` names the book of a matrícula with one of these, and + * codes 1 to 9. `getCertidaoInfo` names the book of a matrícula with one of these, and * `isValidCertidao` accepts a list of them. * * The in-force art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça @@ -26,8 +26,8 @@ export type CertidaoType = | "emancipation" | "interdiction"; -/** The fields `parseCertidao` reads out of the matrícula of a certidão de registro civil. */ -export type Certidao = { +/** The fields `getCertidaoInfo` reads out of the matrícula of a certidão de registro civil. */ +export type CertidaoInfo = { /** The 6 digit CNS (Código Nacional de Serventia) of the serventia that issued the act. */ registryCns: string; /** @@ -71,15 +71,15 @@ export type Certidao = { * hold, so a numeric argument always gives `null` instead of being read as a rounded value. * * @param {string} value - The matrícula value to be parsed. - * @returns {Certidao | null} The parsed matrícula, or `null` when it is not valid. + * @returns {CertidaoInfo | null} The parsed matrícula, or `null` when it is not valid. * * @example * ```typescript - * parseCertidao("104539 01 55 2013 1 00012 021 0000123 21"); + * getCertidaoInfo("104539 01 55 2013 1 00012 021 0000123 21"); * // { registryCns: "104539", acervo: "01", service: "55", year: 2013, type: "birth", * // typeCode: 1, book: "00012", page: "021", term: "0000123", checkDigits: "21" } * - * parseCertidao("invalid"); // null + * getCertidaoInfo("invalid"); // null * ``` * * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 @@ -106,7 +106,7 @@ export type Certidao = { * @see Based on: https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php * Third reference implementation agreeing on the weights and on the remainder of 10 read as 1. */ -export const parseCertidao = (value: string): Certidao | null => { +export const getCertidaoInfo = (value: string): CertidaoInfo | null => { if (!isValidCertidao(value)) return null; const digits = sanitizeToDigits(value); diff --git a/src/parse-iban/parse-iban.test.ts b/src/get-iban-info/get-iban-info.test.ts similarity index 73% rename from src/parse-iban/parse-iban.test.ts rename to src/get-iban-info/get-iban-info.test.ts index 393f64d2..2d74cd34 100644 --- a/src/parse-iban/parse-iban.test.ts +++ b/src/get-iban-info/get-iban-info.test.ts @@ -3,7 +3,7 @@ import * as fc from "fast-check"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { formatIban } from "../format-iban/format-iban"; import { isValidIban } from "../is-valid-iban/is-valid-iban"; -import { parseIban, type Iban } from "./parse-iban"; +import { getIbanInfo, type IbanInfo } from "./get-iban-info"; const findBrazilianIban = (body: string): string => { for (let pair = 2; pair <= 98; pair++) { @@ -15,10 +15,10 @@ const findBrazilianIban = (body: string): string => { return ""; }; -describe("parseIban", () => { +describe("getIbanInfo", () => { describe("should return the parsed iban", () => { test("for a known valid IBAN (iban.com Brazil example)", () => { - expect(parseIban("BR1500000000000010932840814P2")).toEqual({ + expect(getIbanInfo("BR1500000000000010932840814P2")).toEqual({ countryCode: "BR", checkDigits: "15", bankIspb: "00000000", @@ -30,7 +30,7 @@ describe("parseIban", () => { }); test("for a value with grouping spaces", () => { - expect(parseIban("BR15 0000 0000 0000 1093 2840 814P 2")).toEqual({ + expect(getIbanInfo("BR15 0000 0000 0000 1093 2840 814P 2")).toEqual({ countryCode: "BR", checkDigits: "15", bankIspb: "00000000", @@ -42,7 +42,7 @@ describe("parseIban", () => { }); test("for a lowercase value", () => { - expect(parseIban("br1500000000000010932840814p2")).toEqual({ + expect(getIbanInfo("br1500000000000010932840814p2")).toEqual({ countryCode: "BR", checkDigits: "15", bankIspb: "00000000", @@ -54,7 +54,7 @@ describe("parseIban", () => { }); test("for a valid IBAN with a corrente (C) account type", () => { - expect(parseIban("BR3860701190000010000012345C1")).toEqual({ + expect(getIbanInfo("BR3860701190000010000012345C1")).toEqual({ countryCode: "BR", checkDigits: "38", bankIspb: "60701190", @@ -66,7 +66,7 @@ describe("parseIban", () => { }); test("for a valid IBAN with a poupança (P) account type and a non zero branch", () => { - expect(parseIban("BR1460746948000020001234567P2")).toEqual({ + expect(getIbanInfo("BR1460746948000020001234567P2")).toEqual({ countryCode: "BR", checkDigits: "14", bankIspb: "60746948", @@ -78,7 +78,7 @@ describe("parseIban", () => { }); test("for a valid IBAN with an account type letter other than C or P", () => { - expect(parseIban("BR5400000000000010932840814D2")).toEqual({ + expect(getIbanInfo("BR5400000000000010932840814D2")).toEqual({ countryCode: "BR", checkDigits: "54", bankIspb: "00000000", @@ -92,59 +92,59 @@ describe("parseIban", () => { describe("should return null", () => { test("when the check digits do not match", () => { - expect(parseIban("BR1500000000000010932840814P3")).toBeNull(); + expect(getIbanInfo("BR1500000000000010932840814P3")).toBeNull(); }); test("when the country code is not BR", () => { - expect(parseIban("DE89370400440532013000")).toBeNull(); + expect(getIbanInfo("DE89370400440532013000")).toBeNull(); }); test("when it is shorter than 29 characters", () => { - expect(parseIban("BR15000000000000109328408")).toBeNull(); + expect(getIbanInfo("BR15000000000000109328408")).toBeNull(); }); test("when it is longer than 29 characters", () => { - expect(parseIban("BR1500000000000010932840814P2000")).toBeNull(); + expect(getIbanInfo("BR1500000000000010932840814P2000")).toBeNull(); }); test("when the account type is not a letter", () => { - expect(parseIban("BR150000000000001093284081412")).toBeNull(); + expect(getIbanInfo("BR150000000000001093284081412")).toBeNull(); }); test("when the owner indicator is 0, which Circular 3.625 art. 2 § 1 does not assign, even though the check digits match", () => { - expect(parseIban("BR6900000000000010932840814P0")).toBeNull(); + expect(getIbanInfo("BR6900000000000010932840814P0")).toBeNull(); }); test("when the account type letter does not match the check digits", () => { - expect(parseIban("BR1500000000000010932840814X2")).toBeNull(); + expect(getIbanInfo("BR1500000000000010932840814X2")).toBeNull(); }); test("when it carries a character outside the print format", () => { - expect(parseIban("BR1500000000000010932840814P_2")).toBeNull(); - expect(parseIban("BR15,0000,0000,0000,1093,2840,814P2")).toBeNull(); + expect(getIbanInfo("BR1500000000000010932840814P_2")).toBeNull(); + expect(getIbanInfo("BR15,0000,0000,0000,1093,2840,814P2")).toBeNull(); }); test("when a separator falls inside a group instead of at its boundary", () => { - expect(parseIban("BR15 000 00000 0000 1093 2840 814P 2")).toBeNull(); + expect(getIbanInfo("BR15 000 00000 0000 1093 2840 814P 2")).toBeNull(); }); test("when it is an empty string", () => { - expect(parseIban("")).toBeNull(); + expect(getIbanInfo("")).toBeNull(); }); test("when it is null", () => { // @ts-expect-error: intentionally invalid input - expect(parseIban(null)).toBeNull(); + expect(getIbanInfo(null)).toBeNull(); }); test("when it is undefined", () => { // @ts-expect-error: intentionally invalid input - expect(parseIban()).toBeNull(); + expect(getIbanInfo()).toBeNull(); }); test("when it is a number", () => { // @ts-expect-error: intentionally invalid input - expect(parseIban(150_000_000_000)).toBeNull(); + expect(getIbanInfo(150_000_000_000)).toBeNull(); }); }); @@ -160,7 +160,7 @@ describe("parseIban", () => { test(`for ${iban}`, () => { expect(isValidIban(iban)).toBe(true); - const parsed = parseIban(iban); + const parsed = getIbanInfo(iban); expect(parsed).not.toBeNull(); expect( @@ -178,7 +178,7 @@ describe("parseIban", () => { fc.assert( fc.property(bodies, (body) => { const iban = findBrazilianIban(body); - const parsed = parseIban(formatIban(iban)); + const parsed = getIbanInfo(formatIban(iban)); const account = `${parsed?.bankIspb}${parsed?.branch}${parsed?.account}`; const owner = `${parsed?.accountType}${parsed?.owner}`; @@ -190,7 +190,7 @@ describe("parseIban", () => { test("should return a value exactly when the IBAN is valid", () => { fc.assert( fc.property(fc.string({ unit: "grapheme" }), (value) => { - expect(parseIban(value) !== null).toBe(isValidIban(value)); + expect(getIbanInfo(value) !== null).toBe(isValidIban(value)); }), ); }); @@ -198,7 +198,7 @@ describe("parseIban", () => { test("should never throw and always return an IBAN or null", () => { fc.assert( fc.property(fc.anything(), (value) => { - const parsed = parseIban(value as string); + const parsed = getIbanInfo(value as string); expect(parsed === null || parsed.countryCode === "BR").toBe(true); }), @@ -207,11 +207,11 @@ describe("parseIban", () => { }); }); -describe("parseIban types", () => { - test("should take a string and return an Iban or null", () => { - expectTypeOf(parseIban).parameter(0).toEqualTypeOf(); - expectTypeOf(parseIban).returns.toEqualTypeOf(); - expectTypeOf().toEqualTypeOf<{ +describe("getIbanInfo types", () => { + test("should take a string and return an IbanInfo or null", () => { + expectTypeOf(getIbanInfo).parameter(0).toEqualTypeOf(); + expectTypeOf(getIbanInfo).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ countryCode: "BR"; checkDigits: string; bankIspb: string; diff --git a/src/parse-iban/parse-iban.ts b/src/get-iban-info/get-iban-info.ts similarity index 84% rename from src/parse-iban/parse-iban.ts rename to src/get-iban-info/get-iban-info.ts index 451fb29a..f72da77b 100644 --- a/src/parse-iban/parse-iban.ts +++ b/src/get-iban-info/get-iban-info.ts @@ -1,8 +1,8 @@ import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; import { isValidIban } from "../is-valid-iban/is-valid-iban"; -/** The fields `parseIban` reads out of a Brazilian IBAN. */ -export type Iban = { +/** The fields `getIbanInfo` reads out of a Brazilian IBAN. */ +export type IbanInfo = { /** ISO 3166-1 alpha-2 country code. Always `"BR"`, the only country this parser supports. */ countryCode: "BR"; /** The 2 digit ISO 7064 MOD 97-10 check digits. */ @@ -57,11 +57,11 @@ const ACCOUNT_TYPE_END = ACCOUNT_END + ACCOUNT_TYPE_LENGTH; * character other than letters and digits. * * @param {string} value - The IBAN to be parsed. - * @returns {Iban|null} The parsed IBAN, or `null` when it is not a valid Brazilian IBAN. + * @returns {IbanInfo|null} The parsed IBAN, or `null` when it is not a valid Brazilian IBAN. * * @example * ```typescript - * parseIban("BR1500000000000010932840814P2"); + * getIbanInfo("BR1500000000000010932840814P2"); * // { * // countryCode: "BR", * // checkDigits: "15", @@ -72,11 +72,11 @@ const ACCOUNT_TYPE_END = ACCOUNT_END + ACCOUNT_TYPE_LENGTH; * // owner: "2", * // } * - * parseIban("BR15 0000 0000 0000 1093 2840 814P 2"); // same result (grouping spaces) - * parseIban("BR15-0000-0000-0000-1093-2840-814P-2"); // same result (any of the mask characters) - * parseIban("DE89370400440532013000"); // null (non Brazilian IBAN) - * parseIban("BR1500000000000010932840814P3"); // null (bad check digits) - * parseIban("BR15 000 00000 0000 1093 2840 814P 2"); // null (a separator inside a group) + * getIbanInfo("BR15 0000 0000 0000 1093 2840 814P 2"); // same result (grouping spaces) + * getIbanInfo("BR15-0000-0000-0000-1093-2840-814P-2"); // same result (any of the mask characters) + * getIbanInfo("DE89370400440532013000"); // null (non Brazilian IBAN) + * getIbanInfo("BR1500000000000010932840814P3"); // null (bad check digits) + * getIbanInfo("BR15 000 00000 0000 1093 2840 814P 2"); // null (a separator inside a group) * ``` * * @see Official: https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf @@ -88,7 +88,7 @@ const ACCOUNT_TYPE_END = ACCOUNT_END + ACCOUNT_TYPE_LENGTH; * @see Official: https://www.iso.org/standard/31531.html * ISO/IEC 7064:2003 (MOD 97-10 check digit algorithm) */ -export const parseIban = (value: string): Iban | null => { +export const getIbanInfo = (value: string): IbanInfo | null => { if (!isValidIban(value)) return null; const sanitized = sanitizeToAlphanumeric(value); diff --git a/src/parse-nfe-key/constants.ts b/src/get-nfe-key-info/constants.ts similarity index 90% rename from src/parse-nfe-key/constants.ts rename to src/get-nfe-key-info/constants.ts index 6c49c227..df4ece91 100644 --- a/src/parse-nfe-key/constants.ts +++ b/src/get-nfe-key-info/constants.ts @@ -1,5 +1,5 @@ /** - * The `mod` (modelo do documento) values `parseNfeKey` supports, every one of them a document + * The `mod` (modelo do documento) values `getNfeKeyInfo` supports, every one of them a document * whose "chave de acesso" is the same 44 digit string built the same way: 55 NF-e, 57 CT-e, * 58 MDF-e, 62 NFCom, 63 BP-e, 64 GTV-e (the CT-e Guia de Transporte de Valores), 65 NFC-e, * 66 NF3e and 67 CT-e OS (Conhecimento de Transporte Eletrônico para Outros Serviços). @@ -10,7 +10,7 @@ */ export const VALID_MODELS = ["55", "57", "58", "62", "63", "64", "65", "66", "67"] as const; -/** One of the `mod` values `parseNfeKey` supports. */ +/** One of the `mod` values `getNfeKeyInfo` supports. */ export type ValidModel = (typeof VALID_MODELS)[number]; /** @@ -89,13 +89,6 @@ export const FORBIDDEN_CODES: readonly string[] = [ */ export const FORBIDDEN_CODE_MODELS: readonly string[] = ["55", "65"]; -/** - * The prefixes the `Id` attribute of a DF-e XML puts in front of the 44 digits, one per - * document: `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom`. Stripped before the digits are - * read, since `NF3e` carries a digit of its own. - */ -export const XML_ID_PREFIX_REGEX = /^(?:nfe|cte|mdfe|bpe|nf3e|nfcom)/i; - /** * Shape the key has to be written in once the prefix is stripped: the digits, optionally split * into the printed groups of 4 by whitespace or the usual mask characters, a run of them between diff --git a/src/parse-nfe-key/parse-nfe-key.test.ts b/src/get-nfe-key-info/get-nfe-key-info.test.ts similarity index 61% rename from src/parse-nfe-key/parse-nfe-key.test.ts rename to src/get-nfe-key-info/get-nfe-key-info.test.ts index 57104d60..9c385dfc 100644 --- a/src/parse-nfe-key/parse-nfe-key.test.ts +++ b/src/get-nfe-key-info/get-nfe-key-info.test.ts @@ -4,7 +4,7 @@ import { IBGE_UF_CODES } from "../_internals/constants/ibge-uf-codes"; import { type StateCode } from "../_internals/constants/states"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { EMISSION_TYPES_BY_MODEL, FORBIDDEN_CODES, VALID_MODELS } from "./constants"; -import { parseNfeKey, type NfeKey, type NfeKeyModel } from "./parse-nfe-key"; +import { getNfeKeyInfo, type NfeKeyInfo, type NfeKeyModel } from "./get-nfe-key-info"; const KEY_SP = "35170458716523000119550010000000121000123458"; const KEY_RS = "43160472202112000136550000000010571048440722"; @@ -19,70 +19,70 @@ const MODEL_EMISSION_TYPES: { model: string; emissionType: number }[] = VALID_MO ); const buildNfeKey = (base: string): string => - CHECK_DIGITS.map((digit) => `${base}${digit}`).find((key) => parseNfeKey(key) !== null) ?? ""; + CHECK_DIGITS.map((digit) => `${base}${digit}`).find((key) => getNfeKeyInfo(key) !== null) ?? ""; -describe("parseNfeKey", () => { +describe("getNfeKeyInfo", () => { describe("should return null", () => { test("when it is null", () => { // @ts-expect-error: intentionally invalid input - expect(parseNfeKey(null)).toBeNull(); + expect(getNfeKeyInfo(null)).toBeNull(); }); test("when it is undefined", () => { // @ts-expect-error: intentionally invalid input - expect(parseNfeKey()).toBeNull(); + expect(getNfeKeyInfo()).toBeNull(); }); test("when it is a number", () => { // @ts-expect-error: intentionally invalid input - expect(parseNfeKey(123)).toBeNull(); + expect(getNfeKeyInfo(123)).toBeNull(); }); test("when it is an empty string", () => { - expect(parseNfeKey("")).toBeNull(); + expect(getNfeKeyInfo("")).toBeNull(); }); test("when the check digit does not match", () => { - expect(parseNfeKey(`${KEY_SP.slice(0, 43)}9`)).toBeNull(); + expect(getNfeKeyInfo(`${KEY_SP.slice(0, 43)}9`)).toBeNull(); }); test("when the model is not one of the nine supported (model 99 with a matching check digit)", () => { - expect(parseNfeKey("35170458716523000119990010000000121000123453")).toBeNull(); + expect(getNfeKeyInfo("35170458716523000119990010000000121000123453")).toBeNull(); }); test("when the document number is zero", () => { - expect(parseNfeKey("35170458716523000119550010000000001000123457")).toBeNull(); + expect(getNfeKeyInfo("35170458716523000119550010000000001000123457")).toBeNull(); }); test("when tpEmis is 8, which the NF-e MOC does not assign, even with a matching check digit", () => { - expect(parseNfeKey("35170458716523000119550010000000128000123455")).toBeNull(); + expect(getNfeKeyInfo("35170458716523000119550010000000128000123455")).toBeNull(); }); test("when tpEmis belongs to another model: 2 for a CT-e, 3 for a CT-e OS, 9 for an MDF-e, 3 for a BP-e", () => { - expect(parseNfeKey("35170458716523000119570010000000122000123453")).toBeNull(); - expect(parseNfeKey("35170458716523000119670010000000123000123454")).toBeNull(); - expect(parseNfeKey("35170458716523000119580010000000129000123454")).toBeNull(); - expect(parseNfeKey("35170458716523000119630010000000123000123450")).toBeNull(); + expect(getNfeKeyInfo("35170458716523000119570010000000122000123453")).toBeNull(); + expect(getNfeKeyInfo("35170458716523000119670010000000123000123454")).toBeNull(); + expect(getNfeKeyInfo("35170458716523000119580010000000129000123454")).toBeNull(); + expect(getNfeKeyInfo("35170458716523000119630010000000123000123450")).toBeNull(); }); test("when the cNF of an NF-e is one rule B03-10 of the MOC forbids", () => { - expect(parseNfeKey("35170458716523000119550010000000121000000003")).toBeNull(); - expect(parseNfeKey("35170458716523000119550010000000121111111113")).toBeNull(); - expect(parseNfeKey("35170458716523000119550010000000121123456781")).toBeNull(); + expect(getNfeKeyInfo("35170458716523000119550010000000121000000003")).toBeNull(); + expect(getNfeKeyInfo("35170458716523000119550010000000121111111113")).toBeNull(); + expect(getNfeKeyInfo("35170458716523000119550010000000121123456781")).toBeNull(); }); test("when the cNF of an NF-e equals its nNF, the second half of rule B03-10", () => { - expect(parseNfeKey("35170458716523000119550010000123451000123458")).toBeNull(); + expect(getNfeKeyInfo("35170458716523000119550010000123451000123458")).toBeNull(); }); test("when the access key is otherwise invalid", () => { - expect(parseNfeKey("not-a-key")).toBeNull(); + expect(getNfeKeyInfo("not-a-key")).toBeNull(); }); }); describe("should return the parsed access key", () => { test("for a NF-e access key (SP), the NFePHP `Keys::build` doc example also used in is-valid-nfe-key.test.ts", () => { - expect(parseNfeKey(KEY_SP)).toEqual({ + expect(getNfeKeyInfo(KEY_SP)).toEqual({ stateCode: "SP", year: 2017, month: 4, @@ -97,7 +97,7 @@ describe("parseNfeKey", () => { }); test("for a NF-e access key (RS), the NFePHP sped-cte `$infNFe->chave` example (NF-e referenced by a CT-e)", () => { - expect(parseNfeKey(KEY_RS)).toEqual({ + expect(getNfeKeyInfo(KEY_RS)).toEqual({ stateCode: "RS", year: 2016, month: 4, @@ -112,55 +112,55 @@ describe("parseNfeKey", () => { }); test("accepting the NFe XML prefix and a whitespace mask", () => { - expect(parseNfeKey(`NFe${KEY_SP}`)?.taxId).toBe("58716523000119"); - expect(parseNfeKey("3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458")?.number).toBe( + expect(getNfeKeyInfo(`NFe${KEY_SP}`)?.taxId).toBe("58716523000119"); + expect(getNfeKeyInfo("3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458")?.number).toBe( 12, ); }); test("accepting the XML Id prefix of every other covered document", () => { - expect(parseNfeKey("CTe35170458716523000119570010000000121000123455")?.model).toBe("57"); - expect(parseNfeKey("MDFe35170458716523000119580010000000121000123459")?.model).toBe("58"); - expect(parseNfeKey("BPe35170458716523000119630010000000121000123453")?.model).toBe("63"); - expect(parseNfeKey("NF3e35170458716523000119660010000000121000123454")?.model).toBe("66"); - expect(parseNfeKey("NFCom35170458716523000119620010000000121000123450")?.model).toBe("62"); + expect(getNfeKeyInfo("CTe35170458716523000119570010000000121000123455")?.model).toBe("57"); + expect(getNfeKeyInfo("MDFe35170458716523000119580010000000121000123459")?.model).toBe("58"); + expect(getNfeKeyInfo("BPe35170458716523000119630010000000121000123453")?.model).toBe("63"); + expect(getNfeKeyInfo("NF3e35170458716523000119660010000000121000123454")?.model).toBe("66"); + expect(getNfeKeyInfo("NFCom35170458716523000119620010000000121000123450")?.model).toBe("62"); }); test("for the CT-e models the SVC-SP authorises, whose MOC assigns tpEmis 8", () => { - expect(parseNfeKey("35170458716523000119570010000000128000123452")?.emissionType).toBe(8); - expect(parseNfeKey("35170458716523000119670010000000128000123455")?.emissionType).toBe(8); - expect(parseNfeKey("35170458716523000119640010000000128000123454")?.emissionType).toBe(8); + expect(getNfeKeyInfo("35170458716523000119570010000000128000123452")?.emissionType).toBe(8); + expect(getNfeKeyInfo("35170458716523000119670010000000128000123455")?.emissionType).toBe(8); + expect(getNfeKeyInfo("35170458716523000119640010000000128000123454")?.emissionType).toBe(8); }); test("for the MDF-e contingência Regime Especial NFF, tpEmis 3", () => { - expect(parseNfeKey("35170458716523000119580010000000123000123455")?.emissionType).toBe(3); + expect(getNfeKeyInfo("35170458716523000119580010000000123000123455")?.emissionType).toBe(3); }); test("keeping the cNF of a CT-e that rule B03-10 would forbid, since only the NF-e MOC states it", () => { - expect(parseNfeKey("35170458716523000119570010000000121000000000")?.code).toBe("00000000"); - expect(parseNfeKey("35170458716523000119570010000123451000123455")?.code).toBe("00012345"); + expect(getNfeKeyInfo("35170458716523000119570010000000121000000000")?.code).toBe("00000000"); + expect(getNfeKeyInfo("35170458716523000119570010000123451000123455")?.code).toBe("00012345"); }); test("keeping the left zero padding of a CPF issuer, using a synthetic key with an 11-digit CPF left-padded to 14 digits in the tax id field and the check digit recalculated", () => { - expect(parseNfeKey(KEY_CPF_PADDED)?.taxId).toBe("00040364478829"); - expect(parseNfeKey(KEY_CPF_PADDED)?.taxId).toHaveLength(14); + expect(getNfeKeyInfo(KEY_CPF_PADDED)?.taxId).toBe("00040364478829"); + expect(getNfeKeyInfo(KEY_CPF_PADDED)?.taxId).toHaveLength(14); }); test("for tpEmis 9, the off-line NFC-e contingency, same shape as the SP key with the tpEmis field changed and the check digit recalculated", () => { - expect(parseNfeKey("35170458716523000119550010000000129000123453")?.emissionType).toBe(9); + expect(getNfeKeyInfo("35170458716523000119550010000000129000123453")?.emissionType).toBe(9); }); test("for every other DF-e model (CT-e, MDF-e, GTV-e, NFC-e, CT-e OS), same shape as the SP key with the model field changed and the check digit recalculated", () => { - expect(parseNfeKey("35170458716523000119570010000000121000123455")?.model).toBe("57"); - expect(parseNfeKey("35170458716523000119580010000000121000123459")?.model).toBe("58"); - expect(parseNfeKey("35170458716523000119630010000000121000123453")?.model).toBe("63"); - expect(parseNfeKey("35170458716523000119640010000000121000123457")?.model).toBe("64"); - expect(parseNfeKey("35170458716523000119650010000000121000123450")?.model).toBe("65"); - expect(parseNfeKey("35170458716523000119670010000000121000123458")?.model).toBe("67"); + expect(getNfeKeyInfo("35170458716523000119570010000000121000123455")?.model).toBe("57"); + expect(getNfeKeyInfo("35170458716523000119580010000000121000123459")?.model).toBe("58"); + expect(getNfeKeyInfo("35170458716523000119630010000000121000123453")?.model).toBe("63"); + expect(getNfeKeyInfo("35170458716523000119640010000000121000123457")?.model).toBe("64"); + expect(getNfeKeyInfo("35170458716523000119650010000000121000123450")?.model).toBe("65"); + expect(getNfeKeyInfo("35170458716523000119670010000000121000123458")?.model).toBe("67"); }); test("splitting nSiteAutoriz from the 7 digit cNF of an NFCom, per its Visão Geral §2.1.3", () => { - expect(parseNfeKey("35170458716523000119620010000000121000123450")).toEqual({ + expect(getNfeKeyInfo("35170458716523000119620010000000121000123450")).toEqual({ stateCode: "SP", year: 2017, month: 4, @@ -173,13 +173,13 @@ describe("parseNfeKey", () => { code: "0012345", checkDigit: 0, }); - expect(parseNfeKey("35170458716523000119620010000000121700123452")?.authorizationSite).toBe( + expect(getNfeKeyInfo("35170458716523000119620010000000121700123452")?.authorizationSite).toBe( 7, ); }); test("splitting nSiteAutoriz from the 7 digit cNF of an NF3e, per its Visão Geral", () => { - expect(parseNfeKey("35170458716523000119660010000000121000123454")).toEqual({ + expect(getNfeKeyInfo("35170458716523000119660010000000121000123454")).toEqual({ stateCode: "SP", year: 2017, month: 4, @@ -195,7 +195,7 @@ describe("parseNfeKey", () => { }); test("without an authorizationSite property for a model whose key has no nSiteAutoriz", () => { - expect(parseNfeKey(KEY_SP)).not.toHaveProperty("authorizationSite"); + expect(getNfeKeyInfo(KEY_SP)).not.toHaveProperty("authorizationSite"); }); }); @@ -224,7 +224,7 @@ describe("parseNfeKey", () => { const issuer = `${uf}${year}${String(month).padStart(2, "0")}${taxId}`; const numbering = `${model}${series}${String(number).padStart(9, "0")}`; const key = buildNfeKey(`${issuer}${numbering}${emissionType}${tail}`); - const parsed = parseNfeKey(key); + const parsed = getNfeKeyInfo(key); expect(parsed?.stateCode).toBe(IBGE_UF_CODES[uf]); expect(parsed?.year).toBe(2000 + Number(year)); @@ -244,7 +244,7 @@ describe("parseNfeKey", () => { test("should never throw and always return an access key or null", () => { fc.assert( fc.property(fc.anything(), (value) => { - const parsed = parseNfeKey(value as string); + const parsed = getNfeKeyInfo(value as string); expect(parsed === null || typeof parsed.taxId === "string").toBe(true); }), @@ -253,11 +253,11 @@ describe("parseNfeKey", () => { }); }); -describe("parseNfeKey types", () => { - test("should take a string and return an NfeKey or null", () => { - expectTypeOf(parseNfeKey).parameter(0).toEqualTypeOf(); - expectTypeOf(parseNfeKey).returns.toEqualTypeOf(); - expectTypeOf().toEqualTypeOf<{ +describe("getNfeKeyInfo types", () => { + test("should take a string and return an NfeKeyInfo or null", () => { + expectTypeOf(getNfeKeyInfo).parameter(0).toEqualTypeOf(); + expectTypeOf(getNfeKeyInfo).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ stateCode: StateCode; year: number; month: number; diff --git a/src/parse-nfe-key/parse-nfe-key.ts b/src/get-nfe-key-info/get-nfe-key-info.ts similarity index 94% rename from src/parse-nfe-key/parse-nfe-key.ts rename to src/get-nfe-key-info/get-nfe-key-info.ts index ba908715..8995b7ac 100644 --- a/src/parse-nfe-key/parse-nfe-key.ts +++ b/src/get-nfe-key-info/get-nfe-key-info.ts @@ -1,5 +1,5 @@ import { IBGE_UF_CODES } from "../_internals/constants/ibge-uf-codes"; -import { NFE_KEY_LENGTH } from "../_internals/constants/nfe-key"; +import { NFE_KEY_LENGTH, XML_ID_PREFIX_REGEX } from "../_internals/constants/nfe-key"; import { type StateCode } from "../_internals/constants/states"; import { mod11 } from "../_internals/mod11/mod11"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; @@ -13,7 +13,6 @@ import { NUMBER_END, NUMBER_START, VALID_MODELS, - XML_ID_PREFIX_REGEX, } from "./constants"; export type { StateCode } from "../_internals/constants/states"; @@ -22,13 +21,13 @@ export type { StateCode } from "../_internals/constants/states"; * The document models a DF-e access key can carry: `"55"` NF-e, `"57"` CT-e, `"58"` MDF-e, * `"62"` NFCom, `"63"` BP-e, `"64"` GTV-e, `"65"` NFC-e, `"66"` NF3e and `"67"` CT-e OS. * Spelled out instead of derived from `VALID_MODELS` because the allowlist is internal and API - * Extractor cannot name it in the public report; the type test of `parse-nfe-key.test.ts` pins + * Extractor cannot name it in the public report; the type test of `get-nfe-key-info.test.ts` pins * the two together so they cannot drift apart. */ export type NfeKeyModel = "55" | "57" | "58" | "62" | "63" | "64" | "65" | "66" | "67"; -/** The fields `parseNfeKey` reads out of a DF-e access key (chave de acesso). */ -export type NfeKey = { +/** The fields `getNfeKeyInfo` reads out of a DF-e access key (chave de acesso). */ +export type NfeKeyInfo = { /** Two letter code of the issuing state (UF), read from the IBGE UF code. */ stateCode: StateCode; /** Four digit issue year. */ @@ -99,7 +98,7 @@ const isForbiddenCode = (model: string, code: string, number: number): boolean = * own number field (`nCT`, `nMDF`, `nBP`, `nNF`). * * @param {string} value - The access key value to be parsed. - * @returns {NfeKey | null} The parsed access key, or `null` when it is not valid. + * @returns {NfeKeyInfo | null} The parsed access key, or `null` when it is not valid. * * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf * Manual de Orientação do Contribuinte (MOC) NF-e, "chave de acesso". @@ -130,14 +129,14 @@ const isForbiddenCode = (model: string, code: string, number: number): boolean = * * @example * ```typescript - * parseNfeKey("35170458716523000119550010000000121000123458"); + * getNfeKeyInfo("35170458716523000119550010000000121000123458"); * // { stateCode: "SP", year: 2017, month: 4, taxId: "58716523000119", model: "55", * // series: 1, number: 12, emissionType: 1, code: "00012345", checkDigit: 8 } * - * parseNfeKey("invalid"); // null + * getNfeKeyInfo("invalid"); // null * ``` */ -export const parseNfeKey = (value: string): NfeKey | null => { +export const getNfeKeyInfo = (value: string): NfeKeyInfo | null => { if (typeof value !== "string") return null; const body = value.trim().replace(XML_ID_PREFIX_REGEX, "").trimStart(); @@ -184,7 +183,7 @@ export const parseNfeKey = (value: string): NfeKey | null => { return null; } - const parsed: NfeKey = { + const parsed: NfeKeyInfo = { stateCode, year: 2000 + Number(digits.slice(2, 4)), month, diff --git a/src/parse-pix-key/constants.ts b/src/get-pix-key-info/constants.ts similarity index 100% rename from src/parse-pix-key/constants.ts rename to src/get-pix-key-info/constants.ts diff --git a/src/parse-pix-key/parse-pix-key.test.ts b/src/get-pix-key-info/get-pix-key-info.test.ts similarity index 61% rename from src/parse-pix-key/parse-pix-key.test.ts rename to src/get-pix-key-info/get-pix-key-info.test.ts index 10a50ae4..8595a78d 100644 --- a/src/parse-pix-key/parse-pix-key.test.ts +++ b/src/get-pix-key-info/get-pix-key-info.test.ts @@ -5,7 +5,7 @@ import { formatCnpj } from "../format-cnpj/format-cnpj"; import { generateCnpj } from "../generate-cnpj/generate-cnpj"; import { generateCpf } from "../generate-cpf/generate-cpf"; import { generatePhone } from "../generate-phone/generate-phone"; -import { type PixKey, type PixKeyType, parsePixKey } from "./parse-pix-key"; +import { type PixKeyInfo, type PixKeyType, getPixKeyInfo } from "./get-pix-key-info"; const AMBIGUOUS = "51998259765"; @@ -25,124 +25,124 @@ const buildPixKey = (kind: (typeof PIX_KEY_KINDS)[number], email: string, evp: s return kind === "email" ? email : evp; }; -describe("parsePixKey", () => { +describe("getPixKeyInfo", () => { describe("should return null", () => { test("when it is an empty or blank string", () => { - expect(parsePixKey("")).toBeNull(); - expect(parsePixKey(" ")).toBeNull(); + expect(getPixKeyInfo("")).toBeNull(); + expect(getPixKeyInfo(" ")).toBeNull(); }); test("when it is null", () => { // @ts-expect-error: intentionally invalid input - expect(parsePixKey(null)).toBeNull(); + expect(getPixKeyInfo(null)).toBeNull(); }); test("when it is undefined", () => { // @ts-expect-error: intentionally invalid input - expect(parsePixKey()).toBeNull(); + expect(getPixKeyInfo()).toBeNull(); }); test("when it is a number", () => { // @ts-expect-error: intentionally invalid input - expect(parsePixKey(12_345_678_909)).toBeNull(); + expect(getPixKeyInfo(12_345_678_909)).toBeNull(); }); test("when it is a boolean, an object or an array", () => { // @ts-expect-error: intentionally invalid input - expect(parsePixKey(true)).toBeNull(); + expect(getPixKeyInfo(true)).toBeNull(); // @ts-expect-error: intentionally invalid input - expect(parsePixKey({})).toBeNull(); + expect(getPixKeyInfo({})).toBeNull(); // @ts-expect-error: intentionally invalid input - expect(parsePixKey([])).toBeNull(); + expect(getPixKeyInfo([])).toBeNull(); }); test("when it is an invalid CPF", () => { - expect(parsePixKey("11257245286")).toBeNull(); + expect(getPixKeyInfo("11257245286")).toBeNull(); }); test("when it is an invalid CNPJ", () => { - expect(parsePixKey("11222333000182")).toBeNull(); + expect(getPixKeyInfo("11222333000182")).toBeNull(); }); test("when it is an invalid e-mail", () => { - expect(parsePixKey("fulano@")).toBeNull(); - expect(parsePixKey("@example.com")).toBeNull(); - expect(parsePixKey("fulano@example")).toBeNull(); + expect(getPixKeyInfo("fulano@")).toBeNull(); + expect(getPixKeyInfo("@example.com")).toBeNull(); + expect(getPixKeyInfo("fulano@example")).toBeNull(); }); test("when the e-mail is longer than 77 characters", () => { - expect(parsePixKey(`${"a".repeat(66)}@example.com`)).toBeNull(); + expect(getPixKeyInfo(`${"a".repeat(66)}@example.com`)).toBeNull(); }); test("when the random key is not a UUID", () => { - expect(parsePixKey("71c7d9be4b854e439f1c1f3b8b4e9a2d")).toBeNull(); - expect(parsePixKey("71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2")).toBeNull(); - expect(parsePixKey("71c7d9be-4b85-4e43-9f1c-1f3b8b4e9azz")).toBeNull(); + expect(getPixKeyInfo("71c7d9be4b854e439f1c1f3b8b4e9a2d")).toBeNull(); + expect(getPixKeyInfo("71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2")).toBeNull(); + expect(getPixKeyInfo("71c7d9be-4b85-4e43-9f1c-1f3b8b4e9azz")).toBeNull(); }); test("when the phone has an invalid area code", () => { - expect(parsePixKey("(00) 98765-4321")).toBeNull(); + expect(getPixKeyInfo("(00) 98765-4321")).toBeNull(); }); test("when the phone is a landline, since the manual registers a mobile number", () => { - expect(parsePixKey("(11) 3000-0000")).toBeNull(); - expect(parsePixKey("+551130000000")).toBeNull(); - expect(parsePixKey("1130000000")).toBeNull(); + expect(getPixKeyInfo("(11) 3000-0000")).toBeNull(); + expect(getPixKeyInfo("+551130000000")).toBeNull(); + expect(getPixKeyInfo("1130000000")).toBeNull(); }); test("when it is free text", () => { - expect(parsePixKey("chave pix")).toBeNull(); - expect(parsePixKey("---")).toBeNull(); + expect(getPixKeyInfo("chave pix")).toBeNull(); + expect(getPixKeyInfo("---")).toBeNull(); }); test("when a phone number is buried in surrounding text", () => { - expect(parsePixKey("abc(11) 98765-4321xyz")).toBeNull(); - expect(parsePixKey("tel: (11) 98765-4321")).toBeNull(); + expect(getPixKeyInfo("abc(11) 98765-4321xyz")).toBeNull(); + expect(getPixKeyInfo("tel: (11) 98765-4321")).toBeNull(); }); test("when a CPF is buried in surrounding text", () => { - expect(parsePixKey("abc123.456.789-09")).toBeNull(); - expect(parsePixKey("CPF 123.456.789-09")).toBeNull(); + expect(getPixKeyInfo("abc123.456.789-09")).toBeNull(); + expect(getPixKeyInfo("CPF 123.456.789-09")).toBeNull(); }); test("when a CPF is written with separators outside the documented positions", () => { - expect(parsePixKey("1.2.3.4.5.6.7.8.9.0.9")).toBeNull(); - expect(parsePixKey("123/456/789/09")).toBeNull(); + expect(getPixKeyInfo("1.2.3.4.5.6.7.8.9.0.9")).toBeNull(); + expect(getPixKeyInfo("123/456/789/09")).toBeNull(); }); }); describe("should return a CPF", () => { test("when it is masked", () => { - expect(parsePixKey("123.456.789-09")).toEqual({ type: "cpf", value: "12345678909" }); + expect(getPixKeyInfo("123.456.789-09")).toEqual({ type: "cpf", value: "12345678909" }); }); test("when it is unmasked", () => { - expect(parsePixKey("40364478829")).toEqual({ type: "cpf", value: "40364478829" }); + expect(getPixKeyInfo("40364478829")).toEqual({ type: "cpf", value: "40364478829" }); }); test("when surrounded by whitespace", () => { - expect(parsePixKey(" 40364478829 ")).toEqual({ type: "cpf", value: "40364478829" }); + expect(getPixKeyInfo(" 40364478829 ")).toEqual({ type: "cpf", value: "40364478829" }); }); }); describe("should return a CNPJ", () => { test("when it is masked", () => { - expect(parsePixKey("00.038.166/0001-05")).toEqual({ + expect(getPixKeyInfo("00.038.166/0001-05")).toEqual({ type: "cnpj", value: "00038166000105", }); }); test("when it is unmasked", () => { - expect(parsePixKey("00038166000105")).toEqual({ + expect(getPixKeyInfo("00038166000105")).toEqual({ type: "cnpj", value: "00038166000105", }); }); test("when it is the alphanumeric format of the manual", () => { - expect(parsePixKey("12ABC34501DE35")).toEqual({ type: "cnpj", value: "12ABC34501DE35" }); - expect(parsePixKey("12.abc.345/01de-35")).toEqual({ + expect(getPixKeyInfo("12ABC34501DE35")).toEqual({ type: "cnpj", value: "12ABC34501DE35" }); + expect(getPixKeyInfo("12.abc.345/01de-35")).toEqual({ type: "cnpj", value: "12ABC34501DE35", }); @@ -151,32 +151,35 @@ describe("parsePixKey", () => { describe("should resolve the CNPJ and phone ambiguity", () => { test("should read a valid CNPJ as a CNPJ even when it starts with 0055", () => { - expect(parsePixKey("00551760871813")).toEqual({ type: "cnpj", value: "00551760871813" }); - expect(parsePixKey("00.551.760/8718-13")).toEqual({ type: "cnpj", value: "00551760871813" }); + expect(getPixKeyInfo("00551760871813")).toEqual({ type: "cnpj", value: "00551760871813" }); + expect(getPixKeyInfo("00.551.760/8718-13")).toEqual({ + type: "cnpj", + value: "00551760871813", + }); }); test("should still read a 0055 prefixed mobile number as a phone", () => { - expect(parsePixKey("005511987654321")).toEqual({ + expect(getPixKeyInfo("005511987654321")).toEqual({ type: "phone", value: "+5511987654321", }); }); test("should return null for a 0055 prefixed value that is neither a valid CNPJ nor a mobile number", () => { - expect(parsePixKey("00551133334444")).toBeNull(); + expect(getPixKeyInfo("00551133334444")).toBeNull(); }); }); describe("should return an e-mail", () => { test("when it is the example of the manual", () => { - expect(parsePixKey("fulano_da_silva.recebedor@example.com")).toEqual({ + expect(getPixKeyInfo("fulano_da_silva.recebedor@example.com")).toEqual({ type: "email", value: "fulano_da_silva.recebedor@example.com", }); }); test("when it is uppercased or padded", () => { - expect(parsePixKey(" Fulano@Example.COM ")).toEqual({ + expect(getPixKeyInfo(" Fulano@Example.COM ")).toEqual({ type: "email", value: "fulano@example.com", }); @@ -186,39 +189,39 @@ describe("parsePixKey", () => { const email = `${"a".repeat(65)}@example.com`; expect(email).toHaveLength(77); - expect(parsePixKey(email)).toEqual({ type: "email", value: email }); + expect(getPixKeyInfo(email)).toEqual({ type: "email", value: email }); }); }); describe("should return a phone", () => { test("when it is the example of the manual", () => { - expect(parsePixKey("+5561912345678")).toEqual({ + expect(getPixKeyInfo("+5561912345678")).toEqual({ type: "phone", value: "+5561912345678", }); }); test("when it is masked", () => { - expect(parsePixKey("(11) 98765-4321")).toEqual({ + expect(getPixKeyInfo("(11) 98765-4321")).toEqual({ type: "phone", value: "+5511987654321", }); }); test("when it is bare", () => { - expect(parsePixKey("11987654321")).toEqual({ type: "phone", value: "+5511987654321" }); + expect(getPixKeyInfo("11987654321")).toEqual({ type: "phone", value: "+5511987654321" }); }); test("when it carries the country code in every accepted form", () => { - expect(parsePixKey("+55 11 98765-4321")).toEqual({ + expect(getPixKeyInfo("+55 11 98765-4321")).toEqual({ type: "phone", value: "+5511987654321", }); - expect(parsePixKey("005511987654321")).toEqual({ + expect(getPixKeyInfo("005511987654321")).toEqual({ type: "phone", value: "+5511987654321", }); - expect(parsePixKey("5511987654321")).toEqual({ + expect(getPixKeyInfo("5511987654321")).toEqual({ type: "phone", value: "+5511987654321", }); @@ -226,7 +229,7 @@ describe("parsePixKey", () => { test("and never exceed the 14 characters of the E.164 form", () => { for (let index = 0; index < 200; index++) { - const key = parsePixKey(`+55${generatePhone("mobile")}`); + const key = getPixKeyInfo(`+55${generatePhone("mobile")}`); expect(key?.type).toBe("phone"); expect(key?.value.length).toBeLessThanOrEqual(14); @@ -236,21 +239,21 @@ describe("parsePixKey", () => { describe("should return a random key", () => { test("when it is a lowercase UUID", () => { - expect(parsePixKey("71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d")).toEqual({ + expect(getPixKeyInfo("71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d")).toEqual({ type: "evp", value: "71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d", }); }); test("when it is uppercased, lowercasing it", () => { - expect(parsePixKey("71C7D9BE-4B85-4E43-9F1C-1F3B8B4E9A2D")).toEqual({ + expect(getPixKeyInfo("71C7D9BE-4B85-4E43-9F1C-1F3B8B4E9A2D")).toEqual({ type: "evp", value: "71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d", }); }); test("when it is the example of the manual, whose version nibble is not 4", () => { - expect(parsePixKey("123e4567-e12b-12d1-a456-426655440000")).toEqual({ + expect(getPixKeyInfo("123e4567-e12b-12d1-a456-426655440000")).toEqual({ type: "evp", value: "123e4567-e12b-12d1-a456-426655440000", }); @@ -259,29 +262,29 @@ describe("parsePixKey", () => { describe("should resolve the CPF and phone ambiguity", () => { test("preferring the CPF when the value is valid as both", () => { - expect(parsePixKey(AMBIGUOUS)).toEqual({ type: "cpf", value: AMBIGUOUS }); + expect(getPixKeyInfo(AMBIGUOUS)).toEqual({ type: "cpf", value: AMBIGUOUS }); }); test("preferring the phone when it starts with the country code", () => { - expect(parsePixKey(`+55${AMBIGUOUS}`)).toEqual({ + expect(getPixKeyInfo(`+55${AMBIGUOUS}`)).toEqual({ type: "phone", value: `+55${AMBIGUOUS}`, }); - expect(parsePixKey(`0055${AMBIGUOUS}`)).toEqual({ + expect(getPixKeyInfo(`0055${AMBIGUOUS}`)).toEqual({ type: "phone", value: `+55${AMBIGUOUS}`, }); }); test("preferring the phone when the DDD is written between parentheses", () => { - expect(parsePixKey("(51) 99825-9765")).toEqual({ + expect(getPixKeyInfo("(51) 99825-9765")).toEqual({ type: "phone", value: `+55${AMBIGUOUS}`, }); }); test("keeping the CPF when it is written with its own mask", () => { - expect(parsePixKey("519.982.597-65")).toEqual({ type: "cpf", value: AMBIGUOUS }); + expect(getPixKeyInfo("519.982.597-65")).toEqual({ type: "cpf", value: AMBIGUOUS }); }); }); @@ -290,7 +293,7 @@ describe("parsePixKey", () => { for (let index = 0; index < 200; index++) { const cpf = generateCpf(); - expect(parsePixKey(cpf)?.value).toBe(cpf); + expect(getPixKeyInfo(cpf)?.value).toBe(cpf); } }); @@ -298,7 +301,7 @@ describe("parsePixKey", () => { for (let index = 0; index < 200; index++) { const cnpj = generateCnpj(); - expect(parsePixKey(formatCnpj(cnpj))).toEqual({ type: "cnpj", value: cnpj }); + expect(getPixKeyInfo(formatCnpj(cnpj))).toEqual({ type: "cnpj", value: cnpj }); } }); }); @@ -311,7 +314,7 @@ describe("parsePixKey", () => { test("should recognize every kind of key the DICT defines", () => { fc.assert( fc.property(keys, ([kind, email, evp]) => { - const parsed = parsePixKey(buildPixKey(kind, email, evp)); + const parsed = getPixKeyInfo(buildPixKey(kind, email, evp)); expect(parsed?.type).toBe(kind); }), @@ -321,8 +324,8 @@ describe("parsePixKey", () => { test("should return a canonical value that parses back to itself", () => { fc.assert( fc.property(keys, ([kind, email, evp]) => { - const parsed = parsePixKey(buildPixKey(kind, email, evp)); - const again = parsePixKey(parsed?.value ?? ""); + const parsed = getPixKeyInfo(buildPixKey(kind, email, evp)); + const again = getPixKeyInfo(parsed?.value ?? ""); expect(again?.type).toBe(parsed?.type); expect(again?.value).toBe(parsed?.value); @@ -334,8 +337,8 @@ describe("parsePixKey", () => { fc.assert( fc.property(keys, ([kind, email, evp]) => { const key = buildPixKey(kind, email, evp); - const parsed = parsePixKey(key); - const shouted = parsePixKey(` ${key.toUpperCase()} `); + const parsed = getPixKeyInfo(key); + const shouted = getPixKeyInfo(` ${key.toUpperCase()} `); expect(shouted?.value).toBe(parsed?.value); }), @@ -345,7 +348,7 @@ describe("parsePixKey", () => { test("should never throw and always return a Pix key or null", () => { fc.assert( fc.property(fc.anything(), (value) => { - const parsed = parsePixKey(value as string); + const parsed = getPixKeyInfo(value as string); expect(parsed === null || typeof parsed.value === "string").toBe(true); }), @@ -354,14 +357,14 @@ describe("parsePixKey", () => { }); }); -describe("parsePixKey types", () => { +describe("getPixKeyInfo types", () => { test("should take a string and return a Pix key or null", () => { - expectTypeOf(parsePixKey).parameter(0).toEqualTypeOf(); - expectTypeOf(parsePixKey).returns.toEqualTypeOf(); + expectTypeOf(getPixKeyInfo).parameter(0).toEqualTypeOf(); + expectTypeOf(getPixKeyInfo).returns.toEqualTypeOf(); }); test("should restrict the Pix key shape and its type", () => { - expectTypeOf().toEqualTypeOf<{ type: PixKeyType; value: string }>(); + expectTypeOf().toEqualTypeOf<{ type: PixKeyType; value: string }>(); expectTypeOf().toEqualTypeOf<"cpf" | "cnpj" | "email" | "phone" | "evp">(); }); }); diff --git a/src/parse-pix-key/parse-pix-key.ts b/src/get-pix-key-info/get-pix-key-info.ts similarity index 82% rename from src/parse-pix-key/parse-pix-key.ts rename to src/get-pix-key-info/get-pix-key-info.ts index 27bfbeb9..658f7cb5 100644 --- a/src/parse-pix-key/parse-pix-key.ts +++ b/src/get-pix-key-info/get-pix-key-info.ts @@ -8,11 +8,11 @@ import { isValidPhone } from "../is-valid-phone/is-valid-phone"; import { parseCnpj } from "../parse-cnpj/parse-cnpj"; import { CPF_SYNTAX_REGEX, EMAIL_MAX_LENGTH, EVP_REGEX, PHONE_SYNTAX_REGEX } from "./constants"; -/** The kinds of Pix key `parsePixKey` recognizes. */ +/** The kinds of Pix key `getPixKeyInfo` recognizes. */ export type PixKeyType = "cpf" | "cnpj" | "email" | "phone" | "evp"; -/** A Pix key recognized by `parsePixKey`, normalized to the canonical DICT form of its kind. */ -export type PixKey = { +/** A Pix key recognized by `getPixKeyInfo`, normalized to the canonical DICT form of its kind. */ +export type PixKeyInfo = { /** Which kind of Pix key the value was recognized as. */ type: PixKeyType; /** The key in the canonical DICT form for its kind. */ @@ -24,9 +24,9 @@ export type PixKey = { * characters of the usual masks, as the E.164 mobile key of the DICT. * * @param {string} trimmed - The trimmed value to read. - * @returns {PixKey|null} The phone key, or `null` when the value is not a mobile number. + * @returns {PixKeyInfo|null} The phone key, or `null` when the value is not a mobile number. */ -const resolvePhoneKey = (trimmed: string): PixKey | null => { +const resolvePhoneKey = (trimmed: string): PixKeyInfo | null => { if (!PHONE_SYNTAX_REGEX.test(trimmed)) return null; const national = normalizePhone(trimmed); @@ -67,17 +67,17 @@ const resolvePhoneKey = (trimmed: string): PixKey | null => { * a CPF, even when its digits carry a valid CPF check digit. * * @param {string} value - The Pix key to be parsed. - * @returns {PixKey|null} The normalized key, or `null` when the value is not a valid Pix key. + * @returns {PixKeyInfo|null} The normalized key, or `null` when the value is not a valid Pix key. * * @example * ```typescript - * parsePixKey("123.456.789-09"); // { type: "cpf", value: "12345678909" } - * parsePixKey("Fulano@Example.COM "); // { type: "email", value: "fulano@example.com" } - * parsePixKey("(11) 98765-4321"); // { type: "phone", value: "+5511987654321" } - * parsePixKey("71C7D9BE-4B85-4E43-9F1C-1F3B8B4E9A2D"); + * getPixKeyInfo("123.456.789-09"); // { type: "cpf", value: "12345678909" } + * getPixKeyInfo("Fulano@Example.COM "); // { type: "email", value: "fulano@example.com" } + * getPixKeyInfo("(11) 98765-4321"); // { type: "phone", value: "+5511987654321" } + * getPixKeyInfo("71C7D9BE-4B85-4E43-9F1C-1F3B8B4E9A2D"); * // { type: "evp", value: "71c7d9be-4b85-4e43-9f1c-1f3b8b4e9a2d" } - * parsePixKey("51998259765"); // { type: "cpf", value: "51998259765" } (also a valid phone) - * parsePixKey("+5551998259765"); // { type: "phone", value: "+5551998259765" } + * getPixKeyInfo("51998259765"); // { type: "cpf", value: "51998259765" } (also a valid phone) + * getPixKeyInfo("+5551998259765"); // { type: "phone", value: "+5551998259765" } * ``` * * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf @@ -87,7 +87,7 @@ const resolvePhoneKey = (trimmed: string): PixKey | null => { * @see Official: https://github.com/bacen/pix-api * Pix (SPI) OpenAPI spec. */ -export const parsePixKey = (value: string): PixKey | null => { +export const getPixKeyInfo = (value: string): PixKeyInfo | null => { if (typeof value !== "string") return null; const trimmed = value.trim(); diff --git a/src/parse-pix-payload/parse-pix-payload.test.ts b/src/get-pix-payload-info/get-pix-payload-info.test.ts similarity index 79% rename from src/parse-pix-payload/parse-pix-payload.test.ts rename to src/get-pix-payload-info/get-pix-payload-info.test.ts index 57d3913c..79b1765f 100644 --- a/src/parse-pix-payload/parse-pix-payload.test.ts +++ b/src/get-pix-payload-info/get-pix-payload-info.test.ts @@ -4,7 +4,11 @@ import { crc16Ccitt } from "../_internals/crc16-ccitt/crc16-ccitt"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { generateCpf } from "../generate-cpf/generate-cpf"; import { generatePixPayload } from "../generate-pix-payload/generate-pix-payload"; -import { type PixPayload, type PixPointOfInitiation, parsePixPayload } from "./parse-pix-payload"; +import { + type PixPayloadInfo, + type PixPointOfInitiation, + getPixPayloadInfo, +} from "./get-pix-payload-info"; const BACEN_STATIC = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400005204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041D3D"; @@ -166,60 +170,60 @@ const buildPayloadWithMerchantCity = (merchantCity: string): string => { return withoutCrc + crc16Ccitt(withoutCrc); }; -describe("parsePixPayload", () => { +describe("getPixPayloadInfo", () => { describe("should return null", () => { test("when it is an empty or blank string", () => { - expect(parsePixPayload("")).toBeNull(); - expect(parsePixPayload(" ")).toBeNull(); + expect(getPixPayloadInfo("")).toBeNull(); + expect(getPixPayloadInfo(" ")).toBeNull(); }); test("when it is null", () => { // @ts-expect-error: intentionally invalid input - expect(parsePixPayload(null)).toBeNull(); + expect(getPixPayloadInfo(null)).toBeNull(); }); test("when it is undefined", () => { // @ts-expect-error: intentionally invalid input - expect(parsePixPayload()).toBeNull(); + expect(getPixPayloadInfo()).toBeNull(); }); test("when it is a number", () => { // @ts-expect-error: intentionally invalid input - expect(parsePixPayload(20_250_101)).toBeNull(); + expect(getPixPayloadInfo(20_250_101)).toBeNull(); }); test("when it is a boolean, an object or an array", () => { // @ts-expect-error: intentionally invalid input - expect(parsePixPayload(true)).toBeNull(); + expect(getPixPayloadInfo(true)).toBeNull(); // @ts-expect-error: intentionally invalid input - expect(parsePixPayload({})).toBeNull(); + expect(getPixPayloadInfo({})).toBeNull(); // @ts-expect-error: intentionally invalid input - expect(parsePixPayload([])).toBeNull(); + expect(getPixPayloadInfo([])).toBeNull(); }); test("when the CRC does not match", () => { - expect(parsePixPayload(BACEN_STATIC.replace(/1D3D$/, "1D3E"))).toBeNull(); + expect(getPixPayloadInfo(BACEN_STATIC.replace(/1D3D$/, "1D3E"))).toBeNull(); }); test("when it is free text", () => { - expect(parsePixPayload("pix copia e cola")).toBeNull(); + expect(getPixPayloadInfo("pix copia e cola")).toBeNull(); }); test("when the key object is present but empty", () => { const merchantAccountInformation = tlv("00", "br.gov.bcb.pix") + tlv("01", ""); - expect(parsePixPayload(buildPayload(merchantAccountInformation))).toBeNull(); + expect(getPixPayloadInfo(buildPayload(merchantAccountInformation))).toBeNull(); }); test("when the url object is present but empty", () => { const merchantAccountInformation = tlv("00", "br.gov.bcb.pix") + tlv("25", ""); - expect(parsePixPayload(buildPayload(merchantAccountInformation))).toBeNull(); + expect(getPixPayloadInfo(buildPayload(merchantAccountInformation))).toBeNull(); }); test("when the merchant account information carries both a key and a url", () => { expect( - parsePixPayload( + getPixPayloadInfo( "00020101021226500014br.gov.bcb.pix0107a@b.com2517pix.example.com/x5204000053039865802BR5901A6001B62070503***63049A4B", ), ).toBeNull(); @@ -227,26 +231,26 @@ describe("parsePixPayload", () => { test("when the url is not a PSP location (scheme, whitespace, host without a dot)", () => { expect( - parsePixPayload( + getPixPayloadInfo( "00020101021226470014br.gov.bcb.pix2525https://pix.example.com/x5204000053039865802BR5901A6001B62070503***6304F843", ), ).toBeNull(); expect( - parsePixPayload( + getPixPayloadInfo( "00020101021226390014br.gov.bcb.pix2517pix example.com/x5204000053039865802BR5901A6001B62070503***6304C8E4", ), ).toBeNull(); expect( - parsePixPayload( + getPixPayloadInfo( "00020101021226330014br.gov.bcb.pix2511localhost/x5204000053039865802BR5901A6001B62070503***630494D9", ), ).toBeNull(); }); test("when the fss of a Pix Saque is not the 8 digits of an ISPB", () => { - expect(parsePixPayload(buildWithdrawalPayload("1234567", "0.00"))).toBeNull(); - expect(parsePixPayload(buildWithdrawalPayload("123456789", "0.00"))).toBeNull(); - expect(parsePixPayload(buildWithdrawalPayload("1234567x", "0.00"))).toBeNull(); + expect(getPixPayloadInfo(buildWithdrawalPayload("1234567", "0.00"))).toBeNull(); + expect(getPixPayloadInfo(buildWithdrawalPayload("123456789", "0.00"))).toBeNull(); + expect(getPixPayloadInfo(buildWithdrawalPayload("1234567x", "0.00"))).toBeNull(); }); test("when the fss of a Pix Saque is written next to a PSP location", () => { @@ -254,68 +258,68 @@ describe("parsePixPayload", () => { "00020126600014br.gov.bcb.pix2526pix.example.com/qr/v2/12340308123456785204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***6304DA55"; expect(hasValidCrc(payload)).toBe(true); - expect(parsePixPayload(payload)).toBeNull(); + expect(getPixPayloadInfo(payload)).toBeNull(); }); test("when the additional data template is malformed", () => { const merchantAccountInformation = tlv("00", "br.gov.bcb.pix") + tlv("01", "some-key"); - expect(parsePixPayload(buildPayload(merchantAccountInformation, "9"))).toBeNull(); + expect(getPixPayloadInfo(buildPayload(merchantAccountInformation, "9"))).toBeNull(); }); test("when a merchant account information template is malformed TLV, without throwing", () => { - expect(parsePixPayload(buildPayload("XY"))).toBeNull(); + expect(getPixPayloadInfo(buildPayload("XY"))).toBeNull(); }); test("when a merchant account information template is well-formed but carries no GUI, without throwing", () => { const merchantAccountInformation = tlv("01", "12345678909"); - expect(parsePixPayload(buildPayload(merchantAccountInformation))).toBeNull(); + expect(getPixPayloadInfo(buildPayload(merchantAccountInformation))).toBeNull(); }); test("when the country code field is entirely absent, without throwing", () => { - expect(parsePixPayload(buildPayloadWithoutCountryCode())).toBeNull(); + expect(getPixPayloadInfo(buildPayloadWithoutCountryCode())).toBeNull(); }); test("when the CRC tag id is not 6304, even with an otherwise self-consistent checksum", () => { - expect(parsePixPayload(buildPayloadWithCrcTag("9904"))).toBeNull(); + expect(getPixPayloadInfo(buildPayloadWithCrcTag("9904"))).toBeNull(); }); test("when the transaction amount is longer than 13 characters", () => { - expect(parsePixPayload(buildPayloadWithAmount("99999999999.99"))).toBeNull(); + expect(getPixPayloadInfo(buildPayloadWithAmount("99999999999.99"))).toBeNull(); }); test("when the transaction amount is not written as a plain decimal number", () => { - expect(parsePixPayload(buildPayloadWithAmount("+1.00"))).toBeNull(); - expect(parsePixPayload(buildPayloadWithAmount(" 1.00"))).toBeNull(); - expect(parsePixPayload(buildPayloadWithAmount("1.00x"))).toBeNull(); - expect(parsePixPayload(buildPayloadWithAmount("abc"))).toBeNull(); + expect(getPixPayloadInfo(buildPayloadWithAmount("+1.00"))).toBeNull(); + expect(getPixPayloadInfo(buildPayloadWithAmount(" 1.00"))).toBeNull(); + expect(getPixPayloadInfo(buildPayloadWithAmount("1.00x"))).toBeNull(); + expect(getPixPayloadInfo(buildPayloadWithAmount("abc"))).toBeNull(); }); test("when the transaction amount states more than two decimal places", () => { - expect(parsePixPayload(buildPayloadWithAmount("1.234"))).toBeNull(); + expect(getPixPayloadInfo(buildPayloadWithAmount("1.234"))).toBeNull(); }); test("when a key payload states a transaction amount of zero without the fss of a Pix Saque", () => { expect(hasValidCrc(buildPayloadWithAmount("0.00"))).toBe(true); - expect(parsePixPayload(buildPayloadWithAmount("0.00"))).toBeNull(); - expect(parsePixPayload(buildPayloadWithAmount("0"))).toBeNull(); - expect(parsePixPayload(buildPayloadWithAmount("0.0"))).toBeNull(); + expect(getPixPayloadInfo(buildPayloadWithAmount("0.00"))).toBeNull(); + expect(getPixPayloadInfo(buildPayloadWithAmount("0"))).toBeNull(); + expect(getPixPayloadInfo(buildPayloadWithAmount("0.0"))).toBeNull(); }); test("when the merchant name is present but empty", () => { - expect(parsePixPayload(buildPayloadWithMerchantName(""))).toBeNull(); + expect(getPixPayloadInfo(buildPayloadWithMerchantName(""))).toBeNull(); }); test("when the merchant city is present but empty", () => { - expect(parsePixPayload(buildPayloadWithMerchantCity(""))).toBeNull(); + expect(getPixPayloadInfo(buildPayloadWithMerchantCity(""))).toBeNull(); }); }); describe("should parse a static payload", () => { test("should ignore the transaction amount and the txid of a dynamic payload, which belong to the PSP location", () => { expect( - parsePixPayload( + getPixPayloadInfo( "00020101021226480014br.gov.bcb.pix2526pix.example.com/qr/v2/123452040000530398654041.005802BR5901A6001B62100506ABC1236304C7F9", ), ).toEqual({ @@ -327,7 +331,7 @@ describe("parsePixPayload", () => { }); test("should accept a transaction amount of zero in a dynamic payload, whose amount the PSP location settles", () => { - expect(parsePixPayload(buildDynamicPayloadWithAmount("0.00"))).toEqual({ + expect(getPixPayloadInfo(buildDynamicPayloadWithAmount("0.00"))).toEqual({ url: DYNAMIC_URL, merchantName: "Fulano de Tal", merchantCity: "BRASILIA", @@ -336,7 +340,7 @@ describe("parsePixPayload", () => { }); test("from the static QR Code example in the Bacen 'Manual de Padrões para Iniciação do Pix', with no key of its own for a field the payload does not carry", () => { - expect(parsePixPayload(BACEN_STATIC)).toStrictEqual({ + expect(getPixPayloadInfo(BACEN_STATIC)).toStrictEqual({ key: "123e4567-e12b-12d1-a456-426655440000", merchantName: "Fulano de Tal", merchantCity: "BRASILIA", @@ -345,7 +349,9 @@ describe("parsePixPayload", () => { }); test("for a Pix Saque BR Code, reading back the fss (26-03) of §2.6 with a transaction amount of zero", () => { - expect(parsePixPayload(buildWithdrawalPayload(WITHDRAWAL_FACILITATOR_ISPB, "0.00"))).toEqual({ + expect( + getPixPayloadInfo(buildWithdrawalPayload(WITHDRAWAL_FACILITATOR_ISPB, "0.00")), + ).toEqual({ key: "12345678909", withdrawalFacilitator: "12345678", merchantName: "Fulano de Tal", @@ -356,7 +362,7 @@ describe("parsePixPayload", () => { }); test("for a Pix Saque BR Code whose amount is written as the plain '0' of the BR Code field table", () => { - expect(parsePixPayload(buildWithdrawalPayload(WITHDRAWAL_FACILITATOR_ISPB, "0"))).toEqual({ + expect(getPixPayloadInfo(buildWithdrawalPayload(WITHDRAWAL_FACILITATOR_ISPB, "0"))).toEqual({ key: "12345678909", withdrawalFacilitator: "12345678", merchantName: "Fulano de Tal", @@ -367,7 +373,7 @@ describe("parsePixPayload", () => { }); test("for a Pix Saque BR Code that states no transaction amount at all", () => { - expect(parsePixPayload(buildWithdrawalPayload(WITHDRAWAL_FACILITATOR_ISPB))).toEqual({ + expect(getPixPayloadInfo(buildWithdrawalPayload(WITHDRAWAL_FACILITATOR_ISPB))).toEqual({ key: "12345678909", withdrawalFacilitator: "12345678", merchantName: "Fulano de Tal", @@ -378,7 +384,7 @@ describe("parsePixPayload", () => { test("marked single use by the point of initiation method 12, which the manual allows on any BR Code", () => { expect(hasValidCrc(KEY_MARKED_SINGLE_USE)).toBe(true); - expect(parsePixPayload(KEY_MARKED_SINGLE_USE)).toEqual({ + expect(getPixPayloadInfo(KEY_MARKED_SINGLE_USE)).toEqual({ key: "12345678909", merchantName: "Fulano de Tal", merchantCity: "BRASILIA", @@ -387,11 +393,11 @@ describe("parsePixPayload", () => { }); test("dropping the *** placeholder of an absent txid", () => { - expect(parsePixPayload(BACEN_STATIC)).not.toHaveProperty("txid"); + expect(getPixPayloadInfo(BACEN_STATIC)).not.toHaveProperty("txid"); }); test("with an amount and a txid, as in a widely published community example", () => { - expect(parsePixPayload(COMMUNITY_STATIC)).toEqual({ + expect(getPixPayloadInfo(COMMUNITY_STATIC)).toEqual({ key: "bee05743-4291-4f3c-9259-595df1307ba1", merchantName: "Alexandre Lima", merchantCity: "Presidente Prudente", @@ -402,7 +408,7 @@ describe("parsePixPayload", () => { }); test("picking the Pix arrangement out of the multi-arrangement payload from the 'Manual do BR Code' §2.2", () => { - expect(parsePixPayload(BRCODE_MANUAL)).toEqual({ + expect(getPixPayloadInfo(BRCODE_MANUAL)).toEqual({ key: "123e4567-e12b-12d1-a456-426655440000", merchantName: "NOME DO RECEBEDOR", merchantCity: "BRASILIA", @@ -413,7 +419,7 @@ describe("parsePixPayload", () => { }); test("when the merchant account information sits at the last valid id (51), not just at the usual 26", () => { - expect(parsePixPayload(buildPayloadWithMerchantAccountInformationTag("51"))).toEqual({ + expect(getPixPayloadInfo(buildPayloadWithMerchantAccountInformationTag("51"))).toEqual({ key: "12345678909", merchantName: "Fulano de Tal", merchantCity: "BRASILIA", @@ -422,25 +428,25 @@ describe("parsePixPayload", () => { }); test("accepting a transaction amount whose length is exactly 13 characters", () => { - expect(parsePixPayload(buildPayloadWithAmount("9999999999.99"))?.amount).toBe( + expect(getPixPayloadInfo(buildPayloadWithAmount("9999999999.99"))?.amount).toBe( 9_999_999_999.99, ); }); test("accepting a transaction amount written as a whole number, with no decimal point", () => { - expect(parsePixPayload(buildPayloadWithAmount("100"))?.amount).toBe(100); + expect(getPixPayloadInfo(buildPayloadWithAmount("100"))?.amount).toBe(100); }); test("without a key property when the payload is dynamic (carries a url instead)", () => { - expect(parsePixPayload(BACEN_DYNAMIC)).not.toHaveProperty("key"); + expect(getPixPayloadInfo(BACEN_DYNAMIC)).not.toHaveProperty("key"); }); test("without a url property when the payload is static (carries a key instead)", () => { - expect(parsePixPayload(BACEN_STATIC)).not.toHaveProperty("url"); + expect(getPixPayloadInfo(BACEN_STATIC)).not.toHaveProperty("url"); }); test("without a txid property when the payload carries no additional data template at all", () => { - expect(parsePixPayload(buildPayload(MERCHANT_ACCOUNT_INFORMATION))).not.toHaveProperty( + expect(getPixPayloadInfo(buildPayload(MERCHANT_ACCOUNT_INFORMATION))).not.toHaveProperty( "txid", ); }); @@ -453,7 +459,7 @@ describe("parsePixPayload", () => { description: "Pedido 42", }); - expect(parsePixPayload(payload ?? "")).toEqual({ + expect(getPixPayloadInfo(payload ?? "")).toEqual({ key: "12345678909", description: "Pedido 42", merchantName: "Fulano de Tal", @@ -465,7 +471,7 @@ describe("parsePixPayload", () => { describe("should parse a dynamic payload", () => { test("from the dynamic QR Code example in the Bacen 'Manual de Padrões para Iniciação do Pix'", () => { - expect(parsePixPayload(BACEN_DYNAMIC)).toEqual({ + expect(getPixPayloadInfo(BACEN_DYNAMIC)).toEqual({ url: "pix.example.com/8b3da2f39a4140d1a91abd93113bd441", merchantName: "Fulano de Tal", merchantCity: "BRASILIA", @@ -474,7 +480,7 @@ describe("parsePixPayload", () => { }); test("picking the Pix arrangement out of the composite QR Code example in the Bacen manual", () => { - expect(parsePixPayload(BACEN_COMPOSITE)).toEqual({ + expect(getPixPayloadInfo(BACEN_COMPOSITE)).toEqual({ url: "pix.example.com/8b3da2f39a4140d1a91abd93113bd441", merchantName: "Fulano de Tal", merchantCity: "BRASILIA", @@ -483,12 +489,12 @@ describe("parsePixPayload", () => { }); test("reading the point of initiation method 11 as static, per the Bacen static example with it made explicit", () => { - expect(parsePixPayload(STATIC_POINT_OF_INITIATION)?.pointOfInitiation).toBe("static"); + expect(getPixPayloadInfo(STATIC_POINT_OF_INITIATION)?.pointOfInitiation).toBe("static"); }); test("when it carries no point of initiation method at all, which the manual marks optional", () => { expect(hasValidCrc(URL_WITHOUT_POINT_OF_INITIATION)).toBe(true); - expect(parsePixPayload(URL_WITHOUT_POINT_OF_INITIATION)).toEqual({ + expect(getPixPayloadInfo(URL_WITHOUT_POINT_OF_INITIATION)).toEqual({ url: "pix.example.com/qr/v2/1234", merchantName: "Fulano de Tal", merchantCity: "BRASILIA", @@ -498,7 +504,7 @@ describe("parsePixPayload", () => { test("when the point of initiation method is 11, since the PSP location is what makes it dynamic", () => { expect(hasValidCrc(URL_WITH_STATIC_POINT_OF_INITIATION)).toBe(true); - expect(parsePixPayload(URL_WITH_STATIC_POINT_OF_INITIATION)).toEqual({ + expect(getPixPayloadInfo(URL_WITH_STATIC_POINT_OF_INITIATION)).toEqual({ url: "pix.example.com/qr/v2/1234", merchantName: "Fulano de Tal", merchantCity: "BRASILIA", @@ -518,7 +524,7 @@ describe("parsePixPayload", () => { txid: "RP123456782019", }; - expect(parsePixPayload(generatePixPayload(pix) ?? "")).toEqual({ + expect(getPixPayloadInfo(generatePixPayload(pix) ?? "")).toEqual({ ...pix, pointOfInitiation: "static", }); @@ -534,7 +540,7 @@ describe("parsePixPayload", () => { txid: `TX${index}`, }; - expect(parsePixPayload(generatePixPayload(pix) ?? "")).toEqual({ + expect(getPixPayloadInfo(generatePixPayload(pix) ?? "")).toEqual({ ...pix, pointOfInitiation: "static", }); @@ -549,7 +555,7 @@ describe("parsePixPayload", () => { fc.assert( fc.property(names, fc.uuid(), (merchantName, key) => { const payload = generatePixPayload({ key, merchantName, merchantCity: "BRASILIA" }); - const parsed = parsePixPayload(payload ?? ""); + const parsed = getPixPayloadInfo(payload ?? ""); expect(parsed?.merchantName).toBe(merchantName); expect(parsed?.merchantCity).toBe("BRASILIA"); @@ -567,7 +573,7 @@ describe("parsePixPayload", () => { const replacement = crc.charAt(index) === "0" ? "1" : "0"; const broken = `${(payload ?? "").slice(0, -4)}${crc.slice(0, index)}${replacement}${crc.slice(index + 1)}`; - expect(parsePixPayload(broken)).toBeNull(); + expect(getPixPayloadInfo(broken)).toBeNull(); }), ); }); @@ -575,7 +581,7 @@ describe("parsePixPayload", () => { test("should never throw and always return a BR Code or null", () => { fc.assert( fc.property(fc.anything(), (value) => { - const parsed = parsePixPayload(value as string); + const parsed = getPixPayloadInfo(value as string); expect(parsed === null || typeof parsed.merchantName === "string").toBe(true); }), @@ -584,14 +590,14 @@ describe("parsePixPayload", () => { }); }); -describe("parsePixPayload types", () => { +describe("getPixPayloadInfo types", () => { test("should take a string and return a Pix payload or null", () => { - expectTypeOf(parsePixPayload).parameter(0).toEqualTypeOf(); - expectTypeOf(parsePixPayload).returns.toEqualTypeOf(); + expectTypeOf(getPixPayloadInfo).parameter(0).toEqualTypeOf(); + expectTypeOf(getPixPayloadInfo).returns.toEqualTypeOf(); }); test("should restrict the Pix payload shape and its point of initiation", () => { - expectTypeOf().toEqualTypeOf<{ + expectTypeOf().toEqualTypeOf<{ key?: string; url?: string; description?: string; diff --git a/src/parse-pix-payload/parse-pix-payload.ts b/src/get-pix-payload-info/get-pix-payload-info.ts similarity index 97% rename from src/parse-pix-payload/parse-pix-payload.ts rename to src/get-pix-payload-info/get-pix-payload-info.ts index 8b5ce942..4383d280 100644 --- a/src/parse-pix-payload/parse-pix-payload.ts +++ b/src/get-pix-payload-info/get-pix-payload-info.ts @@ -38,8 +38,8 @@ import { type TlvFields, parseTlv } from "../_internals/parse-tlv/parse-tlv"; */ export type PixPointOfInitiation = "static" | "dynamic"; -/** The fields `parsePixPayload` reads out of a Pix BR Code. */ -export type PixPayload = { +/** The fields `getPixPayloadInfo` reads out of a Pix BR Code. */ +export type PixPayloadInfo = { /** The Pix key of the receiver, present in a static payload. */ key?: string; /** URL of the dynamic payload, present instead of `key` in a dynamic one. */ @@ -179,11 +179,11 @@ const buildPixPayload = ( merchantName: string, merchantCity: string, optional: OptionalPixFields, -): PixPayload => { +): PixPayloadInfo => { const { key, url, description, withdrawalFacilitator, amount, txid, pointOfInitiation } = optional; const isDynamic = url !== undefined || pointOfInitiation === PIX_DYNAMIC_POINT_OF_INITIATION; - const pix: PixPayload = { + const pix: PixPayloadInfo = { merchantName, merchantCity, pointOfInitiation: isDynamic ? "dynamic" : "static", @@ -249,12 +249,12 @@ const buildPixPayload = ( * de Pix Troco para QR Codes estáticos, apenas para QR Codes dinâmicos". * * @param {string} value - The BR Code payload to be parsed. - * @returns {PixPayload|null} The Pix data of the payload, or `null` when it is not a valid Pix + * @returns {PixPayloadInfo|null} The Pix data of the payload, or `null` when it is not a valid Pix * BR Code. * * @example * ```typescript - * parsePixPayload( + * getPixPayloadInfo( * "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-426655440000" + * "5204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041D3D", * ); @@ -273,7 +273,7 @@ const buildPixPayload = ( * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html * DICT (Diretório de Identificadores de Contas Transacionais) API specification. */ -export const parsePixPayload = (value: string): PixPayload | null => { +export const getPixPayloadInfo = (value: string): PixPayloadInfo | null => { if (typeof value !== "string") return null; const payload = value.trim(); diff --git a/src/index.test.ts b/src/index.test.ts index 0288bad5..e2b16065 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -9,7 +9,7 @@ import { type Cbo, type CepAddressInfo, type CepProvider, - type Certidao, + type CertidaoInfo, type CertidaoType, type Cfop, type Cnae, @@ -48,7 +48,7 @@ import { type GetMunicipalityOptions, type Holiday, type HolidayType, - type Iban, + type IbanInfo, type IsHolidayOptions, type IsValidBankAccountOptions, type IsValidBankAccountParams, @@ -63,7 +63,7 @@ import { type LegalNatureCategory, type LicensePlateFormat, type Municipality, - type NfeKey, + type NfeKeyInfo, type NfeKeyModel, type NumberToWordsGender, type ParseCnpjOptions, @@ -71,9 +71,9 @@ import { type PhoneMask, type PhoneType, type PhoneVersion, - type PixKey, + type PixKeyInfo, type PixKeyType, - type PixPayload, + type PixPayloadInfo, type PixPointOfInitiation, type RegistroProfissionalCouncil, type State, @@ -147,17 +147,22 @@ const PUBLIC = [ "getBoletoInfo", "getCbo", "getCepInfoByAddress", + "getCertidaoInfo", "getCfop", "getCities", "getCnae", "getFormatLicensePlate", "getHolidays", + "getIbanInfo", "getLegalNature", "getLegalNatures", "getLegalNaturesByCategory", "getMunicipalities", "getMunicipality", "getMunicipalityByCode", + "getNfeKeyInfo", + "getPixKeyInfo", + "getPixPayloadInfo", "getStateByIbgeCode", "getStateCodeByName", "getStateNameByCode", @@ -265,7 +270,7 @@ describe("Public API", () => { Cbo: Cbo; CepAddressInfo: CepAddressInfo; CepProvider: CepProvider; - Certidao: Certidao; + CertidaoInfo: CertidaoInfo; CertidaoType: CertidaoType; Cfop: Cfop; Cnae: Cnae; @@ -304,7 +309,7 @@ describe("Public API", () => { GetMunicipalityOptions: GetMunicipalityOptions; Holiday: Holiday; HolidayType: HolidayType; - Iban: Iban; + IbanInfo: IbanInfo; IsHolidayOptions: IsHolidayOptions; IsValidBankAccountOptions: IsValidBankAccountOptions; IsValidBankAccountParams: IsValidBankAccountParams; @@ -319,7 +324,7 @@ describe("Public API", () => { LegalNatureCategory: LegalNatureCategory; LicensePlateFormat: LicensePlateFormat; Municipality: Municipality; - NfeKey: NfeKey; + NfeKeyInfo: NfeKeyInfo; NfeKeyModel: NfeKeyModel; NumberToWordsGender: NumberToWordsGender; ParseCnpjOptions: ParseCnpjOptions; @@ -327,9 +332,9 @@ describe("Public API", () => { PhoneMask: PhoneMask; PhoneType: PhoneType; PhoneVersion: PhoneVersion; - PixKey: PixKey; + PixKeyInfo: PixKeyInfo; PixKeyType: PixKeyType; - PixPayload: PixPayload; + PixPayloadInfo: PixPayloadInfo; PixPointOfInitiation: PixPointOfInitiation; RegistroProfissionalCouncil: RegistroProfissionalCouncil; State: State; diff --git a/src/index.ts b/src/index.ts index c30eeae6..411774ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -95,6 +95,11 @@ export { GetCepInfoByAddressValidationError, getCepInfoByAddress, } from "./get-cep-info-by-address/get-cep-info-by-address"; +export { + type CertidaoInfo, + type CertidaoType, + getCertidaoInfo, +} from "./get-certidao-info/get-certidao-info"; export { type Cfop, getCfop } from "./get-cfop/get-cfop"; export { getCities } from "./get-cities/get-cities"; export { type Cnae, getCnae } from "./get-cnae/get-cnae"; @@ -108,6 +113,7 @@ export { type HolidayType, getHolidays, } from "./get-holidays/get-holidays"; +export { type IbanInfo, getIbanInfo } from "./get-iban-info/get-iban-info"; export { type LegalNature, type LegalNatureCategory, @@ -123,6 +129,21 @@ export { getMunicipality, } from "./get-municipality/get-municipality"; export { getMunicipalityByCode } from "./get-municipality-by-code/get-municipality-by-code"; +export { + type NfeKeyInfo, + type NfeKeyModel, + getNfeKeyInfo, +} from "./get-nfe-key-info/get-nfe-key-info"; +export { + type PixKeyInfo, + type PixKeyType, + getPixKeyInfo, +} from "./get-pix-key-info/get-pix-key-info"; +export { + type PixPayloadInfo, + type PixPointOfInitiation, + getPixPayloadInfo, +} from "./get-pix-payload-info/get-pix-payload-info"; export { getStateByIbgeCode } from "./get-state-by-ibge-code/get-state-by-ibge-code"; export { getStateCodeByName } from "./get-state-code-by-name/get-state-code-by-name"; export { getStateNameByCode } from "./get-state-name-by-code/get-state-name-by-code"; diff --git a/src/is-valid-certidao/is-valid-certidao.test.ts b/src/is-valid-certidao/is-valid-certidao.test.ts index 66652fe2..d41d21f3 100644 --- a/src/is-valid-certidao/is-valid-certidao.test.ts +++ b/src/is-valid-certidao/is-valid-certidao.test.ts @@ -1,8 +1,8 @@ import * as fc from "fast-check"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; -import { CERTIDAO_TYPES } from "../parse-certidao/constants"; -import { type CertidaoType } from "../parse-certidao/parse-certidao"; +import { CERTIDAO_TYPES } from "../get-certidao-info/constants"; +import { type CertidaoType } from "../get-certidao-info/get-certidao-info"; import { isValidCertidao, type IsValidCertidaoOptions } from "./is-valid-certidao"; const CHECK_DIGIT_PAIRS = Array.from({ length: 100 }, (_, index) => String(index).padStart(2, "0")); diff --git a/src/is-valid-certidao/is-valid-certidao.ts b/src/is-valid-certidao/is-valid-certidao.ts index 47db7124..24b91063 100644 --- a/src/is-valid-certidao/is-valid-certidao.ts +++ b/src/is-valid-certidao/is-valid-certidao.ts @@ -5,8 +5,8 @@ import { CERTIDAO_SERVICE_CODE, } from "../_internals/constants/certidao"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -import { CERTIDAO_TYPES } from "../parse-certidao/constants"; -import { type CertidaoType } from "../parse-certidao/parse-certidao"; +import { CERTIDAO_TYPES } from "../get-certidao-info/constants"; +import { type CertidaoType } from "../get-certidao-info/get-certidao-info"; /** Options of `isValidCertidao`. */ export type IsValidCertidaoOptions = { @@ -43,8 +43,8 @@ const getCheckDigit = (value: string): number => { * as 1. * * The book-type digit (fifteenth position of the matrícula) always has to name one of the nine - * books (see `CertidaoType`, reused from `parseCertidao`), so a matrícula whose digit is `0` is - * rejected however good its check digits are, the same way `parseCertidao` returns `null` for + * books (see `CertidaoType`, reused from `getCertidaoInfo`), so a matrícula whose digit is `0` is + * rejected however good its check digits are, the same way `getCertidaoInfo` returns `null` for * it. `options.accept` narrows that further to the listed types; when it is omitted, or when it * is not an array, every book type is accepted. * diff --git a/src/is-valid-nfe-key/is-valid-nfe-key.ts b/src/is-valid-nfe-key/is-valid-nfe-key.ts index dc69ca18..8d837a91 100644 --- a/src/is-valid-nfe-key/is-valid-nfe-key.ts +++ b/src/is-valid-nfe-key/is-valid-nfe-key.ts @@ -1,4 +1,4 @@ -import { parseNfeKey } from "../parse-nfe-key/parse-nfe-key"; +import { getNfeKeyInfo } from "../get-nfe-key-info/get-nfe-key-info"; /** * Validates a DF-e (Documento Fiscal eletrônico) access key (chave de acesso). @@ -66,4 +66,4 @@ import { parseNfeKey } from "../parse-nfe-key/parse-nfe-key"; * isValidNfeKey("35170458716523000119010010000000121000123450"); // false (invalid mod) * ``` */ -export const isValidNfeKey = (value: string): boolean => parseNfeKey(value) !== null; +export const isValidNfeKey = (value: string): boolean => getNfeKeyInfo(value) !== null; diff --git a/src/is-valid-pix-key/is-valid-pix-key.test.ts b/src/is-valid-pix-key/is-valid-pix-key.test.ts index 01d560ba..eca5f1b4 100644 --- a/src/is-valid-pix-key/is-valid-pix-key.test.ts +++ b/src/is-valid-pix-key/is-valid-pix-key.test.ts @@ -4,7 +4,7 @@ import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime import { generateCnpj } from "../generate-cnpj/generate-cnpj"; import { generateCpf } from "../generate-cpf/generate-cpf"; import { generatePhone } from "../generate-phone/generate-phone"; -import { type PixKeyType, parsePixKey } from "../parse-pix-key/parse-pix-key"; +import { type PixKeyType, getPixKeyInfo } from "../get-pix-key-info/get-pix-key-info"; import { type IsValidPixKeyOptions, isValidPixKey } from "./is-valid-pix-key"; describe("isValidPixKey", () => { @@ -131,10 +131,10 @@ describe("isValidPixKey", () => { ); }); - test("should agree with parsePixKey on every value", () => { + test("should agree with getPixKeyInfo on every value", () => { fc.assert( fc.property(fc.string({ unit: "grapheme" }), (value) => { - expect(isValidPixKey(value)).toBe(parsePixKey(value) !== null); + expect(isValidPixKey(value)).toBe(getPixKeyInfo(value) !== null); }), ); }); diff --git a/src/is-valid-pix-key/is-valid-pix-key.ts b/src/is-valid-pix-key/is-valid-pix-key.ts index cd6ab992..547b9bef 100644 --- a/src/is-valid-pix-key/is-valid-pix-key.ts +++ b/src/is-valid-pix-key/is-valid-pix-key.ts @@ -1,4 +1,4 @@ -import { type PixKeyType, parsePixKey } from "../parse-pix-key/parse-pix-key"; +import { type PixKeyType, getPixKeyInfo } from "../get-pix-key-info/get-pix-key-info"; /** Options of `isValidPixKey`. */ export type IsValidPixKeyOptions = { @@ -9,7 +9,7 @@ export type IsValidPixKeyOptions = { /** * Validates a Pix key (chave Pix) against the DICT key formats. * - * A value is valid when `parsePixKey` recognizes it as a CPF, a CNPJ, an e-mail address, a + * A value is valid when `getPixKeyInfo` recognizes it as a CPF, a CNPJ, an e-mail address, a * Brazilian mobile phone number or a random key (EVP), and when that kind is listed in * `options.accept`. The manual registers a "número de telefone celular", so a landline is not * a valid phone key. @@ -37,7 +37,7 @@ export type IsValidPixKeyOptions = { * Pix (SPI) OpenAPI spec. */ export const isValidPixKey = (value: string, options?: IsValidPixKeyOptions): boolean => { - const key = parsePixKey(value); + const key = getPixKeyInfo(value); if (!key) return false; diff --git a/src/is-valid-pix-payload/is-valid-pix-payload.ts b/src/is-valid-pix-payload/is-valid-pix-payload.ts index 8d22f7da..a0762316 100644 --- a/src/is-valid-pix-payload/is-valid-pix-payload.ts +++ b/src/is-valid-pix-payload/is-valid-pix-payload.ts @@ -1,4 +1,4 @@ -import { parsePixPayload } from "../parse-pix-payload/parse-pix-payload"; +import { getPixPayloadInfo } from "../get-pix-payload-info/get-pix-payload-info"; /** * Validates a Pix BR Code payload, the string behind a Pix QR Code and behind "Pix copia e @@ -51,4 +51,4 @@ import { parsePixPayload } from "../parse-pix-payload/parse-pix-payload"; * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html * DICT (Diretório de Identificadores de Contas Transacionais) API specification. */ -export const isValidPixPayload = (value: string): boolean => parsePixPayload(value) !== null; +export const isValidPixPayload = (value: string): boolean => getPixPayloadInfo(value) !== null; diff --git a/vite.config.ts b/vite.config.ts index 7ca2b736..5bbb7365 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -132,7 +132,7 @@ const sharedPack = { * shipped), and the ~76 util subpaths are built together in a second, separate invocation. Within * that second group, rolldown's default splitting still applies *among the utils themselves*: * most end up self-contained (single importer within that graph), but a few genuine cross-util - * dependencies (e.g. `parsePixKey` reusing `isValidCpf`'s digit-check, several phone utils sharing + * dependencies (e.g. `getPixKeyInfo` reusing `isValidCpf`'s digit-check, several phone utils sharing * `formatPhone`'s area-code table) get factored into a small shared chunk, real code reuse that * would otherwise be duplicated; either way, none of it touches the root. Running * ~77 entries as ~77 *separate* `PackUserConfig`s (fully self-contained, zero sharing at all) was From 72856ebe9a4ec97cafdbf7574d1bec93fc489a69 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 10:24:26 -0300 Subject: [PATCH 60/75] fix(area-codes): return regionName and regionCode from getAreaCodeInfo, as State does `AreaCodeInfo.region` held the value `State.regionName` carries, under a third spelling. The field is now `regionName`, and `regionCode` comes with it, the pair every `State` record exposes. --- docs/llms-full.txt | 6 +++--- docs/pt-br/utilities.md | 6 +++--- docs/utilities.md | 6 +++--- .../get-area-code-info.test.ts | 15 ++++++++++----- src/get-area-code-info/get-area-code-info.ts | 19 ++++++++++++++----- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 7ca08b1a..8931888a 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -693,13 +693,13 @@ Get the state (and its region) a Brazilian DDD (area code) belongs to, out of th import { getAreaCodeInfo } from '@brazilian-utils/brazilian-utils'; getAreaCodeInfo('11'); -// { areaCode: 11, stateCode: 'SP', stateName: 'São Paulo', region: 'Sudeste', stateCodes: ['SP'] } +// { areaCode: 11, stateCode: 'SP', stateName: 'São Paulo', regionCode: 'SE', regionName: 'Sudeste', stateCodes: ['SP'] } getAreaCodeInfo(21); -// { areaCode: 21, stateCode: 'RJ', stateName: 'Rio de Janeiro', region: 'Sudeste', stateCodes: ['RJ'] } +// { areaCode: 21, stateCode: 'RJ', stateName: 'Rio de Janeiro', regionCode: 'SE', regionName: 'Sudeste', stateCodes: ['RJ'] } getAreaCodeInfo('61'); -// { areaCode: 61, stateCode: 'DF', stateName: 'Distrito Federal', region: 'Centro-Oeste', stateCodes: ['DF', 'GO'] } +// { areaCode: 61, stateCode: 'DF', stateName: 'Distrito Federal', regionCode: 'CO', regionName: 'Centro-Oeste', stateCodes: ['DF', 'GO'] } getAreaCodeInfo('00'); // null getAreaCodeInfo(-11); // null diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 4c683d7e..bdbcd9e3 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -446,13 +446,13 @@ Retorna o estado (e a região) a que um DDD brasileiro pertence, dentre os 67 DD import { getAreaCodeInfo } from '@brazilian-utils/brazilian-utils'; getAreaCodeInfo('11'); -// { areaCode: 11, stateCode: 'SP', stateName: 'São Paulo', region: 'Sudeste', stateCodes: ['SP'] } +// { areaCode: 11, stateCode: 'SP', stateName: 'São Paulo', regionCode: 'SE', regionName: 'Sudeste', stateCodes: ['SP'] } getAreaCodeInfo(21); -// { areaCode: 21, stateCode: 'RJ', stateName: 'Rio de Janeiro', region: 'Sudeste', stateCodes: ['RJ'] } +// { areaCode: 21, stateCode: 'RJ', stateName: 'Rio de Janeiro', regionCode: 'SE', regionName: 'Sudeste', stateCodes: ['RJ'] } getAreaCodeInfo('61'); -// { areaCode: 61, stateCode: 'DF', stateName: 'Distrito Federal', region: 'Centro-Oeste', stateCodes: ['DF', 'GO'] } +// { areaCode: 61, stateCode: 'DF', stateName: 'Distrito Federal', regionCode: 'CO', regionName: 'Centro-Oeste', stateCodes: ['DF', 'GO'] } getAreaCodeInfo('00'); // null getAreaCodeInfo(-11); // null diff --git a/docs/utilities.md b/docs/utilities.md index 5c5f4111..c9affaa1 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -446,13 +446,13 @@ Get the state (and its region) a Brazilian DDD (area code) belongs to, out of th import { getAreaCodeInfo } from '@brazilian-utils/brazilian-utils'; getAreaCodeInfo('11'); -// { areaCode: 11, stateCode: 'SP', stateName: 'São Paulo', region: 'Sudeste', stateCodes: ['SP'] } +// { areaCode: 11, stateCode: 'SP', stateName: 'São Paulo', regionCode: 'SE', regionName: 'Sudeste', stateCodes: ['SP'] } getAreaCodeInfo(21); -// { areaCode: 21, stateCode: 'RJ', stateName: 'Rio de Janeiro', region: 'Sudeste', stateCodes: ['RJ'] } +// { areaCode: 21, stateCode: 'RJ', stateName: 'Rio de Janeiro', regionCode: 'SE', regionName: 'Sudeste', stateCodes: ['RJ'] } getAreaCodeInfo('61'); -// { areaCode: 61, stateCode: 'DF', stateName: 'Distrito Federal', region: 'Centro-Oeste', stateCodes: ['DF', 'GO'] } +// { areaCode: 61, stateCode: 'DF', stateName: 'Distrito Federal', regionCode: 'CO', regionName: 'Centro-Oeste', stateCodes: ['DF', 'GO'] } getAreaCodeInfo('00'); // null getAreaCodeInfo(-11); // null diff --git a/src/get-area-code-info/get-area-code-info.test.ts b/src/get-area-code-info/get-area-code-info.test.ts index a1b1ce1e..cc18dfeb 100644 --- a/src/get-area-code-info/get-area-code-info.test.ts +++ b/src/get-area-code-info/get-area-code-info.test.ts @@ -14,7 +14,8 @@ describe("getAreaCodeInfo", () => { areaCode: 11, stateCode: "SP", stateName: "São Paulo", - region: "Sudeste", + regionCode: "SE", + regionName: "Sudeste", stateCodes: ["SP"], }); }); @@ -24,7 +25,8 @@ describe("getAreaCodeInfo", () => { areaCode: 11, stateCode: "SP", stateName: "São Paulo", - region: "Sudeste", + regionCode: "SE", + regionName: "Sudeste", stateCodes: ["SP"], }); }); @@ -38,7 +40,8 @@ describe("getAreaCodeInfo", () => { areaCode: 68, stateCode: "AC", stateName: "Acre", - region: "Norte", + regionCode: "N", + regionName: "Norte", stateCodes: ["AC"], }); }); @@ -48,7 +51,8 @@ describe("getAreaCodeInfo", () => { areaCode: 61, stateCode: "DF", stateName: "Distrito Federal", - region: "Centro-Oeste", + regionCode: "CO", + regionName: "Centro-Oeste", stateCodes: ["DF", "GO"], }); }); @@ -178,7 +182,8 @@ describe("getAreaCodeInfo types", () => { areaCode: number; stateCode: StateCode; stateName: StateName; - region: "Norte" | "Nordeste" | "Centro-Oeste" | "Sudeste" | "Sul"; + regionCode: "N" | "NE" | "CO" | "SE" | "S"; + regionName: "Norte" | "Nordeste" | "Centro-Oeste" | "Sudeste" | "Sul"; stateCodes: StateCode[]; }>(); }); diff --git a/src/get-area-code-info/get-area-code-info.ts b/src/get-area-code-info/get-area-code-info.ts index 4472da57..f3ef2b33 100644 --- a/src/get-area-code-info/get-area-code-info.ts +++ b/src/get-area-code-info/get-area-code-info.ts @@ -13,8 +13,10 @@ export type AreaCodeInfo = { stateCode: StateCode; /** The full name of the state the DDD belongs to, e.g. `"São Paulo"`. */ stateName: StateName; + /** The code of the region the state belongs to, e.g. `"SE"`. */ + regionCode: State["regionCode"]; /** The full name of the region the state belongs to, e.g. `"Sudeste"`. */ - region: State["regionName"]; + regionName: State["regionName"]; /** * Every state the DDD serves, the primary `stateCode` first, e.g. `["SP"]` for 11 and * `["DF", "GO"]` for 61. @@ -55,9 +57,15 @@ export type AreaCodeInfo = { * * @example * ```typescript - * getAreaCodeInfo("11"); // { areaCode: 11, stateCode: "SP", stateName: "São Paulo", region: "Sudeste", stateCodes: ["SP"] } - * getAreaCodeInfo(21); // { areaCode: 21, stateCode: "RJ", stateName: "Rio de Janeiro", region: "Sudeste", stateCodes: ["RJ"] } - * getAreaCodeInfo("61"); // { areaCode: 61, stateCode: "DF", stateName: "Distrito Federal", region: "Centro-Oeste", stateCodes: ["DF", "GO"] } + * getAreaCodeInfo("11"); + * // { areaCode: 11, stateCode: "SP", stateName: "São Paulo", regionCode: "SE", regionName: "Sudeste", stateCodes: ["SP"] } + * + * getAreaCodeInfo(21); + * // { areaCode: 21, stateCode: "RJ", stateName: "Rio de Janeiro", regionCode: "SE", regionName: "Sudeste", stateCodes: ["RJ"] } + * + * getAreaCodeInfo("61"); + * // { areaCode: 61, stateCode: "DF", stateName: "Distrito Federal", regionCode: "CO", regionName: "Centro-Oeste", stateCodes: ["DF", "GO"] } + * * getAreaCodeInfo("00"); // null * getAreaCodeInfo(-11); // null * ``` @@ -82,7 +90,8 @@ export const getAreaCodeInfo = (areaCode: string | number): AreaCodeInfo | null areaCode: numericAreaCode, stateCode, stateName: state.name, - region: state.regionName, + regionCode: state.regionCode, + regionName: state.regionName, stateCodes: [stateCode, ...(AREA_CODE_SECONDARY_STATES[numericAreaCode] ?? [])], }; }; From c2dfcfa09f92b1764765163083d9fe27f2768cb2 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 10:24:26 -0300 Subject: [PATCH 61/75] feat(parse): add the mask-stripping parse function of every new family `parseCns`, `parseCei`, `parseCno`, `parseCaepf`, `parseCnae`, `parseNcm`, `parseNfeKey`, `parseCertidao`, `parseIban`, `parseCbo` and `parseCfop` complete the isValid/format/parse triple the 2.3.0 families have: `(value: string | number) => string`, only the digits (letters and digits, uppercased, for the IBAN) are read, the result is capped at the length of the document, a partial value passes through, and a nullish value gives `""`. `parseNfeKey` also drops the XML id prefix (`NFe`, `CTe`, ...) the validator accepts, through a regex now shared with `getNfeKeyInfo`. --- docs/llms-full.txt | 93 +++++++++++++++++++---- docs/llms.txt | 8 +- docs/pt-br/utilities.md | 85 +++++++++++++++++---- docs/utilities.md | 85 +++++++++++++++++---- src/index.test.ts | 10 ++- src/index.ts | 20 ++--- src/parse-caepf/constants.ts | 2 + src/parse-caepf/parse-caepf.test.ts | 58 ++++++++++++++ src/parse-caepf/parse-caepf.ts | 26 +++++++ src/parse-cbo/constants.ts | 2 + src/parse-cbo/parse-cbo.test.ts | 56 ++++++++++++++ src/parse-cbo/parse-cbo.ts | 26 +++++++ src/parse-cei/constants.ts | 2 + src/parse-cei/parse-cei.test.ts | 58 ++++++++++++++ src/parse-cei/parse-cei.ts | 25 ++++++ src/parse-certidao/parse-certidao.test.ts | 65 ++++++++++++++++ src/parse-certidao/parse-certidao.ts | 32 ++++++++ src/parse-cfop/constants.ts | 2 + src/parse-cfop/parse-cfop.test.ts | 52 +++++++++++++ src/parse-cfop/parse-cfop.ts | 26 +++++++ src/parse-cnae/constants.ts | 2 + src/parse-cnae/parse-cnae.test.ts | 62 +++++++++++++++ src/parse-cnae/parse-cnae.ts | 25 ++++++ src/parse-cno/constants.ts | 2 + src/parse-cno/parse-cno.test.ts | 58 ++++++++++++++ src/parse-cno/parse-cno.ts | 25 ++++++ src/parse-cns/constants.ts | 2 + src/parse-cns/parse-cns.test.ts | 58 ++++++++++++++ src/parse-cns/parse-cns.ts | 27 +++++++ src/parse-iban/parse-iban.test.ts | 68 +++++++++++++++++ src/parse-iban/parse-iban.ts | 30 ++++++++ src/parse-ncm/constants.ts | 2 + src/parse-ncm/parse-ncm.test.ts | 62 +++++++++++++++ src/parse-ncm/parse-ncm.ts | 24 ++++++ src/parse-nfe-key/parse-nfe-key.test.ts | 77 +++++++++++++++++++ src/parse-nfe-key/parse-nfe-key.ts | 50 ++++++++++++ 36 files changed, 1255 insertions(+), 52 deletions(-) create mode 100644 src/parse-caepf/constants.ts create mode 100644 src/parse-caepf/parse-caepf.test.ts create mode 100644 src/parse-caepf/parse-caepf.ts create mode 100644 src/parse-cbo/constants.ts create mode 100644 src/parse-cbo/parse-cbo.test.ts create mode 100644 src/parse-cbo/parse-cbo.ts create mode 100644 src/parse-cei/constants.ts create mode 100644 src/parse-cei/parse-cei.test.ts create mode 100644 src/parse-cei/parse-cei.ts create mode 100644 src/parse-certidao/parse-certidao.test.ts create mode 100644 src/parse-certidao/parse-certidao.ts create mode 100644 src/parse-cfop/constants.ts create mode 100644 src/parse-cfop/parse-cfop.test.ts create mode 100644 src/parse-cfop/parse-cfop.ts create mode 100644 src/parse-cnae/constants.ts create mode 100644 src/parse-cnae/parse-cnae.test.ts create mode 100644 src/parse-cnae/parse-cnae.ts create mode 100644 src/parse-cno/constants.ts create mode 100644 src/parse-cno/parse-cno.test.ts create mode 100644 src/parse-cno/parse-cno.ts create mode 100644 src/parse-cns/constants.ts create mode 100644 src/parse-cns/parse-cns.test.ts create mode 100644 src/parse-cns/parse-cns.ts create mode 100644 src/parse-iban/parse-iban.test.ts create mode 100644 src/parse-iban/parse-iban.ts create mode 100644 src/parse-ncm/constants.ts create mode 100644 src/parse-ncm/parse-ncm.test.ts create mode 100644 src/parse-ncm/parse-ncm.ts create mode 100644 src/parse-nfe-key/parse-nfe-key.test.ts create mode 100644 src/parse-nfe-key/parse-nfe-key.ts diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 8931888a..5d9d9975 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -122,23 +122,29 @@ - [formatCertidao](#formatcertidao) - [parseCertidao](#parsecertidao) - [getCertidaoInfo](#getcertidaoinfo) - - [formatCertidao](#formatcertidao) - [isValidCei](#isvalidcei) - [formatCei](#formatcei) + - [parseCei](#parsecei) - [isValidCno](#isvalidcno) - [formatCno](#formatcno) + - [parseCno](#parsecno) - [isValidCaepf](#isvalidcaepf) - [formatCaepf](#formatcaepf) + - [parseCaepf](#parsecaepf) - [isValidRegistroProfissional](#isvalidregistroprofissional) - [isValidVin](#isvalidvin) - [isValidCbo](#isvalidcbo) + - [parseCbo](#parsecbo) - [getCbo](#getcbo) - [isValidCnae](#isvalidcnae) - [formatCnae](#formatcnae) + - [parseCnae](#parsecnae) - [getCnae](#getcnae) - [isValidNcm](#isvalidncm) - [formatNcm](#formatncm) + - [parseNcm](#parsencm) - [isValidCfop](#isvalidcfop) + - [parseCfop](#parsecfop) - [getCfop](#getcfop) - [isValidCst](#isvalidcst) - [isValidCsosn](#isvalidcsosn) @@ -2048,19 +2054,6 @@ The `CertidaoInfo` result carries: | `term` | The 7 digit term (termo) number, zero padded. | | `checkDigits` | The 2 modulus 11 check digits of the matrícula. | -### formatCertidao - -Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). A number is accepted and read as the string of its digits, like in `formatCpf`, but a full 32 digit matrícula has to be a string: that many digits are more than a JavaScript number can hold exactly. At runtime the value is read for its digits and masked as far as they go, like in every formatter of this package, so a partial matrícula still being typed is masked progressively. - -```javascript -import { formatCertidao } from '@brazilian-utils/brazilian-utils'; - -formatCertidao('10453901552013100012021000012321'); // 104539 01 55 2013 1 00012 021 0000123 21 -formatCertidao('104539.01.55.2013.1.00012.021.0000123-21'); // 104539 01 55 2013 1 00012 021 0000123 21 -formatCertidao('1552010100020112000012087', { pad: true }); // 000000 01 55 2010 1 00020 112 0000120 87 -formatCertidao(104539015520); // 104539 01 55 20 (a number is read as the string of its digits) -``` - ### isValidCei Check if a CEI (Cadastro Específico do INSS) number is valid. The CEI identifies an employer with no CNPJ, such as a construction work or a rural producer: 12 digits printed as `00.000.00000/00`, the last one a check digit calculated over the 11 base digits with the weights 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 and 4. Accepts the usual mask characters and whitespace between/around groups, a run of them between two groups included. The Receita Federal does not publish this check digit rule, so it follows the reference implementations of [yii2-br-validator](https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php) and [Bigai.Documentos.Brasil](https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs), cross-checked against the [Cadastro Nacional de Obras (CNO) open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) of the Receita Federal. @@ -2087,6 +2080,16 @@ formatCei(249859674386); // 24.985.96743/86 formatCei('249', { pad: true }); // 00.000.00002/49 ``` +### parseCei + +Remove CEI (Cadastro Específico do INSS) formatting, keep only digits, and cap the result to 12 digits. A partial value passes through as far as it goes; use `isValidCei` to check the number itself. + +```javascript +import { parseCei } from '@brazilian-utils/brazilian-utils'; + +parseCei('27.729.71181/87'); // '277297118187' +``` + ### isValidCno Check if a CNO (Cadastro Nacional de Obras) number is valid. The CNO replaced the CEI for construction works and kept its numbering, so a work registered under a legacy CEI keeps the same number and both registries validate identically: 12 digits printed as `00.000.00000/00` with a check digit calculated over the 11 base digits. The Receita Federal does not publish the check digit rule; it was confirmed against the [Cadastro Nacional de Obras (CNO) open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) of the Receita Federal: every work in the Minas Gerais extract of that dataset passes this check. The catalogue page itself publishes only the dataset's description and download links, not that result. @@ -2113,6 +2116,16 @@ formatCno(401800097960); // 40.180.00979/60 formatCno('979', { pad: true }); // 00.000.00009/79 ``` +### parseCno + +Remove CNO (Cadastro Nacional de Obras) formatting, keep only digits, and cap the result to 12 digits, the numbering the CNO kept from the CEI. Use `isValidCno` to check the number itself. + +```javascript +import { parseCno } from '@brazilian-utils/brazilian-utils'; + +parseCno('11.113.01373/68'); // '111130137368' +``` + ### isValidCaepf Check if a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number is valid. The CAEPF replaced the CEI for individuals who hire employees: 14 digits printed as `000.000.000/000-00`, formed by the 9 digit CPF base of the holder, a 3 digit sequence for the holder's several registrations and 2 check digits. Both check digits are the CNPJ's modulus 11 in the formulation of the cited reference: the weights cycle from 9 down to 2 from the right and the check digit is the remainder itself, with a remainder of 10 read as 0 — the same digit the CNPJ's 2-to-9 weights with `11 - remainder` produce. The resulting pair is then shifted by 12, wrapping around 100. A base whose 12 digits are all the same is rejected before the check digits are computed, the way `isValidCei` and `isValidCno` reject a repeated CEI/CNO number, so the otherwise well-formed `00000000000012` is invalid. The Receita Federal does not publish the layout or the check digit rule: both are described by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented the same way by [brazilian-values](https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts). @@ -2140,6 +2153,16 @@ formatCaepf(41142260000101); // 411.422.600/001-01 formatCaepf('184', { pad: true }); // 000.000.000/001-84 ``` +### parseCaepf + +Remove CAEPF (Cadastro de Atividade Econômica da Pessoa Física) formatting, keep only digits, and cap the result to 14 digits. Use `isValidCaepf` to check the number itself. + +```javascript +import { parseCaepf } from '@brazilian-utils/brazilian-utils'; + +parseCaepf('293.118.610/001-84'); // '29311861000184' +``` + ### isValidRegistroProfissional Check the structure of a professional council registration number (registro/inscrição profissional). It takes a single object, typed as `IsValidRegistroProfissionalOptions`, the shape `isValidBankAccount` takes: `value` is the registration number, `council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). Anything that is not an object, and an object missing `value` or `council`, is `false`. This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits, the tipo de registro (`"O"` Originário or `"P"` Provisório, which says nothing about the professional category) and the check digit, as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). A Registro Transferido or Secundário appends `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, per that same item and [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: the Manual's own examples are `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` and `PI-111222/O-5 S-AC`. Both UFs must be real state codes, and `stateCode` is compared against the originating one. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. Only the CRC shape and those CRP regional codes rest on a published source: the CFP page publishes no length for the inscription number itself, and the OAB, the CFM and the CFO publish no format at all, so the digit ranges accepted for `"CRP"`, `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative (the OAB/SP public search field is `maxlength="7"`, and the CFM documents `300`-prefixed and `P`-suffixed CRMs, none of which these shapes express). CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). @@ -2188,6 +2211,16 @@ isValidCbo(-212405); // false (not a non-negative safe integer) The occupation titles come from the [official CBO 2002 occupation table published by the MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). +### parseCbo + +Remove CBO (Classificação Brasileira de Ocupações) formatting, keep only digits, and cap the result to 6 digits. Nothing is left padded here, so the leading zero of a code such as `010205` has to be written out; use `getCbo` or `isValidCbo`, which do pad a bare numeric code, to look an occupation up. + +```javascript +import { parseCbo } from '@brazilian-utils/brazilian-utils'; + +parseCbo('2124-05'); // '212405' +``` + ### getCbo Look a CBO (Classificação Brasileira de Ocupações) code up and get its official occupation title, in the `{ code, description }` record every lookup of this library returns. A value written as bare digits keeps its implied leading zeros, as a string as much as a number: `getCbo(10205)` and `getCbo('10205')` are both read as `010205`. Same input rules as `isValidCbo`: a string has to be written as the 6 digits or with the `NNNN-NN` mask, and a number has to be a non-negative safe integer. @@ -2236,6 +2269,17 @@ formatCnae('abc6201501'); // 6201-5/01 (only the digits are read) formatCnae(-6201501); // 6201-5/01 ``` +### parseCnae + +Remove CNAE (Classificação Nacional de Atividades Econômicas) formatting, keep only digits, and cap the result to the 7 digits of a complete subclass code. Nothing is left padded here; use `getCnae` or `isValidCnae`, which do pad a bare numeric code, to look a subclass up. + +```javascript +import { parseCnae } from '@brazilian-utils/brazilian-utils'; + +parseCnae('6201-5/01'); // '6201501' +parseCnae('62'); // '62' (a partial code is kept as written) +``` + ### getCnae Look a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up and get its code and official description. `code` comes back as the 7 bare digits, like every other lookup of this library; pass it to `formatCnae` for the `NNNN-N/NN` form. A value written as bare digits keeps its implied leading zeros, as a string as much as a number: `getCnae(111301)` and `getCnae('111301')` are both read as `0111301`. Same input rules as `isValidCnae`: a string has to be written as the 7 digits or with the `NNNN-N/NN` mask, and a number has to be a non-negative safe integer. @@ -2282,6 +2326,17 @@ formatNcm('abc8471'); // 8471 (only the digits are read) formatNcm(-84713012); // 8471.30.12 ``` +### parseNcm + +Remove NCM (Nomenclatura Comum do Mercosul) formatting, keep only digits, and cap the result to the 8 digits of a complete code. Nothing is left padded here; use `isValidNcm`, which does pad a bare numeric code, to check a code against the official table. + +```javascript +import { parseNcm } from '@brazilian-utils/brazilian-utils'; + +parseNcm('8471.30.12'); // '84713012' +parseNcm('8471'); // '8471' (a partial code is kept as written) +``` + ### isValidCfop Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table. The table is the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), the text in force (current wording given by Ajuste SINIEF 03/24, last amended by [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25)), not the frozen 2001 text of Ajuste SINIEF 07/01. Only operable codes count: the group and subgroup headings of the official nomenclature, the codes ending in `00` and `50` (1000, 1100, 1150, 5350, ...), are section titles rather than codes a document can carry, so they are rejected. @@ -2300,6 +2355,16 @@ isValidCfop('abc5102'); // false (not a documented form) isValidCfop(-5102); // false (not a non-negative safe integer) ``` +### parseCfop + +Remove CFOP (Código Fiscal de Operações e Prestações) formatting, keep only digits, and cap the result to 4 digits. No CFOP code starts with a zero, its first digit is the operation group from 1 to 7, so nothing is ever padded here. + +```javascript +import { parseCfop } from '@brazilian-utils/brazilian-utils'; + +parseCfop('5.102'); // '5102' +``` + ### getCfop Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description, as the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24) words it, in the text in force, last amended by [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25). The group and subgroup headings of the official nomenclature, the codes ending in `00` and `50`, are not in the table and give `null`. Same input rules as `isValidCfop`. diff --git a/docs/llms.txt b/docs/llms.txt index 142f9d1c..92bd6409 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -81,7 +81,6 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [formatVoterId](https://brazilian-utils.com.br/utilities.md#formatvoterid): Format a voter ID number. - [formatCns](https://brazilian-utils.com.br/utilities.md#formatcns): Format a CNS (Cartão Nacional de Saúde) number into the common display groups of 3-4-4-4 digits separated by spaces. - [formatCertidao](https://brazilian-utils.com.br/utilities.md#formatcertidao): Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. -- [formatCertidao](https://brazilian-utils.com.br/utilities.md#formatcertidao): Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. - [formatCei](https://brazilian-utils.com.br/utilities.md#formatcei): Format a CEI (Cadastro Específico do INSS) number according to the usual `00.000.00000/00` mask, the one the reference implementations of the check digit agree on (the Receita Federal does not print it). - [formatCno](https://brazilian-utils.com.br/utilities.md#formatcno): Format a CNO (Cadastro Nacional de Obras) number. - [formatCaepf](https://brazilian-utils.com.br/utilities.md#formatcaepf): Format a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number according to the usual `000.000.000/000-00` mask, the one the sources of the check digit rule agree on (the Receita Federal does not print it). @@ -107,6 +106,13 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [parseVoterId](https://brazilian-utils.com.br/utilities.md#parsevoterid): Remove voter ID formatting, keep only digits, and cap the result to 12 digits (13 when the UF digits identify São Paulo or Minas Gerais). - [parseCns](https://brazilian-utils.com.br/utilities.md#parsecns): Remove CNS (Cartão Nacional de Saúde) formatting, keep only digits, and cap the result to 15 digits. - [parseCertidao](https://brazilian-utils.com.br/utilities.md#parsecertidao): Remove the formatting of the matrícula of a certidão de registro civil, keep only digits, and cap the result to 32 digits. +- [parseCei](https://brazilian-utils.com.br/utilities.md#parsecei): Remove CEI (Cadastro Específico do INSS) formatting, keep only digits, and cap the result to 12 digits. +- [parseCno](https://brazilian-utils.com.br/utilities.md#parsecno): Remove CNO (Cadastro Nacional de Obras) formatting, keep only digits, and cap the result to 12 digits, the numbering the CNO kept from the CEI. +- [parseCaepf](https://brazilian-utils.com.br/utilities.md#parsecaepf): Remove CAEPF (Cadastro de Atividade Econômica da Pessoa Física) formatting, keep only digits, and cap the result to 14 digits. +- [parseCbo](https://brazilian-utils.com.br/utilities.md#parsecbo): Remove CBO (Classificação Brasileira de Ocupações) formatting, keep only digits, and cap the result to 6 digits. +- [parseCnae](https://brazilian-utils.com.br/utilities.md#parsecnae): Remove CNAE (Classificação Nacional de Atividades Econômicas) formatting, keep only digits, and cap the result to the 7 digits of a complete subclass code. +- [parseNcm](https://brazilian-utils.com.br/utilities.md#parsencm): Remove NCM (Nomenclatura Comum do Mercosul) formatting, keep only digits, and cap the result to the 8 digits of a complete code. +- [parseCfop](https://brazilian-utils.com.br/utilities.md#parsecfop): Remove CFOP (Código Fiscal de Operações e Prestações) formatting, keep only digits, and cap the result to 4 digits. ## Generators (generate*) diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index bdbcd9e3..93d70571 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -1801,19 +1801,6 @@ O resultado `CertidaoInfo` traz: | `term` | Número do termo, com 7 dígitos e zeros à esquerda. | | `checkDigits` | Os 2 dígitos verificadores módulo 11 da matrícula. | -## formatCertidao - -Formata a matrícula de uma certidão de registro civil na máscara impressa do Provimento, os 32 dígitos agrupados em 6 2 2 4 1 5 3 7 2 e separados por espaços. `options.pad` (parte de `FormatCertidaoOptions`) preenche o valor com zeros à esquerda até 32 dígitos (padrão `false`). A máscara é a do [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). Um número é aceito e lido como a string dos seus dígitos, como no `formatCpf`, mas uma matrícula completa de 32 dígitos precisa ser uma string: essa quantidade de dígitos é mais do que um número JavaScript comporta com exatidão. Em tempo de execução o valor é lido pelos seus dígitos e a máscara é aplicada até onde eles vão, como em todo formatador deste pacote, então uma matrícula parcial ainda sendo digitada é mascarada progressivamente. - -```javascript -import { formatCertidao } from '@brazilian-utils/brazilian-utils'; - -formatCertidao('10453901552013100012021000012321'); // 104539 01 55 2013 1 00012 021 0000123 21 -formatCertidao('104539.01.55.2013.1.00012.021.0000123-21'); // 104539 01 55 2013 1 00012 021 0000123 21 -formatCertidao('1552010100020112000012087', { pad: true }); // 000000 01 55 2010 1 00020 112 0000120 87 -formatCertidao(104539015520); // 104539 01 55 20 (um número é lido como a string dos seus dígitos) -``` - ## isValidCei Verifica se um número de CEI (Cadastro Específico do INSS) é válido. O CEI identifica o empregador sem CNPJ, como uma obra ou um produtor rural: 12 dígitos impressos como `00.000.00000/00`, sendo o último um dígito verificador calculado sobre os 11 dígitos da base com os pesos 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 e 4. Aceita os caracteres de máscara usuais e espaços entre e ao redor dos grupos, inclusive uma sequência deles entre dois grupos. A Receita Federal não publica essa regra de dígito verificador, então ela segue as implementações de referência do [yii2-br-validator](https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php) e do [Bigai.Documentos.Brasil](https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs), conferida contra os [dados abertos do Cadastro Nacional de Obras (CNO)](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) da Receita Federal. @@ -1840,6 +1827,16 @@ formatCei(249859674386); // 24.985.96743/86 formatCei('249', { pad: true }); // 00.000.00002/49 ``` +## parseCei + +Remove a formatação do CEI (Cadastro Específico do INSS), mantém apenas os dígitos e limita o resultado a 12 dígitos. Um valor parcial passa adiante até onde vai; use `isValidCei` para verificar o número em si. + +```javascript +import { parseCei } from '@brazilian-utils/brazilian-utils'; + +parseCei('27.729.71181/87'); // '277297118187' +``` + ## isValidCno Verifica se um número de CNO (Cadastro Nacional de Obras) é válido. O CNO substituiu o CEI para obras e manteve a mesma numeração, então uma obra registrada sob um CEI antigo conserva o número e os dois cadastros são validados do mesmo jeito: 12 dígitos impressos como `00.000.00000/00`, com o dígito verificador calculado sobre os 11 dígitos da base. A Receita Federal não publica a regra do dígito verificador; ela foi confirmada contra os [dados abertos do Cadastro Nacional de Obras (CNO)](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) da Receita Federal: todas as obras do recorte de Minas Gerais desse conjunto passam nesta verificação. A página do catálogo publica apenas a descrição e os links de download do conjunto, não esse resultado. @@ -1866,6 +1863,16 @@ formatCno(401800097960); // 40.180.00979/60 formatCno('979', { pad: true }); // 00.000.00009/79 ``` +## parseCno + +Remove a formatação do CNO (Cadastro Nacional de Obras), mantém apenas os dígitos e limita o resultado a 12 dígitos, a numeração que o CNO herdou do CEI. Use `isValidCno` para verificar o número em si. + +```javascript +import { parseCno } from '@brazilian-utils/brazilian-utils'; + +parseCno('11.113.01373/68'); // '111130137368' +``` + ## isValidCaepf Verifica se um número de CAEPF (Cadastro de Atividade Econômica da Pessoa Física) é válido. O CAEPF substituiu o CEI para a pessoa física que contrata empregados: 14 dígitos impressos como `000.000.000/000-00`, formados pela base de 9 dígitos do CPF do titular, um número de ordem de 3 dígitos para os vários cadastros do mesmo titular e 2 dígitos verificadores. Os dois dígitos verificadores são o módulo 11 do CNPJ na formulação da referência citada: os pesos vão de 9 até 2 da direita para a esquerda e o dígito é o próprio resto, com o resto 10 lido como 0 — o mesmo dígito que os pesos de 2 a 9 do CNPJ com `11 - resto` produzem. O par resultante é somado a 12, com retorno a zero acima de 99. Uma base cujos 12 dígitos são todos iguais é rejeitada antes do cálculo dos dígitos verificadores, do mesmo jeito que `isValidCei` e `isValidCno` rejeitam um número de CEI/CNO repetido, então o `00000000000012`, que de resto é bem formado, é inválido. A Receita Federal não publica o layout nem a regra dos dígitos verificadores: os dois estão descritos em [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e são implementados do mesmo jeito pelo [brazilian-values](https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts). @@ -1893,6 +1900,16 @@ formatCaepf(41142260000101); // 411.422.600/001-01 formatCaepf('184', { pad: true }); // 000.000.000/001-84 ``` +## parseCaepf + +Remove a formatação do CAEPF (Cadastro de Atividade Econômica da Pessoa Física), mantém apenas os dígitos e limita o resultado a 14 dígitos. Use `isValidCaepf` para verificar o número em si. + +```javascript +import { parseCaepf } from '@brazilian-utils/brazilian-utils'; + +parseCaepf('293.118.610/001-84'); // '29311861000184' +``` + ## isValidRegistroProfissional Verifica a estrutura de um número de registro/inscrição profissional. Recebe um único objeto, tipado como `IsValidRegistroProfissionalOptions`, no mesmo formato do `isValidBankAccount`: `value` é o número do registro, `council` escolhe o conselho emissor (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` ou `"CRC"`) e o `stateCode` opcional verifica a UF embutida (ignorado para `"CRP"`, cujo prefixo de 2 dígitos é um código regional, não uma UF literal). Qualquer coisa que não seja um objeto, e um objeto sem `value` ou sem `council`, é `false`. É apenas uma verificação estrutural: a quantidade de dígitos e a UF são validadas, mas nenhum dígito verificador é calculado, mesmo para o CRC, cujo formato inclui um. Um registro no CRC é a UF, 6 dígitos, o tipo de registro (`"O"` Originário ou `"P"` Provisório, que nada diz sobre a categoria profissional) e o dígito verificador, conforme o [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). Um Registro Transferido ou Secundário acrescenta `"T"` ou `"S"` e a UF do CRC de destino **depois** do dígito verificador, conforme esse mesmo item e a [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: os exemplos do próprio Manual são `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` e `PI-111222/O-5 S-AC`. As duas UFs precisam ser códigos reais, e o `stateCode` é comparado com a de origem. O código regional do CRP precisa ser um dos [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) do sistema CFP, de CRP-01 a CRP-24. Só o formato do CRC e esses códigos regionais do CRP se apoiam em fonte publicada: a página do CFP não publica o tamanho do número de inscrição, e a OAB, o CFM e o CFO não publicam formato algum, então as faixas de dígitos aceitas para `"CRP"`, `"OAB"`, `"CRM"` e `"CRO"` são convencionais, não normativas (a busca pública da OAB/SP tem `maxlength="7"`, e o CFM documenta CRMs com prefixo `300` e sufixo `P`, nenhum deles expresso por esses formatos). O CREA não é suportado: seu formato de registro não pôde ser confirmado em uma fonte oficial e publicamente documentada após a unificação nacional de 2016 (RNP). @@ -1941,6 +1958,16 @@ isValidCbo(-212405); // false (não é um inteiro seguro não negativo) Os títulos das ocupações vêm da [tabela oficial de ocupações da CBO 2002 publicada pelo MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). +## parseCbo + +Remove a formatação do CBO (Classificação Brasileira de Ocupações), mantém apenas os dígitos e limita o resultado a 6 dígitos. Nada é preenchido com zeros à esquerda aqui, então o zero inicial de um código como `010205` precisa ser escrito; use `getCbo` ou `isValidCbo`, que preenchem um código numérico sem máscara, para consultar uma ocupação. + +```javascript +import { parseCbo } from '@brazilian-utils/brazilian-utils'; + +parseCbo('2124-05'); // '212405' +``` + ## getCbo Consulta um código CBO (Classificação Brasileira de Ocupações) e retorna o título oficial da ocupação, no registro `{ code, description }` que toda consulta desta biblioteca devolve. Um valor escrito apenas com dígitos mantém os zeros à esquerda implícitos, tanto como string quanto como número: `getCbo(10205)` e `getCbo('10205')` são lidos como `010205`. Valem as mesmas regras de entrada de `isValidCbo`: uma string precisa estar escrita com os 6 dígitos ou com a máscara `NNNN-NN`, e um número precisa ser um inteiro seguro não negativo. @@ -1989,6 +2016,17 @@ formatCnae('abc6201501'); // 6201-5/01 (só os dígitos são lidos) formatCnae(-6201501); // 6201-5/01 ``` +## parseCnae + +Remove a formatação do CNAE (Classificação Nacional de Atividades Econômicas), mantém apenas os dígitos e limita o resultado aos 7 dígitos de um código de subclasse completo. Nada é preenchido com zeros à esquerda aqui; use `getCnae` ou `isValidCnae`, que preenchem um código numérico sem máscara, para consultar uma subclasse. + +```javascript +import { parseCnae } from '@brazilian-utils/brazilian-utils'; + +parseCnae('6201-5/01'); // '6201501' +parseCnae('62'); // '62' (a partial code is kept as written) +``` + ## getCnae Busca um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas) e retorna seu código e a descrição oficial. O `code` volta com os 7 dígitos crus, como em toda consulta desta biblioteca; passe-o para `formatCnae` para obter a forma `NNNN-N/NN`. Um valor escrito apenas com dígitos mantém os zeros à esquerda implícitos, tanto como string quanto como número: `getCnae(111301)` e `getCnae('111301')` são lidos como `0111301`. Valem as mesmas regras de entrada de `isValidCnae`: uma string precisa estar escrita com os 7 dígitos ou com a máscara `NNNN-N/NN`, e um número precisa ser um inteiro seguro não negativo. @@ -2035,6 +2073,17 @@ formatNcm('abc8471'); // 8471 (só os dígitos são lidos) formatNcm(-84713012); // 8471.30.12 ``` +## parseNcm + +Remove a formatação do NCM (Nomenclatura Comum do Mercosul), mantém apenas os dígitos e limita o resultado aos 8 dígitos de um código completo. Nada é preenchido com zeros à esquerda aqui; use `isValidNcm`, que preenche um código numérico sem máscara, para verificar um código na tabela oficial. + +```javascript +import { parseNcm } from '@brazilian-utils/brazilian-utils'; + +parseNcm('8471.30.12'); // '84713012' +parseNcm('8471'); // '8471' (a partial code is kept as written) +``` + ## isValidCfop Valida se um código CFOP (Código Fiscal de Operações e Prestações) existe na tabela oficial. A tabela é o [Anexo II consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), o texto vigente (redação atual dada pelo Ajuste SINIEF 03/24, última alteração pelo [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25)), e não o texto congelado de 2001 do Ajuste SINIEF 07/01. Só os códigos operáveis contam: os títulos de grupo e subgrupo da nomenclatura oficial, os códigos terminados em `00` e `50` (1000, 1100, 1150, 5350, ...), são títulos de seção e não códigos que um documento pode carregar, então são rejeitados. @@ -2053,6 +2102,16 @@ isValidCfop('abc5102'); // false (não é uma forma documentada) isValidCfop(-5102); // false (não é um inteiro seguro não negativo) ``` +## parseCfop + +Remove a formatação do CFOP (Código Fiscal de Operações e Prestações), mantém apenas os dígitos e limita o resultado a 4 dígitos. Nenhum código CFOP começa com zero, o primeiro dígito é o grupo da operação, de 1 a 7, então nada é preenchido com zeros aqui. + +```javascript +import { parseCfop } from '@brazilian-utils/brazilian-utils'; + +parseCfop('5.102'); // '5102' +``` + ## getCfop Busca um código CFOP (Código Fiscal de Operações e Prestações) e retorna seu código e a descrição oficial, na redação do [Anexo II consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), no texto vigente, com última alteração pelo [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25). Os títulos de grupo e subgrupo da nomenclatura oficial, os códigos terminados em `00` e `50`, não estão na tabela e retornam `null`. Valem as mesmas regras de entrada de `isValidCfop`. diff --git a/docs/utilities.md b/docs/utilities.md index c9affaa1..46697427 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -1801,19 +1801,6 @@ The `CertidaoInfo` result carries: | `term` | The 7 digit term (termo) number, zero padded. | | `checkDigits` | The 2 modulus 11 check digits of the matrícula. | -## formatCertidao - -Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). A number is accepted and read as the string of its digits, like in `formatCpf`, but a full 32 digit matrícula has to be a string: that many digits are more than a JavaScript number can hold exactly. At runtime the value is read for its digits and masked as far as they go, like in every formatter of this package, so a partial matrícula still being typed is masked progressively. - -```javascript -import { formatCertidao } from '@brazilian-utils/brazilian-utils'; - -formatCertidao('10453901552013100012021000012321'); // 104539 01 55 2013 1 00012 021 0000123 21 -formatCertidao('104539.01.55.2013.1.00012.021.0000123-21'); // 104539 01 55 2013 1 00012 021 0000123 21 -formatCertidao('1552010100020112000012087', { pad: true }); // 000000 01 55 2010 1 00020 112 0000120 87 -formatCertidao(104539015520); // 104539 01 55 20 (a number is read as the string of its digits) -``` - ## isValidCei Check if a CEI (Cadastro Específico do INSS) number is valid. The CEI identifies an employer with no CNPJ, such as a construction work or a rural producer: 12 digits printed as `00.000.00000/00`, the last one a check digit calculated over the 11 base digits with the weights 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 and 4. Accepts the usual mask characters and whitespace between/around groups, a run of them between two groups included. The Receita Federal does not publish this check digit rule, so it follows the reference implementations of [yii2-br-validator](https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php) and [Bigai.Documentos.Brasil](https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs), cross-checked against the [Cadastro Nacional de Obras (CNO) open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) of the Receita Federal. @@ -1840,6 +1827,16 @@ formatCei(249859674386); // 24.985.96743/86 formatCei('249', { pad: true }); // 00.000.00002/49 ``` +## parseCei + +Remove CEI (Cadastro Específico do INSS) formatting, keep only digits, and cap the result to 12 digits. A partial value passes through as far as it goes; use `isValidCei` to check the number itself. + +```javascript +import { parseCei } from '@brazilian-utils/brazilian-utils'; + +parseCei('27.729.71181/87'); // '277297118187' +``` + ## isValidCno Check if a CNO (Cadastro Nacional de Obras) number is valid. The CNO replaced the CEI for construction works and kept its numbering, so a work registered under a legacy CEI keeps the same number and both registries validate identically: 12 digits printed as `00.000.00000/00` with a check digit calculated over the 11 base digits. The Receita Federal does not publish the check digit rule; it was confirmed against the [Cadastro Nacional de Obras (CNO) open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) of the Receita Federal: every work in the Minas Gerais extract of that dataset passes this check. The catalogue page itself publishes only the dataset's description and download links, not that result. @@ -1866,6 +1863,16 @@ formatCno(401800097960); // 40.180.00979/60 formatCno('979', { pad: true }); // 00.000.00009/79 ``` +## parseCno + +Remove CNO (Cadastro Nacional de Obras) formatting, keep only digits, and cap the result to 12 digits, the numbering the CNO kept from the CEI. Use `isValidCno` to check the number itself. + +```javascript +import { parseCno } from '@brazilian-utils/brazilian-utils'; + +parseCno('11.113.01373/68'); // '111130137368' +``` + ## isValidCaepf Check if a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number is valid. The CAEPF replaced the CEI for individuals who hire employees: 14 digits printed as `000.000.000/000-00`, formed by the 9 digit CPF base of the holder, a 3 digit sequence for the holder's several registrations and 2 check digits. Both check digits are the CNPJ's modulus 11 in the formulation of the cited reference: the weights cycle from 9 down to 2 from the right and the check digit is the remainder itself, with a remainder of 10 read as 0 — the same digit the CNPJ's 2-to-9 weights with `11 - remainder` produce. The resulting pair is then shifted by 12, wrapping around 100. A base whose 12 digits are all the same is rejected before the check digits are computed, the way `isValidCei` and `isValidCno` reject a repeated CEI/CNO number, so the otherwise well-formed `00000000000012` is invalid. The Receita Federal does not publish the layout or the check digit rule: both are described by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented the same way by [brazilian-values](https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts). @@ -1893,6 +1900,16 @@ formatCaepf(41142260000101); // 411.422.600/001-01 formatCaepf('184', { pad: true }); // 000.000.000/001-84 ``` +## parseCaepf + +Remove CAEPF (Cadastro de Atividade Econômica da Pessoa Física) formatting, keep only digits, and cap the result to 14 digits. Use `isValidCaepf` to check the number itself. + +```javascript +import { parseCaepf } from '@brazilian-utils/brazilian-utils'; + +parseCaepf('293.118.610/001-84'); // '29311861000184' +``` + ## isValidRegistroProfissional Check the structure of a professional council registration number (registro/inscrição profissional). It takes a single object, typed as `IsValidRegistroProfissionalOptions`, the shape `isValidBankAccount` takes: `value` is the registration number, `council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). Anything that is not an object, and an object missing `value` or `council`, is `false`. This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits, the tipo de registro (`"O"` Originário or `"P"` Provisório, which says nothing about the professional category) and the check digit, as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). A Registro Transferido or Secundário appends `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, per that same item and [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: the Manual's own examples are `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` and `PI-111222/O-5 S-AC`. Both UFs must be real state codes, and `stateCode` is compared against the originating one. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. Only the CRC shape and those CRP regional codes rest on a published source: the CFP page publishes no length for the inscription number itself, and the OAB, the CFM and the CFO publish no format at all, so the digit ranges accepted for `"CRP"`, `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative (the OAB/SP public search field is `maxlength="7"`, and the CFM documents `300`-prefixed and `P`-suffixed CRMs, none of which these shapes express). CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). @@ -1941,6 +1958,16 @@ isValidCbo(-212405); // false (not a non-negative safe integer) The occupation titles come from the [official CBO 2002 occupation table published by the MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). +## parseCbo + +Remove CBO (Classificação Brasileira de Ocupações) formatting, keep only digits, and cap the result to 6 digits. Nothing is left padded here, so the leading zero of a code such as `010205` has to be written out; use `getCbo` or `isValidCbo`, which do pad a bare numeric code, to look an occupation up. + +```javascript +import { parseCbo } from '@brazilian-utils/brazilian-utils'; + +parseCbo('2124-05'); // '212405' +``` + ## getCbo Look a CBO (Classificação Brasileira de Ocupações) code up and get its official occupation title, in the `{ code, description }` record every lookup of this library returns. A value written as bare digits keeps its implied leading zeros, as a string as much as a number: `getCbo(10205)` and `getCbo('10205')` are both read as `010205`. Same input rules as `isValidCbo`: a string has to be written as the 6 digits or with the `NNNN-NN` mask, and a number has to be a non-negative safe integer. @@ -1989,6 +2016,17 @@ formatCnae('abc6201501'); // 6201-5/01 (only the digits are read) formatCnae(-6201501); // 6201-5/01 ``` +## parseCnae + +Remove CNAE (Classificação Nacional de Atividades Econômicas) formatting, keep only digits, and cap the result to the 7 digits of a complete subclass code. Nothing is left padded here; use `getCnae` or `isValidCnae`, which do pad a bare numeric code, to look a subclass up. + +```javascript +import { parseCnae } from '@brazilian-utils/brazilian-utils'; + +parseCnae('6201-5/01'); // '6201501' +parseCnae('62'); // '62' (a partial code is kept as written) +``` + ## getCnae Look a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up and get its code and official description. `code` comes back as the 7 bare digits, like every other lookup of this library; pass it to `formatCnae` for the `NNNN-N/NN` form. A value written as bare digits keeps its implied leading zeros, as a string as much as a number: `getCnae(111301)` and `getCnae('111301')` are both read as `0111301`. Same input rules as `isValidCnae`: a string has to be written as the 7 digits or with the `NNNN-N/NN` mask, and a number has to be a non-negative safe integer. @@ -2035,6 +2073,17 @@ formatNcm('abc8471'); // 8471 (only the digits are read) formatNcm(-84713012); // 8471.30.12 ``` +## parseNcm + +Remove NCM (Nomenclatura Comum do Mercosul) formatting, keep only digits, and cap the result to the 8 digits of a complete code. Nothing is left padded here; use `isValidNcm`, which does pad a bare numeric code, to check a code against the official table. + +```javascript +import { parseNcm } from '@brazilian-utils/brazilian-utils'; + +parseNcm('8471.30.12'); // '84713012' +parseNcm('8471'); // '8471' (a partial code is kept as written) +``` + ## isValidCfop Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table. The table is the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), the text in force (current wording given by Ajuste SINIEF 03/24, last amended by [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25)), not the frozen 2001 text of Ajuste SINIEF 07/01. Only operable codes count: the group and subgroup headings of the official nomenclature, the codes ending in `00` and `50` (1000, 1100, 1150, 5350, ...), are section titles rather than codes a document can carry, so they are rejected. @@ -2053,6 +2102,16 @@ isValidCfop('abc5102'); // false (not a documented form) isValidCfop(-5102); // false (not a non-negative safe integer) ``` +## parseCfop + +Remove CFOP (Código Fiscal de Operações e Prestações) formatting, keep only digits, and cap the result to 4 digits. No CFOP code starts with a zero, its first digit is the operation group from 1 to 7, so nothing is ever padded here. + +```javascript +import { parseCfop } from '@brazilian-utils/brazilian-utils'; + +parseCfop('5.102'); // '5102' +``` + ## getCfop Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description, as the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24) words it, in the text in force, last amended by [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25). The group and subgroup headings of the official nomenclature, the codes ending in `00` and `50`, are not in the table and give `null`. Same input rules as `isValidCfop`. diff --git a/src/index.test.ts b/src/index.test.ts index e2b16065..c3d27a22 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -213,21 +213,27 @@ const PUBLIC = [ "isValidVin", "isValidVoterId", "parseBoleto", + "parseCaepf", + "parseCbo", + "parseCei", "parseCep", "parseCertidao", + "parseCfop", + "parseCnae", "parseCnh", + "parseCno", "parseCnpj", + "parseCns", "parseCpf", "parseCurrency", "parseIban", "parseLegalNature", "parseLicensePlate", + "parseNcm", "parseNfeKey", "parsePassport", "parsePhone", "parsePis", - "parsePixKey", - "parsePixPayload", "parseProcessoJuridico", "parseVoterId", "removeAccents", diff --git a/src/index.ts b/src/index.ts index 411774ff..7f618085 100644 --- a/src/index.ts +++ b/src/index.ts @@ -207,25 +207,27 @@ export { isValidServicePhone } from "./is-valid-service-phone/is-valid-service-p export { isValidVin } from "./is-valid-vin/is-valid-vin"; export { isValidVoterId } from "./is-valid-voter-id/is-valid-voter-id"; export { parseBoleto } from "./parse-boleto/parse-boleto"; +export { parseCaepf } from "./parse-caepf/parse-caepf"; +export { parseCbo } from "./parse-cbo/parse-cbo"; +export { parseCei } from "./parse-cei/parse-cei"; export { parseCep } from "./parse-cep/parse-cep"; -export { type Certidao, type CertidaoType, parseCertidao } from "./parse-certidao/parse-certidao"; +export { parseCertidao } from "./parse-certidao/parse-certidao"; +export { parseCfop } from "./parse-cfop/parse-cfop"; +export { parseCnae } from "./parse-cnae/parse-cnae"; export { parseCnh } from "./parse-cnh/parse-cnh"; +export { parseCno } from "./parse-cno/parse-cno"; export { type ParseCnpjOptions, parseCnpj } from "./parse-cnpj/parse-cnpj"; +export { parseCns } from "./parse-cns/parse-cns"; export { parseCpf } from "./parse-cpf/parse-cpf"; export { type ParseCurrencyOptions, parseCurrency } from "./parse-currency/parse-currency"; -export { type Iban, parseIban } from "./parse-iban/parse-iban"; +export { parseIban } from "./parse-iban/parse-iban"; export { parseLegalNature } from "./parse-legal-nature/parse-legal-nature"; export { parseLicensePlate } from "./parse-license-plate/parse-license-plate"; -export { type NfeKey, type NfeKeyModel, parseNfeKey } from "./parse-nfe-key/parse-nfe-key"; +export { parseNcm } from "./parse-ncm/parse-ncm"; +export { parseNfeKey } from "./parse-nfe-key/parse-nfe-key"; export { parsePassport } from "./parse-passport/parse-passport"; export { parsePhone } from "./parse-phone/parse-phone"; export { parsePis } from "./parse-pis/parse-pis"; -export { type PixKey, type PixKeyType, parsePixKey } from "./parse-pix-key/parse-pix-key"; -export { - type PixPayload, - type PixPointOfInitiation, - parsePixPayload, -} from "./parse-pix-payload/parse-pix-payload"; export { parseProcessoJuridico } from "./parse-processo-juridico/parse-processo-juridico"; export { parseVoterId } from "./parse-voter-id/parse-voter-id"; export { removeAccents } from "./remove-accents/remove-accents"; diff --git a/src/parse-caepf/constants.ts b/src/parse-caepf/constants.ts new file mode 100644 index 00000000..95895886 --- /dev/null +++ b/src/parse-caepf/constants.ts @@ -0,0 +1,2 @@ +/** Digits of a CAEPF number, printed as "000.000.000/000-00". */ +export const LENGTH = 14; diff --git a/src/parse-caepf/parse-caepf.test.ts b/src/parse-caepf/parse-caepf.test.ts new file mode 100644 index 00000000..2fb7c947 --- /dev/null +++ b/src/parse-caepf/parse-caepf.test.ts @@ -0,0 +1,58 @@ +import { anyText, anyValue, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { formatCaepf } from "../format-caepf/format-caepf"; +import { parseCaepf } from "./parse-caepf"; + +describe("parseCaepf", () => { + it("should remove CAEPF mask characters", () => { + expect(parseCaepf("293.118.610/001-84")).toBe("29311861000184"); + }); + + it("should remove non numeric characters", () => { + expect(parseCaepf("293.?ABC118.610/001-84abc")).toBe("29311861000184"); + }); + + it("should ignore digits after the CAEPF length", () => { + expect(parseCaepf("29311861000184999")).toBe("29311861000184"); + }); + + it("should read a number as the string of its digits", () => { + expect(parseCaepf(29_311_861_000_184)).toBe("29311861000184"); + }); + + it("should return an empty string for null", () => { + // @ts-expect-error not a string or number + expect(parseCaepf(null)).toBe(""); + }); + + describe("properties", () => { + test("should return at most the digits of a CAEPF", () => { + expectMatchesPattern(parseCaepf, /^\d{0,14}$/, anyText); + }); + + test("should undo formatCaepf", () => { + expectRoundTrip(formatCaepf, parseCaepf, digitsUpTo(14)); + }); + + test("should be idempotent", () => { + expectIdempotent(parseCaepf, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseCaepf, "string", anyValue); + }); + }); +}); + +describe("parseCaepf types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseCaepf).parameter(0).toEqualTypeOf(); + expectTypeOf(parseCaepf).returns.toEqualTypeOf(); + }); +}); diff --git a/src/parse-caepf/parse-caepf.ts b/src/parse-caepf/parse-caepf.ts new file mode 100644 index 00000000..10a668f7 --- /dev/null +++ b/src/parse-caepf/parse-caepf.ts @@ -0,0 +1,26 @@ +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "./constants"; + +/** + * Removes CAEPF (Cadastro de Atividade Econômica da Pessoa Física) formatting characters and + * returns only digits. + * + * The number has 14 digits, 12 of base plus the two check digits, which is the length the result + * is capped at; a shorter value passes through as far as it goes. Use `isValidCaepf` to check the + * number itself. + * + * @param {string|number} value - The CAEPF value to be parsed. + * @returns {string} Up to 14 digits, or an empty string when there is no digit at all. + * + * @example + * ```typescript + * parseCaepf("293.118.610/001-84"); // "29311861000184" + * ``` + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/caepf + * The registry's own page at the Receita Federal, which describes the cadastro but does not print + * the mask; the mask is the one the sources cited by `isValidCaepf` agree on. + */ +export const parseCaepf = (value: string | number): string => + isNullish(value) ? "" : sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-cbo/constants.ts b/src/parse-cbo/constants.ts new file mode 100644 index 00000000..29915d8c --- /dev/null +++ b/src/parse-cbo/constants.ts @@ -0,0 +1,2 @@ +/** Digits of a CBO (Classificação Brasileira de Ocupações) code, printed as "0000-00". */ +export const LENGTH = 6; diff --git a/src/parse-cbo/parse-cbo.test.ts b/src/parse-cbo/parse-cbo.test.ts new file mode 100644 index 00000000..bc6c3a0a --- /dev/null +++ b/src/parse-cbo/parse-cbo.test.ts @@ -0,0 +1,56 @@ +import { anyText, anyValue } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { parseCbo } from "./parse-cbo"; + +describe("parseCbo", () => { + it("should remove CBO mask characters", () => { + expect(parseCbo("2124-05")).toBe("212405"); + }); + + it("should remove non numeric characters", () => { + expect(parseCbo("21?ABC24-05abc")).toBe("212405"); + }); + + it("should ignore digits after the CBO length", () => { + expect(parseCbo("212405999")).toBe("212405"); + }); + + it("should read a number as the string of its digits", () => { + expect(parseCbo(212_405)).toBe("212405"); + }); + + it("should keep a partial code as written, without padding it", () => { + expect(parseCbo("10205")).toBe("10205"); + }); + + it("should return an empty string for null", () => { + // @ts-expect-error not a string or number + expect(parseCbo(null)).toBe(""); + }); + + describe("properties", () => { + test("should return at most the digits of a CBO code", () => { + expectMatchesPattern(parseCbo, /^\d{0,6}$/, anyText); + }); + + test("should be idempotent", () => { + expectIdempotent(parseCbo, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseCbo, "string", anyValue); + }); + }); +}); + +describe("parseCbo types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseCbo).parameter(0).toEqualTypeOf(); + expectTypeOf(parseCbo).returns.toEqualTypeOf(); + }); +}); diff --git a/src/parse-cbo/parse-cbo.ts b/src/parse-cbo/parse-cbo.ts new file mode 100644 index 00000000..86eece2f --- /dev/null +++ b/src/parse-cbo/parse-cbo.ts @@ -0,0 +1,26 @@ +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "./constants"; + +/** + * Removes CBO (Classificação Brasileira de Ocupações) formatting characters and returns only + * digits. + * + * An occupation code has 6 digits, which is the length the result is capped at; a shorter value + * passes through as far as it goes and is never left padded, so the leading zero of a code such + * as `010205` has to be written out. Use `getCbo` or `isValidCbo`, which do pad a bare numeric + * code, to look an occupation up. + * + * @param {string|number} value - The CBO code to be parsed. + * @returns {string} Up to 6 digits, or an empty string when there is no digit at all. + * + * @example + * ```typescript + * parseCbo("2124-05"); // "212405" + * ``` + * + * @see Official: https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv + * The CBO 2002 occupation table, as published by the Ministério do Trabalho e Emprego. + */ +export const parseCbo = (value: string | number): string => + isNullish(value) ? "" : sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-cei/constants.ts b/src/parse-cei/constants.ts new file mode 100644 index 00000000..a063dbe1 --- /dev/null +++ b/src/parse-cei/constants.ts @@ -0,0 +1,2 @@ +/** Digits of a CEI (Cadastro Específico do INSS) number, printed as "00.000.00000/00". */ +export const LENGTH = 12; diff --git a/src/parse-cei/parse-cei.test.ts b/src/parse-cei/parse-cei.test.ts new file mode 100644 index 00000000..393f687f --- /dev/null +++ b/src/parse-cei/parse-cei.test.ts @@ -0,0 +1,58 @@ +import { anyText, anyValue, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { formatCei } from "../format-cei/format-cei"; +import { parseCei } from "./parse-cei"; + +describe("parseCei", () => { + it("should remove CEI mask characters", () => { + expect(parseCei("27.729.71181/87")).toBe("277297118187"); + }); + + it("should remove non numeric characters", () => { + expect(parseCei("27.?ABC729.71181/87abc")).toBe("277297118187"); + }); + + it("should ignore digits after the CEI length", () => { + expect(parseCei("277297118187999")).toBe("277297118187"); + }); + + it("should read a number as the string of its digits", () => { + expect(parseCei(277_297_118_187)).toBe("277297118187"); + }); + + it("should return an empty string for null", () => { + // @ts-expect-error not a string or number + expect(parseCei(null)).toBe(""); + }); + + describe("properties", () => { + test("should return at most the digits of a CEI", () => { + expectMatchesPattern(parseCei, /^\d{0,12}$/, anyText); + }); + + test("should undo formatCei", () => { + expectRoundTrip(formatCei, parseCei, digitsUpTo(12)); + }); + + test("should be idempotent", () => { + expectIdempotent(parseCei, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseCei, "string", anyValue); + }); + }); +}); + +describe("parseCei types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseCei).parameter(0).toEqualTypeOf(); + expectTypeOf(parseCei).returns.toEqualTypeOf(); + }); +}); diff --git a/src/parse-cei/parse-cei.ts b/src/parse-cei/parse-cei.ts new file mode 100644 index 00000000..7e714966 --- /dev/null +++ b/src/parse-cei/parse-cei.ts @@ -0,0 +1,25 @@ +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "./constants"; + +/** + * Removes CEI (Cadastro Específico do INSS) formatting characters and returns only digits. + * + * The numbering has 12 digits, 11 of base and one check digit, which is the length the result is + * capped at; a shorter value passes through as far as it goes, so the mask of an input still + * being typed can be stripped with it. Use `isValidCei` to check the number itself. + * + * @param {string|number} value - The CEI value to be parsed. + * @returns {string} Up to 12 digits, or an empty string when there is no digit at all. + * + * @example + * ```typescript + * parseCei("27.729.71181/87"); // "277297118187" + * ``` + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/cno + * The registry's own page at the Receita Federal, which describes the cadastro but does not print + * the mask; the mask is the one the reference implementations cited by `isValidCei` agree on. + */ +export const parseCei = (value: string | number): string => + isNullish(value) ? "" : sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-certidao/parse-certidao.test.ts b/src/parse-certidao/parse-certidao.test.ts new file mode 100644 index 00000000..611c03a7 --- /dev/null +++ b/src/parse-certidao/parse-certidao.test.ts @@ -0,0 +1,65 @@ +import { CERTIDAO_LENGTH } from "../_internals/constants/certidao"; +import { anyText, anyValue, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { formatCertidao } from "../format-certidao/format-certidao"; +import { parseCertidao } from "./parse-certidao"; + +describe("parseCertidao", () => { + it("should remove the matrícula mask characters", () => { + expect(parseCertidao("104539 01 55 2013 1 00012 021 0000123 21")).toBe( + "10453901552013100012021000012321", + ); + }); + + it("should remove non numeric characters", () => { + expect(parseCertidao("104539.01.55.2013.1.00012.021.0000123-21abc")).toBe( + "10453901552013100012021000012321", + ); + }); + + it(`should ignore digits after the matrícula length (${CERTIDAO_LENGTH})`, () => { + expect(parseCertidao("10453901552013100012021000012321999")).toBe( + "10453901552013100012021000012321", + ); + }); + + it("should keep a partial matrícula as written", () => { + expect(parseCertidao("104539 01")).toBe("10453901"); + }); + + it("should return an empty string for null", () => { + // @ts-expect-error not a string or number + expect(parseCertidao(null)).toBe(""); + }); + + describe("properties", () => { + test("should return at most the digits of a matrícula", () => { + expectMatchesPattern(parseCertidao, /^\d{0,32}$/, anyText); + }); + + test("should undo formatCertidao", () => { + expectRoundTrip(formatCertidao, parseCertidao, digitsUpTo(CERTIDAO_LENGTH)); + }); + + test("should be idempotent", () => { + expectIdempotent(parseCertidao, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseCertidao, "string", anyValue); + }); + }); +}); + +describe("parseCertidao types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseCertidao).parameter(0).toEqualTypeOf(); + expectTypeOf(parseCertidao).returns.toEqualTypeOf(); + }); +}); diff --git a/src/parse-certidao/parse-certidao.ts b/src/parse-certidao/parse-certidao.ts new file mode 100644 index 00000000..66892832 --- /dev/null +++ b/src/parse-certidao/parse-certidao.ts @@ -0,0 +1,32 @@ +import { CERTIDAO_LENGTH } from "../_internals/constants/certidao"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +/** + * Removes the formatting of the matrícula of a certidão de registro civil and returns only + * digits. + * + * The matrícula has 32 digits, which is the length the result is capped at; a shorter value + * passes through as far as it goes, so the mask of an input still being typed can be stripped + * with it. This only takes the mask off: use `isValidCertidao` to check the matrícula and + * `getCertidaoInfo` to read its fields. + * + * @param {string|number} value - The matrícula value to be parsed. + * @returns {string} Up to 32 digits, or an empty string when there is no digit at all. + * + * @example + * ```typescript + * parseCertidao("104539 01 55 2013 1 00012 021 0000123 21"); + * // "10453901552013100012021000012321" + * ``` + * + * @see Official: https://atos.cnj.jus.br/atos/detalhar/5243 + * Código Nacional de Normas da Corregedoria Nacional de Justiça - Foro Extrajudicial (Provimento + * CNJ nº 149/2023), art. 473: the in-force 6 + 2 + 2 + 4 + 1 + 5 + 3 + 7 + 2 layout of the 32 + * digit matrícula. + * @see Official: https://atos.cnj.jus.br/atos/detalhar/1310 + * Provimento CNJ nº 3, de 17/11/2009, art. 7º, where that matrícula first got the same digit + * structure (revoked; historical). + */ +export const parseCertidao = (value: string | number): string => + isNullish(value) ? "" : sanitizeToDigits(value).slice(0, CERTIDAO_LENGTH); diff --git a/src/parse-cfop/constants.ts b/src/parse-cfop/constants.ts new file mode 100644 index 00000000..4690ce14 --- /dev/null +++ b/src/parse-cfop/constants.ts @@ -0,0 +1,2 @@ +/** Digits of a CFOP (Código Fiscal de Operações e Prestações) code, printed as "0.000". */ +export const LENGTH = 4; diff --git a/src/parse-cfop/parse-cfop.test.ts b/src/parse-cfop/parse-cfop.test.ts new file mode 100644 index 00000000..aec7beeb --- /dev/null +++ b/src/parse-cfop/parse-cfop.test.ts @@ -0,0 +1,52 @@ +import { anyText, anyValue } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { parseCfop } from "./parse-cfop"; + +describe("parseCfop", () => { + it("should remove CFOP mask characters", () => { + expect(parseCfop("5.102")).toBe("5102"); + }); + + it("should remove non numeric characters", () => { + expect(parseCfop("5?ABC.102abc")).toBe("5102"); + }); + + it("should ignore digits after the CFOP length", () => { + expect(parseCfop("5102999")).toBe("5102"); + }); + + it("should read a number as the string of its digits", () => { + expect(parseCfop(5102)).toBe("5102"); + }); + + it("should return an empty string for null", () => { + // @ts-expect-error not a string or number + expect(parseCfop(null)).toBe(""); + }); + + describe("properties", () => { + test("should return at most the digits of a CFOP code", () => { + expectMatchesPattern(parseCfop, /^\d{0,4}$/, anyText); + }); + + test("should be idempotent", () => { + expectIdempotent(parseCfop, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseCfop, "string", anyValue); + }); + }); +}); + +describe("parseCfop types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseCfop).parameter(0).toEqualTypeOf(); + expectTypeOf(parseCfop).returns.toEqualTypeOf(); + }); +}); diff --git a/src/parse-cfop/parse-cfop.ts b/src/parse-cfop/parse-cfop.ts new file mode 100644 index 00000000..077f9ced --- /dev/null +++ b/src/parse-cfop/parse-cfop.ts @@ -0,0 +1,26 @@ +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "./constants"; + +/** + * Removes CFOP (Código Fiscal de Operações e Prestações) formatting characters and returns only + * digits. + * + * A code has 4 digits, which is the length the result is capped at; a shorter value passes + * through as far as it goes. No CFOP code starts with a zero, its first digit is the operation + * group from 1 to 7, so nothing is ever padded here. Use `getCfop` or `isValidCfop` to look a + * code up in the official table. + * + * @param {string|number} value - The CFOP code to be parsed. + * @returns {string} Up to 4 digits, or an empty string when there is no digit at all. + * + * @example + * ```typescript + * parseCfop("5.102"); // "5102" + * ``` + * + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24 + * Consolidated Anexo II of Convênio SINIEF s/nº 1970, which prints the codes in the "N.NNN" form. + */ +export const parseCfop = (value: string | number): string => + isNullish(value) ? "" : sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-cnae/constants.ts b/src/parse-cnae/constants.ts new file mode 100644 index 00000000..04d0bc61 --- /dev/null +++ b/src/parse-cnae/constants.ts @@ -0,0 +1,2 @@ +/** Digits of a complete CNAE subclass code, printed as "0000-0/00". */ +export const LENGTH = 7; diff --git a/src/parse-cnae/parse-cnae.test.ts b/src/parse-cnae/parse-cnae.test.ts new file mode 100644 index 00000000..00a41e6a --- /dev/null +++ b/src/parse-cnae/parse-cnae.test.ts @@ -0,0 +1,62 @@ +import { anyText, anyValue, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { formatCnae } from "../format-cnae/format-cnae"; +import { parseCnae } from "./parse-cnae"; + +describe("parseCnae", () => { + it("should remove CNAE mask characters", () => { + expect(parseCnae("6201-5/01")).toBe("6201501"); + }); + + it("should remove non numeric characters", () => { + expect(parseCnae("62?ABC01-5/01abc")).toBe("6201501"); + }); + + it("should ignore digits after the CNAE length", () => { + expect(parseCnae("6201501999")).toBe("6201501"); + }); + + it("should read a number as the string of its digits", () => { + expect(parseCnae(6_201_501)).toBe("6201501"); + }); + + it("should keep a partial code as written, without padding it", () => { + expect(parseCnae("62")).toBe("62"); + }); + + it("should return an empty string for null", () => { + // @ts-expect-error not a string or number + expect(parseCnae(null)).toBe(""); + }); + + describe("properties", () => { + test("should return at most the digits of a CNAE subclass", () => { + expectMatchesPattern(parseCnae, /^\d{0,7}$/, anyText); + }); + + test("should undo formatCnae", () => { + expectRoundTrip(formatCnae, parseCnae, digitsUpTo(7)); + }); + + test("should be idempotent", () => { + expectIdempotent(parseCnae, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseCnae, "string", anyValue); + }); + }); +}); + +describe("parseCnae types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseCnae).parameter(0).toEqualTypeOf(); + expectTypeOf(parseCnae).returns.toEqualTypeOf(); + }); +}); diff --git a/src/parse-cnae/parse-cnae.ts b/src/parse-cnae/parse-cnae.ts new file mode 100644 index 00000000..aea5a35b --- /dev/null +++ b/src/parse-cnae/parse-cnae.ts @@ -0,0 +1,25 @@ +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "./constants"; + +/** + * Removes CNAE (Classificação Nacional de Atividades Econômicas) formatting characters and + * returns only digits. + * + * A complete subclass code has 7 digits, which is the length the result is capped at; a shorter + * value (a division, a group or a class still being typed) passes through as far as it goes and + * is never left padded, so the leading zeros a code carries have to be written out. Use + * `getCnae` or `isValidCnae`, which do pad a bare numeric code, to look a code up. + * + * @param {string|number} value - The CNAE code to be parsed. + * @returns {string} Up to 7 digits, or an empty string when there is no digit at all. + * + * @example + * ```typescript + * parseCnae("6201-5/01"); // "6201501" + * ``` + * + * @see Official: https://servicodados.ibge.gov.br/api/v2/cnae/subclasses + */ +export const parseCnae = (value: string | number): string => + isNullish(value) ? "" : sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-cno/constants.ts b/src/parse-cno/constants.ts new file mode 100644 index 00000000..5aac1b3c --- /dev/null +++ b/src/parse-cno/constants.ts @@ -0,0 +1,2 @@ +/** Digits of a CNO (Cadastro Nacional de Obras) number, printed as "00.000.00000/00". */ +export const LENGTH = 12; diff --git a/src/parse-cno/parse-cno.test.ts b/src/parse-cno/parse-cno.test.ts new file mode 100644 index 00000000..deb7b399 --- /dev/null +++ b/src/parse-cno/parse-cno.test.ts @@ -0,0 +1,58 @@ +import { anyText, anyValue, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { formatCno } from "../format-cno/format-cno"; +import { parseCno } from "./parse-cno"; + +describe("parseCno", () => { + it("should remove CNO mask characters", () => { + expect(parseCno("11.113.01373/68")).toBe("111130137368"); + }); + + it("should remove non numeric characters", () => { + expect(parseCno("11.?ABC113.01373/68abc")).toBe("111130137368"); + }); + + it("should ignore digits after the CNO length", () => { + expect(parseCno("111130137368999")).toBe("111130137368"); + }); + + it("should read a number as the string of its digits", () => { + expect(parseCno(111_130_137_368)).toBe("111130137368"); + }); + + it("should return an empty string for null", () => { + // @ts-expect-error not a string or number + expect(parseCno(null)).toBe(""); + }); + + describe("properties", () => { + test("should return at most the digits of a CNO", () => { + expectMatchesPattern(parseCno, /^\d{0,12}$/, anyText); + }); + + test("should undo formatCno", () => { + expectRoundTrip(formatCno, parseCno, digitsUpTo(12)); + }); + + test("should be idempotent", () => { + expectIdempotent(parseCno, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseCno, "string", anyValue); + }); + }); +}); + +describe("parseCno types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseCno).parameter(0).toEqualTypeOf(); + expectTypeOf(parseCno).returns.toEqualTypeOf(); + }); +}); diff --git a/src/parse-cno/parse-cno.ts b/src/parse-cno/parse-cno.ts new file mode 100644 index 00000000..bc9c49a2 --- /dev/null +++ b/src/parse-cno/parse-cno.ts @@ -0,0 +1,25 @@ +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "./constants"; + +/** + * Removes CNO (Cadastro Nacional de Obras) formatting characters and returns only digits. + * + * The CNO replaced the CEI for construction works and kept its 12 digit numbering, so the result + * is capped at the same length; a shorter value passes through as far as it goes. Use + * `isValidCno` to check the number itself. + * + * @param {string|number} value - The CNO value to be parsed. + * @returns {string} Up to 12 digits, or an empty string when there is no digit at all. + * + * @example + * ```typescript + * parseCno("11.113.01373/68"); // "111130137368" + * ``` + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/cno + * The registry's own page at the Receita Federal, which describes the cadastro but does not print + * the mask; the mask is the one the reference implementations cited by `isValidCno` agree on. + */ +export const parseCno = (value: string | number): string => + isNullish(value) ? "" : sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-cns/constants.ts b/src/parse-cns/constants.ts new file mode 100644 index 00000000..f2b4dcc9 --- /dev/null +++ b/src/parse-cns/constants.ts @@ -0,0 +1,2 @@ +/** Digits of a CNS (Cartão Nacional de Saúde) number, printed as "000 0000 0000 0000". */ +export const LENGTH = 15; diff --git a/src/parse-cns/parse-cns.test.ts b/src/parse-cns/parse-cns.test.ts new file mode 100644 index 00000000..61478897 --- /dev/null +++ b/src/parse-cns/parse-cns.test.ts @@ -0,0 +1,58 @@ +import { anyText, anyValue, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { formatCns } from "../format-cns/format-cns"; +import { parseCns } from "./parse-cns"; + +describe("parseCns", () => { + it("should remove CNS mask characters", () => { + expect(parseCns("123 4567 8901 0000")).toBe("123456789010000"); + }); + + it("should remove non numeric characters", () => { + expect(parseCns("123.?ABC4567 8901-0000abc")).toBe("123456789010000"); + }); + + it("should ignore digits after the CNS length", () => { + expect(parseCns("123456789010000999")).toBe("123456789010000"); + }); + + it("should read a number as the string of its digits", () => { + expect(parseCns(123_456_789_010_000)).toBe("123456789010000"); + }); + + it("should return an empty string for null", () => { + // @ts-expect-error not a string or number + expect(parseCns(null)).toBe(""); + }); + + describe("properties", () => { + test("should return at most the digits of a CNS", () => { + expectMatchesPattern(parseCns, /^\d{0,15}$/, anyText); + }); + + test("should undo formatCns", () => { + expectRoundTrip(formatCns, parseCns, digitsUpTo(15)); + }); + + test("should be idempotent", () => { + expectIdempotent(parseCns, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseCns, "string", anyValue); + }); + }); +}); + +describe("parseCns types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseCns).parameter(0).toEqualTypeOf(); + expectTypeOf(parseCns).returns.toEqualTypeOf(); + }); +}); diff --git a/src/parse-cns/parse-cns.ts b/src/parse-cns/parse-cns.ts new file mode 100644 index 00000000..3700d8f4 --- /dev/null +++ b/src/parse-cns/parse-cns.ts @@ -0,0 +1,27 @@ +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "./constants"; + +/** + * Removes CNS (Cartão Nacional de Saúde) formatting characters and returns only digits. + * + * The number the ANVISA and DATASUS routines check has 15 digits, the length the result is + * capped at; a shorter value passes through as far as it goes, so the parser can strip the mask + * off an input still being typed. Use `isValidCns` to check the number itself. + * + * @param {string|number} value - The CNS value to be parsed. + * @returns {string} Up to 15 digits, or an empty string when there is no digit at all. + * + * @example + * ```typescript + * parseCns("123 4567 8901 0000"); // "123456789010000" + * ``` + * + * @see Official: https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/ + * ANVISA's validation routines, which fix the 15 digit length. The page sits behind a bot filter + * and answers HTTP 403 to every non-browser client, so it has to be opened in a browser. + * @see Based on: https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html + * e-SUS APS documentation of the same DATASUS algorithm, reachable without a browser. + */ +export const parseCns = (value: string | number): string => + isNullish(value) ? "" : sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-iban/parse-iban.test.ts b/src/parse-iban/parse-iban.test.ts new file mode 100644 index 00000000..add0fcf9 --- /dev/null +++ b/src/parse-iban/parse-iban.test.ts @@ -0,0 +1,68 @@ +import * as fc from "fast-check"; + +import { BR_IBAN_LENGTH } from "../_internals/constants/iban"; +import { anyText, anyValue, asciiAlphanumericText } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectCaseInsensitive, + expectIdempotent, + expectMatchesPattern, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { formatIban } from "../format-iban/format-iban"; +import { parseIban } from "./parse-iban"; + +describe("parseIban", () => { + it("should remove the ISO 13616 print grouping", () => { + expect(parseIban("BR15 0000 0000 0000 1093 2840 814P 2")).toBe("BR1500000000000010932840814P2"); + }); + + it("should uppercase the letters and drop every other character", () => { + expect(parseIban("br15-0000.0000/0000 1093 2840 814p-2")).toBe("BR1500000000000010932840814P2"); + }); + + it(`should ignore characters after the Brazilian IBAN length (${BR_IBAN_LENGTH})`, () => { + expect(parseIban("BR1500000000000010932840814P2EXTRA")).toBe("BR1500000000000010932840814P2"); + }); + + it("should keep a partial IBAN as written", () => { + expect(parseIban("BR15")).toBe("BR15"); + }); + + it("should return an empty string for null", () => { + // @ts-expect-error not a string or number + expect(parseIban(null)).toBe(""); + }); + + describe("properties", () => { + const upToAnIban = fc.stringMatching(/^[0-9A-Z]{0,29}$/); + + test("should return at most the characters of a Brazilian IBAN", () => { + expectMatchesPattern(parseIban, /^[0-9A-Z]{0,29}$/, anyText); + }); + + test("should undo formatIban", () => { + expectRoundTrip(formatIban, parseIban, upToAnIban); + }); + + test("should be idempotent", () => { + expectIdempotent(parseIban, anyText); + }); + + test("should ignore the case of an ascii alphanumeric value", () => { + expectCaseInsensitive(parseIban, asciiAlphanumericText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseIban, "string", anyValue); + }); + }); +}); + +describe("parseIban types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseIban).parameter(0).toEqualTypeOf(); + expectTypeOf(parseIban).returns.toEqualTypeOf(); + }); +}); diff --git a/src/parse-iban/parse-iban.ts b/src/parse-iban/parse-iban.ts new file mode 100644 index 00000000..3043b023 --- /dev/null +++ b/src/parse-iban/parse-iban.ts @@ -0,0 +1,30 @@ +import { BR_IBAN_LENGTH } from "../_internals/constants/iban"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; + +/** + * Removes IBAN formatting characters, uppercases the result and returns the compact IBAN. + * + * An IBAN carries letters as well as digits (the country code, the account type and, from the + * tenth holder on, the owner indicator), so the value is read for its letters and digits rather + * than for its digits alone, exactly like `parsePassport` and `formatIban` do. The result is + * capped at the 29 characters of a Brazilian IBAN, the same cap `formatIban` applies, and a + * shorter value passes through as far as it goes. Use `isValidIban` to check the check digits and + * `getIbanInfo` to read the fields. + * + * @param {string|number} value - The IBAN to be parsed. + * @returns {string} Up to 29 uppercase alphanumeric characters, or an empty string when there is + * no letter or digit at all. + * + * @example + * ```typescript + * parseIban("BR15 0000 0000 0000 1093 2840 814P 2"); // "BR1500000000000010932840814P2" + * ``` + * + * @see Official: https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf + * Circular BCB nº 3.625/2013 + * @see Official: https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf + * Diretrizes de Implementação do IBAN no Brasil, which fix the 29 character Brazilian length. + */ +export const parseIban = (value: string | number): string => + isNullish(value) ? "" : sanitizeToAlphanumeric(value).slice(0, BR_IBAN_LENGTH); diff --git a/src/parse-ncm/constants.ts b/src/parse-ncm/constants.ts new file mode 100644 index 00000000..aab8be37 --- /dev/null +++ b/src/parse-ncm/constants.ts @@ -0,0 +1,2 @@ +/** Digits of a complete NCM code, printed as "0000.00.00". */ +export const LENGTH = 8; diff --git a/src/parse-ncm/parse-ncm.test.ts b/src/parse-ncm/parse-ncm.test.ts new file mode 100644 index 00000000..83e701d8 --- /dev/null +++ b/src/parse-ncm/parse-ncm.test.ts @@ -0,0 +1,62 @@ +import { anyText, anyValue, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { formatNcm } from "../format-ncm/format-ncm"; +import { parseNcm } from "./parse-ncm"; + +describe("parseNcm", () => { + it("should remove NCM mask characters", () => { + expect(parseNcm("8471.30.12")).toBe("84713012"); + }); + + it("should remove non numeric characters", () => { + expect(parseNcm("84?ABC71.30.12abc")).toBe("84713012"); + }); + + it("should ignore digits after the NCM length", () => { + expect(parseNcm("84713012999")).toBe("84713012"); + }); + + it("should read a number as the string of its digits", () => { + expect(parseNcm(84_713_012)).toBe("84713012"); + }); + + it("should keep a partial code as written, without padding it", () => { + expect(parseNcm("8471")).toBe("8471"); + }); + + it("should return an empty string for null", () => { + // @ts-expect-error not a string or number + expect(parseNcm(null)).toBe(""); + }); + + describe("properties", () => { + test("should return at most the digits of an NCM code", () => { + expectMatchesPattern(parseNcm, /^\d{0,8}$/, anyText); + }); + + test("should undo formatNcm", () => { + expectRoundTrip(formatNcm, parseNcm, digitsUpTo(8)); + }); + + test("should be idempotent", () => { + expectIdempotent(parseNcm, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseNcm, "string", anyValue); + }); + }); +}); + +describe("parseNcm types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseNcm).parameter(0).toEqualTypeOf(); + expectTypeOf(parseNcm).returns.toEqualTypeOf(); + }); +}); diff --git a/src/parse-ncm/parse-ncm.ts b/src/parse-ncm/parse-ncm.ts new file mode 100644 index 00000000..fa25d92b --- /dev/null +++ b/src/parse-ncm/parse-ncm.ts @@ -0,0 +1,24 @@ +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "./constants"; + +/** + * Removes NCM (Nomenclatura Comum do Mercosul) formatting characters and returns only digits. + * + * A complete code has 8 digits, which is the length the result is capped at; a shorter value (a + * position or a subposition, or a code still being typed) passes through as far as it goes and is + * never left padded, so the leading zeros a code carries have to be written out. Use `isValidNcm`, + * which does pad a bare numeric code, to check a code against the official table. + * + * @param {string|number} value - The NCM code to be parsed. + * @returns {string} Up to 8 digits, or an empty string when there is no digit at all. + * + * @example + * ```typescript + * parseNcm("8471.30.12"); // "84713012" + * ``` + * + * @see Official: https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json + */ +export const parseNcm = (value: string | number): string => + isNullish(value) ? "" : sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-nfe-key/parse-nfe-key.test.ts b/src/parse-nfe-key/parse-nfe-key.test.ts new file mode 100644 index 00000000..815fe45f --- /dev/null +++ b/src/parse-nfe-key/parse-nfe-key.test.ts @@ -0,0 +1,77 @@ +import { NFE_KEY_LENGTH } from "../_internals/constants/nfe-key"; +import { anyText, anyValue, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { formatNfeKey } from "../format-nfe-key/format-nfe-key"; +import { parseNfeKey } from "./parse-nfe-key"; + +const KEY = "35170458716523000119550010000000121000123458"; + +describe("parseNfeKey", () => { + it("should remove the printed grouping of an access key", () => { + expect(parseNfeKey("3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458")).toBe(KEY); + }); + + it("should remove non numeric characters", () => { + expect(parseNfeKey("3517.0458.7165.2300.0119.5500.1000.0000.1210.0012.3458")).toBe(KEY); + }); + + it("should strip the XML Id prefix of every document", () => { + expect(parseNfeKey(`NFe${KEY}`)).toBe(KEY); + expect(parseNfeKey(`CTe${KEY}`)).toBe(KEY); + expect(parseNfeKey(`MDFe${KEY}`)).toBe(KEY); + expect(parseNfeKey(`BPe${KEY}`)).toBe(KEY); + expect(parseNfeKey(`NFCom${KEY}`)).toBe(KEY); + }); + + it("should strip the NF3e prefix instead of reading its digit as part of the key", () => { + expect(parseNfeKey(`NF3e${KEY}`)).toBe(KEY); + }); + + it("should strip the XML Id prefix behind leading whitespace", () => { + expect(parseNfeKey(` NF3e${KEY}`)).toBe(KEY); + }); + + it(`should ignore digits after the access key length (${NFE_KEY_LENGTH})`, () => { + expect(parseNfeKey(`${KEY}999`)).toBe(KEY); + }); + + it("should keep a partial access key as written", () => { + expect(parseNfeKey("3517 0458")).toBe("35170458"); + }); + + it("should return an empty string for null", () => { + // @ts-expect-error not a string or number + expect(parseNfeKey(null)).toBe(""); + }); + + describe("properties", () => { + test("should return at most the digits of an access key", () => { + expectMatchesPattern(parseNfeKey, /^\d{0,44}$/, anyText); + }); + + test("should undo formatNfeKey", () => { + expectRoundTrip(formatNfeKey, parseNfeKey, digitsUpTo(NFE_KEY_LENGTH)); + }); + + test("should be idempotent", () => { + expectIdempotent(parseNfeKey, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseNfeKey, "string", anyValue); + }); + }); +}); + +describe("parseNfeKey types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseNfeKey).parameter(0).toEqualTypeOf(); + expectTypeOf(parseNfeKey).returns.toEqualTypeOf(); + }); +}); diff --git a/src/parse-nfe-key/parse-nfe-key.ts b/src/parse-nfe-key/parse-nfe-key.ts new file mode 100644 index 00000000..32576e5a --- /dev/null +++ b/src/parse-nfe-key/parse-nfe-key.ts @@ -0,0 +1,50 @@ +import { NFE_KEY_LENGTH, XML_ID_PREFIX_REGEX } from "../_internals/constants/nfe-key"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { toStringSafe } from "../_internals/to-string-safe/to-string-safe"; + +/** + * Drops the XML `Id` prefix a DF-e document writes in front of its access key, so the digit it + * may carry (the `3` of `NF3e`) is not read as part of the key. + * @param {string} value - The trimmed value to strip the prefix from. + * @returns {string} The value without its leading `NFe`/`CTe`/`MDFe`/`BPe`/`NF3e`/`NFCom` prefix. + */ +const stripXmlIdPrefix = (value: string): string => { + const [prefix = ""] = XML_ID_PREFIX_REGEX.exec(value) ?? []; + + return value.slice(prefix.length); +}; + +/** + * Removes the formatting of a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) and + * returns only digits. + * + * The `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes the `Id` attribute of the + * document's XML puts in front of the key are stripped before the digits are read, with any + * whitespace around them, the same way `isValidNfeKey` accepts them. The prefix has to go first + * because `NF3e` carries a digit of its own that is not part of the key. + * + * The result is capped at the 44 digits of an access key; a shorter value passes through as far + * as it goes, so the grouping of a key still being typed can be stripped with it. Use + * `isValidNfeKey` to check the key and `getNfeKeyInfo` to read its fields. + * + * @param {string|number} value - The access key value to be parsed. + * @returns {string} Up to 44 digits, or an empty string when there is no digit at all. + * + * @example + * ```typescript + * parseNfeKey("3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458"); + * // "35170458716523000119550010000000121000123458" + * + * parseNfeKey("NFe35170458716523000119550010000000121000123458"); + * // "35170458716523000119550010000000121000123458" + * ``` + * + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf + * Manual de Orientação do Contribuinte (MOC) NF-e, "chave de acesso", which fixes the 44 digits + * and the `Id` attribute the prefixes come from. + */ +export const parseNfeKey = (value: string | number): string => + isNullish(value) + ? "" + : sanitizeToDigits(stripXmlIdPrefix(toStringSafe(value).trim())).slice(0, NFE_KEY_LENGTH); From 6d3bd150d9474c21ac7f3a298ee81796e6e58883 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 10:49:29 -0300 Subject: [PATCH 62/75] refactor(format): drop the nullish guards the safe sanitizer made redundant and pin the rest Once the sanitizers read a value through `toStringSafe`, `null` and `undefined` reach the mask as `"null"` and `"undefined"`, which have no digit, so the leading `isNullish` guard of `formatVoterId`, `formatPhone`, `parseBoleto` and `parseVoterId`, and the `typeof` guard of `isValidBoleto`, `isValidPhone`, `getFormatLicensePlate` and the CEI/CNO checker, no longer changed any result and showed up as surviving mutants. They are removed; the guards that still matter (a formatter under `pad: true`, which would otherwise print a zero-filled document for `null`) keep a literal test for that case. --- .../is-valid-cei-cno-number/is-valid-cei-cno-number.ts | 5 ++--- src/format-boleto/format-boleto.test.ts | 9 +++++++++ src/format-caepf/format-caepf.test.ts | 9 +++++++++ src/format-cei/format-cei.test.ts | 9 +++++++++ src/format-cno/format-cno.test.ts | 9 +++++++++ src/format-cnpj/format-cnpj.test.ts | 9 +++++++++ src/format-cpf/format-cpf.test.ts | 9 +++++++++ src/format-phone/format-phone.ts | 3 --- src/format-voter-id/format-voter-id.ts | 3 --- .../get-format-license-plate.test.ts | 7 +++++++ src/get-format-license-plate/get-format-license-plate.ts | 2 -- src/is-valid-boleto/is-valid-boleto.test.ts | 7 +++++++ src/is-valid-boleto/is-valid-boleto.ts | 2 -- src/is-valid-passport/is-valid-passport.test.ts | 7 +++++++ src/is-valid-phone/is-valid-phone.test.ts | 7 +++++++ src/is-valid-phone/is-valid-phone.ts | 2 -- .../is-valid-service-phone.test.ts | 7 +++++++ src/parse-boleto/parse-boleto.ts | 3 --- src/parse-voter-id/parse-voter-id.ts | 3 --- 19 files changed, 91 insertions(+), 21 deletions(-) diff --git a/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.ts b/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.ts index b2e1c24d..e08c4458 100644 --- a/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.ts +++ b/src/_internals/is-valid-cei-cno-number/is-valid-cei-cno-number.ts @@ -2,6 +2,7 @@ import { calculateCeiCheckDigit } from "../calculate-cei-check-digit/calculate-c import { CEI_BASE_LENGTH, CEI_FORMAT_REGEX } from "../constants/cei"; import { isRepeatedDigits } from "../is-repeated-digits/is-repeated-digits"; import { sanitizeToDigits } from "../sanitize-to-digits/sanitize-to-digits"; +import { toStringSafe } from "../to-string-safe/to-string-safe"; /** * Validates a number that follows the CEI (Cadastro Específico do INSS) numbering, which the @@ -48,11 +49,9 @@ import { sanitizeToDigits } from "../sanitize-to-digits/sanitize-to-digits"; * Second, independent reference implementation agreeing with the first. */ export const isValidCeiCnoNumber = (value: string | number): boolean => { - if (typeof value !== "string" && typeof value !== "number") return false; - const digits = sanitizeToDigits(value); - if (!CEI_FORMAT_REGEX.test(String(value).trim())) return false; + if (!CEI_FORMAT_REGEX.test(toStringSafe(value).trim())) return false; if (isRepeatedDigits(digits)) return false; diff --git a/src/format-boleto/format-boleto.test.ts b/src/format-boleto/format-boleto.test.ts index 68925f64..78c4581f 100644 --- a/src/format-boleto/format-boleto.test.ts +++ b/src/format-boleto/format-boleto.test.ts @@ -213,6 +213,15 @@ describe("formatBoleto", () => { }); }); +describe("formatBoleto with a nullish value under pad", () => { + test("should return an empty string instead of a zero-filled document", () => { + // @ts-expect-error: intentionally invalid input + expect(formatBoleto(null, { pad: true })).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatBoleto(undefined, { pad: true })).toBe(""); + }); +}); + describe("formatBoleto types", () => { test("should take a string or number, optional options, and return a string", () => { expectTypeOf(formatBoleto).parameter(0).toEqualTypeOf(); diff --git a/src/format-caepf/format-caepf.test.ts b/src/format-caepf/format-caepf.test.ts index b1eb6bb5..d202767f 100644 --- a/src/format-caepf/format-caepf.test.ts +++ b/src/format-caepf/format-caepf.test.ts @@ -81,6 +81,15 @@ describe("formatCaepf", () => { }); }); +describe("formatCaepf with a nullish value under pad", () => { + test("should return an empty string instead of a zero-filled document", () => { + // @ts-expect-error: intentionally invalid input + expect(formatCaepf(null, { pad: true })).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCaepf(undefined, { pad: true })).toBe(""); + }); +}); + describe("formatCaepf types", () => { test("should take a string or number, optional options, and return a string", () => { expectTypeOf(formatCaepf).parameter(0).toEqualTypeOf(); diff --git a/src/format-cei/format-cei.test.ts b/src/format-cei/format-cei.test.ts index c11157f9..345a9265 100644 --- a/src/format-cei/format-cei.test.ts +++ b/src/format-cei/format-cei.test.ts @@ -77,6 +77,15 @@ describe("formatCei", () => { }); }); +describe("formatCei with a nullish value under pad", () => { + test("should return an empty string instead of a zero-filled document", () => { + // @ts-expect-error: intentionally invalid input + expect(formatCei(null, { pad: true })).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCei(undefined, { pad: true })).toBe(""); + }); +}); + describe("formatCei types", () => { test("should take a string or number, optional options, and return a string", () => { expectTypeOf(formatCei).parameter(0).toEqualTypeOf(); diff --git a/src/format-cno/format-cno.test.ts b/src/format-cno/format-cno.test.ts index 7de89270..255ae9c7 100644 --- a/src/format-cno/format-cno.test.ts +++ b/src/format-cno/format-cno.test.ts @@ -69,6 +69,15 @@ describe("formatCno", () => { }); }); +describe("formatCno with a nullish value under pad", () => { + test("should return an empty string instead of a zero-filled document", () => { + // @ts-expect-error: intentionally invalid input + expect(formatCno(null, { pad: true })).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCno(undefined, { pad: true })).toBe(""); + }); +}); + describe("formatCno types", () => { test("should take a string or number, optional options, and return a string", () => { expectTypeOf(formatCno).parameter(0).toEqualTypeOf(); diff --git a/src/format-cnpj/format-cnpj.test.ts b/src/format-cnpj/format-cnpj.test.ts index c36ab344..ba2f2afa 100644 --- a/src/format-cnpj/format-cnpj.test.ts +++ b/src/format-cnpj/format-cnpj.test.ts @@ -195,6 +195,15 @@ describe("formatCnpj", () => { }); }); +describe("formatCnpj with a nullish value under pad", () => { + test("should return an empty string instead of a zero-filled document", () => { + // @ts-expect-error: intentionally invalid input + expect(formatCnpj(null, { pad: true })).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCnpj(undefined, { pad: true })).toBe(""); + }); +}); + describe("formatCnpj types", () => { test("should take a string or number value and options and return a string", () => { expectTypeOf(formatCnpj).parameter(0).toEqualTypeOf(); diff --git a/src/format-cpf/format-cpf.test.ts b/src/format-cpf/format-cpf.test.ts index b1cba888..573950ef 100644 --- a/src/format-cpf/format-cpf.test.ts +++ b/src/format-cpf/format-cpf.test.ts @@ -139,6 +139,15 @@ describe("formatCpf", () => { }); }); +describe("formatCpf with a nullish value under pad", () => { + test("should return an empty string instead of a zero-filled document", () => { + // @ts-expect-error: intentionally invalid input + expect(formatCpf(null, { pad: true })).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatCpf(undefined, { pad: true })).toBe(""); + }); +}); + describe("formatCpf types", () => { test("should take a string or number value and options and return a string", () => { expectTypeOf(formatCpf).parameter(0).toEqualTypeOf(); diff --git a/src/format-phone/format-phone.ts b/src/format-phone/format-phone.ts index d30e6e1d..15b95f09 100644 --- a/src/format-phone/format-phone.ts +++ b/src/format-phone/format-phone.ts @@ -5,7 +5,6 @@ import { SERVICE_PHONE_NON_GEOGRAPHIC_PREFIXES, } from "../_internals/constants/service-phone"; import { format } from "../_internals/format/format"; -import { isNullish } from "../_internals/is-nullish/is-nullish"; import { normalizePhone } from "../_internals/normalize-phone/normalize-phone"; import { resolveServicePhoneDigits } from "../_internals/resolve-service-phone-digits/resolve-service-phone-digits"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; @@ -138,8 +137,6 @@ const isPhoneMask = (value: unknown): value is PhoneMask => PHONE_MASKS.has(valu * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749 */ export const formatPhone = (value: string | number, options?: FormatPhoneOptions): string => { - if (isNullish(value)) return ""; - const enhancedValue = sanitizeToDigits(value); const serviceDigits = resolveServicePhoneDigits(value); diff --git a/src/format-voter-id/format-voter-id.ts b/src/format-voter-id/format-voter-id.ts index 6e702c58..e6054adb 100644 --- a/src/format-voter-id/format-voter-id.ts +++ b/src/format-voter-id/format-voter-id.ts @@ -1,6 +1,5 @@ import { NINE_DIGIT_FEDERATIVE_UNION_CODES } from "../_internals/constants/voter-id"; import { format } from "../_internals/format/format"; -import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; const PATTERN = "0000 0000 00 00"; @@ -40,8 +39,6 @@ const LENGTH = 12; * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/voter_id.py */ export const formatVoterId = (value: string | number): string => { - if (isNullish(value)) return ""; - const digits = sanitizeToDigits(value); const federativeUnion = digits.slice(9, 11); const isExtended = diff --git a/src/get-format-license-plate/get-format-license-plate.test.ts b/src/get-format-license-plate/get-format-license-plate.test.ts index cf3f122b..da42ca91 100644 --- a/src/get-format-license-plate/get-format-license-plate.test.ts +++ b/src/get-format-license-plate/get-format-license-plate.test.ts @@ -46,6 +46,13 @@ describe("getFormatLicensePlate", () => { }); }); +describe("getFormatLicensePlate with an array of characters", () => { + test("should reject it instead of reading it as the joined string", () => { + // @ts-expect-error: intentionally invalid input + expect(getFormatLicensePlate(["A", "B", "C", "1", "D", "2", "3"])).toBeNull(); + }); +}); + describe("getFormatLicensePlate types", () => { test("should take a string and return a license plate format or null", () => { expectTypeOf(getFormatLicensePlate).parameter(0).toEqualTypeOf(); diff --git a/src/get-format-license-plate/get-format-license-plate.ts b/src/get-format-license-plate/get-format-license-plate.ts index 775736f8..e474e2c7 100644 --- a/src/get-format-license-plate/get-format-license-plate.ts +++ b/src/get-format-license-plate/get-format-license-plate.ts @@ -41,8 +41,6 @@ export type LicensePlateFormat = "LLLNNNN" | "LLLNLNN"; * @see Official: https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022anexos.pdf */ export const getFormatLicensePlate = (value: string): LicensePlateFormat | null => { - if (typeof value !== "string") return null; - if (sanitizeToAlphanumeric(value).length !== LENGTH) return null; const parsed = parseLicensePlate(value); diff --git a/src/is-valid-boleto/is-valid-boleto.test.ts b/src/is-valid-boleto/is-valid-boleto.test.ts index eac30538..df85e9a7 100644 --- a/src/is-valid-boleto/is-valid-boleto.test.ts +++ b/src/is-valid-boleto/is-valid-boleto.test.ts @@ -170,6 +170,13 @@ describe("isValidBoleto", () => { }); }); +describe("isValidBoleto with an array of characters", () => { + test("should reject it instead of reading it as the joined string", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidBoleto("34191790010104351004791020150008291070026000".match(/\d/g))).toBe(false); + }); +}); + describe("isValidBoleto types", () => { test("should take a string and return a boolean", () => { expectTypeOf(isValidBoleto).parameter(0).toEqualTypeOf(); diff --git a/src/is-valid-boleto/is-valid-boleto.ts b/src/is-valid-boleto/is-valid-boleto.ts index 1a2e02f5..5f016cfc 100644 --- a/src/is-valid-boleto/is-valid-boleto.ts +++ b/src/is-valid-boleto/is-valid-boleto.ts @@ -62,8 +62,6 @@ const isValidCheckDigit = (boleto: string): boolean => { * @see Official: https://portal.febraban.org.br/pagina/3425/33/pt-br/layout-febraban */ export const isValidBoleto = (value: string): boolean => { - if (typeof value !== "string") return false; - const digits = sanitizeToDigits(value); if (digits.startsWith(ARRECADACAO_PRODUCT) && parseArrecadacao(digits)) return true; diff --git a/src/is-valid-passport/is-valid-passport.test.ts b/src/is-valid-passport/is-valid-passport.test.ts index 7b8fb3a3..6385db45 100644 --- a/src/is-valid-passport/is-valid-passport.test.ts +++ b/src/is-valid-passport/is-valid-passport.test.ts @@ -80,6 +80,13 @@ describe("isValidPassport", () => { }); }); +describe("isValidPassport with an array of characters", () => { + test("should reject it instead of reading it as the joined string", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidPassport(["A", "B", "1", "2", "3", "4", "5", "6"])).toBe(false); + }); +}); + describe("isValidPassport types", () => { test("should take a string or number and return a boolean", () => { expectTypeOf(isValidPassport).parameter(0).toEqualTypeOf(); diff --git a/src/is-valid-phone/is-valid-phone.test.ts b/src/is-valid-phone/is-valid-phone.test.ts index d43a11ab..49b8051e 100644 --- a/src/is-valid-phone/is-valid-phone.test.ts +++ b/src/is-valid-phone/is-valid-phone.test.ts @@ -158,6 +158,13 @@ describe("isValidPhone", () => { }); }); +describe("isValidPhone with an array of characters", () => { + test("should reject it instead of reading it as the joined string", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidPhone("11987654321".match(/\d/g))).toBe(false); + }); +}); + describe("isValidPhone types", () => { test("should take a string, optional options, and return a boolean", () => { expectTypeOf(isValidPhone).parameter(0).toEqualTypeOf(); diff --git a/src/is-valid-phone/is-valid-phone.ts b/src/is-valid-phone/is-valid-phone.ts index a4c72321..363be4fe 100644 --- a/src/is-valid-phone/is-valid-phone.ts +++ b/src/is-valid-phone/is-valid-phone.ts @@ -59,8 +59,6 @@ export type IsValidPhoneOptions = { * @see Official: https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749 */ export const isValidPhone = (value: string, options?: IsValidPhoneOptions): boolean => { - if (typeof value !== "string") return false; - const requested = options?.accept; const accept: PhoneType[] = Array.isArray(requested) ? requested : DEFAULT_ACCEPT; diff --git a/src/is-valid-service-phone/is-valid-service-phone.test.ts b/src/is-valid-service-phone/is-valid-service-phone.test.ts index 100714bc..f5c2b7ef 100644 --- a/src/is-valid-service-phone/is-valid-service-phone.test.ts +++ b/src/is-valid-service-phone/is-valid-service-phone.test.ts @@ -150,6 +150,13 @@ describe("isValidServicePhone", () => { }); }); +describe("isValidServicePhone with an array of characters", () => { + test("should reject it instead of reading it as the joined string", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidServicePhone("08001234567".match(/\d/g))).toBe(false); + }); +}); + describe("isValidServicePhone types", () => { test("should take a string and return a boolean", () => { expectTypeOf(isValidServicePhone).parameter(0).toEqualTypeOf(); diff --git a/src/parse-boleto/parse-boleto.ts b/src/parse-boleto/parse-boleto.ts index e8b324b3..f9f25f4a 100644 --- a/src/parse-boleto/parse-boleto.ts +++ b/src/parse-boleto/parse-boleto.ts @@ -1,6 +1,5 @@ import { ARRECADACAO_LINE_LENGTH, ARRECADACAO_PRODUCT } from "../_internals/constants/arrecadacao"; import { BOLETO_LENGTH } from "../_internals/constants/boleto"; -import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; /** @@ -32,8 +31,6 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * @see Official: https://portal.febraban.org.br/pagina/3425/33/pt-br/layout-febraban */ export const parseBoleto = (value: string | number): string => { - if (isNullish(value)) return ""; - const digits = sanitizeToDigits(value); return digits.slice( diff --git a/src/parse-voter-id/parse-voter-id.ts b/src/parse-voter-id/parse-voter-id.ts index c812bd69..a0d1834a 100644 --- a/src/parse-voter-id/parse-voter-id.ts +++ b/src/parse-voter-id/parse-voter-id.ts @@ -1,5 +1,4 @@ import { NINE_DIGIT_FEDERATIVE_UNION_CODES } from "../_internals/constants/voter-id"; -import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { EXTENDED_LENGTH, LENGTH } from "./constants"; @@ -30,8 +29,6 @@ import { EXTENDED_LENGTH, LENGTH } from "./constants"; * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/voter_id.py */ export const parseVoterId = (value: string | number): string => { - if (isNullish(value)) return ""; - const digits = sanitizeToDigits(value); const federativeUnion = digits.slice(9, 11); From d8927659a09b18cba24a85e821cd913be9f767bf Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 11:08:07 -0300 Subject: [PATCH 63/75] refactor(caepf): read the value through the safe sanitizer instead of a type guard The `typeof` guard in front of `isValidCaepf` no longer changed any result once the sanitizer stopped throwing, and survived mutation testing; the format check now reads the value through `toStringSafe`, as the CEI/CNO checker does, and a value with no string conversion fails the mask. --- src/is-valid-caepf/is-valid-caepf.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/is-valid-caepf/is-valid-caepf.ts b/src/is-valid-caepf/is-valid-caepf.ts index ac8b4996..0dabe6c5 100644 --- a/src/is-valid-caepf/is-valid-caepf.ts +++ b/src/is-valid-caepf/is-valid-caepf.ts @@ -1,6 +1,7 @@ import { generateChecksum } from "../_internals/generate-checksum/generate-checksum"; import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { toStringSafe } from "../_internals/to-string-safe/to-string-safe"; import { CAEPF_BASE_LENGTH, CAEPF_CHECK_DIGITS_OFFSET, @@ -57,11 +58,9 @@ const getCheckDigit = (base: string, weights: number[]): number => * Third reference implementation. */ export const isValidCaepf = (value: string | number): boolean => { - if (typeof value !== "string" && typeof value !== "number") return false; - const digits = sanitizeToDigits(value); - if (!CAEPF_FORMAT_REGEX.test(String(value).trim())) return false; + if (!CAEPF_FORMAT_REGEX.test(toStringSafe(value).trim())) return false; const base = digits.slice(0, CAEPF_BASE_LENGTH); From d313bcc27a169453cabd41ccbbc2ea8bc71374bc Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:07:33 -0300 Subject: [PATCH 64/75] docs(municipalities): deprecate getCities and getMunicipality in favour of the municipality family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The municipality is the entity of the Constituição (art. 18) and of the IBGE dataset the library ships, so `getMunicipalities` and `getMunicipalityByCode` are the API. `getCities` (names only) and the asynchronous `getMunicipality` of 2.3.0 keep working unchanged and are marked deprecated, to be removed in the next major; matching a municipality by name, which `getMunicipality` also did, is left to the application over `getMunicipalities`, since names vary in ways no library rule settles (abbreviations, former names, hyphens, typos). --- docs/llms-full.txt | 5 +-- docs/pt-br/utilities.md | 5 +-- docs/utilities.md | 5 +-- .../normalize-municipality-name.test.ts | 41 +++++++++++++++++++ .../normalize-municipality-name.ts | 29 +++++++++++++ src/get-cities/get-cities.ts | 2 + src/get-municipality/get-municipality.ts | 20 +++++---- vite.config.ts | 11 ++++- 8 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 src/_internals/normalize-municipality-name/normalize-municipality-name.test.ts create mode 100644 src/_internals/normalize-municipality-name/normalize-municipality-name.ts diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 5d9d9975..3e303bbc 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -1303,7 +1303,7 @@ getTimezoneByState('ZZ'); // null ### getCities -Get Brazilian cities. Returns all cities if no state is provided, or cities from a specific state. Each call returns a fresh array, so mutating the result never affects subsequent calls. An unknown state code (or a non-`StateCode` value) returns an empty array instead of throwing, except for a falsy one: `getCities(null)` and `getCities('')` are read as "no state given" and return every city, where the stricter `getMunicipalities` returns `[]` for them. The state code is matched exactly, case included: `getCities('sp')` returns `[]` where `getCities('SP')` returns the 645 São Paulo cities. `getCities` and `getMunicipalities` are the only state-taking lookups that are case-sensitive; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` and `getMunicipality` all fold case. +Get Brazilian cities. **Deprecated:** use `getMunicipalities` instead. Returns all cities if no state is provided, or cities from a specific state. Each call returns a fresh array, so mutating the result never affects subsequent calls. An unknown state code (or a non-`StateCode` value) returns an empty array instead of throwing, except for a falsy one: `getCities(null)` and `getCities('')` are read as "no state given" and return every city, where the stricter `getMunicipalities` returns `[]` for them. The state code is matched exactly, case included: `getCities('sp')` returns `[]` where `getCities('SP')` returns the 645 São Paulo cities. `getCities` and `getMunicipalities` are the only state-taking lookups that are case-sensitive; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` and `getMunicipality` all fold case. ```javascript import { getCities } from '@brazilian-utils/brazilian-utils'; @@ -1698,7 +1698,7 @@ generatePis(); // '91077906857' ### getMunicipality -Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. A single function handles both directions, based on whether `options` has a `code` or a `municipalityName`/`uf`. `code` accepts both `string` and `number` input and must be exactly 7 digits, otherwise the function resolves to `null`. A `code` given as a number must be a non-negative integer: a sign and a decimal point are not digits, so `-3550308` and `355030.8` resolve to `null` instead of being read as `3550308`. Resolution is entirely offline, from a bundled IBGE dataset: no network request is made. The municipality name match ignores accents and casing, and every run of whitespace collapses into a single space, so `'sao paulo'` matches `'São Paulo'` while a name written without the space does not; the casing is folded to upper case, the direction Unicode expands `'ß'` to `'SS'` in, so `'Paßos'` matches `'Passos'`. An unknown municipality, an unknown UF or invalid input all resolve to `null`. The `[name, uf]` pair is a fresh array on every call, so mutating the result never affects subsequent lookups. +Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. **Deprecated:** use `getMunicipalityByCode` instead, which is synchronous and offline; matching a municipality by name is up to the application, over `getMunicipalities`. A single function handles both directions, based on whether `options` has a `code` or a `municipalityName`/`uf`. `code` accepts both `string` and `number` input and must be exactly 7 digits, otherwise the function resolves to `null`. A `code` given as a number must be a non-negative integer: a sign and a decimal point are not digits, so `-3550308` and `355030.8` resolve to `null` instead of being read as `3550308`. Resolution is entirely offline, from a bundled IBGE dataset: no network request is made. The municipality name match ignores accents and casing, and every run of whitespace collapses into a single space, so `'sao paulo'` matches `'São Paulo'` while a name written without the space does not; the casing is folded to upper case, the direction Unicode expands `'ß'` to `'SS'` in, so `'Paßos'` matches `'Passos'`. An unknown municipality, an unknown UF or invalid input all resolve to `null`. The `[name, uf]` pair is a fresh array on every call, so mutating the result never affects subsequent lookups. ```javascript import { getMunicipality } from '@brazilian-utils/brazilian-utils'; @@ -1792,7 +1792,6 @@ getMunicipalityByCode(3550308); getMunicipalityByCode('0000000'); // null (unknown code) getMunicipalityByCode('123'); // null (not 7 digits) ``` - ### isHoliday Check if a specific date is a Brazilian holiday. The check compares `targetDate`'s local calendar date (year/month/day as read locally), not its underlying UTC instant. Returns `false` when `targetDate` is missing or not a valid `Date`. An invalid `stateCode` is treated in two different ways: a string that is not a known state code is ignored and only national holidays are considered, the same as `getHolidays`, while a `stateCode` that is present and is not a string at all (a number, `null`, an object) is rejected and makes the call return `false` even for a national holiday. diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 93d70571..9eed0387 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -1050,7 +1050,7 @@ getTimezoneByState('ZZ'); // null ## getCities -Retorna as cidades brasileiras. Retorna todas as cidades se nenhum estado for fornecido, ou cidades de um estado específico. Cada chamada retorna um array novo, então alterar o resultado nunca afeta chamadas seguintes. Um código de estado desconhecido (ou um valor que não seja `StateCode`) retorna um array vazio em vez de lançar erro, exceto quando é um valor falsy: `getCities(null)` e `getCities('')` são lidos como "nenhum estado informado" e retornam todas as cidades, enquanto o mais estrito `getMunicipalities` retorna `[]` para eles. O código do estado é comparado exatamente, inclusive na caixa: `getCities('sp')` retorna `[]` enquanto `getCities('SP')` retorna as 645 cidades paulistas. `getCities` e `getMunicipalities` são as únicas buscas por estado sensíveis à caixa; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` e `getMunicipality` ignoram a caixa. +Retorna as cidades brasileiras. **Obsoleta:** use `getMunicipalities` no lugar. Retorna todas as cidades se nenhum estado for fornecido, ou cidades de um estado específico. Cada chamada retorna um array novo, então alterar o resultado nunca afeta chamadas seguintes. Um código de estado desconhecido (ou um valor que não seja `StateCode`) retorna um array vazio em vez de lançar erro, exceto quando é um valor falsy: `getCities(null)` e `getCities('')` são lidos como "nenhum estado informado" e retornam todas as cidades, enquanto o mais estrito `getMunicipalities` retorna `[]` para eles. O código do estado é comparado exatamente, inclusive na caixa: `getCities('sp')` retorna `[]` enquanto `getCities('SP')` retorna as 645 cidades paulistas. `getCities` e `getMunicipalities` são as únicas buscas por estado sensíveis à caixa; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` e `getMunicipality` ignoram a caixa. ```javascript import { getCities } from '@brazilian-utils/brazilian-utils'; @@ -1445,7 +1445,7 @@ generatePis(); // '91077906857' ## getMunicipality -Busca informações de município por código IBGE, ou obtém o código IBGE a partir do nome do município e UF. Uma única função cobre as duas direções, dependendo se `options` tem `code` ou `municipalityName`/`uf`. `code` aceita tanto `string` quanto `number` e deve ter exatamente 7 dígitos, caso contrário a função resolve para `null`. Um `code` informado como número precisa ser um inteiro não negativo: sinal e ponto decimal não são dígitos, então `-3550308` e `355030.8` resolvem para `null` em vez de serem lidos como `3550308`. A resolução é totalmente offline, a partir de um dataset do IBGE embutido na biblioteca: nenhuma requisição de rede é feita. A comparação do nome do município ignora acentos e diferenças entre maiúsculas/minúsculas, e toda sequência de espaços vira um único espaço, então `'sao paulo'` corresponde a `'São Paulo'`, enquanto um nome escrito sem o espaço não; a caixa é convertida para maiúsculas, a direção em que o Unicode expande `'ß'` para `'SS'`, então `'Paßos'` corresponde a `'Passos'`. Um município desconhecido, uma UF desconhecida ou uma entrada inválida resolvem para `null`. O par `[name, uf]` é um array novo a cada chamada, então alterar o resultado nunca afeta as buscas seguintes. +Busca informações de município por código IBGE, ou obtém o código IBGE a partir do nome do município e UF. **Obsoleta:** use `getMunicipalityByCode` no lugar, que é síncrona e offline; casar um município pelo nome fica a cargo da aplicação, sobre `getMunicipalities`. Uma única função cobre as duas direções, dependendo se `options` tem `code` ou `municipalityName`/`uf`. `code` aceita tanto `string` quanto `number` e deve ter exatamente 7 dígitos, caso contrário a função resolve para `null`. Um `code` informado como número precisa ser um inteiro não negativo: sinal e ponto decimal não são dígitos, então `-3550308` e `355030.8` resolvem para `null` em vez de serem lidos como `3550308`. A resolução é totalmente offline, a partir de um dataset do IBGE embutido na biblioteca: nenhuma requisição de rede é feita. A comparação do nome do município ignora acentos e diferenças entre maiúsculas/minúsculas, e toda sequência de espaços vira um único espaço, então `'sao paulo'` corresponde a `'São Paulo'`, enquanto um nome escrito sem o espaço não; a caixa é convertida para maiúsculas, a direção em que o Unicode expande `'ß'` para `'SS'`, então `'Paßos'` corresponde a `'Passos'`. Um município desconhecido, uma UF desconhecida ou uma entrada inválida resolvem para `null`. O par `[name, uf]` é um array novo a cada chamada, então alterar o resultado nunca afeta as buscas seguintes. ```javascript import { getMunicipality } from '@brazilian-utils/brazilian-utils'; @@ -1539,7 +1539,6 @@ getMunicipalityByCode(3550308); getMunicipalityByCode('0000000'); // null (código desconhecido) getMunicipalityByCode('123'); // null (não tem 7 dígitos) ``` - ## isHoliday Verifica se uma data específica é feriado brasileiro. A verificação compara a data local do `targetDate` (ano/mês/dia lidos localmente), não seu instante UTC subjacente. Retorna `false` quando `targetDate` está ausente ou não é um `Date` válido. Um `stateCode` inválido é tratado de duas formas diferentes: uma string que não é um código de estado conhecido é ignorada e só os feriados nacionais são considerados, igual ao `getHolidays`, enquanto um `stateCode` presente que não é uma string (um número, `null`, um objeto) é rejeitado e faz a chamada retornar `false` mesmo em um feriado nacional. diff --git a/docs/utilities.md b/docs/utilities.md index 46697427..bcf2a17b 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -1050,7 +1050,7 @@ getTimezoneByState('ZZ'); // null ## getCities -Get Brazilian cities. Returns all cities if no state is provided, or cities from a specific state. Each call returns a fresh array, so mutating the result never affects subsequent calls. An unknown state code (or a non-`StateCode` value) returns an empty array instead of throwing, except for a falsy one: `getCities(null)` and `getCities('')` are read as "no state given" and return every city, where the stricter `getMunicipalities` returns `[]` for them. The state code is matched exactly, case included: `getCities('sp')` returns `[]` where `getCities('SP')` returns the 645 São Paulo cities. `getCities` and `getMunicipalities` are the only state-taking lookups that are case-sensitive; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` and `getMunicipality` all fold case. +Get Brazilian cities. **Deprecated:** use `getMunicipalities` instead. Returns all cities if no state is provided, or cities from a specific state. Each call returns a fresh array, so mutating the result never affects subsequent calls. An unknown state code (or a non-`StateCode` value) returns an empty array instead of throwing, except for a falsy one: `getCities(null)` and `getCities('')` are read as "no state given" and return every city, where the stricter `getMunicipalities` returns `[]` for them. The state code is matched exactly, case included: `getCities('sp')` returns `[]` where `getCities('SP')` returns the 645 São Paulo cities. `getCities` and `getMunicipalities` are the only state-taking lookups that are case-sensitive; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` and `getMunicipality` all fold case. ```javascript import { getCities } from '@brazilian-utils/brazilian-utils'; @@ -1445,7 +1445,7 @@ generatePis(); // '91077906857' ## getMunicipality -Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. A single function handles both directions, based on whether `options` has a `code` or a `municipalityName`/`uf`. `code` accepts both `string` and `number` input and must be exactly 7 digits, otherwise the function resolves to `null`. A `code` given as a number must be a non-negative integer: a sign and a decimal point are not digits, so `-3550308` and `355030.8` resolve to `null` instead of being read as `3550308`. Resolution is entirely offline, from a bundled IBGE dataset: no network request is made. The municipality name match ignores accents and casing, and every run of whitespace collapses into a single space, so `'sao paulo'` matches `'São Paulo'` while a name written without the space does not; the casing is folded to upper case, the direction Unicode expands `'ß'` to `'SS'` in, so `'Paßos'` matches `'Passos'`. An unknown municipality, an unknown UF or invalid input all resolve to `null`. The `[name, uf]` pair is a fresh array on every call, so mutating the result never affects subsequent lookups. +Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. **Deprecated:** use `getMunicipalityByCode` instead, which is synchronous and offline; matching a municipality by name is up to the application, over `getMunicipalities`. A single function handles both directions, based on whether `options` has a `code` or a `municipalityName`/`uf`. `code` accepts both `string` and `number` input and must be exactly 7 digits, otherwise the function resolves to `null`. A `code` given as a number must be a non-negative integer: a sign and a decimal point are not digits, so `-3550308` and `355030.8` resolve to `null` instead of being read as `3550308`. Resolution is entirely offline, from a bundled IBGE dataset: no network request is made. The municipality name match ignores accents and casing, and every run of whitespace collapses into a single space, so `'sao paulo'` matches `'São Paulo'` while a name written without the space does not; the casing is folded to upper case, the direction Unicode expands `'ß'` to `'SS'` in, so `'Paßos'` matches `'Passos'`. An unknown municipality, an unknown UF or invalid input all resolve to `null`. The `[name, uf]` pair is a fresh array on every call, so mutating the result never affects subsequent lookups. ```javascript import { getMunicipality } from '@brazilian-utils/brazilian-utils'; @@ -1539,7 +1539,6 @@ getMunicipalityByCode(3550308); getMunicipalityByCode('0000000'); // null (unknown code) getMunicipalityByCode('123'); // null (not 7 digits) ``` - ## isHoliday Check if a specific date is a Brazilian holiday. The check compares `targetDate`'s local calendar date (year/month/day as read locally), not its underlying UTC instant. Returns `false` when `targetDate` is missing or not a valid `Date`. An invalid `stateCode` is treated in two different ways: a string that is not a known state code is ignored and only national holidays are considered, the same as `getHolidays`, while a `stateCode` that is present and is not a string at all (a number, `null`, an object) is rejected and makes the call return `false` even for a national holiday. diff --git a/src/_internals/normalize-municipality-name/normalize-municipality-name.test.ts b/src/_internals/normalize-municipality-name/normalize-municipality-name.test.ts new file mode 100644 index 00000000..7906066f --- /dev/null +++ b/src/_internals/normalize-municipality-name/normalize-municipality-name.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "../test/runtime"; +import { normalizeMunicipalityName } from "./normalize-municipality-name"; + +describe("normalizeMunicipalityName", () => { + it("should drop the accents of a name", () => { + expect(normalizeMunicipalityName("São Paulo")).toBe("SAO PAULO"); + expect(normalizeMunicipalityName("Ceará-Mirim")).toBe("CEARA-MIRIM"); + }); + + it("should fold the casing to upper case", () => { + expect(normalizeMunicipalityName("sao paulo")).toBe("SAO PAULO"); + }); + + it("should fold the casing in the direction that expands ß to SS", () => { + expect(normalizeMunicipalityName("Paßos")).toBe(normalizeMunicipalityName("Passos")); + }); + + it("should collapse every run of internal whitespace into a single space", () => { + expect(normalizeMunicipalityName("São Paulo")).toBe("SAO PAULO"); + expect(normalizeMunicipalityName("São\t\nPaulo")).toBe("SAO PAULO"); + }); + + it("should keep a name written without the space a separate name", () => { + expect(normalizeMunicipalityName("SaoPaulo")).toBe("SAOPAULO"); + }); + + it("should trim the surrounding whitespace", () => { + expect(normalizeMunicipalityName(" São Paulo ")).toBe("SAO PAULO"); + }); + + it("should return an empty string for an empty string", () => { + expect(normalizeMunicipalityName("")).toBe(""); + }); + + it("should return an empty string for a value that is not a string", () => { + // @ts-expect-error: intentionally invalid input + expect(normalizeMunicipalityName(null)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(normalizeMunicipalityName(3_550_308)).toBe(""); + }); +}); diff --git a/src/_internals/normalize-municipality-name/normalize-municipality-name.ts b/src/_internals/normalize-municipality-name/normalize-municipality-name.ts new file mode 100644 index 00000000..b46b101f --- /dev/null +++ b/src/_internals/normalize-municipality-name/normalize-municipality-name.ts @@ -0,0 +1,29 @@ +import { removeAccents } from "../../remove-accents/remove-accents"; + +const WHITESPACE_RUN_REGEX = /\s+/g; + +/** + * Normalizes a municipality name so that two spellings of the same municipality compare equal. + * + * Accents are dropped, every run of whitespace collapses into a single space, the surrounding + * whitespace is trimmed, and the casing is folded to upper case, the direction Unicode expands + * `"ß"` to `"SS"` in, so `"Paßos"` normalizes to what `"Passos"` normalizes to. Only the runs + * of whitespace that are there collapse, so a name written without a space the dataset carries + * stays a different name. + * + * `removeAccents` already folds a value that is not a string down to `""`, which no real + * municipality name normalizes to, so a caller may hand this helper an unvalidated value and + * simply compare the result. + * + * @param {string} value - The municipality name to normalize. + * @returns {string} The normalized name, or `""` when `value` is not a non-empty string. + * + * @example + * ```typescript + * normalizeMunicipalityName("São Paulo"); // "SAO PAULO" + * normalizeMunicipalityName(" Ceará-Mirim "); // "CEARA-MIRIM" + * normalizeMunicipalityName(""); // "" + * ``` + */ +export const normalizeMunicipalityName = (value: string): string => + removeAccents(value).replaceAll(WHITESPACE_RUN_REGEX, " ").trim().toUpperCase(); diff --git a/src/get-cities/get-cities.ts b/src/get-cities/get-cities.ts index 83448c1d..eb71c02a 100644 --- a/src/get-cities/get-cities.ts +++ b/src/get-cities/get-cities.ts @@ -22,6 +22,8 @@ let allCitiesCache: string[] | undefined; * the only state-taking lookups that are case-sensitive; `getStateNameByCode`, * `getTimezoneByState`, `getAreaCodesByState` and `getMunicipality` all fold case. * + * @deprecated Use `getMunicipalities` instead. + * * @param {StateCode} [state] - The code of the Brazilian state to filter cities by. Optional. * @returns {string[]} An array of city names, sorted alphabetically. Returns an empty array if the state is not found. * diff --git a/src/get-municipality/get-municipality.ts b/src/get-municipality/get-municipality.ts index fb1e3752..743f2211 100644 --- a/src/get-municipality/get-municipality.ts +++ b/src/get-municipality/get-municipality.ts @@ -1,8 +1,8 @@ import { DATA as CITIES_DATA } from "../_internals/constants/cities"; import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code"; import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { normalizeMunicipalityName } from "../_internals/normalize-municipality-name/normalize-municipality-name"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -import { removeAccents } from "../remove-accents/remove-accents"; /** The `getMunicipality` query by IBGE municipality code. */ export type GetMunicipalityByCodeOptions = { @@ -23,9 +23,6 @@ export type GetMunicipalityOptions = GetMunicipalityByCodeOptions | GetMunicipal let codeIndex: Map | undefined; -const normalizeName = (value: string): string => - removeAccents(value).replaceAll(/\s+/g, " ").trim().toUpperCase(); - const getMunicipalityByCode = (code: string | number): [string, string] | null => { if (!isLookupCode(code)) return null; @@ -62,11 +59,11 @@ const getMunicipalityCodeByName = ({ if (!stateEntry) return null; - // `removeAccents` (and so `normalizeName`) already folds a non-string or empty + // `removeAccents` (and so `normalizeMunicipalityName`) already folds a non-string or empty // `municipalityName` down to `""`, which no real municipality name normalizes to, so there is // no need to pre-validate `municipalityName` here first. - const normalizedName = normalizeName(municipalityName); - const match = stateEntry[1].find(([name]) => normalizeName(name) === normalizedName); + const normalizedName = normalizeMunicipalityName(municipalityName); + const match = stateEntry[1].find(([name]) => normalizeMunicipalityName(name) === normalizedName); return match ? match[1] : null; }; @@ -77,6 +74,9 @@ const getMunicipalityCodeByName = ({ * A `code` given as a number must be a non-negative integer: a sign and a decimal point are not * digits, so `-3550308` and `355030.8` are rejected instead of being read as `3550308`. * + * @deprecated Use `getMunicipalityByCode` instead, which is synchronous and offline; matching a + * municipality by name is up to the application, over `getMunicipalities`. + * * @param {GetMunicipalityByCodeOptions} options - The `{ code }` query. * @returns {Promise<[string, string] | null>} A fresh `[name, uf]` pair, which the caller owns * and may mutate, or null when the code is malformed or unknown. @@ -101,6 +101,9 @@ export function getMunicipality( * not, since only the runs that are there collapse. The casing is folded to upper case, the * direction Unicode expands `"ß"` to `"SS"` in, so `"Paßos"` matches `"Passos"`. * + * @deprecated Use `getMunicipalityByCode` instead, which is synchronous and offline; matching a + * municipality by name is up to the application, over `getMunicipalities`. + * * @param {GetMunicipalityByNameOptions} options - The `{ municipalityName, uf }` query. * @returns {Promise} The 7 digit IBGE code, or null when the state code or the * municipality is unknown. @@ -122,6 +125,9 @@ export function getMunicipality(options: GetMunicipalityByNameOptions): Promise< * `uf` it resolves the IBGE code. Validation failures and unknown municipalities are reported * as `null`. * + * @deprecated Use `getMunicipalityByCode` instead, which is synchronous and offline; matching a + * municipality by name is up to the application, over `getMunicipalities`. + * * @param {GetMunicipalityOptions} options - Either `{ code }` or `{ municipalityName, uf }`. * @returns {Promise<[string, string] | string | null>} The `[name, uf]` pair when looking up * by code, the IBGE code when looking up by name, or null when the municipality is unknown diff --git a/vite.config.ts b/vite.config.ts index 5bbb7365..027e6ace 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -451,8 +451,17 @@ export default defineConfig({ "vitest/warn-todo": "off", }, }, + // The barrel re-exports every deprecated alias, and a deprecated util's own tests (plus + // the `getMunicipalities` property that cross-checks it against `getCities`) have to + // keep calling it for as long as it is still supported. { - files: ["src/index.ts", "src/index.test.ts"], + files: [ + "src/index.ts", + "src/index.test.ts", + "src/get-cities/get-cities.test.ts", + "src/get-municipalities/get-municipalities.test.ts", + "src/get-municipality/get-municipality.test.ts", + ], rules: { "typescript/no-deprecated": "off", }, From a94107661296303942099753c00aef257f970dbc Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:41:42 -0300 Subject: [PATCH 65/75] test(business-days): keep the never-throw walk of differenceInBusinessDays inside a few years The property fed two dates anywhere between 1950 and 2050, and the function visits every day in between, so a single run could iterate tens of thousands of times; under Stryker's instrumented initial run on a busy runner that crossed the 5 second test timeout and failed the mutation job. The dates that are dates now come from 2020 to 2026; the garbage half of the arbitrary, which is what the property exists for, is unchanged. --- .../difference-in-business-days.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/difference-in-business-days/difference-in-business-days.test.ts b/src/difference-in-business-days/difference-in-business-days.test.ts index 428530e5..5669113d 100644 --- a/src/difference-in-business-days/difference-in-business-days.test.ts +++ b/src/difference-in-business-days/difference-in-business-days.test.ts @@ -1,7 +1,6 @@ import * as fc from "fast-check"; import { - anyBusinessDayDate, anyBusinessDayOptions, businessDayDates, PROTOTYPE_KEYS, @@ -169,9 +168,17 @@ describe("differenceInBusinessDays", () => { const amounts = fc.integer({ min: -100, max: 100 }); test("should never throw, regardless of the input, prototype chain state codes included", () => { + // The walk visits every day between the two dates, so the dates that are dates stay inside a + // few years: a pair a century apart is thousands of iterations per run, which is what the + // other properties already cover and what made this one time out under mutation testing. + const anyNearDate = fc.oneof( + fc.date({ min: new Date(2020, 0, 1), max: new Date(2026, 11, 31), noInvalidDate: true }), + fc.anything(), + ); + expectNeverThrowsWithArguments( differenceInBusinessDays, - fc.tuple(anyBusinessDayDate, anyBusinessDayDate, anyBusinessDayOptions), + fc.tuple(anyNearDate, anyNearDate, anyBusinessDayOptions), ); }); From 154644b2d8572d0316589138a4e730c541edbbac Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:46:34 -0300 Subject: [PATCH 66/75] feat(capitalize): break words at apostrophes and touching punctuation, keep name particles low MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `capitalize("santa bárbara d'oeste")` gave `"Santa Bárbara D'oeste"` and `capitalize("(empresa) ltda")` left `"(empresa)"` untouched, because only whitespace, `-` and `/` separated words. The apostrophe and the punctuation that touches a word (`( ) [ ] { } " “ ” : ; ,`) now start a new word too, kept where they are, so the results are `"Santa Bárbara d'Oeste"` and `"(Empresa) LTDA"`. The particles of foreign-origin names (`d`, `del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) join the default lower-case list, so `"luiz von schmidt"` is `"Luiz von Schmidt"`; an explicit `lowerCaseWords` still replaces the list as before. --- docs/llms-full.txt | 5 ++++- docs/pt-br/utilities.md | 5 ++++- docs/utilities.md | 5 ++++- src/capitalize/capitalize.test.ts | 20 ++++++++++++++++++++ src/capitalize/capitalize.ts | 17 ++++++++++++----- src/capitalize/constants.ts | 19 ++++++++++++++++++- 6 files changed, 62 insertions(+), 9 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 3e303bbc..df82f90d 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -1109,7 +1109,7 @@ isValidCreditCard(4111111111111111111); // false (above 2^53 - 1, pass it as a s ### capitalize -Transforms the first letter into a capital one of each word, the way a Brazilian name, company name or address is written, with no options needed. Words are separated by whitespace, by `-` and by `/`, so `'MOGI-GUAÇU'` becomes `'Mogi-Guaçu'`. Every run of whitespace (tabs, newlines, repeated spaces) collapses into a single space, and the leading and trailing whitespace is dropped. +Transforms the first letter into a capital one of each word, the way a Brazilian name, company name or address is written, with no options needed. Words are separated by whitespace, by `-` and `/`, by the apostrophe (`'d'oeste'` becomes `'d'Oeste'`) and by punctuation that touches a word (`'(empresa)'` becomes `'(Empresa)'`, `'bairro:centro'` becomes `'Bairro:Centro'`), so `'MOGI-GUAÇU'` becomes `'Mogi-Guaçu'`; the separators are kept where they are. Every run of whitespace (tabs, newlines, repeated spaces) collapses into a single space, and the leading and trailing whitespace is dropped. The particles of foreign-origin names (`d'`, `del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) stay lower case like the Portuguese prepositions. `options.lowerCaseWords` defaults to the Portuguese prepositions, articles and conjunctions that stay in lower case inside a proper name (`de`, `da`, `do`, `e`, ...), except when one of them is the first word. `options.upperCaseWords` defaults to the company designations and document abbreviations written in upper case in Brazilian usage (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) plus the roman numerals that appear in names and addresses (`II` through `XXIII`, except `VI`, which collides with the pt-BR verb form "vi"). `SA` without punctuation is deliberately absent, since it is indistinguishable from the surname "Sá" typed without its accent, while `ME` does match the pronoun "me" (`'diga-me'` becomes `'Diga-ME'`), so pass your own `upperCaseWords` when the input is free text rather than a name. `S/A` and `S/S` are matched across the slash even though a slash separates words. A two letter word that follows a `/` is upper-cased when it is the code of a Brazilian state (`'porto alegre/rs'` becomes `'Porto Alegre/RS'`); that rule is structural and stays on even when `upperCaseWords` is given, while a state code that does not follow a `/` is left alone. @@ -1124,6 +1124,9 @@ capitalize('empresa ltda'); // Empresa LTDA capitalize('banco do brasil s.a.'); // Banco do Brasil S.A. capitalize('casa de carnes s/a'); // Casa de Carnes S/A ("S/A" is matched across the slash) capitalize('mogi-guaçu'); // Mogi-Guaçu ("-" starts a new word) +capitalize("santa bárbara d'oeste"); // Santa Bárbara d'Oeste ("'" starts a new word, "d" stays lower case) +capitalize('(empresa) ltda'); // (Empresa) LTDA +capitalize('luiz von schmidt'); // Luiz von Schmidt capitalize('santana/rs'); // Santana/RS ("RS" is a state code right after a "/") capitalize('porto alegre/rs'); // Porto Alegre/RS capitalize('santana rs'); // Santana Rs (no "/", so "rs" is just a word) diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 9eed0387..5f512855 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -856,7 +856,7 @@ isValidCreditCard(4111111111111111111); // false (acima de 2^53 - 1, passe como ## capitalize -Transforma a primeira letra de cada palavra em maiúscula do jeito que se escreve um nome, uma razão social ou um endereço brasileiro, sem precisar de opções. As palavras são separadas por espaço em branco, por `-` e por `/`, então `'MOGI-GUAÇU'` vira `'Mogi-Guaçu'`. Toda sequência de espaços em branco (tabs, quebras de linha, espaços repetidos) vira um único espaço, e o espaço no início e no fim é descartado. +Transforma a primeira letra de cada palavra em maiúscula do jeito que se escreve um nome, uma razão social ou um endereço brasileiro, sem precisar de opções. As palavras são separadas por espaço em branco, por `-` e `/`, pelo apóstrofo (`'d'oeste'` vira `'d'Oeste'`) e pela pontuação colada à palavra (`'(empresa)'` vira `'(Empresa)'`, `'bairro:centro'` vira `'Bairro:Centro'`), então `'MOGI-GUAÇU'` vira `'Mogi-Guaçu'`; os separadores ficam onde estão. Toda sequência de espaços em branco (tabs, quebras de linha, espaços repetidos) vira um único espaço, e o espaço no início e no fim é descartado. As partículas de nomes de origem estrangeira (`d'`, `del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) ficam em minúsculas como as preposições do português. `options.lowerCaseWords` tem como padrão as preposições, artigos e conjunções que permanecem em minúsculas dentro de um nome próprio (`de`, `da`, `do`, `e`, ...), exceto quando uma delas é a primeira palavra. `options.upperCaseWords` tem como padrão as designações societárias e as abreviações de documentos escritas em maiúsculas no uso brasileiro (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) mais os algarismos romanos que aparecem em nomes e endereços (de `II` a `XXIII`, exceto `VI`, que colide com a forma verbal "vi"). `SA` sem pontuação ficou de fora de propósito, por ser indistinguível do sobrenome "Sá" digitado sem o acento, enquanto `ME` casa também com o pronome "me" (`'diga-me'` vira `'Diga-ME'`), então informe o seu próprio `upperCaseWords` quando a entrada for texto livre em vez de um nome. `S/A` e `S/S` são reconhecidos mesmo com a barra no meio, embora a barra separe palavras. Uma palavra de duas letras logo depois de uma `/` vira maiúscula quando é a sigla de um estado brasileiro (`'porto alegre/rs'` vira `'Porto Alegre/RS'`); essa regra é estrutural e continua valendo mesmo com `upperCaseWords` informado, enquanto uma sigla de estado que não venha depois de uma `/` é deixada como está. @@ -871,6 +871,9 @@ capitalize('empresa ltda'); // Empresa LTDA capitalize('banco do brasil s.a.'); // Banco do Brasil S.A. capitalize('casa de carnes s/a'); // Casa de Carnes S/A ("S/A" é reconhecido com a barra no meio) capitalize('mogi-guaçu'); // Mogi-Guaçu ("-" inicia uma nova palavra) +capitalize("santa bárbara d'oeste"); // Santa Bárbara d'Oeste ("'" inicia uma nova palavra, "d" fica minúsculo) +capitalize('(empresa) ltda'); // (Empresa) LTDA +capitalize('luiz von schmidt'); // Luiz von Schmidt capitalize('santana/rs'); // Santana/RS ("RS" é sigla de estado logo depois de uma "/") capitalize('porto alegre/rs'); // Porto Alegre/RS capitalize('santana rs'); // Santana Rs (sem "/", "rs" é só uma palavra) diff --git a/docs/utilities.md b/docs/utilities.md index bcf2a17b..461edfc5 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -856,7 +856,7 @@ isValidCreditCard(4111111111111111111); // false (above 2^53 - 1, pass it as a s ## capitalize -Transforms the first letter into a capital one of each word, the way a Brazilian name, company name or address is written, with no options needed. Words are separated by whitespace, by `-` and by `/`, so `'MOGI-GUAÇU'` becomes `'Mogi-Guaçu'`. Every run of whitespace (tabs, newlines, repeated spaces) collapses into a single space, and the leading and trailing whitespace is dropped. +Transforms the first letter into a capital one of each word, the way a Brazilian name, company name or address is written, with no options needed. Words are separated by whitespace, by `-` and `/`, by the apostrophe (`'d'oeste'` becomes `'d'Oeste'`) and by punctuation that touches a word (`'(empresa)'` becomes `'(Empresa)'`, `'bairro:centro'` becomes `'Bairro:Centro'`), so `'MOGI-GUAÇU'` becomes `'Mogi-Guaçu'`; the separators are kept where they are. Every run of whitespace (tabs, newlines, repeated spaces) collapses into a single space, and the leading and trailing whitespace is dropped. The particles of foreign-origin names (`d'`, `del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) stay lower case like the Portuguese prepositions. `options.lowerCaseWords` defaults to the Portuguese prepositions, articles and conjunctions that stay in lower case inside a proper name (`de`, `da`, `do`, `e`, ...), except when one of them is the first word. `options.upperCaseWords` defaults to the company designations and document abbreviations written in upper case in Brazilian usage (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) plus the roman numerals that appear in names and addresses (`II` through `XXIII`, except `VI`, which collides with the pt-BR verb form "vi"). `SA` without punctuation is deliberately absent, since it is indistinguishable from the surname "Sá" typed without its accent, while `ME` does match the pronoun "me" (`'diga-me'` becomes `'Diga-ME'`), so pass your own `upperCaseWords` when the input is free text rather than a name. `S/A` and `S/S` are matched across the slash even though a slash separates words. A two letter word that follows a `/` is upper-cased when it is the code of a Brazilian state (`'porto alegre/rs'` becomes `'Porto Alegre/RS'`); that rule is structural and stays on even when `upperCaseWords` is given, while a state code that does not follow a `/` is left alone. @@ -871,6 +871,9 @@ capitalize('empresa ltda'); // Empresa LTDA capitalize('banco do brasil s.a.'); // Banco do Brasil S.A. capitalize('casa de carnes s/a'); // Casa de Carnes S/A ("S/A" is matched across the slash) capitalize('mogi-guaçu'); // Mogi-Guaçu ("-" starts a new word) +capitalize("santa bárbara d'oeste"); // Santa Bárbara d'Oeste ("'" starts a new word, "d" stays lower case) +capitalize('(empresa) ltda'); // (Empresa) LTDA +capitalize('luiz von schmidt'); // Luiz von Schmidt capitalize('santana/rs'); // Santana/RS ("RS" is a state code right after a "/") capitalize('porto alegre/rs'); // Porto Alegre/RS capitalize('santana rs'); // Santana Rs (no "/", so "rs" is just a word) diff --git a/src/capitalize/capitalize.test.ts b/src/capitalize/capitalize.test.ts index fd03895b..80e34bf9 100644 --- a/src/capitalize/capitalize.test.ts +++ b/src/capitalize/capitalize.test.ts @@ -79,6 +79,26 @@ describe("capitalize", () => { expect(capitalize("são paulo/sp")).toBe("São Paulo/SP"); }); + test("when a word is bound by an apostrophe or by punctuation", () => { + expect(capitalize("santa bárbara d'oeste")).toBe("Santa Bárbara d'Oeste"); + expect(capitalize("SANTA BÁRBARA D'OESTE")).toBe("Santa Bárbara d'Oeste"); + expect(capitalize("joão d’ávila")).toBe("João d’Ávila"); + expect(capitalize("o'neill")).toBe("O'Neill"); + expect(capitalize("(empresa) ltda")).toBe("(Empresa) LTDA"); + expect(capitalize('"joão" silva')).toBe('"João" Silva'); + expect(capitalize("bairro:centro")).toBe("Bairro:Centro"); + expect(capitalize("rua b,número 10")).toBe("Rua B,Número 10"); + expect(capitalize("casa;lote [3]")).toBe("Casa;Lote [3]"); + }); + + test("when the name carries a foreign particle", () => { + expect(capitalize("luiz von schmidt")).toBe("Luiz von Schmidt"); + expect(capitalize("maria van der berg")).toBe("Maria van der Berg"); + expect(capitalize("são joão del rei")).toBe("São João del Rei"); + expect(capitalize("carlo di giovanni")).toBe("Carlo di Giovanni"); + expect(capitalize("von schmidt")).toBe("Von Schmidt"); + }); + test("when a word after a slash is not a state code, and when a state code has no slash before it", () => { expect(capitalize("santana/br")).toBe("Santana/Br"); expect(capitalize("santana/xingu")).toBe("Santana/Xingu"); diff --git a/src/capitalize/capitalize.ts b/src/capitalize/capitalize.ts index f0a7ef9e..3ae28744 100644 --- a/src/capitalize/capitalize.ts +++ b/src/capitalize/capitalize.ts @@ -1,5 +1,6 @@ import { PREPOSITIONS, + PUNCTUATION_REGEX, SEPARATOR_REGEX, STATE_CODES, UPPER_CASE_WORDS, @@ -31,10 +32,13 @@ const toWordSet = ( * written, with no configuration needed: `"jose da silva"` becomes `"Jose da Silva"`, * `"empresa ltda"` becomes `"Empresa LTDA"` and `"santana/rs"` becomes `"Santana/RS"`. * - * Words are separated by whitespace, by `-` and by `/`, so `"MOGI-GUAÇU"` becomes - * `"Mogi-Guaçu"`. Hyphens and slashes are kept where they are, while every run of whitespace - * (spaces, tabs, newlines) collapses into a single space and the leading and trailing whitespace - * is dropped. + * Words are separated by whitespace, by `-` and `/`, by the apostrophe (`"d'oeste"` becomes + * `"d'Oeste"`) and by punctuation that touches a word (`"(empresa)"` becomes `"(Empresa)"`, + * `"bairro:centro"` becomes `"Bairro:Centro"`), so `"MOGI-GUAÇU"` becomes `"Mogi-Guaçu"`. The + * separators are kept where they are, while every run of whitespace (spaces, tabs, newlines) + * collapses into a single space and the leading and trailing whitespace is dropped. The particles + * of foreign-origin names (`d'`, `del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) stay lower + * case like the Portuguese prepositions, so `"luiz von schmidt"` becomes `"Luiz von Schmidt"`. * * - Words listed in `lowerCaseWords` are converted to lower case, except for the first word. The * default list is the Portuguese prepositions, articles and conjunctions that stay in lower @@ -84,6 +88,9 @@ const toWordSet = ( * capitalize("JOSÉ DA SILVA"); // "José da Silva" * capitalize("empresa ltda"); // "Empresa LTDA" * capitalize("banco do brasil s.a."); // "Banco do Brasil S.A." + * capitalize("santa bárbara d'oeste"); // "Santa Bárbara d'Oeste" + * capitalize("(empresa) ltda"); // "(Empresa) LTDA" + * capitalize("luiz von schmidt"); // "Luiz von Schmidt" * capitalize("casa de carnes s/a"); // "Casa de Carnes S/A" * capitalize("MOGI-GUAÇU"); // "Mogi-Guaçu" * capitalize("santana/rs"); // "Santana/RS" @@ -118,7 +125,7 @@ export const capitalize = (value: string, options?: CapitalizeOptions): string = continue; } - if (token === "-" || token === "/") { + if (PUNCTUATION_REGEX.test(token)) { output.push(token); continue; } diff --git a/src/capitalize/constants.ts b/src/capitalize/constants.ts index 524a7533..32cc5faf 100644 --- a/src/capitalize/constants.ts +++ b/src/capitalize/constants.ts @@ -27,6 +27,13 @@ export const PREPOSITIONS = [ "de", "do", "dos", + "d", + "del", + "della", + "den", + "der", + "di", + "du", "e", "em", "na", @@ -36,6 +43,8 @@ export const PREPOSITIONS = [ "o", "por", "sem", + "van", + "von", ]; /** @@ -154,6 +163,14 @@ export const STATE_CODES: StateCode[] = [ "TO", ]; -export const SEPARATOR_REGEX = /(\s+|[-/])/; +/** + * Word boundaries: runs of whitespace, hyphen and slash (kept in place), the apostrophe of + * `d'Oeste`, and the punctuation that may wrap or follow a word without a space, so `(empresa)` + * and `bairro:centro` still capitalize the word after the mark. + */ +export const SEPARATOR_REGEX = /(\s+|[-/'’‘(){}[\]"“”:;,])/; + +/** A single separator token that is kept where it is, as opposed to a whitespace run. */ +export const PUNCTUATION_REGEX = /^[-/'’‘(){}[\]"“”:;,]$/; export const WHITESPACE_REGEX = /^\s+$/; From 110bc1260d34b5c8f6bd6d161c305dbcd8786b84 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:33:51 -0300 Subject: [PATCH 67/75] =?UTF-8?q?fix(processo-juridico):=20check=20the=20?= =?UTF-8?q?=C3=B3rg=C3=A3o=20and=20tribunal=20codes=20against=20Resolu?= =?UTF-8?q?=C3=A7=C3=A3o=20CNJ=2065/2008?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `isValidProcessoJuridico` accepted any 20 digit number whose check digits matched, so a number with a tribunal that does not exist (`…2020.8.99.0001`) passed. The J (órgão, art. 1º § 4º) and TR (tribunal, art. 1º § 5º) codes are now checked against the closed list of the resolution: 00 for the superior courts, 90 for the CJF and the CSJT, 01 to 06 for the TRFs (the 6ª Região per Resolução CNJ 477/2022 and Lei 14.226/2021), 01 to 24 for the TRTs, 01 to 27 for the TREs and the TJs, 01 to 12 for the CJMs and 13, 21 and 26 for the TJMs. The unidade de origem (OOOO) stays unchecked: each tribunal assigns its own and there is no central list. `generateProcessoJuridico` draws only J/TR pairs from that list. --- docs/llms-full.txt | 9 +- docs/llms.txt | 2 +- docs/pt-br/utilities.md | 9 +- docs/utilities.md | 9 +- src/_internals/constants/processo-juridico.ts | 65 +++++++++++ .../generate-processo-juridico.test.ts | 51 ++++++++- .../generate-processo-juridico.ts | 34 +++--- src/is-valid-processo-juridico/constants.ts | 3 + .../is-valid-processo-juridico.test.ts | 105 +++++++++++++++++- .../is-valid-processo-juridico.ts | 31 +++++- 10 files changed, 291 insertions(+), 27 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index df82f90d..0d2ad0a7 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -843,13 +843,15 @@ const addressFromNumber = await getAddressInfoByCep(1310100); ### isValidProcessoJuridico -Validate the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119). The CNJ mask separators (whitespace, `.` and `-`) are accepted between the `NNNNNNN-DD.AAAA.J.TR.OOOO` fields, but any other character, a letter in particular, makes the value invalid. +Validate the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119): the `NNNNNNN-DD.AAAA.J.TR.OOOO` layout, the `DD` check digits and the `J`/`TR` pair, which has to name an órgão and a tribunal Resolução CNJ nº 65/2008 created, so a number carrying a correct check digit but a court that does not exist is rejected. The closed lists come from art. 1º, § 4º and § 5º of the resolution, § 5º, III in the wording Resolução CNJ nº 477/2022 gave it to seat the TRF da 6ª Região. The unidade de origem (`OOOO`) is only read as four digits, since art. 1º, § 6º leaves its codification to each tribunal and publishes no central list. The CNJ mask separators (whitespace, `.` and `-`) are accepted between the fields, but any other character, a letter in particular, makes the value invalid. ```javascript import { isValidProcessoJuridico } from '@brazilian-utils/brazilian-utils'; isValidProcessoJuridico('00020802520125150049'); // true isValidProcessoJuridico('0002080-25.2012.5.15.0049'); // true (CNJ mask) +isValidProcessoJuridico('0000100-68.2008.4.06.0000'); // true (TRF da 6ª Região) +isValidProcessoJuridico('0000100-23.2008.8.28.0000'); // false (no 28th Tribunal de Justiça) isValidProcessoJuridico('ab00020802520125150049'); // false (letters are rejected) ``` @@ -1512,14 +1514,15 @@ const ceps = await getCepInfoByAddress({ ### generateProcessoJuridico -Generate a valid random processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119). `year` must be between the current year and 9999, `court` between 1 and 9; out-of-range values return `null`. Uses `Math.random()` internally, so it is not cryptographically secure. +Generate a valid random processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119). `year` must be between the current year and 9999, `court` between 1 and 9; out-of-range values return `null`. The órgão (`J`) and the tribunal (`TR`) are drawn from the closed lists of art. 1º, § 4º and § 5º, so the pair always names a court that exists: `court` picks the órgão and the `TR` is drawn among the tribunais that órgão has. The unidade de origem (`OOOO`) is drawn freely, since the resolution publishes no central list for it. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generateProcessoJuridico } from '@brazilian-utils/brazilian-utils'; -generateProcessoJuridico(); // '89478643020269670326' +generateProcessoJuridico(); // '89478645020266070326' generateProcessoJuridico({ year: 2026, court: 5 }); // string | null generateProcessoJuridico({ year: 10000 }); // null (year out of range) +generateProcessoJuridico({ court: 10 }); // null (no such órgão) ``` ### formatLegalNature diff --git a/docs/llms.txt b/docs/llms.txt index 92bd6409..27d6e9ca 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -39,7 +39,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [isValidLicensePlate](https://brazilian-utils.com.br/utilities.md#isvalidlicenseplate): Check if license plate is valid. - [isValidRenavam](https://brazilian-utils.com.br/utilities.md#isvalidrenavam): Check if RENAVAM (Registro Nacional de Veículos Automotores) is valid. - [isValidPis](https://brazilian-utils.com.br/utilities.md#isvalidpis): Check if PIS is valid. -- [isValidProcessoJuridico](https://brazilian-utils.com.br/utilities.md#isvalidprocessojuridico): Validate the processo jurídico number according to CNJ's definition. +- [isValidProcessoJuridico](https://brazilian-utils.com.br/utilities.md#isvalidprocessojuridico): Validate the processo jurídico number according to CNJ's definition: the `NNNNNNN-DD.AAAA.J.TR.OOOO` layout, the `DD` check digits and the `J`/`TR` pair, which has to name an órgão and a tribunal Resolução CNJ nº 65/2008 created, so a number carrying a correct check digit but a court that does not exist is rejected. - [isValidIe](https://brazilian-utils.com.br/utilities.md#isvalidie): Check if inscrição estadual (state registration) is valid. - [isValidBankAccount](https://brazilian-utils.com.br/utilities.md#isvalidbankaccount): Check if a Brazilian bank account is valid. - [isValidIban](https://brazilian-utils.com.br/utilities.md#isvalidiban): Check if a Brazilian IBAN (International Bank Account Number) is valid, per Bacen's Diretrizes de Implementação do IBAN no Brasil (Circular BCB nº 3.625/2013): `BR` + 2 ISO 7064 MOD 97-10 check digits + 8 digit ISPB + 5 digit branch + 10 digit account + 1 letter account type (any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 owner indicator (`1` for the first or only holder up to `9` for the ninth, then `A` to `Z` from the tenth, so `0` is rejected), 29 characters total. diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 5f512855..a5f89b47 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -590,13 +590,15 @@ const addressFromNumber = await getAddressInfoByCep(1310100); ## isValidProcessoJuridico -Valida o número do processo jurídico de acordo com definição do [CNJ](https://atos.cnj.jus.br/atos/detalhar/119). Os separadores da máscara do CNJ (espaços, `.` e `-`) são aceitos entre os campos `NNNNNNN-DD.AAAA.J.TR.OOOO`, mas qualquer outro caractere, uma letra em especial, invalida o valor. +Valida o número do processo jurídico de acordo com definição do [CNJ](https://atos.cnj.jus.br/atos/detalhar/119): o layout `NNNNNNN-DD.AAAA.J.TR.OOOO`, os dígitos verificadores `DD` e o par `J`/`TR`, que precisa nomear um órgão e um tribunal que a Resolução CNJ nº 65/2008 criou, de modo que um número com dígito verificador correto mas com um tribunal inexistente é rejeitado. As listas fechadas vêm do art. 1º, § 4º e § 5º da resolução, o § 5º, III na redação que a Resolução CNJ nº 477/2022 lhe deu para acomodar o TRF da 6ª Região. A unidade de origem (`OOOO`) é lida apenas como quatro dígitos, já que o art. 1º, § 6º deixa a codificação dela a cargo de cada tribunal e não publica lista central. Os separadores da máscara do CNJ (espaços, `.` e `-`) são aceitos entre os campos, mas qualquer outro caractere, uma letra em especial, invalida o valor. ```javascript import { isValidProcessoJuridico } from '@brazilian-utils/brazilian-utils'; isValidProcessoJuridico('00020802520125150049'); // true isValidProcessoJuridico('0002080-25.2012.5.15.0049'); // true (máscara do CNJ) +isValidProcessoJuridico('0000100-68.2008.4.06.0000'); // true (TRF da 6ª Região) +isValidProcessoJuridico('0000100-23.2008.8.28.0000'); // false (não existe 28º Tribunal de Justiça) isValidProcessoJuridico('ab00020802520125150049'); // false (letras são rejeitadas) ``` @@ -1259,14 +1261,15 @@ const ceps = await getCepInfoByAddress({ ## generateProcessoJuridico -Gera um número de processo jurídico válido de acordo com a definição do [CNJ](https://atos.cnj.jus.br/atos/detalhar/119). `year` deve estar entre o ano atual e 9999, `court` entre 1 e 9; valores fora do intervalo retornam `null`. Usa `Math.random()` internamente, então não é criptograficamente seguro. +Gera um número de processo jurídico válido de acordo com a definição do [CNJ](https://atos.cnj.jus.br/atos/detalhar/119). `year` deve estar entre o ano atual e 9999, `court` entre 1 e 9; valores fora do intervalo retornam `null`. O órgão (`J`) e o tribunal (`TR`) são sorteados das listas fechadas do art. 1º, § 4º e § 5º, então o par sempre nomeia um tribunal que existe: `court` escolhe o órgão e o `TR` é sorteado entre os tribunais que aquele órgão tem. A unidade de origem (`OOOO`) é sorteada livremente, já que a resolução não publica lista central para ela. Usa `Math.random()` internamente, então não é criptograficamente seguro. ```javascript import { generateProcessoJuridico } from '@brazilian-utils/brazilian-utils'; -generateProcessoJuridico(); // '89478643020269670326' +generateProcessoJuridico(); // '89478645020266070326' generateProcessoJuridico({ year: 2026, court: 5 }); // string | null generateProcessoJuridico({ year: 10000 }); // null (ano fora do intervalo) +generateProcessoJuridico({ court: 10 }); // null (órgão inexistente) ``` ## formatLegalNature diff --git a/docs/utilities.md b/docs/utilities.md index 461edfc5..4136a044 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -590,13 +590,15 @@ const addressFromNumber = await getAddressInfoByCep(1310100); ## isValidProcessoJuridico -Validate the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119). The CNJ mask separators (whitespace, `.` and `-`) are accepted between the `NNNNNNN-DD.AAAA.J.TR.OOOO` fields, but any other character, a letter in particular, makes the value invalid. +Validate the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119): the `NNNNNNN-DD.AAAA.J.TR.OOOO` layout, the `DD` check digits and the `J`/`TR` pair, which has to name an órgão and a tribunal Resolução CNJ nº 65/2008 created, so a number carrying a correct check digit but a court that does not exist is rejected. The closed lists come from art. 1º, § 4º and § 5º of the resolution, § 5º, III in the wording Resolução CNJ nº 477/2022 gave it to seat the TRF da 6ª Região. The unidade de origem (`OOOO`) is only read as four digits, since art. 1º, § 6º leaves its codification to each tribunal and publishes no central list. The CNJ mask separators (whitespace, `.` and `-`) are accepted between the fields, but any other character, a letter in particular, makes the value invalid. ```javascript import { isValidProcessoJuridico } from '@brazilian-utils/brazilian-utils'; isValidProcessoJuridico('00020802520125150049'); // true isValidProcessoJuridico('0002080-25.2012.5.15.0049'); // true (CNJ mask) +isValidProcessoJuridico('0000100-68.2008.4.06.0000'); // true (TRF da 6ª Região) +isValidProcessoJuridico('0000100-23.2008.8.28.0000'); // false (no 28th Tribunal de Justiça) isValidProcessoJuridico('ab00020802520125150049'); // false (letters are rejected) ``` @@ -1259,14 +1261,15 @@ const ceps = await getCepInfoByAddress({ ## generateProcessoJuridico -Generate a valid random processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119). `year` must be between the current year and 9999, `court` between 1 and 9; out-of-range values return `null`. Uses `Math.random()` internally, so it is not cryptographically secure. +Generate a valid random processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119). `year` must be between the current year and 9999, `court` between 1 and 9; out-of-range values return `null`. The órgão (`J`) and the tribunal (`TR`) are drawn from the closed lists of art. 1º, § 4º and § 5º, so the pair always names a court that exists: `court` picks the órgão and the `TR` is drawn among the tribunais that órgão has. The unidade de origem (`OOOO`) is drawn freely, since the resolution publishes no central list for it. Uses `Math.random()` internally, so it is not cryptographically secure. ```javascript import { generateProcessoJuridico } from '@brazilian-utils/brazilian-utils'; -generateProcessoJuridico(); // '89478643020269670326' +generateProcessoJuridico(); // '89478645020266070326' generateProcessoJuridico({ year: 2026, court: 5 }); // string | null generateProcessoJuridico({ year: 10000 }); // null (year out of range) +generateProcessoJuridico({ court: 10 }); // null (no such órgão) ``` ## formatLegalNature diff --git a/src/_internals/constants/processo-juridico.ts b/src/_internals/constants/processo-juridico.ts index e96ff847..57e5e72d 100644 --- a/src/_internals/constants/processo-juridico.ts +++ b/src/_internals/constants/processo-juridico.ts @@ -1,2 +1,67 @@ +/** + * Número Único de Processo (`NNNNNNN-DD.AAAA.J.TR.OOOO`) of Resolução CNJ nº 65/2008: the length + * of the digits only value and the closed list of tribunal codes (TR) each órgão code (J) accepts. + * + * `J` comes from art. 1º, § 4º, which names one segment per digit: Supremo Tribunal Federal `1`, + * Conselho Nacional de Justiça `2`, Superior Tribunal de Justiça `3`, Justiça Federal `4`, + * Justiça do Trabalho `5`, Justiça Eleitoral `6`, Justiça Militar da União `7`, Justiça dos + * Estados e do Distrito Federal e Territórios `8` and Justiça Militar Estadual `9`. + * + * `TR` comes from art. 1º, § 5º, whose incisos close the list segment by segment: `00` for the + * processes originating in the STF, the CNJ, the STJ, the TST, the TSE and the STM (inciso I); + * `90` for those originating in the Conselho da Justiça Federal and in the Conselho Superior da + * Justiça do Trabalho (inciso II); `01` to `06` for the Tribunais Regionais Federais (inciso III, + * in the wording Resolução CNJ nº 477/2022 gave it to seat the TRF da 6ª Região created by Lei nº + * 14.226/2021); `01` to `24` for the Tribunais Regionais do Trabalho (inciso IV); `01` to `27` + * for the Tribunais Regionais Eleitorais (inciso V); `01` to `12` for the Circunscrições + * Judiciárias Militares (inciso VI); `01` to `27` for the Tribunais de Justiça (inciso VII); and + * `13`, `21` and `26` for the Tribunais de Justiça Militar of Minas Gerais, Rio Grande do Sul and + * São Paulo (inciso VIII). + * + * The unidade de origem (`OOOO`) is left out on purpose: art. 1º, § 6º hands its codification to + * each tribunal, which only has to publish its own list on its website, so there is no central + * roll to check a code against. + * + * @see Official: https://atos.cnj.jus.br/atos/detalhar/119 + * Resolução CNJ nº 65, de 16 de dezembro de 2008, whose art. 1º, § 4º and § 5º carry the two + * lists above and whose Anexos I to VII print one example number per tribunal. + * @see Official: https://atos.cnj.jus.br/atos/detalhar/4781 + * Resolução CNJ nº 477, de 10 de outubro de 2022, art. 1º: "nos processos da Justiça Federal, os + * Tribunais Regionais Federais devem ser identificados no campo (TR) pelos números de 01 a 06, + * observadas as respectivas regiões". Its Anexo II prints `0000100-15.2008.406.0000` for the TRF + * da 6ª Região. + * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2019-2022/2021/lei/l14226.htm + * Lei nº 14.226, de 20 de outubro de 2021, art. 1º: "É criado o Tribunal Regional Federal da 6ª + * Região, com sede em Belo Horizonte e jurisdição no Estado de Minas Gerais", the court Resolução + * CNJ nº 477/2022 added to the TR range of the Justiça Federal. + */ + /** Digits of a processo jurídico number (`NNNNNNNDDAAAAJTROOOO`, Resolução CNJ nº 65/2008). */ export const PROCESSO_JURIDICO_LENGTH = 20; + +/** + * @param {number} first Lowest code of the range. + * @param {number} last Highest code of the range. + * @returns {number[]} Every code from `first` to `last`, both included. + */ +const range = (first: number, last: number): number[] => + Array.from({ length: last - first + 1 }, (_, index) => first + index); + +/** Superior court of a segment, which files its own processes under a zeroed `TR` (§ 5º, I). */ +const SUPERIOR_COURT = 0; + +/** Conselho da Justiça Federal and Conselho Superior da Justiça do Trabalho (§ 5º, II). */ +const COUNCIL = 90; + +/** Tribunal codes (`TR`) Resolução CNJ nº 65/2008 allows under each órgão code (`J`). */ +export const PROCESSO_JURIDICO_TRIBUNALS: ReadonlyMap = new Map([ + [1, [SUPERIOR_COURT]], + [2, [SUPERIOR_COURT]], + [3, [SUPERIOR_COURT]], + [4, [...range(1, 6), COUNCIL]], + [5, [SUPERIOR_COURT, ...range(1, 24), COUNCIL]], + [6, [SUPERIOR_COURT, ...range(1, 27)]], + [7, [SUPERIOR_COURT, ...range(1, 12)]], + [8, range(1, 27)], + [9, [13, 21, 26]], +]); diff --git a/src/generate-processo-juridico/generate-processo-juridico.test.ts b/src/generate-processo-juridico/generate-processo-juridico.test.ts index 9c1fa604..568208cd 100644 --- a/src/generate-processo-juridico/generate-processo-juridico.test.ts +++ b/src/generate-processo-juridico/generate-processo-juridico.test.ts @@ -1,6 +1,9 @@ import * as fc from "fast-check"; -import { PROCESSO_JURIDICO_LENGTH } from "../_internals/constants/processo-juridico"; +import { + PROCESSO_JURIDICO_LENGTH, + PROCESSO_JURIDICO_TRIBUNALS, +} from "../_internals/constants/processo-juridico"; import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { isValidProcessoJuridico } from "../is-valid-processo-juridico/is-valid-processo-juridico"; import { @@ -16,6 +19,13 @@ const expectValidGeneratedProcessoJuridico = (value: string | null) => { expect(isValidProcessoJuridico(value as string)).toBe(true); }; +const expectListedCourtAndTribunal = (value: string | null) => { + const court = Number((value as string).charAt(13)); + const tribunal = Number((value as string).slice(14, 16)); + + expect(PROCESSO_JURIDICO_TRIBUNALS.get(court)).toContain(tribunal); +}; + describe("generateProcessoJuridico", () => { it("should generate a valid processo juridico", () => { expectValidGeneratedProcessoJuridico(generateProcessoJuridico()); @@ -69,6 +79,34 @@ describe("generateProcessoJuridico", () => { expect(generateProcessoJuridico(42)).toBe(null); }); + it("should draw a tribunal the órgão really has for every court option", () => { + for (const court of PROCESSO_JURIDICO_TRIBUNALS.keys()) { + const value = generateProcessoJuridico({ court }); + + expectValidGeneratedProcessoJuridico(value); + expect((value as string).charAt(13)).toBe(String(court)); + expectListedCourtAndTribunal(value); + } + }); + + it("should zero the tribunal of a segment whose only listed code is the superior court", () => { + expect(generateProcessoJuridico({ court: 1 })?.slice(14, 16)).toBe("00"); + expect(generateProcessoJuridico({ court: 2 })?.slice(14, 16)).toBe("00"); + expect(generateProcessoJuridico({ court: 3 })?.slice(14, 16)).toBe("00"); + }); + + it("should pad a single digit tribunal to the two digits of the CNJ field", () => { + const originalRandom = Math.random; + + Math.random = () => 0; + + try { + expect(generateProcessoJuridico({ court: 4 })?.slice(14, 16)).toBe("01"); + } finally { + Math.random = originalRandom; + } + }); + it("should map a forced random value to the hand-computed default court", () => { const originalRandom = Math.random; @@ -119,6 +157,17 @@ describe("generateProcessoJuridico", () => { ); }); + test("should only ever produce a valid number whose órgão and tribunal pair is listed", () => { + fc.assert( + fc.property(fc.option(court, { nil: undefined }), (chosenCourt) => { + const value = generateProcessoJuridico({ court: chosenCourt }); + + expectValidGeneratedProcessoJuridico(value); + expectListedCourtAndTribunal(value); + }), + ); + }); + test("should return null for every court outside 1 to 9", () => { const invalidCourts = fc .integer({ min: -100, max: 100 }) diff --git a/src/generate-processo-juridico/generate-processo-juridico.ts b/src/generate-processo-juridico/generate-processo-juridico.ts index b79d0646..9ad28656 100644 --- a/src/generate-processo-juridico/generate-processo-juridico.ts +++ b/src/generate-processo-juridico/generate-processo-juridico.ts @@ -1,3 +1,4 @@ +import { PROCESSO_JURIDICO_TRIBUNALS } from "../_internals/constants/processo-juridico"; import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; import { isNullish } from "../_internals/is-nullish/is-nullish"; @@ -10,8 +11,12 @@ export type GenerateProcessoJuridicoOptions = { }; const MAX_YEAR = 9999; -const MIN_COURT = 1; -const MAX_COURT = 9; +const TRIBUNAL_LENGTH = 2; + +const COURTS = [...PROCESSO_JURIDICO_TRIBUNALS.keys()]; + +const pick = (items: readonly Item[]): Item => + items[Math.floor(Math.random() * items.length)]; const calculateCheckDigits = (base: string): string => { const checksum = 98n - ((BigInt(base) * 100n) % 97n); @@ -22,6 +27,13 @@ const calculateCheckDigits = (base: string): string => { * Generates a random valid Brazilian Processo Jurídico (court case) number, * following the `NNNNNNNDDAAAAJTROOOO` layout of Resolução CNJ nº 65/2008. * + * The órgão (`J`) and the tribunal (`TR`) are drawn from the closed lists of art. 1º, § 4º and + * § 5º of the resolution, so the pair always names a court that exists: `court` picks the órgão + * and the `TR` is then drawn among the tribunais that órgão has, which is why a `court` outside + * 1 to 9, the only value with no tribunal to draw from, returns `null` instead of a number. The + * unidade de origem (`OOOO`) is drawn freely, since art. 1º, § 6º leaves its codification to each + * tribunal and publishes no central list. + * * Uses `Math.random()` internally, so it is not cryptographically secure, do not use for security purposes. * * @param {GenerateProcessoJuridicoOptions} [options] - Optional generation options. @@ -36,9 +48,11 @@ const calculateCheckDigits = (base: string): string => { * generateProcessoJuridico(); // "00020803420265150049" * generateProcessoJuridico({ year: 2030, court: 5 }); // "12345679820305120049" * generateProcessoJuridico({ year: 10000 }); // null + * generateProcessoJuridico({ court: 10 }); // null (no such órgão) * ``` * - * Resolução CNJ nº 65/2008 defines this Número Único de Processo layout and its check digits. + * Resolução CNJ nº 65/2008 defines this Número Único de Processo layout and its check digits, and + * closes the list of órgão (`J`) and tribunal (`TR`) codes in art. 1º, § 4º and § 5º. * * @see Official: https://atos.cnj.jus.br/atos/detalhar/119 */ @@ -47,22 +61,16 @@ export const generateProcessoJuridico = ( ): string | null => { if (isNullish(options) || typeof options !== "object") return null; - const { year = new Date().getFullYear(), court = Math.floor(Math.random() * 9) + 1 } = options; + const { year = new Date().getFullYear(), court = pick(COURTS) } = options; const currentYear = new Date().getFullYear(); + const tribunals = PROCESSO_JURIDICO_TRIBUNALS.get(court); - if ( - !Number.isInteger(year) || - year < currentYear || - year > MAX_YEAR || - !Number.isInteger(court) || - court < MIN_COURT || - court > MAX_COURT - ) { + if (!Number.isInteger(year) || year < currentYear || year > MAX_YEAR || tribunals === undefined) { return null; } const sequencial = generateRandomNumber(7); - const tribunal = generateRandomNumber(2); + const tribunal = String(pick(tribunals)).padStart(TRIBUNAL_LENGTH, "0"); const foro = generateRandomNumber(4); const base = `${sequencial}${year}${court}${tribunal}${foro}`; const checkDigits = calculateCheckDigits(base); diff --git a/src/is-valid-processo-juridico/constants.ts b/src/is-valid-processo-juridico/constants.ts index 21d4482c..79bb55e5 100644 --- a/src/is-valid-processo-juridico/constants.ts +++ b/src/is-valid-processo-juridico/constants.ts @@ -2,3 +2,6 @@ export const CHECK_DIGIT_START_POSITION = 7; export const CHECK_DIGIT_LENGTH = 2; export const MOD_97_10_QUOTIENT = 97; export const MOD_97_10_SUM = 98; +export const COURT_POSITION = 13; +export const TRIBUNAL_START_POSITION = 14; +export const TRIBUNAL_LENGTH = 2; diff --git a/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts b/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts index 2b5a4941..78436ccf 100644 --- a/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts +++ b/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts @@ -1,6 +1,9 @@ import * as fc from "fast-check"; -import { PROCESSO_JURIDICO_LENGTH } from "../_internals/constants/processo-juridico"; +import { + PROCESSO_JURIDICO_LENGTH, + PROCESSO_JURIDICO_TRIBUNALS, +} from "../_internals/constants/processo-juridico"; import { anyValue, digitsOfOtherLength, maskSeparators } from "../_internals/test/arbitraries"; import { expectAlwaysReturnsType, expectRejected } from "../_internals/test/properties"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; @@ -47,6 +50,34 @@ describe("isValidProcessoJuridico", () => { test("when a mask separator falls outside the CNJ field boundaries", () => { expect(isValidProcessoJuridico("000208-0252012.5.15.0049")).toBe(false); }); + + test("when the órgão (J) is not one of the nine segments of art. 1º, § 4º", () => { + expect(isValidProcessoJuridico("0000100-69.2008.0.00.0000")).toBe(false); + }); + + test("when the Justiça Federal carries a region beyond the sixth (art. 1º, § 5º, III)", () => { + expect(isValidProcessoJuridico("0000100-41.2008.4.07.0000")).toBe(false); + }); + + test("when the Justiça Estadual carries a tribunal beyond the twenty seventh (art. 1º, § 5º, VII)", () => { + expect(isValidProcessoJuridico("0000100-23.2008.8.28.0000")).toBe(false); + }); + + test("when the Justiça Estadual zeroes the tribunal, which has no superior court of its own", () => { + expect(isValidProcessoJuridico("0000100-03.2008.8.00.0000")).toBe(false); + }); + + test("when the Justiça Militar Estadual names a state without a military court (art. 1º, § 5º, VIII)", () => { + expect(isValidProcessoJuridico("0000100-89.2008.9.01.0000")).toBe(false); + }); + + test("when a superior court carries a tribunal instead of the zeroed field (art. 1º, § 5º, I)", () => { + expect(isValidProcessoJuridico("0000100-58.2008.1.01.0000")).toBe(false); + }); + + test("when a segment without a council carries the council code (art. 1º, § 5º, II)", () => { + expect(isValidProcessoJuridico("0000100-14.2008.9.90.0000")).toBe(false); + }); }); describe("should return true", () => { @@ -66,6 +97,59 @@ describe("isValidProcessoJuridico", () => { test("when is a processo juridico valid with the legacy fused mask", () => { expect(isValidProcessoJuridico("0002080-25.2012.515.0049")).toBe(true); }); + + test("when the órgão is the Supremo Tribunal Federal (art. 1º, § 4º, I)", () => { + expect(isValidProcessoJuridico("0000100-85.2008.1.00.0000")).toBe(true); + }); + + test("when the órgão is the Conselho Nacional de Justiça (art. 1º, § 4º, II)", () => { + expect(isValidProcessoJuridico("0000100-04.2008.2.00.0000")).toBe(true); + }); + + test("when the órgão is the Superior Tribunal de Justiça (art. 1º, § 4º, III)", () => { + expect(isValidProcessoJuridico("0000100-20.2008.3.00.0000")).toBe(true); + }); + + test("when the órgão is the Justiça Federal and the tribunal a TRF (art. 1º, § 5º, III)", () => { + expect(isValidProcessoJuridico("0000100-09.2008.4.01.0000")).toBe(true); + }); + + test("when the órgão is the Justiça do Trabalho and the tribunal a TRT (art. 1º, § 5º, IV)", () => { + expect(isValidProcessoJuridico("0000100-35.2008.5.15.0000")).toBe(true); + }); + + test("when the órgão is the Justiça Eleitoral and the tribunal a TRE (art. 1º, § 5º, V)", () => { + expect(isValidProcessoJuridico("0000100-18.2008.6.27.0000")).toBe(true); + }); + + test("when the órgão is the Justiça Militar da União and the tribunal a CJM (art. 1º, § 5º, VI)", () => { + expect(isValidProcessoJuridico("0000100-51.2008.7.12.0000")).toBe(true); + }); + + test("when the órgão is the Justiça Estadual and the tribunal a TJ (art. 1º, § 5º, VII)", () => { + expect(isValidProcessoJuridico("0000100-73.2008.8.01.0000")).toBe(true); + }); + + test("when the órgão is the Justiça Militar Estadual and the tribunal a TJM (art. 1º, § 5º, VIII)", () => { + expect(isValidProcessoJuridico("0000100-56.2008.9.13.0000")).toBe(true); + expect(isValidProcessoJuridico("0000100-34.2008.9.21.0000")).toBe(true); + expect(isValidProcessoJuridico("0000100-93.2008.9.26.0000")).toBe(true); + }); + + test("when the Justiça Federal names the TRF da 6ª Região, added by Resolução CNJ nº 477/2022", () => { + expect(isValidProcessoJuridico("0000100-68.2008.4.06.0000")).toBe(true); + }); + + test("when the number originates in the CJF or in the CSJT, whose tribunal is 90 (art. 1º, § 5º, II)", () => { + expect(isValidProcessoJuridico("0000100-31.2008.4.90.0000")).toBe(true); + expect(isValidProcessoJuridico("0000100-47.2008.5.90.0000")).toBe(true); + }); + + test("when the TST, the TSE or the STM zeroes the tribunal (art. 1º, § 5º, I)", () => { + expect(isValidProcessoJuridico("0000100-52.2008.5.00.0000")).toBe(true); + expect(isValidProcessoJuridico("0000100-68.2008.6.00.0000")).toBe(true); + expect(isValidProcessoJuridico("0000100-84.2008.7.00.0000")).toBe(true); + }); }); describe("properties", () => { @@ -87,6 +171,25 @@ describe("isValidProcessoJuridico", () => { expectRejected(isValidProcessoJuridico, digitsOfOtherLength(30, [PROCESSO_JURIDICO_LENGTH])); }); + test("should reject every tribunal the órgão of the value does not have", () => { + const courts = [...PROCESSO_JURIDICO_TRIBUNALS.keys()]; + + fc.assert( + fc.property( + fc.constantFrom(...courts), + fc.integer({ min: 0, max: 99 }), + (court, tribunal) => { + fc.pre(!(PROCESSO_JURIDICO_TRIBUNALS.get(court) as number[]).includes(tribunal)); + + const base = `00001002008${court}${String(tribunal).padStart(2, "0")}0000`; + const checkDigits = (98n - ((BigInt(base) * 100n) % 97n)).toString().padStart(2, "0"); + + expect(isValidProcessoJuridico(`0000100${checkDigits}${base.slice(7)}`)).toBe(false); + }, + ), + ); + }); + test("should never throw and always return a boolean", () => { expectAlwaysReturnsType(isValidProcessoJuridico, "boolean", anyValue); }); diff --git a/src/is-valid-processo-juridico/is-valid-processo-juridico.ts b/src/is-valid-processo-juridico/is-valid-processo-juridico.ts index c4d87a98..fb470dfd 100644 --- a/src/is-valid-processo-juridico/is-valid-processo-juridico.ts +++ b/src/is-valid-processo-juridico/is-valid-processo-juridico.ts @@ -1,8 +1,12 @@ +import { PROCESSO_JURIDICO_TRIBUNALS } from "../_internals/constants/processo-juridico"; import { CHECK_DIGIT_LENGTH, CHECK_DIGIT_START_POSITION, + COURT_POSITION, MOD_97_10_QUOTIENT, MOD_97_10_SUM, + TRIBUNAL_LENGTH, + TRIBUNAL_START_POSITION, } from "./constants"; const SEPARATORS_REGEX = /[\s.-]/g; @@ -38,9 +42,26 @@ const verifyCheckDigit = (value: string): boolean => { return verifier === verificationDigits; }; +const verifyCourtAndTribunal = (value: string): boolean => { + const tribunals = PROCESSO_JURIDICO_TRIBUNALS.get(Number(value.charAt(COURT_POSITION))); + + if (tribunals === undefined) return false; + + return tribunals.includes( + Number(value.slice(TRIBUNAL_START_POSITION, TRIBUNAL_START_POSITION + TRIBUNAL_LENGTH)), + ); +}; + /** * Validates a Brazilian Processo Jurídico (court case) number. * + * Three things are checked: the `NNNNNNN-DD.AAAA.J.TR.OOOO` layout, the `DD` check digits (ISO + * 7064 MOD 97-10) and the `J` and `TR` pair, which has to name an órgão and a tribunal Resolução + * CNJ nº 65/2008 actually created, so a number carrying a correct check digit but a court that + * does not exist, `0000100-23.2008.8.28.0000`, is rejected. The unidade de origem (`OOOO`) is + * only read as four digits: art. 1º, § 6º leaves its codification to each tribunal, so there is + * no central list to check it against. + * * The CNJ mask separators (whitespace, `.` and `-`) are accepted between the * `NNNNNNN-DD.AAAA.J.TR.OOOO` fields, and whitespace around the value is ignored, but any other * character, a letter in particular, makes the value invalid. @@ -53,10 +74,12 @@ const verifyCheckDigit = (value: string): boolean => { * isValidProcessoJuridico("00020802520125150049"); // true * isValidProcessoJuridico("0002080-25.2012.5.15.0049"); // true * isValidProcessoJuridico(" 0002080-25.2012.5.15.0049 "); // true (surrounding whitespace) + * isValidProcessoJuridico("0000100-23.2008.8.28.0000"); // false (there is no 28th Tribunal de Justiça) * isValidProcessoJuridico("ab00020802520125150049"); // false (invalid format) * ``` * - * Resolução CNJ nº 65/2008 defines this Número Único de Processo layout and its check digits. + * Resolução CNJ nº 65/2008 defines this Número Único de Processo layout and its check digits, and + * closes the list of órgão (`J`) and tribunal (`TR`) codes in art. 1º, § 4º and § 5º. * * @see Official: https://atos.cnj.jus.br/atos/detalhar/119 */ @@ -65,5 +88,9 @@ export const isValidProcessoJuridico = (value: string): boolean => { if (!FORMAT_REGEX.test(value.trim())) return false; - return verifyCheckDigit(value.replace(SEPARATORS_REGEX, "")); + const digits = value.replace(SEPARATORS_REGEX, ""); + + if (!verifyCheckDigit(digits)) return false; + + return verifyCourtAndTribunal(digits); }; From 7565d3de2e571d1c986d0e0eaf26f90bf0eb0d8d Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:09:15 -0300 Subject: [PATCH 68/75] fix(capitalize): keep the English possessive, single-letter designators and a pronoun me low MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The apostrophe boundary added in the previous commit turned `"bob's"` into `"Bob'S"`, and the comma boundary plus the bare `d` particle turned `"rua a, 100"` into `"Rua a, 100"` and `"rua d"` into `"Rua d"`. A single letter after an apostrophe is now the possessive and stays lower case; a word of the lower-case list is only lowered when it links two words, so a trailing or punctuation-bound `a`, `d`, `e` or `o` keeps its capital (`"Rua A, 100"`, `"Quadra D"`); the `d` particle is lowered only before an apostrophe and a word (`"d'Oeste"`); and `ME` is written as the microempresa designation only as the last word or before another designation, so `"não-me-toque"` is `"Não-Me-Toque"` and `"fulano comércio me"` is `"Fulano Comércio ME"`. --- docs/llms-full.txt | 28 ++++--- docs/llms.txt | 4 +- docs/pt-br/utilities.md | 8 +- docs/utilities.md | 8 +- src/capitalize/capitalize.test.ts | 37 +++++++++ src/capitalize/capitalize.ts | 131 +++++++++++++++++++++++++++--- src/capitalize/constants.ts | 41 +++++++++- 7 files changed, 226 insertions(+), 31 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 0d2ad0a7..f5256498 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -157,7 +157,7 @@ Brazilian Utils is a library focused on solving problems that we face daily in t ### Why Brazilian Utils - **Zero runtime dependencies.** Nothing else lands in your `node_modules` or in your bundle. -- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs about 1.2 KB minified (0.6 KB gzipped); every util is also its own subpath entry (`@brazilian-utils/brazilian-utils/get-cities`) for the heavy ones. +- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs about 1.4 KB minified (0.8 KB gzipped); every util is also its own subpath entry (`@brazilian-utils/brazilian-utils/get-cities`) for the heavy ones. - **Runs everywhere.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno and evergreen browsers, tested in CI on every one of them. - **Written in TypeScript.** Types ship with the package; the public API is tracked by an API report so nothing changes silently. - **Validated against the official rules.** Every validator cites the specification, law or dataset it implements (`@see` in the docs), and the test suite is mutation-tested, not just covered. @@ -215,19 +215,19 @@ You can check a list of utilities [by clicking here](utilities.md). ### Bundle size -The package is tree-shakeable: importing one util from the root pulls in only that util's code, not the rest of the library. `isValidCpf`, for example, adds roughly 1.2 KB minified (0.6 KB gzipped) to your bundle. A bundler that supports tree-shaking (webpack, Rollup, esbuild, Vite, etc.) drops every other util. +The package is tree-shakeable: importing one util from the root pulls in only that util's code, not the rest of the library. `isValidCpf`, for example, adds roughly 1.4 KB minified (0.8 KB gzipped) to your bundle. A bundler that supports tree-shaking (webpack, Rollup, esbuild, Vite, etc.) drops every other util. A handful of utils are the exception: each embeds an official dataset, so it weighs far more than every other util combined. These are their single-import sizes, minified and gzipped: | Util | Dataset | Minified | Gzipped | | --- | --- | --- | --- | -| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 IBGE municipalities, with names and codes | 156.2 KB | 50.2 KB | -| `getCities` | 5571 IBGE municipality names | 154.0 KB | 49.7 KB | -| `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 113.8 KB | 24.3 KB | -| `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 118.8 KB | 30.4 KB | -| `isValidCnae` · `getCnae` | CNAE-Subclasses 2.3 | 94.0 KB | 21.3 KB | -| `isValidCfop` · `getCfop` | CFOP operation descriptions | 68.7 KB | 6.8 KB | -| `getBanks` · `getBankByCode` | Banco Central STR participants (COMPE + ISPB) | 38.3 KB | 9.6 KB | +| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 IBGE municipalities, with names and codes | 154.9 - 156.5 KB | 50.3 - 50.4 KB | +| `getCities` | 5571 IBGE municipality names | 154.2 KB | 49.8 KB | +| `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 114.1 KB | 24.6 KB | +| `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 119.1 KB | 30.6 KB | +| `isValidCnae` · `getCnae` | CNAE-Subclasses 2.3 | 93.9 KB | 21.2 KB | +| `isValidCfop` · `getCfop` | CFOP operation descriptions | 68.9 KB | 6.9 KB | +| `getBanks` · `getBankByCode` | Banco Central STR participants (COMPE + ISPB) | 38.3 - 38.6 KB | 9.5 - 9.7 KB | Importing any of them from the root, even alongside a single small util, pulls that whole dataset into your main bundle, because this package ships as a single ESM module: a dynamic `import()` of the root (`await import('@brazilian-utils/brazilian-utils')`) still resolves to that same one file, so it can't be split out on its own. A bundler doing code-splitting needs a separate module to split *into*. @@ -249,7 +249,7 @@ getMunicipalityByCode('3550308'); Every util is available this way, as `@brazilian-utils/brazilian-utils/` (kebab-case, matching the function name: `isValidCpf` → `is-valid-cpf`), for the same lazy-loading/code-splitting reason. -Pick one style per util in a given app: a bundler treats the root import and the subpath import as two unrelated modules, so importing `getCities` from both the root *and* `/get-cities` in the same app bundles the 154.0 KB city table twice, once in each module's own output. +Pick one style per util in a given app: a bundler treats the root import and the subpath import as two unrelated modules, so importing `getCities` from both the root *and* `/get-cities` in the same app bundles the 154.2 KB city table twice, once in each module's own output. ## Utilities @@ -1111,9 +1111,9 @@ isValidCreditCard(4111111111111111111); // false (above 2^53 - 1, pass it as a s ### capitalize -Transforms the first letter into a capital one of each word, the way a Brazilian name, company name or address is written, with no options needed. Words are separated by whitespace, by `-` and `/`, by the apostrophe (`'d'oeste'` becomes `'d'Oeste'`) and by punctuation that touches a word (`'(empresa)'` becomes `'(Empresa)'`, `'bairro:centro'` becomes `'Bairro:Centro'`), so `'MOGI-GUAÇU'` becomes `'Mogi-Guaçu'`; the separators are kept where they are. Every run of whitespace (tabs, newlines, repeated spaces) collapses into a single space, and the leading and trailing whitespace is dropped. The particles of foreign-origin names (`d'`, `del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) stay lower case like the Portuguese prepositions. +Transforms the first letter into a capital one of each word, the way a Brazilian name, company name or address is written, with no options needed. Words are separated by whitespace, by `-` and `/`, by the apostrophe (`'d'oeste'` becomes `'d'Oeste'`) and by punctuation that touches a word (`'(empresa)'` becomes `'(Empresa)'`, `'bairro:centro'` becomes `'Bairro:Centro'`), so `'MOGI-GUAÇU'` becomes `'Mogi-Guaçu'`; the separators are kept where they are. Every run of whitespace (tabs, newlines, repeated spaces) collapses into a single space, and the leading and trailing whitespace is dropped. The particles of foreign-origin names (`del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) stay lower case like the Portuguese prepositions, and so does the elided `d'`, wherever it appears, whenever an apostrophe and a word follow it (`'dias d'ávila'` becomes `'Dias d'Ávila'`); a single letter written right after an apostrophe is the English possessive and stays lower case too (`"bob's"` becomes `"Bob's"`). -`options.lowerCaseWords` defaults to the Portuguese prepositions, articles and conjunctions that stay in lower case inside a proper name (`de`, `da`, `do`, `e`, ...), except when one of them is the first word. `options.upperCaseWords` defaults to the company designations and document abbreviations written in upper case in Brazilian usage (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) plus the roman numerals that appear in names and addresses (`II` through `XXIII`, except `VI`, which collides with the pt-BR verb form "vi"). `SA` without punctuation is deliberately absent, since it is indistinguishable from the surname "Sá" typed without its accent, while `ME` does match the pronoun "me" (`'diga-me'` becomes `'Diga-ME'`), so pass your own `upperCaseWords` when the input is free text rather than a name. `S/A` and `S/S` are matched across the slash even though a slash separates words. A two letter word that follows a `/` is upper-cased when it is the code of a Brazilian state (`'porto alegre/rs'` becomes `'Porto Alegre/RS'`); that rule is structural and stays on even when `upperCaseWords` is given, while a state code that does not follow a `/` is left alone. +`options.lowerCaseWords` defaults to the Portuguese prepositions, articles and conjunctions that stay in lower case inside a proper name (`de`, `da`, `do`, `e`, ...), and they are only written in lower case when they link two words: one of them that is the first word, that ends the value, or that is followed by punctuation is a designator instead and keeps its capital (`'rua a, 100'` becomes `'Rua A, 100'` and `'condomínio a, quadra d, lote o'` becomes `'Condomínio A, Quadra D, Lote O'`). `options.upperCaseWords` defaults to the company designations and document abbreviations written in upper case in Brazilian usage (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) plus the roman numerals that appear in names and addresses (`II` through `XXIII`, except `VI`, which collides with the pt-BR verb form "vi"). `SA` without punctuation is deliberately absent, since it is indistinguishable from the surname "Sá" typed without its accent, while `ME` is also the pronoun "me", so it is only written in upper case in the designation position, as the last word of the value (`'fulano comércio me'` becomes `'Fulano Comércio ME'`) or right before another designation (`'fulano me epp'` becomes `'Fulano ME EPP'`); anywhere else it is an ordinary word (`'diga-me a verdade'` becomes `'Diga-Me a Verdade'`, `'não-me-toque'` becomes `'Não-Me-Toque'`). `S/A` and `S/S` are matched across the slash even though a slash separates words. A two letter word that follows a `/` is upper-cased when it is the code of a Brazilian state (`'porto alegre/rs'` becomes `'Porto Alegre/RS'`); that rule is structural and stays on even when `upperCaseWords` is given, while a state code that does not follow a `/` is left alone. Either list given in `options` replaces its default entirely, and the comparison against both is case-insensitive (pt-BR locale). Options are typed as `CapitalizeOptions`. @@ -1127,6 +1127,10 @@ capitalize('banco do brasil s.a.'); // Banco do Brasil S.A. capitalize('casa de carnes s/a'); // Casa de Carnes S/A ("S/A" is matched across the slash) capitalize('mogi-guaçu'); // Mogi-Guaçu ("-" starts a new word) capitalize("santa bárbara d'oeste"); // Santa Bárbara d'Oeste ("'" starts a new word, "d" stays lower case) +capitalize("bob's"); // Bob's (a single letter after an apostrophe is the English possessive) +capitalize('rua a, 100'); // Rua A, 100 (a preposition followed by punctuation is a designator) +capitalize('fulano comércio me'); // Fulano Comércio ME ("ME" as the last word is the designation) +capitalize('não-me-toque'); // Não-Me-Toque (anywhere else "me" is an ordinary word) capitalize('(empresa) ltda'); // (Empresa) LTDA capitalize('luiz von schmidt'); // Luiz von Schmidt capitalize('santana/rs'); // Santana/RS ("RS" is a state code right after a "/") diff --git a/docs/llms.txt b/docs/llms.txt index 27d6e9ca..1db5e2c1 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -149,14 +149,14 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [getStateCodeByName](https://brazilian-utils.com.br/utilities.md#getstatecodebyname): Get the two-letter code (sigla) of a Brazilian state given its full name. - [getStateNameByCode](https://brazilian-utils.com.br/utilities.md#getstatenamebycode): Get the full name of a Brazilian state given its two-letter code (sigla). - [getTimezoneByState](https://brazilian-utils.com.br/utilities.md#gettimezonebystate): Get the IANA time zone database name (tzdata zone) for a Brazilian state, chosen as the zone of the state capital. -- [getCities](https://brazilian-utils.com.br/utilities.md#getcities): Get Brazilian cities. +- [getCities](https://brazilian-utils.com.br/utilities.md#getcities): Get Brazilian cities. Deprecated: use `getMunicipalities` instead. - [getHolidays](https://brazilian-utils.com.br/utilities.md#getholidays): Get Brazilian holidays for a given year. - [getCepInfoByAddress](https://brazilian-utils.com.br/utilities.md#getcepinfobyaddress): Fetch CEPs from an address using ViaCEP. - [getLegalNatures](https://brazilian-utils.com.br/utilities.md#getlegalnatures): Get the legal nature map keyed by code. - [getLegalNaturesByCategory](https://brazilian-utils.com.br/utilities.md#getlegalnaturesbycategory): Get every legal nature of a CONCLA category, the group given by the first digit of the code: `1` Administração Pública, `2` Entidades Empresariais, `3` Entidades sem Fins Lucrativos, `4` Pessoas Físicas and `5` Organizações Internacionais e Outras Instituições Extraterritoriais. - [getLegalNature](https://brazilian-utils.com.br/utilities.md#getlegalnature): Look a legal nature code up in the official IBGE/CONCLA table. - [getFormatLicensePlate](https://brazilian-utils.com.br/utilities.md#getformatlicenseplate): Detect the normalized format of a license plate. -- [getMunicipality](https://brazilian-utils.com.br/utilities.md#getmunicipality): Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. +- [getMunicipality](https://brazilian-utils.com.br/utilities.md#getmunicipality): Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. Deprecated: use `getMunicipalityByCode` instead, which is synchronous and offline; matching a municipality by name is up to the application, over `getMunicipalities`. - [getMunicipalities](https://brazilian-utils.com.br/utilities.md#getmunicipalities): Get Brazilian municipalities published by the IBGE. - [getMunicipalityByCode](https://brazilian-utils.com.br/utilities.md#getmunicipalitybycode): Look up a Brazilian municipality by its 7-digit IBGE code. - [getCertidaoInfo](https://brazilian-utils.com.br/utilities.md#getcertidaoinfo): Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index a5f89b47..f997fc8c 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -858,9 +858,9 @@ isValidCreditCard(4111111111111111111); // false (acima de 2^53 - 1, passe como ## capitalize -Transforma a primeira letra de cada palavra em maiúscula do jeito que se escreve um nome, uma razão social ou um endereço brasileiro, sem precisar de opções. As palavras são separadas por espaço em branco, por `-` e `/`, pelo apóstrofo (`'d'oeste'` vira `'d'Oeste'`) e pela pontuação colada à palavra (`'(empresa)'` vira `'(Empresa)'`, `'bairro:centro'` vira `'Bairro:Centro'`), então `'MOGI-GUAÇU'` vira `'Mogi-Guaçu'`; os separadores ficam onde estão. Toda sequência de espaços em branco (tabs, quebras de linha, espaços repetidos) vira um único espaço, e o espaço no início e no fim é descartado. As partículas de nomes de origem estrangeira (`d'`, `del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) ficam em minúsculas como as preposições do português. +Transforma a primeira letra de cada palavra em maiúscula do jeito que se escreve um nome, uma razão social ou um endereço brasileiro, sem precisar de opções. As palavras são separadas por espaço em branco, por `-` e `/`, pelo apóstrofo (`'d'oeste'` vira `'d'Oeste'`) e pela pontuação colada à palavra (`'(empresa)'` vira `'(Empresa)'`, `'bairro:centro'` vira `'Bairro:Centro'`), então `'MOGI-GUAÇU'` vira `'Mogi-Guaçu'`; os separadores ficam onde estão. Toda sequência de espaços em branco (tabs, quebras de linha, espaços repetidos) vira um único espaço, e o espaço no início e no fim é descartado. As partículas de nomes de origem estrangeira (`del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) ficam em minúsculas como as preposições do português, e a partícula elidida `d'` também, onde quer que apareça, sempre que um apóstrofo e uma palavra vierem logo depois (`'dias d'ávila'` vira `'Dias d'Ávila'`); uma letra sozinha logo depois de um apóstrofo é o possessivo do inglês e também fica em minúscula (`"bob's"` vira `"Bob's"`). -`options.lowerCaseWords` tem como padrão as preposições, artigos e conjunções que permanecem em minúsculas dentro de um nome próprio (`de`, `da`, `do`, `e`, ...), exceto quando uma delas é a primeira palavra. `options.upperCaseWords` tem como padrão as designações societárias e as abreviações de documentos escritas em maiúsculas no uso brasileiro (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) mais os algarismos romanos que aparecem em nomes e endereços (de `II` a `XXIII`, exceto `VI`, que colide com a forma verbal "vi"). `SA` sem pontuação ficou de fora de propósito, por ser indistinguível do sobrenome "Sá" digitado sem o acento, enquanto `ME` casa também com o pronome "me" (`'diga-me'` vira `'Diga-ME'`), então informe o seu próprio `upperCaseWords` quando a entrada for texto livre em vez de um nome. `S/A` e `S/S` são reconhecidos mesmo com a barra no meio, embora a barra separe palavras. Uma palavra de duas letras logo depois de uma `/` vira maiúscula quando é a sigla de um estado brasileiro (`'porto alegre/rs'` vira `'Porto Alegre/RS'`); essa regra é estrutural e continua valendo mesmo com `upperCaseWords` informado, enquanto uma sigla de estado que não venha depois de uma `/` é deixada como está. +`options.lowerCaseWords` tem como padrão as preposições, artigos e conjunções que permanecem em minúsculas dentro de um nome próprio (`de`, `da`, `do`, `e`, ...), e elas só ficam em minúsculas quando ligam duas palavras: uma delas que seja a primeira palavra, que encerre o valor ou que venha antes de uma pontuação é um designativo e mantém a maiúscula (`'rua a, 100'` vira `'Rua A, 100'` e `'condomínio a, quadra d, lote o'` vira `'Condomínio A, Quadra D, Lote O'`). `options.upperCaseWords` tem como padrão as designações societárias e as abreviações de documentos escritas em maiúsculas no uso brasileiro (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) mais os algarismos romanos que aparecem em nomes e endereços (de `II` a `XXIII`, exceto `VI`, que colide com a forma verbal "vi"). `SA` sem pontuação ficou de fora de propósito, por ser indistinguível do sobrenome "Sá" digitado sem o acento, enquanto `ME` é também o pronome "me", então só fica em maiúsculas na posição de designação, como última palavra do valor (`'fulano comércio me'` vira `'Fulano Comércio ME'`) ou logo antes de outra designação (`'fulano me epp'` vira `'Fulano ME EPP'`); em qualquer outro lugar é uma palavra comum (`'diga-me a verdade'` vira `'Diga-Me a Verdade'`, `'não-me-toque'` vira `'Não-Me-Toque'`). `S/A` e `S/S` são reconhecidos mesmo com a barra no meio, embora a barra separe palavras. Uma palavra de duas letras logo depois de uma `/` vira maiúscula quando é a sigla de um estado brasileiro (`'porto alegre/rs'` vira `'Porto Alegre/RS'`); essa regra é estrutural e continua valendo mesmo com `upperCaseWords` informado, enquanto uma sigla de estado que não venha depois de uma `/` é deixada como está. Qualquer uma das listas informada em `options` substitui inteiramente a lista padrão correspondente, e a comparação com as duas é case-insensitive (locale pt-BR). As opções são tipadas como `CapitalizeOptions`. @@ -874,6 +874,10 @@ capitalize('banco do brasil s.a.'); // Banco do Brasil S.A. capitalize('casa de carnes s/a'); // Casa de Carnes S/A ("S/A" é reconhecido com a barra no meio) capitalize('mogi-guaçu'); // Mogi-Guaçu ("-" inicia uma nova palavra) capitalize("santa bárbara d'oeste"); // Santa Bárbara d'Oeste ("'" inicia uma nova palavra, "d" fica minúsculo) +capitalize("bob's"); // Bob's (uma letra sozinha depois do apóstrofo é o possessivo do inglês) +capitalize('rua a, 100'); // Rua A, 100 (uma preposição antes de pontuação é um designativo) +capitalize('fulano comércio me'); // Fulano Comércio ME ("ME" como última palavra é a designação) +capitalize('não-me-toque'); // Não-Me-Toque (em qualquer outro lugar "me" é palavra comum) capitalize('(empresa) ltda'); // (Empresa) LTDA capitalize('luiz von schmidt'); // Luiz von Schmidt capitalize('santana/rs'); // Santana/RS ("RS" é sigla de estado logo depois de uma "/") diff --git a/docs/utilities.md b/docs/utilities.md index 4136a044..69846560 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -858,9 +858,9 @@ isValidCreditCard(4111111111111111111); // false (above 2^53 - 1, pass it as a s ## capitalize -Transforms the first letter into a capital one of each word, the way a Brazilian name, company name or address is written, with no options needed. Words are separated by whitespace, by `-` and `/`, by the apostrophe (`'d'oeste'` becomes `'d'Oeste'`) and by punctuation that touches a word (`'(empresa)'` becomes `'(Empresa)'`, `'bairro:centro'` becomes `'Bairro:Centro'`), so `'MOGI-GUAÇU'` becomes `'Mogi-Guaçu'`; the separators are kept where they are. Every run of whitespace (tabs, newlines, repeated spaces) collapses into a single space, and the leading and trailing whitespace is dropped. The particles of foreign-origin names (`d'`, `del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) stay lower case like the Portuguese prepositions. +Transforms the first letter into a capital one of each word, the way a Brazilian name, company name or address is written, with no options needed. Words are separated by whitespace, by `-` and `/`, by the apostrophe (`'d'oeste'` becomes `'d'Oeste'`) and by punctuation that touches a word (`'(empresa)'` becomes `'(Empresa)'`, `'bairro:centro'` becomes `'Bairro:Centro'`), so `'MOGI-GUAÇU'` becomes `'Mogi-Guaçu'`; the separators are kept where they are. Every run of whitespace (tabs, newlines, repeated spaces) collapses into a single space, and the leading and trailing whitespace is dropped. The particles of foreign-origin names (`del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) stay lower case like the Portuguese prepositions, and so does the elided `d'`, wherever it appears, whenever an apostrophe and a word follow it (`'dias d'ávila'` becomes `'Dias d'Ávila'`); a single letter written right after an apostrophe is the English possessive and stays lower case too (`"bob's"` becomes `"Bob's"`). -`options.lowerCaseWords` defaults to the Portuguese prepositions, articles and conjunctions that stay in lower case inside a proper name (`de`, `da`, `do`, `e`, ...), except when one of them is the first word. `options.upperCaseWords` defaults to the company designations and document abbreviations written in upper case in Brazilian usage (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) plus the roman numerals that appear in names and addresses (`II` through `XXIII`, except `VI`, which collides with the pt-BR verb form "vi"). `SA` without punctuation is deliberately absent, since it is indistinguishable from the surname "Sá" typed without its accent, while `ME` does match the pronoun "me" (`'diga-me'` becomes `'Diga-ME'`), so pass your own `upperCaseWords` when the input is free text rather than a name. `S/A` and `S/S` are matched across the slash even though a slash separates words. A two letter word that follows a `/` is upper-cased when it is the code of a Brazilian state (`'porto alegre/rs'` becomes `'Porto Alegre/RS'`); that rule is structural and stays on even when `upperCaseWords` is given, while a state code that does not follow a `/` is left alone. +`options.lowerCaseWords` defaults to the Portuguese prepositions, articles and conjunctions that stay in lower case inside a proper name (`de`, `da`, `do`, `e`, ...), and they are only written in lower case when they link two words: one of them that is the first word, that ends the value, or that is followed by punctuation is a designator instead and keeps its capital (`'rua a, 100'` becomes `'Rua A, 100'` and `'condomínio a, quadra d, lote o'` becomes `'Condomínio A, Quadra D, Lote O'`). `options.upperCaseWords` defaults to the company designations and document abbreviations written in upper case in Brazilian usage (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) plus the roman numerals that appear in names and addresses (`II` through `XXIII`, except `VI`, which collides with the pt-BR verb form "vi"). `SA` without punctuation is deliberately absent, since it is indistinguishable from the surname "Sá" typed without its accent, while `ME` is also the pronoun "me", so it is only written in upper case in the designation position, as the last word of the value (`'fulano comércio me'` becomes `'Fulano Comércio ME'`) or right before another designation (`'fulano me epp'` becomes `'Fulano ME EPP'`); anywhere else it is an ordinary word (`'diga-me a verdade'` becomes `'Diga-Me a Verdade'`, `'não-me-toque'` becomes `'Não-Me-Toque'`). `S/A` and `S/S` are matched across the slash even though a slash separates words. A two letter word that follows a `/` is upper-cased when it is the code of a Brazilian state (`'porto alegre/rs'` becomes `'Porto Alegre/RS'`); that rule is structural and stays on even when `upperCaseWords` is given, while a state code that does not follow a `/` is left alone. Either list given in `options` replaces its default entirely, and the comparison against both is case-insensitive (pt-BR locale). Options are typed as `CapitalizeOptions`. @@ -874,6 +874,10 @@ capitalize('banco do brasil s.a.'); // Banco do Brasil S.A. capitalize('casa de carnes s/a'); // Casa de Carnes S/A ("S/A" is matched across the slash) capitalize('mogi-guaçu'); // Mogi-Guaçu ("-" starts a new word) capitalize("santa bárbara d'oeste"); // Santa Bárbara d'Oeste ("'" starts a new word, "d" stays lower case) +capitalize("bob's"); // Bob's (a single letter after an apostrophe is the English possessive) +capitalize('rua a, 100'); // Rua A, 100 (a preposition followed by punctuation is a designator) +capitalize('fulano comércio me'); // Fulano Comércio ME ("ME" as the last word is the designation) +capitalize('não-me-toque'); // Não-Me-Toque (anywhere else "me" is an ordinary word) capitalize('(empresa) ltda'); // (Empresa) LTDA capitalize('luiz von schmidt'); // Luiz von Schmidt capitalize('santana/rs'); // Santana/RS ("RS" is a state code right after a "/") diff --git a/src/capitalize/capitalize.test.ts b/src/capitalize/capitalize.test.ts index 80e34bf9..e79314dc 100644 --- a/src/capitalize/capitalize.test.ts +++ b/src/capitalize/capitalize.test.ts @@ -60,6 +60,7 @@ describe("capitalize", () => { expect(capitalize("empresa ltda")).toBe("Empresa LTDA"); expect(capitalize("banco do brasil s.a.")).toBe("Banco do Brasil S.A."); expect(capitalize("casa de carnes s/a")).toBe("Casa de Carnes S/A"); + expect(capitalize("casa de carnes s/a comércio")).toBe("Casa de Carnes S/A Comércio"); expect(capitalize("consultoria s/s")).toBe("Consultoria S/S"); expect(capitalize("padaria e confeitaria me")).toBe("Padaria e Confeitaria ME"); expect(capitalize("meu cpf e rg")).toBe("Meu CPF e RG"); @@ -91,6 +92,42 @@ describe("capitalize", () => { expect(capitalize("casa;lote [3]")).toBe("Casa;Lote [3]"); }); + test("when a single letter follows an apostrophe, the English possessive, which stays in lower case", () => { + expect(capitalize("bob's")).toBe("Bob's"); + expect(capitalize("habib's")).toBe("Habib's"); + expect(capitalize("mc donald's")).toBe("Mc Donald's"); + expect(capitalize("x'd")).toBe("X'd"); + expect(capitalize("sant'ana")).toBe("Sant'Ana"); + }); + + test("when the elided particle d' is followed by an apostrophe and a word, wherever it appears", () => { + expect(capitalize("d'oeste")).toBe("d'Oeste"); + expect(capitalize("dias d'ávila")).toBe("Dias d'Ávila"); + expect(capitalize("olho d'água do piauí")).toBe("Olho d'Água do Piauí"); + expect(capitalize("rua d'")).toBe("Rua D'"); + expect(capitalize("d''oeste")).toBe("D''Oeste"); + }); + + test("when a word of the lower case list ends the value or is followed by punctuation, so it is a designator rather than a link between two words", () => { + expect(capitalize("rua d")).toBe("Rua D"); + expect(capitalize("rua a, 100")).toBe("Rua A, 100"); + expect(capitalize("condomínio a, quadra d, lote o")).toBe("Condomínio A, Quadra D, Lote O"); + expect(capitalize("maria e joão")).toBe("Maria e João"); + expect(capitalize("maria e--joão")).toBe("Maria e--João"); + expect(capitalize("josé da silva")).toBe("José da Silva"); + expect(capitalize("de")).toBe("De"); + expect(capitalize("luiz von schmidt")).toBe("Luiz von Schmidt"); + expect(capitalize("são joão del rei")).toBe("São João del Rei"); + }); + + test("when ME is the pronoun rather than the designation of a microempresa, which is written at the end of the name", () => { + expect(capitalize("fulano comércio me")).toBe("Fulano Comércio ME"); + expect(capitalize("fulano me epp")).toBe("Fulano ME EPP"); + expect(capitalize("fulano ltda me")).toBe("Fulano LTDA ME"); + expect(capitalize("não-me-toque")).toBe("Não-Me-Toque"); + expect(capitalize("diga-me a verdade")).toBe("Diga-Me a Verdade"); + }); + test("when the name carries a foreign particle", () => { expect(capitalize("luiz von schmidt")).toBe("Luiz von Schmidt"); expect(capitalize("maria van der berg")).toBe("Maria van der Berg"); diff --git a/src/capitalize/capitalize.ts b/src/capitalize/capitalize.ts index 3ae28744..bf362957 100644 --- a/src/capitalize/capitalize.ts +++ b/src/capitalize/capitalize.ts @@ -1,10 +1,15 @@ import { + APOSTROPHE_REGEX, + ELIDED_PARTICLE, + JOINER_REGEX, PREPOSITIONS, PUNCTUATION_REGEX, SEPARATOR_REGEX, STATE_CODES, + TRAILING_DESIGNATIONS, UPPER_CASE_WORDS, WHITESPACE_REGEX, + WORD_REGEX, } from "./constants"; /** Options of `capitalize`. */ @@ -17,6 +22,8 @@ export type CapitalizeOptions = { const stateCodeSet: Set = new Set(STATE_CODES); +const trailingDesignationSet: Set = new Set(TRAILING_DESIGNATIONS); + const toWordSet = ( words: unknown, fallback: readonly string[], @@ -27,6 +34,84 @@ const toWordSet = ( return new Set(source.filter((word) => typeof word === "string").map((word) => fold(word))); }; +/** + * A token that carries a word, as opposed to a separator or the empty token between two of them. + * + * @param {string} token - The token to classify. + * @returns {boolean} `true` when the token is a word. + */ +const isWord = (token: string): boolean => WORD_REGEX.test(token); + +/** + * An apostrophe token, the one that elides the particle of `d'Oeste` and marks the possessive of + * `Bob's`. + * + * @param {string} token - The token to classify. + * @returns {boolean} `true` when the token is an apostrophe. + */ +const isApostrophe = (token: string): boolean => APOSTROPHE_REGEX.test(token); + +/** + * The next word of the value after `index`, plus whether it is joined to the word at `index`, that + * is, whether only whitespace, `-`, `/` or an apostrophe stands between the two. + * + * @param {string[]} tokens - Every token of the value, words and separators alike. + * @param {number} index - The index of the word to look ahead from. + * @returns {{ joined: boolean; next: string }} The next word (`""` when there is none) and whether it is joined to the word at `index`. + */ +const lookAhead = (tokens: string[], index: number): { joined: boolean; next: string } => { + let joined = true; + + for (let position = index + 1; position < tokens.length; position++) { + const token = tokens[position]; + + if (token === "") continue; + if (isWord(token)) return { joined, next: token }; + if (!JOINER_REGEX.test(token)) joined = false; + } + + return { joined: false, next: "" }; +}; + +/** + * The `d` of `d'Oeste`: an elided particle only when an apostrophe and a word follow it. Splitting + * on the separators always leaves a token after each of them, so the token two places ahead of a + * word followed by an apostrophe is always there, even when it is the empty one of `"rua d'"`. + * + * @param {string[]} tokens - Every token of the value, words and separators alike. + * @param {number} index - The index of the word being written. + * @param {string} word - That word, in lower case. + * @returns {boolean} `true` when the word is the elided particle. + */ +const isElidedParticle = (tokens: string[], index: number, word: string): boolean => + word === ELIDED_PARTICLE && isApostrophe(tokens[index + 1]) && isWord(tokens[index + 2]); + +/** + * The `s` of `Bob's`: the English possessive, a single letter written right after an apostrophe. + * + * @param {string[]} tokens - Every token of the value, words and separators alike. + * @param {number} index - The index of the word being written. + * @param {string} word - That word, in lower case. + * @returns {boolean} `true` when the word is an English possessive. + */ +const isPossessive = (tokens: string[], index: number, word: string): boolean => + word.length === 1 && isApostrophe(tokens[index - 1]); + +/** + * Whether a word of the upper case list stands where it is written in upper case. Every + * designation but the ones of `TRAILING_DESIGNATIONS` is upper case wherever it appears; those + * are upper case only as the last word of the value or right before another designation. + * + * @param {string} word - The word being written, in upper case. + * @param {string} next - The next word of the value, `""` when the word is the last one. + * @param {Set} upperCaseSet - The upper case word list in force. + * @returns {boolean} `true` when the word is written in upper case where it stands. + */ +const isUpperCasePosition = (word: string, next: string, upperCaseSet: Set): boolean => + !trailingDesignationSet.has(word) || + next === "" || + upperCaseSet.has(next.toLocaleUpperCase("pt-BR")); + /** * Capitalizes a given string according to the way a Brazilian name, company name or address is * written, with no configuration needed: `"jose da silva"` becomes `"Jose da Silva"`, @@ -37,21 +122,33 @@ const toWordSet = ( * `"bairro:centro"` becomes `"Bairro:Centro"`), so `"MOGI-GUAÇU"` becomes `"Mogi-Guaçu"`. The * separators are kept where they are, while every run of whitespace (spaces, tabs, newlines) * collapses into a single space and the leading and trailing whitespace is dropped. The particles - * of foreign-origin names (`d'`, `del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) stay lower + * of foreign-origin names (`del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) stay lower * case like the Portuguese prepositions, so `"luiz von schmidt"` becomes `"Luiz von Schmidt"`. * - * - Words listed in `lowerCaseWords` are converted to lower case, except for the first word. The - * default list is the Portuguese prepositions, articles and conjunctions that stay in lower - * case inside a proper name ("de", "da", "do", "e", ...), so `"JOSÉ DA SILVA"` becomes - * `"José da Silva"`. + * - Words listed in `lowerCaseWords` are converted to lower case when they link two words, that + * is, when they are neither the first word nor the last one and another word follows them + * across whitespace, `-`, `/` or an apostrophe. The default list is the Portuguese + * prepositions, articles and conjunctions that stay in lower case inside a proper name ("de", + * "da", "do", "e", ...), so `"JOSÉ DA SILVA"` becomes `"José da Silva"`. A word of the list + * that ends the value or is followed by punctuation is a designator instead, and keeps its + * capital: `"rua a, 100"` becomes `"Rua A, 100"` and `"condomínio a, quadra d, lote o"` becomes + * `"Condomínio A, Quadra D, Lote O"`. + * - The elided particle `d'` is written in lower case wherever it appears, including as the first + * word, but only when an apostrophe and a word follow it, so `"santa bárbara d'oeste"` becomes + * `"Santa Bárbara d'Oeste"` and `"dias d'ávila"` becomes `"Dias d'Ávila"` while the designator + * `"rua d"` becomes `"Rua D"`. A single letter written right after an apostrophe is the English + * possessive and stays in lower case, so `"bob's"` becomes `"Bob's"`, not `"Bob'S"`. * - Words listed in `upperCaseWords` are converted to upper case wherever they appear. The * default list is the company designations and document abbreviations that are written in upper * case in Brazilian usage (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, * `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) plus the roman numerals that appear in names * and addresses (`II` through `XXIII`, except `VI`, so `"joão paulo ii"` becomes - * `"João Paulo II"` and `"rua xv de novembro"` becomes `"Rua XV de Novembro"`). `ME` matches - * the pronoun "me" too, so free text such as `"diga-me"` becomes `"Diga-ME"`: pass an - * `upperCaseWords` of your own when the input is not a name. A designation + * `"João Paulo II"` and `"rua xv de novembro"` becomes `"Rua XV de Novembro"`). `ME` is also + * the pt-BR pronoun "me", so it is only upper cased in the designation position, as the last + * word of the value (`"fulano comércio me"` becomes `"Fulano Comércio ME"`) or right before + * another designation (`"fulano me epp"` becomes `"Fulano ME EPP"`); anywhere else it is an + * ordinary word, so `"diga-me a verdade"` becomes `"Diga-Me a Verdade"` and the municipality + * `"não-me-toque"` becomes `"Não-Me-Toque"`. A designation * written around a slash, `S/A` and `S/S`, is matched across that slash even though a slash * separates words, so `"casa de carnes s/a"` becomes `"Casa de Carnes S/A"`. * - A two letter word that follows a `/` is converted to upper case when it is the code of a @@ -89,6 +186,10 @@ const toWordSet = ( * capitalize("empresa ltda"); // "Empresa LTDA" * capitalize("banco do brasil s.a."); // "Banco do Brasil S.A." * capitalize("santa bárbara d'oeste"); // "Santa Bárbara d'Oeste" + * capitalize("bob's"); // "Bob's" + * capitalize("rua a, 100"); // "Rua A, 100" + * capitalize("fulano comércio me"); // "Fulano Comércio ME" + * capitalize("não-me-toque"); // "Não-Me-Toque" * capitalize("(empresa) ltda"); // "(Empresa) LTDA" * capitalize("luiz von schmidt"); // "Luiz von Schmidt" * capitalize("casa de carnes s/a"); // "Casa de Carnes S/A" @@ -117,7 +218,7 @@ export const capitalize = (value: string, options?: CapitalizeOptions): string = const output: string[] = []; let wordIndex = 0; - for (const token of tokens) { + for (const [index, token] of tokens.entries()) { if (!token) continue; if (WHITESPACE_REGEX.test(token)) { @@ -133,12 +234,20 @@ export const capitalize = (value: string, options?: CapitalizeOptions): string = const lowerCaseWord = token.toLocaleLowerCase("pt-BR"); const upperCaseWord = token.toLocaleUpperCase("pt-BR"); const designation = (output.slice(-2).join("") + upperCaseWord).toLocaleUpperCase("pt-BR"); + const { joined, next } = lookAhead(tokens, index); if (upperCaseSet.has(designation)) { output.splice(-2, 2, designation); - } else if (wordIndex > 0 && lowerCaseSet.has(lowerCaseWord)) { + } else if (isPossessive(tokens, index, lowerCaseWord)) { + output.push(lowerCaseWord); + } else if (isElidedParticle(tokens, index, lowerCaseWord)) { + output.push(lowerCaseWord); + } else if (wordIndex > 0 && joined && lowerCaseSet.has(lowerCaseWord)) { output.push(lowerCaseWord); - } else if (upperCaseSet.has(upperCaseWord)) { + } else if ( + upperCaseSet.has(upperCaseWord) && + isUpperCasePosition(upperCaseWord, next, upperCaseSet) + ) { output.push(upperCaseWord); } else if (output.at(-1) === "/" && stateCodeSet.has(upperCaseWord)) { output.push(upperCaseWord); diff --git a/src/capitalize/constants.ts b/src/capitalize/constants.ts index 32cc5faf..76a99d18 100644 --- a/src/capitalize/constants.ts +++ b/src/capitalize/constants.ts @@ -7,7 +7,9 @@ import { type StateCode } from "../_internals/constants/states"; * preposições que liguem as palavras do cargo devem ser grafadas em minúsculas" (item 5.1.8 b), * and a title is written "com inicial maiúscula em todas as palavras, exceto nas de ligação" * (item 10.2 a). The same convention is used by the IBGE for the names of municipalities - * ("Mogi das Cruzes", "Santa Bárbara d'Oeste"). Applying it to personal and institutional names + * ("Mogi das Cruzes", "Santa Bárbara d'Oeste"). The elided `d` of "d'Oeste" is not a member of + * this list: on its own it is a designator ("Rua D", "Quadra D"), so `capitalize` lower-cases it + * structurally, only when an apostrophe and a word follow it. Applying it to personal and institutional names * ("Ministério da Justiça", "José da Silva") is this library's extension of that rule; the * Manual does not spell those two cases out. * @@ -27,7 +29,6 @@ export const PREPOSITIONS = [ "de", "do", "dos", - "d", "del", "della", "den", @@ -174,3 +175,39 @@ export const SEPARATOR_REGEX = /(\s+|[-/'’‘(){}[\]"“”:;,])/; export const PUNCTUATION_REGEX = /^[-/'’‘(){}[\]"“”:;,]$/; export const WHITESPACE_REGEX = /^\s+$/; + +/** + * A token that carries a word: one that holds at least one character that is not a separator. The + * empty token that `String.prototype.split` leaves between two separators does not, and neither + * does a whitespace run or a single punctuation mark. + */ +export const WORD_REGEX = /[^\s/'’‘(){}[\]"“”:;,-]/; + +/** + * The separators that join two words into one name ("Rio-de-Janeiro", "Santa Bárbara d'Oeste", + * "Porto Alegre/RS"), as opposed to the punctuation that closes a phrase (`,`, `;`, `:`, brackets + * and quotes). A word of the lower case list is only written in lower case when another word + * follows it across separators of this kind; before a closing mark, or at the end of the value, it + * is a designator ("Rua D", "Quadra A, Lote B") and keeps its capital. + */ +export const JOINER_REGEX = /^(?:\s+|[-/'’‘])$/; + +/** The apostrophe that elides the particle of `d'Oeste` and marks the English possessive of `Bob's`. */ +export const APOSTROPHE_REGEX = /^['’‘]$/; + +/** The elided particle of `Santa Bárbara d'Oeste`, lower case only when an apostrophe and a word follow it. */ +export const ELIDED_PARTICLE = "d"; + +/** + * Designations that are only written in upper case in the designation position, that is, as the + * last word of the name ("Fulano Comércio ME") or right before another designation + * ("Fulano ME EPP"). `ME` is also the pt-BR pronoun "me", so upper-casing it wherever it appears + * turned free text into `"Diga-ME a Verdade"` and the municipality of Não-Me-Toque/RS into + * `"Não-ME-Toque"`; anywhere else in the value it is written as an ordinary word. + * + * @see Official: https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp123.htm + * Lei Complementar nº 123/2006, art. 72 (revoked by the Lei Complementar nº 155/2016): the + * abbreviation is added "ao final" of the firma or denominação, which is the position this list + * keeps it in. + */ +export const TRAILING_DESIGNATIONS = ["ME"]; From ca8d37d60fb1477c62bda4806b01c3e979d8910a Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:09:16 -0300 Subject: [PATCH 69/75] fix(generate): never throw on a prototype key or an object without toString `generateVoterId("__proto__")` and `generateCpf(Object.create(null))` threw a TypeError, in 2.3.0 too, because the state lookup read the table without an own-property guard and the state code was coerced before being checked. Both now take the documented fallback for an unknown state. --- src/generate-cpf/generate-cpf.test.ts | 34 +++++++++++++++++ src/generate-cpf/generate-cpf.ts | 15 +++++++- .../generate-voter-id.test.ts | 38 +++++++++++++++++++ src/generate-voter-id/generate-voter-id.ts | 20 +++++++++- 4 files changed, 103 insertions(+), 4 deletions(-) diff --git a/src/generate-cpf/generate-cpf.test.ts b/src/generate-cpf/generate-cpf.test.ts index 8b92d810..63beca5b 100644 --- a/src/generate-cpf/generate-cpf.test.ts +++ b/src/generate-cpf/generate-cpf.test.ts @@ -2,6 +2,7 @@ import * as fc from "fast-check"; import { CPF_LENGTH } from "../_internals/constants/cpf"; import { DATA, type StateCode } from "../_internals/constants/states"; +import { PROTOTYPE_KEYS } from "../_internals/test/arbitraries"; import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidCpf } from "../is-valid-cpf/is-valid-cpf"; import { STATE_CODES } from "./constants"; @@ -69,9 +70,31 @@ describe("generateCpf", () => { expect(isValidCpf(cpf)).toBe(true); }); + test("should fall back to a random digit instead of reaching the prototype chain for a state code", () => { + for (const key of PROTOTYPE_KEYS) { + // @ts-expect-error: intentionally invalid input + const cpf = generateCpf(key); + expect(cpf).toHaveLength(CPF_LENGTH); + expect(isValidCpf(cpf)).toBe(true); + } + }); + + test("should fall back to a random digit instead of throwing for a state code with no string conversion", () => { + const nullPrototype = generateCpf(Object.create(null)); + const throwing = generateCpf({ + toString() { + throw new Error("no string conversion"); + }, + } as unknown as StateCode); + + expect(isValidCpf(nullPrototype)).toBe(true); + expect(isValidCpf(throwing)).toBe(true); + }); + describe("properties", () => { const stateCode = fc.constantFrom(...DATA.map((state) => state.code)); const batchSize = fc.integer({ min: 1, max: 10 }); + const hostileStateCode = fc.oneof(fc.constantFrom(...PROTOTYPE_KEYS), fc.anything()); test("should generate a valid CPF carrying the state digit of every state", () => { fc.assert( @@ -85,6 +108,17 @@ describe("generateCpf", () => { ); }); + test("should generate a valid CPF for any state code at all, prototype chain keys included", () => { + fc.assert( + fc.property(hostileStateCode, (state) => { + const cpf = generateCpf(state as StateCode); + + expect(cpf).toMatch(/^\d{11}$/); + expect(isValidCpf(cpf)).toBe(true); + }), + ); + }); + test("should never draw a base made of a single repeated digit", () => { fc.assert( fc.property(batchSize, (size) => { diff --git a/src/generate-cpf/generate-cpf.ts b/src/generate-cpf/generate-cpf.ts index 18e3ea84..d651814e 100644 --- a/src/generate-cpf/generate-cpf.ts +++ b/src/generate-cpf/generate-cpf.ts @@ -6,8 +6,17 @@ import { BASE_LENGTH, STATE_CODES } from "./constants"; export type { StateCode } from "../_internals/constants/states"; +/** + * The região fiscal digit of a state, a random one for anything else. The state is read as a + * string before the own property lookup, so a value with no string conversion (an object created + * with `Object.create(null)`, one whose `toString` throws) is an unknown state rather than a + * `TypeError` thrown while `Object.hasOwn` coerces it into a property key. + * + * @param {StateCode} [state] - The state code the CPF is generated for, if any. + * @returns {string} The região fiscal digit of that state, or a random digit. + */ const getStateCode = (state?: StateCode): string => { - if (state && Object.hasOwn(STATE_CODES, state)) return STATE_CODES[state]; + if (typeof state === "string" && Object.hasOwn(STATE_CODES, state)) return STATE_CODES[state]; return generateRandomNumber(1); }; @@ -21,7 +30,9 @@ const calculateCheckDigit = (base: string, weight: number): string => { * * Uses `Math.random()` internally, so it is not cryptographically secure, do not use for security purposes. * - * @param {StateCode} [state] - The Brazilian state code to generate a CPF for. + * @param {StateCode} [state] - The Brazilian state code to generate a CPF for. An unknown state + * draws a random região fiscal digit instead of throwing, a key of the prototype chain + * (`"__proto__"`, `"constructor"`) and a value with no string conversion included. * @returns {string} A valid 11-digit CPF string without formatting. * * @example diff --git a/src/generate-voter-id/generate-voter-id.test.ts b/src/generate-voter-id/generate-voter-id.test.ts index cead4fe4..3d589cd8 100644 --- a/src/generate-voter-id/generate-voter-id.test.ts +++ b/src/generate-voter-id/generate-voter-id.test.ts @@ -1,6 +1,7 @@ import * as fc from "fast-check"; import { type StateCode } from "../_internals/constants/states"; +import { PROTOTYPE_KEYS } from "../_internals/test/arbitraries"; import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { UF_TO_VOTER_ID_CODE } from "../is-valid-voter-id/constants"; import { isValidVoterId } from "../is-valid-voter-id/is-valid-voter-id"; @@ -26,9 +27,35 @@ describe("generateVoterId", () => { expect(isValidVoterId(voterId)).toBe(true); }); + it("should fall back to the default UF instead of reaching the prototype chain for a state", () => { + for (const key of PROTOTYPE_KEYS) { + // @ts-expect-error: intentionally invalid input + const voterId = generateVoterId(key); + + expect(voterId).toMatch(/^\d{12}$/); + expect(voterId.slice(8, 10)).toBe("28"); + expect(isValidVoterId(voterId)).toBe(true); + } + }); + + it("should fall back to the default UF instead of throwing for a state that is not a string", () => { + const nullPrototype = generateVoterId(Object.create(null)); + const throwing = generateVoterId({ + toString() { + throw new Error("no string conversion"); + }, + } as unknown as StateCode); + + expect(nullPrototype.slice(8, 10)).toBe("28"); + expect(throwing.slice(8, 10)).toBe("28"); + expect(isValidVoterId(nullPrototype)).toBe(true); + expect(isValidVoterId(throwing)).toBe(true); + }); + describe("properties", () => { const states = Object.keys(UF_TO_VOTER_ID_CODE) as (StateCode | "ZZ")[]; const stateCode = fc.constantFrom(...states); + const hostileStateCode = fc.oneof(fc.constantFrom(...PROTOTYPE_KEYS), fc.anything()); test("should carry the federative union code of every state it supports", () => { fc.assert( @@ -41,6 +68,17 @@ describe("generateVoterId", () => { }), ); }); + + test("should generate a valid voter id for any state at all, prototype chain keys included", () => { + fc.assert( + fc.property(hostileStateCode, (state) => { + const voterId = generateVoterId(state as StateCode); + + expect(voterId).toMatch(/^\d{12}$/); + expect(isValidVoterId(voterId)).toBe(true); + }), + ); + }); }); }); diff --git a/src/generate-voter-id/generate-voter-id.ts b/src/generate-voter-id/generate-voter-id.ts index 7c93506b..8dbb1a96 100644 --- a/src/generate-voter-id/generate-voter-id.ts +++ b/src/generate-voter-id/generate-voter-id.ts @@ -6,13 +6,29 @@ import { UF_TO_VOTER_ID_CODE } from "../is-valid-voter-id/constants"; export type { StateCode } from "../_internals/constants/states"; +/** + * The federative union code of a state, `"ZZ"`'s own code for anything else. The lookup is an own + * property one, so a key of the prototype chain (`"__proto__"`, `"constructor"`, `"toString"`) + * resolves as an unknown state instead of reaching `Object.prototype` and handing a function or an + * object to the check digit calculation. + * + * @param {StateCode | "ZZ"} state - The state the voter id is generated for. + * @returns {string} The two digit federative union code of that state, or `"ZZ"`'s own code. + */ +const getFederativeUnion = (state: StateCode | "ZZ"): string => + typeof state === "string" && Object.hasOwn(UF_TO_VOTER_ID_CODE, state) + ? UF_TO_VOTER_ID_CODE[state] + : UF_TO_VOTER_ID_CODE.ZZ; + /** * Generates a valid random Brazilian voter id (título de eleitor). * * Uses `Math.random()` internally, so it is not cryptographically secure, do not use for security purposes. * * @param {StateCode | "ZZ"} state - Optional. The Brazilian state code to generate a voter id - * for, or `"ZZ"` for a voter id issued abroad. Defaults to `"ZZ"` when omitted or unknown. + * for, or `"ZZ"` for a voter id issued abroad. Defaults to `"ZZ"` when omitted or unknown, a key + * of the prototype chain (`"__proto__"`, `"constructor"`) and a value that is not a string + * included, so a malformed state never throws. * @returns {string} A valid 12-digit voter id string without formatting. * * @example @@ -36,7 +52,7 @@ export const generateVoterId = ( // Stryker disable next-line StringLiteral: any default other than a valid key still falls through the ?? UF_TO_VOTER_ID_CODE.ZZ lookup below, so the literal default value is unobservable. state: StateCode | "ZZ" = "ZZ", ): string => { - const federativeUnion = UF_TO_VOTER_ID_CODE[state] ?? UF_TO_VOTER_ID_CODE.ZZ; + const federativeUnion = getFederativeUnion(state); const sequentialNumber = generateRandomNumber(8); const digit1 = calculateVoterIdFirstDigit({ sequentialNumber, federativeUnion }); const digit2 = calculateVoterIdSecondDigit({ federativeUnion, firstDigit: digit1 }); From 5c8a9d424149f882ddfa077e86f9cdb4e1e984bd Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:09:16 -0300 Subject: [PATCH 70/75] docs(words): cite the articles of Lei 14.822/2024 that carry each spelling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "quatrocentos e quatorze bilhões" is in art. 2º and art. 3º of the law, not in art. 1º, and the "e" before a last group below one hundred is shown by art. 2º, III ("novecentos e trinta e um mil e oitenta e um reais"); the two citations pointed at art. 1º. --- src/_internals/constants/number-words.ts | 7 +++++-- src/_internals/number-to-words/number-to-words.ts | 10 +++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/_internals/constants/number-words.ts b/src/_internals/constants/number-words.ts index e4efd314..d68e7d43 100644 --- a/src/_internals/constants/number-words.ts +++ b/src/_internals/constants/number-words.ts @@ -3,8 +3,11 @@ * "por extenso" formatter (`convertNumberToWords`, `convertCurrencyToWords`, `convertDateToWords`). * * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2024/lei/L14822.htm - * Lei nº 14.822/2024 (Lei Orçamentária Anual de 2024), art. 1º, which spells 14 "quatorze" - * ("quatrocentos e quatorze bilhões"), the form the official Brazilian texts use; the Vocabulário + * Lei nº 14.822/2024 (Lei Orçamentária Anual de 2024), which spells 14 "quatorze" in the caput of + * art. 2º and in the caput of art. 3º, both writing the same amount out as "cinco trilhões + * quatrocentos e quatorze bilhões novecentos e dezenove milhões quatrocentos e noventa e dois mil + * novecentos e oitenta e seis reais" (the only two occurrences of the word in the law; art. 1º + * writes an amount with no 14 in it), the form the official Brazilian texts use; the Vocabulário * Ortográfico admits both "catorze" and "quatorze", and num2words' Portuguese table (below) picks * "quatorze". * @see Based on: https://github.com/savoirfairelinux/num2words/blob/master/num2words/lang_PT.py diff --git a/src/_internals/number-to-words/number-to-words.ts b/src/_internals/number-to-words/number-to-words.ts index d732de32..c92bb531 100644 --- a/src/_internals/number-to-words/number-to-words.ts +++ b/src/_internals/number-to-words/number-to-words.ts @@ -82,7 +82,9 @@ const isRoundHundred = (value: number): boolean => value % 100 === 0; * um"`, `1235` -> `"mil duzentos e trinta e cinco"`, `1045678` -> `"um milhão quarenta e cinco mil * seiscentos e setenta e oito"`. This is the spelling of the Lei Orçamentária Anual ("cinco * trilhões quinhentos e sessenta e seis bilhões duzentos e oitenta e quatro milhões oitocentos e - * dez mil trezentos e setenta e três reais", Lei 14.822/2024, art. 1º), of the salário mínimo + * dez mil trezentos e setenta e três reais", Lei 14.822/2024, art. 1º, and "novecentos e trinta e + * um mil e oitenta e um reais" for the "e" before a last group below 100, art. 2º, inciso III), of + * the salário mínimo * decrees ("mil quinhentos e dezoito reais", Decreto 12.342/2024) and of the examples in the Manual * de Redação da Presidência da República ("mil duzentos e cinquenta reais", "mil e quatrocentos * reais"). It deviates from `num2words`' pt_BR locale, which separates the groups with commas @@ -106,8 +108,10 @@ const isRoundHundred = (value: number): boolean => value % 100 === 0; * ``` * * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2024/lei/L14822.htm - * Lei nº 14.822, de 22 de janeiro de 2024 (Lei Orçamentária Anual de 2024), art. 1º: amounts written - * out with the groups separated by spaces, "e" only inside a group, and "quatorze". + * Lei nº 14.822, de 22 de janeiro de 2024 (Lei Orçamentária Anual de 2024): amounts written out with + * the groups separated by spaces and "e" only inside a group (art. 1º), "e" before the last group + * when that group is below 100 ("novecentos e trinta e um mil e oitenta e um reais", art. 2º, + * inciso III) and "quatorze" for 14 (art. 2º and art. 3º, caput). * @see Official: https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2024/decreto/D12342.htm * Decreto nº 12.342, de 30 de dezembro de 2024, art. 1º: "R$ 1.518,00 (mil quinhentos e dezoito reais)". * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/currency.py From 8cb0a290df89bfc6050827a6e63bf14c40ce9681 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:09:16 -0300 Subject: [PATCH 71/75] fix(types): keep the @deprecated tag of the 2.3.0 aliases in the declarations The ten upper-case aliases (`formatCPF`, `isValidCNPJ`, ...) were re-exported in one list, so the bundled declarations dropped their `@deprecated` tag and editors showed no strikethrough. Each alias is now its own documented constant with the same type as its target; the bundle size of every export is unchanged. --- src/index.ts | 96 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 76 insertions(+), 20 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7f618085..30921d45 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,14 @@ +import { formatCep } from "./format-cep/format-cep"; +import { formatCnpj } from "./format-cnpj/format-cnpj"; +import { formatCpf } from "./format-cpf/format-cpf"; +import { generateCnpj } from "./generate-cnpj/generate-cnpj"; +import { generateCpf } from "./generate-cpf/generate-cpf"; +import { isValidCep } from "./is-valid-cep/is-valid-cep"; +import { isValidCnpj } from "./is-valid-cnpj/is-valid-cnpj"; +import { isValidCpf } from "./is-valid-cpf/is-valid-cpf"; +import { isValidIe } from "./is-valid-ie/is-valid-ie"; +import { isValidPis } from "./is-valid-pis/is-valid-pis"; + export type { Bank } from "./_internals/constants/banks"; export type { Municipality } from "./_internals/constants/cities"; export type { State, StateCode, StateName } from "./_internals/constants/states"; @@ -240,23 +251,68 @@ export { subBusinessDays } from "./sub-business-days/sub-business-days"; * @deprecated Use `IsValidBankAccountOptions` instead. */ export type { IsValidBankAccountParams } from "./is-valid-bank-account/is-valid-bank-account"; -/** @deprecated Use `formatCep` instead. */ -export { formatCep as formatCEP } from "./format-cep/format-cep"; -/** @deprecated Use `formatCnpj` instead. */ -export { formatCnpj as formatCNPJ } from "./format-cnpj/format-cnpj"; -/** @deprecated Use `formatCpf` instead. */ -export { formatCpf as formatCPF } from "./format-cpf/format-cpf"; -/** @deprecated Use `generateCnpj` instead. */ -export { generateCnpj as generateCNPJ } from "./generate-cnpj/generate-cnpj"; -/** @deprecated Use `generateCpf` instead. */ -export { generateCpf as generateCPF } from "./generate-cpf/generate-cpf"; -/** @deprecated Use `isValidCep` instead. */ -export { isValidCep as isValidCEP } from "./is-valid-cep/is-valid-cep"; -/** @deprecated Use `isValidCnpj` instead. */ -export { isValidCnpj as isValidCNPJ } from "./is-valid-cnpj/is-valid-cnpj"; -/** @deprecated Use `isValidCpf` instead. */ -export { isValidCpf as isValidCPF } from "./is-valid-cpf/is-valid-cpf"; -/** @deprecated Use `isValidIe` instead. */ -export { isValidIe as isValidIE } from "./is-valid-ie/is-valid-ie"; -/** @deprecated Use `isValidPis` instead. */ -export { isValidPis as isValidPIS } from "./is-valid-pis/is-valid-pis"; +// The deprecated aliases below are declared as constants rather than as renamed re-exports +// (`export { formatCpf as formatCPF }`) so that their `@deprecated` tag survives into the bundled +// declaration file: the bundler collapses every renamed re-export of the entry point into a single +// `export { ... }` statement, which carries no documentation, while a `declare const` keeps the +// comment written right above it. +/** + * Formats a CEP, the 1.x name of `formatCep`. + * + * @deprecated Use `formatCep` instead. + */ +export const formatCEP: typeof formatCep = formatCep; +/** + * Formats a CNPJ, the 1.x name of `formatCnpj`. + * + * @deprecated Use `formatCnpj` instead. + */ +export const formatCNPJ: typeof formatCnpj = formatCnpj; +/** + * Formats a CPF, the 1.x name of `formatCpf`. + * + * @deprecated Use `formatCpf` instead. + */ +export const formatCPF: typeof formatCpf = formatCpf; +/** + * Generates a valid random CNPJ, the 1.x name of `generateCnpj`. + * + * @deprecated Use `generateCnpj` instead. + */ +export const generateCNPJ: typeof generateCnpj = generateCnpj; +/** + * Generates a valid random CPF, the 1.x name of `generateCpf`. + * + * @deprecated Use `generateCpf` instead. + */ +export const generateCPF: typeof generateCpf = generateCpf; +/** + * Checks whether a CEP is valid, the 1.x name of `isValidCep`. + * + * @deprecated Use `isValidCep` instead. + */ +export const isValidCEP: typeof isValidCep = isValidCep; +/** + * Checks whether a CNPJ is valid, the 1.x name of `isValidCnpj`. + * + * @deprecated Use `isValidCnpj` instead. + */ +export const isValidCNPJ: typeof isValidCnpj = isValidCnpj; +/** + * Checks whether a CPF is valid, the 1.x name of `isValidCpf`. + * + * @deprecated Use `isValidCpf` instead. + */ +export const isValidCPF: typeof isValidCpf = isValidCpf; +/** + * Checks whether a state registration (inscrição estadual) is valid, the 1.x name of `isValidIe`. + * + * @deprecated Use `isValidIe` instead. + */ +export const isValidIE: typeof isValidIe = isValidIe; +/** + * Checks whether a PIS/PASEP is valid, the 1.x name of `isValidPis`. + * + * @deprecated Use `isValidPis` instead. + */ +export const isValidPIS: typeof isValidPis = isValidPis; From 0ed0edbfb82820b72b23fb8ed1f08c4d73b06f9d Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:09:17 -0300 Subject: [PATCH 72/75] build(api): commit the API Extractor baseline so check:api fails on a change The `reports` folder was ignored, so the API report was recreated on every run and `check:api` could never fail. The baseline is committed, `check:api` runs without `--local` (a changed or missing report fails the check) and `check:api:update` regenerates it; the report is excluded from the formatter, which was rewriting its code block. --- .gitignore | 8 +- CONTRIBUTING.md | 57 +- package.json | 3 +- reports/api/brazilian-utils.api.md | 1113 ++++++++++++++++++++++++++++ vite.config.ts | 4 +- 5 files changed, 1156 insertions(+), 29 deletions(-) create mode 100644 reports/api/brazilian-utils.api.md diff --git a/.gitignore b/.gitignore index 895e7d16..7465c7a3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,10 @@ node_modules dist coverage .stryker-tmp -reports +# Every report is generated (stryker, coverage, jscpd) except the API Extractor baseline, which is +# committed so that `npm run check:api` compares the public API against the reviewed one instead of +# writing a new file on every run. +reports/* +!reports/api/ +reports/api/* +!reports/api/brazilian-utils.api.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7772a12..cc31214e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,28 +28,29 @@ and is invoked through the `npm` scripts below, so you don't need to install any ### Useful scripts -| Command | What it does | -| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `npm run check` | Runs `vp check`: format check, lint and type-check together. Run this before opening a PR. | -| `npm run check:fix` | Same as above, but auto-fixes what it can. | -| `npm run format` / `npm run format:check` | Formats the codebase / checks formatting with `vp fmt`. | -| `npm run lint` / `npm run lint:fix` | Lints the codebase with `vp lint`. | -| `npm run test` | Runs the unit test suite with `vp test`. | -| `npm run test:coverage` | Runs tests with coverage (`vp test run --coverage`). | -| `npm run test:bun` | Runs the test suite on [Bun](https://bun.sh) (`bun test src`). | -| `npm run test:deno` | Runs the test suite on [Deno](https://deno.com) (`deno test`). | -| `npm run test:live` | Runs the live CEP-provider test against the real network (`RUN_LIVE_CEP_TESTS=1 vp test src/get-address-info-by-cep/get-address-info-by-cep.test.ts`); not part of the regular test run, only of the scheduled `Live tests` workflow. | -| `npm run test:chrome-browser`, `npm run test:firefox-browser`, `npm run test:edge-browser`, `npm run test:safari-browser` | Runs the test suite in real browsers via `vp test --browser.enabled`. | -| `npm run build` | Builds the library for publishing with `vp pack` (also runs attw and publint over the built output). | -| `npm run build:data` | Regenerates the datasets under `src/_internals/constants` from the IBGE/CONCLA sources (`scripts/data.ts`); run by the scheduled `Update datasets` workflow. | -| `npm run build:llms` | Regenerates `docs/llms.txt` and `docs/llms-full.txt` from the docs (`scripts/llms.ts`); CI fails if they're out of date. | -| `npm run check:dependencies` | Fails if `package.json` declares any runtime `dependencies` (this package ships zero by design). | -| `npm run check:duplication` | Runs [jscpd](https://jscpd.dev) over `src` and `scripts`; any copy-pasted block of 5+ lines / 50+ tokens fails. | -| `npm run check:unused` | Runs [knip](https://knip.dev): unused files, exports, types and dependencies fail. | -| `npm run test:mutation` | Runs [Stryker](https://stryker-mutator.io) mutation tests (`stryker run`); pass `-- --mutate src//.ts` for one file. | -| `npm run check:api` | Builds the package and runs API Extractor over `dist/brazilian-utils.d.ts`: a public type without a doc comment, or a type the API refers to without exporting, fails. | -| `npm run check:commits` | Checks the commit messages since `origin/main` with commitlint (Conventional Commits). | -| `npm run check:lockfile` | Checks `package-lock.json` only resolves to the npm registry over HTTPS with integrity hashes (lockfile-lint). | +| Command | What it does | +| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `npm run check` | Runs `vp check`: format check, lint and type-check together. Run this before opening a PR. | +| `npm run check:fix` | Same as above, but auto-fixes what it can. | +| `npm run format` / `npm run format:check` | Formats the codebase / checks formatting with `vp fmt`. | +| `npm run lint` / `npm run lint:fix` | Lints the codebase with `vp lint`. | +| `npm run test` | Runs the unit test suite with `vp test`. | +| `npm run test:coverage` | Runs tests with coverage (`vp test run --coverage`). | +| `npm run test:bun` | Runs the test suite on [Bun](https://bun.sh) (`bun test src`). | +| `npm run test:deno` | Runs the test suite on [Deno](https://deno.com) (`deno test`). | +| `npm run test:live` | Runs the live CEP-provider test against the real network (`RUN_LIVE_CEP_TESTS=1 vp test src/get-address-info-by-cep/get-address-info-by-cep.test.ts`); not part of the regular test run, only of the scheduled `Live tests` workflow. | +| `npm run test:chrome-browser`, `npm run test:firefox-browser`, `npm run test:edge-browser`, `npm run test:safari-browser` | Runs the test suite in real browsers via `vp test --browser.enabled`. | +| `npm run build` | Builds the library for publishing with `vp pack` (also runs attw and publint over the built output). | +| `npm run build:data` | Regenerates the datasets under `src/_internals/constants` from the IBGE/CONCLA sources (`scripts/data.ts`); run by the scheduled `Update datasets` workflow. | +| `npm run build:llms` | Regenerates `docs/llms.txt` and `docs/llms-full.txt` from the docs (`scripts/llms.ts`); CI fails if they're out of date. | +| `npm run check:dependencies` | Fails if `package.json` declares any runtime `dependencies` (this package ships zero by design). | +| `npm run check:duplication` | Runs [jscpd](https://jscpd.dev) over `src` and `scripts`; any copy-pasted block of 5+ lines / 50+ tokens fails. | +| `npm run check:unused` | Runs [knip](https://knip.dev): unused files, exports, types and dependencies fail. | +| `npm run test:mutation` | Runs [Stryker](https://stryker-mutator.io) mutation tests (`stryker run`); pass `-- --mutate src//.ts` for one file. | +| `npm run check:api` | Builds the package and runs API Extractor over `dist/brazilian-utils.d.ts`: a public type without a doc comment, a type the API refers to without exporting, or a public signature that differs from the committed baseline `reports/api/brazilian-utils.api.md`, fails. | +| `npm run check:api:update` | Rewrites the committed API Extractor baseline `reports/api/brazilian-utils.api.md` from the current build; run it when a public signature changes on purpose and commit the new report. | +| `npm run check:commits` | Checks the commit messages since `origin/main` with commitlint (Conventional Commits). | +| `npm run check:lockfile` | Checks `package-lock.json` only resolves to the npm registry over HTTPS with integrity hashes (lockfile-lint). | Before opening a pull request, make sure `npm run check` and `npm run test` both pass locally. If your change touches runtime behavior, also consider running the Bun/Deno scripts above. The library is @@ -220,10 +221,14 @@ pull request so the CI result is not a surprise. [API Extractor](https://api-extractor.com) runs over the bundled `dist/brazilian-utils.d.ts` in CI (`npm run check:api`). It fails when a type the public API refers to is not itself exported (a -consumer could not name it) and when an exported function, type or class has no doc comment. The -report it writes lands in the ignored `reports/api/` folder and is not committed: the public -signatures are pinned by the `describe(" types")` blocks in the tests, and the -`src/index.test.ts` export map catches an export that goes missing. +consumer could not name it) and when an exported function, type or class has no doc comment. It +also compares the public API against the reviewed baseline committed at +`reports/api/brazilian-utils.api.md` (the only file of the ignored `reports/` folder that is +committed) and fails when the two differ, so a change to a public signature has to be reviewed in +the diff of that report: run `npm run check:api:update` to write the new baseline and commit it +along with the change. On top of that, the public signatures are pinned by the +`describe(" types")` blocks in the tests, and the `src/index.test.ts` export map catches an +export that goes missing. ## Supply chain diff --git a/package.json b/package.json index 538f29a9..c42ed391 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,8 @@ "check:duplication": "jscpd", "check:unused": "knip", "test:mutation": "stryker run", - "check:api": "npm run build && api-extractor run --local --verbose", + "check:api": "npm run build && api-extractor run --verbose", + "check:api:update": "npm run build && api-extractor run --local --verbose", "check:commits": "commitlint --from origin/main --to HEAD --verbose", "check:lockfile": "lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https --validate-integrity", "build:data": "node ./scripts/data.ts", diff --git a/reports/api/brazilian-utils.api.md b/reports/api/brazilian-utils.api.md new file mode 100644 index 00000000..58601ba1 --- /dev/null +++ b/reports/api/brazilian-utils.api.md @@ -0,0 +1,1113 @@ +## API Report File for "@brazilian-utils/brazilian-utils" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public +export const addBusinessDays: (date: Date, amount: number, options?: BusinessDayOptions) => Date | null; + +// @public +export type AddressInfo = { + cep: string; + state: string; + city: string; + neighborhood: string; + street: string; +}; + +// @public +export type AreaCodeInfo = { + areaCode: number; + stateCode: StateCode; + stateName: StateName; + regionCode: State["regionCode"]; + regionName: State["regionName"]; + stateCodes: StateCode[]; +}; + +// @public +export type Bank = { + code: string; + ispb: string; + name: string; +}; + +// @public +export type BoletoInfo = { + amount: number; + expirationDate: Date | null; + bankCode: string; + type?: "arrecadacao"; + segment?: number; + value?: number; + hasEffectiveValue?: boolean; +}; + +// @public +export type BusinessDayOptions = { + stateCode?: StateCode; + includeOptional?: boolean; +}; + +// @public +export const capitalize: (value: string, options?: CapitalizeOptions) => string; + +// @public +export type CapitalizeOptions = { + lowerCaseWords?: string[]; + upperCaseWords?: string[]; +}; + +// @public +export type Cbo = { + code: string; + description: string; +}; + +// @public +export type CepAddressInfo = { + cep: string; + logradouro: string; + complemento: string; + unidade?: string; + bairro: string; + localidade: string; + uf: string; + estado?: string; + regiao?: string; + ibge?: string; + gia?: string; + ddd?: string; + siafi?: string; +}; + +// @public +export type CepProvider = "viacep" | "widenet" | "brasilapi"; + +// @public +export type CertidaoInfo = { + registryCns: string; + acervo: string; + service: string; + year: number; + type: CertidaoType; + typeCode: number; + book: string; + page: string; + term: string; + checkDigits: string; +}; + +// @public +export type CertidaoType = "birth" | "marriage" | "religious-marriage" | "death" | "stillbirth" | "banns" | "other" | "emancipation" | "interdiction"; + +// @public +export type Cfop = { + code: string; + description: string; +}; + +// @public +export type Cnae = { + code: string; + description: string; +}; + +// @public +export const convertCurrencyToWords: (value: number) => string; + +// @public +export const convertDateToWords: (value: Date | string, options?: ConvertDateToWordsOptions) => string; + +// @public +export type ConvertDateToWordsOptions = { + style?: "full" | "month"; + weekday?: boolean; +}; + +// @public +export const convertLicensePlateToMercosul: (value: string) => string; + +// @public +export const convertNumberToWords: (value: number, options?: ConvertNumberToWordsOptions) => string; + +// @public +export type ConvertNumberToWordsOptions = { + gender?: NumberToWordsGender; +}; + +// @public +export const differenceInBusinessDays: (laterDate: Date, earlierDate: Date, options?: BusinessDayOptions) => number | null; + +// @public +export const formatBoleto: (value: string | number, options?: FormatBoletoOptions) => string; + +// @public +export type FormatBoletoOptions = { + pad?: boolean; +}; + +// @public +export const formatCaepf: (value: string | number, options?: FormatCaepfOptions) => string; + +// @public +export type FormatCaepfOptions = { + pad?: boolean; +}; + +// @public +export const formatCei: (value: string | number, options?: FormatCeiOptions) => string; + +// @public +export type FormatCeiOptions = { + pad?: boolean; +}; + +// @public @deprecated +export const formatCEP: typeof formatCep; + +// @public +export const formatCep: (value: string | number, options?: FormatCepOptions) => string; + +// @public +export type FormatCepOptions = { + pad?: boolean; +}; + +// @public +export const formatCertidao: (value: string | number, options?: FormatCertidaoOptions) => string; + +// @public +export type FormatCertidaoOptions = { + pad?: boolean; +}; + +// @public +export const formatCnae: (value: string | number, options?: FormatCnaeOptions) => string; + +// @public +export type FormatCnaeOptions = { + pad?: boolean; +}; + +// @public +export const formatCnh: (value: string | number, options?: FormatCnhOptions) => string; + +// @public +export type FormatCnhOptions = { + pad?: boolean; +}; + +// @public +export const formatCno: (value: string | number, options?: FormatCnoOptions) => string; + +// @public +export type FormatCnoOptions = { + pad?: boolean; +}; + +// @public @deprecated +export const formatCNPJ: typeof formatCnpj; + +// @public +export const formatCnpj: (value: string | number, options?: FormatCnpjOptions) => string; + +// @public +export type FormatCnpjOptions = { + pad?: boolean; + version?: 1 | 2; + obfuscate?: boolean; +}; + +// @public +export const formatCns: (value: string | number, options?: FormatCnsOptions) => string; + +// @public +export type FormatCnsOptions = { + pad?: boolean; +}; + +// @public @deprecated +export const formatCPF: typeof formatCpf; + +// @public +export const formatCpf: (value: string | number, options?: FormatCpfOptions) => string; + +// @public +export type FormatCpfOptions = { + pad?: boolean; + obfuscate?: boolean; +}; + +// @public +export const formatCurrency: (value: string | number, options?: FormatCurrencyOptions) => string; + +// @public +export type FormatCurrencyOptions = { + symbol?: boolean; + precision?: number; +}; + +// @public +export const formatIban: (value: string) => string; + +// @public +export const formatLegalNature: (value: string | number, options?: FormatLegalNatureOptions) => string; + +// @public +export type FormatLegalNatureOptions = { + pad?: boolean; +}; + +// @public +export const formatLicensePlate: (value: string) => string; + +// @public +export const formatNcm: (value: string | number, options?: FormatNcmOptions) => string; + +// @public +export type FormatNcmOptions = { + pad?: boolean; +}; + +// @public +export const formatNfeKey: (value: string, options?: FormatNfeKeyOptions) => string; + +// @public +export type FormatNfeKeyOptions = { + pad?: boolean; +}; + +// @public +export const formatPassport: (passport: string) => string; + +// @public +export const formatPhone: (value: string | number, options?: FormatPhoneOptions) => string; + +// @public +export type FormatPhoneOptions = { + mask?: PhoneMask; +}; + +// @public +export const formatPis: (value: string | number, options?: FormatPisOptions) => string; + +// @public +export type FormatPisOptions = { + pad?: boolean; +}; + +// @public +export const formatProcessoJuridico: (value: string | number, options?: FormatProcessoJuridicoOptions) => string; + +// @public +export type FormatProcessoJuridicoOptions = { + pad?: boolean; +}; + +// @public +export const formatVoterId: (value: string | number) => string; + +// @public +export const generateBoleto: (options?: GenerateBoletoOptions) => string; + +// @public +export type GenerateBoletoOptions = { + type?: "bancario" | "arrecadacao"; +}; + +// @public +export const generateCep: () => string; + +// @public +export const generateCnh: () => string; + +// @public @deprecated +export const generateCNPJ: typeof generateCnpj; + +// @public +export const generateCnpj: (versionOrOptions?: 1 | 2 | GenerateCnpjOptions) => string; + +// @public +export type GenerateCnpjOptions = { + version?: 1 | 2; + branch?: number; +}; + +// @public @deprecated +export const generateCPF: typeof generateCpf; + +// @public +export const generateCpf: (state?: StateCode) => string; + +// @public +export const generateLegalNature: () => string; + +// @public +export const generateLicensePlate: (format?: GenerateLicensePlateFormat) => string; + +// @public +export type GenerateLicensePlateFormat = LicensePlateFormat; + +// @public +export const generatePassport: () => string; + +// @public +export const generatePhone: (type?: GeneratePhoneType) => string; + +// @public +export type GeneratePhoneType = "mobile" | "landline" | "service"; + +// @public +export const generatePis: () => string; + +// @public +export const generatePixPayload: (params: GeneratePixPayloadOptions) => string | null; + +// @public +export type GeneratePixPayloadOptions = { + key?: string; + url?: string; + merchantName: string; + merchantCity: string; + amount?: number; + txid?: string; + description?: string; +}; + +// @public +export const generateProcessoJuridico: (options?: GenerateProcessoJuridicoOptions) => string | null; + +// @public +export type GenerateProcessoJuridicoOptions = { + year?: number; + court?: number; +}; + +// @public +export const generateRenavam: () => string; + +// @public +export const generateVoterId: (state?: StateCode | "ZZ") => string; + +// @public +export const getAddressInfoByCep: (cep: string | number, options?: GetAddressInfoByCepOptions) => Promise; + +// @public +export class GetAddressInfoByCepError extends Error { + constructor(message: string); +} + +// @public +export class GetAddressInfoByCepNotFoundError extends GetAddressInfoByCepError { + constructor(message: string); +} + +// @public +export type GetAddressInfoByCepOptions = { + providers?: CepProvider[]; +}; + +// @public +export class GetAddressInfoByCepServiceError extends GetAddressInfoByCepError { + constructor(message: string); +} + +// @public +export class GetAddressInfoByCepValidationError extends GetAddressInfoByCepError { + constructor(message: string); +} + +// @public +export const getAreaCodeInfo: (areaCode: string | number) => AreaCodeInfo | null; + +// @public +export const getAreaCodesByState: (stateCode: string) => number[]; + +// @public +export const getBankByCode: (code: string | number) => Bank | null; + +// @public +export const getBankByIspb: (value: string | number) => Bank | null; + +// @public +export const getBanks: () => Bank[]; + +// @public +export const getBoletoInfo: (value: string, options?: GetBoletoInfoOptions) => BoletoInfo | null; + +// @public +export type GetBoletoInfoOptions = { + referenceDate?: Date; +}; + +// @public +export const getCbo: (value: string | number) => Cbo | null; + +// @public +export const getCepInfoByAddress: (params: GetCepInfoByAddressOptions) => Promise; + +// @public +export class GetCepInfoByAddressError extends Error { + constructor(message: string); +} + +// @public +export class GetCepInfoByAddressNotFoundError extends GetCepInfoByAddressError { + constructor(message: string); +} + +// @public +export type GetCepInfoByAddressOptions = { + federalUnit: string; + city: string; + street: string; +}; + +// @public +export class GetCepInfoByAddressValidationError extends GetCepInfoByAddressError { + constructor(message: string); +} + +// @public +export const getCertidaoInfo: (value: string) => CertidaoInfo | null; + +// @public +export const getCfop: (value: string | number) => Cfop | null; + +// @public @deprecated +export const getCities: (state?: StateCode) => string[]; + +// @public +export const getCnae: (value: string | number) => Cnae | null; + +// @public +export const getFormatLicensePlate: (value: string) => LicensePlateFormat | null; + +// @public +export function getHolidays(year: number): Holiday[]; + +// @public +export function getHolidays(options: GetHolidaysOptions): Holiday[]; + +// @public +export type GetHolidaysOptions = { + year: number; + stateCode?: StateCode; +}; + +// @public +export const getIbanInfo: (value: string) => IbanInfo | null; + +// @public +export const getLegalNature: (value: string | number) => LegalNature | null; + +// @public +export const getLegalNatures: () => Record; + +// @public +export const getLegalNaturesByCategory: (category: string | number) => LegalNature[]; + +// @public +export const getMunicipalities: (stateCode?: StateCode) => Municipality[]; + +// @public @deprecated +export function getMunicipality(options: GetMunicipalityByCodeOptions): Promise<[string, string] | null>; + +// @public @deprecated +export function getMunicipality(options: GetMunicipalityByNameOptions): Promise; + +// @public @deprecated +export function getMunicipality(options: GetMunicipalityOptions): Promise<[string, string] | string | null>; + +// @public +export const getMunicipalityByCode: (code: string | number) => Municipality | null; + +// @public +export type GetMunicipalityByCodeOptions = { + code: string | number; +}; + +// @public +export type GetMunicipalityByNameOptions = { + municipalityName: string; + uf: string; +}; + +// @public +export type GetMunicipalityOptions = GetMunicipalityByCodeOptions | GetMunicipalityByNameOptions; + +// @public +export const getNfeKeyInfo: (value: string) => NfeKeyInfo | null; + +// @public +export const getPixKeyInfo: (value: string) => PixKeyInfo | null; + +// @public +export const getPixPayloadInfo: (value: string) => PixPayloadInfo | null; + +// @public +export const getStateByIbgeCode: (code: string | number) => State | null; + +// @public +export const getStateCodeByName: (name: string) => StateCode | null; + +// @public +export const getStateNameByCode: (code: string) => StateName | null; + +// @public +export const getStates: () => State[]; + +// @public +export const getTimezoneByState: (stateCode: string) => string | null; + +// @public +export type Holiday = { + name: string; + date: Date; + type: HolidayType; +}; + +// @public +export type HolidayType = "national" | "state" | "optional" | "religious"; + +// @public +export type IbanInfo = { + countryCode: "BR"; + checkDigits: string; + bankIspb: string; + branch: string; + account: string; + accountType: string; + owner: string; +}; + +// @public +export const isBusinessDay: (value: Date, options?: BusinessDayOptions) => boolean; + +// @public +export const isHoliday: (options?: IsHolidayOptions) => boolean; + +// @public +export type IsHolidayOptions = { + targetDate: Date; + stateCode?: StateCode; +}; + +// @public +export const isValidBankAccount: (params: IsValidBankAccountOptions) => boolean; + +// @public +export type IsValidBankAccountOptions = { + bankCode: string; + agency: string; + account: string; + digit: string; +}; + +// @public @deprecated +export type IsValidBankAccountParams = IsValidBankAccountOptions; + +// @public +export const isValidBoleto: (value: string) => boolean; + +// @public +export const isValidCaepf: (value: string | number) => boolean; + +// @public +export const isValidCbo: (value: string | number) => boolean; + +// @public +export const isValidCei: (value: string | number) => boolean; + +// @public @deprecated +export const isValidCEP: typeof isValidCep; + +// @public +export const isValidCep: (cep: string | number) => boolean; + +// @public +export const isValidCertidao: (value: string, options?: IsValidCertidaoOptions) => boolean; + +// @public +export type IsValidCertidaoOptions = { + accept?: CertidaoType[]; +}; + +// @public +export const isValidCfop: (value: string | number) => boolean; + +// @public +export const isValidCnae: (value: string | number) => boolean; + +// @public +export const isValidCnh: (value: string) => boolean; + +// @public +export const isValidCno: (value: string | number) => boolean; + +// @public @deprecated +export const isValidCNPJ: typeof isValidCnpj; + +// @public +export const isValidCnpj: (cnpj: string, options?: IsValidCnpjOptions) => boolean; + +// @public +export type IsValidCnpjOptions = { + version?: 1 | 2; +}; + +// @public +export const isValidCns: (value: string | number) => boolean; + +// @public @deprecated +export const isValidCPF: typeof isValidCpf; + +// @public +export const isValidCpf: (cpf: string) => boolean; + +// @public +export const isValidCreditCard: (value: string | number) => boolean; + +// @public +export const isValidCsosn: (value: string | number) => boolean; + +// @public +export const isValidCst: (value: string | number, options?: IsValidCstOptions) => boolean; + +// @public +export type IsValidCstOptions = { + tax?: "icms" | "ipi" | "pis" | "cofins"; +}; + +// @public +export const isValidEmail: (value: string) => boolean; + +// @public +export const isValidIban: (value: string) => boolean; + +// @public @deprecated +export const isValidIE: typeof isValidIe; + +// @public +export const isValidIe: (stateCode: StateCode, ie: string) => boolean; + +// @public +export const isValidLandlinePhone: (value: string) => boolean; + +// @public +export const isValidLegalNature: (code: string) => boolean; + +// @public +export const isValidLicensePlate: (value: string) => boolean; + +// @public +export const isValidMobilePhone: (value: string, options?: IsValidMobilePhoneOptions) => boolean; + +// @public +export type IsValidMobilePhoneOptions = { + version?: PhoneVersion; +}; + +// @public +export const isValidNcm: (value: string | number) => boolean; + +// @public +export const isValidNfeKey: (value: string) => boolean; + +// @public +export const isValidPassport: (passport: string | number) => boolean; + +// @public +export const isValidPhone: (value: string, options?: IsValidPhoneOptions) => boolean; + +// @public +export type IsValidPhoneOptions = { + version?: PhoneVersion; + accept?: PhoneType[]; +}; + +// @public @deprecated +export const isValidPIS: typeof isValidPis; + +// @public +export const isValidPis: (pis: string) => boolean; + +// @public +export const isValidPixKey: (value: string, options?: IsValidPixKeyOptions) => boolean; + +// @public +export type IsValidPixKeyOptions = { + accept?: PixKeyType[]; +}; + +// @public +export const isValidPixPayload: (value: string) => boolean; + +// @public +export const isValidProcessoJuridico: (value: string) => boolean; + +// @public +export const isValidRegistroProfissional: (params: IsValidRegistroProfissionalOptions) => boolean; + +// @public +export type IsValidRegistroProfissionalOptions = { + value: string; + council: RegistroProfissionalCouncil; + stateCode?: StateCode; +}; + +// @public +export const isValidRenavam: (renavam: string | number) => boolean; + +// @public +export const isValidServicePhone: (value: string) => boolean; + +// @public +export const isValidVin: (value: string) => boolean; + +// @public +export const isValidVoterId: (value: string) => boolean; + +// @public +export type LegalNature = { + code: string; + description: string; + category: LegalNatureCategory; +}; + +// @public +export type LegalNatureCategory = { + code: "1" | "2" | "3" | "4" | "5"; + description: string; +}; + +// @public +export type LicensePlateFormat = "LLLNNNN" | "LLLNLNN"; + +// @public +export type Municipality = { + code: string; + name: string; + stateCode: StateCode; +}; + +// @public +export type NfeKeyInfo = { + stateCode: StateCode; + year: number; + month: number; + taxId: string; + model: NfeKeyModel; + series: number; + number: number; + emissionType: number; + authorizationSite?: number; + code: string; + checkDigit: number; +}; + +// @public +export type NfeKeyModel = "55" | "57" | "58" | "62" | "63" | "64" | "65" | "66" | "67"; + +// @public +export type NumberToWordsGender = "masculine" | "feminine"; + +// @public +export const parseBoleto: (value: string | number) => string; + +// @public +export const parseCaepf: (value: string | number) => string; + +// @public +export const parseCbo: (value: string | number) => string; + +// @public +export const parseCei: (value: string | number) => string; + +// @public +export const parseCep: (value: string | number) => string; + +// @public +export const parseCertidao: (value: string | number) => string; + +// @public +export const parseCfop: (value: string | number) => string; + +// @public +export const parseCnae: (value: string | number) => string; + +// @public +export const parseCnh: (value: string | number) => string; + +// @public +export const parseCno: (value: string | number) => string; + +// @public +export const parseCnpj: (value: string | number, options?: ParseCnpjOptions) => string; + +// @public +export type ParseCnpjOptions = Pick; + +// @public +export const parseCns: (value: string | number) => string; + +// @public +export const parseCpf: (value: string | number) => string; + +// @public +export const parseCurrency: (value: string, options?: ParseCurrencyOptions) => number; + +// @public +export type ParseCurrencyOptions = { + precision?: number; +}; + +// @public +export const parseIban: (value: string | number) => string; + +// @public +export const parseLegalNature: (value: string | number) => string; + +// @public +export const parseLicensePlate: (value: string) => string; + +// @public +export const parseNcm: (value: string | number) => string; + +// @public +export const parseNfeKey: (value: string | number) => string; + +// @public +export const parsePassport: (passport: string) => string; + +// @public +export const parsePhone: (value: string | number) => string; + +// @public +export const parsePis: (value: string | number) => string; + +// @public +export const parseProcessoJuridico: (value: string | number) => string; + +// @public +export const parseVoterId: (value: string | number) => string; + +// @public +export type PhoneMask = "auto" | "e164" | "international" | "service" | "sn" | "nanp"; + +// @public +export type PhoneType = "mobile" | "landline" | "service"; + +// @public +export type PhoneVersion = 1 | 2; + +// @public +export type PixKeyInfo = { + type: PixKeyType; + value: string; +}; + +// @public +export type PixKeyType = "cpf" | "cnpj" | "email" | "phone" | "evp"; + +// @public +export type PixPayloadInfo = { + key?: string; + url?: string; + description?: string; + withdrawalFacilitator?: string; + merchantName: string; + merchantCity: string; + amount?: number; + txid?: string; + pointOfInitiation: PixPointOfInitiation; +}; + +// @public +export type PixPointOfInitiation = "static" | "dynamic"; + +// @public +export type RegistroProfissionalCouncil = "OAB" | "CRM" | "CRO" | "CRP" | "CRC"; + +// @public +export const removeAccents: (value: string) => string; + +// @public +export type State = { + readonly code: "AC"; + readonly name: "Acre"; + readonly regionCode: "N"; + readonly regionName: "Norte"; + readonly ibgeCode: 12; +} | { + readonly code: "AL"; + readonly name: "Alagoas"; + readonly regionCode: "NE"; + readonly regionName: "Nordeste"; + readonly ibgeCode: 27; +} | { + readonly code: "AP"; + readonly name: "Amapá"; + readonly regionCode: "N"; + readonly regionName: "Norte"; + readonly ibgeCode: 16; +} | { + readonly code: "AM"; + readonly name: "Amazonas"; + readonly regionCode: "N"; + readonly regionName: "Norte"; + readonly ibgeCode: 13; +} | { + readonly code: "BA"; + readonly name: "Bahia"; + readonly regionCode: "NE"; + readonly regionName: "Nordeste"; + readonly ibgeCode: 29; +} | { + readonly code: "CE"; + readonly name: "Ceará"; + readonly regionCode: "NE"; + readonly regionName: "Nordeste"; + readonly ibgeCode: 23; +} | { + readonly code: "DF"; + readonly name: "Distrito Federal"; + readonly regionCode: "CO"; + readonly regionName: "Centro-Oeste"; + readonly ibgeCode: 53; +} | { + readonly code: "ES"; + readonly name: "Espírito Santo"; + readonly regionCode: "SE"; + readonly regionName: "Sudeste"; + readonly ibgeCode: 32; +} | { + readonly code: "GO"; + readonly name: "Goiás"; + readonly regionCode: "CO"; + readonly regionName: "Centro-Oeste"; + readonly ibgeCode: 52; +} | { + readonly code: "MA"; + readonly name: "Maranhão"; + readonly regionCode: "NE"; + readonly regionName: "Nordeste"; + readonly ibgeCode: 21; +} | { + readonly code: "MT"; + readonly name: "Mato Grosso"; + readonly regionCode: "CO"; + readonly regionName: "Centro-Oeste"; + readonly ibgeCode: 51; +} | { + readonly code: "MS"; + readonly name: "Mato Grosso do Sul"; + readonly regionCode: "CO"; + readonly regionName: "Centro-Oeste"; + readonly ibgeCode: 50; +} | { + readonly code: "MG"; + readonly name: "Minas Gerais"; + readonly regionCode: "SE"; + readonly regionName: "Sudeste"; + readonly ibgeCode: 31; +} | { + readonly code: "PA"; + readonly name: "Pará"; + readonly regionCode: "N"; + readonly regionName: "Norte"; + readonly ibgeCode: 15; +} | { + readonly code: "PB"; + readonly name: "Paraíba"; + readonly regionCode: "NE"; + readonly regionName: "Nordeste"; + readonly ibgeCode: 25; +} | { + readonly code: "PR"; + readonly name: "Paraná"; + readonly regionCode: "S"; + readonly regionName: "Sul"; + readonly ibgeCode: 41; +} | { + readonly code: "PE"; + readonly name: "Pernambuco"; + readonly regionCode: "NE"; + readonly regionName: "Nordeste"; + readonly ibgeCode: 26; +} | { + readonly code: "PI"; + readonly name: "Piauí"; + readonly regionCode: "NE"; + readonly regionName: "Nordeste"; + readonly ibgeCode: 22; +} | { + readonly code: "RJ"; + readonly name: "Rio de Janeiro"; + readonly regionCode: "SE"; + readonly regionName: "Sudeste"; + readonly ibgeCode: 33; +} | { + readonly code: "RN"; + readonly name: "Rio Grande do Norte"; + readonly regionCode: "NE"; + readonly regionName: "Nordeste"; + readonly ibgeCode: 24; +} | { + readonly code: "RS"; + readonly name: "Rio Grande do Sul"; + readonly regionCode: "S"; + readonly regionName: "Sul"; + readonly ibgeCode: 43; +} | { + readonly code: "RO"; + readonly name: "Rondônia"; + readonly regionCode: "N"; + readonly regionName: "Norte"; + readonly ibgeCode: 11; +} | { + readonly code: "RR"; + readonly name: "Roraima"; + readonly regionCode: "N"; + readonly regionName: "Norte"; + readonly ibgeCode: 14; +} | { + readonly code: "SC"; + readonly name: "Santa Catarina"; + readonly regionCode: "S"; + readonly regionName: "Sul"; + readonly ibgeCode: 42; +} | { + readonly code: "SP"; + readonly name: "São Paulo"; + readonly regionCode: "SE"; + readonly regionName: "Sudeste"; + readonly ibgeCode: 35; +} | { + readonly code: "SE"; + readonly name: "Sergipe"; + readonly regionCode: "NE"; + readonly regionName: "Nordeste"; + readonly ibgeCode: 28; +} | { + readonly code: "TO"; + readonly name: "Tocantins"; + readonly regionCode: "N"; + readonly regionName: "Norte"; + readonly ibgeCode: 17; +}; + +// @public +export type StateCode = State["code"]; + +// @public +export type StateName = State["name"]; + +// @public +export const subBusinessDays: (date: Date, amount: number, options?: BusinessDayOptions) => Date | null; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/vite.config.ts b/vite.config.ts index 027e6ace..9025502c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -145,7 +145,9 @@ const sharedPack = { export default defineConfig({ fmt: { - ignorePatterns: ["dist", "coverage", "docs", ".claude"], + // `reports` holds generated output only, the committed API Extractor baseline included: + // reformatting its code block would make every `check:api` run report a changed API. + ignorePatterns: ["dist", "coverage", "docs", "reports", ".stryker-tmp", ".claude"], singleQuote: false, sortImports: true, useTabs: true, From 8368612fe98e5ae6ea8f4ff7e07b226833099154 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:09:18 -0300 Subject: [PATCH 73/75] build(data): make an unchanged Bacen list a no-op and format before the checks `scripts/banks.ts` read "the replaced text equals the original" as "pattern not found" and failed whenever the STR list had not changed; it now tests the pattern before replacing. The dataset runner formats the regenerated files before any failure check, so a failure never leaves them unformatted, and the legal-nature generator emits the explanatory paragraph its output file carries, which every run used to delete. --- scripts/banks.ts | 9 ++++----- scripts/data.ts | 16 +++++----------- scripts/legal-natures.ts | 4 ++++ 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/scripts/banks.ts b/scripts/banks.ts index d0ce0823..8de6a6ce 100644 --- a/scripts/banks.ts +++ b/scripts/banks.ts @@ -187,15 +187,14 @@ export const BANKS: Bank[] = ${JSON.stringify(sorted)};`; const constantsPath = resolve(scriptsDir, "..", "./src/is-valid-bank-account/constants.ts"); const constants = await readFile(constantsPath, "utf8"); const literal = (compeCodes.match(/.{1,90}/g) ?? []).map((chunk) => `\t"${chunk}"`).join(" +\n"); - const updated = constants.replace( - /export const COMPE_CODES =\n(?:\t"\d*" \+\n)*\t"\d*";/, - `export const COMPE_CODES =\n${literal};`, - ); + const compeCodesPattern = /export const COMPE_CODES =\n(?:\t"\d*" \+\n)*\t"\d*";/; - if (updated === constants) { + if (!compeCodesPattern.test(constants)) { throw new Error("COMPE_CODES literal not found in src/is-valid-bank-account/constants.ts"); } + const updated = constants.replace(compeCodesPattern, `export const COMPE_CODES =\n${literal};`); + console.log(`Generated ${sorted.length} banks from ${source}`); await writeFile(banksPath, banksFile); diff --git a/scripts/data.ts b/scripts/data.ts index 52c40380..850c569d 100644 --- a/scripts/data.ts +++ b/scripts/data.ts @@ -45,19 +45,13 @@ const results = await Promise.all( generators.map((generator) => run("node", [resolve(scriptsDir, generator)])), ); -if (results.some((result) => result !== 0)) { - process.exit(1); -} - -const formatResult = await run("vp", ["fmt", "--write", ...generatedFiles]); - -if (formatResult !== 0) { - process.exit(1); -} - +// Lint and format before checking the generators, so a failing generator never leaves +// unformatted files behind in the working tree. `vp fmt` runs last because `vp lint --fix` +// rewrites code without reformatting it. const lintResult = await run("vp", ["lint", "--fix", ...generatedFiles]); +const formatResult = await run("vp", ["fmt", "--write", ...generatedFiles]); -if (lintResult !== 0) { +if (results.some((result) => result !== 0) || lintResult !== 0 || formatResult !== 0) { process.exit(1); } diff --git a/scripts/legal-natures.ts b/scripts/legal-natures.ts index 2d8bcb3b..13b9deab 100644 --- a/scripts/legal-natures.ts +++ b/scripts/legal-natures.ts @@ -211,6 +211,10 @@ const main = async (): Promise => { * compatibility. Separately, and unrelated to those legacy codes, the descriptions of the * following official codes fix an accent typo of the PDF: ${typoFixedCodes.join(", ")}. * + * The CONCLA table page sits behind a bot filter and answers HTTP 403 to every non-browser + * client, so it has to be opened in a browser; the detailed structure PDF next to it is served + * normally. + * * @see Official: ${SOURCE_PAGE_URL} * @see Official: ${SOURCE_URL} */ From 4e087c99a7fe38cc066d5844a73a3f772c74c6d2 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:09:18 -0300 Subject: [PATCH 74/75] build(package): ship CHANGELOG.md in the tarball npm only adds package.json, README and LICENSE on its own; the changelog release-please maintains was not published. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index c42ed391..476f5bcd 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "url": "git+https://github.com/brazilian-utils/javascript.git" }, "files": [ + "./CHANGELOG.md", "./dist" ], "type": "module", From 6a33dea44faaaa8f3faff13354d5142b1ca2232a Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:09:19 -0300 Subject: [PATCH 75/75] docs: final pass before the release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migration guide: the `formatPhone` examples show the real outputs, the three `isValidBankAccount` examples validate, the unpublished `_internals` import is gone, the `formatPIS` row (v1 never had it), the repository link and the deprecation of `getCities` are corrected. Bundle sizes measured again (`isValidCpf` from the root is 1.4 KB, 0.8 KB gzipped) and the heavy-util table refreshed. CHANGELOG 2.3.0 dated as its tag. Pull request and bug report templates fixed. llms.txt keeps the deprecation sentence of a section. Facts that were only in the JSDoc now appear in the docs (registro profissional digit ranges, certidão service, Pix key and payload limits, IBAN of another country, processo whitespace, short-value parsing), and two example comments follow the runtime key order. --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 3 ++- CHANGELOG.md | 4 +-- README.md | 4 ++- docs/getting-started.md | 20 +++++++-------- docs/llms-full.txt | 30 +++++++++++----------- docs/llms.txt | 2 +- docs/migration-v1-to-v2.md | 37 ++++++++++++--------------- docs/pt-br/getting-started.md | 20 +++++++-------- docs/pt-br/migration-v1-to-v2.md | 37 ++++++++++++--------------- docs/pt-br/utilities.md | 30 +++++++++++----------- docs/utilities.md | 30 +++++++++++----------- scripts/llms.ts | 23 ++++++++++++++++- 13 files changed, 127 insertions(+), 115 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 76199ca6..d1aa97a5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -13,7 +13,7 @@ body: attributes: label: Package version description: Which version of `@brazilian-utils/brazilian-utils` are you using? - placeholder: "2.3.0" + placeholder: "2.4.0" validations: required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 56b68a5a..ca59c274 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -9,7 +9,8 @@ - [ ] I updated the documentation if this adds/changes a utility, in **both**: - [ ] `docs/utilities.md` (English) - [ ] `docs/pt-br/utilities.md` (Portuguese) -- [ ] `npm check` passes locally (format, lint, types). +- [ ] `npm run check` passes locally (format, lint, types). +- [ ] I ran `npm run build:llms` if I touched `docs/utilities.md` (the Check workflow fails when `docs/llms.txt` is stale). - [ ] This change does not introduce a breaking change, **or** I flagged it clearly below and it was discussed with maintainers beforehand. - [ ] This change does not add any runtime dependency (this library is zero-runtime-dependency by design). diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dffb64b..74ae8e84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [2.3.0](https://github.com/brazilian-utils/javascript/compare/2.2.0...2.3.0) (2026-04-09) +## [2.3.0](https://github.com/brazilian-utils/javascript/compare/2.2.0...2.3.0) (2026-04-08) ### Features @@ -193,5 +193,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 🎡 adjust travis config ([0ec107f](https://github.com/brazilian-utils/javascript/commit/0ec107f1ebee536e15a6bb991750342457bf1a44)) - 🎡 rename travis file ([25da01e](https://github.com/brazilian-utils/javascript/commit/25da01e5c700c17c3e88217c4bf272f5eef5639a)) - -[Unreleased]: https://github.com/brazilian-utils/javascript/compare/2.3.0...HEAD diff --git a/README.md b/README.md index 1ba59330..0afabd35 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ - [Getting Started](#getting-started) - [Why Brazilian Utils](#why-brazilian-utils) - [Installation](#installation) + - [Runtime support](#runtime-support) - [Usage](#usage) + - [Development](#development) - [Contributors](#contributors) - [License](#license) @@ -32,7 +34,7 @@ Brazilian Utils is a library focused on solving problems that we face daily in t ## Why Brazilian Utils - **Zero runtime dependencies.** Nothing else lands in your `node_modules` or in your bundle. -- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs about 1.2 KB minified (0.6 KB gzipped); every util is also its own subpath entry (`@brazilian-utils/brazilian-utils/get-cities`) for the heavy ones. +- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs about 1.4 KB minified (0.8 KB gzipped); every util is also its own subpath entry (`@brazilian-utils/brazilian-utils/get-cities`) for the heavy ones. - **Runs everywhere.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno and evergreen browsers, tested in CI on every one of them. - **Written in TypeScript.** Types ship with the package; the public API is tracked by an API report so nothing changes silently. - **Validated against the official rules.** Every validator cites the specification, law or dataset it implements (`@see` in the docs), and the test suite is mutation-tested, not just covered. diff --git a/docs/getting-started.md b/docs/getting-started.md index 72fe7d09..092cd612 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,7 +5,7 @@ Brazilian Utils is a library focused on solving problems that we face daily in t ## Why Brazilian Utils - **Zero runtime dependencies.** Nothing else lands in your `node_modules` or in your bundle. -- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs about 1.2 KB minified (0.6 KB gzipped); every util is also its own subpath entry (`@brazilian-utils/brazilian-utils/get-cities`) for the heavy ones. +- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs about 1.4 KB minified (0.8 KB gzipped); every util is also its own subpath entry (`@brazilian-utils/brazilian-utils/get-cities`) for the heavy ones. - **Runs everywhere.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno and evergreen browsers, tested in CI on every one of them. - **Written in TypeScript.** Types ship with the package; the public API is tracked by an API report so nothing changes silently. - **Validated against the official rules.** Every validator cites the specification, law or dataset it implements (`@see` in the docs), and the test suite is mutation-tested, not just covered. @@ -63,19 +63,19 @@ You can check a list of utilities [by clicking here](utilities.md). ## Bundle size -The package is tree-shakeable: importing one util from the root pulls in only that util's code, not the rest of the library. `isValidCpf`, for example, adds roughly 1.2 KB minified (0.6 KB gzipped) to your bundle. A bundler that supports tree-shaking (webpack, Rollup, esbuild, Vite, etc.) drops every other util. +The package is tree-shakeable: importing one util from the root pulls in only that util's code, not the rest of the library. `isValidCpf`, for example, adds roughly 1.4 KB minified (0.8 KB gzipped) to your bundle. A bundler that supports tree-shaking (webpack, Rollup, esbuild, Vite, etc.) drops every other util. A handful of utils are the exception: each embeds an official dataset, so it weighs far more than every other util combined. These are their single-import sizes, minified and gzipped: | Util | Dataset | Minified | Gzipped | | --- | --- | --- | --- | -| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 IBGE municipalities, with names and codes | 156.2 KB | 50.2 KB | -| `getCities` | 5571 IBGE municipality names | 154.0 KB | 49.7 KB | -| `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 113.8 KB | 24.3 KB | -| `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 118.8 KB | 30.4 KB | -| `isValidCnae` · `getCnae` | CNAE-Subclasses 2.3 | 94.0 KB | 21.3 KB | -| `isValidCfop` · `getCfop` | CFOP operation descriptions | 68.7 KB | 6.8 KB | -| `getBanks` · `getBankByCode` | Banco Central STR participants (COMPE + ISPB) | 38.3 KB | 9.6 KB | +| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 IBGE municipalities, with names and codes | 154.9 - 156.5 KB | 50.3 - 50.4 KB | +| `getCities` | 5571 IBGE municipality names | 154.2 KB | 49.8 KB | +| `isValidNcm` | NCM (Nomenclatura Comum do Mercosul) codes | 114.1 KB | 24.6 KB | +| `isValidCbo` · `getCbo` | CBO 2002 occupation titles | 119.1 KB | 30.6 KB | +| `isValidCnae` · `getCnae` | CNAE-Subclasses 2.3 | 93.9 KB | 21.2 KB | +| `isValidCfop` · `getCfop` | CFOP operation descriptions | 68.9 KB | 6.9 KB | +| `getBanks` · `getBankByCode` | Banco Central STR participants (COMPE + ISPB) | 38.3 - 38.6 KB | 9.5 - 9.7 KB | Importing any of them from the root, even alongside a single small util, pulls that whole dataset into your main bundle, because this package ships as a single ESM module: a dynamic `import()` of the root (`await import('@brazilian-utils/brazilian-utils')`) still resolves to that same one file, so it can't be split out on its own. A bundler doing code-splitting needs a separate module to split *into*. @@ -97,4 +97,4 @@ getMunicipalityByCode('3550308'); Every util is available this way, as `@brazilian-utils/brazilian-utils/` (kebab-case, matching the function name: `isValidCpf` → `is-valid-cpf`), for the same lazy-loading/code-splitting reason. -Pick one style per util in a given app: a bundler treats the root import and the subpath import as two unrelated modules, so importing `getCities` from both the root *and* `/get-cities` in the same app bundles the 154.0 KB city table twice, once in each module's own output. +Pick one style per util in a given app: a bundler treats the root import and the subpath import as two unrelated modules, so importing `getCities` from both the root *and* `/get-cities` in the same app bundles the 154.2 KB city table twice, once in each module's own output. diff --git a/docs/llms-full.txt b/docs/llms-full.txt index f5256498..9c57aa14 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -449,7 +449,7 @@ isValidPixKey('not a key'); // false ### getPixKeyInfo -Identifies a Pix key and normalizes it to the canonical form the DICT expects inside a BR Code: 11 digit CPF, 14 character CNPJ, lowercased e-mail, E.164 mobile phone (a landline is not a Pix key) or lowercase UUID EVP. An 11 digit value that is valid both as a CPF and as a mobile phone is read as a CPF, unless it was written as a phone number (a `+55`/`0055` prefix or a DDD wrapped in parentheses). The CPF and the phone number are recognized by the way they are written, not only by the digits they carry, so surrounding text is not stripped away and `'abc123.456.789-09'` is not a CPF key. Returns `null` when the value is not a valid Pix key. The result is typed as `PixKeyInfo`. +Identifies a Pix key and normalizes it to the canonical form the DICT expects inside a BR Code: 11 digit CPF, 14 character CNPJ, lowercased e-mail, E.164 mobile phone (a landline is not a Pix key) or lowercase UUID EVP. An 11 digit value that is valid both as a CPF and as a mobile phone is read as a CPF, unless it was written as a phone number (a `+55`/`0055` prefix or a DDD wrapped in parentheses). The CPF and the phone number are recognized by the way they are written, not only by the digits they carry, so surrounding text is not stripped away and `'abc123.456.789-09'` is not a CPF key. An e-mail key is trimmed and lowercased, and one longer than the 77 characters the DICT allows is rejected. A value whose digits carry a valid CNPJ check digit is read as a CNPJ even when it starts with `0055`, since a phone key inside a BR Code always carries the `+55` prefix. Returns `null` when the value is not a valid Pix key. The result is typed as `PixKeyInfo`. ```javascript import { getPixKeyInfo } from '@brazilian-utils/brazilian-utils'; @@ -481,7 +481,7 @@ isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (broken CRC) ### getPixPayloadInfo -Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The result is typed as `PixPayloadInfo`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. A `fss` written next to a PSP location returns `null`: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template of §2.6. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Unreserved Templates (IDs 80 to 99) are ignored: a "QR Code composto" of Pix Automático that also carries a payment location in 26-25 is parsed as an ordinary dynamic payload and its recurrence location is dropped, so a consumer that has to tell the two apart cannot rely on this parser. Only a payload with no Pix template at all in IDs 26 to 51 returns `null`. +Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The Pix key itself is not validated, since the manual allows a static QR Code built around a key that no longer exists in the DICT; key ownership is only settled at payment time. The "Additional Data Field Template" (ID 62) is mandatory in the BR Code table but optional in the EMV® specification it refers to, so it is accepted when absent. The lengths the manual reserves for the merchant name (25), the merchant city (15), the `txid` (25) and the Pix key field 26-01 (77) are generator side limits, enforced by `generatePixPayload` and not checked here, since payloads in the wild routinely overrun them. The result is typed as `PixPayloadInfo`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. A `fss` written next to a PSP location returns `null`: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template of §2.6. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Unreserved Templates (IDs 80 to 99) are ignored: a "QR Code composto" of Pix Automático that also carries a payment location in 26-25 is parsed as an ordinary dynamic payload and its recurrence location is dropped, so a consumer that has to tell the two apart cannot rely on this parser. Only a payload with no Pix template at all in IDs 26 to 51 returns `null`. ```javascript import { getPixPayloadInfo } from '@brazilian-utils/brazilian-utils'; @@ -491,10 +491,10 @@ getPixPayloadInfo( '5204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041D3D' ); // { -// key: '123e4567-e12b-12d1-a456-426655440000', // merchantName: 'Fulano de Tal', // merchantCity: 'BRASILIA', -// pointOfInitiation: 'static' +// pointOfInitiation: 'static', +// key: '123e4567-e12b-12d1-a456-426655440000' // } ``` @@ -588,7 +588,7 @@ getNfeKeyInfo('35170458716523000119550010000000121000123458'); getNfeKeyInfo('35170458716523000119620010000000121000123450'); // { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', -// series: 1, number: 12, emissionType: 1, authorizationSite: 0, code: '0012345', checkDigit: 0 } +// series: 1, number: 12, emissionType: 1, code: '0012345', checkDigit: 0, authorizationSite: 0 } getNfeKeyInfo('invalid'); // null ``` @@ -843,7 +843,7 @@ const addressFromNumber = await getAddressInfoByCep(1310100); ### isValidProcessoJuridico -Validate the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119): the `NNNNNNN-DD.AAAA.J.TR.OOOO` layout, the `DD` check digits and the `J`/`TR` pair, which has to name an órgão and a tribunal Resolução CNJ nº 65/2008 created, so a number carrying a correct check digit but a court that does not exist is rejected. The closed lists come from art. 1º, § 4º and § 5º of the resolution, § 5º, III in the wording Resolução CNJ nº 477/2022 gave it to seat the TRF da 6ª Região. The unidade de origem (`OOOO`) is only read as four digits, since art. 1º, § 6º leaves its codification to each tribunal and publishes no central list. The CNJ mask separators (whitespace, `.` and `-`) are accepted between the fields, but any other character, a letter in particular, makes the value invalid. +Validate the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119): the `NNNNNNN-DD.AAAA.J.TR.OOOO` layout, the `DD` check digits and the `J`/`TR` pair, which must identify an existing órgão and tribunal from the closed lists defined by Resolução CNJ nº 65/2008, so a number carrying a correct check digit but a court that does not exist is rejected. The closed lists come from art. 1º, § 4º and § 5º of the resolution, § 5º, III in the wording Resolução CNJ nº 477/2022 gave it to seat the TRF da 6ª Região. The unidade de origem (`OOOO`) is only read as four digits, since art. 1º, § 6º leaves its codification to each tribunal and publishes no central list. The CNJ mask separators (whitespace, `.` and `-`) are accepted between the fields, and whitespace around the value is ignored, but any other character, a letter in particular, makes the value invalid. ```javascript import { isValidProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -1071,7 +1071,7 @@ parseIban('br15-0000.0000/0000 1093 2840 814p-2'); // 'BR15000000000000109328408 ### getIbanInfo -Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups of 4 split by a single whitespace, `.`, `-` or `/`), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying a separator away from a group boundary, a run of separators or any character other than letters and digits. The result is typed as `IbanInfo`, whose `accountType` is a `string`. +Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Only Brazilian IBANs are supported: the field layout of the other ISO 13616 countries is out of scope, so a well-formed non `BR` IBAN also returns `null`. Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups of 4 split by a single whitespace, `.`, `-` or `/`), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying a separator away from a group boundary, a run of separators or any character other than letters and digits. The result is typed as `IbanInfo`, whose `accountType` is a `string`. ```javascript import { getIbanInfo } from '@brazilian-utils/brazilian-utils'; @@ -1350,7 +1350,7 @@ getCities('SP'); // ] ``` -`getCities` embeds all 5571 IBGE municipality names (~154.0 KB minified, ~49.7 KB gzipped) and is one of the few heavy exceptions in this otherwise tree-shakeable package. See [Bundle size](getting-started.md#bundle-size) for how to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities` instead of the root import. +`getCities` embeds all 5571 IBGE municipality names (~154.2 KB minified, ~49.8 KB gzipped) and is one of the few heavy exceptions in this otherwise tree-shakeable package. See [Bundle size](getting-started.md#bundle-size) for how to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities` instead of the root import. ### getHolidays @@ -1524,7 +1524,7 @@ Generate a valid random processo jurídico number according to [CNJ's definition import { generateProcessoJuridico } from '@brazilian-utils/brazilian-utils'; generateProcessoJuridico(); // '89478645020266070326' -generateProcessoJuridico({ year: 2026, court: 5 }); // string | null +generateProcessoJuridico({ year: 2026, court: 5 }); // '98412562120265087260' (Justiça do Trabalho, TRT da 8ª Região) generateProcessoJuridico({ year: 10000 }); // null (year out of range) generateProcessoJuridico({ court: 10 }); // null (no such órgão) ``` @@ -2026,7 +2026,7 @@ parseCertidao('104539 01 55 2013 1 00012 021 0000123 21'); ### getCertidaoInfo -Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. [Art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lists the codes 1 to 7; no CNJ primary text reachable today publishes the other two, the Anexo IV of the revoked Provimento CNJ nº 63/2017 included, which lists the same seven. The codes 8 (emancipação) and 9 (interdição) come from the references the check digit rule rests on: [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) both print the nine book list. They are kept because matrículas carrying them circulate. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. +Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. A serviço other than the `55` that art. 473, III fixes for the registro civil das pessoas naturais also gives `null`. [Art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lists the codes 1 to 7; no CNJ primary text reachable today publishes the other two, the Anexo IV of the revoked Provimento CNJ nº 63/2017 included, which lists the same seven. The codes 8 (emancipação) and 9 (interdição) come from the references the check digit rule rests on: [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) both print the nine book list. They are kept because matrículas carrying them circulate. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. ```javascript import { getCertidaoInfo } from '@brazilian-utils/brazilian-utils'; @@ -2127,7 +2127,7 @@ formatCno('979', { pad: true }); // 00.000.00009/79 ### parseCno -Remove CNO (Cadastro Nacional de Obras) formatting, keep only digits, and cap the result to 12 digits, the numbering the CNO kept from the CEI. Use `isValidCno` to check the number itself. +Remove CNO (Cadastro Nacional de Obras) formatting, keep only digits, and cap the result to 12 digits, the numbering the CNO kept from the CEI. A shorter value passes through as far as it goes; use `isValidCno` to check the number itself. ```javascript import { parseCno } from '@brazilian-utils/brazilian-utils'; @@ -2164,7 +2164,7 @@ formatCaepf('184', { pad: true }); // 000.000.000/001-84 ### parseCaepf -Remove CAEPF (Cadastro de Atividade Econômica da Pessoa Física) formatting, keep only digits, and cap the result to 14 digits. Use `isValidCaepf` to check the number itself. +Remove CAEPF (Cadastro de Atividade Econômica da Pessoa Física) formatting, keep only digits, and cap the result to 14 digits. A shorter value passes through as far as it goes; use `isValidCaepf` to check the number itself. ```javascript import { parseCaepf } from '@brazilian-utils/brazilian-utils'; @@ -2174,7 +2174,7 @@ parseCaepf('293.118.610/001-84'); // '29311861000184' ### isValidRegistroProfissional -Check the structure of a professional council registration number (registro/inscrição profissional). It takes a single object, typed as `IsValidRegistroProfissionalOptions`, the shape `isValidBankAccount` takes: `value` is the registration number, `council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). Anything that is not an object, and an object missing `value` or `council`, is `false`. This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits, the tipo de registro (`"O"` Originário or `"P"` Provisório, which says nothing about the professional category) and the check digit, as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). A Registro Transferido or Secundário appends `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, per that same item and [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: the Manual's own examples are `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` and `PI-111222/O-5 S-AC`. Both UFs must be real state codes, and `stateCode` is compared against the originating one. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. Only the CRC shape and those CRP regional codes rest on a published source: the CFP page publishes no length for the inscription number itself, and the OAB, the CFM and the CFO publish no format at all, so the digit ranges accepted for `"CRP"`, `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative (the OAB/SP public search field is `maxlength="7"`, and the CFM documents `300`-prefixed and `P`-suffixed CRMs, none of which these shapes express). CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). +Check the structure of a professional council registration number (registro/inscrição profissional). It takes a single object, typed as `IsValidRegistroProfissionalOptions`, the shape `isValidBankAccount` takes: `value` is the registration number, `council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). Anything that is not an object, and an object missing `value` or `council`, is `false`. The accepted shapes are 4 to 6 digits plus the UF for `"OAB"` and `"CRM"`, 3 to 6 digits plus the UF for `"CRO"`, a 2 digit regional code plus 4 to 6 digits for `"CRP"`, and the UF plus 6 digits, the tipo de registro and one check digit for `"CRC"`. This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits, the tipo de registro (`"O"` Originário or `"P"` Provisório, which says nothing about the professional category) and the check digit, as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). A Registro Transferido or Secundário appends `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, per that same item and [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: the Manual's own examples are `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` and `PI-111222/O-5 S-AC`. Both UFs must be real state codes, and `stateCode` is compared against the originating one. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. Only the CRC shape and those CRP regional codes rest on a published source: the CFP page publishes no length for the inscription number itself, and the OAB, the CFM and the CFO publish no format at all, so the digit ranges accepted for `"CRP"`, `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative (the OAB/SP public search field is `maxlength="7"`, and the CFM documents `300`-prefixed and `P`-suffixed CRMs, none of which these shapes express). CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). ```javascript import { isValidRegistroProfissional } from '@brazilian-utils/brazilian-utils'; @@ -2222,7 +2222,7 @@ The occupation titles come from the [official CBO 2002 occupation table publishe ### parseCbo -Remove CBO (Classificação Brasileira de Ocupações) formatting, keep only digits, and cap the result to 6 digits. Nothing is left padded here, so the leading zero of a code such as `010205` has to be written out; use `getCbo` or `isValidCbo`, which do pad a bare numeric code, to look an occupation up. +Remove CBO (Classificação Brasileira de Ocupações) formatting, keep only digits, and cap the result to 6 digits. A shorter value passes through as far as it goes and nothing is left padded here, so the leading zero of a code such as `010205` has to be written out; use `getCbo` or `isValidCbo`, which do pad a bare numeric code, to look an occupation up. ```javascript import { parseCbo } from '@brazilian-utils/brazilian-utils'; @@ -2366,7 +2366,7 @@ isValidCfop(-5102); // false (not a non-negative safe integer) ### parseCfop -Remove CFOP (Código Fiscal de Operações e Prestações) formatting, keep only digits, and cap the result to 4 digits. No CFOP code starts with a zero, its first digit is the operation group from 1 to 7, so nothing is ever padded here. +Remove CFOP (Código Fiscal de Operações e Prestações) formatting, keep only digits, and cap the result to 4 digits. A shorter value passes through as far as it goes. No CFOP code starts with a zero, its first digit is the operation group from 1 to 7, so nothing is ever padded here. ```javascript import { parseCfop } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/llms.txt b/docs/llms.txt index 1db5e2c1..f12e2abc 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -39,7 +39,7 @@ const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities' - [isValidLicensePlate](https://brazilian-utils.com.br/utilities.md#isvalidlicenseplate): Check if license plate is valid. - [isValidRenavam](https://brazilian-utils.com.br/utilities.md#isvalidrenavam): Check if RENAVAM (Registro Nacional de Veículos Automotores) is valid. - [isValidPis](https://brazilian-utils.com.br/utilities.md#isvalidpis): Check if PIS is valid. -- [isValidProcessoJuridico](https://brazilian-utils.com.br/utilities.md#isvalidprocessojuridico): Validate the processo jurídico number according to CNJ's definition: the `NNNNNNN-DD.AAAA.J.TR.OOOO` layout, the `DD` check digits and the `J`/`TR` pair, which has to name an órgão and a tribunal Resolução CNJ nº 65/2008 created, so a number carrying a correct check digit but a court that does not exist is rejected. +- [isValidProcessoJuridico](https://brazilian-utils.com.br/utilities.md#isvalidprocessojuridico): Validate the processo jurídico number according to CNJ's definition: the `NNNNNNN-DD.AAAA.J.TR.OOOO` layout, the `DD` check digits and the `J`/`TR` pair, which must identify an existing órgão and tribunal from the closed lists defined by Resolução CNJ nº 65/2008, so a number carrying a correct check digit but a court that does not exist is rejected. - [isValidIe](https://brazilian-utils.com.br/utilities.md#isvalidie): Check if inscrição estadual (state registration) is valid. - [isValidBankAccount](https://brazilian-utils.com.br/utilities.md#isvalidbankaccount): Check if a Brazilian bank account is valid. - [isValidIban](https://brazilian-utils.com.br/utilities.md#isvalidiban): Check if a Brazilian IBAN (International Bank Account Number) is valid, per Bacen's Diretrizes de Implementação do IBAN no Brasil (Circular BCB nº 3.625/2013): `BR` + 2 ISO 7064 MOD 97-10 check digits + 8 digit ISPB + 5 digit branch + 10 digit account + 1 letter account type (any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 owner indicator (`1` for the first or only holder up to `9` for the ninth, then `A` to `Z` from the tenth, so `0` is rejected), 29 characters total. diff --git a/docs/migration-v1-to-v2.md b/docs/migration-v1-to-v2.md index 45f40c92..e6fe8b62 100644 --- a/docs/migration-v1-to-v2.md +++ b/docs/migration-v1-to-v2.md @@ -148,7 +148,6 @@ To make the migration easier, **v2.x still exports the old PascalCase names as d | `formatCPF` | `formatCpf` | | `formatCNPJ` | `formatCnpj` | | `formatCEP` | `formatCep` | -| `formatPIS` | `formatPis` | | `formatProcessoJuridico` | `formatProcessoJuridico` (unchanged) | | `formatBoleto` | `formatBoleto` (unchanged) | | `formatCurrency` | `formatCurrency` (unchanged) | @@ -182,7 +181,7 @@ generateCnpj(); // Currently generates numeric (v1), but will be random in v3.0. | `parseCurrency` | `parseCurrency` (unchanged) | | `capitalize` | `capitalize` (unchanged) | | `getStates` | `getStates` (unchanged) | -| `getCities` | `getCities` (unchanged) | +| `getCities` | `getCities` (unchanged; deprecated in 2.4.0 in favour of `getMunicipalities`) | | `getAddressInfoByCep` | `getAddressInfoByCep` (API changed, see below) | ### Migration Example @@ -238,16 +237,12 @@ if (index === input.length - 1) { /* ... */ } ``` #### `generateChecksum` -This function is now internal and no longer exported in the public API. +This function is now internal and no longer exported in the public API. The package exports no internals: `dist/_internals` is not published and there is no subpath for it, so there is no supported way to import this function in v2. Inline the check digit calculation you need instead. **Migration:** ```javascript // v1 - Don't use this anymore import { generateChecksum } from '@brazilian-utils/brazilian-utils'; - -// v2 - If you absolutely need it, import from internals (not recommended) -// This is not part of the public API and may change without notice -import { generateChecksum } from '@brazilian-utils/brazilian-utils/dist/_internals/generate-checksum/generate-checksum'; ``` #### `generateRandomNumber` @@ -304,9 +299,9 @@ Format phone numbers according to Brazilian patterns. ```javascript import { formatPhone } from '@brazilian-utils/brazilian-utils'; -formatPhone('11900000000'); // 90000-0000 +formatPhone('11900000000'); // 11900-0000 (BEWARE: default "sn" truncates a DDD-prefixed number) formatPhone('11900000000', { mask: 'nanp' }); // (11) 90000-0000 -formatPhone('11900000000', { mask: 'auto' }); // Auto-detects mask +formatPhone('11900000000', { mask: 'auto' }); // (11) 90000-0000 ``` ### `isValidRenavam` @@ -331,26 +326,26 @@ import { isValidBankAccount } from '@brazilian-utils/brazilian-utils'; // Banco do Brasil isValidBankAccount({ bankCode: '001', - agency: '1234', - account: '12345678', - digit: '5' -}); // true (if valid) + agency: '1584', + account: '00210169', + digit: '6' +}); // true // Itaú isValidBankAccount({ bankCode: '341', - agency: '1234', - account: '12345', - digit: '6' -}); // true (if valid) + agency: '2545', + account: '02366', + digit: '1' +}); // true // Other banks use generic validation isValidBankAccount({ - bankCode: '999', + bankCode: '246', agency: '1234', account: '123456', - digit: '7' -}); // true (if mod10/mod11 validation passes) + digit: '6' +}); // true (the digit matches mod10) ``` ## API Changes @@ -438,4 +433,4 @@ If you encounter any issues during migration, please: 1. Check the [utilities documentation](utilities.md) for the correct function signatures 2. Review the examples in this migration guide -3. Open an issue on the [GitHub repository](https://github.com/brazilian-utils/brazilian-utils) if you find a bug +3. Open an issue on the [GitHub repository](https://github.com/brazilian-utils/javascript) if you find a bug diff --git a/docs/pt-br/getting-started.md b/docs/pt-br/getting-started.md index 3ce59e3f..3b84740e 100644 --- a/docs/pt-br/getting-started.md +++ b/docs/pt-br/getting-started.md @@ -5,7 +5,7 @@ Brazilian Utils é uma biblioteca com foco na resolução de problemas que enfre ## Por que Brazilian Utils - **Zero dependências de runtime.** Nada além da lib entra no seu `node_modules` ou no seu bundle. -- **Tree-shakeable até a função.** `import { isValidCpf }` custa cerca de 1,2 KB minificado (0,6 KB com gzip); cada utilitário também é um subpath próprio (`@brazilian-utils/brazilian-utils/get-cities`) para os mais pesados. +- **Tree-shakeable até a função.** `import { isValidCpf }` custa cerca de 1,4 KB minificado (0,8 KB com gzip); cada utilitário também é um subpath próprio (`@brazilian-utils/brazilian-utils/get-cities`) para os mais pesados. - **Roda em qualquer lugar.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno e navegadores modernos, testados no CI em todos eles. - **Escrita em TypeScript.** Os tipos vêm no pacote; a API pública é acompanhada por um relatório de API, então nada muda em silêncio. - **Validada contra as regras oficiais.** Cada validador cita a especificação, lei ou base de dados que implementa (`@see` na documentação), e a suíte de testes passa por mutation testing, não só por cobertura. @@ -63,19 +63,19 @@ Você pode conferir a lista de utilitários [clicando aqui](utilities.md). ## Tamanho do bundle -O pacote é tree-shakeable: importar um utilitário da raiz traz apenas o código daquele utilitário, não o resto da biblioteca. `isValidCpf`, por exemplo, adiciona cerca de 1,2 KB minificado (0,6 KB com gzip) ao seu bundle. Um bundler com suporte a tree-shaking (webpack, Rollup, esbuild, Vite, etc.) descarta todos os outros utilitários. +O pacote é tree-shakeable: importar um utilitário da raiz traz apenas o código daquele utilitário, não o resto da biblioteca. `isValidCpf`, por exemplo, adiciona cerca de 1,4 KB minificado (0,8 KB com gzip) ao seu bundle. Um bundler com suporte a tree-shaking (webpack, Rollup, esbuild, Vite, etc.) descarta todos os outros utilitários. Alguns utilitários são a exceção: cada um embute um dataset oficial e pesa muito mais que todos os outros utilitários somados. Estes são os tamanhos de um import isolado, minificado e com gzip: | Utilitário | Dataset | Minificado | Gzip | | --- | --- | --- | --- | -| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 municípios do IBGE, com nomes e códigos | 156,2 KB | 50,2 KB | -| `getCities` | nomes dos 5571 municípios do IBGE | 154,0 KB | 49,7 KB | -| `isValidNcm` | códigos NCM (Nomenclatura Comum do Mercosul) | 113,8 KB | 24,3 KB | -| `isValidCbo` · `getCbo` | títulos das ocupações da CBO 2002 | 118,8 KB | 30,4 KB | -| `isValidCnae` · `getCnae` | CNAE-Subclasses 2.3 | 94,0 KB | 21,3 KB | -| `isValidCfop` · `getCfop` | descrições das operações do CFOP | 68,7 KB | 6,8 KB | -| `getBanks` · `getBankByCode` | participantes do STR do Banco Central (COMPE + ISPB) | 38,3 KB | 9,6 KB | +| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 municípios do IBGE, com nomes e códigos | 154,9 - 156,5 KB | 50,3 - 50,4 KB | +| `getCities` | nomes dos 5571 municípios do IBGE | 154,2 KB | 49,8 KB | +| `isValidNcm` | códigos NCM (Nomenclatura Comum do Mercosul) | 114,1 KB | 24,6 KB | +| `isValidCbo` · `getCbo` | títulos das ocupações da CBO 2002 | 119,1 KB | 30,6 KB | +| `isValidCnae` · `getCnae` | CNAE-Subclasses 2.3 | 93,9 KB | 21,2 KB | +| `isValidCfop` · `getCfop` | descrições das operações do CFOP | 68,9 KB | 6,9 KB | +| `getBanks` · `getBankByCode` | participantes do STR do Banco Central (COMPE + ISPB) | 38,3 - 38,6 KB | 9,5 - 9,7 KB | Importar qualquer um deles da raiz, mesmo ao lado de um único utilitário pequeno, traz todo esse dataset para o seu bundle principal, porque este pacote é publicado como um único módulo ESM: um `import()` dinâmico da raiz (`await import('@brazilian-utils/brazilian-utils')`) ainda resolve para esse mesmo arquivo único, então não há como separá-lo sozinho. Um bundler que faz code-splitting precisa de um módulo separado para separar. @@ -97,4 +97,4 @@ getMunicipalityByCode('3550308'); Todos os utilitários estão disponíveis dessa forma, como `@brazilian-utils/brazilian-utils/` (kebab-case, seguindo o nome da função: `isValidCpf` → `is-valid-cpf`), pelo mesmo motivo de lazy-loading/code-splitting. -Escolha um estilo por utilitário em cada aplicação: um bundler trata o import da raiz e o import do subpath como dois módulos independentes, então importar `getCities` tanto da raiz quanto de `/get-cities` na mesma aplicação inclui a tabela de 154,0 KB de cidades duas vezes, uma em cada módulo. +Escolha um estilo por utilitário em cada aplicação: um bundler trata o import da raiz e o import do subpath como dois módulos independentes, então importar `getCities` tanto da raiz quanto de `/get-cities` na mesma aplicação inclui a tabela de 154,2 KB de cidades duas vezes, uma em cada módulo. diff --git a/docs/pt-br/migration-v1-to-v2.md b/docs/pt-br/migration-v1-to-v2.md index f45cf647..c8e3e6a8 100644 --- a/docs/pt-br/migration-v1-to-v2.md +++ b/docs/pt-br/migration-v1-to-v2.md @@ -148,7 +148,6 @@ Para facilitar a migração, **a v2.x ainda exporta os nomes antigos em PascalCa | `formatCPF` | `formatCpf` | | `formatCNPJ` | `formatCnpj` | | `formatCEP` | `formatCep` | -| `formatPIS` | `formatPis` | | `formatProcessoJuridico` | `formatProcessoJuridico` (inalterado) | | `formatBoleto` | `formatBoleto` (inalterado) | | `formatCurrency` | `formatCurrency` (inalterado) | @@ -182,7 +181,7 @@ generateCnpj(); // Atualmente gera numérico (v1), mas será aleatório na v3.0. | `parseCurrency` | `parseCurrency` (inalterado) | | `capitalize` | `capitalize` (inalterado) | | `getStates` | `getStates` (inalterado) | -| `getCities` | `getCities` (inalterado) | +| `getCities` | `getCities` (inalterado; descontinuado na 2.4.0 em favor de `getMunicipalities`) | | `getAddressInfoByCep` | `getAddressInfoByCep` (API alterada, veja abaixo) | ### Exemplo de Migração @@ -238,16 +237,12 @@ if (index === input.length - 1) { /* ... */ } ``` #### `generateChecksum` -Esta função agora é interna e não é mais exportada na API pública. +Esta função agora é interna e não é mais exportada na API pública. O pacote não exporta internals: `dist/_internals` não é publicado e não existe subpath para ele, então não há forma suportada de importar essa função na v2. Calcule o dígito verificador que você precisa no seu próprio código. **Migração:** ```javascript // v1 - Não use mais isso import { generateChecksum } from '@brazilian-utils/brazilian-utils'; - -// v2 - Se você absolutamente precisar, importe dos internals (não recomendado) -// Isto não faz parte da API pública e pode mudar sem aviso -import { generateChecksum } from '@brazilian-utils/brazilian-utils/dist/_internals/generate-checksum/generate-checksum'; ``` #### `generateRandomNumber` @@ -304,9 +299,9 @@ Formata números de telefone de acordo com padrões brasileiros. ```javascript import { formatPhone } from '@brazilian-utils/brazilian-utils'; -formatPhone('11900000000'); // 90000-0000 +formatPhone('11900000000'); // 11900-0000 (CUIDADO: a máscara padrão "sn" trunca um número com DDD) formatPhone('11900000000', { mask: 'nanp' }); // (11) 90000-0000 -formatPhone('11900000000', { mask: 'auto' }); // Detecta automaticamente a máscara +formatPhone('11900000000', { mask: 'auto' }); // (11) 90000-0000 ``` ### `isValidRenavam` @@ -331,26 +326,26 @@ import { isValidBankAccount } from '@brazilian-utils/brazilian-utils'; // Banco do Brasil isValidBankAccount({ bankCode: '001', - agency: '1234', - account: '12345678', - digit: '5' -}); // true (se válido) + agency: '1584', + account: '00210169', + digit: '6' +}); // true // Itaú isValidBankAccount({ bankCode: '341', - agency: '1234', - account: '12345', - digit: '6' -}); // true (se válido) + agency: '2545', + account: '02366', + digit: '1' +}); // true // Outros bancos usam validação genérica isValidBankAccount({ - bankCode: '999', + bankCode: '246', agency: '1234', account: '123456', - digit: '7' -}); // true (se validação mod10/mod11 passar) + digit: '6' +}); // true (o dígito corresponde ao mod10) ``` ## Mudanças na API @@ -438,4 +433,4 @@ Se você encontrar problemas durante a migração, por favor: 1. Verifique a [documentação de utilitários](/pt-br/utilities.md) para as assinaturas corretas das funções 2. Revise os exemplos neste guia de migração -3. Abra uma issue no [repositório GitHub](https://github.com/brazilian-utils/brazilian-utils) se encontrar um bug +3. Abra uma issue no [repositório GitHub](https://github.com/brazilian-utils/javascript) se encontrar um bug diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index f997fc8c..54395e9c 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -196,7 +196,7 @@ isValidPixKey('not a key'); // false ## getPixKeyInfo -Identifica uma chave Pix e a normaliza para a forma canônica que o DICT espera dentro do BR Code: CPF com 11 dígitos, CNPJ com 14 caracteres, e-mail em minúsculas, telefone celular em E.164 (um telefone fixo não é chave Pix) ou UUID em minúsculas (EVP). Um valor de 11 dígitos válido tanto como CPF quanto como celular é lido como CPF, a menos que tenha sido escrito como telefone (prefixo `+55`/`0055` ou DDD entre parênteses). O CPF e o telefone são reconhecidos pela forma como são escritos, não apenas pelos dígitos que carregam, então texto ao redor não é descartado e `'abc123.456.789-09'` não é uma chave CPF. Retorna `null` quando o valor não é uma chave Pix válida. O resultado é tipado como `PixKeyInfo`. +Identifica uma chave Pix e a normaliza para a forma canônica que o DICT espera dentro do BR Code: CPF com 11 dígitos, CNPJ com 14 caracteres, e-mail em minúsculas, telefone celular em E.164 (um telefone fixo não é chave Pix) ou UUID em minúsculas (EVP). Um valor de 11 dígitos válido tanto como CPF quanto como celular é lido como CPF, a menos que tenha sido escrito como telefone (prefixo `+55`/`0055` ou DDD entre parênteses). O CPF e o telefone são reconhecidos pela forma como são escritos, não apenas pelos dígitos que carregam, então texto ao redor não é descartado e `'abc123.456.789-09'` não é uma chave CPF. Uma chave de e-mail é trimada e passada para minúsculas, e uma maior que os 77 caracteres que o DICT permite é rejeitada. Um valor cujos dígitos carregam um dígito verificador de CNPJ válido é lido como CNPJ mesmo quando começa com `0055`, já que uma chave de telefone dentro do BR Code sempre carrega o prefixo `+55`. Retorna `null` quando o valor não é uma chave Pix válida. O resultado é tipado como `PixKeyInfo`. ```javascript import { getPixKeyInfo } from '@brazilian-utils/brazilian-utils'; @@ -228,7 +228,7 @@ isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (CRC quebrado) ## getPixPayloadInfo -Interpreta um payload de BR Code Pix e retorna seus campos. O payload é validado pelo `isValidPixPayload` primeiro, então uma estrutura malformada, um CRC quebrado ou um objeto obrigatório ausente retornam `null` em vez de um resultado parcial. Um payload estático vem com `key`, um dinâmico com `url`. O resultado é tipado como `PixPayloadInfo`; `pointOfInitiation` está sempre presente e é tipado como `PixPointOfInitiation`, `"dynamic"` quando o payload traz uma localização de PSP ou quando o objeto "Point of Initiation Method" (`01`) é `"12"`, e `"static"` nos demais casos. As informações da conta do recebedor devem trazer exatamente um entre uma chave e uma `url` (verificada com a mesma regra de localização de PSP do `generatePixPayload`); o próprio `01` é informativo, então pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` retorna `null`. Quando um payload construído em torno de uma chave traz um valor, esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque: o §2.6 do manual do Pix coloca o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`), devolvido como `withdrawalFacilitator`, e `54` igual a `"0"` ou `"0.00"` é aceito junto dele. Rejeitar um valor zero sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. Um `fss` escrito ao lado de uma localização de PSP retorna `null`: o §2.7 do Manual de Padrões para Iniciação do Pix mapeia o QR Code dinâmico para exatamente dois subobjetos, `00` (GUI) e `25` (URL), e o `fss` pertence ao template estático do §2.6. Quando o payload traz uma localização de PSP, o valor e o `txid` são ignorados, como o manual determina. Os Unreserved Templates (IDs 80 a 99) são ignorados: um "QR Code composto" do Pix Automático que também traga uma localização de pagamento em 26-25 é interpretado como um payload dinâmico comum e sua localização de recorrência é descartada, então quem precisa distinguir os dois não pode se apoiar neste parser. Só um payload sem nenhum template Pix nos IDs 26 a 51 retorna `null`. +Interpreta um payload de BR Code Pix e retorna seus campos. O payload é validado pelo `isValidPixPayload` primeiro, então uma estrutura malformada, um CRC quebrado ou um objeto obrigatório ausente retornam `null` em vez de um resultado parcial. Um payload estático vem com `key`, um dinâmico com `url`. A chave Pix em si não é validada, já que o manual permite um QR Code estático construído com uma chave que não existe mais no DICT; a titularidade da chave só é resolvida no momento do pagamento. O "Additional Data Field Template" (ID 62) é obrigatório na tabela do BR Code mas opcional na especificação EMV® a que ela se refere, então é aceito quando ausente. Os tamanhos que o manual reserva para o nome do recebedor (25), a cidade do recebedor (15), o `txid` (25) e o campo 26-01 da chave Pix (77) são limites do lado do gerador, aplicados por `generatePixPayload` e não verificados aqui, já que payloads reais os ultrapassam com frequência. O resultado é tipado como `PixPayloadInfo`; `pointOfInitiation` está sempre presente e é tipado como `PixPointOfInitiation`, `"dynamic"` quando o payload traz uma localização de PSP ou quando o objeto "Point of Initiation Method" (`01`) é `"12"`, e `"static"` nos demais casos. As informações da conta do recebedor devem trazer exatamente um entre uma chave e uma `url` (verificada com a mesma regra de localização de PSP do `generatePixPayload`); o próprio `01` é informativo, então pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` retorna `null`. Quando um payload construído em torno de uma chave traz um valor, esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque: o §2.6 do manual do Pix coloca o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`), devolvido como `withdrawalFacilitator`, e `54` igual a `"0"` ou `"0.00"` é aceito junto dele. Rejeitar um valor zero sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. Um `fss` escrito ao lado de uma localização de PSP retorna `null`: o §2.7 do Manual de Padrões para Iniciação do Pix mapeia o QR Code dinâmico para exatamente dois subobjetos, `00` (GUI) e `25` (URL), e o `fss` pertence ao template estático do §2.6. Quando o payload traz uma localização de PSP, o valor e o `txid` são ignorados, como o manual determina. Os Unreserved Templates (IDs 80 a 99) são ignorados: um "QR Code composto" do Pix Automático que também traga uma localização de pagamento em 26-25 é interpretado como um payload dinâmico comum e sua localização de recorrência é descartada, então quem precisa distinguir os dois não pode se apoiar neste parser. Só um payload sem nenhum template Pix nos IDs 26 a 51 retorna `null`. ```javascript import { getPixPayloadInfo } from '@brazilian-utils/brazilian-utils'; @@ -238,10 +238,10 @@ getPixPayloadInfo( '5204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041D3D' ); // { -// key: '123e4567-e12b-12d1-a456-426655440000', // merchantName: 'Fulano de Tal', // merchantCity: 'BRASILIA', -// pointOfInitiation: 'static' +// pointOfInitiation: 'static', +// key: '123e4567-e12b-12d1-a456-426655440000' // } ``` @@ -335,7 +335,7 @@ getNfeKeyInfo('35170458716523000119550010000000121000123458'); getNfeKeyInfo('35170458716523000119620010000000121000123450'); // { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', -// series: 1, number: 12, emissionType: 1, authorizationSite: 0, code: '0012345', checkDigit: 0 } +// series: 1, number: 12, emissionType: 1, code: '0012345', checkDigit: 0, authorizationSite: 0 } getNfeKeyInfo('invalid'); // null ``` @@ -590,7 +590,7 @@ const addressFromNumber = await getAddressInfoByCep(1310100); ## isValidProcessoJuridico -Valida o número do processo jurídico de acordo com definição do [CNJ](https://atos.cnj.jus.br/atos/detalhar/119): o layout `NNNNNNN-DD.AAAA.J.TR.OOOO`, os dígitos verificadores `DD` e o par `J`/`TR`, que precisa nomear um órgão e um tribunal que a Resolução CNJ nº 65/2008 criou, de modo que um número com dígito verificador correto mas com um tribunal inexistente é rejeitado. As listas fechadas vêm do art. 1º, § 4º e § 5º da resolução, o § 5º, III na redação que a Resolução CNJ nº 477/2022 lhe deu para acomodar o TRF da 6ª Região. A unidade de origem (`OOOO`) é lida apenas como quatro dígitos, já que o art. 1º, § 6º deixa a codificação dela a cargo de cada tribunal e não publica lista central. Os separadores da máscara do CNJ (espaços, `.` e `-`) são aceitos entre os campos, mas qualquer outro caractere, uma letra em especial, invalida o valor. +Valida o número do processo jurídico de acordo com definição do [CNJ](https://atos.cnj.jus.br/atos/detalhar/119): o layout `NNNNNNN-DD.AAAA.J.TR.OOOO`, os dígitos verificadores `DD` e o par `J`/`TR`, que precisa identificar um órgão e um tribunal existentes nas listas fechadas definidas pela Resolução CNJ nº 65/2008, de modo que um número com dígito verificador correto mas com um tribunal inexistente é rejeitado. As listas fechadas vêm do art. 1º, § 4º e § 5º da resolução, o § 5º, III na redação que a Resolução CNJ nº 477/2022 lhe deu para acomodar o TRF da 6ª Região. A unidade de origem (`OOOO`) é lida apenas como quatro dígitos, já que o art. 1º, § 6º deixa a codificação dela a cargo de cada tribunal e não publica lista central. Os separadores da máscara do CNJ (espaços, `.` e `-`) são aceitos entre os campos, e espaços em branco ao redor do valor são ignorados, mas qualquer outro caractere, uma letra em especial, invalida o valor. ```javascript import { isValidProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -818,7 +818,7 @@ parseIban('br15-0000.0000/0000 1093 2840 814p-2'); // 'BR15000000000000109328408 ## getIbanInfo -Interpreta um IBAN brasileiro em seus campos: 2 (código do país, sempre `BR`) + 2 (dígitos verificadores ISO 7064 MOD 97-10) + 8 (ISPB) + 5 (agência) + 10 (conta) + 1 (tipo de conta, qualquer letra, normalmente `C` para conta corrente ou `P` para conta poupança) + 1 (indicador do titular, `1` a `9` e depois `A` a `Z`). Aceita as mesmas formas de entrada que `isValidIban`, compacta ou no formato impresso da ISO 13616 (grupos de 4 separados por um único espaço em branco, `.`, `-` ou `/`), em ambos os casos com espaços em branco opcionais no início e no fim e sem diferenciar maiúsculas de minúsculas, e retorna `null` sempre que `isValidIban` retornaria `false`, inclusive quando o valor carrega um separador fora do limite de um grupo, uma sequência de separadores ou qualquer caractere além de letras e dígitos. O resultado é tipado como `IbanInfo`, cujo `accountType` é uma `string`. +Interpreta um IBAN brasileiro em seus campos: 2 (código do país, sempre `BR`) + 2 (dígitos verificadores ISO 7064 MOD 97-10) + 8 (ISPB) + 5 (agência) + 10 (conta) + 1 (tipo de conta, qualquer letra, normalmente `C` para conta corrente ou `P` para conta poupança) + 1 (indicador do titular, `1` a `9` e depois `A` a `Z`). Apenas IBANs brasileiros são suportados: o layout de campos dos demais países da ISO 13616 está fora de escopo, então um IBAN bem formado que não seja `BR` também retorna `null`. Aceita as mesmas formas de entrada que `isValidIban`, compacta ou no formato impresso da ISO 13616 (grupos de 4 separados por um único espaço em branco, `.`, `-` ou `/`), em ambos os casos com espaços em branco opcionais no início e no fim e sem diferenciar maiúsculas de minúsculas, e retorna `null` sempre que `isValidIban` retornaria `false`, inclusive quando o valor carrega um separador fora do limite de um grupo, uma sequência de separadores ou qualquer caractere além de letras e dígitos. O resultado é tipado como `IbanInfo`, cujo `accountType` é uma `string`. ```javascript import { getIbanInfo } from '@brazilian-utils/brazilian-utils'; @@ -1097,7 +1097,7 @@ getCities('SP'); // ] ``` -`getCities` embute os nomes dos 5571 municípios do IBGE (~154,0 KB minificado, ~49,7 KB com gzip) e é uma das poucas exceções pesadas neste pacote, que é tree-shakeable no restante. Veja [Tamanho do bundle](getting-started.md#tamanho-do-bundle) para saber como carregá-lo sob demanda via `@brazilian-utils/brazilian-utils/get-cities` em vez do import da raiz. +`getCities` embute os nomes dos 5571 municípios do IBGE (~154,2 KB minificado, ~49,8 KB com gzip) e é uma das poucas exceções pesadas neste pacote, que é tree-shakeable no restante. Veja [Tamanho do bundle](getting-started.md#tamanho-do-bundle) para saber como carregá-lo sob demanda via `@brazilian-utils/brazilian-utils/get-cities` em vez do import da raiz. ## getHolidays @@ -1271,7 +1271,7 @@ Gera um número de processo jurídico válido de acordo com a definição do [CN import { generateProcessoJuridico } from '@brazilian-utils/brazilian-utils'; generateProcessoJuridico(); // '89478645020266070326' -generateProcessoJuridico({ year: 2026, court: 5 }); // string | null +generateProcessoJuridico({ year: 2026, court: 5 }); // '98412562120265087260' (Justiça do Trabalho, TRT da 8ª Região) generateProcessoJuridico({ year: 10000 }); // null (ano fora do intervalo) generateProcessoJuridico({ court: 10 }); // null (órgão inexistente) ``` @@ -1773,7 +1773,7 @@ parseCertidao('104539 01 55 2013 1 00012 021 0000123 21'); ## getCertidaoInfo -Extrai os campos da matrícula de uma certidão de registro civil, retornando `null` quando a matrícula é inválida, o que inclui um código de livro que não é um dos nove livros. O [art. 473, V do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lista os códigos de 1 a 7; nenhum texto primário do CNJ acessível hoje publica os outros dois, inclusive o Anexo IV do revogado Provimento CNJ nº 63/2017, que lista os mesmos sete. Os códigos 8 (emancipação) e 9 (interdição) vêm das referências em que a regra do dígito verificador se apoia: o [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e o [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) publicam a lista dos nove livros. Eles são mantidos porque matrículas com eles circulam. Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. +Extrai os campos da matrícula de uma certidão de registro civil, retornando `null` quando a matrícula é inválida, o que inclui um código de livro que não é um dos nove livros. Um serviço diferente do `55` que o art. 473, III fixa para o registro civil das pessoas naturais também resulta em `null`. O [art. 473, V do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lista os códigos de 1 a 7; nenhum texto primário do CNJ acessível hoje publica os outros dois, inclusive o Anexo IV do revogado Provimento CNJ nº 63/2017, que lista os mesmos sete. Os códigos 8 (emancipação) e 9 (interdição) vêm das referências em que a regra do dígito verificador se apoia: o [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e o [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) publicam a lista dos nove livros. Eles são mantidos porque matrículas com eles circulam. Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. ```javascript import { getCertidaoInfo } from '@brazilian-utils/brazilian-utils'; @@ -1874,7 +1874,7 @@ formatCno('979', { pad: true }); // 00.000.00009/79 ## parseCno -Remove a formatação do CNO (Cadastro Nacional de Obras), mantém apenas os dígitos e limita o resultado a 12 dígitos, a numeração que o CNO herdou do CEI. Use `isValidCno` para verificar o número em si. +Remove a formatação do CNO (Cadastro Nacional de Obras), mantém apenas os dígitos e limita o resultado a 12 dígitos, a numeração que o CNO herdou do CEI. Um valor mais curto passa adiante até onde vai; use `isValidCno` para verificar o número em si. ```javascript import { parseCno } from '@brazilian-utils/brazilian-utils'; @@ -1911,7 +1911,7 @@ formatCaepf('184', { pad: true }); // 000.000.000/001-84 ## parseCaepf -Remove a formatação do CAEPF (Cadastro de Atividade Econômica da Pessoa Física), mantém apenas os dígitos e limita o resultado a 14 dígitos. Use `isValidCaepf` para verificar o número em si. +Remove a formatação do CAEPF (Cadastro de Atividade Econômica da Pessoa Física), mantém apenas os dígitos e limita o resultado a 14 dígitos. Um valor mais curto passa adiante até onde vai; use `isValidCaepf` para verificar o número em si. ```javascript import { parseCaepf } from '@brazilian-utils/brazilian-utils'; @@ -1921,7 +1921,7 @@ parseCaepf('293.118.610/001-84'); // '29311861000184' ## isValidRegistroProfissional -Verifica a estrutura de um número de registro/inscrição profissional. Recebe um único objeto, tipado como `IsValidRegistroProfissionalOptions`, no mesmo formato do `isValidBankAccount`: `value` é o número do registro, `council` escolhe o conselho emissor (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` ou `"CRC"`) e o `stateCode` opcional verifica a UF embutida (ignorado para `"CRP"`, cujo prefixo de 2 dígitos é um código regional, não uma UF literal). Qualquer coisa que não seja um objeto, e um objeto sem `value` ou sem `council`, é `false`. É apenas uma verificação estrutural: a quantidade de dígitos e a UF são validadas, mas nenhum dígito verificador é calculado, mesmo para o CRC, cujo formato inclui um. Um registro no CRC é a UF, 6 dígitos, o tipo de registro (`"O"` Originário ou `"P"` Provisório, que nada diz sobre a categoria profissional) e o dígito verificador, conforme o [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). Um Registro Transferido ou Secundário acrescenta `"T"` ou `"S"` e a UF do CRC de destino **depois** do dígito verificador, conforme esse mesmo item e a [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: os exemplos do próprio Manual são `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` e `PI-111222/O-5 S-AC`. As duas UFs precisam ser códigos reais, e o `stateCode` é comparado com a de origem. O código regional do CRP precisa ser um dos [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) do sistema CFP, de CRP-01 a CRP-24. Só o formato do CRC e esses códigos regionais do CRP se apoiam em fonte publicada: a página do CFP não publica o tamanho do número de inscrição, e a OAB, o CFM e o CFO não publicam formato algum, então as faixas de dígitos aceitas para `"CRP"`, `"OAB"`, `"CRM"` e `"CRO"` são convencionais, não normativas (a busca pública da OAB/SP tem `maxlength="7"`, e o CFM documenta CRMs com prefixo `300` e sufixo `P`, nenhum deles expresso por esses formatos). O CREA não é suportado: seu formato de registro não pôde ser confirmado em uma fonte oficial e publicamente documentada após a unificação nacional de 2016 (RNP). +Verifica a estrutura de um número de registro/inscrição profissional. Recebe um único objeto, tipado como `IsValidRegistroProfissionalOptions`, no mesmo formato do `isValidBankAccount`: `value` é o número do registro, `council` escolhe o conselho emissor (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` ou `"CRC"`) e o `stateCode` opcional verifica a UF embutida (ignorado para `"CRP"`, cujo prefixo de 2 dígitos é um código regional, não uma UF literal). Qualquer coisa que não seja um objeto, e um objeto sem `value` ou sem `council`, é `false`. Os formatos aceitos são de 4 a 6 dígitos mais a UF para `"OAB"` e `"CRM"`, de 3 a 6 dígitos mais a UF para `"CRO"`, um código regional de 2 dígitos mais 4 a 6 dígitos para `"CRP"`, e a UF mais 6 dígitos, o tipo de registro e um dígito verificador para `"CRC"`. É apenas uma verificação estrutural: a quantidade de dígitos e a UF são validadas, mas nenhum dígito verificador é calculado, mesmo para o CRC, cujo formato inclui um. Um registro no CRC é a UF, 6 dígitos, o tipo de registro (`"O"` Originário ou `"P"` Provisório, que nada diz sobre a categoria profissional) e o dígito verificador, conforme o [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). Um Registro Transferido ou Secundário acrescenta `"T"` ou `"S"` e a UF do CRC de destino **depois** do dígito verificador, conforme esse mesmo item e a [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: os exemplos do próprio Manual são `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` e `PI-111222/O-5 S-AC`. As duas UFs precisam ser códigos reais, e o `stateCode` é comparado com a de origem. O código regional do CRP precisa ser um dos [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) do sistema CFP, de CRP-01 a CRP-24. Só o formato do CRC e esses códigos regionais do CRP se apoiam em fonte publicada: a página do CFP não publica o tamanho do número de inscrição, e a OAB, o CFM e o CFO não publicam formato algum, então as faixas de dígitos aceitas para `"CRP"`, `"OAB"`, `"CRM"` e `"CRO"` são convencionais, não normativas (a busca pública da OAB/SP tem `maxlength="7"`, e o CFM documenta CRMs com prefixo `300` e sufixo `P`, nenhum deles expresso por esses formatos). O CREA não é suportado: seu formato de registro não pôde ser confirmado em uma fonte oficial e publicamente documentada após a unificação nacional de 2016 (RNP). ```javascript import { isValidRegistroProfissional } from '@brazilian-utils/brazilian-utils'; @@ -1969,7 +1969,7 @@ Os títulos das ocupações vêm da [tabela oficial de ocupações da CBO 2002 p ## parseCbo -Remove a formatação do CBO (Classificação Brasileira de Ocupações), mantém apenas os dígitos e limita o resultado a 6 dígitos. Nada é preenchido com zeros à esquerda aqui, então o zero inicial de um código como `010205` precisa ser escrito; use `getCbo` ou `isValidCbo`, que preenchem um código numérico sem máscara, para consultar uma ocupação. +Remove a formatação do CBO (Classificação Brasileira de Ocupações), mantém apenas os dígitos e limita o resultado a 6 dígitos. Um valor mais curto passa adiante até onde vai e nada é preenchido com zeros à esquerda aqui, então o zero inicial de um código como `010205` precisa ser escrito; use `getCbo` ou `isValidCbo`, que preenchem um código numérico sem máscara, para consultar uma ocupação. ```javascript import { parseCbo } from '@brazilian-utils/brazilian-utils'; @@ -2113,7 +2113,7 @@ isValidCfop(-5102); // false (não é um inteiro seguro não negativo) ## parseCfop -Remove a formatação do CFOP (Código Fiscal de Operações e Prestações), mantém apenas os dígitos e limita o resultado a 4 dígitos. Nenhum código CFOP começa com zero, o primeiro dígito é o grupo da operação, de 1 a 7, então nada é preenchido com zeros aqui. +Remove a formatação do CFOP (Código Fiscal de Operações e Prestações), mantém apenas os dígitos e limita o resultado a 4 dígitos. Um valor mais curto passa adiante até onde vai. Nenhum código CFOP começa com zero, o primeiro dígito é o grupo da operação, de 1 a 7, então nada é preenchido com zeros aqui. ```javascript import { parseCfop } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/utilities.md b/docs/utilities.md index 69846560..5a496e51 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -196,7 +196,7 @@ isValidPixKey('not a key'); // false ## getPixKeyInfo -Identifies a Pix key and normalizes it to the canonical form the DICT expects inside a BR Code: 11 digit CPF, 14 character CNPJ, lowercased e-mail, E.164 mobile phone (a landline is not a Pix key) or lowercase UUID EVP. An 11 digit value that is valid both as a CPF and as a mobile phone is read as a CPF, unless it was written as a phone number (a `+55`/`0055` prefix or a DDD wrapped in parentheses). The CPF and the phone number are recognized by the way they are written, not only by the digits they carry, so surrounding text is not stripped away and `'abc123.456.789-09'` is not a CPF key. Returns `null` when the value is not a valid Pix key. The result is typed as `PixKeyInfo`. +Identifies a Pix key and normalizes it to the canonical form the DICT expects inside a BR Code: 11 digit CPF, 14 character CNPJ, lowercased e-mail, E.164 mobile phone (a landline is not a Pix key) or lowercase UUID EVP. An 11 digit value that is valid both as a CPF and as a mobile phone is read as a CPF, unless it was written as a phone number (a `+55`/`0055` prefix or a DDD wrapped in parentheses). The CPF and the phone number are recognized by the way they are written, not only by the digits they carry, so surrounding text is not stripped away and `'abc123.456.789-09'` is not a CPF key. An e-mail key is trimmed and lowercased, and one longer than the 77 characters the DICT allows is rejected. A value whose digits carry a valid CNPJ check digit is read as a CNPJ even when it starts with `0055`, since a phone key inside a BR Code always carries the `+55` prefix. Returns `null` when the value is not a valid Pix key. The result is typed as `PixKeyInfo`. ```javascript import { getPixKeyInfo } from '@brazilian-utils/brazilian-utils'; @@ -228,7 +228,7 @@ isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (broken CRC) ## getPixPayloadInfo -Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The result is typed as `PixPayloadInfo`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. A `fss` written next to a PSP location returns `null`: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template of §2.6. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Unreserved Templates (IDs 80 to 99) are ignored: a "QR Code composto" of Pix Automático that also carries a payment location in 26-25 is parsed as an ordinary dynamic payload and its recurrence location is dropped, so a consumer that has to tell the two apart cannot rely on this parser. Only a payload with no Pix template at all in IDs 26 to 51 returns `null`. +Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The Pix key itself is not validated, since the manual allows a static QR Code built around a key that no longer exists in the DICT; key ownership is only settled at payment time. The "Additional Data Field Template" (ID 62) is mandatory in the BR Code table but optional in the EMV® specification it refers to, so it is accepted when absent. The lengths the manual reserves for the merchant name (25), the merchant city (15), the `txid` (25) and the Pix key field 26-01 (77) are generator side limits, enforced by `generatePixPayload` and not checked here, since payloads in the wild routinely overrun them. The result is typed as `PixPayloadInfo`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. A `fss` written next to a PSP location returns `null`: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template of §2.6. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Unreserved Templates (IDs 80 to 99) are ignored: a "QR Code composto" of Pix Automático that also carries a payment location in 26-25 is parsed as an ordinary dynamic payload and its recurrence location is dropped, so a consumer that has to tell the two apart cannot rely on this parser. Only a payload with no Pix template at all in IDs 26 to 51 returns `null`. ```javascript import { getPixPayloadInfo } from '@brazilian-utils/brazilian-utils'; @@ -238,10 +238,10 @@ getPixPayloadInfo( '5204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041D3D' ); // { -// key: '123e4567-e12b-12d1-a456-426655440000', // merchantName: 'Fulano de Tal', // merchantCity: 'BRASILIA', -// pointOfInitiation: 'static' +// pointOfInitiation: 'static', +// key: '123e4567-e12b-12d1-a456-426655440000' // } ``` @@ -335,7 +335,7 @@ getNfeKeyInfo('35170458716523000119550010000000121000123458'); getNfeKeyInfo('35170458716523000119620010000000121000123450'); // { stateCode: 'SP', year: 2017, month: 4, taxId: '58716523000119', model: '62', -// series: 1, number: 12, emissionType: 1, authorizationSite: 0, code: '0012345', checkDigit: 0 } +// series: 1, number: 12, emissionType: 1, code: '0012345', checkDigit: 0, authorizationSite: 0 } getNfeKeyInfo('invalid'); // null ``` @@ -590,7 +590,7 @@ const addressFromNumber = await getAddressInfoByCep(1310100); ## isValidProcessoJuridico -Validate the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119): the `NNNNNNN-DD.AAAA.J.TR.OOOO` layout, the `DD` check digits and the `J`/`TR` pair, which has to name an órgão and a tribunal Resolução CNJ nº 65/2008 created, so a number carrying a correct check digit but a court that does not exist is rejected. The closed lists come from art. 1º, § 4º and § 5º of the resolution, § 5º, III in the wording Resolução CNJ nº 477/2022 gave it to seat the TRF da 6ª Região. The unidade de origem (`OOOO`) is only read as four digits, since art. 1º, § 6º leaves its codification to each tribunal and publishes no central list. The CNJ mask separators (whitespace, `.` and `-`) are accepted between the fields, but any other character, a letter in particular, makes the value invalid. +Validate the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119): the `NNNNNNN-DD.AAAA.J.TR.OOOO` layout, the `DD` check digits and the `J`/`TR` pair, which must identify an existing órgão and tribunal from the closed lists defined by Resolução CNJ nº 65/2008, so a number carrying a correct check digit but a court that does not exist is rejected. The closed lists come from art. 1º, § 4º and § 5º of the resolution, § 5º, III in the wording Resolução CNJ nº 477/2022 gave it to seat the TRF da 6ª Região. The unidade de origem (`OOOO`) is only read as four digits, since art. 1º, § 6º leaves its codification to each tribunal and publishes no central list. The CNJ mask separators (whitespace, `.` and `-`) are accepted between the fields, and whitespace around the value is ignored, but any other character, a letter in particular, makes the value invalid. ```javascript import { isValidProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -818,7 +818,7 @@ parseIban('br15-0000.0000/0000 1093 2840 814p-2'); // 'BR15000000000000109328408 ## getIbanInfo -Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups of 4 split by a single whitespace, `.`, `-` or `/`), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying a separator away from a group boundary, a run of separators or any character other than letters and digits. The result is typed as `IbanInfo`, whose `accountType` is a `string`. +Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Only Brazilian IBANs are supported: the field layout of the other ISO 13616 countries is out of scope, so a well-formed non `BR` IBAN also returns `null`. Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups of 4 split by a single whitespace, `.`, `-` or `/`), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying a separator away from a group boundary, a run of separators or any character other than letters and digits. The result is typed as `IbanInfo`, whose `accountType` is a `string`. ```javascript import { getIbanInfo } from '@brazilian-utils/brazilian-utils'; @@ -1097,7 +1097,7 @@ getCities('SP'); // ] ``` -`getCities` embeds all 5571 IBGE municipality names (~154.0 KB minified, ~49.7 KB gzipped) and is one of the few heavy exceptions in this otherwise tree-shakeable package. See [Bundle size](getting-started.md#bundle-size) for how to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities` instead of the root import. +`getCities` embeds all 5571 IBGE municipality names (~154.2 KB minified, ~49.8 KB gzipped) and is one of the few heavy exceptions in this otherwise tree-shakeable package. See [Bundle size](getting-started.md#bundle-size) for how to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities` instead of the root import. ## getHolidays @@ -1271,7 +1271,7 @@ Generate a valid random processo jurídico number according to [CNJ's definition import { generateProcessoJuridico } from '@brazilian-utils/brazilian-utils'; generateProcessoJuridico(); // '89478645020266070326' -generateProcessoJuridico({ year: 2026, court: 5 }); // string | null +generateProcessoJuridico({ year: 2026, court: 5 }); // '98412562120265087260' (Justiça do Trabalho, TRT da 8ª Região) generateProcessoJuridico({ year: 10000 }); // null (year out of range) generateProcessoJuridico({ court: 10 }); // null (no such órgão) ``` @@ -1773,7 +1773,7 @@ parseCertidao('104539 01 55 2013 1 00012 021 0000123 21'); ## getCertidaoInfo -Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. [Art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lists the codes 1 to 7; no CNJ primary text reachable today publishes the other two, the Anexo IV of the revoked Provimento CNJ nº 63/2017 included, which lists the same seven. The codes 8 (emancipação) and 9 (interdição) come from the references the check digit rule rests on: [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) both print the nine book list. They are kept because matrículas carrying them circulate. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. +Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. A serviço other than the `55` that art. 473, III fixes for the registro civil das pessoas naturais also gives `null`. [Art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lists the codes 1 to 7; no CNJ primary text reachable today publishes the other two, the Anexo IV of the revoked Provimento CNJ nº 63/2017 included, which lists the same seven. The codes 8 (emancipação) and 9 (interdição) come from the references the check digit rule rests on: [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) both print the nine book list. They are kept because matrículas carrying them circulate. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. ```javascript import { getCertidaoInfo } from '@brazilian-utils/brazilian-utils'; @@ -1874,7 +1874,7 @@ formatCno('979', { pad: true }); // 00.000.00009/79 ## parseCno -Remove CNO (Cadastro Nacional de Obras) formatting, keep only digits, and cap the result to 12 digits, the numbering the CNO kept from the CEI. Use `isValidCno` to check the number itself. +Remove CNO (Cadastro Nacional de Obras) formatting, keep only digits, and cap the result to 12 digits, the numbering the CNO kept from the CEI. A shorter value passes through as far as it goes; use `isValidCno` to check the number itself. ```javascript import { parseCno } from '@brazilian-utils/brazilian-utils'; @@ -1911,7 +1911,7 @@ formatCaepf('184', { pad: true }); // 000.000.000/001-84 ## parseCaepf -Remove CAEPF (Cadastro de Atividade Econômica da Pessoa Física) formatting, keep only digits, and cap the result to 14 digits. Use `isValidCaepf` to check the number itself. +Remove CAEPF (Cadastro de Atividade Econômica da Pessoa Física) formatting, keep only digits, and cap the result to 14 digits. A shorter value passes through as far as it goes; use `isValidCaepf` to check the number itself. ```javascript import { parseCaepf } from '@brazilian-utils/brazilian-utils'; @@ -1921,7 +1921,7 @@ parseCaepf('293.118.610/001-84'); // '29311861000184' ## isValidRegistroProfissional -Check the structure of a professional council registration number (registro/inscrição profissional). It takes a single object, typed as `IsValidRegistroProfissionalOptions`, the shape `isValidBankAccount` takes: `value` is the registration number, `council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). Anything that is not an object, and an object missing `value` or `council`, is `false`. This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits, the tipo de registro (`"O"` Originário or `"P"` Provisório, which says nothing about the professional category) and the check digit, as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). A Registro Transferido or Secundário appends `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, per that same item and [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: the Manual's own examples are `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` and `PI-111222/O-5 S-AC`. Both UFs must be real state codes, and `stateCode` is compared against the originating one. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. Only the CRC shape and those CRP regional codes rest on a published source: the CFP page publishes no length for the inscription number itself, and the OAB, the CFM and the CFO publish no format at all, so the digit ranges accepted for `"CRP"`, `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative (the OAB/SP public search field is `maxlength="7"`, and the CFM documents `300`-prefixed and `P`-suffixed CRMs, none of which these shapes express). CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). +Check the structure of a professional council registration number (registro/inscrição profissional). It takes a single object, typed as `IsValidRegistroProfissionalOptions`, the shape `isValidBankAccount` takes: `value` is the registration number, `council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). Anything that is not an object, and an object missing `value` or `council`, is `false`. The accepted shapes are 4 to 6 digits plus the UF for `"OAB"` and `"CRM"`, 3 to 6 digits plus the UF for `"CRO"`, a 2 digit regional code plus 4 to 6 digits for `"CRP"`, and the UF plus 6 digits, the tipo de registro and one check digit for `"CRC"`. This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits, the tipo de registro (`"O"` Originário or `"P"` Provisório, which says nothing about the professional category) and the check digit, as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). A Registro Transferido or Secundário appends `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, per that same item and [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: the Manual's own examples are `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` and `PI-111222/O-5 S-AC`. Both UFs must be real state codes, and `stateCode` is compared against the originating one. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. Only the CRC shape and those CRP regional codes rest on a published source: the CFP page publishes no length for the inscription number itself, and the OAB, the CFM and the CFO publish no format at all, so the digit ranges accepted for `"CRP"`, `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative (the OAB/SP public search field is `maxlength="7"`, and the CFM documents `300`-prefixed and `P`-suffixed CRMs, none of which these shapes express). CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). ```javascript import { isValidRegistroProfissional } from '@brazilian-utils/brazilian-utils'; @@ -1969,7 +1969,7 @@ The occupation titles come from the [official CBO 2002 occupation table publishe ## parseCbo -Remove CBO (Classificação Brasileira de Ocupações) formatting, keep only digits, and cap the result to 6 digits. Nothing is left padded here, so the leading zero of a code such as `010205` has to be written out; use `getCbo` or `isValidCbo`, which do pad a bare numeric code, to look an occupation up. +Remove CBO (Classificação Brasileira de Ocupações) formatting, keep only digits, and cap the result to 6 digits. A shorter value passes through as far as it goes and nothing is left padded here, so the leading zero of a code such as `010205` has to be written out; use `getCbo` or `isValidCbo`, which do pad a bare numeric code, to look an occupation up. ```javascript import { parseCbo } from '@brazilian-utils/brazilian-utils'; @@ -2113,7 +2113,7 @@ isValidCfop(-5102); // false (not a non-negative safe integer) ## parseCfop -Remove CFOP (Código Fiscal de Operações e Prestações) formatting, keep only digits, and cap the result to 4 digits. No CFOP code starts with a zero, its first digit is the operation group from 1 to 7, so nothing is ever padded here. +Remove CFOP (Código Fiscal de Operações e Prestações) formatting, keep only digits, and cap the result to 4 digits. A shorter value passes through as far as it goes. No CFOP code starts with a zero, its first digit is the operation group from 1 to 7, so nothing is ever padded here. ```javascript import { parseCfop } from '@brazilian-utils/brazilian-utils'; diff --git a/scripts/llms.ts b/scripts/llms.ts index 3b154366..edd5180e 100644 --- a/scripts/llms.ts +++ b/scripts/llms.ts @@ -72,6 +72,23 @@ function firstSentence(paragraph: string): string { return sentence.split(ABBREVIATION_PLACEHOLDER).join(".").trim(); } +const DEPRECATION_MARKER = "**Deprecated:**"; + +/** + * Extracts the `**Deprecated:** ...` sentence of a paragraph, without its markdown bold. The + * description of an entry is its first sentence, and a deprecation notice never is the first + * sentence, so without this it would be dropped from the generated index. + * @param {string} paragraph - The paragraph to read the deprecation notice of. + * @returns {string} The deprecation sentence, or an empty string when the paragraph carries none. + */ +function deprecationSentence(paragraph: string): string { + const markerIndex = paragraph.indexOf(DEPRECATION_MARKER); + + if (markerIndex === -1) return ""; + + return firstSentence(paragraph.slice(markerIndex).replaceAll("**", "")); +} + /** * Parses every `## ` section of `utilities.md` into name/slug/description. * @param {string} utilitiesMd - The full contents of `utilities.md`. @@ -86,11 +103,15 @@ function parseUtilities(utilitiesMd: string): UtilSection[] { const body = section.slice(newlineIndex + 1); const [firstParagraphRaw = ""] = body.split(/\n\s*\n/); const firstParagraph = firstParagraphRaw.trim(); + const description = firstSentence(firstParagraph); + const deprecation = description.includes(DEPRECATION_MARKER) + ? "" + : deprecationSentence(firstParagraph); return { name, slug: slugify(name), - description: firstSentence(firstParagraph), + description: deprecation === "" ? description : `${description} ${deprecation}`, }; }); }