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
65 changes: 65 additions & 0 deletions docs/pt-br/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -2755,6 +2755,71 @@ isValidCsosn(-101); // false (não é um inteiro seguro não negativo)

Fonte: [Anexo III-A consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70) e [Ajuste SINIEF 03/2010](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2010/aj_003_10).

## GTIN (código de barras de produto)

### isValidGtin

Verifica se um GTIN (Global Trade Item Number, o número sob um código de barras EAN/UPC) é válido.

- Cobre as quatro estruturas das GS1 General Specifications, as mesmas quatro que a NF-e aceita em `cEAN` e `cEANTrib`: GTIN-8, GTIN-12 (UPC), GTIN-13 (EAN) e GTIN-14 (DUN-14).
- **Opções** (`IsValidGtinOptions`): `lengths` aceita só alguns dos quatro tamanhos. O padrão são os quatro.
- O valor deve ser uma string de 8, 12, 13 ou 14 dígitos, fora os espaços em volta, cujo último dígito é o dígito verificador módulo 10 da GS1: pesos 3 e 1 alternados a partir da direita, e a soma subtraída do múltiplo de dez igual ou imediatamente superior. É o que as regras I03-10 e I12-10 da Nota Técnica 2021.003 da SEFAZ verificam (rejeições 611 e 612).
- Zeros à esquerda contam, então um número nunca é aceito, e um valor com máscara (`'7 890000 000017'`) é rejeitado em vez de ter seus dígitos extraídos.
- O literal `'SEM GTIN'`, que a NF-e usa para produto sem GTIN, não é um GTIN e portanto não é válido aqui: teste por ele antes de chamar.
- O prefixo não muda o veredito. Os Números de Circulação Restrita (prefixos 02, 04 e 20 a 29, os códigos que a loja imprime nas etiquetas da própria balança) e as faixas de ISSN, ISBN e cupons têm a mesma estrutura e o mesmo dígito verificador, e a "Tabela Prefixo GS1", contra a qual a SEFAZ valida o `cEAN`, lista essas faixas como válidas; use `getGtinInfo` para distingui-las.
- O prefixo também não é conferido contra a lista de Organizações Membro da GS1: a GS1 segue atribuindo faixas, e uma cópia dessa lista passaria a recusar números válidos conforme envelhecesse. Se o número está cadastrado (a consulta ao Cadastro Centralizado de GTIN que a SEFAZ faz para os prefixos 789 e 790) não dá para verificar offline.

```javascript
import { isValidGtin } from '@brazilian-utils/brazilian-utils';

isValidGtin('7890000000017'); // true (GTIN-13, prefixo da GS1 Brasil)
isValidGtin('6291041500213'); // true (o exemplo da página de dígito verificador da GS1)
isValidGtin('78912342'); // true (GTIN-8)
isValidGtin('061414112345'); // true (GTIN-12)
isValidGtin('17890000000014'); // true (GTIN-14)
isValidGtin('7890000000018'); // false (dígito verificador errado)
isValidGtin('17890000000014', { lengths: [8, 12, 13] }); // false (GTIN-14 não aceito)
isValidGtin('7 890000 000017'); // false (somente dígitos)
isValidGtin('SEM GTIN'); // false
```

### getGtinInfo

Extrai os campos de um GTIN, como um `GtinInfo`.

- Retorna `null` quando o valor não é um GTIN válido, sob as mesmas regras de `isValidGtin`.
- O prefixo é lido como a "Tabela Prefixo GS1" do Portal da NF-e orienta: o valor é preenchido com zeros à esquerda até 14 dígitos, e o prefixo são as posições 7 a 9 quando as seis primeiras são zeros (um GTIN-8) e as posições 2 a 4 caso contrário. Um GTIN-12 tem, portanto, um prefixo que começa com `0`, e um GTIN-14 tem o prefixo do GTIN-13 que ele agrupa, depois do dígito indicador.

| Campo | Descrição |
| --- | --- |
| `type` | `'GTIN-8'`, `'GTIN-12'`, `'GTIN-13'` ou `'GTIN-14'` (`GtinType`), conforme o tamanho com que o valor foi escrito |
| `length` | `8`, `12`, `13` ou `14` (`GtinLength`) |
| `prefix` | O Prefixo GS1 de três dígitos, ou um Prefixo GS1-8 quando os seis primeiros dígitos da forma de 14 dígitos são zeros, o que cobre todo GTIN-8 e o Prefixo GS1 `0000000`. Identifica a Organização Membro da GS1 que licenciou o número, não o país de origem |
| `isBrazilian` | `true` quando o prefixo é um dos da GS1 Brasil, `789` ou `790`, o que a NT 2021.003 chama de "prefixo do Brasil" |
| `isRestrictedCirculation` | `true` quando o prefixo está em uma faixa que a GS1 reserva para Números de Circulação Restrita (Prefixos GS1 02, 04 e 20 a 29; Prefixos GS1-8 000 a 099 e 200 a 299, faixa em que também cai o Prefixo GS1 `0000000`, já que sua forma de 14 dígitos começa com seis zeros), ou seja, o número só é único dentro de uma empresa ou região |
| `checkDigit` | O dígito verificador módulo 10, o último dígito |

```javascript
import { getGtinInfo } from '@brazilian-utils/brazilian-utils';

getGtinInfo('7890000000017');
// { type: 'GTIN-13', length: 13, prefix: '789', isBrazilian: true,
// isRestrictedCirculation: false, checkDigit: 7 }

getGtinInfo('17890000000014');
// { type: 'GTIN-14', length: 14, prefix: '789', isBrazilian: true,
// isRestrictedCirculation: false, checkDigit: 4 }

getGtinInfo('061414112345');
// { type: 'GTIN-12', length: 12, prefix: '006', isBrazilian: false,
// isRestrictedCirculation: false, checkDigit: 5 }

getGtinInfo('2000000000015')?.isRestrictedCirculation; // true (número interno da loja)
getGtinInfo('7890000000018'); // null (dígito verificador errado)
```

Fonte: [GS1 General Specifications](https://ref.gs1.org/standards/genspecs/), [calculadora de dígito verificador da GS1](https://www.gs1.org/services/how-calculate-check-digit-manually), [Nota Técnica 2021.003 da SEFAZ](https://www.nfe.fazenda.gov.br/portal/exibirArquivo.aspx?conteudo=SrQT9ys8ODo%3D) e [Tabela Prefixo GS1](https://www.nfe.fazenda.gov.br/portal/exibirArquivo.aspx?conteudo=Oc+fygAxwmc%3D) do Portal da NF-e.

## Texto

### capitalize
Expand Down
65 changes: 65 additions & 0 deletions docs/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -2755,6 +2755,71 @@ isValidCsosn(-101); // false (not a non-negative safe integer)

Source: [consolidated Anexo III-A of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70) and [Ajuste SINIEF 03/2010](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2010/aj_003_10).

## GTIN (product barcode)

### isValidGtin

Check if a GTIN (Global Trade Item Number, the number under an EAN/UPC barcode) is valid.

- Covers the four structures of the GS1 General Specifications, the same four the NF-e accepts in `cEAN` and `cEANTrib`: GTIN-8, GTIN-12 (UPC), GTIN-13 (EAN) and GTIN-14 (DUN-14).
- **Options** (`IsValidGtinOptions`): `lengths` accepts only some of the four lengths, and defaults to all four.
- The value must be a string of 8, 12, 13 or 14 digits, surrounding whitespace aside, whose last digit is the GS1 modulo 10 check digit: weights 3 and 1 alternating from the right, the sum subtracted from the nearest equal or higher multiple of ten. That is what rules I03-10 and I12-10 of SEFAZ Nota Técnica 2021.003 check (rejections 611 and 612).
- Leading zeros count, so a number is never accepted, and a masked value (`'7 890000 000017'`) is rejected instead of having its digits picked out.
- The `'SEM GTIN'` literal the NF-e uses for a product without a GTIN is not a GTIN, so it is not valid here: test for it before calling.
- The prefix does not change the verdict. Restricted Circulation Numbers (prefixes 02, 04 and 20 to 29, the codes a shop prints on its own scale labels) and the ISSN, ISBN and coupon ranges share the structure and the check digit, and the "Tabela Prefixo GS1" SEFAZ validates `cEAN` against lists them as valid; use `getGtinInfo` to tell them apart.
- The prefix is not checked against the list of GS1 Member Organisations either: GS1 keeps assigning ranges, so a copy of that list would turn down valid numbers as it ages. Whether the number is registered (the Cadastro Centralizado de GTIN lookup SEFAZ runs for the 789 and 790 prefixes) cannot be checked offline.

```javascript
import { isValidGtin } from '@brazilian-utils/brazilian-utils';

isValidGtin('7890000000017'); // true (GTIN-13, GS1 Brasil prefix)
isValidGtin('6291041500213'); // true (the example of the GS1 check digit page)
isValidGtin('78912342'); // true (GTIN-8)
isValidGtin('061414112345'); // true (GTIN-12)
isValidGtin('17890000000014'); // true (GTIN-14)
isValidGtin('7890000000018'); // false (wrong check digit)
isValidGtin('17890000000014', { lengths: [8, 12, 13] }); // false (GTIN-14 not accepted)
isValidGtin('7 890000 000017'); // false (digits only)
isValidGtin('SEM GTIN'); // false
```

### getGtinInfo

Parse a GTIN into its fields, as a `GtinInfo`.

- Returns `null` when the value is not a valid GTIN, under the same rules as `isValidGtin`.
- The prefix is read the way the "Tabela Prefixo GS1" of the Portal da NF-e tells: the value is left padded with zeros to 14 digits, and the prefix is positions 7 to 9 when the first six are zeros (a GTIN-8) and positions 2 to 4 otherwise. A GTIN-12 therefore has a prefix that starts with `0`, and a GTIN-14 has the prefix of the GTIN-13 it packs, after the indicator digit.

| Field | Description |
| --- | --- |
| `type` | `'GTIN-8'`, `'GTIN-12'`, `'GTIN-13'` or `'GTIN-14'` (`GtinType`), from the length the value was written with |
| `length` | `8`, `12`, `13` or `14` (`GtinLength`) |
| `prefix` | The three digit GS1 Prefix, or a GS1-8 Prefix when the first six digits of the 14 digit form are zeros, which covers every GTIN-8 and the GS1 Prefix `0000000`. It names the GS1 Member Organisation that licensed the number, not the country of origin |
| `isBrazilian` | `true` when the prefix is one of GS1 Brasil, `789` or `790`, what NT 2021.003 calls "prefixo do Brasil" |
| `isRestrictedCirculation` | `true` when the prefix is in a range GS1 sets aside for Restricted Circulation Numbers (GS1 Prefixes 02, 04 and 20 to 29; GS1-8 Prefixes 000 to 099 and 200 to 299, which is also where the GS1 Prefix `0000000` lands, since its 14 digit form starts with six zeros), so the number is only unique inside a company or region |
| `checkDigit` | The modulo 10 check digit, the last digit |

```javascript
import { getGtinInfo } from '@brazilian-utils/brazilian-utils';

getGtinInfo('7890000000017');
// { type: 'GTIN-13', length: 13, prefix: '789', isBrazilian: true,
// isRestrictedCirculation: false, checkDigit: 7 }

getGtinInfo('17890000000014');
// { type: 'GTIN-14', length: 14, prefix: '789', isBrazilian: true,
// isRestrictedCirculation: false, checkDigit: 4 }

getGtinInfo('061414112345');
// { type: 'GTIN-12', length: 12, prefix: '006', isBrazilian: false,
// isRestrictedCirculation: false, checkDigit: 5 }

getGtinInfo('2000000000015')?.isRestrictedCirculation; // true (in-store number)
getGtinInfo('7890000000018'); // null (wrong check digit)
```

Source: [GS1 General Specifications](https://ref.gs1.org/standards/genspecs/), [GS1 check digit calculator](https://www.gs1.org/services/how-calculate-check-digit-manually), [SEFAZ Nota Técnica 2021.003](https://www.nfe.fazenda.gov.br/portal/exibirArquivo.aspx?conteudo=SrQT9ys8ODo%3D) and the [Tabela Prefixo GS1](https://www.nfe.fazenda.gov.br/portal/exibirArquivo.aspx?conteudo=Oc+fygAxwmc%3D) of the Portal da NF-e.

## Text

### capitalize
Expand Down
2 changes: 2 additions & 0 deletions jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"./get-cnpj-info": "./src/get-cnpj-info/get-cnpj-info.ts",
"./get-cpf-info": "./src/get-cpf-info/get-cpf-info.ts",
"./get-format-license-plate": "./src/get-format-license-plate/get-format-license-plate.ts",
"./get-gtin-info": "./src/get-gtin-info/get-gtin-info.ts",
"./get-holidays": "./src/get-holidays/get-holidays.ts",
"./get-iban-info": "./src/get-iban-info/get-iban-info.ts",
"./get-legal-nature": "./src/get-legal-nature/get-legal-nature.ts",
Expand Down Expand Up @@ -102,6 +103,7 @@
"./is-valid-csosn": "./src/is-valid-csosn/is-valid-csosn.ts",
"./is-valid-cst": "./src/is-valid-cst/is-valid-cst.ts",
"./is-valid-email": "./src/is-valid-email/is-valid-email.ts",
"./is-valid-gtin": "./src/is-valid-gtin/is-valid-gtin.ts",
"./is-valid-iban": "./src/is-valid-iban/is-valid-iban.ts",
"./is-valid-ie": "./src/is-valid-ie/is-valid-ie.ts",
"./is-valid-landline-phone": "./src/is-valid-landline-phone/is-valid-landline-phone.ts",
Expand Down
27 changes: 27 additions & 0 deletions reports/api/brazilian-utils.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,9 @@ export const getCpfInfo: (value: string) => CpfInfo | null;
// @public
export const getFormatLicensePlate: (value: string) => LicensePlateFormat | null;

// @public
export const getGtinInfo: (value: string) => GtinInfo | null;

// @public
export function getHolidays(year: number): Holiday[];

Expand Down Expand Up @@ -629,6 +632,22 @@ export const getStates: () => State[];
// @public
export const getTimezoneByState: (stateCode: string) => string | null;

// @public
export type GtinInfo = {
type: GtinType;
length: GtinLength;
prefix: string;
isBrazilian: boolean;
isRestrictedCirculation: boolean;
checkDigit: number;
};

// @public
export type GtinLength = 8 | 12 | 13 | 14;

// @public
export type GtinType = "GTIN-8" | "GTIN-12" | "GTIN-13" | "GTIN-14";

// @public
export type Holiday = {
name: string;
Expand Down Expand Up @@ -754,6 +773,14 @@ export type IsValidCstOptions = {
// @public
export const isValidEmail: (value: string) => boolean;

// @public
export const isValidGtin: (value: string, options?: IsValidGtinOptions) => boolean;

// @public
export type IsValidGtinOptions = {
lengths?: GtinLength[];
};

// @public
export const isValidIban: (value: string) => boolean;

Expand Down
26 changes: 26 additions & 0 deletions src/_internals/mod10/mod10.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,30 @@ describe("mod10", () => {
test("should return 0 when mod is 0", () => {
expect(mod10("000000000")).toBe(0);
});

describe("with the gs1 variant", () => {
test("should calculate the 18 digit example of the GS1 General Specifications, table 7-9", () => {
expect(mod10("37610425002123456", { variant: "gs1" })).toBe(9);
});

test("should calculate the check digit of the GTIN examples of the GS1 General Specifications", () => {
expect(mod10("952123450001", { variant: "gs1" })).toBe(8);
expect(mod10("0952414123456", { variant: "gs1" })).toBe(4);
expect(mod10("06141411234", { variant: "gs1" })).toBe(5);
});

test("should return 0 when the sum is a multiple of ten", () => {
expect(mod10("0000000", { variant: "gs1" })).toBe(0);
expect(mod10("1234567", { variant: "gs1" })).toBe(0);
});

test("should differ from the luhn variant for the same digits", () => {
expect(mod10("952123450001")).toBe(1);
expect(mod10("952123450001", { variant: "gs1" })).toBe(8);
});
});

test("should apply the luhn variant when it is named", () => {
expect(mod10("001900000", { variant: "luhn" })).toBe(9);
});
});
23 changes: 20 additions & 3 deletions src/_internals/mod10/mod10.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
type Mod10Variant = "luhn" | "gs1";

export type Mod10Options = {
/** Which modulo 10 rule to apply (default: `"luhn"`). */
variant?: Mod10Variant;
};

/**
* Calculates the modulus 10 check digit for a given string.
*
* Both variants weigh the digits from the right, alternating with 1. `"luhn"` uses the weight 2
* and adds the digits of each product (the boleto, credit card and bank account rule). `"gs1"`
* uses the weight 3 and adds the products as they are (the rule of the GS1 General
* Specifications, section 7.9.1, for GTIN and the other fixed length GS1 keys).
*
* @param {string} str - The string to calculate the check digit for.
* @param {Mod10Options} [options] - Optional options.
* @param {Mod10Variant} [options.variant] - The weighting rule to apply. Defaults to `"luhn"`.
* @returns {number} The calculated check digit (0-9).
*
* @example
* ```typescript
* mod10("001900000"); // 9
* mod10("37610425002123456", { variant: "gs1" }); // 9
* ```
*/
export const mod10 = (str: string): number => {
export const mod10 = (str: string, options?: Mod10Options): number => {
const isGs1 = options?.variant === "gs1";
const weight = isGs1 ? 3 : 2;
let sum = 0;
const len = str.length;
for (let i = 0; i < len; i++) {
const digit = str.charCodeAt(len - 1 - i) - 48;
const result = digit * (i % 2 === 0 ? 2 : 1);
sum += result > 9 ? result - 9 : result;
const result = digit * (i % 2 === 0 ? weight : 1);
sum += !isGs1 && result > 9 ? result - 9 : result;
}
const mod = sum % 10;
return mod > 0 ? 10 - mod : 0;
Expand Down
46 changes: 46 additions & 0 deletions src/get-gtin-info/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { type GtinLength, type GtinType } from "./get-gtin-info";

/** A GTIN is written with digits only, so anything else is turned down before it is measured. */
export const DIGITS_REGEX = /^\d+$/;

/**
* The four GTIN structures of the GS1 General Specifications, the same four the NF-e leiaute
* accepts in `cEAN` and `cEANTrib` (NT 2021.003, chapter 3, fields I03 and I12: "GTIN-8, GTIN-12,
* GTIN-13 ou GTIN-14").
*/
export const GTIN_LENGTHS: readonly GtinLength[] = [8, 12, 13, 14];

/** The name GS1 gives to the structure of each length. */
export const GTIN_TYPES: Record<GtinLength, GtinType> = {
8: "GTIN-8",
12: "GTIN-12",
13: "GTIN-13",
14: "GTIN-14",
};

/** Every GTIN is compared in its 14 digit form, left padded with zeros. */
export const NORMALIZED_LENGTH = 14;

/**
* The six zeros a GTIN-8 gets in the 14 digit form. GS1 leaves the prefixes 0000001 to 0000099
* unused so that no longer GTIN collides with one (General Specifications, table 1-4), and the
* "Tabela Prefixo GS1" of the Portal da NF-e reads the prefix the same way: from positions 7 to 9
* when the first six are zeros, from positions 2 to 4 otherwise.
*/
export const GS1_8_PADDING = "000000";

/** The GS1 Prefixes of GS1 Brasil, the ones NT 2021.003 calls "prefixo do Brasil". */
export const BRAZILIAN_PREFIXES: readonly string[] = ["789", "790"];

/**
* The GS1 Prefix ranges table 1-4 of the General Specifications sets aside for Restricted
* Circulation Numbers: 02 and 20 to 29 (within a geographic region) and 04 (within a company).
* The range 0000000 falls under the GS1-8 reading below.
*/
export const RESTRICTED_PREFIX_REGEX = /^(?:02|04|2)/;

/**
* The GS1-8 Prefix ranges table 1-5 of the General Specifications sets aside for Restricted
* Circulation Numbers within a company: 000 to 099 and 200 to 299.
*/
export const RESTRICTED_GS1_8_PREFIX_REGEX = /^[02]/;
Loading
Loading