"""Composition TRACABLE du numero e-NCF (anti-invention · CLAUDE.md #6). Meme doctrine que `commlib/finance.py` / `banclib/finance.py` : une composition transparente, reproductible et entierement sourcee n'est PAS une invention — c'est de la modelisation auditable. Le e-NCF (Numero de Comprobante Fiscal electronique) suit le format DGII : e-NCF = "E" + tipoeCF (2 chiffres) + secuencia (10 chiffres) → 13 caracteres La `secuencia` provient d'un rango autorise par la DGII (jamais fabriquee ici). `compose_encf` reste donc `None` tant qu'un operande manque (placeholder, jamais 0-invente), et publie toujours sa FORMULE. Aucun tipo ni sequence n'est jamais fabrique par defaut : le caller fournit des operandes reels (cote VPS/Compupar). """ from __future__ import annotations import re from typing import Any, Optional from . import deps is_filled = deps.is_filled # Format e-NCF DGII (Ley 32-23) : E + tipo(2) + secuencia(10) = 13 caracteres. ENCF_RE = re.compile(r"^E([0-9]{2})([0-9]{10})$") FORMULE = "E + tipoeCF(2) + secuencia(10)" def is_valid_encf(value: Any) -> bool: """True si `value` respecte exactement le format e-NCF DGII (13 car.).""" return bool(is_filled(value) and ENCF_RE.match(str(value).strip())) def parse_encf(value: Any) -> Optional[dict]: """Decompose un e-NCF valide → {prefix, tipo, secuencia}. None sinon.""" if not is_filled(value): return None m = ENCF_RE.match(str(value).strip()) if not m: return None return {"prefix": "E", "tipo": m.group(1), "secuencia": m.group(2)} def _tipo(value: Any) -> Optional[str]: """Un code de type sur 2 chiffres, ou None si absent/mal forme (jamais fabrique).""" if not is_filled(value): return None txt = str(value).strip() return txt if re.fullmatch(r"[0-9]{2}", txt) else None def _secuencia(value: Any) -> Optional[str]: """Une secuencia numerique → 10 chiffres zero-paddes, ou None si absente/invalide. Ne fabrique jamais de sequence : `None` en entree reste `None` en sortie. """ if not is_filled(value): return None txt = str(value).strip() if not re.fullmatch(r"[0-9]{1,10}", txt): return None return txt.zfill(10) def compose_encf(tipo: Any, secuencia: Any) -> dict: """Compose un e-NCF tracable a partir d'operandes reels. Retourne un dict publiant la formule, la valeur composee (None si un operande manque — jamais fabriquee), et les champs manquants. Aucun defaut n'est invente : `tipo`/`secuencia` absents ⇒ `e_ncf: None`, `incomplete: True`. """ t = _tipo(tipo) s = _secuencia(secuencia) manquants: list[str] = [] if t is None: manquants.append("tipo") if s is None: manquants.append("secuencia") e_ncf = f"E{t}{s}" if (t is not None and s is not None) else None return { "e_ncf": e_ncf, "tipo": t, "secuencia": s, "formule": FORMULE, "incomplete": bool(manquants), "champs_manquants": manquants, }