feat: deterministic cClassTrib resolver + gated CCLASSTRIB auto-apply (default OFF), RTC panel summary; 1.1.9

- resolver (rules RTC-R0..R4/A1..A3/G1..G2) over the official cClassTrib table 2026-06-22 and LC 214/2025 annexes; local vs official catalog comparison
- ONE root-cause RTC blocker per product (missing/unresolved/ambiguous/conflict/official mismatch/catalog outdated/evidence missing)
- auto-apply: C000025.CCLASSTRIB only, three switches (FIREBIRD_WRITES + RTC_AUTO_APPLY_ENABLED + Hub authorization), per-product re-read and re-decision, never overwrites, rowcount 1, audit table rtc_write_audits (migration 0027)
- copy-only write E2E (280/280 applied, only CCLASSTRIB changed, 2nd run 0 writes, stale decision 0 writes); real databases untouched
- docs: cclasstrib_resolution, rtc_auto_apply, unified_fiscal_completeness

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-24 21:18:34 -03:00
co-authored by Claude Sonnet 5
parent 88f4318072
commit 9f08998800
51 changed files with 16461 additions and 47 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "gti-fiscal-agent"
version = "1.1.8"
version = "1.1.9"
requires-python = ">=3.10"
dependencies = ["httpx>=0.27,<1"]
+40 -10
View File
@@ -30,7 +30,7 @@ sys.path.insert(0, str(AGENT_ROOT / "src"))
sys.path.insert(0, str(PROJECT_ROOT)) # ships central/app engine modules read-only (central.app.*)
from gti_fiscal_agent.client import HubClient # noqa: E402
from gti_fiscal_agent import firebird_reader as _firebird_reader, unified_fiscal as _unified # noqa: E402
from gti_fiscal_agent import firebird_reader as _firebird_reader, rtc_auto_apply as _rtc_apply, unified_fiscal as _unified # noqa: E402
from gti_fiscal_agent.firebird_reader import check_connection, read_products # noqa: E402
from gti_fiscal_agent.pipeline import run_pipeline # noqa: E402
from gti_fiscal_agent.runner import REFERENCE_SCHEMA_V1, product_fingerprint, to_hub_product # noqa: E402
@@ -104,7 +104,10 @@ class Config:
# (A legacy AGENT_JOBS_ENABLED line in an old agent.env is deliberately ignored.)
self.agent_jobs_enabled = True
self.poll_interval_seconds = float(env.get("POLL_INTERVAL_SECONDS", "5"))
self.agent_version = env.get("AGENT_VERSION", "1.1.8")
# RTC auto-apply is a SEPARATE switch (default OFF): FIREBIRD_WRITES alone never lets the Agent fill cClassTrib.
self.rtc_auto_apply_enabled = env.get("RTC_AUTO_APPLY_ENABLED", "0").strip() == "1"
self.rtc_auto_apply_max_per_job = int(env.get("RTC_AUTO_APPLY_MAX_PER_JOB", str(_rtc_apply.DEFAULT_MAX_PER_JOB)))
self.agent_version = env.get("AGENT_VERSION", "1.1.9")
self.schema_fingerprint = env.get("SCHEMA_FINGERPRINT", REFERENCE_SCHEMA_V1)
self.state_db = env.get("AGENT_STATE_DB", str(AGENT_ROOT / "agent_state.sqlite"))
# Pilot write authorization: both must be explicitly set by the installer for
@@ -187,12 +190,12 @@ def open_write_connection(env, database_path, firebird_writes=True):
)
def _unified_fiscal(env, company):
def _unified_fiscal(env, company, ncm_valid=None):
"""PIS/COFINS applicability + RTC (only when the ERP has the reform on), read-only (SELECT only). Fail-closed: if it cannot be assessed the job fails
instead of reporting a coverage that ignores these fields."""
connection = _firebird_reader._connect(env)
try:
return _unified.assess_from_connection(connection, company)
return _unified.assess_from_connection(connection, company, ncm_valid=ncm_valid)
finally:
connection.close()
@@ -210,6 +213,28 @@ def _fiscal_blocked_products(products, findings):
return out
def _rtc_auto_apply(env, config, job, products, fiscal, company):
"""Report (always) and, only when all three gates are open, fill CCLASSTRIB for products whose class is PROVEN by a deterministic rule.
Returns the summary stored in the job result; never raises for a single product."""
gate = _rtc_apply.write_gate(firebird_writes=bool(getattr(config, "firebird_writes", False)), agent_enabled=bool(getattr(config, "rtc_auto_apply_enabled", False)),
hub_authorized=bool((job.get("authorizations") or {}).get("rtc_auto_apply")))
limit = int(getattr(config, "rtc_auto_apply_max_per_job", _rtc_apply.DEFAULT_MAX_PER_JOB))
candidates, ready_total = _rtc_apply.select_candidates(fiscal["findings"], products, limit=limit)
summary = {"gate": gate, "mode": "AUTO_APPLY" if gate["allowed"] else "REPORT_ONLY", "ready_total": ready_total, "max_per_job": limit, "write_attempts": 0, "write_successes": 0, "applied": [], "aborted": [], "aborted_reasons": {},
"allowed_fields": list(_rtc_apply.ALLOWED_RTC_WRITE_FIELDS)}
if not (gate["allowed"] and candidates and fiscal["context"].get("rtc_enabled")):
return summary
from datetime import datetime, timezone
connection = open_write_connection(env, config.database_path)
try:
outcome = _rtc_apply.apply_candidates(connection, candidates, company=company or {}, decision={"local_catalog_hash": fiscal["context"].get("local_catalog_hash")},
now=datetime.now(timezone.utc).isoformat(timespec="seconds"), job_id=job.get("job_id"))
finally:
connection.close()
summary.update({k: outcome[k] for k in ("applied", "aborted", "aborted_reasons", "write_attempts", "write_successes")})
return summary
def execute_job(job, config, executor, state, env):
"""Run the pipeline for one claimed job. Never raises for job-domain
failures (bad Firebird read, classify error) — returns (result_summary, error)."""
@@ -249,7 +274,7 @@ def execute_job(job, config, executor, state, env):
if write_connection is not None:
write_connection.close()
operational = coverage = fiscal_context = None
operational = coverage = fiscal_context = rtc_apply = None
fiscal = None
if sync_kind:
if cache:
@@ -261,13 +286,17 @@ def execute_job(job, config, executor, state, env):
all_decisions = decisions
coverage = build_job_coverage(env, client, products, all_decisions)
company = {"crt": ((coverage or {}).get("company_context") or {}).get("company_crt")}
fiscal = _unified_fiscal(env, company)
ncm_ok = {str(p["CODIGO"]): (d or {}).get("status") == "EXACT_NCM" for p, d in zip(products, all_decisions)}
fiscal = _unified_fiscal(env, company, ncm_ok)
rtc_apply = _rtc_auto_apply(env, config, job, products, fiscal, company)
if rtc_apply["write_successes"]:
fiscal = _unified_fiscal(env, company, ncm_ok) # coverage must reflect the fields that were just filled
findings = fiscal["findings"]
product_states = [classify_product_state(p, d, fiscal=findings.get(str(p["CODIGO"]))) for p, d in zip(products, all_decisions)]
operational = summarize_operational_states(product_states)
fiscal_context = {**fiscal["context"], "evaluated_all": kind != "REPROCESS_PRODUCTS"}
else:
fiscal = _unified_fiscal(env, None)
fiscal = _unified_fiscal(env, None, {str(p["CODIGO"]): (d or {}).get("status") == "EXACT_NCM" for p, d in zip(to_process, decisions)})
fiscal_blocked = _fiscal_blocked_products(products, fiscal["findings"])
fiscal_codes = {b["product_code"] for b in fiscal_blocked}
summary["blocked_products"] = summary["blocked_products"] + fiscal_blocked
@@ -276,8 +305,9 @@ def execute_job(job, config, executor, state, env):
"coverage": coverage,
"operational": operational,
"fiscal_context": fiscal_context,
"rtc_auto_apply": rtc_apply,
"fiscal_mode": getattr(config, "fiscal_mode", None),
"release_version": env.get("AGENT_VERSION") or "1.1.8",
"release_version": env.get("AGENT_VERSION") or "1.1.9",
"products_total": len(products),
"products_changed": len(to_process),
"products_processed": summary["processed"],
@@ -365,7 +395,7 @@ def run(config, env, *, max_iterations=None, stopper=None):
"agent_version": config.agent_version, "last_job_id": last_job_id, "display_name": env.get("AGENT_DISPLAY_NAME", ""),
"customer_code": env.get("CUSTOMER_CODE"), "hostname": env.get("AGENT_HOSTNAME") or os.environ.get("COMPUTERNAME"),
"windows_version": env.get("WINDOWS_VERSION"), "company_code": config.company_id, "company_uf": config.company_uf,
"database_path": config.database_path, "capabilities": {"heartbeat": True, "jobs": True, "local_logs": True},
"database_path": config.database_path, "capabilities": {"heartbeat": True, "jobs": True, "local_logs": True, "rtc_auto_apply": True, "rtc_auto_apply_enabled": bool(getattr(config, "rtc_auto_apply_enabled", False))},
"fiscal_engine_version": env.get("FISCAL_ENGINE_VERSION", config.agent_version), "dataset_versions": {},
}
if hasattr(client, "heartbeat_metadata"):
@@ -435,7 +465,7 @@ def run(config, env, *, max_iterations=None, stopper=None):
def enforce_startup_preflight(env):
"""Full read-only preflight at startup (Hub, auth, Firebird, capabilities, company context, guards). Fail closed."""
from gti_fiscal_agent.preflight import run_preflight
report = run_preflight(env, agent_version=env.get("AGENT_VERSION") or "1.1.8")
report = run_preflight(env, agent_version=env.get("AGENT_VERSION") or "1.1.9")
logger.info("startup preflight: %s", json.dumps({k: report[k] for k in ("PREFLIGHT", "blockers", "fiscal_mode", "effective_firebird_writes")}, ensure_ascii=False))
print(json.dumps({"PREFLIGHT": report["PREFLIGHT"], "blockers": report["blockers"]}, ensure_ascii=False))
if report["PREFLIGHT"] != "PASS":
+1 -1
View File
@@ -9,7 +9,7 @@ import os
import sqlite3
from pathlib import Path
AGENT_VERSION_DEFAULT = "1.1.8"
AGENT_VERSION_DEFAULT = "1.1.9"
REQUIRED_ENV = ("GTI_HUB_URL", "AGENT_TOKEN", "FIREBIRD_DATABASE_PATH", "FB_USER", "FB_PASSWORD")
REQUIRED_CAPABILITIES = ("supports_ncm", "supports_ipi", "supports_cest")
FISCAL_MODES = ("DRY_RUN", "AUTO_APPLY")
@@ -0,0 +1,136 @@
"""RTC auto-apply: fills C000025.CCLASSTRIB, and NOTHING else, when a deterministic rule proves the class.
Safety model (every gate must hold, default is OFF everywhere):
FIREBIRD_WRITES effective (FISCAL_MODE=AUTO_APPLY + path/schema authorization) AND RTC_AUTO_APPLY_ENABLED (this Agent) AND Hub authorization (per installation)
Write allowlist: ALLOWED_RTC_WRITE_FIELDS = ["CCLASSTRIB"]. One short transaction per product. Immediately before the UPDATE the product is re-read and
the whole decision is re-derived from fresh data; if ANYTHING changed (RTC switch, catalog, NCM, current value, decision) that product is ABORTED.
An existing value is never overwritten. Every applied change returns an audit record with the old value, so it can be reverted manually and auditably.
"""
from __future__ import annotations
import re
from datetime import date
from central.app.cclasstrib_resolver import compare_catalogs, local_catalog_hash, resolve_cclasstrib
from central.app.unified_fiscal_coverage import rtc_enabled
from .unified_fiscal import detect_rtc_capabilities, load_rtc_context
ALLOWED_RTC_WRITE_FIELDS = ("CCLASSTRIB",)
DEFAULT_MAX_PER_JOB = 500
class RtcApplyAborted(Exception):
"""The product is left untouched; `reason` is reported in the audit."""
def __init__(self, reason: str):
super().__init__(reason)
self.reason = reason
def _text(value) -> str:
return "" if value is None else str(value).strip()
def _digits(value) -> str:
return re.sub(r"\D", "", _text(value))
def write_gate(*, firebird_writes: bool, agent_enabled: bool, hub_authorized: bool) -> dict:
"""{"allowed": bool, "missing": [...]}: writes are possible only when all three switches are on."""
missing = [name for name, on in (("FIREBIRD_WRITES", firebird_writes), ("RTC_AUTO_APPLY_ENABLED", agent_enabled), ("HUB_AUTHORIZATION", hub_authorized)) if not on]
return {"allowed": not missing, "missing": missing, "firebird_writes": bool(firebird_writes), "agent_enabled": bool(agent_enabled), "hub_authorized": bool(hub_authorized)}
def select_candidates(findings: dict, products: list[dict], *, limit: int = DEFAULT_MAX_PER_JOB) -> tuple[list[dict], int]:
"""Products READY_TO_APPLY: empty CCLASSTRIB + a deterministic resolution, no other RTC problem. Returns (candidates up to `limit`, total ready)."""
by_code = {str(p.get("CODIGO") or ""): p for p in products}
ready = []
for code, found in findings.items():
res = found.get("rtc_resolution") or {}
if not res.get("auto_apply_candidate"):
continue
rtc_types = {b["type"] for b in found.get("blockers", []) if b["category"] == "RTC"}
if rtc_types != {"CCLASSTRIB_MISSING"}:
continue
p = by_code.get(code, {})
if _text(p.get("CCLASSTRIB")):
continue
ready.append({"product_code": code, "proposed": res["resolved_cclasstrib"], "cst": res["resolved_cst_ibscbs"], "rule_id": res["rule_id"], "ncm": _digits(p.get("CLASSIFICACAO_FISCAL")), "confidence_type": res["confidence_type"]})
return ready[:limit], len(ready)
def apply_candidate(conn, candidate: dict, *, company: dict, decision: dict, now: str, job_id=None, today: date | None = None) -> dict:
"""One product, one short transaction. `decision` = {"local_catalog_hash": ...} captured when the candidate was chosen. Raises RtcApplyAborted (nothing written)."""
field = "CCLASSTRIB"
assert field in ALLOWED_RTC_WRITE_FIELDS
today = today or date.today()
code, proposed = candidate["product_code"], candidate["proposed"]
cur = conn.cursor()
try:
schema, catalog, extras = load_rtc_context(cur) # fresh: the ERP is asked again, nothing cached
caps = detect_rtc_capabilities(schema, param_value=extras["param_value"], catalog=catalog, physical_fk=extras["physical_fk"])
if extras["param_conflict"] or not rtc_enabled(caps):
raise RtcApplyAborted("RTC_NO_LONGER_ENABLED")
if not caps["RTC_CATALOG_VALID"]:
raise RtcApplyAborted("RTC_CATALOG_INVALID")
local_rows = (catalog or {}).get("rows") or {}
fresh_hash = local_catalog_hash(local_rows)
if decision.get("local_catalog_hash") and fresh_hash != decision["local_catalog_hash"]:
raise RtcApplyAborted("LOCAL_CATALOG_CHANGED_SINCE_DECISION")
cur.execute(f"SELECT {field}, CLASSIFICACAO_FISCAL FROM C000025 WHERE CODIGO = ?", (code,))
rows = cur.fetchall()
if len(rows) != 1:
raise RtcApplyAborted("PRODUCT_ROW_NOT_UNIQUE" if rows else "PRODUCT_NOT_FOUND")
current, ncm = _text(rows[0][0]), _digits(rows[0][1])
if current:
raise RtcApplyAborted("VALUE_ALREADY_SET_NEVER_OVERWRITTEN" if current != proposed else "NO_WRITE_REQUIRED")
if ncm != candidate["ncm"]:
raise RtcApplyAborted("PRODUCT_CHANGED_NCM")
compat = compare_catalogs(local_rows, today)
resolution = resolve_cclasstrib({"CODIGO": code, "CLASSIFICACAO_FISCAL": ncm, "ncm_valid": True}, company, {"rtc_enabled": True, "local_catalog_valid": True}, local_rows, compat=compat, today=today)
if not resolution.auto_apply_candidate or resolution.resolved_cclasstrib != proposed or resolution.rule_id != candidate["rule_id"]:
raise RtcApplyAborted("DECISION_CHANGED")
cur.execute(f"UPDATE C000025 SET {field} = ? WHERE CODIGO = ? AND ({field} IS NULL OR TRIM({field}) = '')", (proposed, code))
if cur.rowcount != 1:
conn.rollback()
raise RtcApplyAborted("UPDATE_ROWCOUNT_NOT_ONE")
conn.commit()
cur = conn.cursor()
cur.execute(f"SELECT {field} FROM C000025 WHERE CODIGO = ?", (code,))
after = _text(cur.fetchone()[0])
conn.commit()
if after != proposed:
raise RuntimeError(f"post-write verification failed for {code}: {after!r} != {proposed!r}")
official = compat["official_catalog"]
return {"outcome": "APPLIED", "product_code": code, "field": field, "old_value": None, "new_value": proposed, "cst_ibscbs": resolution.resolved_cst_ibscbs, "rule_id": resolution.rule_id,
"confidence_type": resolution.confidence_type, "evidence": {"inputs_used": resolution.inputs_used, "evidence": resolution.evidence[:5], "reason": resolution.reason},
"official_source_version": {"cclasstrib_table": official["publication"], "cclasstrib_sha256": official["sha256"], "lc214_sha256": resolution.source_references[0]["sha256"]},
"local_catalog_hash": fresh_hash, "timestamp": now, "job_id": job_id, "rowcount": 1, "verified_after": True}
except RtcApplyAborted:
try:
conn.rollback()
except Exception: # noqa: BLE001
pass
raise
except Exception:
try:
conn.rollback()
except Exception: # noqa: BLE001
pass
raise
def apply_candidates(conn, candidates: list[dict], *, company: dict, decision: dict, now: str, job_id=None, today: date | None = None) -> dict:
"""Applies each candidate independently; a failure or an abort never stops the rest and never leaves a partial change."""
applied, aborted, reasons = [], [], {}
for candidate in candidates:
try:
applied.append(apply_candidate(conn, candidate, company=company, decision=decision, now=now, job_id=job_id, today=today))
except RtcApplyAborted as stop:
aborted.append({"outcome": "ABORTED", "product_code": candidate["product_code"], "field": "CCLASSTRIB", "reason": stop.reason, "rule_id": candidate["rule_id"], "job_id": job_id, "timestamp": now})
reasons[stop.reason] = reasons.get(stop.reason, 0) + 1
except Exception as error: # noqa: BLE001 - technical failure of ONE product must not abort the batch
aborted.append({"outcome": "FAILED", "product_code": candidate["product_code"], "field": "CCLASSTRIB", "reason": f"{type(error).__name__}", "rule_id": candidate["rule_id"], "job_id": job_id, "timestamp": now})
reasons["TECHNICAL_FAILURE"] = reasons.get("TECHNICAL_FAILURE", 0) + 1
return {"applied": applied, "aborted": aborted, "aborted_reasons": reasons, "write_attempts": len(candidates), "write_successes": len(applied)}
+26 -4
View File
@@ -11,12 +11,14 @@ import json
from collections import Counter
from datetime import date, datetime, timezone
from central.app.cclasstrib_resolver import compare_catalogs, local_catalog_hash, resolve_cclasstrib
from central.app.rtc_findings import refine_rtc_finding, summarize_rtc
from central.app.unified_fiscal_coverage import PIS_COFINS_FIELDS, catalog_integrity, evaluate_product, legal_effective_context, rtc_enabled
PRODUCT_TABLE = "C000025"
RTC_CCLASSTRIB_TABLE = "RTC_CCLASSTRIB"
RTC_CST_TABLE = "RTC_CST"
PRODUCT_COLUMNS = ("CODIGO", "CCLASSTRIB") + PIS_COFINS_FIELDS + ("PISCOFINS", "INCIDENCIA_PISCOFINS")
PRODUCT_COLUMNS = ("CODIGO", "CCLASSTRIB", "CLASSIFICACAO_FISCAL") + PIS_COFINS_FIELDS + ("PISCOFINS", "INCIDENCIA_PISCOFINS")
TRUTHY = {"1", "S", "T", "TRUE"}
@@ -67,13 +69,23 @@ def load_rtc_context(cursor) -> tuple[dict, dict | None, dict]:
class_cols = schema.get(RTC_CCLASSTRIB_TABLE, set())
if {"CCLASSTRIB", "CSTIBSCBS"} <= class_cols and "CSTIBSCBS" in schema.get(RTC_CST_TABLE, set()):
validity = "INICIO_VIGENCIA" in class_cols and "FIM_VIGENCIA" in class_cols
cursor.execute("SELECT TRIM(CCLASSTRIB), TRIM(CSTIBSCBS)" + (", INICIO_VIGENCIA, FIM_VIGENCIA" if validity else "") + " FROM RTC_CCLASSTRIB")
extra = [c for c in ("TIPO_ALIQUOTA", "PRED_IBS", "PRED_CBS", "IND_NFE", "IND_NFCE") if c in class_cols]
cursor.execute("SELECT TRIM(CCLASSTRIB), TRIM(CSTIBSCBS)" + (", INICIO_VIGENCIA, FIM_VIGENCIA" if validity else "") + "".join(f", {c}" for c in extra) + " FROM RTC_CCLASSTRIB")
rows = cursor.fetchall()
classes = {r[0]: (r[1] or None) for r in rows}
cursor.execute("SELECT TRIM(CSTIBSCBS) FROM RTC_CST")
catalog = {"classes": classes, "cst": {r[0] for r in cursor.fetchall()}, "duplicates": len(rows) - len(classes)}
if validity:
catalog["validity"] = {r[0]: (r[2], r[3]) for r in rows}
offset = 4 if validity else 2
catalog["rows"] = {r[0]: {"cst": r[1], "start": (r[2] if validity else None), "end": (r[3] if validity else None), **{c: r[offset + i] for i, c in enumerate(extra)}} for r in rows}
for value in catalog["rows"].values():
for key in ("TIPO_ALIQUOTA", "PRED_IBS", "PRED_CBS", "IND_NFE", "IND_NFCE"):
if key in value:
value[key.lower()] = value.pop(key)
for flag in ("ind_nfe", "ind_nfce"):
if flag in value:
value[flag] = str(value[flag]).strip() in ("1", "True", "true")
return schema, catalog, extras
@@ -87,9 +99,10 @@ def read_products(cursor, schema: dict) -> list[dict]:
return [dict(zip(columns, row)) for row in cursor.fetchall()]
def assess_from_connection(conn, company: dict | None, *, today: date | None = None, rtc_override: bool | None = None) -> dict:
def assess_from_connection(conn, company: dict | None, *, today: date | None = None, rtc_override: bool | None = None, ncm_valid: dict | None = None) -> dict:
"""One read-only pass. Returns {"context": {...}, "findings": {product_code: evaluate_product(...)}}.
rtc_override forces RTC on/off for simulations WITHOUT touching the ERP parameter (tests / what-if only)."""
rtc_override forces RTC on/off for simulations WITHOUT touching the ERP parameter (tests / what-if only).
ncm_valid: {product_code: bool} from the decision engine (EXACT_NCM); a product whose NCM is not confirmed never gets a cClassTrib proposal."""
cursor = conn.cursor()
schema, catalog, extras = load_rtc_context(cursor)
caps = detect_rtc_capabilities(schema, param_value=extras["param_value"], catalog=catalog, physical_fk=extras["physical_fk"])
@@ -102,8 +115,15 @@ def assess_from_connection(conn, company: dict | None, *, today: date | None = N
warn_products: set = set()
samples: list = []
applicability: Counter = Counter()
local_rows = (catalog or {}).get("rows") or {}
compat = compare_catalogs(local_rows, today) if (enabled and local_rows) else None
resolver_ctx = {"rtc_enabled": enabled, "local_catalog_valid": bool(caps["RTC_CATALOG_VALID"])}
for p in products:
found = evaluate_product(p, company, rtc, today)
if enabled:
code0 = str(p.get("CODIGO") or "")
resolution = resolve_cclasstrib({**p, "ncm_valid": bool((ncm_valid or {}).get(code0, False))}, company, resolver_ctx, local_rows, compat=compat, today=today)
refine_rtc_finding(found, p, resolution, compat["per_code"] if compat else None)
found["input_fingerprint"] = hashlib.sha256(json.dumps({k: (None if v is None else str(v).strip()) for k, v in p.items()} | {"rtc": enabled}, sort_keys=True).encode()).hexdigest()
code = str(p.get("CODIGO") or "")
findings[code] = found
@@ -119,5 +139,7 @@ def assess_from_connection(conn, company: dict | None, *, today: date | None = N
"rtc_observed_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), "legal_effective_context": legal_effective_context((company or {}).get("crt")),
"catalog_integrity": integrity, "crt": (company or {}).get("crt"), "capabilities": caps, "products_evaluated": len(products),
"pis_cofins_applicability": dict(applicability),
"rtc_resolution": summarize_rtc(findings, compat) if enabled else None, "local_catalog_hash": local_catalog_hash(local_rows) if local_rows else None,
"official_catalog": (compat or {}).get("official_catalog"),
"non_canonical_warnings": {"products": len(warn_products), "by_type": dict(sorted(warn_by_type.items())), "samples": samples}},
"findings": findings}
+297
View File
@@ -0,0 +1,297 @@
import re
from datetime import date
import pytest
from central.app import rtc_reference_data as ref
from central.app.cclasstrib_resolver import local_catalog_hash
from gti_fiscal_agent import rtc_auto_apply as apply_mod
from gti_fiscal_agent import unified_fiscal as uf
pytestmark = pytest.mark.real_unified_fiscal
TODAY = date(2026, 9, 24)
NORMAL = {"crt": "3"}
CLASS_COLS = ("CCLASSTRIB", "CSTIBSCBS", "INICIO_VIGENCIA", "FIM_VIGENCIA", "TIPO_ALIQUOTA", "PRED_IBS", "PRED_CBS", "IND_NFE", "IND_NFCE")
SCHEMA = ([("C000025", c) for c in ("CODIGO", "CCLASSTRIB", "CLASSIFICACAO_FISCAL", "PISCOFINS", "INCIDENCIA_PISCOFINS")] + [("RTC_CCLASSTRIB", c) for c in CLASS_COLS] + [("RTC_CST", "CSTIBSCBS"), ("PARAMETROS", "USAR_REFORMA_TRIBUTARIA")])
class FakeFirebird:
"""A tiny SGV: PARAMETROS, the RTC catalog (from the official table) and C000025. Every statement is recorded; only SELECT and the single allowed UPDATE are understood."""
def __init__(self, products, param=(1,), catalog_tweak=None, skip=()):
self.products = {c: dict(v) for c, v in products.items()} # code -> {"CCLASSTRIB":..., "CLASSIFICACAO_FISCAL":...}
self.param = list(param)
self.catalog = {r["code"]: {"cst": r["cst"], "start": r["start"], "end": r["end"], "tipo": r["tipo_aliquota"], "pred_ibs": r["p_red_ibs"], "pred_cbs": r["p_red_cbs"], "ind_nfe": r["ind_nfe"], "ind_nfce": r["ind_nfce"]}
for r in ref.OFFICIAL_CCLASSTRIB if r["code"] not in skip and not r["end"]}
for code, change in (catalog_tweak or {}).items():
self.catalog[code] = {**self.catalog[code], **change}
self.cst_table = {r["cst"] for r in ref.OFFICIAL_CCLASSTRIB} # RTC_CST is its own table: a class may point to a CST that is not in it
self.statements, self.commits, self.rollbacks = [], 0, 0
self._pending = {}
def cursor(self):
return FakeCursor(self)
def commit(self):
for code, value in self._pending.items():
self.products[code]["CCLASSTRIB"] = value
self._pending = {}
self.commits += 1
def rollback(self):
self._pending = {}
self.rollbacks += 1
class FakeCursor:
def __init__(self, db):
self.db, self._rows, self.rowcount = db, [], -1
def execute(self, sql, params=()):
db = self.db
db.statements.append((sql, tuple(params)))
text = " ".join(sql.split())
up = text.upper()
if up.startswith("SELECT TRIM(RF.RDB$RELATION_NAME)"):
self._rows = list(SCHEMA)
elif up.startswith("SELECT FIRST 20 USAR_REFORMA_TRIBUTARIA"):
self._rows = [(v,) for v in db.param]
elif "RDB$RELATION_CONSTRAINTS" in up:
self._rows = [(1,)]
elif up.startswith("SELECT TRIM(CCLASSTRIB)") and "FROM RTC_CCLASSTRIB" in up:
cols = [c.strip() for c in text[len("SELECT "):up.index(" FROM ")].split(",")]
rows = []
for code, r in db.catalog.items():
mapping = {"CSTIBSCBS": r["cst"], "INICIO_VIGENCIA": date.fromisoformat(r["start"]) if r["start"] else None, "FIM_VIGENCIA": None, "TIPO_ALIQUOTA": r["tipo"], "PRED_IBS": r["pred_ibs"],
"PRED_CBS": r["pred_cbs"], "IND_NFE": "1" if r["ind_nfe"] else "0", "IND_NFCE": "1" if r["ind_nfce"] else "0"}
rows.append(tuple(code if c.startswith("TRIM(CCLASSTRIB") else mapping[re.sub(r"TRIM\((\w+)\)", r"\1", c)] for c in cols))
self._rows = rows
elif up.startswith("SELECT TRIM(CSTIBSCBS) FROM RTC_CST"):
self._rows = [(c,) for c in sorted(db.cst_table)]
elif up.startswith("SELECT CODIGO, CCLASSTRIB, CLASSIFICACAO_FISCAL"):
self._rows = [(c, v["CCLASSTRIB"], v["CLASSIFICACAO_FISCAL"], "N", "NENHUMA") for c, v in sorted(db.products.items())]
elif up.startswith("SELECT CCLASSTRIB, CLASSIFICACAO_FISCAL FROM C000025"):
code = params[0]
self._rows = [(db.products[code]["CCLASSTRIB"], db.products[code]["CLASSIFICACAO_FISCAL"])] if code in db.products else []
if code in db.products and db.products[code].get("DUP"):
self._rows = self._rows * 2
elif up.startswith("SELECT CCLASSTRIB FROM C000025"):
code = params[0]
self._rows = [(db.products[code]["CCLASSTRIB"],)]
elif up.startswith("UPDATE C000025 SET CCLASSTRIB = ? WHERE CODIGO = ?"):
value, code = params
product = db.products.get(code)
hit = product is not None and not (product["CCLASSTRIB"] or "").strip()
self.rowcount = (2 if product and product.get("DUP") else 1) if hit else 0
if hit and self.rowcount == 1:
db._pending[code] = value
else:
raise AssertionError(f"unexpected statement: {sql}")
def fetchall(self):
return self._rows
def fetchone(self):
return self._rows[0] if self._rows else None
def product(ncm, value=None, **extra):
return {"CCLASSTRIB": value, "CLASSIFICACAO_FISCAL": ncm, **extra}
def assess(db, ncm_ok=None, company=NORMAL):
return uf.assess_from_connection(db, company, today=TODAY, ncm_valid=ncm_ok if ncm_ok is not None else {c: True for c in db.products})
def candidates_of(db, limit=500):
out = assess(db)
products = [{"CODIGO": c, "CCLASSTRIB": v["CCLASSTRIB"], "CLASSIFICACAO_FISCAL": v["CLASSIFICACAO_FISCAL"]} for c, v in db.products.items()]
cands, total = apply_mod.select_candidates(out["findings"], products, limit=limit)
return out, cands, total
def run(db, cands, out, **kw):
return apply_mod.apply_candidates(db, cands, company=NORMAL, decision={"local_catalog_hash": out["context"]["local_catalog_hash"]}, now="2026-09-24T20:00:00+00:00", job_id=7, today=TODAY, **kw)
PRODUCTS = {"000001": product("10063021"), "000002": product("39235000"), "000003": product("15171000"), "000004": product("30049099"), "000005": product("39235000", "000001")}
def test_gate_needs_all_three_switches_and_defaults_to_closed():
assert apply_mod.write_gate(firebird_writes=True, agent_enabled=True, hub_authorized=True)["allowed"] is True
for combo in ((False, True, True), (True, False, True), (True, True, False), (False, False, False)):
g = apply_mod.write_gate(firebird_writes=combo[0], agent_enabled=combo[1], hub_authorized=combo[2])
assert g["allowed"] is False and g["missing"]
assert apply_mod.ALLOWED_RTC_WRITE_FIELDS == ("CCLASSTRIB",)
def test_only_proven_products_become_candidates():
db = FakeFirebird(PRODUCTS)
out, cands, total = candidates_of(db)
assert sorted((c["product_code"], c["proposed"], c["rule_id"]) for c in cands) == [("000001", "200003", "RTC-R3"), ("000002", "000001", "RTC-R4"), ("000003", "200003", "RTC-R3")]
assert total == 3 and out["context"]["rtc_resolution"]["ready_to_apply"] == 3 and out["context"]["rtc_resolution"]["ambiguous"] == 1 # medicine chapter is ambiguous
assert out["context"]["rtc_resolution"]["valid_existing"] == 1 # 000005 already has the right class
assert "000004" not in {c["product_code"] for c in cands}
def test_apply_writes_only_cclasstrib_with_audit_and_second_run_writes_nothing():
db = FakeFirebird(PRODUCTS)
out, cands, _ = candidates_of(db)
result = run(db, cands, out)
assert result["write_successes"] == 3 and result["write_attempts"] == 3 and result["aborted"] == []
assert {c: db.products[c]["CCLASSTRIB"] for c in ("000001", "000002", "000003")} == {"000001": "200003", "000002": "000001", "000003": "200003"}
updates = [s for s, _ in db.statements if s.upper().startswith("UPDATE")]
assert len(updates) == 3 and all(re.fullmatch(r"UPDATE C000025 SET CCLASSTRIB = \? WHERE CODIGO = \? AND \(CCLASSTRIB IS NULL OR TRIM\(CCLASSTRIB\) = ''\)", u) for u in updates)
assert not [s for s, _ in db.statements if re.match(r"(INSERT|DELETE|ALTER|DROP)", s.strip(), re.I)]
audit = result["applied"][0]
assert {"product_code", "field", "old_value", "new_value", "rule_id", "evidence", "official_source_version", "local_catalog_hash", "timestamp", "job_id", "outcome"} <= set(audit)
assert audit["old_value"] is None and audit["field"] == "CCLASSTRIB" and audit["official_source_version"]["cclasstrib_sha256"] == ref.OFFICIAL_CATALOG["sha256"] and audit["job_id"] == 7
out2, cands2, total2 = candidates_of(db) # idempotent: nothing left to write
assert cands2 == [] and total2 == 0 and out2["context"]["rtc_resolution"]["valid_existing"] == 4
before = len(db.statements)
assert run(db, cands2, out2)["write_attempts"] == 0 and len(db.statements) == before
def test_rtc_switched_off_between_decision_and_write_aborts_every_product():
db = FakeFirebird(PRODUCTS)
out, cands, _ = candidates_of(db)
db.param = [0]
result = run(db, cands, out)
assert result["write_successes"] == 0 and result["aborted_reasons"] == {"RTC_NO_LONGER_ENABLED": 3} and all(p["CCLASSTRIB"] in (None, "000001") for p in db.products.values())
def test_conflicting_parameter_rows_abort():
db = FakeFirebird(PRODUCTS)
out, cands, _ = candidates_of(db)
db.param = [1, 0]
assert run(db, cands, out)["aborted_reasons"] == {"RTC_NO_LONGER_ENABLED": 3}
def test_catalog_changed_or_invalid_between_decision_and_write_aborts():
db = FakeFirebird(PRODUCTS)
out, cands, _ = candidates_of(db)
db.catalog["200003"]["pred_ibs"] = "60"
assert run(db, cands, out)["aborted_reasons"] == {"LOCAL_CATALOG_CHANGED_SINCE_DECISION": 3}
db2 = FakeFirebird(PRODUCTS)
out2, cands2, _ = candidates_of(db2)
db2.catalog["999999"] = {**db2.catalog["000001"], "cst": "555"} # class pointing to a CST that does not exist -> invalid catalog
invalid = apply_mod.apply_candidates(db2, cands2, company=NORMAL, decision={}, now="2026-09-24T20:00:00+00:00", job_id=7, today=TODAY) # no hash pinned: integrity check alone must stop it
assert invalid["aborted_reasons"] == {"RTC_CATALOG_INVALID": 3}
def test_product_changed_between_decision_and_write_is_left_alone():
db = FakeFirebird(PRODUCTS)
out, cands, _ = candidates_of(db)
db.products["000001"]["CLASSIFICACAO_FISCAL"] = "30049099" # NCM edited in the ERP meanwhile
db.products["000002"]["CCLASSTRIB"] = "000001" # operator filled it meanwhile (same value)
db.products["000003"]["CCLASSTRIB"] = "200034" # ... or with a DIFFERENT value: never overwritten
result = run(db, cands, out)
assert result["aborted_reasons"] == {"PRODUCT_CHANGED_NCM": 1, "NO_WRITE_REQUIRED": 1, "VALUE_ALREADY_SET_NEVER_OVERWRITTEN": 1} and result["write_successes"] == 0
assert db.products["000003"]["CCLASSTRIB"] == "200034" and not [s for s, _ in db.statements if s.upper().startswith("UPDATE")]
def test_duplicate_code_rowcount_protection_and_isolation_of_failures():
products = {**PRODUCTS, "000002": product("39235000", DUP=True)}
db = FakeFirebird(products)
out, cands, _ = candidates_of(db)
result = run(db, cands, out)
assert result["aborted_reasons"] == {"PRODUCT_ROW_NOT_UNIQUE": 1} and result["write_successes"] == 2 # the ambiguous row is skipped, the rest still applied
assert db.products["000002"]["CCLASSTRIB"] is None
def test_existing_different_valid_class_is_a_conflict_blocker_never_a_write():
db = FakeFirebird({"000001": product("10063021", "000001")}) # rice already classified as fully taxed: differs from the proven Anexo I class
out = assess(db)
types = {b["type"] for b in out["findings"]["000001"]["blockers"]}
assert types == {"CCLASSTRIB_CONFLICT"} and out["context"]["rtc_resolution"]["conflicts"] == 1
_, cands, _ = candidates_of(db)
assert cands == []
def test_official_mismatch_of_an_existing_class_is_reported():
db = FakeFirebird({"000001": product("39235000", "000001")}, catalog_tweak={"000001": {"ind_nfe": False, "ind_nfce": False}})
types = {b["type"] for b in assess(db)["findings"]["000001"]["blockers"]}
assert types == {"CCLASSTRIB_OFFICIAL_MISMATCH"}
def test_batch_limit_bounds_the_blast_radius():
db = FakeFirebird({f"{i:06d}": product("39235000") for i in range(1, 8)})
out, cands, total = candidates_of(db, limit=3)
assert len(cands) == 3 and total == 7
def test_simples_never_produces_candidates_even_with_rtc_on():
db = FakeFirebird(PRODUCTS)
out = uf.assess_from_connection(db, {"crt": "1"}, today=TODAY, ncm_valid={c: True for c in PRODUCTS})
assert out["context"]["rtc_resolution"]["ready_to_apply"] == 0
kinds = {b["type"] for f in out["findings"].values() for b in f["blockers"]}
assert "CCLASSTRIB_MISSING_UNRESOLVED" in kinds and "CCLASSTRIB_MISSING" not in kinds
def test_rtc_off_has_no_resolution_and_no_candidates():
db = FakeFirebird(PRODUCTS, param=(0,))
out, cands, total = candidates_of(db)
assert cands == [] and total == 0 and out["context"]["rtc_resolution"] is None
def test_catalog_hash_is_recorded_for_audit():
db = FakeFirebird(PRODUCTS)
out = assess(db)
schema, catalog, _ = uf.load_rtc_context(db.cursor())
assert out["context"]["local_catalog_hash"] == local_catalog_hash(catalog["rows"]) and re.fullmatch(r"[0-9a-f]{64}", out["context"]["local_catalog_hash"])
# ---------------------------------------------------------------- job integration (gates)
def _service():
from test_run_agent_service import _config, service
return service, _config
def _fiscal_for(db):
return assess(db)
def test_job_reports_ready_products_but_writes_nothing_with_default_switches(monkeypatch):
service, _config = _service()
db = FakeFirebird(PRODUCTS)
fiscal = _fiscal_for(db)
monkeypatch.setattr(service, "open_write_connection", lambda *a, **k: (_ for _ in ()).throw(AssertionError("no write connection may be opened")))
products = [{"CODIGO": c, "CCLASSTRIB": v["CCLASSTRIB"], "CLASSIFICACAO_FISCAL": v["CLASSIFICACAO_FISCAL"]} for c, v in db.products.items()]
for cfg_kwargs, auth in (({}, False), ({"firebird_writes": True}, False), ({"firebird_writes": True, "rtc_auto_apply_enabled": True}, False), ({"rtc_auto_apply_enabled": True}, True), ({"firebird_writes": True}, True)):
config = _config(**cfg_kwargs)
summary = service._rtc_auto_apply({}, config, {"job_id": 1, "authorizations": {"rtc_auto_apply": auth}}, products, fiscal, NORMAL)
assert summary["mode"] == "REPORT_ONLY" and summary["gate"]["allowed"] is False and summary["ready_total"] == 3 and summary["write_attempts"] == 0 and summary["applied"] == []
def test_job_applies_only_when_all_three_switches_are_open(monkeypatch):
service, _config = _service()
db = FakeFirebird(PRODUCTS)
fiscal = _fiscal_for(db)
monkeypatch.setattr(service, "open_write_connection", lambda env, path: _Closeable(db))
products = [{"CODIGO": c, "CCLASSTRIB": v["CCLASSTRIB"], "CLASSIFICACAO_FISCAL": v["CLASSIFICACAO_FISCAL"]} for c, v in db.products.items()]
config = _config(firebird_writes=True, rtc_auto_apply_enabled=True)
summary = service._rtc_auto_apply({}, config, {"job_id": 9, "authorizations": {"rtc_auto_apply": True}}, products, fiscal, NORMAL)
assert summary["mode"] == "AUTO_APPLY" and summary["write_successes"] == 3 and summary["allowed_fields"] == ["CCLASSTRIB"] and all(a["job_id"] == 9 for a in summary["applied"])
assert [db.products[c]["CCLASSTRIB"] for c in ("000001", "000002", "000003")] == ["200003", "000001", "200003"]
def test_job_never_writes_when_the_erp_says_rtc_is_off_even_with_all_switches_open(monkeypatch):
service, _config = _service()
db = FakeFirebird(PRODUCTS, param=(0,))
fiscal = _fiscal_for(db)
monkeypatch.setattr(service, "open_write_connection", lambda *a, **k: (_ for _ in ()).throw(AssertionError("no write connection may be opened")))
products = [{"CODIGO": c, "CCLASSTRIB": None, "CLASSIFICACAO_FISCAL": "39235000"} for c in db.products]
summary = service._rtc_auto_apply({}, _config(firebird_writes=True, rtc_auto_apply_enabled=True), {"job_id": 1, "authorizations": {"rtc_auto_apply": True}}, products, fiscal, NORMAL)
assert summary["write_attempts"] == 0 and summary["ready_total"] == 0
class _Closeable:
def __init__(self, db):
self._db = db
def __getattr__(self, name):
return getattr(self._db, name)
def close(self):
pass
+4 -4
View File
@@ -69,14 +69,14 @@ def test_rtc_off_in_the_erp_is_out_of_scope_and_never_blocks():
def test_rtc_on_in_the_erp_brings_cclasstrib_into_the_same_findings():
out = uf.assess_from_connection(FakeConn(param=1), {"crt": "1"}, today=date(2026, 9, 24))
assert out["context"]["rtc_enabled"] is True
assert kinds(out["findings"]["1"]) == ["CCLASSTRIB_MISSING"] and kinds(out["findings"]["2"]) == []
assert kinds(out["findings"]["3"]) == ["CCLASSTRIB_MISSING"] # one queue, one verdict per product
assert kinds(out["findings"]["1"]) == ["CCLASSTRIB_MISSING_UNRESOLVED"] and kinds(out["findings"]["2"]) == []
assert kinds(out["findings"]["3"]) == ["CCLASSTRIB_MISSING_UNRESOLVED"] # one queue, one verdict per product
def test_simulation_flag_never_touches_the_erp_parameter():
conn = FakeConn(param=0)
on = uf.assess_from_connection(conn, {"crt": "1"}, rtc_override=True)
assert on["context"]["rtc_enabled"] is True and kinds(on["findings"]["1"]) == ["CCLASSTRIB_MISSING"]
assert on["context"]["rtc_enabled"] is True and kinds(on["findings"]["1"]) == ["CCLASSTRIB_MISSING_UNRESOLVED"]
assert on["context"]["rtc_param"] == "0" and on["context"]["rtc_source"] == "SIMULATION" and not any("UPDATE" in s.upper() for s in conn.cur.statements)
@@ -124,7 +124,7 @@ def test_toggle_between_syncs_is_followed_in_both_directions_without_any_cache()
assert first["context"]["rtc_enabled"] is False and not [b for f in first["findings"].values() for b in f["blockers"] if b["category"] == "RTC"]
conn.cur.param = 1 # the operator switches the reform on in the ERP between two syncs
second = uf.assess_from_connection(conn, {"crt": "1"})
assert second["context"]["rtc_enabled"] is True and kinds(second["findings"]["1"]) == ["CCLASSTRIB_MISSING"]
assert second["context"]["rtc_enabled"] is True and kinds(second["findings"]["1"]) == ["CCLASSTRIB_MISSING_UNRESOLVED"]
assert second["findings"]["1"]["input_fingerprint"] != first["findings"]["1"]["input_fingerprint"] # a new blocker fingerprint: the old (RTC off) state is not reused
conn.cur.param = 0 # and off again
third = uf.assess_from_connection(conn, {"crt": "1"})