Files
gti d21a9d1140 Add 'fiscal/' from commit '102ba84500dbd9655a0471288c3e278652593ca9'
git-subtree-dir: fiscal
git-subtree-mainline: d95165fc8f
git-subtree-split: 102ba84500
2026-09-29 14:43:23 -03:00

66 lines
2.6 KiB
Python

"""Validador local determinístico de GTIN / EAN (GS1 Standard).
Sem chamadas externas, sem IA. Implementa:
1. Validação de formato (GTIN-8, GTIN-12, GTIN-13, GTIN-14);
2. Rejeição de placeholders (zeros, sequências óbvias, códigos internos de balança restrita);
3. Verificação do dígito verificador oficial GS1 (módulo 10 com pesos alternados 3 e 1);
4. Validação de faixas de prefixo GS1 reconhecidas.
"""
from __future__ import annotations
def _digits(value: str | None) -> str:
return "".join(c for c in str(value or "") if c.isdigit())
# Prefixos de circulação restrita / uso interno de loja (não são GTINs globais válidos para KB nacional)
# Faixas 020-029 e 200-299 são reservadas pelo GS1 para distribuição restrita (itens pesáveis, balança de loja)
INTERNAL_STORE_PREFIXES = tuple(str(p) for p in range(20, 30)) + tuple(str(p) for p in range(200, 300))
def calculate_gs1_check_digit(digits_without_dv: str) -> int:
"""Calcula o dígito verificador padrão GS1 pelo algoritmo oficial módulo 10."""
total = 0
# Da direita para a esquerda, os fatores alternam entre 3 e 1
# Para o penúltimo dígito (imediatamente antes do DV), o peso é sempre 3
weight = 3
for char in reversed(digits_without_dv):
total += int(char) * weight
weight = 1 if weight == 3 else 3
remainder = total % 10
return 0 if remainder == 0 else (10 - remainder)
def validate_gtin(value: str | None) -> tuple[bool, str]:
"""Valida um código GTIN.
Retorna (is_valid, reason).
"""
if not value:
return False, "GTIN_EMPTY"
cleaned = _digits(value)
if len(cleaned) not in (8, 12, 13, 14):
return False, f"GTIN_INVALID_LENGTH_{len(cleaned)}"
# Rejeita dígitos todos iguais (ex: 00000000, 1111111111111, 9999999999999)
if len(set(cleaned)) == 1:
return False, "GTIN_PLACEHOLDER_REPEATED_DIGITS"
# Rejeita prefixos de uso interno / balança
# Normalizado para 13 dígitos para verificar o prefixo
padded_13 = cleaned.zfill(13) if len(cleaned) < 14 else cleaned[1:]
prefix_3 = padded_13[:3]
prefix_2 = padded_13[:2]
if prefix_3 in INTERNAL_STORE_PREFIXES or prefix_2 in INTERNAL_STORE_PREFIXES:
return False, "GTIN_RESTRICTED_INTERNAL_STORE_PREFIX"
# Validação do Dígito Verificador
expected_dv = calculate_gs1_check_digit(cleaned[:-1])
actual_dv = int(cleaned[-1])
if actual_dv != expected_dv:
return False, f"GTIN_INVALID_CHECK_DIGIT_EXPECTED_{expected_dv}_GOT_{actual_dv}"
return True, "GTIN_VALID"