"""Deterministic GTIN normalization and validation (GTIN-8, UPC/GTIN-12, EAN/GTIN-13, GTIN-14).""" import re VALID, INVALID, MISSING, RESTRICTED = "VALID_GTIN", "INVALID_GTIN", "MISSING_GTIN", "RESTRICTED_GTIN" def check_digit_ok(digits: str) -> bool: body, check = digits[:-1], int(digits[-1]) total = sum(int(d) * (3 if i % 2 == 0 else 1) for i, d in enumerate(reversed(body))) return (10 - total % 10) % 10 == check def normalize_gtin(raw) -> dict: """Returns {status, gtin (digits as typed), key (zero-padded to 14: the KB identity), reason}. Never calls anything external. RESTRICTED: EAN prefixes 02, 04 and 20-29 are store-internal / restricted circulation (not a global product identity), so no provider is asked.""" text = "" if raw is None else str(raw).strip() if not text or text.upper() in ("SEM GTIN", "SEM EAN", "0", "NULL"): return {"status": MISSING, "gtin": None, "key": None, "reason": "EMPTY"} digits = re.sub(r"\D", "", text) if not digits or len(digits) not in (8, 12, 13, 14): return {"status": INVALID, "gtin": digits or None, "key": None, "reason": "BAD_LENGTH"} if len(set(digits)) == 1: return {"status": INVALID, "gtin": digits, "key": None, "reason": "REPEATED_DIGITS"} if not check_digit_ok(digits): return {"status": INVALID, "gtin": digits, "key": None, "reason": "BAD_CHECK_DIGIT"} key = digits.zfill(14) prefix = key[1:3] if len(digits) == 14 else key[1:3] # key = 0 + EAN-13 layout for 12/13; GTIN-14 keeps its indicator digit in key[0] ean13 = key[1:] if ean13[:2] in ("02", "04") or "20" <= ean13[:2] <= "29": return {"status": RESTRICTED, "gtin": digits, "key": key, "reason": "STORE_INTERNAL_PREFIX"} return {"status": VALID, "gtin": digits, "key": key, "reason": None}