From 38c64fbf9fa909783dc9a5f1827994bcbc2dc53f Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 19 Sep 2026 10:50:50 -0300 Subject: [PATCH 1/4] =?UTF-8?q?refactor(generate-cpf):=20move=20the=20regi?= =?UTF-8?q?=C3=A3o=20fiscal=20table=20to=20the=20shared=20constants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getCpfInfo reads the same state to região fiscal digit table generateCpf writes with, and a table used by two modules lives under src/_internals/constants. The table and the base length move to constants/cpf.ts under CPF prefixed names; generateCpf behaves as before. --- src/_internals/constants/cpf.ts | 43 +++++++++++++++++++++++++++ src/generate-cpf/constants.ts | 33 -------------------- src/generate-cpf/generate-cpf.test.ts | 13 ++++---- src/generate-cpf/generate-cpf.ts | 11 ++++--- 4 files changed, 56 insertions(+), 44 deletions(-) delete mode 100644 src/generate-cpf/constants.ts diff --git a/src/_internals/constants/cpf.ts b/src/_internals/constants/cpf.ts index 1c7a36256..0ef7a3ef8 100644 --- a/src/_internals/constants/cpf.ts +++ b/src/_internals/constants/cpf.ts @@ -1,2 +1,45 @@ +import { type StateCode } from "./states"; + /** Digits of a CPF. */ export const CPF_LENGTH = 11; + +/** Digits of the base of a CPF, the sequential number ahead of the região fiscal digit. */ +export const CPF_BASE_LENGTH = 8; + +/** + * The região fiscal digit (the 9th digit of a CPF) of each state, as listed by the Receita + * Federal in the folheto "Cadastros: CPF e CNPJ": 1 for DF, GO, MT, MS and TO; 2 for PA, AM, AC, + * AP, RO and RR; 3 for CE, MA and PI; 4 for PE, RN, PB and AL; 5 for BA and SE; 6 for MG; 7 for + * RJ and ES; 8 for SP; 9 for PR and SC; 0 for RS. + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/educacao-fiscal/educacao_fiscal/folhetos-orientativos/cadastros-dig.pdf + */ +export const CPF_FISCAL_REGION_BY_STATE: Record = { + AC: "2", + AL: "4", + AP: "2", + AM: "2", + BA: "5", + CE: "3", + DF: "1", + ES: "7", + GO: "1", + MA: "3", + MT: "1", + MS: "1", + MG: "6", + PR: "9", + PB: "4", + PA: "2", + PE: "4", + PI: "3", + RN: "4", + RS: "0", + RJ: "7", + RO: "2", + RR: "2", + SC: "9", + SE: "5", + SP: "8", + TO: "1", +}; diff --git a/src/generate-cpf/constants.ts b/src/generate-cpf/constants.ts deleted file mode 100644 index 758b1f5f1..000000000 --- a/src/generate-cpf/constants.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { type StateCode } from "../_internals/constants/states"; - -export const BASE_LENGTH = 8; - -export const STATE_CODES: Record = { - AC: "2", - AL: "4", - AP: "2", - AM: "2", - BA: "5", - CE: "3", - DF: "1", - ES: "7", - GO: "1", - MA: "3", - MT: "1", - MS: "1", - MG: "6", - PR: "9", - PB: "4", - PA: "2", - PE: "4", - PI: "3", - RN: "4", - RS: "0", - RJ: "7", - RO: "2", - RR: "2", - SC: "9", - SE: "5", - SP: "8", - TO: "1", -} as const; diff --git a/src/generate-cpf/generate-cpf.test.ts b/src/generate-cpf/generate-cpf.test.ts index 63beca5b9..fa44665d0 100644 --- a/src/generate-cpf/generate-cpf.test.ts +++ b/src/generate-cpf/generate-cpf.test.ts @@ -1,11 +1,10 @@ import * as fc from "fast-check"; -import { CPF_LENGTH } from "../_internals/constants/cpf"; +import { CPF_FISCAL_REGION_BY_STATE, 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"; import { generateCpf } from "./generate-cpf"; describe("generateCpf", () => { @@ -50,15 +49,15 @@ describe("generateCpf", () => { } }); - test("should embed the literal STATE_CODES digit at the 9th position, not a random one", () => { + test("should embed the literal CPF_FISCAL_REGION_BY_STATE digit at the 9th position, not a random one", () => { for (let i = 0; i < 20; i++) { - expect(generateCpf("SP")[8]).toBe(STATE_CODES.SP); + expect(generateCpf("SP")[8]).toBe(CPF_FISCAL_REGION_BY_STATE.SP); } }); test("should embed the 1st região fiscal digit for the states the Receita Federal groups there", () => { - expect(STATE_CODES.MS).toBe("1"); - expect(STATE_CODES.MT).toBe("1"); + expect(CPF_FISCAL_REGION_BY_STATE.MS).toBe("1"); + expect(CPF_FISCAL_REGION_BY_STATE.MT).toBe("1"); expect(generateCpf("MS")[8]).toBe("1"); expect(generateCpf("MT")[8]).toBe("1"); }); @@ -102,7 +101,7 @@ describe("generateCpf", () => { const cpf = generateCpf(state); expect(cpf).toHaveLength(CPF_LENGTH); - expect(cpf[8]).toBe(STATE_CODES[state]); + expect(cpf[8]).toBe(CPF_FISCAL_REGION_BY_STATE[state]); expect(isValidCpf(cpf)).toBe(true); }), ); diff --git a/src/generate-cpf/generate-cpf.ts b/src/generate-cpf/generate-cpf.ts index 2d9b8798a..525362a25 100644 --- a/src/generate-cpf/generate-cpf.ts +++ b/src/generate-cpf/generate-cpf.ts @@ -1,8 +1,8 @@ import { calculateCpfCheckDigit } from "../_internals/calculate-cpf-check-digit/calculate-cpf-check-digit"; +import { CPF_BASE_LENGTH, CPF_FISCAL_REGION_BY_STATE } from "../_internals/constants/cpf"; import { type StateCode } from "../_internals/constants/states"; import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; -import { BASE_LENGTH, STATE_CODES } from "./constants"; export type { StateCode } from "../_internals/constants/states"; @@ -16,7 +16,10 @@ export type { StateCode } from "../_internals/constants/states"; * @returns {string} The região fiscal digit of that state, or a random digit. */ const getStateCode = (state?: StateCode): string => { - if (typeof state === "string" && Object.hasOwn(STATE_CODES, state)) return STATE_CODES[state]; + if (typeof state === "string" && Object.hasOwn(CPF_FISCAL_REGION_BY_STATE, state)) { + return CPF_FISCAL_REGION_BY_STATE[state]; + } + return generateRandomNumber(1); }; @@ -52,10 +55,10 @@ const getStateCode = (state?: StateCode): string => { * @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); + let base = generateRandomNumber(CPF_BASE_LENGTH) + getStateCode(state); while (isRepeatedDigits(base)) { - base = generateRandomNumber(BASE_LENGTH) + getStateCode(state); + base = generateRandomNumber(CPF_BASE_LENGTH) + getStateCode(state); } const firstCheckDigit = String(calculateCpfCheckDigit(base)); From 6cc0ab5d2b3e9ce80f036774080ef4ca7a9db9b4 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 19 Sep 2026 10:50:50 -0300 Subject: [PATCH 2/4] feat(get-cpf-info): add getCpfInfo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A CPF carries more than its check digits: the 9th digit is the Região Fiscal of the Receita Federal the number was registered in. getCpfInfo returns the base, that digit, the states of the region and the check digits of a valid CPF, masked or not, and null for anything isValidCpf rejects. The region to state mapping was checked against the Receita Federal's folheto "Cadastros: CPF e CNPJ" and its Superintendências Regionais page. --- docs/pt-br/utilities.md | 31 +++++ docs/utilities.md | 31 +++++ reports/api/brazilian-utils.api.md | 11 ++ src/get-cpf-info/get-cpf-info.test.ts | 193 ++++++++++++++++++++++++++ src/get-cpf-info/get-cpf-info.ts | 74 ++++++++++ src/index.test.ts | 3 + src/index.ts | 1 + 7 files changed, 344 insertions(+) create mode 100644 src/get-cpf-info/get-cpf-info.test.ts create mode 100644 src/get-cpf-info/get-cpf-info.ts diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index a4adb4a72..f8487d222 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -73,6 +73,37 @@ generateCpf('SP'); // o 9º dígito é 8, o código da região fiscal de SP generateCpf('MG'); // o 9º dígito é 6, o código da região fiscal de MG ``` +### getCpfInfo + +Lê os campos que um CPF codifica, como um `CpfInfo`: a `base` de 8 dígitos, o dígito `fiscalRegion` (o 9º dígito, a Região Fiscal da Receita Federal em que o CPF foi inscrito, `"1"` a `"9"` e `"0"` para a 10ª), os `states` dessa região (`StateCode[]`, ordenados pelo nome do estado) e os 2 `checkDigits`. Aceita a mesma entrada com ou sem máscara que o `isValidCpf` e retorna `null` para tudo que não for um CPF válido. A região é a do endereço informado na primeira inscrição: ela diz onde o CPF foi emitido, não onde o titular nasceu ou mora, e uma região com mais de um estado não diz qual deles foi. + +| `fiscalRegion` | `states` | +| --- | --- | +| `"1"` | DF, GO, MT, MS, TO | +| `"2"` | AC, AP, AM, PA, RO, RR | +| `"3"` | CE, MA, PI | +| `"4"` | AL, PB, PE, RN | +| `"5"` | BA, SE | +| `"6"` | MG | +| `"7"` | ES, RJ | +| `"8"` | SP | +| `"9"` | PR, SC | +| `"0"` | RS | + +```javascript +import { getCpfInfo } from '@brazilian-utils/brazilian-utils'; + +getCpfInfo('123.456.789-09'); +// { +// base: '12345678', +// fiscalRegion: '9', +// states: ['PR', 'SC'], +// checkDigits: '09', +// } + +getCpfInfo('12345678900'); // null (dígitos verificadores inválidos) +``` + Fonte: [Receita Federal, "Cadastros: CPF e CNPJ"](https://www.gov.br/receitafederal/pt-br/assuntos/educacao-fiscal/educacao_fiscal/folhetos-orientativos/cadastros-dig.pdf). ## CNPJ diff --git a/docs/utilities.md b/docs/utilities.md index ad699c341..9f08de7a4 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -73,6 +73,37 @@ generateCpf('SP'); // the 9th digit is 8, the SP região fiscal code generateCpf('MG'); // the 9th digit is 6, the MG região fiscal code ``` +### getCpfInfo + +Read the fields a CPF encodes, as a `CpfInfo`: the 8 digit `base`, the `fiscalRegion` digit (the 9th digit, the Região Fiscal of the Receita Federal the CPF was registered in, `"1"` to `"9"` and `"0"` for the 10ª), the `states` of that region (`StateCode[]`, sorted by state name) and the 2 `checkDigits`. Accepts the same masked or unmasked input as `isValidCpf` and returns `null` for anything that is not a valid CPF. The region is the one of the address given at the first registration: it tells where the CPF was issued, not where the holder was born or lives, and a region with more than one state does not tell which of them it was. + +| `fiscalRegion` | `states` | +| --- | --- | +| `"1"` | DF, GO, MT, MS, TO | +| `"2"` | AC, AP, AM, PA, RO, RR | +| `"3"` | CE, MA, PI | +| `"4"` | AL, PB, PE, RN | +| `"5"` | BA, SE | +| `"6"` | MG | +| `"7"` | ES, RJ | +| `"8"` | SP | +| `"9"` | PR, SC | +| `"0"` | RS | + +```javascript +import { getCpfInfo } from '@brazilian-utils/brazilian-utils'; + +getCpfInfo('123.456.789-09'); +// { +// base: '12345678', +// fiscalRegion: '9', +// states: ['PR', 'SC'], +// checkDigits: '09', +// } + +getCpfInfo('12345678900'); // null (invalid check digits) +``` + Source: [Receita Federal, "Cadastros: CPF e CNPJ"](https://www.gov.br/receitafederal/pt-br/assuntos/educacao-fiscal/educacao_fiscal/folhetos-orientativos/cadastros-dig.pdf). ## CNPJ diff --git a/reports/api/brazilian-utils.api.md b/reports/api/brazilian-utils.api.md index 75b328bda..1ccd42087 100644 --- a/reports/api/brazilian-utils.api.md +++ b/reports/api/brazilian-utils.api.md @@ -137,6 +137,14 @@ export type ConvertNumberToWordsOptions = { gender?: NumberToWordsGender; }; +// @public +export type CpfInfo = { + base: string; + fiscalRegion: string; + states: StateCode[]; + checkDigits: string; +}; + // @public export const differenceInBusinessDays: (laterDate: Date, earlierDate: Date, options?: BusinessDayOptions) => number | null; @@ -488,6 +496,9 @@ export const getCities: (state?: StateCode) => string[]; // @public export const getCnae: (value: string | number) => Cnae | null; +// @public +export const getCpfInfo: (value: string) => CpfInfo | null; + // @public export const getFormatLicensePlate: (value: string) => LicensePlateFormat | null; diff --git a/src/get-cpf-info/get-cpf-info.test.ts b/src/get-cpf-info/get-cpf-info.test.ts new file mode 100644 index 000000000..7e79b0c29 --- /dev/null +++ b/src/get-cpf-info/get-cpf-info.test.ts @@ -0,0 +1,193 @@ +import * as fc from "fast-check"; + +import { type StateCode } from "../_internals/constants/states"; +import { anyGarbage, anyText, anyValue, stateCodes } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { formatCpf } from "../format-cpf/format-cpf"; +import { generateCpf } from "../generate-cpf/generate-cpf"; +import { isValidCpf } from "../is-valid-cpf/is-valid-cpf"; +import { type CpfInfo, getCpfInfo } from "./get-cpf-info"; + +const REGIONS: [string, CpfInfo][] = [ + [ + "40152673113", + { + base: "40152673", + fiscalRegion: "1", + states: ["DF", "GO", "MT", "MS", "TO"], + checkDigits: "13", + }, + ], + [ + "73091462200", + { + base: "73091462", + fiscalRegion: "2", + states: ["AC", "AP", "AM", "PA", "RO", "RR"], + checkDigits: "00", + }, + ], + [ + "20581746317", + { base: "20581746", fiscalRegion: "3", states: ["CE", "MA", "PI"], checkDigits: "17" }, + ], + [ + "91827364483", + { base: "91827364", fiscalRegion: "4", states: ["AL", "PB", "PE", "RN"], checkDigits: "83" }, + ], + ["56473829598", { base: "56473829", fiscalRegion: "5", states: ["BA", "SE"], checkDigits: "98" }], + ["37192048631", { base: "37192048", fiscalRegion: "6", states: ["MG"], checkDigits: "31" }], + ["84620513717", { base: "84620513", fiscalRegion: "7", states: ["ES", "RJ"], checkDigits: "17" }], + ["15937264819", { base: "15937264", fiscalRegion: "8", states: ["SP"], checkDigits: "19" }], + ["60248175920", { base: "60248175", fiscalRegion: "9", states: ["PR", "SC"], checkDigits: "20" }], + ["48301692065", { base: "48301692", fiscalRegion: "0", states: ["RS"], checkDigits: "65" }], +]; + +describe("getCpfInfo", () => { + describe("should return the fields of the CPF", () => { + for (const [cpf, expected] of REGIONS) { + test(`for ${cpf}, of the região fiscal ${expected.fiscalRegion}`, () => { + expect(getCpfInfo(cpf)).toEqual(expected); + }); + } + + test("for the worked example of the e-Financeira manual (280012389-38)", () => { + expect(getCpfInfo("280012389-38")).toEqual({ + base: "28001238", + fiscalRegion: "9", + states: ["PR", "SC"], + checkDigits: "38", + }); + }); + + test("for a masked value", () => { + expect(getCpfInfo("123.456.789-09")).toEqual({ + base: "12345678", + fiscalRegion: "9", + states: ["PR", "SC"], + checkDigits: "09", + }); + }); + + test("for a value with whitespace around and between the groups", () => { + expect(getCpfInfo(" 111 444 777 35\n")).toEqual({ + base: "11144477", + fiscalRegion: "7", + states: ["ES", "RJ"], + checkDigits: "35", + }); + }); + + test("with a states list of its own on every call", () => { + const first = getCpfInfo("12345678909"); + + first?.states.push("SP"); + + expect(getCpfInfo("12345678909")?.states).toEqual(["PR", "SC"]); + }); + }); + + describe("should return null", () => { + test("when the check digits do not match", () => { + expect(getCpfInfo("12345678900")).toBeNull(); + }); + + test("when every digit is the same", () => { + expect(getCpfInfo("00000000000")).toBeNull(); + expect(getCpfInfo("111.111.111-11")).toBeNull(); + }); + + test("when it is shorter than 11 digits", () => { + expect(getCpfInfo("1234567890")).toBeNull(); + }); + + test("when it is longer than 11 digits", () => { + expect(getCpfInfo("123456789090")).toBeNull(); + }); + + test("when it carries a character outside the mask", () => { + expect(getCpfInfo("123.456.789-09a")).toBeNull(); + expect(getCpfInfo("123_456_789_09")).toBeNull(); + }); + + test("when it is an empty string", () => { + expect(getCpfInfo("")).toBeNull(); + }); + + test("when it is null", () => { + // @ts-expect-error: intentionally invalid input + expect(getCpfInfo(null)).toBeNull(); + }); + + test("when it is undefined", () => { + // @ts-expect-error: intentionally invalid input + expect(getCpfInfo()).toBeNull(); + }); + + test("when it is a number", () => { + // @ts-expect-error: intentionally invalid input + expect(getCpfInfo(12_345_678_909)).toBeNull(); + }); + + test("when it is an object, a null prototype one included", () => { + // @ts-expect-error: intentionally invalid input + expect(getCpfInfo({})).toBeNull(); + expect(getCpfInfo(Object.create(null))).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(getCpfInfo(["12345678909"])).toBeNull(); + }); + + test("when it is a key of the prototype chain", () => { + expect(getCpfInfo("__proto__")).toBeNull(); + expect(getCpfInfo("constructor")).toBeNull(); + }); + }); + + describe("properties", () => { + test("should split a CPF into fields that spell it back, masked or not", () => { + fc.assert( + fc.property(fc.boolean(), (masked) => { + const cpf = generateCpf(); + const parsed = getCpfInfo(masked ? formatCpf(cpf) : cpf); + + expect(`${parsed?.base}${parsed?.fiscalRegion}${parsed?.checkDigits}`).toBe(cpf); + }), + ); + }); + + test("should list the state a CPF was generated for", () => { + fc.assert( + fc.property(stateCodes, (state) => { + expect(getCpfInfo(generateCpf(state))?.states).toContain(state); + }), + ); + }); + + test("should return a value exactly when the CPF is valid", () => { + fc.assert( + fc.property(anyText, (value) => { + expect(getCpfInfo(value) !== null).toBe(isValidCpf(value)); + }), + ); + }); + + test("should never throw", () => { + expectNeverThrows(getCpfInfo, anyValue); + expectNeverThrows(getCpfInfo, anyGarbage); + }); + }); +}); + +describe("getCpfInfo types", () => { + test("should take a string and return a CpfInfo or null", () => { + expectTypeOf(getCpfInfo).parameter(0).toEqualTypeOf(); + expectTypeOf(getCpfInfo).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + base: string; + fiscalRegion: string; + states: StateCode[]; + checkDigits: string; + }>(); + }); +}); diff --git a/src/get-cpf-info/get-cpf-info.ts b/src/get-cpf-info/get-cpf-info.ts new file mode 100644 index 000000000..12fc6adbc --- /dev/null +++ b/src/get-cpf-info/get-cpf-info.ts @@ -0,0 +1,74 @@ +import { CPF_BASE_LENGTH, CPF_FISCAL_REGION_BY_STATE } from "../_internals/constants/cpf"; +import { STATE_CODES } from "../_internals/constants/state-codes"; +import { type StateCode } from "../_internals/constants/states"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { isValidCpf } from "../is-valid-cpf/is-valid-cpf"; + +/** The fields `getCpfInfo` reads out of a CPF. */ +export type CpfInfo = { + /** The first 8 digits, the sequential number of the registration. */ + base: string; + /** + * The 9th digit, the Região Fiscal of the Receita Federal the CPF was registered in: `"1"` to + * `"9"` for the 1ª to the 9ª Região Fiscal and `"0"` for the 10ª. + */ + fiscalRegion: string; + /** The two letter codes of the states of that Região Fiscal, sorted by state name. */ + states: StateCode[]; + /** The 2 check digits. */ + checkDigits: string; +}; + +const CHECK_DIGITS_START = CPF_BASE_LENGTH + 1; + +/** + * Reads the fields a CPF (Cadastro de Pessoas Físicas) encodes: the 8 digit base, the Região + * Fiscal digit with the states it covers, and the 2 check digits. + * + * The Receita Federal is split into ten Regiões Fiscais, and the 9th digit of a CPF is the one of + * the address given when the number was first registered. It tells where the CPF was issued, not + * where its holder was born or lives today, and a region with more than one state does not tell + * which of them it was. + * + * Accepts the same input forms as `isValidCpf`, masked or not, with whitespace around and between + * the groups, and returns `null` whenever `isValidCpf` would return `false`. + * + * @param {string} value - The CPF to be read. + * @returns {CpfInfo|null} The fields of the CPF, or `null` when it is not a valid CPF. + * + * @example + * ```typescript + * getCpfInfo("123.456.789-09"); + * // { + * // base: "12345678", + * // fiscalRegion: "9", + * // states: ["PR", "SC"], + * // checkDigits: "09", + * // } + * + * getCpfInfo("12345678909"); // same result (no mask) + * getCpfInfo("12345678900"); // null (invalid check digits) + * getCpfInfo("00000000000"); // null (reserved number) + * ``` + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/educacao-fiscal/educacao_fiscal/folhetos-orientativos/cadastros-dig.pdf + * Folheto "Cadastros: CPF e CNPJ" of the Receita Federal: the 9th digit is the Região Fiscal, and + * the states of each of the ten regions. + * @see Official: https://www.gov.br/receitafederal/pt-br/composicao/srrf + * Superintendências Regionais da Receita Federal: the states under each of the 1ª to the 10ª + * Região Fiscal, the 10ª (Rio Grande do Sul) being the digit 0. + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/meu-cpf + */ +export const getCpfInfo = (value: string): CpfInfo | null => { + if (!isValidCpf(value)) return null; + + const digits = sanitizeToDigits(value); + const fiscalRegion = digits.charAt(CPF_BASE_LENGTH); + + return { + base: digits.slice(0, CPF_BASE_LENGTH), + fiscalRegion, + states: STATE_CODES.filter((state) => CPF_FISCAL_REGION_BY_STATE[state] === fiscalRegion), + checkDigits: digits.slice(CHECK_DIGITS_START), + }; +}; diff --git a/src/index.test.ts b/src/index.test.ts index 877a4bdfd..61ad45293 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -15,6 +15,7 @@ import { type Cnae, type ConvertDateToWordsOptions, type ConvertNumberToWordsOptions, + type CpfInfo, type FormatBoletoOptions, type FormatCaepfOptions, type FormatCeiOptions, @@ -171,6 +172,7 @@ const PUBLIC = [ "getCfop", "getCities", "getCnae", + "getCpfInfo", "getFormatLicensePlate", "getHolidays", "getIbanInfo", @@ -303,6 +305,7 @@ describe("Public API", () => { Cnae: Cnae; ConvertDateToWordsOptions: ConvertDateToWordsOptions; ConvertNumberToWordsOptions: ConvertNumberToWordsOptions; + CpfInfo: CpfInfo; FormatBoletoOptions: FormatBoletoOptions; FormatCaepfOptions: FormatCaepfOptions; FormatCeiOptions: FormatCeiOptions; diff --git a/src/index.ts b/src/index.ts index a9e85d45f..68c6b5693 100644 --- a/src/index.ts +++ b/src/index.ts @@ -114,6 +114,7 @@ export { 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"; +export { type CpfInfo, getCpfInfo } from "./get-cpf-info/get-cpf-info"; export { getFormatLicensePlate, type LicensePlateFormat, From 094d098ff403be9bd4f2666b2302989af69ebd28 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 19 Sep 2026 11:41:25 -0300 Subject: [PATCH 3/4] feat(get-cpf-info): re-export StateCode from the subpath entry CpfInfo.states is a StateCode[], but a consumer importing from the get-cpf-info subpath had no way to name that type, while every other module whose public type mentions StateCode re-exports it, generate-cpf and get-nfe-key-info included. The doc of CpfInfo.base and of CPF_BASE_LENGTH also called the first 8 digits the sequential number of the registration. None of the cited Receita Federal sources says that, so they now say only what those sources support. --- src/_internals/constants/cpf.ts | 2 +- src/get-cpf-info/get-cpf-info.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/_internals/constants/cpf.ts b/src/_internals/constants/cpf.ts index 0ef7a3ef8..86e43b10a 100644 --- a/src/_internals/constants/cpf.ts +++ b/src/_internals/constants/cpf.ts @@ -3,7 +3,7 @@ import { type StateCode } from "./states"; /** Digits of a CPF. */ export const CPF_LENGTH = 11; -/** Digits of the base of a CPF, the sequential number ahead of the região fiscal digit. */ +/** Digits of a CPF ahead of the região fiscal digit. */ export const CPF_BASE_LENGTH = 8; /** diff --git a/src/get-cpf-info/get-cpf-info.ts b/src/get-cpf-info/get-cpf-info.ts index 12fc6adbc..bbd306d4f 100644 --- a/src/get-cpf-info/get-cpf-info.ts +++ b/src/get-cpf-info/get-cpf-info.ts @@ -4,9 +4,11 @@ import { type StateCode } from "../_internals/constants/states"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { isValidCpf } from "../is-valid-cpf/is-valid-cpf"; +export type { StateCode } from "../_internals/constants/states"; + /** The fields `getCpfInfo` reads out of a CPF. */ export type CpfInfo = { - /** The first 8 digits, the sequential number of the registration. */ + /** The first 8 digits, the ones ahead of the Região Fiscal digit. */ base: string; /** * The 9th digit, the Região Fiscal of the Receita Federal the CPF was registered in: `"1"` to From e92e93a4c9b128122b1d593ff1f705ad6a5c00d6 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 19 Sep 2026 12:10:46 -0300 Subject: [PATCH 4/4] docs(get-cpf-info): drop the claim that the digit tells where the CPF was issued MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The folheto only says the 9th digit is the Região Fiscal of the address given at the first registration. Where the number was asked for is a different thing, and no cited source ties the two, so the JSDoc and both docs now say what the digit does not tell instead of naming an issuing place. --- docs/pt-br/utilities.md | 2 +- docs/utilities.md | 2 +- jsr.json | 1 + src/get-cpf-info/get-cpf-info.ts | 6 +++--- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index f8487d222..e67670f41 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -75,7 +75,7 @@ generateCpf('MG'); // o 9º dígito é 6, o código da região fiscal de MG ### getCpfInfo -Lê os campos que um CPF codifica, como um `CpfInfo`: a `base` de 8 dígitos, o dígito `fiscalRegion` (o 9º dígito, a Região Fiscal da Receita Federal em que o CPF foi inscrito, `"1"` a `"9"` e `"0"` para a 10ª), os `states` dessa região (`StateCode[]`, ordenados pelo nome do estado) e os 2 `checkDigits`. Aceita a mesma entrada com ou sem máscara que o `isValidCpf` e retorna `null` para tudo que não for um CPF válido. A região é a do endereço informado na primeira inscrição: ela diz onde o CPF foi emitido, não onde o titular nasceu ou mora, e uma região com mais de um estado não diz qual deles foi. +Lê os campos que um CPF codifica, como um `CpfInfo`: a `base` de 8 dígitos, o dígito `fiscalRegion` (o 9º dígito, a Região Fiscal da Receita Federal em que o CPF foi inscrito, `"1"` a `"9"` e `"0"` para a 10ª), os `states` dessa região (`StateCode[]`, ordenados pelo nome do estado) e os 2 `checkDigits`. Aceita a mesma entrada com ou sem máscara que o `isValidCpf` e retorna `null` para tudo que não for um CPF válido. A região é a do endereço informado na primeira inscrição: ela não diz onde o titular nasceu, onde mora hoje nem onde pediu o número, e uma região com mais de um estado não diz qual deles foi. | `fiscalRegion` | `states` | | --- | --- | diff --git a/docs/utilities.md b/docs/utilities.md index 9f08de7a4..563acf8f7 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -75,7 +75,7 @@ generateCpf('MG'); // the 9th digit is 6, the MG região fiscal code ### getCpfInfo -Read the fields a CPF encodes, as a `CpfInfo`: the 8 digit `base`, the `fiscalRegion` digit (the 9th digit, the Região Fiscal of the Receita Federal the CPF was registered in, `"1"` to `"9"` and `"0"` for the 10ª), the `states` of that region (`StateCode[]`, sorted by state name) and the 2 `checkDigits`. Accepts the same masked or unmasked input as `isValidCpf` and returns `null` for anything that is not a valid CPF. The region is the one of the address given at the first registration: it tells where the CPF was issued, not where the holder was born or lives, and a region with more than one state does not tell which of them it was. +Read the fields a CPF encodes, as a `CpfInfo`: the 8 digit `base`, the `fiscalRegion` digit (the 9th digit, the Região Fiscal of the Receita Federal the CPF was registered in, `"1"` to `"9"` and `"0"` for the 10ª), the `states` of that region (`StateCode[]`, sorted by state name) and the 2 `checkDigits`. Accepts the same masked or unmasked input as `isValidCpf` and returns `null` for anything that is not a valid CPF. The region is the one of the address given at the first registration: it says nothing about where the holder was born, lives today or asked for the number, and a region with more than one state does not tell which of them it was. | `fiscalRegion` | `states` | | --- | --- | diff --git a/jsr.json b/jsr.json index 4b41130dd..9c7925fa5 100644 --- a/jsr.json +++ b/jsr.json @@ -60,6 +60,7 @@ "./get-cfop": "./src/get-cfop/get-cfop.ts", "./get-cities": "./src/get-cities/get-cities.ts", "./get-cnae": "./src/get-cnae/get-cnae.ts", + "./get-cpf-info": "./src/get-cpf-info/get-cpf-info.ts", "./get-format-license-plate": "./src/get-format-license-plate/get-format-license-plate.ts", "./get-holidays": "./src/get-holidays/get-holidays.ts", "./get-iban-info": "./src/get-iban-info/get-iban-info.ts", diff --git a/src/get-cpf-info/get-cpf-info.ts b/src/get-cpf-info/get-cpf-info.ts index bbd306d4f..eacd0a20e 100644 --- a/src/get-cpf-info/get-cpf-info.ts +++ b/src/get-cpf-info/get-cpf-info.ts @@ -28,9 +28,9 @@ const CHECK_DIGITS_START = CPF_BASE_LENGTH + 1; * Fiscal digit with the states it covers, and the 2 check digits. * * The Receita Federal is split into ten Regiões Fiscais, and the 9th digit of a CPF is the one of - * the address given when the number was first registered. It tells where the CPF was issued, not - * where its holder was born or lives today, and a region with more than one state does not tell - * which of them it was. + * the address given when the number was first registered. It says nothing about where its holder + * was born, lives today or asked for the number, and a region with more than one state does not + * tell which of them it was. * * Accepts the same input forms as `isValidCpf`, masked or not, with whitespace around and between * the groups, and returns `null` whenever `isValidCpf` would return `false`.