diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index eab6d0d5..af5601f7 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -1347,6 +1347,32 @@ generateProcessoJuridico({ court: 10 }); // null (órgão inexistente) Fonte: [Resolução CNJ nº 65/2008](https://atos.cnj.jus.br/atos/detalhar/119). +### getProcessoJuridicoInfo + +Lê os campos de um número de processo jurídico, como um `ProcessoJuridicoInfo`, ou `null` quando o `isValidProcessoJuridico` retornaria `false`. + +- Campos: `sequentialNumber` (`NNNNNNN`), `checkDigits` (`DD`), `year` (`AAAA`, um número), `segment` (um nome para o órgão `J`: `'supreme-federal-court'`, `'national-council-of-justice'`, `'superior-court-of-justice'`, `'federal'`, `'labor'`, `'electoral'`, `'military'`, `'state'` ou `'state-military'`), `segmentCode` (`J`, `'1'` a `'9'`), `tribunalCode` (`TR`, dois dígitos) e `originUnit` (`OOOO`). Os códigos são strings que mantêm os zeros à esquerda. +- `tribunalCode` é `'00'` para os processos de um tribunal superior ou do STF, do CNJ, do STJ, do TST, do TSE e do STM, `'90'` para o Conselho da Justiça Federal e o Conselho Superior da Justiça do Trabalho, e o número da região ou do estado nos demais casos. A unidade de origem não é verificada: cada tribunal a codifica por conta própria. + +```javascript +import { getProcessoJuridicoInfo } from '@brazilian-utils/brazilian-utils'; + +getProcessoJuridicoInfo('0002080-25.2012.5.15.0049'); +// { +// sequentialNumber: '0002080', +// checkDigits: '25', +// year: 2012, +// segment: 'labor', +// segmentCode: '5', +// tribunalCode: '15', +// originUnit: '0049', +// } + +getProcessoJuridicoInfo('0000100-23.2008.8.28.0000'); // null (não existe o 28º Tribunal de Justiça) +``` + +Fonte: [Resolução CNJ nº 65/2008](https://atos.cnj.jus.br/atos/detalhar/119). + ## Contas bancárias e bancos ### isValidBankAccount @@ -2604,6 +2630,31 @@ generateVoterId('XX'); // usa "ZZ" em vez de lançar erro Fonte: [Lei nº 14.194/2021, art. 149](https://www.planalto.gov.br/ccivil_03/_ato2019-2022/2021/lei/L14194.htm), a regra de mascaramento do CPF que o `obfuscate` toma emprestada, criada pela [Lei nº 12.309/2010, art. 87, § 5º](https://www.planalto.gov.br/ccivil_03/_ato2007-2010/2010/lei/l12309.htm) e repetida pelas LDOs seguintes (a de 2026, [Lei nº 15.321/2025, art. 163](https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2025/lei/L15321.htm#art163), a repete). +### getVoterIdInfo + +Lê os campos de um título de eleitor, como um `VoterIdInfo`, ou `null` quando o `isValidVoterId` retornaria `false`. + +- Campos: `sequentialNumber` (8 dígitos), `federativeUnion` (o código `'01'` a `'28'`), `stateCode` (um `StateCode`, ou `null` para `'28'`, os eleitores no exterior) e `checkDigits` (2 dígitos). Os códigos são strings que mantêm os zeros à esquerda. +- Um título expedido sem os zeros à esquerda do número sequencial é lido como o `isValidVoterId` o lê, preenchido com zeros à esquerda até 12 dígitos: `'123450159'` dá o `sequentialNumber` `'00012345'`. +- O `stateCode` é a unidade federativa da inscrição, não necessariamente onde o eleitor mora hoje. + +```javascript +import { getVoterIdInfo } from '@brazilian-utils/brazilian-utils'; + +getVoterIdInfo('1023 8501 06 71'); +// { +// sequentialNumber: '10238501', +// federativeUnion: '06', +// stateCode: 'PR', +// checkDigits: '71', +// } + +getVoterIdInfo('000000002801'); // { sequentialNumber: '00000000', federativeUnion: '28', stateCode: null, checkDigits: '01' } +getVoterIdInfo('123456780124'); // null (dígitos verificadores inválidos) +``` + +Fonte: [Resolução TSE nº 23.659/2021, art. 36](https://www.tse.jus.br/legislacao/compilada/res/2021/resolucao-no-23-659-de-26-de-outubro-de-2021). + ## CNS ### isValidCns diff --git a/docs/utilities.md b/docs/utilities.md index 9cf1e0a8..04064c5b 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -1347,6 +1347,32 @@ generateProcessoJuridico({ court: 10 }); // null (no such órgão) Source: [Resolução CNJ nº 65/2008](https://atos.cnj.jus.br/atos/detalhar/119). +### getProcessoJuridicoInfo + +Read the fields of a processo jurídico number, as a `ProcessoJuridicoInfo`, or `null` when `isValidProcessoJuridico` would return `false`. + +- Fields: `sequentialNumber` (`NNNNNNN`), `checkDigits` (`DD`), `year` (`AAAA`, a number), `segment` (a name for the órgão `J`: `'supreme-federal-court'`, `'national-council-of-justice'`, `'superior-court-of-justice'`, `'federal'`, `'labor'`, `'electoral'`, `'military'`, `'state'` or `'state-military'`), `segmentCode` (`J`, `'1'` to `'9'`), `tribunalCode` (`TR`, two digits) and `originUnit` (`OOOO`). Codes are strings that keep their leading zeros. +- `tribunalCode` is `'00'` for the processes of a superior court or of the STF, the CNJ, the STJ, the TST, the TSE and the STM, `'90'` for the Conselho da Justiça Federal and the Conselho Superior da Justiça do Trabalho, and the number of the region or state otherwise. The unit of origin is not checked: each tribunal codifies its own. + +```javascript +import { getProcessoJuridicoInfo } from '@brazilian-utils/brazilian-utils'; + +getProcessoJuridicoInfo('0002080-25.2012.5.15.0049'); +// { +// sequentialNumber: '0002080', +// checkDigits: '25', +// year: 2012, +// segment: 'labor', +// segmentCode: '5', +// tribunalCode: '15', +// originUnit: '0049', +// } + +getProcessoJuridicoInfo('0000100-23.2008.8.28.0000'); // null (no 28th Tribunal de Justiça) +``` + +Source: [Resolução CNJ nº 65/2008](https://atos.cnj.jus.br/atos/detalhar/119). + ## Bank accounts and banks ### isValidBankAccount @@ -2604,6 +2630,31 @@ generateVoterId('XX'); // falls back to "ZZ" instead of throwing Source: [Lei nº 14.194/2021, art. 149](https://www.planalto.gov.br/ccivil_03/_ato2019-2022/2021/lei/L14194.htm), the CPF masking rule `obfuscate` borrows, first set by [Lei nº 12.309/2010, art. 87, § 5º](https://www.planalto.gov.br/ccivil_03/_ato2007-2010/2010/lei/l12309.htm) and repeated by the later LDOs ([Lei nº 15.321/2025, art. 163](https://www.planalto.gov.br/ccivil_03/_ato2023-2026/2025/lei/L15321.htm#art163), the one for 2026, repeats it). +### getVoterIdInfo + +Read the fields of a voter ID, as a `VoterIdInfo`, or `null` when `isValidVoterId` would return `false`. + +- Fields: `sequentialNumber` (8 digits), `federativeUnion` (the code `'01'` to `'28'`), `stateCode` (a `StateCode`, or `null` for `'28'`, the voters abroad) and `checkDigits` (2 digits). Codes are strings that keep their leading zeros. +- A voter ID issued without the leading zeros of its sequential number is read as `isValidVoterId` reads it, left padded with zeros to 12 digits: `'123450159'` gives the `sequentialNumber` `'00012345'`. +- The `stateCode` is the federative union of the registration, not necessarily where the voter lives today. + +```javascript +import { getVoterIdInfo } from '@brazilian-utils/brazilian-utils'; + +getVoterIdInfo('1023 8501 06 71'); +// { +// sequentialNumber: '10238501', +// federativeUnion: '06', +// stateCode: 'PR', +// checkDigits: '71', +// } + +getVoterIdInfo('000000002801'); // { sequentialNumber: '00000000', federativeUnion: '28', stateCode: null, checkDigits: '01' } +getVoterIdInfo('123456780124'); // null (invalid check digits) +``` + +Source: [Resolução TSE nº 23.659/2021, art. 36](https://www.tse.jus.br/legislacao/compilada/res/2021/resolucao-no-23-659-de-26-de-outubro-de-2021). + ## CNS ### isValidCns diff --git a/jsr.json b/jsr.json index ca1939c9..65094539 100644 --- a/jsr.json +++ b/jsr.json @@ -93,6 +93,7 @@ "./get-nfse-key-info": "./src/get-nfse-key-info/get-nfse-key-info.ts", "./get-pix-key-info": "./src/get-pix-key-info/get-pix-key-info.ts", "./get-pix-payload-info": "./src/get-pix-payload-info/get-pix-payload-info.ts", + "./get-processo-juridico-info": "./src/get-processo-juridico-info/get-processo-juridico-info.ts", "./get-regions": "./src/get-regions/get-regions.ts", "./get-service-item": "./src/get-service-item/get-service-item.ts", "./get-state-by-cep": "./src/get-state-by-cep/get-state-by-cep.ts", @@ -103,6 +104,7 @@ "./get-states": "./src/get-states/get-states.ts", "./get-states-by-region": "./src/get-states-by-region/get-states-by-region.ts", "./get-timezone-by-state": "./src/get-timezone-by-state/get-timezone-by-state.ts", + "./get-voter-id-info": "./src/get-voter-id-info/get-voter-id-info.ts", "./is-business-day": "./src/is-business-day/is-business-day.ts", "./is-holiday": "./src/is-holiday/is-holiday.ts", "./is-valid-bank-account": "./src/is-valid-bank-account/is-valid-bank-account.ts", diff --git a/src/get-processo-juridico-info/constants.ts b/src/get-processo-juridico-info/constants.ts new file mode 100644 index 00000000..01d52b9b --- /dev/null +++ b/src/get-processo-juridico-info/constants.ts @@ -0,0 +1,12 @@ +/** Segments of the Judiciary, in the order of the órgão digit `J` (1 to 9, Resolução CNJ nº 65/2008, art. 1º, § 4º). */ +export const PROCESSO_JURIDICO_SEGMENTS = [ + "supreme-federal-court", + "national-council-of-justice", + "superior-court-of-justice", + "federal", + "labor", + "electoral", + "military", + "state", + "state-military", +] as const; diff --git a/src/get-processo-juridico-info/get-processo-juridico-info.test.ts b/src/get-processo-juridico-info/get-processo-juridico-info.test.ts new file mode 100644 index 00000000..c1c3e047 --- /dev/null +++ b/src/get-processo-juridico-info/get-processo-juridico-info.test.ts @@ -0,0 +1,329 @@ +import * as fc from "fast-check"; + +import { + anyGarbage, + anyText, + anyValue, + maskSeparators, + processosJuridicos, +} from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { isValidProcessoJuridico } from "../is-valid-processo-juridico/is-valid-processo-juridico"; +import { PROCESSO_JURIDICO_SEGMENTS } from "./constants"; +import { + type ProcessoJuridicoInfo, + type ProcessoJuridicoSegment, + getProcessoJuridicoInfo, +} from "./get-processo-juridico-info"; + +const SEGMENTS: [string, ProcessoJuridicoInfo][] = [ + [ + "0000100-85.2008.1.00.0000", + { + sequentialNumber: "0000100", + checkDigits: "85", + year: 2008, + segment: "supreme-federal-court", + segmentCode: "1", + tribunalCode: "00", + originUnit: "0000", + }, + ], + [ + "0000100-04.2008.2.00.0000", + { + sequentialNumber: "0000100", + checkDigits: "04", + year: 2008, + segment: "national-council-of-justice", + segmentCode: "2", + tribunalCode: "00", + originUnit: "0000", + }, + ], + [ + "0000100-20.2008.3.00.0000", + { + sequentialNumber: "0000100", + checkDigits: "20", + year: 2008, + segment: "superior-court-of-justice", + segmentCode: "3", + tribunalCode: "00", + originUnit: "0000", + }, + ], + [ + "0000100-09.2008.4.01.0000", + { + sequentialNumber: "0000100", + checkDigits: "09", + year: 2008, + segment: "federal", + segmentCode: "4", + tribunalCode: "01", + originUnit: "0000", + }, + ], + [ + "0002080-25.2012.5.15.0049", + { + sequentialNumber: "0002080", + checkDigits: "25", + year: 2012, + segment: "labor", + segmentCode: "5", + tribunalCode: "15", + originUnit: "0049", + }, + ], + [ + "0000100-18.2008.6.27.0000", + { + sequentialNumber: "0000100", + checkDigits: "18", + year: 2008, + segment: "electoral", + segmentCode: "6", + tribunalCode: "27", + originUnit: "0000", + }, + ], + [ + "0000100-51.2008.7.12.0000", + { + sequentialNumber: "0000100", + checkDigits: "51", + year: 2008, + segment: "military", + segmentCode: "7", + tribunalCode: "12", + originUnit: "0000", + }, + ], + [ + "0000100-73.2008.8.01.0000", + { + sequentialNumber: "0000100", + checkDigits: "73", + year: 2008, + segment: "state", + segmentCode: "8", + tribunalCode: "01", + originUnit: "0000", + }, + ], + [ + "0000100-93.2008.9.26.0000", + { + sequentialNumber: "0000100", + checkDigits: "93", + year: 2008, + segment: "state-military", + segmentCode: "9", + tribunalCode: "26", + originUnit: "0000", + }, + ], +]; + +const SEGMENT_OF_COURT: Record = { + "1": "supreme-federal-court", + "2": "national-council-of-justice", + "3": "superior-court-of-justice", + "4": "federal", + "5": "labor", + "6": "electoral", + "7": "military", + "8": "state", + "9": "state-military", +}; + +describe("getProcessoJuridicoInfo", () => { + describe("should return the fields of the number", () => { + for (const [value, expected] of SEGMENTS) { + test(`for ${value}, of the segment ${expected.segment}`, () => { + expect(getProcessoJuridicoInfo(value)).toEqual(expected); + }); + } + + test("for a value without the mask", () => { + expect(getProcessoJuridicoInfo("00020802520125150049")).toEqual({ + sequentialNumber: "0002080", + checkDigits: "25", + year: 2012, + segment: "labor", + segmentCode: "5", + tribunalCode: "15", + originUnit: "0049", + }); + }); + + test("for a value with slashes, whitespace and no separator before the tribunal", () => { + const expected = getProcessoJuridicoInfo("00020802520125150049"); + + expect(getProcessoJuridicoInfo("0002080/25.2012.5.15.0049")).toEqual(expected); + expect(getProcessoJuridicoInfo("0002080-25.2012.515.0049")).toEqual(expected); + expect(getProcessoJuridicoInfo(" 0002080-25.2012.5.15.0049\n")).toEqual(expected); + }); + + test("for the TRF da 6ª Região, whose Anexo II example is 0000100-68.2008.4.06.0000", () => { + expect(getProcessoJuridicoInfo("0000100-68.2008.4.06.0000")).toEqual({ + sequentialNumber: "0000100", + checkDigits: "68", + year: 2008, + segment: "federal", + segmentCode: "4", + tribunalCode: "06", + originUnit: "0000", + }); + }); + + test("for a council, whose tribunal code is 90", () => { + expect(getProcessoJuridicoInfo("0000100-31.2008.4.90.0000")?.tribunalCode).toBe("90"); + expect(getProcessoJuridicoInfo("0000100-47.2008.5.90.0000")?.tribunalCode).toBe("90"); + }); + + test("for the superior courts of the labor, electoral and military segments, whose tribunal code is 00", () => { + expect(getProcessoJuridicoInfo("0000100-52.2008.5.00.0000")).toMatchObject({ + segment: "labor", + tribunalCode: "00", + }); + expect(getProcessoJuridicoInfo("0000100-68.2008.6.00.0000")).toMatchObject({ + segment: "electoral", + tribunalCode: "00", + }); + expect(getProcessoJuridicoInfo("0000100-84.2008.7.00.0000")).toMatchObject({ + segment: "military", + tribunalCode: "00", + }); + }); + + test("for the tribunais de justiça militar of Minas Gerais and Rio Grande do Sul", () => { + expect(getProcessoJuridicoInfo("0000100-56.2008.9.13.0000")?.tribunalCode).toBe("13"); + expect(getProcessoJuridicoInfo("0000100-34.2008.9.21.0000")?.tribunalCode).toBe("21"); + }); + + test("with a year that is a number, not a string", () => { + expect(typeof getProcessoJuridicoInfo("00020802520125150049")?.year).toBe("number"); + }); + + test("as a new object on every call", () => { + const first = getProcessoJuridicoInfo("00020802520125150049"); + const second = getProcessoJuridicoInfo("00020802520125150049"); + + expect(first).not.toBe(second); + + // @ts-expect-error: the test corrupts its own copy + first.year = 1999; + + expect(getProcessoJuridicoInfo("00020802520125150049")?.year).toBe(2012); + }); + }); + + describe("should return null", () => { + test("when the check digits do not match", () => { + expect(getProcessoJuridicoInfo("0002080-26.2012.5.15.0049")).toBeNull(); + }); + + test("when the órgão and tribunal pair does not exist", () => { + expect(getProcessoJuridicoInfo("0000100-23.2008.8.28.0000")).toBeNull(); + expect(getProcessoJuridicoInfo("0000100-68.2008.4.07.0000")).toBeNull(); + }); + + test("when it is shorter or longer than 20 digits", () => { + expect(getProcessoJuridicoInfo("0002080252012515004")).toBeNull(); + expect(getProcessoJuridicoInfo("000208025201251500490")).toBeNull(); + }); + + test("when it carries a character outside the mask", () => { + expect(getProcessoJuridicoInfo("ab00020802520125150049")).toBeNull(); + expect(getProcessoJuridicoInfo("0002080_25.2012.5.15.0049")).toBeNull(); + }); + + test("when it is an empty string", () => { + expect(getProcessoJuridicoInfo("")).toBeNull(); + }); + + test("when it is null or undefined", () => { + // @ts-expect-error: intentionally invalid input + expect(getProcessoJuridicoInfo(null)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(getProcessoJuridicoInfo()).toBeNull(); + }); + + test("when it is a number", () => { + // @ts-expect-error: intentionally invalid input + expect(getProcessoJuridicoInfo(2_080_252_012_515)).toBeNull(); + }); + + test("when it is an object or a key of the prototype chain", () => { + // @ts-expect-error: intentionally invalid input + expect(getProcessoJuridicoInfo({})).toBeNull(); + expect(getProcessoJuridicoInfo("__proto__")).toBeNull(); + }); + }); + + describe("properties", () => { + test("should split a valid number into fields that spell it back, whatever mask it has", () => { + fc.assert( + fc.property(fc.gen(), maskSeparators([".", "-", " "], 5, 3), (g, separators) => { + const value = g(processosJuridicos); + const masked = `${value.slice(0, 7)}${separators[0]}${value.slice(7, 9)}${separators[1]}${value.slice(9, 13)}${separators[2]}${value.slice(13, 14)}${separators[3]}${value.slice(14, 16)}${separators[4]}${value.slice(16)}`; + const info = getProcessoJuridicoInfo(masked); + + expect(info).not.toBeNull(); + expect( + `${info?.sequentialNumber}${info?.checkDigits}${String(info?.year).padStart(4, "0")}${info?.segmentCode}${info?.tribunalCode}${info?.originUnit}`, + ).toBe(value); + }), + ); + }); + + test("should name the segment of the órgão digit", () => { + fc.assert( + fc.property(fc.gen(), (g) => { + const value = g(processosJuridicos); + + expect(getProcessoJuridicoInfo(value)?.segment).toBe(SEGMENT_OF_COURT[value.charAt(13)]); + }), + ); + }); + + test("should return a value exactly when the number is valid", () => { + fc.assert( + fc.property(anyText, (value) => { + expect(getProcessoJuridicoInfo(value) !== null).toBe(isValidProcessoJuridico(value)); + }), + ); + }); + + test("should never throw", () => { + expectNeverThrows(getProcessoJuridicoInfo, anyValue); + expectNeverThrows(getProcessoJuridicoInfo, anyGarbage); + }); + }); +}); + +describe("getProcessoJuridicoInfo types", () => { + test("should take a string and return a ProcessoJuridicoInfo or null", () => { + expectTypeOf(getProcessoJuridicoInfo).parameter(0).toEqualTypeOf(); + expectTypeOf(getProcessoJuridicoInfo).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + sequentialNumber: string; + checkDigits: string; + year: number; + segment: ProcessoJuridicoSegment; + segmentCode: string; + tribunalCode: string; + originUnit: string; + }>(); + }); + + test("should spell out the segments of the internal list", () => { + expectTypeOf().toEqualTypeOf< + (typeof PROCESSO_JURIDICO_SEGMENTS)[number] + >(); + }); +}); diff --git a/src/get-processo-juridico-info/get-processo-juridico-info.ts b/src/get-processo-juridico-info/get-processo-juridico-info.ts new file mode 100644 index 00000000..bf1df746 --- /dev/null +++ b/src/get-processo-juridico-info/get-processo-juridico-info.ts @@ -0,0 +1,112 @@ +import { SEPARATORS_REGEX } from "../_internals/constants/separators"; +import { + CHECK_DIGIT_LENGTH, + CHECK_DIGIT_START_POSITION, + COURT_POSITION, + TRIBUNAL_LENGTH, + TRIBUNAL_START_POSITION, +} from "../is-valid-processo-juridico/constants"; +import { isValidProcessoJuridico } from "../is-valid-processo-juridico/is-valid-processo-juridico"; +import { PROCESSO_JURIDICO_SEGMENTS } from "./constants"; + +/** + * The segments of the Judiciary a processo number can belong to, one per órgão digit `J` (art. + * 1º, § 4º of Resolução CNJ nº 65/2008): `"supreme-federal-court"` (`1`), `"national-council-of-justice"` + * (`2`), `"superior-court-of-justice"` (`3`), `"federal"` (`4`, Justiça Federal), `"labor"` (`5`, + * Justiça do Trabalho), `"electoral"` (`6`, Justiça Eleitoral), `"military"` (`7`, Justiça Militar + * da União), `"state"` (`8`, Justiça dos Estados e do Distrito Federal e Territórios) and + * `"state-military"` (`9`, Justiça Militar Estadual). Spelled out instead of derived from the + * internal list because API Extractor cannot name that list in the public report; the type test + * of `get-processo-juridico-info.test.ts` pins the two together. + * + * @see Official: https://atos.cnj.jus.br/atos/detalhar/119 + */ +export type ProcessoJuridicoSegment = + | "supreme-federal-court" + | "national-council-of-justice" + | "superior-court-of-justice" + | "federal" + | "labor" + | "electoral" + | "military" + | "state" + | "state-military"; + +/** The fields `getProcessoJuridicoInfo` reads out of a Número Único de Processo. */ +export type ProcessoJuridicoInfo = { + /** The 7 digit sequential number (`NNNNNNN`), zero padded, counted by the unit of origin per year. */ + sequentialNumber: string; + /** The 2 check digits (`DD`, ISO 7064 MOD 97-10). */ + checkDigits: string; + /** Four digit year the process was filed (`AAAA`). */ + year: number; + /** The segment of the Judiciary (`J`), as an English name. */ + segment: ProcessoJuridicoSegment; + /** Raw órgão code (`J`), `"1"` to `"9"`. */ + segmentCode: string; + /** The 2 digit tribunal code (`TR`): `"00"` for a superior court, `"90"` for a council, the region or state otherwise. */ + tribunalCode: string; + /** The 4 digit unit of origin (`OOOO`), whose codification each tribunal sets. */ + originUnit: string; +}; + +/** + * Reads the fields of a Número Único de Processo (`NNNNNNN-DD.AAAA.J.TR.OOOO`) of Resolução CNJ + * nº 65/2008: the sequential number, the check digits, the year of filing, the segment of the + * Judiciary, the tribunal and the unit of origin. + * + * Accepts the same input forms as `isValidProcessoJuridico`, masked or not, and returns `null` + * whenever it would return `false`, so the `J` and `TR` pair is always one the resolution + * created. + * + * `segmentCode` is the órgão digit `J` and `segment` its name. `tribunalCode` is `TR` as written, + * two digits: `"00"` for the processes of a superior court or of a segment's own court (the STF, + * the CNJ, the STJ, the TST, the TSE and the STM, art. 1º, § 5º, I), `"90"` for those of the + * Conselho da Justiça Federal and of the Conselho Superior da Justiça do Trabalho (§ 5º, II), and + * the number of the Tribunal Regional, Circunscrição Judiciária Militar or Tribunal de Justiça + * otherwise. What each number stands for depends on `segment`. The unidade de origem is not + * checked: art. 1º, § 6º hands its codification to each tribunal. + * + * @param {string} value - The Número Único de Processo to be read. + * @returns {ProcessoJuridicoInfo|null} The fields of the number, or `null` when it is not valid. + * + * @example + * ```typescript + * getProcessoJuridicoInfo("0002080-25.2012.5.15.0049"); + * // { + * // sequentialNumber: "0002080", + * // checkDigits: "25", + * // year: 2012, + * // segment: "labor", + * // segmentCode: "5", + * // tribunalCode: "15", + * // originUnit: "0049", + * // } + * + * getProcessoJuridicoInfo("00020802520125150049"); // same result (no mask) + * getProcessoJuridicoInfo("0000100-23.2008.8.28.0000"); // null (there is no 28th Tribunal de Justiça) + * ``` + * + * @see Official: https://atos.cnj.jus.br/atos/detalhar/119 + * Resolução CNJ nº 65, de 16 de dezembro de 2008: the layout (art. 1º, § 1º), the órgão and + * tribunal codes (art. 1º, § 4º and § 5º) and the unidade de origem (§ 6º). + */ +export const getProcessoJuridicoInfo = (value: string): ProcessoJuridicoInfo | null => { + if (!isValidProcessoJuridico(value)) return null; + + const digits = value.replace(SEPARATORS_REGEX, ""); + const yearStart = CHECK_DIGIT_START_POSITION + CHECK_DIGIT_LENGTH; + const originStart = TRIBUNAL_START_POSITION + TRIBUNAL_LENGTH; + const segmentCode = digits.charAt(COURT_POSITION); + const segments: readonly ProcessoJuridicoSegment[] = PROCESSO_JURIDICO_SEGMENTS; + + return { + sequentialNumber: digits.slice(0, CHECK_DIGIT_START_POSITION), + checkDigits: digits.slice(CHECK_DIGIT_START_POSITION, yearStart), + year: Number(digits.slice(yearStart, COURT_POSITION)), + segment: segments[Number(segmentCode) - 1], + segmentCode, + tribunalCode: digits.slice(TRIBUNAL_START_POSITION, originStart), + originUnit: digits.slice(originStart), + }; +}; diff --git a/src/get-voter-id-info/get-voter-id-info.test.ts b/src/get-voter-id-info/get-voter-id-info.test.ts new file mode 100644 index 00000000..f80a8c5a --- /dev/null +++ b/src/get-voter-id-info/get-voter-id-info.test.ts @@ -0,0 +1,250 @@ +import * as fc from "fast-check"; + +import { type StateCode } from "../_internals/constants/states"; +import { + anyGarbage, + anyText, + anyValue, + stateCodes, + voterIds, +} from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { isValidVoterId } from "../is-valid-voter-id/is-valid-voter-id"; +import { type VoterIdInfo, getVoterIdInfo } from "./get-voter-id-info"; + +const UF_CODES: Record = { + SP: "01", + MG: "02", + RJ: "03", + RS: "04", + BA: "05", + PR: "06", + CE: "07", + PE: "08", + SC: "09", + GO: "10", + MA: "11", + PB: "12", + PA: "13", + ES: "14", + PI: "15", + RN: "16", + AL: "17", + MT: "18", + MS: "19", + DF: "20", + SE: "21", + AM: "22", + RO: "23", + AC: "24", + AP: "25", + RR: "26", + TO: "27", +}; + +describe("getVoterIdInfo", () => { + describe("should return the fields of the voter id", () => { + test("for a 12 digit value", () => { + expect(getVoterIdInfo("102385010671")).toEqual({ + sequentialNumber: "10238501", + federativeUnion: "06", + stateCode: "PR", + checkDigits: "71", + }); + }); + + test("for a masked value", () => { + expect(getVoterIdInfo("1023 8501 06 71")).toEqual({ + sequentialNumber: "10238501", + federativeUnion: "06", + stateCode: "PR", + checkDigits: "71", + }); + }); + + test("for a value with whitespace, dots, hyphens and slashes around and between the groups", () => { + const expected = getVoterIdInfo("123456780191"); + + expect(getVoterIdInfo(" 1234 5678 01 91\n")).toEqual(expected); + expect(getVoterIdInfo("1234.5678-01/91")).toEqual(expected); + }); + + test("for a São Paulo voter id, whose check digits follow the remainder zero rule", () => { + expect(getVoterIdInfo("123456780191")).toEqual({ + sequentialNumber: "12345678", + federativeUnion: "01", + stateCode: "SP", + checkDigits: "91", + }); + }); + + test("for a voter id issued without its leading zeros, padded to 8 sequential digits", () => { + const expected = { + sequentialNumber: "00012345", + federativeUnion: "01", + stateCode: "SP", + checkDigits: "59", + }; + + expect(getVoterIdInfo("000123450159")).toEqual(expected); + expect(getVoterIdInfo("123450159")).toEqual(expected); + expect(getVoterIdInfo("12345 01 59")).toEqual(expected); + }); + + test("for a short voter id of another state", () => { + expect(getVoterIdInfo("12340639")).toEqual({ + sequentialNumber: "00001234", + federativeUnion: "06", + stateCode: "PR", + checkDigits: "39", + }); + }); + + test("for a sequential number of zeros", () => { + expect(getVoterIdInfo("000000000116")).toEqual({ + sequentialNumber: "00000000", + federativeUnion: "01", + stateCode: "SP", + checkDigits: "16", + }); + }); + + test("with a null state for the code 28, the voters abroad", () => { + expect(getVoterIdInfo("000000002801")).toEqual({ + sequentialNumber: "00000000", + federativeUnion: "28", + stateCode: null, + checkDigits: "01", + }); + expect(getVoterIdInfo("122844")).toEqual({ + sequentialNumber: "00000012", + federativeUnion: "28", + stateCode: null, + checkDigits: "44", + }); + }); + + test("as a new object on every call", () => { + const first = getVoterIdInfo("102385010671"); + const second = getVoterIdInfo("102385010671"); + + expect(first).not.toBe(second); + + // @ts-expect-error: the test corrupts its own copy + first.state = "SP"; + + expect(getVoterIdInfo("102385010671")?.stateCode).toBe("PR"); + }); + }); + + describe("should return null", () => { + test("when the check digits do not match", () => { + expect(getVoterIdInfo("123456780124")).toBeNull(); + }); + + test("when the federative union code is 00 or above 28", () => { + expect(getVoterIdInfo("123456780013")).toBeNull(); + expect(getVoterIdInfo("123456782913")).toBeNull(); + }); + + test("when it has 13 digits, more than the 12 the TSE allows", () => { + expect(getVoterIdInfo("1234567880191")).toBeNull(); + }); + + test("when it has fewer than 5 digits", () => { + expect(getVoterIdInfo("0191")).toBeNull(); + }); + + test("when it carries a character outside the mask", () => { + expect(getVoterIdInfo("ab102385010671")).toBeNull(); + expect(getVoterIdInfo("1023_8501_06_71")).toBeNull(); + }); + + test("when it is an empty string", () => { + expect(getVoterIdInfo("")).toBeNull(); + }); + + test("when it is null or undefined", () => { + // @ts-expect-error: intentionally invalid input + expect(getVoterIdInfo(null)).toBeNull(); + // @ts-expect-error: intentionally invalid input + expect(getVoterIdInfo()).toBeNull(); + }); + + test("when it is a number", () => { + // @ts-expect-error: intentionally invalid input + expect(getVoterIdInfo(102_385_010)).toBeNull(); + }); + + test("when it is an object or a key of the prototype chain", () => { + // @ts-expect-error: intentionally invalid input + expect(getVoterIdInfo({})).toBeNull(); + expect(getVoterIdInfo("__proto__")).toBeNull(); + }); + }); + + describe("properties", () => { + test("should split a valid voter id into fields that spell it back, with or without the leading zeros", () => { + fc.assert( + fc.property(fc.gen(), fc.boolean(), (g, trimmed) => { + const voterId = g(voterIds); + const info = getVoterIdInfo(trimmed ? voterId.replace(/^0+/, "") || "0" : voterId); + + expect(info).not.toBeNull(); + expect(`${info?.sequentialNumber}${info?.federativeUnion}${info?.checkDigits}`).toBe( + voterId, + ); + }), + ); + }); + + test("should name the state a voter id was generated for", () => { + fc.assert( + fc.property(stateCodes, fc.gen(), (state, g) => { + const info = getVoterIdInfo(g(() => voterIds(state))); + + expect(info?.stateCode).toBe(state); + expect(info?.federativeUnion).toBe(UF_CODES[state]); + }), + ); + }); + + test("should give no state to a voter id generated abroad", () => { + fc.assert( + fc.property(fc.gen(), (g) => { + const info = getVoterIdInfo(g(() => voterIds("ZZ"))); + + expect(info?.stateCode).toBeNull(); + expect(info?.federativeUnion).toBe("28"); + }), + ); + }); + + test("should return a value exactly when the voter id is valid", () => { + fc.assert( + fc.property(anyText, (value) => { + expect(getVoterIdInfo(value) !== null).toBe(isValidVoterId(value)); + }), + ); + }); + + test("should never throw", () => { + expectNeverThrows(getVoterIdInfo, anyValue); + expectNeverThrows(getVoterIdInfo, anyGarbage); + }); + }); +}); + +describe("getVoterIdInfo types", () => { + test("should take a string and return a VoterIdInfo or null", () => { + expectTypeOf(getVoterIdInfo).parameter(0).toEqualTypeOf(); + expectTypeOf(getVoterIdInfo).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + sequentialNumber: string; + federativeUnion: string; + stateCode: StateCode | null; + checkDigits: string; + }>(); + }); +}); diff --git a/src/get-voter-id-info/get-voter-id-info.ts b/src/get-voter-id-info/get-voter-id-info.ts new file mode 100644 index 00000000..69207ac5 --- /dev/null +++ b/src/get-voter-id-info/get-voter-id-info.ts @@ -0,0 +1,77 @@ +import { SEPARATORS_REGEX } from "../_internals/constants/separators"; +import { STATE_CODES } from "../_internals/constants/state-codes"; +import { type StateCode } from "../_internals/constants/states"; +import { UF_TO_VOTER_ID_CODE, VOTER_ID_LENGTH } from "../_internals/constants/voter-id"; +import { isValidVoterId } from "../is-valid-voter-id/is-valid-voter-id"; + +export type { StateCode } from "../_internals/constants/states"; + +/** The fields `getVoterIdInfo` reads out of a voter id (título de eleitor). */ +export type VoterIdInfo = { + /** The 8 digit sequential number, zero padded when the voter id was issued without its leading zeros. */ + sequentialNumber: string; + /** The 2 digit federative union code (UF), `"01"` to `"28"`. */ + federativeUnion: string; + /** Two letter code of the state of the federative union, or `null` for `"28"` (ZZ), the voters abroad. */ + stateCode: StateCode | null; + /** The 2 check digits. */ + checkDigits: string; +}; + +const SEQUENTIAL_NUMBER_END = 8; +const FEDERATIVE_UNION_END = 10; + +/** + * Reads the fields of a voter id (título de eleitor): the sequential number, the federative union + * code with the state it stands for, and the 2 check digits. + * + * Accepts the same input forms as `isValidVoterId`, masked or not, and returns `null` whenever it + * would return `false`. A voter id issued without the leading zeros of its sequential number is + * read as `isValidVoterId` reads it, left padded with zeros to 12 digits, so `sequentialNumber` + * always has 8 digits. + * + * The `federativeUnion` is the code of the table of Resolução TSE nº 23.659/2021, art. 36: `"01"` + * (SP) to `"27"` (TO), and `"28"` (ZZ) for a voter registered abroad, which has no state, so its + * `stateCode` is `null`. The code is where the voter first registered, not necessarily where they + * live today. + * + * @param {string} value - The voter id to be read. + * @returns {VoterIdInfo|null} The fields of the voter id, or `null` when it is not valid. + * + * @example + * ```typescript + * getVoterIdInfo("1023 8501 06 71"); + * // { + * // sequentialNumber: "10238501", + * // federativeUnion: "06", + * // stateCode: "PR", + * // checkDigits: "71", + * // } + * + * getVoterIdInfo("123450159"); + * // { sequentialNumber: "00012345", federativeUnion: "01", stateCode: "SP", checkDigits: "59" } + * + * getVoterIdInfo("000000002801"); + * // { sequentialNumber: "00000000", federativeUnion: "28", stateCode: null, checkDigits: "01" } + * + * getVoterIdInfo("123456780124"); // null (invalid check digits) + * ``` + * + * @see Official: https://www.tse.jus.br/legislacao/compilada/res/2021/resolucao-no-23-659-de-26-de-outubro-de-2021 + * Resolução TSE nº 23.659/2021, art. 36: the sequential number, the table of the federative + * union codes 01 to 28 and the two check digits. + * @see Based on: https://github.com/brazilian-utils/python/blob/main/brutils/voter_id.py + */ +export const getVoterIdInfo = (value: string): VoterIdInfo | null => { + if (!isValidVoterId(value)) return null; + + const digits = value.replace(SEPARATORS_REGEX, "").padStart(VOTER_ID_LENGTH, "0"); + const federativeUnion = digits.slice(SEQUENTIAL_NUMBER_END, FEDERATIVE_UNION_END); + + return { + sequentialNumber: digits.slice(0, SEQUENTIAL_NUMBER_END), + federativeUnion, + stateCode: STATE_CODES.find((state) => UF_TO_VOTER_ID_CODE[state] === federativeUnion) ?? null, + checkDigits: digits.slice(FEDERATIVE_UNION_END), + }; +}; diff --git a/src/index.test.ts b/src/index.test.ts index 4c075d53..9854c16d 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -111,6 +111,8 @@ import { type PixKeyType, type PixPayloadInfo, type PixPointOfInitiation, + type ProcessoJuridicoInfo, + type ProcessoJuridicoSegment, type Region, type RegionCode, type RegistroProfissionalCouncil, @@ -128,6 +130,7 @@ import { type StandardSchemaV1Types, type StateName, type ToStandardSchemaOptions, + type VoterIdInfo, } from "./index"; import * as brazilianUtils from "./index"; @@ -233,6 +236,7 @@ const PUBLIC = [ "getNfseKeyInfo", "getPixKeyInfo", "getPixPayloadInfo", + "getProcessoJuridicoInfo", "getServiceItem", "getStateByCep", "getRegions", @@ -243,6 +247,7 @@ const PUBLIC = [ "getStates", "getStatesByRegion", "getTimezoneByState", + "getVoterIdInfo", "isBusinessDay", "isHoliday", "isValidBankAccount", @@ -470,6 +475,8 @@ describe("Public API", () => { PixKeyType: PixKeyType; PixPayloadInfo: PixPayloadInfo; PixPointOfInitiation: PixPointOfInitiation; + ProcessoJuridicoInfo: ProcessoJuridicoInfo; + ProcessoJuridicoSegment: ProcessoJuridicoSegment; Region: Region; RegionCode: RegionCode; RegistroProfissionalCouncil: RegistroProfissionalCouncil; @@ -487,6 +494,7 @@ describe("Public API", () => { StandardSchemaV1Types: StandardSchemaV1Types; StateName: StateName; ToStandardSchemaOptions: ToStandardSchemaOptions; + VoterIdInfo: VoterIdInfo; }> = {}; expect(publicTypes).toEqual({}); diff --git a/src/index.ts b/src/index.ts index 771c24b1..83fb430c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -192,6 +192,11 @@ export { type PixPointOfInitiation, getPixPayloadInfo, } from "./get-pix-payload-info/get-pix-payload-info"; +export { + type ProcessoJuridicoInfo, + type ProcessoJuridicoSegment, + getProcessoJuridicoInfo, +} from "./get-processo-juridico-info/get-processo-juridico-info"; export { type ServiceItem, getServiceItem } from "./get-service-item/get-service-item"; export { getStateByCep } from "./get-state-by-cep/get-state-by-cep"; export { type Region, type RegionCode, getRegions } from "./get-regions/get-regions"; @@ -202,6 +207,7 @@ export { getStateCapital } from "./get-state-capital/get-state-capital"; export { getStates } from "./get-states/get-states"; export { getStatesByRegion } from "./get-states-by-region/get-states-by-region"; export { getTimezoneByState } from "./get-timezone-by-state/get-timezone-by-state"; +export { type VoterIdInfo, getVoterIdInfo } from "./get-voter-id-info/get-voter-id-info"; export { type BusinessDayOptions, isBusinessDay } from "./is-business-day/is-business-day"; export { type IsHolidayParams, isHoliday } from "./is-holiday/is-holiday"; export {