Files
GTI_GERENCIADOR_FISCAL/agent/src/gti_fiscal_agent/company_context.py
T

219 lines
12 KiB
Python

"""Deterministic, auditable company-context resolution for an Agent installation (READ-ONLY).
Evidence (proved on 4 SGV databases, see docs/COMPANY_CONTEXT_RESOLUTION_V1.json):
* EMISSOR.EMITENTE_CNPJ/UF/CRT is the emitter block read by the emission programs (GtiVendas_NFCe.exe and Envio NF-e.exe both bind
QrEmissor.EMITENTE_*; neither reads C000004.CRT/OPTANTE_SIMPLES). 100% of emitted NFC-e access keys carry the CNPJ of the C000004 row
whose CNPJ equals EMISSOR.EMITENTE_CNPJ, and cUF=29.
* C000004.CRT / OPTANTE_SIMPLES belong to the ERP filial registry (SPED, filial screen) and can disagree with EMISSOR.
Never guesses: first row / lowest code / latest row are NOT used. Ambiguity -> FAIL with the reason, unless the operator configures
COMPANY_CODE or COMPANY_CNPJ (company) and COMPANY_CRT_SOURCE = EMISSOR | C000004 (explicit choice when the two CRT sources conflict).
"""
import hashlib
import os
IBGE_UF = {"11": "RO", "12": "AC", "13": "AM", "14": "RR", "15": "PA", "16": "AP", "17": "TO", "21": "MA", "22": "PI", "23": "CE", "24": "RN", "25": "PB", "26": "PE",
"27": "AL", "28": "SE", "29": "BA", "31": "MG", "32": "ES", "33": "RJ", "35": "SP", "41": "PR", "42": "SC", "43": "RS", "50": "MS", "51": "MT", "52": "GO",
"53": "DF"}
VALID_CRT = {"1", "2", "3"}
class CompanyContextError(Exception):
def __init__(self, result):
super().__init__(f"COMPANY_CONTEXT=FAIL ({result.get('classification')}): {'; '.join(result.get('reasons', []))}")
self.result = result
def digits(value):
return "".join(c for c in str(value or "") if c.isdigit())
def mask_cnpj(value):
d = digits(value)
return f"{d[:2]}**********{d[-2:]}" if len(d) >= 4 else None
def cnpj_fingerprint(value):
d = digits(value)
return hashlib.sha256(d.encode()).hexdigest()[:8] if d else None
def _s(v):
return None if v is None else str(v).strip()
def config_from_env(env):
crt_source = (env.get("COMPANY_CRT_SOURCE") or "").strip().upper() or None
if crt_source == "AUTO": # AUTO = resolve automatically, only when there is no conflict
crt_source = None
invalid = False
else:
invalid = bool(crt_source and crt_source not in ("EMISSOR", "C000004"))
uf = (env.get("COMPANY_UF") or "").strip().upper() or None
return {"company_code": (env.get("COMPANY_CODE") or "").strip() or None, "company_cnpj": digits(env.get("COMPANY_CNPJ")) or None,
"crt_source": crt_source if crt_source in ("EMISSOR", "C000004") else None, "crt_source_invalid": invalid,
"uf_override": uf}
def resolve(evidence, config=None):
"""evidence: {"c000004": [{code, cnpj, ie, uf, crt, optante, name}], "emissor": [{cnpj, uf, crt}],
"nfce_keys": [{"cuf", "cnpj", "n"}], "nf_by_filial": {code: n}}. Pure function."""
cfg = config or {}
out = {"status": "FAIL", "classification": "UNRESOLVED", "company_code": None, "company_cnpj": None, "company_cnpj_fingerprint": None, "company_uf": None, "company_crt": None,
"source_of_truth": {}, "confidence": "NONE", "evidence": [], "warnings": [], "reasons": [], "configured": False}
def fail(cls, reason):
out["classification"] = cls; out["reasons"].append(reason); return out
if cfg.get("crt_source_invalid"):
return fail("CONFIG_REQUIRED", "COMPANY_CRT_SOURCE must be EMISSOR or C000004")
companies = [{**c, "cnpj": digits(c.get("cnpj"))} for c in evidence.get("c000004", [])]
emissors = [{**e, "cnpj": digits(e.get("cnpj"))} for e in evidence.get("emissor", [])]
docs = evidence.get("nfce_keys", [])
if not companies:
return fail("UNRESOLVED", "C000004 has no company rows")
# ---- company selection -------------------------------------------------------------------
selected = None; basis = None
by_code, by_cnpj = cfg.get("company_code"), cfg.get("company_cnpj")
if by_code or by_cnpj:
out["configured"] = True
cand = [c for c in companies if (not by_code or c["code"] == by_code) and (not by_cnpj or c["cnpj"] == by_cnpj)]
if len(cand) != 1:
return fail("CONFIG_REQUIRED", "configured company (COMPANY_CODE/COMPANY_CNPJ) does not identify exactly one C000004 row")
selected, basis = cand[0], "CONFIGURED"
elif len(companies) == 1:
selected, basis = companies[0], "SINGLE_C000004_ROW"
elif len(emissors) == 1 and emissors[0]["cnpj"]:
match = [c for c in companies if c["cnpj"] == emissors[0]["cnpj"]]
if len(match) != 1:
return fail("CONFIG_REQUIRED", f"{len(companies)} C000004 rows and the EMISSOR CNPJ matches {len(match)} of them: configure COMPANY_CODE or COMPANY_CNPJ")
selected, basis = match[0], "EMISSOR_CNPJ_MATCH"
else:
return fail("CONFIG_REQUIRED", f"{len(companies)} C000004 rows and no unique EMISSOR row to disambiguate: configure COMPANY_CODE or COMPANY_CNPJ")
out["evidence"].append(f"company selected by {basis}")
out.update(company_code=selected["code"], company_cnpj=mask_cnpj(selected["cnpj"]), company_cnpj_fingerprint=cnpj_fingerprint(selected["cnpj"]))
out["source_of_truth"]["company"] = basis
# ---- emitter (EMISSOR) must describe the same company --------------------------------------
emissor = None
if emissors:
same = [e for e in emissors if e["cnpj"] and e["cnpj"] == selected["cnpj"]]
if same:
emissor = same[0]; out["evidence"].append("EMISSOR.EMITENTE_CNPJ equals the selected company CNPJ")
else:
return fail("CONFLICT", "EMISSOR emitter CNPJ differs from the selected company (unexplained): refusing to guess")
else:
out["warnings"].append("NO_EMISSOR_ROW")
# ---- emitted documents must belong to the same CNPJ ------------------------------------------
doc_ufs = set()
if docs:
total = sum(d["n"] for d in docs); own = sum(d["n"] for d in docs if digits(d["cnpj"]) == selected["cnpj"])
out["evidence"].append(f"emitted NFC-e keys: {own}/{total} carry the selected CNPJ")
if own == 0:
return fail("CONFLICT", f"none of the {total} emitted NFC-e keys carries the selected company CNPJ")
if own != total: # e.g. a database copy/migration that kept another emitter's history: evidence, not a reason to guess
out["warnings"].append(f"HISTORICAL_NFCE_KEYS_FROM_OTHER_CNPJ {total - own}/{total}")
doc_ufs = {IBGE_UF.get(d["cuf"]) for d in docs if digits(d["cnpj"]) == selected["cnpj"]} - {None}
# ---- UF ------------------------------------------------------------------------------------------
uf_sources = {}
if emissor and _s(emissor.get("uf")): uf_sources["EMISSOR"] = _s(emissor["uf"]).upper()
if _s(selected.get("uf")): uf_sources["C000004"] = _s(selected["uf"]).upper()
if len(doc_ufs) == 1: uf_sources["DOCUMENT_KEYS"] = next(iter(doc_ufs))
if len(doc_ufs) > 1: return fail("CONFLICT", "emitted NFC-e keys carry several UFs")
values = set(uf_sources.values())
override = cfg.get("uf_override")
if len(values) > 1:
return fail("CONFLICT", f"UF_SOURCE_CONFLICT {uf_sources}")
if override and values and override not in values:
return fail("CONFLICT", f"COMPANY_UF={override} differs from the UF proved by the database {sorted(values)}: an override never replaces the resolver silently")
if not values and override: # explicit override only when the database proves nothing
uf_sources["CONFIG_OVERRIDE"] = override; values = {override}; out["warnings"].append("UF_FROM_EXPLICIT_OVERRIDE")
if not values:
return fail("UNRESOLVED", "UF_SOURCE_UNKNOWN")
out["company_uf"] = next(iter(values)); out["source_of_truth"]["uf"] = "+".join(uf_sources)
out["uf_status"] = "UF_SOURCE_CONFIRMED"
# ---- CRT ------------------------------------------------------------------------------------------
e_crt = _s(emissor.get("crt")) if emissor else None
c_crt = _s(selected.get("crt"))
e_ok, c_ok = e_crt in VALID_CRT, c_crt in VALID_CRT
if e_ok and c_ok and e_crt == c_crt:
crt, src, status = e_crt, "EMISSOR+C000004", "CRT_SOURCE_CONFIRMED"
elif e_ok and c_ok:
choice = cfg.get("crt_source")
if not choice:
out["crt_candidates"] = {"EMISSOR": e_crt, "C000004": c_crt}
return fail("CONFLICT", f"CRT_SOURCE_CONFLICT EMISSOR={e_crt} C000004={c_crt}: configure COMPANY_CRT_SOURCE (emission programs read EMISSOR)")
crt, src, status = (e_crt if choice == "EMISSOR" else c_crt), f"{choice} (configured; conflict acknowledged)", "CRT_SOURCE_CONFLICT_RESOLVED_BY_CONFIG"
out["warnings"].append(f"CRT_CONFLICT EMISSOR={e_crt} C000004={c_crt}")
elif e_ok or c_ok:
crt, src, status = (e_crt, "EMISSOR", "CRT_SOURCE_SINGLE") if e_ok else (c_crt, "C000004", "CRT_SOURCE_SINGLE")
out["warnings"].append("CRT_HAS_A_SINGLE_SOURCE")
else:
return fail("UNRESOLVED", "CRT_SOURCE_UNKNOWN (no valid CRT in EMISSOR or C000004)")
out.update(company_crt=crt, crt_status=status); out["source_of_truth"]["crt"] = src
opt = _s(selected.get("optante"))
if opt and ((crt in ("1", "2") and opt.upper() != "S") or (crt == "3" and opt.upper() == "S")):
out["warnings"].append(f"OPTANTE_SIMPLES={opt} inconsistent with CRT={crt}")
agree = len(uf_sources) + (2 if status == "CRT_SOURCE_CONFIRMED" else 1)
out["confidence"] = "HIGH" if (status == "CRT_SOURCE_CONFIRMED" and len(uf_sources) >= 2) else "MEDIUM"
out["status"] = "PASS"
out["classification"] = "CONFIGURED" if out["configured"] or status == "CRT_SOURCE_CONFLICT_RESOLVED_BY_CONFIG" else "AUTO_RESOLVED"
out["reasons"] = []
out["evidence_source_count"] = agree
return out
def collect_evidence(cursor):
"""Read-only SELECTs. Missing optional tables/columns simply yield less evidence."""
cursor.execute("SELECT TRIM(RDB$RELATION_NAME), TRIM(RDB$FIELD_NAME) FROM RDB$RELATION_FIELDS WHERE RDB$RELATION_NAME IN ('C000004','EMISSOR','NFCE','C000061')")
cols = {}
for t, f in cursor.fetchall():
cols.setdefault(t, set()).add(f)
ev = {"c000004": [], "emissor": [], "nfce_keys": [], "nf_by_filial": {}}
c4 = cols.get("C000004", set())
want = [("CODIGO", "code"), ("CNPJ", "cnpj"), ("IE", "ie"), ("UF", "uf"), ("CRT", "crt"), ("OPTANTE_SIMPLES", "optante"), ("FILIAL", "name")]
use = [(c, k) for c, k in want if c in c4]
if use:
cursor.execute(f"SELECT {','.join(c for c, _ in use)} FROM C000004")
for row in cursor.fetchall():
ev["c000004"].append({k: _s(v) for (_, k), v in zip(use, row)})
em = cols.get("EMISSOR", set())
want = [("EMITENTE_CNPJ", "cnpj"), ("EMITENTE_UF", "uf"), ("EMITENTE_CRT", "crt")]
use = [(c, k) for c, k in want if c in em]
if use:
cursor.execute(f"SELECT {','.join(c for c, _ in use)} FROM EMISSOR")
for row in cursor.fetchall():
ev["emissor"].append({k: _s(v) for (_, k), v in zip(use, row)})
if "CHAVE" in cols.get("NFCE", set()):
cursor.execute("SELECT SUBSTRING(CHAVE FROM 1 FOR 2), SUBSTRING(CHAVE FROM 7 FOR 14), COUNT(*) FROM NFCE WHERE CHAVE IS NOT NULL AND CHAR_LENGTH(TRIM(CHAVE)) = 44 "
"GROUP BY SUBSTRING(CHAVE FROM 1 FOR 2), SUBSTRING(CHAVE FROM 7 FOR 14)")
ev["nfce_keys"] = [{"cuf": _s(a), "cnpj": _s(b), "n": n} for a, b, n in cursor.fetchall()]
if "CODFILIAL" in cols.get("C000061", set()):
cursor.execute("SELECT CODFILIAL, COUNT(*) FROM C000061 GROUP BY CODFILIAL")
ev["nf_by_filial"] = {_s(a): n for a, n in cursor.fetchall()}
return ev
def resolve_from_cursor(cursor, env=None):
return resolve(collect_evidence(cursor), config_from_env(env or os.environ))
def preflight_company_context(env=None):
"""Opens the configured Firebird database read-only and resolves the company. Never prints the full CNPJ."""
from .firebird_reader import _connect
env = env or os.environ
conn = _connect(env)
try:
return resolve_from_cursor(conn.cursor(), env)
finally:
conn.close()
def preflight_report(result):
keys = ("company_code", "company_cnpj", "company_uf", "company_crt", "source_of_truth", "confidence", "classification", "warnings", "reasons")
return {"COMPANY_CONTEXT": "PASS" if result["status"] == "PASS" else "FAIL", **{k: result.get(k) for k in keys}}