Files
GTI_GERENCIADOR_FISCAL/central/app/context_keys.py
T
gtiandClaude Sonnet 5 e935ed61d5 feat: audited-decision gate for PIS/COFINS and CFOP_ENTRADA NOT_APPLICABLE/OPERATION_TIME
Missao 2 (fechamento de lacunas fiscais, read-only + UI/engine gated):
- field_contract: pis_cofins_profile now also accepts the literal sentinels
  NOT_APPLICABLE/OPERATION_TIME, resolving to NA only from an audited
  FiscalContextEntry - never inferred from emission statistics. CFOP_ENTRADA
  under an already-decided entry_scope=MIXED_OR_DYNAMIC now resolves to NA
  instead of MANUAL, so it stops blocking product completeness and stops
  re-asking an already-answered question. Regression tests prove: no ADMIN
  decision => unchanged old behavior; decision present => gated bucket, no write.
- context_keys: validate_entry accepts the two sentinels for pis_cofins_profile.
- Contexto Fiscal UI: PIS/COFINS and CFOP_ENTRADA cards show the Bloco 2
  cross-database evidence (5 installations, CRT 1 and 3: emission never reads
  the PIS/COFINS cadastral fields anywhere) and a link to an ADMIN-facing,
  anonymized dossie-for-the-accountant; entry_scope is now a dropdown.
- release/tests: decoupled the Hub's GTI_FISCAL_VERSION from the Agent's own
  version (test_versions_are_coherent_across_hub_and_packages vs the new
  test_agent_version_is_internally_coherent) - a Hub-only release must not
  require touching, rebuilding or republishing the Agent.
- New read-only evidence: docs/runs/GTI_dryrun_universe_summary.json (527
  products, Agent 2's real database), docs/runs/BLOCO2_PIS_COFINS_CROSS_DB_V1.json
  (5 installations), docs/fiscal_completeness_matrix.md, docs/dossie_contador.md
  (internal, real names) + central/static/docs/dossie_contador.md (public,
  anonymized - no CNPJ/razao social/product names), docs/AGENT2_UPGRADE_RUNBOOK_V1.md.
- CLAUDE.md: recorded the permanent fiscal-mission rules (no AI/scraping, no
  Firebird schema changes, deploy only via deploy_hub.ps1, never reenroll,
  no remote access to client Agent hosts, no client data in unauthenticated
  static routes).

No schema/migration change. Zero Firebird writes (new scripts never open a
write connection). Hub only - Agent code untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 09:44:05 -03:00

74 lines
4.0 KiB
Python

"""Validation of operator fiscal-context entries (facts the ERP does not know). Pure; shared by the Hub API, the importer CLI and the resolver."""
import json
SCOPES = ("COMPANY", "GROUP", "NCM", "PRODUCT")
ENUMS = {
"product_role": {"RESALE", "OWN_PRODUCTION", "INPUT", "SERVICE"},
"st_status": {"SUBJECT", "NOT_SUBJECT"},
"st_withheld": {"Y", "N"},
"icms_treatment": {"TAXED", "EXEMPT", "NOT_TAXED", "ST_WITHHELD", "SUSPENDED", "DEFERRED"},
# MIXED_OR_DYNAMIC: purchases are not predominantly intra- or interstate, so no single
# static entry CFOP is fiscally correct for all operations (section 0, AI semantic layer V1).
"entry_scope": {"INTRA", "INTER", "MIXED_OR_DYNAMIC"},
"rounding": {"A", "T"},
"piscofins": {"N", "S"},
"incidence": {"NENHUMA"},
"origin": {str(i) for i in range(9)},
"store_ipi_profile": {"POPULATE_REFERENCE_RATE", "LEAVE_NOT_APPLICABLE"},
# C000004.CONTRIBUINTE_IPI is not yet sent at heartbeat (no Agent-side reader exists for it): until that auto-discovery lands, this is the
# one-time operator answer field_contract's CST_IPI rule needs - never assumed "N" just because the Agent hasn't reported it.
"contribuinte_ipi": {"S", "N"},
}
PROFILE_KEYS = {"pis_in_cst", "pis_out_cst", "pis_rate", "cofins_in_cst", "cofins_out_cst", "cofins_rate"}
FREE_KEYS = {"ncm", "cest", "ex_tipi", "icms_rate", "credit_rate", "cst_ipi", "service_cfop", "pis_cofins_profile"}
ALLOWED_KEYS = set(ENUMS) | FREE_KEYS
PRODUCT_ONLY = {"ncm"}
COMPANY_ONLY = {"store_ipi_profile", "contribuinte_ipi"}
NCM_KEYS = {"cest", "ex_tipi"} # tied to the NCM tables: answered per NCM or per product # an NCM answer is always about one product
def validate_entry(entry):
"""Returns (normalized_entry, error). error is None when valid."""
scope = str(entry.get("scope", "")).strip().upper()
key = str(entry.get("key", "")).strip()
value = entry.get("value")
value = "" if value is None else str(value).strip()
scope_key = str(entry.get("scope_key") or "").strip()
if scope not in SCOPES:
return None, f"scope must be one of {SCOPES}"
if key not in ALLOWED_KEYS:
return None, f"unknown key {key!r}"
if scope != "COMPANY" and not scope_key:
return None, "scope_key is required for GROUP/NCM/PRODUCT scope"
if key in COMPANY_ONLY and scope != "COMPANY":
return None, f"{key} is a COMPANY-scope answer"
if key in PRODUCT_ONLY and scope != "PRODUCT":
return None, f"{key} is a PRODUCT-scope answer"
if key in NCM_KEYS and scope not in ("PRODUCT", "NCM"):
return None, f"{key} can only be answered for a PRODUCT or an NCM"
if not value:
return None, "value is empty"
if key in ENUMS and value.upper() not in ENUMS[key] and value not in ENUMS[key]:
return None, f"{key} must be one of {sorted(ENUMS[key])}"
if key in ENUMS:
value = value.upper() if key != "origin" else value
if key == "ncm" and not (value.isdigit() and len(value) == 8):
return None, "ncm must be 8 digits"
if key == "cest" and value.upper() != "NONE" and not (value.replace(".", "").isdigit() and len(value.replace(".", "")) == 7):
return None, "cest must be 7 digits or NONE"
if key in ("icms_rate", "credit_rate"):
try:
float(value.replace(",", "."))
except ValueError:
return None, f"{key} must be numeric"
if key == "pis_cofins_profile" and value.upper() not in ("NOT_APPLICABLE", "OPERATION_TIME"):
try:
prof = json.loads(value)
except ValueError:
return None, "pis_cofins_profile must be JSON, or exactly NOT_APPLICABLE / OPERATION_TIME"
if not isinstance(prof, dict) or not prof or set(prof) - PROFILE_KEYS:
return None, f"pis_cofins_profile keys must be a non-empty subset of {sorted(PROFILE_KEYS)}"
elif key == "pis_cofins_profile":
value = value.upper()
return {"scope": scope, "scope_key": scope_key if scope != "COMPANY" else "", "key": key, "value": value, "reason": (entry.get("reason") or "").strip() or None}, None