219 lines
12 KiB
Python
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}}
|