From 09877acc92924c6d1d461fd4b291a35f3a06b917 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 19 Sep 2026 10:51:26 -0300 Subject: [PATCH 1/2] feat(suframa): add isValidSuframa, formatSuframa, parseSuframa and generateSuframa MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Inscrição SUFRAMA goes in the ISUF field of the NF-e recipient and a wrong one is rejected by the SEFAZ (rejection 235), so issuers need to check it before sending the document. The rules come from Anexo XII.01 of the NF-e Manual de Orientação do Contribuinte 6.0: the number is SS.NNNN.LLD (sector, sequence, locality, check digit), the field holds 8 or 9 digits because a sector code such as 01 loses its leading zero, the sector code is never 00, and the check digit is módulo 11 with weights 2 to 9 and 0 for a remainder of 0 or 1. That is the existing mod11 "arrecadacao" rule, so no new internal is needed. The manual lists the sector and locality codes only as examples, so they are not validated against a table. --- docs/pt-br/utilities.md | 59 +++++++ docs/utilities.md | 59 +++++++ jsr.json | 4 + reports/api/brazilian-utils.api.md | 17 ++ src/_internals/constants/suframa.ts | 2 + src/format-suframa/format-suframa.test.ts | 105 ++++++++++++ src/format-suframa/format-suframa.ts | 41 +++++ src/generate-suframa/generate-suframa.test.ts | 81 +++++++++ src/generate-suframa/generate-suframa.ts | 33 ++++ src/index.test.ts | 6 + src/index.ts | 4 + src/is-valid-suframa/is-valid-suframa.test.ts | 154 ++++++++++++++++++ src/is-valid-suframa/is-valid-suframa.ts | 53 ++++++ src/parse-suframa/parse-suframa.test.ts | 60 +++++++ src/parse-suframa/parse-suframa.ts | 22 +++ 15 files changed, 700 insertions(+) create mode 100644 src/_internals/constants/suframa.ts create mode 100644 src/format-suframa/format-suframa.test.ts create mode 100644 src/format-suframa/format-suframa.ts create mode 100644 src/generate-suframa/generate-suframa.test.ts create mode 100644 src/generate-suframa/generate-suframa.ts create mode 100644 src/is-valid-suframa/is-valid-suframa.test.ts create mode 100644 src/is-valid-suframa/is-valid-suframa.ts create mode 100644 src/parse-suframa/parse-suframa.test.ts create mode 100644 src/parse-suframa/parse-suframa.ts diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index e67670f4..c6e9334a 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -600,6 +600,65 @@ getNfeKeyInfo('35170458716523000119620010000000121000123450'); getNfeKeyInfo('invalid'); // null ``` +## SUFRAMA + +### isValidSuframa + +Valida uma Inscrição SUFRAMA. É o número de registro que a Superintendência da Zona Franca de Manaus dá às empresas com incentivo fiscal, informado no campo `ISUF` do destinatário da NF-e. + +- O número tem a forma `SS.NNNN.LLD`: setor de atividade, número sequencial, localidade da unidade da SUFRAMA e dígito verificador. +- Aceita 8 ou 9 dígitos: um valor de 8 dígitos é um número cujo código de setor perdeu o zero à esquerda. +- Retorna `false` para um código de setor `00` e para um dígito verificador módulo 11 errado. +- Os códigos de setor e de localidade não são conferidos com uma tabela, pois o manual os lista apenas como exemplos. +- Além dos caracteres de máscara usuais, `(`, `)`, `,` e `*` também são ignorados. + +```javascript +import { isValidSuframa } from '@brazilian-utils/brazilian-utils'; + +isValidSuframa('123456789'); // true +isValidSuframa('12.3456.789'); // true +isValidSuframa('10001018'); // true (o mesmo que '010001018') +isValidSuframa('123456780'); // false +isValidSuframa('001234560'); // false (setor 00) +``` + +### formatSuframa + +Formata uma Inscrição SUFRAMA. + +- **Opções** (`FormatSuframaOptions`): `pad` completa o valor com zeros à esquerda até os 9 dígitos antes de aplicar a máscara (padrão `false`), o que devolve o zero à esquerda de um valor de 8 dígitos. + +```javascript +import { formatSuframa } from '@brazilian-utils/brazilian-utils'; + +formatSuframa('123456789'); // 12.3456.789 +formatSuframa('10001018', { pad: true }); // 01.0001.018 +``` + +### parseSuframa + +Remove a formatação da Inscrição SUFRAMA, mantém apenas os dígitos e limita o resultado a 9 dígitos. + +```javascript +import { parseSuframa } from '@brazilian-utils/brazilian-utils'; + +parseSuframa('12.3456.789'); // 123456789 +``` + +### generateSuframa + +Gera uma Inscrição SUFRAMA aleatória válida de 9 dígitos. + +- O dígito verificador é válido e o código de setor nunca é `00`. Os códigos de setor e de localidade são aleatórios. + +```javascript +import { generateSuframa } from '@brazilian-utils/brazilian-utils'; + +generateSuframa(); // '205678106' +``` + +Fonte: [Manual de Orientação do Contribuinte da NF-e 6.0, Anexo XII.01](https://portal.fazenda.sp.gov.br/servicos/nfce/Downloads/Manual_de_Orientacao_Contribuinte_v_6.pdf). + ## Telefone ### isValidPhone diff --git a/docs/utilities.md b/docs/utilities.md index 563acf8f..c5f348a6 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -600,6 +600,65 @@ getNfeKeyInfo('35170458716523000119620010000000121000123450'); getNfeKeyInfo('invalid'); // null ``` +## SUFRAMA + +### isValidSuframa + +Check if an Inscrição SUFRAMA is valid. It is the registration number the Superintendência da Zona Franca de Manaus gives to companies with tax incentives, carried by the `ISUF` field of the NF-e recipient. + +- The number is `SS.NNNN.LLD`: sector of activity, sequential number, locality of the SUFRAMA unit and check digit. +- Accepts 8 or 9 digits: an 8 digit value is a number whose sector code lost its leading zero. +- Returns `false` for a sector code of `00` and for a wrong módulo 11 check digit. +- The sector and locality codes are not checked against a table, since the manual lists them only as examples. +- Besides the usual mask characters, `(`, `)`, `,` and `*` are also ignored. + +```javascript +import { isValidSuframa } from '@brazilian-utils/brazilian-utils'; + +isValidSuframa('123456789'); // true +isValidSuframa('12.3456.789'); // true +isValidSuframa('10001018'); // true (same as '010001018') +isValidSuframa('123456780'); // false +isValidSuframa('001234560'); // false (sector 00) +``` + +### formatSuframa + +Format an Inscrição SUFRAMA. + +- **Options** (`FormatSuframaOptions`): `pad` left-pads the value with zeros to the full 9 digits before masking (default `false`), which restores the leading zero of an 8 digit value. + +```javascript +import { formatSuframa } from '@brazilian-utils/brazilian-utils'; + +formatSuframa('123456789'); // 12.3456.789 +formatSuframa('10001018', { pad: true }); // 01.0001.018 +``` + +### parseSuframa + +Remove Inscrição SUFRAMA formatting, keep only digits, and cap the result to 9 digits. + +```javascript +import { parseSuframa } from '@brazilian-utils/brazilian-utils'; + +parseSuframa('12.3456.789'); // 123456789 +``` + +### generateSuframa + +Generate a valid random 9 digit Inscrição SUFRAMA. + +- The check digit is valid and the sector code is never `00`. The sector and locality codes are random. + +```javascript +import { generateSuframa } from '@brazilian-utils/brazilian-utils'; + +generateSuframa(); // '205678106' +``` + +Source: [NF-e Manual de Orientação do Contribuinte 6.0, Anexo XII.01](https://portal.fazenda.sp.gov.br/servicos/nfce/Downloads/Manual_de_Orientacao_Contribuinte_v_6.pdf). + ## Phone ### isValidPhone diff --git a/jsr.json b/jsr.json index 9c7925fa..6a2299eb 100644 --- a/jsr.json +++ b/jsr.json @@ -32,6 +32,7 @@ "./format-phone": "./src/format-phone/format-phone.ts", "./format-pis": "./src/format-pis/format-pis.ts", "./format-processo-juridico": "./src/format-processo-juridico/format-processo-juridico.ts", + "./format-suframa": "./src/format-suframa/format-suframa.ts", "./format-voter-id": "./src/format-voter-id/format-voter-id.ts", "./generate-boleto": "./src/generate-boleto/generate-boleto.ts", "./generate-cep": "./src/generate-cep/generate-cep.ts", @@ -46,6 +47,7 @@ "./generate-pix-payload": "./src/generate-pix-payload/generate-pix-payload.ts", "./generate-processo-juridico": "./src/generate-processo-juridico/generate-processo-juridico.ts", "./generate-renavam": "./src/generate-renavam/generate-renavam.ts", + "./generate-suframa": "./src/generate-suframa/generate-suframa.ts", "./generate-voter-id": "./src/generate-voter-id/generate-voter-id.ts", "./get-address-info-by-cep": "./src/get-address-info-by-cep/get-address-info-by-cep.ts", "./get-area-code-info": "./src/get-area-code-info/get-area-code-info.ts", @@ -115,6 +117,7 @@ "./is-valid-registro-profissional": "./src/is-valid-registro-profissional/is-valid-registro-profissional.ts", "./is-valid-renavam": "./src/is-valid-renavam/is-valid-renavam.ts", "./is-valid-service-phone": "./src/is-valid-service-phone/is-valid-service-phone.ts", + "./is-valid-suframa": "./src/is-valid-suframa/is-valid-suframa.ts", "./is-valid-vin": "./src/is-valid-vin/is-valid-vin.ts", "./is-valid-voter-id": "./src/is-valid-voter-id/is-valid-voter-id.ts", "./parse-boleto": "./src/parse-boleto/parse-boleto.ts", @@ -140,6 +143,7 @@ "./parse-phone": "./src/parse-phone/parse-phone.ts", "./parse-pis": "./src/parse-pis/parse-pis.ts", "./parse-processo-juridico": "./src/parse-processo-juridico/parse-processo-juridico.ts", + "./parse-suframa": "./src/parse-suframa/parse-suframa.ts", "./parse-voter-id": "./src/parse-voter-id/parse-voter-id.ts", "./remove-accents": "./src/remove-accents/remove-accents.ts", "./sub-business-days": "./src/sub-business-days/sub-business-days.ts", diff --git a/reports/api/brazilian-utils.api.md b/reports/api/brazilian-utils.api.md index 1ccd4208..0d630e3f 100644 --- a/reports/api/brazilian-utils.api.md +++ b/reports/api/brazilian-utils.api.md @@ -314,6 +314,14 @@ export type FormatProcessoJuridicoOptions = { pad?: boolean; }; +// @public +export const formatSuframa: (value: string | number, options?: FormatSuframaOptions) => string; + +// @public +export type FormatSuframaOptions = { + pad?: boolean; +}; + // @public export const formatVoterId: (value: string | number) => string; @@ -399,6 +407,9 @@ export type GenerateProcessoJuridicoParams = { // @public export const generateRenavam: () => string; +// @public +export const generateSuframa: () => string; + // @public export const generateVoterId: (state?: StateCode | "ZZ") => string; @@ -815,6 +826,9 @@ export const isValidRenavam: (renavam: string | number) => boolean; // @public export const isValidServicePhone: (value: string) => boolean; +// @public +export const isValidSuframa: (suframa: string) => boolean; + // @public export const isValidVin: (value: string) => boolean; @@ -947,6 +961,9 @@ export const parsePis: (value: string | number) => string; // @public export const parseProcessoJuridico: (value: string | number) => string; +// @public +export const parseSuframa: (value: string | number) => string; + // @public export const parseVoterId: (value: string | number) => string; diff --git a/src/_internals/constants/suframa.ts b/src/_internals/constants/suframa.ts new file mode 100644 index 00000000..3dec53b8 --- /dev/null +++ b/src/_internals/constants/suframa.ts @@ -0,0 +1,2 @@ +/** Digits of an Inscrição SUFRAMA written in full, sector code with its leading zero included. */ +export const SUFRAMA_LENGTH = 9; diff --git a/src/format-suframa/format-suframa.test.ts b/src/format-suframa/format-suframa.test.ts new file mode 100644 index 00000000..4ac5a800 --- /dev/null +++ b/src/format-suframa/format-suframa.test.ts @@ -0,0 +1,105 @@ +import { SUFRAMA_LENGTH } from "../_internals/constants/suframa"; +import { anyValue, digits, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectMatchesPattern, + expectPadsToLength, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { parseSuframa } from "../parse-suframa/parse-suframa"; +import { formatSuframa, type FormatSuframaOptions } from "./format-suframa"; + +describe("formatSuframa", () => { + it("should format the example of the NF-e manual", () => { + expect(formatSuframa("123456789")).toBe("12.3456.789"); + }); + + it("when it is a no formatted string", () => { + expect(formatSuframa("")).toBe(""); + expect(formatSuframa("2")).toBe("2"); + expect(formatSuframa("20")).toBe("20"); + expect(formatSuframa("205")).toBe("20.5"); + expect(formatSuframa("2056")).toBe("20.56"); + expect(formatSuframa("20567")).toBe("20.567"); + expect(formatSuframa("205678")).toBe("20.5678"); + expect(formatSuframa("2056781")).toBe("20.5678.1"); + expect(formatSuframa("20567810")).toBe("20.5678.10"); + expect(formatSuframa("205678106")).toBe("20.5678.106"); + }); + + it("when it is a formatted string", () => { + expect(formatSuframa("20.5")).toBe("20.5"); + expect(formatSuframa("20.5678")).toBe("20.5678"); + expect(formatSuframa("20.56781")).toBe("20.5678.1"); + expect(formatSuframa("20.5678.106")).toBe("20.5678.106"); + expect(formatSuframa("20.5678.10-6")).toBe("20.5678.106"); + }); + + it("when it is a malformed string", () => { + expect(formatSuframa("20#Error*&@#5678#Char!106")).toBe("20.5678.106"); + expect(formatSuframa("#-+Error#205678106#Char!")).toBe("20.5678.106"); + }); + + it("when it is a number", () => { + expect(formatSuframa(1)).toBe("1"); + expect(formatSuframa(205)).toBe("20.5"); + expect(formatSuframa(205_678_106)).toBe("20.5678.106"); + expect(formatSuframa(10_001_018)).toBe("10.0010.18"); + }); + + it("should left pad with zeros when the pad option is set", () => { + expect(formatSuframa("10001018", { pad: true })).toBe("01.0001.018"); + expect(formatSuframa(10_001_018, { pad: true })).toBe("01.0001.018"); + expect(formatSuframa("1", { pad: true })).toBe("00.0000.001"); + expect(formatSuframa("205678106", { pad: true })).toBe("20.5678.106"); + }); + + it("should not pad when the pad option is false", () => { + expect(formatSuframa("10001018", { pad: false })).toBe("10.0010.18"); + }); + + it(`should NOT add digits after the Inscrição SUFRAMA length (${SUFRAMA_LENGTH})`, () => { + expect(formatSuframa("2056781069")).toBe("20.5678.106"); + expect(formatSuframa("205678106999")).toBe("20.5678.106"); + }); + + it("should return an empty string for null or undefined", () => { + // @ts-expect-error: intentionally invalid input + expect(formatSuframa(null)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(formatSuframa()).toBe(""); + }); + + describe("properties", () => { + const upToASuframa = digitsUpTo(9); + + test("should only add the mask, never change the digits", () => { + expectRoundTrip(formatSuframa, parseSuframa, upToASuframa); + }); + + test("should produce the documented mask shape for a full Inscrição SUFRAMA", () => { + expectMatchesPattern(formatSuframa, /^\d{2}\.\d{4}\.\d{3}$/, digits(9)); + }); + + test("should left pad a shorter value up to the Inscrição SUFRAMA length", () => { + expectPadsToLength(formatSuframa, parseSuframa, upToASuframa, SUFRAMA_LENGTH); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(formatSuframa, "string", anyValue); + }); + }); +}); + +describe("formatSuframa types", () => { + test("should take a string or number value and options and return a string", () => { + expectTypeOf(formatSuframa).parameter(0).toEqualTypeOf(); + expectTypeOf(formatSuframa).parameter(1).toEqualTypeOf(); + expectTypeOf(formatSuframa).returns.toEqualTypeOf(); + }); + + test("should type the pad option as an optional boolean", () => { + expectTypeOf().toEqualTypeOf(); + }); +}); diff --git a/src/format-suframa/format-suframa.ts b/src/format-suframa/format-suframa.ts new file mode 100644 index 00000000..f450af37 --- /dev/null +++ b/src/format-suframa/format-suframa.ts @@ -0,0 +1,41 @@ +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 `formatSuframa`. */ +export type FormatSuframaOptions = { + /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ + pad?: boolean; +}; + +/** + * Formats an Inscrição SUFRAMA with the `SS.NNNN.LLD` mask: sector of activity, sequential + * number, locality and check digit. + * + * An 8 digit value is a number whose sector code lost its leading zero, so format it with + * `pad: true` to get the zero back. + * + * @param {string|number} value - The Inscrição SUFRAMA to be formatted. It can be a string or a number. + * @param {FormatSuframaOptions} [options] - Optional formatting options. + * @param {boolean} options.pad - If true, pads the value with leading zeros if necessary. + * @returns {string} The formatted Inscrição SUFRAMA as a string. + * + * @example + * ```typescript + * formatSuframa("123456789"); // "12.3456.789" + * formatSuframa(123456789); // "12.3456.789" + * formatSuframa("10001018", { pad: true }); // "01.0001.018" + * ``` + * + * @see Official: https://portal.fazenda.sp.gov.br/servicos/nfce/Downloads/Manual_de_Orientacao_Contribuinte_v_6.pdf + * Manual de Orientação do Contribuinte da NF-e 6.0, Anexo XII.01, which gives the composition + * as `SS.NNNN.LLD`. + */ +export const formatSuframa = (value: string | number, options?: FormatSuframaOptions): string => + isNullish(value) + ? "" + : format({ + pad: options?.pad, + value: sanitizeToDigits(value), + pattern: "00.0000.000", + }); diff --git a/src/generate-suframa/generate-suframa.test.ts b/src/generate-suframa/generate-suframa.test.ts new file mode 100644 index 00000000..edcbb601 --- /dev/null +++ b/src/generate-suframa/generate-suframa.test.ts @@ -0,0 +1,81 @@ +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { isValidSuframa } from "../is-valid-suframa/is-valid-suframa"; +import { generateSuframa } from "./generate-suframa"; + +const withRandomDigits = (digits: number[], run: () => void): void => { + const originalRandom = Math.random; + let call = 0; + + Math.random = () => { + const digit = digits[call]; + call += 1; + return (digit + 0.5) / 10; + }; + + try { + run(); + } finally { + Math.random = originalRandom; + } +}; + +describe("generateSuframa", () => { + test("should have the right length without mask (9)", () => { + expect(generateSuframa()).toHaveLength(9); + expect(/^\d{9}$/.test(generateSuframa())).toBe(true); + }); + + test("should always generate a valid Inscrição SUFRAMA", () => { + for (let i = 0; i < 1000; i++) { + expect(isValidSuframa(generateSuframa())).toBe(true); + } + }); + + test("should append the check digit of the NF-e manual example", () => { + withRandomDigits([1, 2, 3, 4, 5, 6, 7, 8], () => { + expect(generateSuframa()).toBe("123456789"); + }); + }); + + test("should append 0 when the remainder is 0 or 1", () => { + withRandomDigits([1, 0, 0, 0, 0, 0, 0, 1], () => { + expect(generateSuframa()).toBe("100000010"); + }); + withRandomDigits([6, 0, 0, 0, 0, 1, 3, 0], () => { + expect(generateSuframa()).toBe("600001300"); + }); + }); + + test("should regenerate the base when the sector code comes out as 00", () => { + withRandomDigits([0, 0, 1, 2, 3, 4, 5, 6, 0, 1, 0, 0, 0, 1, 0, 1], () => { + expect(generateSuframa()).toBe("010001018"); + }); + }); + + describe("properties", () => { + const batchSize = fc.integer({ min: 1, max: 20 }); + + test("should generate 9 digit numbers its own validator accepts, never with sector code 00", () => { + fc.assert( + fc.property(batchSize, (size) => { + for (let index = 0; index < size; index++) { + const suframa = generateSuframa(); + + expect(suframa).toMatch(/^\d{9}$/); + expect(suframa.startsWith("00")).toBe(false); + expect(isValidSuframa(suframa)).toBe(true); + } + }), + ); + }); + }); +}); + +describe("generateSuframa types", () => { + test("should take no parameters and return a string", () => { + expectTypeOf(generateSuframa).parameters.toEqualTypeOf<[]>(); + expectTypeOf(generateSuframa).returns.toEqualTypeOf(); + }); +}); diff --git a/src/generate-suframa/generate-suframa.ts b/src/generate-suframa/generate-suframa.ts new file mode 100644 index 00000000..95085cad --- /dev/null +++ b/src/generate-suframa/generate-suframa.ts @@ -0,0 +1,33 @@ +import { SUFRAMA_LENGTH } from "../_internals/constants/suframa"; +import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; +import { mod11 } from "../_internals/mod11/mod11"; + +/** + * Generates a random Inscrição SUFRAMA with a valid check digit, for tests and fixtures. + * + * The sector code is never `00`, the only structural rule the NF-e manual states. The manual lists + * sector and locality codes only as examples, so the generated ones are random and need not match + * a code SUFRAMA uses. + * + * Uses `Math.random()` internally, so it is not cryptographically secure, do not use for security purposes. + * + * @returns {string} A valid 9-digit Inscrição SUFRAMA string without formatting. + * + * @example + * ```typescript + * generateSuframa(); // "123456789" + * ``` + * + * @see Official: https://portal.fazenda.sp.gov.br/servicos/nfce/Downloads/Manual_de_Orientacao_Contribuinte_v_6.pdf + * Manual de Orientação do Contribuinte da NF-e 6.0, Anexo XII.01 (composition, validation and + * check digit example). + */ +export const generateSuframa = (): string => { + let base = generateRandomNumber(SUFRAMA_LENGTH - 1); + + while (base.startsWith("00")) { + base = generateRandomNumber(SUFRAMA_LENGTH - 1); + } + + return `${base}${mod11(base, { variant: "arrecadacao" })}`; +}; diff --git a/src/index.test.ts b/src/index.test.ts index 61ad4529..a4e4cf4f 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -34,6 +34,7 @@ import { type FormatPhoneOptions, type FormatPisOptions, type FormatProcessoJuridicoOptions, + type FormatSuframaOptions, type GenerateBoletoParams, type GenerateCnpjParams, type GenerateLicensePlateFormat, @@ -142,6 +143,7 @@ const PUBLIC = [ "formatPhone", "formatPis", "formatProcessoJuridico", + "formatSuframa", "formatVoterId", "generateBoleto", "generateCNPJ", @@ -158,6 +160,7 @@ const PUBLIC = [ "generatePixPayload", "generateProcessoJuridico", "generateRenavam", + "generateSuframa", "generateVoterId", "getAddressInfoByCep", "getAreaCodeInfo", @@ -232,6 +235,7 @@ const PUBLIC = [ "isValidRegistroProfissional", "isValidRenavam", "isValidServicePhone", + "isValidSuframa", "isValidVin", "isValidVoterId", "parseBoleto", @@ -257,6 +261,7 @@ const PUBLIC = [ "parsePhone", "parsePis", "parseProcessoJuridico", + "parseSuframa", "parseVoterId", "removeAccents", "subBusinessDays", @@ -324,6 +329,7 @@ describe("Public API", () => { FormatPhoneOptions: FormatPhoneOptions; FormatPisOptions: FormatPisOptions; FormatProcessoJuridicoOptions: FormatProcessoJuridicoOptions; + FormatSuframaOptions: FormatSuframaOptions; GenerateBoletoParams: GenerateBoletoParams; GenerateCnpjParams: GenerateCnpjParams; GenerateLicensePlateFormat: GenerateLicensePlateFormat; diff --git a/src/index.ts b/src/index.ts index 68c6b569..98c3dd61 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,6 +53,7 @@ export { type FormatProcessoJuridicoOptions, formatProcessoJuridico, } from "./format-processo-juridico/format-processo-juridico"; +export { type FormatSuframaOptions, formatSuframa } from "./format-suframa/format-suframa"; export { formatVoterId } from "./format-voter-id/format-voter-id"; export { type GenerateBoletoParams, generateBoleto } from "./generate-boleto/generate-boleto"; export { generateCep } from "./generate-cep/generate-cep"; @@ -76,6 +77,7 @@ export { generateProcessoJuridico, } from "./generate-processo-juridico/generate-processo-juridico"; export { generateRenavam } from "./generate-renavam/generate-renavam"; +export { generateSuframa } from "./generate-suframa/generate-suframa"; export { generateVoterId } from "./generate-voter-id/generate-voter-id"; export { type AddressInfo, @@ -219,6 +221,7 @@ export { } from "./is-valid-registro-profissional/is-valid-registro-profissional"; export { isValidRenavam } from "./is-valid-renavam/is-valid-renavam"; export { isValidServicePhone } from "./is-valid-service-phone/is-valid-service-phone"; +export { isValidSuframa } from "./is-valid-suframa/is-valid-suframa"; 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"; @@ -244,6 +247,7 @@ export { parsePassport } from "./parse-passport/parse-passport"; export { parsePhone } from "./parse-phone/parse-phone"; export { parsePis } from "./parse-pis/parse-pis"; export { parseProcessoJuridico } from "./parse-processo-juridico/parse-processo-juridico"; +export { parseSuframa } from "./parse-suframa/parse-suframa"; 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"; diff --git a/src/is-valid-suframa/is-valid-suframa.test.ts b/src/is-valid-suframa/is-valid-suframa.test.ts new file mode 100644 index 00000000..2b4d398c --- /dev/null +++ b/src/is-valid-suframa/is-valid-suframa.test.ts @@ -0,0 +1,154 @@ +import * as fc from "fast-check"; + +import { anyValue, digitsOfOtherLength, maskSeparators } from "../_internals/test/arbitraries"; +import { expectAlwaysReturnsType, expectRejected } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { generateSuframa } from "../generate-suframa/generate-suframa"; +import { isValidSuframa } from "./is-valid-suframa"; + +describe("isValidSuframa", () => { + describe("should return false", () => { + test("when it is an empty string", () => { + expect(isValidSuframa("")).toBe(false); + }); + + test("when it is null", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidSuframa(null)).toBe(false); + }); + + test("when it is undefined", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidSuframa()).toBe(false); + }); + + test("when it is a boolean", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidSuframa(true)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidSuframa(false)).toBe(false); + }); + + test("when it is an object", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidSuframa({})).toBe(false); + }); + + test("when it is an array", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidSuframa([])).toBe(false); + }); + + test("when it is a non-string that stringifies to a valid Inscrição SUFRAMA", () => { + // @ts-expect-error: intentionally invalid input + expect(isValidSuframa(123_456_789)).toBe(false); + // @ts-expect-error: intentionally invalid input + expect(isValidSuframa([123_456_789])).toBe(false); + }); + + test("when it has fewer than 8 digits", () => { + expect(isValidSuframa("1234567")).toBe(false); + expect(isValidSuframa("0001018")).toBe(false); + }); + + test("when it has more than 9 digits, even if the first 9 are valid", () => { + expect(isValidSuframa("1234567899")).toBe(false); + expect(isValidSuframa("1234567090")).toBe(false); + }); + + test("when it contains letters or special characters", () => { + expect(isValidSuframa("12345678A9")).toBe(false); + expect(isValidSuframa("12#3456#789")).toBe(false); + expect(isValidSuframa("abcdefghi")).toBe(false); + }); + + test("when the check digit is wrong", () => { + expect(isValidSuframa("123456780")).toBe(false); + expect(isValidSuframa("123456788")).toBe(false); + expect(isValidSuframa("205678105")).toBe(false); + expect(isValidSuframa("10001019")).toBe(false); + }); + + test("when the remainder is 0 or 1 and the check digit is not 0", () => { + expect(isValidSuframa("100000011")).toBe(false); + expect(isValidSuframa("600001301")).toBe(false); + }); + + test("when the sector code is 00", () => { + expect(isValidSuframa("001234560")).toBe(false); + expect(isValidSuframa("000000000")).toBe(false); + expect(isValidSuframa("000000019")).toBe(false); + }); + + test("when an 8 digit value starts with 0, which reads as sector code 00", () => { + expect(isValidSuframa("01234560")).toBe(false); + }); + }); + + describe("should return true", () => { + test("when it is the example of the NF-e manual", () => { + expect(isValidSuframa("123456789")).toBe(true); + }); + + test("when it is a valid Inscrição SUFRAMA without mask", () => { + expect(isValidSuframa("010001018")).toBe(true); + expect(isValidSuframa("101234015")).toBe(true); + expect(isValidSuframa("205678106")).toBe(true); + expect(isValidSuframa("601234308")).toBe(true); + }); + + test("when it is a valid Inscrição SUFRAMA with mask", () => { + expect(isValidSuframa("12.3456.789")).toBe(true); + expect(isValidSuframa("20.5678.10-6")).toBe(true); + expect(isValidSuframa("20 5678 10 6")).toBe(true); + }); + + test("when it has 8 digits because the sector code lost its leading zero", () => { + expect(isValidSuframa("10001018")).toBe(true); + expect(isValidSuframa("1.0001.018")).toBe(true); + }); + + test("when the remainder is 0 and the check digit is 0", () => { + expect(isValidSuframa("100000010")).toBe(true); + }); + + test("when the remainder is 1 and the check digit is 0", () => { + expect(isValidSuframa("600001300")).toBe(true); + }); + + test("when the remainder is 10 and the check digit is 1", () => { + expect(isValidSuframa("100000061")).toBe(true); + }); + }); + + describe("properties", () => { + test("should accept a generated Inscrição SUFRAMA written with any of the accepted mask characters", () => { + const masks = maskSeparators([".", "-", "/", " ", "(", ")", ",", "*"], 4, 3); + + fc.assert( + fc.property(masks, (separators) => { + const suframa = generateSuframa(); + const head = `${separators[0]}${suframa.slice(0, 2)}${separators[1]}`; + const tail = `${suframa.slice(2, 6)}${separators[2]}${suframa.slice(6)}`; + + expect(isValidSuframa(`${head}${tail}${separators[3]}`)).toBe(true); + }), + ); + }); + + test("should reject any digits only value that is not 8 or 9 digits long", () => { + expectRejected(isValidSuframa, digitsOfOtherLength(22, [8, 9])); + }); + + test("should never throw and always return a boolean", () => { + expectAlwaysReturnsType(isValidSuframa, "boolean", anyValue); + }); + }); +}); + +describe("isValidSuframa types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidSuframa).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidSuframa).returns.toEqualTypeOf(); + }); +}); diff --git a/src/is-valid-suframa/is-valid-suframa.ts b/src/is-valid-suframa/is-valid-suframa.ts new file mode 100644 index 00000000..1f407a47 --- /dev/null +++ b/src/is-valid-suframa/is-valid-suframa.ts @@ -0,0 +1,53 @@ +import { SUFRAMA_LENGTH } from "../_internals/constants/suframa"; +import { mod11 } from "../_internals/mod11/mod11"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +/** + * Validates an Inscrição SUFRAMA, the registration number the Superintendência da Zona Franca de + * Manaus gives to companies with tax incentives, carried by the `ISUF` field of the NF-e. + * Accepts the usual mask characters (`.`, `-`, `/`, `(`, `)`, `,`, `*`) and whitespace. + * + * The number is `SS.NNNN.LLD`: sector of activity, sequential number, locality of the SUFRAMA + * unit that registered the company and check digit. The NF-e field is numeric with 8 or 9 + * positions, because a sector code such as `01` loses its leading zero, so an 8 digit value is + * read with that zero back in place. The sector code can never be `00`. The check digit is módulo + * 11 with weights 2 to 9 from right to left, and is 0 when the remainder is 0 or 1. + * + * The manual lists sector and locality codes only as examples, so they are not checked against a + * table. + * + * @param {string} suframa - The Inscrição SUFRAMA to validate. + * @returns {boolean} True if the Inscrição SUFRAMA is valid, false otherwise. + * + * @example + * ```typescript + * isValidSuframa("123456789"); // true + * isValidSuframa("12.3456.789"); // true + * isValidSuframa("10001018"); // true (same as "010001018") + * isValidSuframa("123456780"); // false (wrong check digit) + * isValidSuframa("001234560"); // false (sector 00) + * ``` + * + * @see Official: https://portal.fazenda.sp.gov.br/servicos/nfce/Downloads/Manual_de_Orientacao_Contribuinte_v_6.pdf + * Manual de Orientação do Contribuinte da NF-e 6.0, Anexo XII.01 (composition, validation and + * check digit example) and validation rule E18-20 (rejection 235). + */ +export const isValidSuframa = (suframa: string): boolean => { + if (typeof suframa !== "string") return false; + + const hasInvalidChars = /[^0-9\s().,*/-]/.test(suframa); + + if (hasInvalidChars) return false; + + const digits = sanitizeToDigits(suframa); + + if (digits.length !== SUFRAMA_LENGTH && digits.length !== SUFRAMA_LENGTH - 1) return false; + + const full = digits.padStart(SUFRAMA_LENGTH, "0"); + + if (full.startsWith("00")) return false; + + const checkDigit = mod11(full.slice(0, -1), { variant: "arrecadacao" }); + + return full.charCodeAt(SUFRAMA_LENGTH - 1) - 48 === checkDigit; +}; diff --git a/src/parse-suframa/parse-suframa.test.ts b/src/parse-suframa/parse-suframa.test.ts new file mode 100644 index 00000000..cfd74469 --- /dev/null +++ b/src/parse-suframa/parse-suframa.test.ts @@ -0,0 +1,60 @@ +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 { parseSuframa } from "./parse-suframa"; + +describe("parseSuframa", () => { + it("should remove Inscrição SUFRAMA mask characters", () => { + expect(parseSuframa("12.3456.789")).toBe("123456789"); + expect(parseSuframa("20.5678.10-6")).toBe("205678106"); + }); + + it("should keep an unmasked value as it is", () => { + expect(parseSuframa("123456789")).toBe("123456789"); + expect(parseSuframa("10001018")).toBe("10001018"); + }); + + it("should accept a number", () => { + expect(parseSuframa(123_456_789)).toBe("123456789"); + }); + + it("should remove non numeric characters", () => { + expect(parseSuframa("12#Error*&@#3456#Char!789")).toBe("123456789"); + }); + + it("should ignore digits after the Inscrição SUFRAMA length", () => { + expect(parseSuframa("123456789123")).toBe("123456789"); + }); + + it("should return an empty string for null or undefined", () => { + // @ts-expect-error: intentionally invalid input + expect(parseSuframa(null)).toBe(""); + // @ts-expect-error: intentionally invalid input + expect(parseSuframa()).toBe(""); + }); + + describe("properties", () => { + test("should return at most the digits of an Inscrição SUFRAMA", () => { + expectMatchesPattern(parseSuframa, /^\d{0,9}$/, anyText); + }); + + test("should be idempotent", () => { + expectIdempotent(parseSuframa, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseSuframa, "string", anyValue); + }); + }); +}); + +describe("parseSuframa types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseSuframa).parameter(0).toEqualTypeOf(); + expectTypeOf(parseSuframa).returns.toEqualTypeOf(); + }); +}); diff --git a/src/parse-suframa/parse-suframa.ts b/src/parse-suframa/parse-suframa.ts new file mode 100644 index 00000000..4acc7df7 --- /dev/null +++ b/src/parse-suframa/parse-suframa.ts @@ -0,0 +1,22 @@ +import { SUFRAMA_LENGTH } from "../_internals/constants/suframa"; +import { isNullish } from "../_internals/is-nullish/is-nullish"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +/** + * Removes Inscrição SUFRAMA formatting characters and returns only digits, the way the `ISUF` + * field of the NF-e expects them. + * + * @param {string|number} value - The Inscrição SUFRAMA to be parsed. + * @returns {string} The Inscrição SUFRAMA without formatting. + * + * @example + * ```typescript + * parseSuframa("12.3456.789"); // "123456789" + * ``` + * + * @see Official: https://portal.fazenda.sp.gov.br/servicos/nfce/Downloads/Manual_de_Orientacao_Contribuinte_v_6.pdf + * Manual de Orientação do Contribuinte da NF-e 6.0, Anexo XII.01 and field E18 (`ISUF`, numeric, + * 8 or 9 positions). + */ +export const parseSuframa = (value: string | number): string => + isNullish(value) ? "" : sanitizeToDigits(value).slice(0, SUFRAMA_LENGTH); From 5b2b19971b452c7540a7e2fb9c9915bf130cba52 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 19 Sep 2026 11:46:36 -0300 Subject: [PATCH 2/2] docs(suframa): cite MOC 7.0 and show the unpadded 8 digit mask MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four utilities cited a SEFAZ-SP copy of the superseded MOC 6.0. The current national manual, MOC 7.0, is published by CONFAZ and says the same in the Visão Geral, section 8.4: the composition SS.NNNN.LLD, the numeric field with 8 or 9 positions whose SS may start with 0 but never be 00, and the módulo 11 check digit with weights 2 to 9. Its Anexo I still types field 79 (E18, ISUF) as numeric with 8 to 9 positions and still rejects an invalid check digit through rule E18-20 (rejection 235), so nothing in the code changes, only the citation. That also answers the open point about MOC 7.0 changing the annex: it does not. The docs never showed what formatSuframa does to an 8 digit value without pad, although 8 positions is a valid ISUF length, so a reader could not see that the mask is progressive and groups one position early. Both docs and the example block now show it next to the padded form. The test that guarded against a 10 digit value was also named after the first 9 digits being valid while its second vector, 1234567090, is the opposite case: 123456709 has check digit 3, not 9, and it is the last 9 digits, 234567090, that are valid. Split into the two named cases. --- docs/pt-br/utilities.md | 4 +++- docs/utilities.md | 4 +++- src/format-suframa/format-suframa.ts | 10 ++++++---- src/generate-suframa/generate-suframa.ts | 6 +++--- src/is-valid-suframa/is-valid-suframa.test.ts | 3 +++ src/is-valid-suframa/is-valid-suframa.ts | 10 +++++++--- src/parse-suframa/parse-suframa.ts | 8 +++++--- 7 files changed, 30 insertions(+), 15 deletions(-) diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index c6e9334a..75d119ef 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -627,11 +627,13 @@ isValidSuframa('001234560'); // false (setor 00) Formata uma Inscrição SUFRAMA. - **Opções** (`FormatSuframaOptions`): `pad` completa o valor com zeros à esquerda até os 9 dígitos antes de aplicar a máscara (padrão `false`), o que devolve o zero à esquerda de um valor de 8 dígitos. +- A máscara é progressiva, como nas outras funções `format`, então um valor de 8 dígitos sem `pad` é agrupado uma posição antes: use `pad: true` para um valor lido direto do campo `ISUF`, que pode vir com 8 dígitos. ```javascript import { formatSuframa } from '@brazilian-utils/brazilian-utils'; formatSuframa('123456789'); // 12.3456.789 +formatSuframa('10001018'); // 10.0010.18 (8 dígitos, a máscara agrupa uma posição antes) formatSuframa('10001018', { pad: true }); // 01.0001.018 ``` @@ -657,7 +659,7 @@ import { generateSuframa } from '@brazilian-utils/brazilian-utils'; generateSuframa(); // '205678106' ``` -Fonte: [Manual de Orientação do Contribuinte da NF-e 6.0, Anexo XII.01](https://portal.fazenda.sp.gov.br/servicos/nfce/Downloads/Manual_de_Orientacao_Contribuinte_v_6.pdf). +Fonte: [Manual de Orientação do Contribuinte da NF-e 7.0, Visão Geral](https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf) (seção 8.4), [MOC 7.0, Anexo I](https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-anexo-i-leiaute-e-rv.pdf) (campo 79, `E18` `ISUF`, e regra E18-20). ## Telefone diff --git a/docs/utilities.md b/docs/utilities.md index c5f348a6..b10fc7bc 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -627,11 +627,13 @@ isValidSuframa('001234560'); // false (sector 00) Format an Inscrição SUFRAMA. - **Options** (`FormatSuframaOptions`): `pad` left-pads the value with zeros to the full 9 digits before masking (default `false`), which restores the leading zero of an 8 digit value. +- The mask is progressive, as in the other `format` utilities, so an 8 digit value without `pad` is grouped one position early: use `pad: true` for a value read straight out of the `ISUF` field, which may be stored with 8 digits. ```javascript import { formatSuframa } from '@brazilian-utils/brazilian-utils'; formatSuframa('123456789'); // 12.3456.789 +formatSuframa('10001018'); // 10.0010.18 (8 digits, the mask groups one position early) formatSuframa('10001018', { pad: true }); // 01.0001.018 ``` @@ -657,7 +659,7 @@ import { generateSuframa } from '@brazilian-utils/brazilian-utils'; generateSuframa(); // '205678106' ``` -Source: [NF-e Manual de Orientação do Contribuinte 6.0, Anexo XII.01](https://portal.fazenda.sp.gov.br/servicos/nfce/Downloads/Manual_de_Orientacao_Contribuinte_v_6.pdf). +Source: [NF-e Manual de Orientação do Contribuinte 7.0, Visão Geral](https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf) (section 8.4), [MOC 7.0, Anexo I](https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-anexo-i-leiaute-e-rv.pdf) (field 79, `E18` `ISUF`, and rule E18-20). ## Phone diff --git a/src/format-suframa/format-suframa.ts b/src/format-suframa/format-suframa.ts index f450af37..de1fa0ab 100644 --- a/src/format-suframa/format-suframa.ts +++ b/src/format-suframa/format-suframa.ts @@ -13,7 +13,8 @@ export type FormatSuframaOptions = { * number, locality and check digit. * * An 8 digit value is a number whose sector code lost its leading zero, so format it with - * `pad: true` to get the zero back. + * `pad: true` to get the zero back. The mask is progressive, as in the other `format` utilities, + * so an 8 digit value without `pad` is grouped one position early. * * @param {string|number} value - The Inscrição SUFRAMA to be formatted. It can be a string or a number. * @param {FormatSuframaOptions} [options] - Optional formatting options. @@ -24,12 +25,13 @@ export type FormatSuframaOptions = { * ```typescript * formatSuframa("123456789"); // "12.3456.789" * formatSuframa(123456789); // "12.3456.789" + * formatSuframa("10001018"); // "10.0010.18" (8 digits, the mask groups one position early) * formatSuframa("10001018", { pad: true }); // "01.0001.018" * ``` * - * @see Official: https://portal.fazenda.sp.gov.br/servicos/nfce/Downloads/Manual_de_Orientacao_Contribuinte_v_6.pdf - * Manual de Orientação do Contribuinte da NF-e 6.0, Anexo XII.01, which gives the composition - * as `SS.NNNN.LLD`. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf + * Manual de Orientação do Contribuinte (MOC) NF-e 7.0, Visão Geral, section 8.4, which gives the + * composition as `SS.NNNN.LLD`. */ export const formatSuframa = (value: string | number, options?: FormatSuframaOptions): string => isNullish(value) diff --git a/src/generate-suframa/generate-suframa.ts b/src/generate-suframa/generate-suframa.ts index 95085cad..41128254 100644 --- a/src/generate-suframa/generate-suframa.ts +++ b/src/generate-suframa/generate-suframa.ts @@ -18,9 +18,9 @@ import { mod11 } from "../_internals/mod11/mod11"; * generateSuframa(); // "123456789" * ``` * - * @see Official: https://portal.fazenda.sp.gov.br/servicos/nfce/Downloads/Manual_de_Orientacao_Contribuinte_v_6.pdf - * Manual de Orientação do Contribuinte da NF-e 6.0, Anexo XII.01 (composition, validation and - * check digit example). + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf + * Manual de Orientação do Contribuinte (MOC) NF-e 7.0, Visão Geral, section 8.4: the composition + * `SS.NNNN.LLD` with `SS` never `00`, and the módulo 11 check digit with weights 2 to 9. */ export const generateSuframa = (): string => { let base = generateRandomNumber(SUFRAMA_LENGTH - 1); diff --git a/src/is-valid-suframa/is-valid-suframa.test.ts b/src/is-valid-suframa/is-valid-suframa.test.ts index 2b4d398c..6ffc60f5 100644 --- a/src/is-valid-suframa/is-valid-suframa.test.ts +++ b/src/is-valid-suframa/is-valid-suframa.test.ts @@ -53,6 +53,9 @@ describe("isValidSuframa", () => { test("when it has more than 9 digits, even if the first 9 are valid", () => { expect(isValidSuframa("1234567899")).toBe(false); + }); + + test("when it has more than 9 digits, even if the last 9 are valid", () => { expect(isValidSuframa("1234567090")).toBe(false); }); diff --git a/src/is-valid-suframa/is-valid-suframa.ts b/src/is-valid-suframa/is-valid-suframa.ts index 1f407a47..6e63a3c5 100644 --- a/src/is-valid-suframa/is-valid-suframa.ts +++ b/src/is-valid-suframa/is-valid-suframa.ts @@ -28,9 +28,13 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * isValidSuframa("001234560"); // false (sector 00) * ``` * - * @see Official: https://portal.fazenda.sp.gov.br/servicos/nfce/Downloads/Manual_de_Orientacao_Contribuinte_v_6.pdf - * Manual de Orientação do Contribuinte da NF-e 6.0, Anexo XII.01 (composition, validation and - * check digit example) and validation rule E18-20 (rejection 235). + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf + * Manual de Orientação do Contribuinte (MOC) NF-e 7.0, Visão Geral, section 8.4: the composition + * `SS.NNNN.LLD`, the numeric field with 8 or 9 positions whose `SS` may start with 0 but never be + * `00`, and the módulo 11 check digit with weights 2 to 9 and 0 for a remainder of 0 or 1. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-anexo-i-leiaute-e-rv.pdf + * MOC 7.0, Anexo I: field 79 (`E18`, `ISUF`) is numeric with 8 to 9 positions, and validation + * rule E18-20 turns down an Inscrição SUFRAMA with an invalid check digit (rejection 235). */ export const isValidSuframa = (suframa: string): boolean => { if (typeof suframa !== "string") return false; diff --git a/src/parse-suframa/parse-suframa.ts b/src/parse-suframa/parse-suframa.ts index 4acc7df7..40336f18 100644 --- a/src/parse-suframa/parse-suframa.ts +++ b/src/parse-suframa/parse-suframa.ts @@ -14,9 +14,11 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * parseSuframa("12.3456.789"); // "123456789" * ``` * - * @see Official: https://portal.fazenda.sp.gov.br/servicos/nfce/Downloads/Manual_de_Orientacao_Contribuinte_v_6.pdf - * Manual de Orientação do Contribuinte da NF-e 6.0, Anexo XII.01 and field E18 (`ISUF`, numeric, - * 8 or 9 positions). + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf + * Manual de Orientação do Contribuinte (MOC) NF-e 7.0, Visão Geral, section 8.4, which gives the + * composition as `SS.NNNN.LLD`. + * @see Official: https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-anexo-i-leiaute-e-rv.pdf + * MOC 7.0, Anexo I: field 79 (`E18`, `ISUF`) is numeric with 8 to 9 positions. */ export const parseSuframa = (value: string | number): string => isNullish(value) ? "" : sanitizeToDigits(value).slice(0, SUFRAMA_LENGTH);