Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions docs/pt-br/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,67 @@ 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.
- 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
```

### 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 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

### isValidPhone
Expand Down
61 changes: 61 additions & 0 deletions docs/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,67 @@ 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.
- 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
```

### 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 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

### isValidPhone
Expand Down
4 changes: 4 additions & 0 deletions jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions reports/api/brazilian-utils.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down
2 changes: 2 additions & 0 deletions src/_internals/constants/suframa.ts
Original file line number Diff line number Diff line change
@@ -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;
105 changes: 105 additions & 0 deletions src/format-suframa/format-suframa.test.ts
Original file line number Diff line number Diff line change
@@ -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<string | number>();
expectTypeOf(formatSuframa).parameter(1).toEqualTypeOf<FormatSuframaOptions | undefined>();
expectTypeOf(formatSuframa).returns.toEqualTypeOf<string>();
});

test("should type the pad option as an optional boolean", () => {
expectTypeOf<FormatSuframaOptions["pad"]>().toEqualTypeOf<boolean | undefined>();
});
});
43 changes: 43 additions & 0 deletions src/format-suframa/format-suframa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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. 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.
* @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"); // "10.0010.18" (8 digits, the mask groups one position early)
* formatSuframa("10001018", { pad: true }); // "01.0001.018"
* ```
*
* @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)
? ""
: format({
pad: options?.pad,
value: sanitizeToDigits(value),
pattern: "00.0000.000",
});
Loading
Loading