diff --git a/.gitignore b/.gitignore
index 299c6dc..e5a785b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,4 @@ scripts/_*
*.gz
*.pkl
.pytmp/
+sources_cache/legal/
diff --git a/agent/scripts/run_agent_service.py b/agent/scripts/run_agent_service.py
index 9d0fd21..eebd886 100644
--- a/agent/scripts/run_agent_service.py
+++ b/agent/scripts/run_agent_service.py
@@ -30,8 +30,8 @@ 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.firebird_reader import _connect as _open_read_only, check_connection, read_products # noqa: E402
-from gti_fiscal_agent.rtc_pis_cofins import assess_from_connection # noqa: E402
+from gti_fiscal_agent import firebird_reader as _firebird_reader, 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
from central.app.operational_state import classify_product_state, summarize_operational_states # noqa: E402
@@ -187,16 +187,27 @@ def open_write_connection(env, database_path, firebird_writes=True):
)
-def _expanded_scope(env, legacy_states):
- """RTC + PIS/COFINS assessment (read-only, SELECT only). Isolated: a failure here never fails the job or changes legacy coverage."""
+def _unified_fiscal(env, company):
+ """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:
- connection = _open_read_only(env)
- try:
- return assess_from_connection(connection, legacy_states)
- finally:
- connection.close()
- except Exception as error: # noqa: BLE001
- return {"scope_version": "expanded-v1", "unavailable": type(error).__name__}
+ return _unified.assess_from_connection(connection, company)
+ finally:
+ connection.close()
+
+
+def _fiscal_blocked_products(products, findings):
+ """One queue entry per (product, blocker). Same shape as every other blocker; `field` is the dashboard category (PIS/COFINS/RTC)."""
+ out = []
+ for product in products:
+ code = str(product.get("CODIGO") or "")
+ found = findings.get(code) or {}
+ for blocker in found.get("blockers") or []:
+ out.append({"company": "", "product_code": code, "gtin": product.get("GTIN") or product.get("EAN_GTIN"), "description": product.get("PRODUTO"), "blocker_type": "BLOCKED_FISCAL_DATA",
+ "field": blocker["category"], "reason_code": blocker["type"], "evidence_summary": {"reason": blocker["reason"], "applicability": found.get("applicability")},
+ "input_fingerprint": found.get("input_fingerprint", "")})
+ return out
def execute_job(job, config, executor, state, env):
@@ -238,7 +249,8 @@ def execute_job(job, config, executor, state, env):
if write_connection is not None:
write_connection.close()
- operational = coverage = expanded_scope = None
+ operational = coverage = fiscal_context = None
+ fiscal = None
if sync_kind:
if cache:
for product, decision in zip(to_process, decisions):
@@ -247,14 +259,23 @@ def execute_job(job, config, executor, state, env):
all_decisions = [state.cached_decision(p["CODIGO"]) or {} for p in products]
else:
all_decisions = decisions
- product_states = [classify_product_state(p, d) for p, d in zip(products, all_decisions)]
- operational = summarize_operational_states(product_states)
coverage = build_job_coverage(env, client, products, all_decisions)
- expanded_scope = _expanded_scope(env, {s["product_code"]: s["state"] for s in product_states})
+ company = {"crt": ((coverage or {}).get("company_context") or {}).get("company_crt")}
+ fiscal = _unified_fiscal(env, company)
+ 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_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
+ summary["resolved_products"] = [c for c in summary["resolved_products"] if str(c) not in fiscal_codes]
result_summary = {
"coverage": coverage,
"operational": operational,
- "expanded_scope": expanded_scope,
+ "fiscal_context": fiscal_context,
"fiscal_mode": getattr(config, "fiscal_mode", None),
"release_version": env.get("AGENT_VERSION") or "1.1.7",
"products_total": len(products),
diff --git a/agent/src/gti_fiscal_agent/rtc_pis_cofins.py b/agent/src/gti_fiscal_agent/rtc_pis_cofins.py
deleted file mode 100644
index c0b3d1c..0000000
--- a/agent/src/gti_fiscal_agent/rtc_pis_cofins.py
+++ /dev/null
@@ -1,277 +0,0 @@
-"""READ-ONLY RTC (cClassTrib / CST IBS-CBS) and PIS/COFINS assessment of SGV products.
-
-Everything here is observation: capability detection, catalog integrity, deterministic validators, applicability and an
-EXPANDED coverage view that sits next to (never replaces) the legacy scope coverage. Nothing writes to Firebird, nothing
-fills a value, and no fiscal rule is invented: where local data cannot prove that a field applies, the answer is
-UNKNOWN_APPLICABILITY, not a blocker.
-
-Vocabulary kept apart on purpose: SCHEMA_SUPPORT (the column exists) / DATA_PRESENT (it has a value) /
-APPLICABILITY (the field applies to this product) / MANDATORYNESS (a missing value is a defect). Only the last one
-may block, and only when applicability is proven from local, reliable data.
-"""
-from __future__ import annotations
-
-import re
-
-LOCAL_CATALOG_ONLY = True # the RTC catalog used is the one stored in the client's own SGV database; no official dataset is integrated here
-
-PRODUCT_TABLE = "C000025"
-RTC_CCLASSTRIB_TABLE = "RTC_CCLASSTRIB"
-RTC_CST_TABLE = "RTC_CST"
-PIS_COFINS_COLUMNS = ("COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS", "PISCOFINS", "INCIDENCIA_PISCOFINS")
-PRODUCT_COLUMNS = ("CODIGO", "CCLASSTRIB") + PIS_COFINS_COLUMNS
-
-# Official CST PIS/COFINS codes (2 digits). Used for FORMAT/membership only, never to pick a value.
-OFFICIAL_PIS_COFINS_CST = frozenset([f"{n:02d}" for n in (*range(1, 10), 49, *range(50, 57), *range(60, 68), *range(70, 76), 98, 99)])
-
-# ---------------------------------------------------------------- capabilities -------------------------------------------------
-
-
-def detect_rtc_capabilities(schema: dict, *, param_value=None, catalog: dict | None = None, physical_fk: bool | None = None) -> dict:
- """schema: {TABLE: {COLUMN, ...}}. Fail-closed: anything not proven is False. Old databases without RTC simply report False."""
- product_cols = set(schema.get(PRODUCT_TABLE, ()))
- class_cols = set(schema.get(RTC_CCLASSTRIB_TABLE, ()))
- cst_cols = set(schema.get(RTC_CST_TABLE, ()))
- has_product = "CCLASSTRIB" in product_cols
- has_class = {"CCLASSTRIB", "CSTIBSCBS"} <= class_cols
- has_cst = "CSTIBSCBS" in cst_cols
- has_relationship = bool(has_product and has_class and has_cst) # column-level relationship: product -> class -> cst
- caps = {
- "HAS_PRODUCT_CCLASSTRIB": has_product,
- "HAS_RTC_CCLASSTRIB_TABLE": has_class,
- "HAS_RTC_CST_TABLE": has_cst,
- "HAS_RTC_RELATIONSHIP": has_relationship,
- "HAS_PHYSICAL_RTC_FK": bool(physical_fk),
- "RTC_ENABLED_BY_PARAM": None if param_value is None else str(param_value).strip() in {"1", "S", "T", "TRUE"},
- "RTC_CATALOG_VALID": False,
- "LOCAL_CATALOG_ONLY": LOCAL_CATALOG_ONLY,
- }
- if catalog is not None and has_class and has_cst:
- caps["RTC_CATALOG_VALID"] = catalog_integrity(catalog)["valid"]
- return caps
-
-
-def catalog_integrity(catalog: dict) -> dict:
- """catalog: {"classes": {cClassTrib: cst_or_None}, "cst": {code, ...}, "duplicates": int}. Pure."""
- classes, cst = catalog.get("classes", {}), set(catalog.get("cst", ()))
- bad_format = [c for c in classes if not re.fullmatch(r"\d{6}", str(c))]
- bad_cst_format = [c for c in cst if not re.fullmatch(r"\d{3}", str(c))]
- orphans = [c for c, s in classes.items() if s is None or str(s) not in cst]
- return {"classes": len(classes), "cst": len(cst), "orphan_classes": len(orphans), "bad_class_format": len(bad_format),
- "bad_cst_format": len(bad_cst_format), "duplicates": int(catalog.get("duplicates", 0)),
- "valid": bool(classes and cst and not orphans and not bad_format and not bad_cst_format and not catalog.get("duplicates"))}
-
-# ---------------------------------------------------------------- validators ---------------------------------------------------
-
-
-def _text(value) -> str:
- return "" if value is None else str(value).strip()
-
-
-def validate_cclasstrib(value, catalog: dict | None) -> str:
- """EMPTY | INVALID_FORMAT | NOT_IN_LOCAL_CATALOG | VALID_LOCAL | CST_MISSING | VALID_WITH_CST (deterministic, no inference)."""
- code = _text(value)
- if not code:
- return "EMPTY"
- if not re.fullmatch(r"\d{6}", code):
- return "INVALID_FORMAT"
- if not catalog or code not in catalog.get("classes", {}):
- return "NOT_IN_LOCAL_CATALOG"
- if not catalog.get("cst"):
- return "VALID_LOCAL" # code exists, but there is no CST table to verify its CST
- cst = catalog["classes"][code]
- return "VALID_WITH_CST" if cst is not None and str(cst) in catalog["cst"] else "CST_MISSING"
-
-
-def validate_pis_cofins_cst(value) -> str:
- code = _text(value)
- if not code:
- return "EMPTY"
- return "VALID" if code in OFFICIAL_PIS_COFINS_CST else "INVALID_FORMAT"
-
-
-def validate_pis_cofins_rate(value) -> str:
- text = _text(value)
- if not text:
- return "EMPTY"
- if not re.fullmatch(r"\d{1,3}([.,]\d{1,4})?", text):
- return "INVALID_RATE"
- return "VALID" if float(text.replace(",", ".")) <= 100 else "INVALID_RATE"
-
-
-def validate_pis_cofins(product: dict) -> dict:
- """Field-level validation for one product row. Flag consistency compares the ERP's own flag/incidence with its own codes."""
- out = {}
- for tax in ("PIS", "COFINS"):
- out[f"{tax}_ENTRY_CST"] = validate_pis_cofins_cst(product.get(f"COD_ENTRADA_{tax}"))
- out[f"{tax}_EXIT_CST"] = validate_pis_cofins_cst(product.get(f"COD_SAIDA_{tax}"))
- out[f"{tax}_RATE"] = validate_pis_cofins_rate(product.get(f"ALIQ_{tax}"))
- flag = _text(product.get("PISCOFINS")).upper()
- incidence = _text(product.get("INCIDENCIA_PISCOFINS")).upper()
- has_codes = any(_text(product.get(c)) for c in PIS_COFINS_COLUMNS[:6])
- special = incidence not in ("", "NENHUMA")
- inconsistent = (flag == "N" and (special or has_codes)) or (flag == "S" and incidence in ("", "NENHUMA"))
- out["FLAG_CONSISTENCY"] = "INCONSISTENT" if inconsistent else ("EMPTY" if not flag else "CONSISTENT")
- return out
-
-# ---------------------------------------------------------------- applicability -----------------------------------------------
-
-
-def rtc_applicability(caps: dict) -> str:
- """APPLICABLE only when the ERP itself is switched to the reform (PARAMETROS.USAR_REFORMA_TRIBUTARIA) AND the model exists.
- NOT_ENABLED_BY_ERP when the ERP says it does not use RTC; NOT_SUPPORTED when the schema cannot hold it; else UNKNOWN."""
- if not (caps.get("HAS_PRODUCT_CCLASSTRIB") and caps.get("HAS_RTC_RELATIONSHIP")):
- return "NOT_SUPPORTED"
- enabled = caps.get("RTC_ENABLED_BY_PARAM")
- if enabled is None:
- return "UNKNOWN_APPLICABILITY"
- return "APPLICABLE" if enabled else "NOT_ENABLED_BY_ERP"
-
-
-def pis_cofins_applicability(product: dict, schema_ok: bool) -> str:
- """Product-level PIS/COFINS defaults are treated as applicable only when the product itself carries evidence of them
- (a code, or a special incidence/flag). A product with no data and flag N / NENHUMA gives NO proof either way (documents
- take their CST from elsewhere), so it is UNKNOWN_APPLICABILITY - not complete and not blocked."""
- if not schema_ok:
- return "NOT_SUPPORTED"
- evidence = any(_text(product.get(c)) for c in PIS_COFINS_COLUMNS[:6]) or _text(product.get("PISCOFINS")).upper() == "S" \
- or _text(product.get("INCIDENCIA_PISCOFINS")).upper() not in ("", "NENHUMA")
- return "APPLICABLE" if evidence else "UNKNOWN_APPLICABILITY"
-
-# ---------------------------------------------------------------- blockers + expanded coverage ------------------------------
-
-
-def expanded_findings(product: dict, caps: dict, catalog: dict | None) -> dict:
- """{"blockers": [(TYPE, reason)], "unknown": [area], "notes": [...]}: specific blockers only where applicability is proven."""
- blockers, unknown = [], []
- rtc = rtc_applicability(caps)
- if rtc == "APPLICABLE":
- state = validate_cclasstrib(product.get("CCLASSTRIB"), catalog)
- if state == "EMPTY":
- blockers.append(("CCLASSTRIB_MISSING", "EMPTY"))
- elif state == "INVALID_FORMAT":
- blockers.append(("CCLASSTRIB_INVALID", state))
- elif state == "NOT_IN_LOCAL_CATALOG":
- blockers.append(("CCLASSTRIB_NOT_IN_CATALOG", state))
- elif state == "CST_MISSING":
- blockers.append(("RTC_CST_INVALID", state))
- elif rtc == "UNKNOWN_APPLICABILITY":
- unknown.append("RTC")
- schema_ok = caps.get("_PIS_COFINS_SCHEMA", False)
- pis_cofins = pis_cofins_applicability(product, schema_ok)
- if pis_cofins == "APPLICABLE":
- checks = validate_pis_cofins(product)
- for tax in ("PIS", "COFINS"):
- for slot, label in (("ENTRY_CST", "ENTRY"), ("EXIT_CST", "EXIT")):
- result = checks[f"{tax}_{slot}"]
- if result == "INVALID_FORMAT":
- blockers.append((f"{tax}_CST_INVALID", f"{label}:{result}"))
- if checks[f"{tax}_RATE"] == "INVALID_RATE":
- blockers.append((f"{tax}_RATE_INVALID", "INVALID_RATE"))
- exit_state = checks[f"{tax}_EXIT_CST"]
- if exit_state == "EMPTY" and checks[f"{tax}_RATE"] != "EMPTY":
- blockers.append((f"{tax}_CST_MISSING", "EXIT:EMPTY_WITH_RATE"))
- if checks["FLAG_CONSISTENCY"] == "INCONSISTENT":
- blockers.append(("PIS_COFINS_FLAG_INCONSISTENT", "FLAG_VS_DATA"))
- elif pis_cofins == "UNKNOWN_APPLICABILITY":
- unknown.append("PIS_COFINS")
- return {"blockers": blockers, "unknown": unknown, "rtc_applicability": rtc, "pis_cofins_applicability": pis_cofins}
-
-
-def expanded_scope_summary(products: list[dict], legacy_states: dict, caps: dict, catalog: dict | None) -> dict:
- """Legacy coverage is reported unchanged (LEGACY_SCOPE_COVERAGE); EXPANDED_FISCAL_SCOPE_COVERAGE adds RTC + PIS/COFINS.
- legacy_states: {product_code: state} from classify_product_state (COMPLETE/CORRECTED/BLOCKED_*/FAILED_TECHNICAL)."""
- legacy = {"complete": 0, "safe_blocked": 0, "failed": 0}
- expanded = {"complete": 0, "safe_blocked": 0, "failed": 0, "unknown_applicability": 0}
- blockers: dict[str, int] = {}
- blocked_rows = []
- caps = dict(caps)
- for product in products:
- code = str(product.get("CODIGO") or "")
- state = legacy_states.get(code, "COMPLETE")
- if state == "FAILED_TECHNICAL":
- legacy["failed"] += 1
- expanded["failed"] += 1
- continue
- if state.startswith("BLOCKED_"):
- legacy["safe_blocked"] += 1
- expanded["safe_blocked"] += 1 # blocked in the legacy scope stays blocked: the expanded scope only ADDS conditions
- continue
- legacy["complete"] += 1
- found = expanded_findings(product, caps, catalog)
- if found["blockers"]:
- expanded["safe_blocked"] += 1
- for kind, reason in found["blockers"]:
- blockers[kind] = blockers.get(kind, 0) + 1
- if len(blocked_rows) < 200:
- blocked_rows.append({"product_code": code, "type": kind, "reason": reason})
- elif found["unknown"]:
- expanded["unknown_applicability"] += 1
- else:
- expanded["complete"] += 1
- total = len(products)
- return {"scope_version": "expanded-v1", "local_catalog_only": LOCAL_CATALOG_ONLY, "total_products": total,
- "legacy_scope": legacy, "expanded_scope": expanded, "blockers_by_type": dict(sorted(blockers.items())),
- "blocker_samples": blocked_rows, "invariant_pass": sum(expanded.values()) == total and sum(legacy.values()) == total,
- "capabilities": {k: v for k, v in caps.items() if not k.startswith("_")}}
-
-# ---------------------------------------------------------------- read-only loader -------------------------------------------
-
-
-def load_rtc_context(cursor) -> tuple[dict, dict | None, dict]:
- """Read schema + catalog + ERP parameter through an already open READ ONLY cursor (SELECT only).
- Returns (schema, catalog|None, extras). Never raises for a database that lacks RTC."""
- schema: dict[str, set] = {}
- cursor.execute("SELECT TRIM(rf.RDB$RELATION_NAME), TRIM(rf.RDB$FIELD_NAME) FROM RDB$RELATION_FIELDS rf WHERE rf.RDB$RELATION_NAME IN ('C000025','RTC_CCLASSTRIB','RTC_CST','PARAMETROS')")
- for table, column in cursor.fetchall():
- schema.setdefault(table, set()).add(column)
- extras: dict = {"param_value": None, "physical_fk": False}
- if "USAR_REFORMA_TRIBUTARIA" in schema.get("PARAMETROS", ()):
- try:
- cursor.execute("SELECT FIRST 1 USAR_REFORMA_TRIBUTARIA FROM PARAMETROS")
- row = cursor.fetchone()
- extras["param_value"] = None if row is None or row[0] is None else row[0]
- except Exception: # noqa: BLE001 - unreadable parameter => applicability stays unknown
- extras["param_value"] = None
- try:
- cursor.execute("SELECT COUNT(*) FROM RDB$RELATION_CONSTRAINTS WHERE RDB$RELATION_NAME = 'C000025' AND RDB$CONSTRAINT_TYPE = 'FOREIGN KEY' AND RDB$CONSTRAINT_NAME CONTAINING 'CCLASSTRIB'")
- extras["physical_fk"] = cursor.fetchone()[0] > 0
- except Exception: # noqa: BLE001
- pass
- catalog = None
- if {"CCLASSTRIB", "CSTIBSCBS"} <= schema.get(RTC_CCLASSTRIB_TABLE, set()) and "CSTIBSCBS" in schema.get(RTC_CST_TABLE, set()):
- cursor.execute("SELECT TRIM(CCLASSTRIB), TRIM(CSTIBSCBS) FROM RTC_CCLASSTRIB")
- rows = cursor.fetchall()
- classes = {c: (s or None) for c, s in rows}
- cursor.execute("SELECT TRIM(CSTIBSCBS) FROM RTC_CST")
- cst = {r[0] for r in cursor.fetchall()}
- catalog = {"classes": classes, "cst": cst, "duplicates": len(rows) - len(classes)}
- return schema, catalog, extras
-
-
-def read_products_rtc_pis_cofins(cursor, schema: dict) -> list[dict]:
- """SELECT of only the columns that exist (old schemas keep working)."""
- have = schema.get(PRODUCT_TABLE, set())
- columns = [c for c in PRODUCT_COLUMNS if c in have]
- if "CODIGO" not in columns:
- return []
- cursor.execute(f"SELECT {', '.join(columns)} FROM {PRODUCT_TABLE} ORDER BY CODIGO")
- return [dict(zip(columns, row)) for row in cursor.fetchall()]
-
-
-def assess_from_connection(conn, legacy_states: dict) -> dict:
- """One read-only pass: capabilities -> catalog integrity -> expanded scope. Any failure degrades to an explicit unavailable
- marker; it never raises into the job and never touches the database beyond SELECT."""
- try:
- 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"])
- caps["_PIS_COFINS_SCHEMA"] = all(c in schema.get(PRODUCT_TABLE, ()) for c in PIS_COFINS_COLUMNS[:6])
- products = read_products_rtc_pis_cofins(cursor, schema)
- summary = expanded_scope_summary(products, legacy_states, caps, catalog)
- summary["catalog_integrity"] = catalog_integrity(catalog) if catalog else None
- summary["capabilities"]["PIS_COFINS_SCHEMA_VALID"] = caps["_PIS_COFINS_SCHEMA"]
- summary["data_present"] = {c: sum(1 for p in products if _text(p.get(c))) for c in ("CCLASSTRIB",) + PIS_COFINS_COLUMNS if any(c in p for p in products[:1])}
- return summary
- except Exception as error: # noqa: BLE001
- return {"scope_version": "expanded-v1", "unavailable": type(error).__name__}
diff --git a/agent/src/gti_fiscal_agent/unified_fiscal.py b/agent/src/gti_fiscal_agent/unified_fiscal.py
new file mode 100644
index 0000000..6835d37
--- /dev/null
+++ b/agent/src/gti_fiscal_agent/unified_fiscal.py
@@ -0,0 +1,102 @@
+"""READ-ONLY ERP loader for the unified fiscal completeness (PIS/COFINS applicability + RTC when the ERP has the reform on).
+
+The rules live in central/app/unified_fiscal_coverage.py (pure). This module only observes the ERP through an already open
+READ ONLY connection (SELECT only): schema capabilities, RTC catalog, PARAMETROS.USAR_REFORMA_TRIBUTARIA and the product columns.
+Nothing is written and no value is ever filled in.
+"""
+from __future__ import annotations
+
+import hashlib
+import json
+from datetime import date
+
+from central.app.unified_fiscal_coverage import PIS_COFINS_FIELDS, catalog_integrity, evaluate_product, 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")
+TRUTHY = {"1", "S", "T", "TRUE"}
+
+
+def detect_rtc_capabilities(schema: dict, *, param_value=None, catalog: dict | None = None, physical_fk: bool | None = None) -> dict:
+ """schema: {TABLE: {COLUMN, ...}}. Fail-closed: anything not proven is False. Old databases without RTC simply report False."""
+ product_cols = set(schema.get(PRODUCT_TABLE, ()))
+ class_cols = set(schema.get(RTC_CCLASSTRIB_TABLE, ()))
+ cst_cols = set(schema.get(RTC_CST_TABLE, ()))
+ has_product = "CCLASSTRIB" in product_cols
+ has_class = {"CCLASSTRIB", "CSTIBSCBS"} <= class_cols
+ has_cst = "CSTIBSCBS" in cst_cols
+ caps = {
+ "HAS_PRODUCT_CCLASSTRIB": has_product, "HAS_RTC_CCLASSTRIB_TABLE": has_class, "HAS_RTC_CST_TABLE": has_cst,
+ "HAS_RTC_RELATIONSHIP": bool(has_product and has_class and has_cst), "HAS_PHYSICAL_RTC_FK": bool(physical_fk),
+ "RTC_ENABLED_BY_PARAM": False if param_value is None else str(param_value).strip().upper() in TRUTHY,
+ "RTC_CATALOG_VALID": False,
+ }
+ if catalog is not None and has_class and has_cst:
+ caps["RTC_CATALOG_VALID"] = catalog_integrity(catalog)["valid"]
+ return caps
+
+
+def load_rtc_context(cursor) -> tuple[dict, dict | None, dict]:
+ """Schema + catalog + ERP parameter through a READ ONLY cursor. Never raises for a database that lacks RTC."""
+ schema: dict[str, set] = {}
+ cursor.execute("SELECT TRIM(rf.RDB$RELATION_NAME), TRIM(rf.RDB$FIELD_NAME) FROM RDB$RELATION_FIELDS rf WHERE rf.RDB$RELATION_NAME IN ('C000025','RTC_CCLASSTRIB','RTC_CST','PARAMETROS')")
+ for table, column in cursor.fetchall():
+ schema.setdefault(table, set()).add(column)
+ extras: dict = {"param_value": None, "physical_fk": False}
+ if "USAR_REFORMA_TRIBUTARIA" in schema.get("PARAMETROS", ()):
+ try:
+ cursor.execute("SELECT FIRST 1 USAR_REFORMA_TRIBUTARIA FROM PARAMETROS")
+ row = cursor.fetchone()
+ extras["param_value"] = None if row is None or row[0] is None else row[0]
+ except Exception: # noqa: BLE001 - unreadable parameter => RTC stays off (not enabled by the ERP)
+ extras["param_value"] = None
+ try:
+ cursor.execute("SELECT COUNT(*) FROM RDB$RELATION_CONSTRAINTS WHERE RDB$RELATION_NAME = 'C000025' AND RDB$CONSTRAINT_TYPE = 'FOREIGN KEY' AND RDB$CONSTRAINT_NAME CONTAINING 'CCLASSTRIB'")
+ extras["physical_fk"] = cursor.fetchone()[0] > 0
+ except Exception: # noqa: BLE001
+ pass
+ catalog = None
+ 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")
+ 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}
+ return schema, catalog, extras
+
+
+def read_products(cursor, schema: dict) -> list[dict]:
+ """SELECT of only the columns that exist (old schemas keep working)."""
+ have = schema.get(PRODUCT_TABLE, set())
+ columns = [c for c in PRODUCT_COLUMNS if c in have]
+ if "CODIGO" not in columns:
+ return []
+ cursor.execute(f"SELECT {', '.join(columns)} FROM {PRODUCT_TABLE} ORDER BY CODIGO")
+ 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:
+ """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)."""
+ 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"])
+ enabled = rtc_enabled(caps) if rtc_override is None else bool(rtc_override and caps["HAS_RTC_RELATIONSHIP"])
+ rtc = {"enabled": enabled, "catalog": catalog, "catalog_valid": caps["RTC_CATALOG_VALID"]}
+ today = today or date.today()
+ products = read_products(cursor, schema)
+ findings = {}
+ for p in products:
+ found = evaluate_product(p, company, rtc, today)
+ 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()
+ findings[str(p.get("CODIGO") or "")] = found
+ integrity = catalog_integrity(catalog) if catalog else None
+ return {"context": {"rtc_enabled": enabled, "rtc_supported": caps["HAS_RTC_RELATIONSHIP"], "rtc_catalog_valid": caps["RTC_CATALOG_VALID"], "rtc_param": extras["param_value"],
+ "catalog_integrity": integrity, "crt": (company or {}).get("crt"), "capabilities": caps, "products_evaluated": len(products)},
+ "findings": findings}
diff --git a/agent/tests/conftest.py b/agent/tests/conftest.py
index 791612f..f2d94b5 100644
--- a/agent/tests/conftest.py
+++ b/agent/tests/conftest.py
@@ -1,3 +1,24 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
+
+
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def _no_real_firebird_for_unified_fiscal(monkeypatch, request):
+ """The unified fiscal assessment opens its own READ ONLY connection; unit tests have no Firebird, so it is neutral here unless a test opts in."""
+ if request.node.get_closest_marker("real_unified_fiscal"):
+ return
+ from gti_fiscal_agent import firebird_reader, unified_fiscal
+
+ class _Conn:
+ def close(self):
+ pass
+ monkeypatch.setattr(firebird_reader, "_connect", lambda env: _Conn())
+ monkeypatch.setattr(unified_fiscal, "assess_from_connection", lambda conn, company, **kw: {"context": {"rtc_enabled": False, "rtc_supported": False, "rtc_catalog_valid": False, "products_evaluated": 0}, "findings": {}})
+
+
+def pytest_configure(config):
+ config.addinivalue_line("markers", "real_unified_fiscal: use the real unified fiscal assessment (with a fake cursor) instead of the neutral stub")
diff --git a/agent/tests/test_rtc_pis_cofins.py b/agent/tests/test_rtc_pis_cofins.py
deleted file mode 100644
index 752c042..0000000
--- a/agent/tests/test_rtc_pis_cofins.py
+++ /dev/null
@@ -1,134 +0,0 @@
-"""RTC (cClassTrib / CST IBS-CBS) and PIS/COFINS: capabilities, validators, applicability, legacy vs expanded coverage. Read-only."""
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
-
-from gti_fiscal_agent import rtc_pis_cofins as r # noqa: E402
-
-FULL_SCHEMA = {"C000025": {"CODIGO", "CCLASSTRIB", *r.PIS_COFINS_COLUMNS}, "RTC_CCLASSTRIB": {"CCLASSTRIB", "CSTIBSCBS"}, "RTC_CST": {"CSTIBSCBS"}}
-CATALOG = {"classes": {"000001": "000", "200003": "200", "999999": None}, "cst": {"000", "200"}, "duplicates": 0}
-
-
-def caps(schema=FULL_SCHEMA, param=0, catalog=CATALOG):
- c = r.detect_rtc_capabilities(schema, param_value=param, catalog=catalog, physical_fk=True)
- c["_PIS_COFINS_SCHEMA"] = all(col in schema.get("C000025", ()) for col in r.PIS_COFINS_COLUMNS[:6])
- return c
-
-
-def test_database_without_rtc_fails_closed_and_is_not_blocked():
- c = caps({"C000025": {"CODIGO"}}, param=None, catalog=None)
- assert not any(c[k] for k in ("HAS_PRODUCT_CCLASSTRIB", "HAS_RTC_CCLASSTRIB_TABLE", "HAS_RTC_CST_TABLE", "HAS_RTC_RELATIONSHIP", "RTC_CATALOG_VALID"))
- assert r.rtc_applicability(c) == "NOT_SUPPORTED"
- found = r.expanded_findings({"CODIGO": 1}, c, None)
- assert found["blockers"] == [] and found["unknown"] == []
-
-
-def test_partial_rtc_column_without_catalog_tables_is_not_a_relationship():
- c = caps({"C000025": {"CODIGO", "CCLASSTRIB"}}, param=1, catalog=None)
- assert c["HAS_PRODUCT_CCLASSTRIB"] and not c["HAS_RTC_RELATIONSHIP"] and not c["RTC_CATALOG_VALID"]
- assert r.rtc_applicability(c) == "NOT_SUPPORTED"
-
-
-def test_full_model_and_valid_catalog_capabilities():
- good = {"classes": {"000001": "000"}, "cst": {"000"}, "duplicates": 0}
- c = caps(catalog=good)
- assert c["HAS_RTC_RELATIONSHIP"] and c["RTC_CATALOG_VALID"] and c["HAS_PHYSICAL_RTC_FK"] and c["RTC_ENABLED_BY_PARAM"] is False and c["LOCAL_CATALOG_ONLY"]
- assert not caps(catalog=CATALOG)["RTC_CATALOG_VALID"] # a class pointing to a missing CST makes the catalog invalid
-
-
-def test_cclasstrib_validator_states():
- assert r.validate_cclasstrib(None, CATALOG) == "EMPTY" and r.validate_cclasstrib(" ", CATALOG) == "EMPTY"
- assert r.validate_cclasstrib("12A456", CATALOG) == "INVALID_FORMAT" and r.validate_cclasstrib("12345", CATALOG) == "INVALID_FORMAT"
- assert r.validate_cclasstrib("123456", CATALOG) == "NOT_IN_LOCAL_CATALOG"
- assert r.validate_cclasstrib("200003", CATALOG) == "VALID_WITH_CST"
- assert r.validate_cclasstrib("999999", CATALOG) == "CST_MISSING"
- assert r.validate_cclasstrib("000001", {"classes": {"000001": None}, "cst": set()}) == "VALID_LOCAL"
- assert r.validate_cclasstrib("000001", None) == "NOT_IN_LOCAL_CATALOG"
-
-
-def test_pis_cofins_validators():
- assert r.validate_pis_cofins_cst("") == "EMPTY" and r.validate_pis_cofins_cst(None) == "EMPTY"
- assert r.validate_pis_cofins_cst("01") == "VALID" and r.validate_pis_cofins_cst("73") == "VALID"
- assert r.validate_pis_cofins_cst("2") == "INVALID_FORMAT" and r.validate_pis_cofins_cst("10") == "INVALID_FORMAT"
- assert r.validate_pis_cofins_rate("0,65") == "VALID" and r.validate_pis_cofins_rate("3") == "VALID" and r.validate_pis_cofins_rate("") == "EMPTY"
- assert r.validate_pis_cofins_rate("abc") == "INVALID_RATE" and r.validate_pis_cofins_rate("150") == "INVALID_RATE"
- ok = r.validate_pis_cofins({"PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFASICA", "COD_SAIDA_PIS": "04", "COD_SAIDA_COFINS": "04"})
- assert ok["FLAG_CONSISTENCY"] == "CONSISTENT"
- bad = r.validate_pis_cofins({"PISCOFINS": "N", "INCIDENCIA_PISCOFINS": "NENHUMA", "COD_SAIDA_PIS": "01"})
- assert bad["FLAG_CONSISTENCY"] == "INCONSISTENT"
-
-
-def test_empty_pis_cofins_is_unknown_applicability_not_a_blocker():
- found = r.expanded_findings({"CODIGO": 1, "PISCOFINS": "N", "INCIDENCIA_PISCOFINS": "NENHUMA"}, caps(), CATALOG)
- assert found["blockers"] == [] and found["unknown"] == ["PIS_COFINS"] and found["rtc_applicability"] == "NOT_ENABLED_BY_ERP"
-
-
-def test_populated_pis_cofins_is_applicable_and_valid_data_completes():
- row = {"CODIGO": 1, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "SUBST.TRIBUTARIA", "COD_ENTRADA_PIS": "73", "COD_SAIDA_PIS": "01", "ALIQ_PIS": "0,65",
- "COD_ENTRADA_COFINS": "73", "COD_SAIDA_COFINS": "01", "ALIQ_COFINS": "3"}
- found = r.expanded_findings(row, caps(), CATALOG)
- assert found["blockers"] == [] and found["unknown"] == [] and found["pis_cofins_applicability"] == "APPLICABLE"
-
-
-def test_invalid_values_where_applicable_create_specific_blockers():
- row = {"CODIGO": 2, "PISCOFINS": "N", "INCIDENCIA_PISCOFINS": "NENHUMA", "COD_ENTRADA_PIS": "2", "COD_SAIDA_PIS": "2", "ALIQ_PIS": "2",
- "COD_ENTRADA_COFINS": "2", "COD_SAIDA_COFINS": "2", "ALIQ_COFINS": "x"}
- kinds = {k for k, _ in r.expanded_findings(row, caps(), CATALOG)["blockers"]}
- assert {"PIS_CST_INVALID", "COFINS_CST_INVALID", "COFINS_RATE_INVALID", "PIS_COFINS_FLAG_INCONSISTENT"} <= kinds
-
-
-def test_rtc_becomes_applicable_only_when_the_erp_enables_it():
- on = caps(param=1)
- assert r.rtc_applicability(on) == "APPLICABLE"
- assert {k for k, _ in r.expanded_findings({"CODIGO": 1}, on, CATALOG)["blockers"]} == {"CCLASSTRIB_MISSING"}
- assert {k for k, _ in r.expanded_findings({"CODIGO": 1, "CCLASSTRIB": "123456"}, on, CATALOG)["blockers"]} == {"CCLASSTRIB_NOT_IN_CATALOG"}
- assert {k for k, _ in r.expanded_findings({"CODIGO": 1, "CCLASSTRIB": "999999"}, on, CATALOG)["blockers"]} == {"RTC_CST_INVALID"}
- assert {k for k, _ in r.expanded_findings({"CODIGO": 1, "CCLASSTRIB": "12x"}, on, CATALOG)["blockers"]} == {"CCLASSTRIB_INVALID"}
- assert not [k for k, _ in r.expanded_findings({"CODIGO": 1, "CCLASSTRIB": "200003"}, on, CATALOG)["blockers"] if k.startswith(("CCLASS", "RTC"))]
- assert r.rtc_applicability(caps(param=None)) == "UNKNOWN_APPLICABILITY"
-
-
-def test_legacy_and_expanded_coverage_are_reported_side_by_side_and_close():
- products = [{"CODIGO": 1}, {"CODIGO": 2}, {"CODIGO": 3},
- {"CODIGO": 4, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFASICA", "COD_SAIDA_PIS": "01", "COD_SAIDA_COFINS": "01", "COD_ENTRADA_PIS": "73", "COD_ENTRADA_COFINS": "73"},
- {"CODIGO": 5, "COD_SAIDA_PIS": "2", "PISCOFINS": "S"}]
- states = {"1": "COMPLETE", "2": "BLOCKED_MISSING_EVIDENCE", "3": "FAILED_TECHNICAL", "4": "COMPLETE", "5": "CORRECTED"}
- out = r.expanded_scope_summary(products, states, caps(), CATALOG)
- assert out["legacy_scope"] == {"complete": 3, "safe_blocked": 1, "failed": 1}
- # 1 unknown (no PIS/COFINS evidence), 1 complete (#4 valid), blocked = legacy-blocked #2 + #5 (invalid CST), failed #3
- assert out["expanded_scope"] == {"complete": 1, "safe_blocked": 2, "failed": 1, "unknown_applicability": 1}
- assert out["invariant_pass"] and out["blockers_by_type"].get("PIS_CST_INVALID") == 1 and out["local_catalog_only"]
-
-
-class FakeCursor:
- def __init__(self, tables, rows):
- self.tables, self.rows, self.result = tables, rows, []
-
- def execute(self, sql):
- assert sql.lstrip().upper().startswith("SELECT") # read-only by construction
- if "RDB$RELATION_FIELDS" in sql:
- self.result = [(t, c) for t, cols in self.tables.items() for c in cols]
- elif "USAR_REFORMA_TRIBUTARIA" in sql:
- self.result = [(0,)]
- elif "RDB$RELATION_CONSTRAINTS" in sql:
- self.result = [(1,)]
- elif "FROM RTC_CCLASSTRIB" in sql:
- self.result = [("000001", "000")]
- elif "FROM RTC_CST" in sql:
- self.result = [("000",)]
- elif "FROM C000025" in sql:
- self.result = self.rows
-
- def fetchall(self):
- return self.result
-
- def fetchone(self):
- return self.result[0] if self.result else None
-
-
-def test_loader_reads_schema_catalog_and_param_with_select_only():
- cur = FakeCursor({"C000025": ["CODIGO", "CCLASSTRIB"], "RTC_CCLASSTRIB": ["CCLASSTRIB", "CSTIBSCBS"], "RTC_CST": ["CSTIBSCBS"], "PARAMETROS": ["USAR_REFORMA_TRIBUTARIA"]}, [(1, None)])
- schema, catalog, extras = r.load_rtc_context(cur)
- assert catalog == {"classes": {"000001": "000"}, "cst": {"000"}, "duplicates": 0} and extras == {"param_value": 0, "physical_fk": True}
- assert r.read_products_rtc_pis_cofins(cur, schema) == [{"CODIGO": 1, "CCLASSTRIB": None}]
diff --git a/agent/tests/test_unified_fiscal.py b/agent/tests/test_unified_fiscal.py
new file mode 100644
index 0000000..d75bb2c
--- /dev/null
+++ b/agent/tests/test_unified_fiscal.py
@@ -0,0 +1,110 @@
+import re
+from datetime import date
+
+import pytest
+
+from gti_fiscal_agent import unified_fiscal as uf
+
+pytestmark = pytest.mark.real_unified_fiscal
+
+SCHEMA_ROWS = ([("C000025", c) for c in ("CODIGO", "CCLASSTRIB", "COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS", "PISCOFINS", "INCIDENCIA_PISCOFINS")]
+ + [("RTC_CCLASSTRIB", c) for c in ("CCLASSTRIB", "CSTIBSCBS", "INICIO_VIGENCIA", "FIM_VIGENCIA")] + [("RTC_CST", "CSTIBSCBS")] + [("PARAMETROS", "USAR_REFORMA_TRIBUTARIA")])
+PRODUCT_COLS = ("CODIGO", "CCLASSTRIB", "COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS", "PISCOFINS", "INCIDENCIA_PISCOFINS")
+PRODUCTS = [("1", None, None, None, None, None, None, None, "N", "NENHUMA"), ("2", "200003", None, "04", None, None, "04", None, "S", "MONOFÁSICA"), ("3", None, "2", "2", "2", "2", "2", "2", "N", "NENHUMA")]
+CLASSES = [("000001", "000", date(2025, 5, 5), None), ("200003", "200", date(2025, 5, 5), None)]
+
+
+class FakeCursor:
+ """READ ONLY fake: answers only SELECTs and records every statement so tests can prove nothing else ran."""
+ def __init__(self, param=0, schema=SCHEMA_ROWS, products=PRODUCTS, classes=CLASSES, cst=(("000",), ("200",))):
+ self.param, self.schema, self.products, self.classes, self.cst, self.statements, self._rows = param, schema, products, classes, cst, [], []
+
+ def execute(self, sql, *args):
+ self.statements.append(sql)
+ text = " ".join(sql.split()).upper()
+ assert text.startswith("SELECT"), f"non-SELECT statement: {sql}"
+ if "FROM RDB$RELATION_FIELDS" in text:
+ self._rows = list(self.schema)
+ elif "FROM PARAMETROS" in text:
+ self._rows = [(self.param,)]
+ elif "RDB$RELATION_CONSTRAINTS" in text:
+ self._rows = [(1,)]
+ elif "FROM RTC_CCLASSTRIB" in text:
+ self._rows = list(self.classes)
+ elif "FROM RTC_CST" in text:
+ self._rows = list(self.cst)
+ elif "FROM C000025" in text:
+ self._rows = list(self.products)
+ else:
+ raise AssertionError(sql)
+
+ def fetchall(self):
+ return self._rows
+
+ def fetchone(self):
+ return self._rows[0] if self._rows else None
+
+
+class FakeConn:
+ def __init__(self, **kw):
+ self.cur = FakeCursor(**kw)
+
+ def cursor(self):
+ return self.cur
+
+
+def kinds(found):
+ return sorted({b["type"] for b in found["blockers"]})
+
+
+def test_rtc_off_in_the_erp_is_out_of_scope_and_never_blocks():
+ conn = FakeConn(param=0)
+ out = uf.assess_from_connection(conn, {"crt": "1"}, today=date(2026, 9, 24))
+ assert out["context"]["rtc_enabled"] is False and out["context"]["rtc_supported"] is True and out["context"]["rtc_catalog_valid"] is True
+ assert not [b for f in out["findings"].values() for b in f["blockers"] if b["category"] == "RTC"]
+ assert kinds(out["findings"]["1"]) == [] and kinds(out["findings"]["2"]) == [] and kinds(out["findings"]["3"]) == ["COFINS_CST_INVALID", "PIS_CST_INVALID"]
+ assert all(s.strip().upper().startswith("SELECT") for s in conn.cur.statements) # read-only: SELECT statements only
+
+
+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", "COFINS_CST_INVALID", "PIS_CST_INVALID"] # 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_param"] == 0 and not any("UPDATE" in s.upper() for s in conn.cur.statements)
+
+
+def test_old_schema_without_rtc_columns_reports_unsupported_and_still_evaluates_pis_cofins():
+ old = [("C000025", c) for c in ("CODIGO", "COD_SAIDA_PIS", "PISCOFINS", "INCIDENCIA_PISCOFINS")]
+ rows = [("1", None, "N", "NENHUMA"), ("2", "9", "N", "NENHUMA")]
+ out = uf.assess_from_connection(FakeConn(schema=old, products=rows, classes=[], cst=()), {"crt": "3"})
+ assert out["context"]["rtc_supported"] is False and out["context"]["rtc_enabled"] is False
+ assert kinds(out["findings"]["1"]) == [] and kinds(out["findings"]["2"]) == ["PIS_CST_INVALID"]
+
+
+@pytest.mark.parametrize("value,expected", [(None, False), (0, False), ("0", False), (1, True), ("1", True), ("S", True), ("N", False)])
+def test_parameter_values_are_read_fail_closed(value, expected):
+ caps = uf.detect_rtc_capabilities({"C000025": {"CCLASSTRIB"}, "RTC_CCLASSTRIB": {"CCLASSTRIB", "CSTIBSCBS"}, "RTC_CST": {"CSTIBSCBS"}}, param_value=value)
+ assert caps["RTC_ENABLED_BY_PARAM"] is expected
+
+
+def test_catalog_with_orphan_or_duplicate_is_invalid_and_flagged_only_when_rtc_is_on():
+ bad = FakeConn(param=1, classes=CLASSES + [("200003", "200", date(2025, 5, 5), None)]) # duplicate class
+ on = uf.assess_from_connection(bad, {"crt": "1"})
+ assert on["context"]["rtc_catalog_valid"] is False and kinds(on["findings"]["2"]) == ["RTC_CATALOG_INVALID"]
+ off = uf.assess_from_connection(FakeConn(param=0, classes=CLASSES + [("200003", "200", date(2025, 5, 5), None)]), {"crt": "1"})
+ assert not [b for f in off["findings"].values() for b in f["blockers"] if b["category"] == "RTC"]
+
+
+def test_input_fingerprint_changes_with_the_fiscal_values_and_the_rtc_switch():
+ a = uf.assess_from_connection(FakeConn(param=0), {"crt": "1"})["findings"]["1"]["input_fingerprint"]
+ b = uf.assess_from_connection(FakeConn(param=1), {"crt": "1"})["findings"]["1"]["input_fingerprint"]
+ changed = [("1", None, None, "99", None, None, None, None, "N", "NENHUMA")] + PRODUCTS[1:]
+ c = uf.assess_from_connection(FakeConn(param=0, products=changed), {"crt": "1"})["findings"]["1"]["input_fingerprint"]
+ assert len({a, b, c}) == 3 and re.fullmatch(r"[0-9a-f]{64}", a)
diff --git a/agent/tests/test_zero_touch_agent.py b/agent/tests/test_zero_touch_agent.py
index 2829b7a..2f513cf 100644
--- a/agent/tests/test_zero_touch_agent.py
+++ b/agent/tests/test_zero_touch_agent.py
@@ -146,3 +146,42 @@ def test_heartbeats_keep_flowing_while_a_long_job_runs(monkeypatch, tmp_path):
monkeypatch.setattr(service, "execute_job", lambda *a, **k: (time.sleep(0.4), ({"ok": 1}, None))[1])
service.run(_config(agent_id="2", poll_interval_seconds=0, state_db=str(tmp_path / "a.db")), {"AGENT_ID": "2"}, stopper=stopper)
assert beats.count("BUSY") >= 3 # heartbeats during the job, not only before it
+
+
+def _findings(**by_code):
+ out = {}
+ for code, kinds in by_code.items():
+ out[code] = {"blockers": [{"type": t, "category": c, "reason": "R"} for t, c in kinds], "applicability": {"PIS_COFINS": "REQUIRED", "RTC": "NOT_APPLICABLE"}, "input_fingerprint": "fp" + code, "needs_review": False}
+ return out
+
+
+def test_unified_fiscal_blockers_change_the_single_coverage_and_the_single_queue(env_setup, monkeypatch):
+ from gti_fiscal_agent import unified_fiscal
+ findings = _findings(**{"2": [("PIS_CST_INVALID", "PIS"), ("COFINS_CST_INVALID", "COFINS")]})
+ monkeypatch.setattr(unified_fiscal, "assess_from_connection", lambda conn, company, **kw: {"context": {"rtc_enabled": False, "rtc_supported": True, "rtc_catalog_valid": True}, "findings": findings})
+ hub, state = env_setup
+ result, error = run_job("INITIAL_SYNC", [product("1"), product("2"), product("3", ncm="")], state, monkeypatch)
+ assert error is None
+ op = result["operational"]
+ assert op["TOTAL_PRODUCTS"] == 3 and op["COMPLETE_PRODUCTS"] == 1 and op["SAFE_BLOCKED"] == 2 and op["BLOCKED_FISCAL_DATA"] == 1 # one number, one definition
+ assert op["COMPLETE_PRODUCTS"] + op["SAFE_BLOCKED"] + op["FAILED_TECHNICAL"] + op["UNCLASSIFIED"] == 3
+ queue = sorted((b["product_code"], b["field"], b["reason_code"]) for b in result["blocked_products"])
+ assert queue == [("2", "COFINS", "COFINS_CST_INVALID"), ("2", "PIS", "PIS_CST_INVALID"), ("3", "NCM", "INVALID_NCM")] # ONE queue: NCM and fiscal blockers together
+ assert result["resolved_products"] == ["1"] # a fiscal blocker keeps the product out of resolved
+ assert result["fiscal_context"]["rtc_enabled"] is False and result["fiscal_context"]["evaluated_all"] is True
+ assert "expanded_scope" not in result
+
+
+def test_rtc_context_is_reported_but_blockers_only_exist_when_the_evaluator_produced_them(env_setup, monkeypatch):
+ from gti_fiscal_agent import unified_fiscal
+ monkeypatch.setattr(unified_fiscal, "assess_from_connection", lambda conn, company, **kw: {"context": {"rtc_enabled": True, "rtc_supported": True, "rtc_catalog_valid": True}, "findings": _findings(**{"1": [("CCLASSTRIB_MISSING", "RTC")]})})
+ hub, state = env_setup
+ result, _ = run_job("REPROCESS_ALL", [product("1"), product("2")], state, monkeypatch)
+ assert [b["reason_code"] for b in result["blocked_products"]] == ["CCLASSTRIB_MISSING"] and result["blocked_products"][0]["field"] == "RTC"
+ assert result["fiscal_context"]["rtc_enabled"] is True and result["operational"]["BLOCKED_FISCAL_DATA"] == 1
+
+
+def test_partial_reprocess_does_not_claim_a_full_fiscal_evaluation(env_setup, monkeypatch):
+ hub, state = env_setup
+ result, _ = run_job("REPROCESS_PRODUCTS", [product("1")], state, monkeypatch)
+ assert result["fiscal_context"] is None and result["operational"] is None
diff --git a/central/app/api.py b/central/app/api.py
index 56faa1e..7833832 100644
--- a/central/app/api.py
+++ b/central/app/api.py
@@ -11,7 +11,7 @@ from .classification import ENGINE_VERSION, Classifier
from .config import settings
from .datasets.service import DatasetService
from .jobs import SYNC_KINDS, claim_next_job, complete_job, create_job, fail_job, schedule_automatic_sync
-from .blocked_products import list_blockers, resolve_blocker, resolve_product_blockers, upsert_blocker
+from .blocked_products import list_blockers, resolve_blocker, resolve_product_blockers, resolve_stale_fiscal_blockers, upsert_blocker
from .context_keys import validate_entry
from .models import AgentSnapshot, BlockedProduct, FiscalContextEntry, ClassificationCache, ClassificationRequest, ClassificationResult, Dataset, Installation, Job
from .events import record_event
@@ -422,10 +422,13 @@ def router(session_factory, bootstrap_token):
if job is None or job.agent_id != agent.id: raise HTTPException(404, "job not found")
result_summary = payload.get("result_summary", {})
installation = session.scalar(select(Installation).where(Installation.agent_id == agent.id))
+ pass_started = datetime.now(timezone.utc)
for blocker in result_summary.get("blocked_products", []):
upsert_blocker(session, agent.tenant_id, blocker, installation_id=installation.id if installation else None)
for product_code in result_summary.get("resolved_products", []):
resolve_product_blockers(session, agent.tenant_id, product_code, installation_id=installation.id if installation else None)
+ if installation is not None and (result_summary.get("fiscal_context") or {}).get("evaluated_all"):
+ resolve_stale_fiscal_blockers(session, agent.tenant_id, installation.id, pass_started)
if not complete_job(session, job, result_summary): raise HTTPException(409, "job is not RUNNING")
if installation is not None:
_apply_job_outcome(session, agent, installation, job, result_summary, failed=False)
diff --git a/central/app/blocked_products.py b/central/app/blocked_products.py
index 89cbc09..042a5fe 100644
--- a/central/app/blocked_products.py
+++ b/central/app/blocked_products.py
@@ -18,7 +18,7 @@ def upsert_blocker(session, tenant_id: int, payload: dict, *, installation_id: i
row = session.scalar(select(BlockedProduct).where(BlockedProduct.blocker_fingerprint == fingerprint))
now = datetime.now(timezone.utc)
if row is None:
- row = BlockedProduct(tenant_id=tenant_id, installation_id=installation_id, company=payload.get("company", ""), product_code=str(payload.get("product_code", "")), gtin=payload.get("gtin"), description=payload.get("description"), blocker_type=payload.get("blocker_type", "BLOCKED_MISSING_EVIDENCE"), field=payload.get("field", ""), reason_code=payload.get("reason_code", ""), evidence_summary=payload.get("evidence_summary") or {}, input_fingerprint=payload.get("input_fingerprint", ""), blocker_fingerprint=fingerprint)
+ row = BlockedProduct(tenant_id=tenant_id, installation_id=installation_id, company=payload.get("company", ""), product_code=str(payload.get("product_code", "")), gtin=payload.get("gtin"), description=payload.get("description"), blocker_type=payload.get("blocker_type", "BLOCKED_MISSING_EVIDENCE"), field=payload.get("field", ""), reason_code=payload.get("reason_code", ""), evidence_summary=payload.get("evidence_summary") or {}, input_fingerprint=payload.get("input_fingerprint", ""), blocker_fingerprint=fingerprint, first_seen_at=now, last_seen_at=now)
session.add(row)
else:
row.last_seen_at = now
@@ -73,3 +73,20 @@ def resolve_product_blockers(session, tenant_id: int, product_code: str, source:
if rows:
session.commit()
return len(rows)
+
+
+FISCAL_BLOCKER_FIELDS = ("PIS", "COFINS", "RTC")
+
+
+def resolve_stale_fiscal_blockers(session, tenant_id: int, installation_id: int, since: datetime) -> int:
+ """After a FULL evaluation of the unified fiscal fields, every open PIS/COFINS/RTC blocker of the installation that this pass did not
+ re-report (last_seen_at < since) no longer holds: the ERP data changed, or RTC was switched off. Other blocker fields are untouched."""
+ rows = session.scalars(select(BlockedProduct).where(BlockedProduct.tenant_id == tenant_id, BlockedProduct.installation_id == installation_id,
+ BlockedProduct.resolved_at.is_(None), BlockedProduct.field.in_(FISCAL_BLOCKER_FIELDS), BlockedProduct.last_seen_at < since)).all()
+ now = datetime.now(timezone.utc)
+ for row in rows:
+ row.resolved_at = now
+ row.resolution_source = "AUTO_REEVALUATION"
+ if rows:
+ session.commit()
+ return len(rows)
diff --git a/central/app/control_plane.py b/central/app/control_plane.py
index 45ba38d..5a34696 100644
--- a/central/app/control_plane.py
+++ b/central/app/control_plane.py
@@ -16,7 +16,7 @@ from .panel_presentation import health_reason_label, blocker_reason_label, block
HEALTH_STATES = ("ONLINE", "DEGRADED", "OFFLINE", "FAILED", "UNKNOWN")
-BLOCKER_FIELDS = ("CFOP", "ST", "ICMS", "CSOSN", "NCM", "OTHER")
+BLOCKER_FIELDS = ("NCM", "CEST", "CFOP", "ST", "ICMS", "CSOSN", "PIS", "COFINS", "RTC", "OTHER")
ATTENTION_CANDIDATE_LIMIT = 10
INACTIVE_ENROLLMENT_STATES = ("REVOKED", "FAILED", "EXPIRED")
diff --git a/central/app/operational_state.py b/central/app/operational_state.py
index 6cea7f2..dc8911d 100644
--- a/central/app/operational_state.py
+++ b/central/app/operational_state.py
@@ -6,7 +6,7 @@ import json
STATES = {
"COMPLETE", "CORRECTED", "BLOCKED_MISSING_EVIDENCE", "BLOCKED_CONFLICT",
- "BLOCKED_COMPANY_CONTEXT", "BLOCKED_SCHEMA", "FAILED_TECHNICAL",
+ "BLOCKED_COMPANY_CONTEXT", "BLOCKED_SCHEMA", "BLOCKED_FISCAL_DATA", "FAILED_TECHNICAL", "NEEDS_REVIEW",
}
@@ -17,7 +17,8 @@ def blocker_fingerprint(tenant: str, company: str, product_code: str, field: str
return hashlib.sha256(json.dumps(raw, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
-def classify_product_state(product: dict, decision: dict, *, write_status: str | None = None) -> dict:
+def classify_product_state(product: dict, decision: dict, *, write_status: str | None = None, fiscal: dict | None = None) -> dict:
+ """`fiscal` = unified_fiscal_coverage.evaluate_product(...): PIS/COFINS + (only when the ERP has RTC on) cClassTrib. One definition of COMPLETE."""
status = str(decision.get("status") or "").upper()
if write_status == "FAILED":
state = "FAILED_TECHNICAL"
@@ -33,7 +34,13 @@ def classify_product_state(product: dict, decision: dict, *, write_status: str |
state = "CORRECTED"
else:
state = "COMPLETE"
- return {"product_code": str(product.get("CODIGO") or ""), "state": state, "decision_status": status}
+ fiscal = fiscal or {}
+ if state in ("COMPLETE", "CORRECTED"):
+ if fiscal.get("blockers"):
+ state = "BLOCKED_FISCAL_DATA"
+ elif fiscal.get("needs_review"):
+ state = "NEEDS_REVIEW"
+ return {"product_code": str(product.get("CODIGO") or ""), "state": state, "decision_status": status, "fiscal_blockers": list(fiscal.get("blockers") or [])}
def should_retry_blocker(previous_input_fingerprint: str | None, current_input_fingerprint: str) -> bool:
@@ -44,6 +51,8 @@ def summarize_operational_states(states: list[dict]) -> dict:
counts = Counter(item.get("state") for item in states)
result = {state: counts.get(state, 0) for state in sorted(STATES)}
result["TOTAL_PRODUCTS"] = len(states)
+ result["UNCLASSIFIED"] = result["NEEDS_REVIEW"]
+ result["COMPLETE_PRODUCTS"] = result["COMPLETE"] + result["CORRECTED"]
result["SAFE_BLOCKED"] = sum(result[state] for state in STATES if state.startswith("BLOCKED_"))
result["AUTONOMOUSLY_PROCESSABLE_PRODUCTS"] = result["TOTAL_PRODUCTS"] - result["FAILED_TECHNICAL"]
result["AGENT_HEALTH"] = "HEALTHY" if result["FAILED_TECHNICAL"] == 0 else "DEGRADED"
diff --git a/central/app/panel_presentation.py b/central/app/panel_presentation.py
index 02221d2..d2239ee 100644
--- a/central/app/panel_presentation.py
+++ b/central/app/panel_presentation.py
@@ -80,7 +80,7 @@ def operational_priority(blocker) -> str:
return "CRITICAL"
if blocker_type in {"BLOCKED_MISSING_EVIDENCE", "MISSING_EVIDENCE"}:
return "HIGH"
- if field in {"CFOP", "ST", "ICMS", "CSOSN", "NCM"} or reason in {"INSUFFICIENT_EVIDENCE", "NO_EVIDENCE"}:
+ if field in {"CFOP", "ST", "ICMS", "CSOSN", "NCM", "CEST", "PIS", "COFINS", "RTC"} or reason in {"INSUFFICIENT_EVIDENCE", "NO_EVIDENCE"}:
return "NORMAL"
return "LOW"
@@ -120,16 +120,17 @@ def job_kind_label(value):
return _human_label(value, {"SYNC_INCREMENTAL": "Sincronização incremental", "INITIAL_SYNC": "Sincronização inicial", "REPROCESS_ALL": "Reprocessar tudo", "REPROCESS_PRODUCTS": "Reprocessar produtos"})
-_RTC_LABELS = {
- "CCLASSTRIB_MISSING": "cClassTrib ausente", "CCLASSTRIB_INVALID": "cClassTrib inválido", "CCLASSTRIB_NOT_IN_CATALOG": "cClassTrib fora do catálogo local",
- "RTC_CST_INVALID": "CST IBS/CBS inválido", "PIS_CST_MISSING": "CST PIS ausente", "PIS_CST_INVALID": "CST PIS inválido", "PIS_RATE_INVALID": "Alíquota PIS inválida",
- "COFINS_CST_MISSING": "CST COFINS ausente", "COFINS_CST_INVALID": "CST COFINS inválido", "COFINS_RATE_INVALID": "Alíquota COFINS inválida",
- "PIS_COFINS_FLAG_INCONSISTENT": "Flag PIS/COFINS inconsistente",
+_FISCAL_LABELS = {
+ "CCLASSTRIB_MISSING": "cClassTrib ausente", "CCLASSTRIB_INVALID_FORMAT": "cClassTrib com formato inválido", "CCLASSTRIB_NOT_IN_CATALOG": "cClassTrib fora do catálogo local",
+ "RTC_CST_MISSING": "CST IBS/CBS ausente no catálogo", "RTC_CATALOG_INVALID": "Catálogo RTC local inválido",
+ "PIS_CST_MISSING": "CST PIS ausente", "PIS_CST_INVALID": "CST PIS inválido", "PIS_RATE_MISSING": "Alíquota PIS ausente", "PIS_RATE_INVALID": "Alíquota PIS inválida", "PIS_CST_INCIDENCE_MISMATCH": "CST PIS incompatível com a incidência",
+ "COFINS_CST_MISSING": "CST COFINS ausente", "COFINS_CST_INVALID": "CST COFINS inválido", "COFINS_RATE_MISSING": "Alíquota COFINS ausente", "COFINS_RATE_INVALID": "Alíquota COFINS inválida", "COFINS_CST_INCIDENCE_MISMATCH": "CST COFINS incompatível com a incidência",
+ "PIS_COFINS_FLAG_INCONSISTENT": "Flag PIS/COFINS inconsistente", "BLOCKED_FISCAL_DATA": "Dado fiscal do cadastro",
}
def blocker_type_label(value):
- return _human_label(value, {"BLOCKED_MISSING_EVIDENCE": "Evidência fiscal ausente", "MISSING_EVIDENCE": "Evidência fiscal ausente", "TECHNICAL_FAILURE": "Falha técnica", "CONNECTION_ERROR": "Erro de conexão", "OTHER": "Outro", **_RTC_LABELS})
+ return _human_label(value, {"BLOCKED_MISSING_EVIDENCE": "Evidência fiscal ausente", "MISSING_EVIDENCE": "Evidência fiscal ausente", "TECHNICAL_FAILURE": "Falha técnica", "CONNECTION_ERROR": "Erro de conexão", "OTHER": "Outro", **_FISCAL_LABELS})
_REASON_LABELS = {
@@ -143,7 +144,7 @@ _REASON_LABELS = {
def blocker_reason_label(value):
- return _human_label(value, _REASON_LABELS)
+ return _human_label(value, {**_REASON_LABELS, **_FISCAL_LABELS})
def health_reason_label(value):
@@ -160,12 +161,10 @@ def priority_label(value):
return _human_label(value, {"CRITICAL": "Crítica", "HIGH": "Alta", "NORMAL": "Média", "MEDIUM": "Média", "LOW": "Baixa"})
-def expanded_scope_view(summary):
- """Panel-ready view of the Agent's `expanded_scope` job summary (legacy and expanded coverage side by side). None when absent/unavailable."""
- if not isinstance(summary, dict) or "expanded_scope" not in summary:
+def rtc_status_view(summary):
+ """Discreet Reforma Tributária status from the last completed job's `fiscal_context`. None when the Agent did not report it (older Agents)."""
+ context = (summary or {}).get("fiscal_context") if isinstance(summary, dict) else None
+ if not isinstance(context, dict) or "rtc_enabled" not in context:
return None
- legacy, expanded, total = summary.get("legacy_scope") or {}, summary["expanded_scope"] or {}, summary.get("total_products") or 0
- percent = lambda part: round(100 * part / total, 1) if total else None
- return {"total": total, "legacy": legacy, "expanded": expanded, "legacy_percent": percent(legacy.get("complete", 0)), "expanded_percent": percent(expanded.get("complete", 0)),
- "blockers": [{"type": key, "label": blocker_type_label(key), "count": count} for key, count in (summary.get("blockers_by_type") or {}).items()],
- "capabilities": summary.get("capabilities") or {}, "local_catalog_only": bool(summary.get("local_catalog_only"))}
+ return {"enabled": bool(context["rtc_enabled"]), "label": "Ativa" if context["rtc_enabled"] else "Desativada", "supported": bool(context.get("rtc_supported")),
+ "catalog_valid": bool(context.get("rtc_catalog_valid"))}
diff --git a/central/app/unified_fiscal_coverage.py b/central/app/unified_fiscal_coverage.py
new file mode 100644
index 0000000..fea3c17
--- /dev/null
+++ b/central/app/unified_fiscal_coverage.py
@@ -0,0 +1,211 @@
+"""UNIFIED fiscal completeness: ONE definition of "complete" for a product (pure, no I/O).
+
+mandatory(product, company, rtc) = base fiscal fields (decided elsewhere: NCM/CEST/CFOP/ICMS/IPI ...)
+ + PIS/COFINS when they APPLY to the product
+ + RTC (cClassTrib -> CST IBS/CBS) ONLY when the ERP has the tax reform switched on.
+
+There is no "legacy scope" and no "expanded scope": every finding here becomes a normal blocker in the single queue and
+changes the single coverage number. Nothing here writes to the ERP or invents a fiscal value.
+
+Sources (see docs/pis_cofins_applicability.md): SPED EFD-Contribuicoes table 4.3.3/4.3.4 (CST domain), 4.3.10-4.3.15 (special regimes),
+LC 123/2006 art. 13/18 (PIS/Cofins inside the Simples DAS), Leis 9.718/98, 10.637/2002, 10.833/2003 (cumulative / non-cumulative),
+NT 2025.002-RTC v1.51 (IBS/CBS fields), LC 214/2025 art. 348.
+"""
+from __future__ import annotations
+
+import re
+import unicodedata
+from datetime import date
+
+# Official CST PIS / CST COFINS domain (SPED tables 4.3.3 / 4.3.4). Membership only - never used to pick a value.
+CST_DOMAIN = frozenset([f"{n:02d}" for n in (*range(1, 10), 49, *range(50, 57), *range(60, 68), *range(70, 76), 98, 99)])
+RATE_REQUIRED_CST = frozenset({"01", "02"}) # taxable by percentage: a rate is needed
+RATE_NOT_APPLICABLE_CST = frozenset({"03", "04", "05", "06", "07", "08", "09"}) # per-unit amount / monophasic / ST / zero / exempt / no incidence / suspended
+INCIDENCE_CST = {"MONOFASICA": frozenset({"02", "03", "04"}), "SUBSTITUICAO": frozenset({"05"})} # 4.3.10/4.3.11 (02,03,04) and 4.3.12 (05)
+SIMPLES_FAMILY = frozenset({"1", "2", "4"}) # CRT 1 Simples, 2 Simples excess sublimit, 4 MEI (LC 123/2006): PIS/Cofins are collected inside the DAS
+
+PIS_COFINS_FIELDS = ("COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS")
+
+FIELD_BY_BLOCKER_PREFIX = (("PIS", "PIS"), ("COFINS", "COFINS"), ("CCLASSTRIB", "RTC"), ("RTC", "RTC"))
+
+
+def _text(value) -> str:
+ return "" if value is None else str(value).strip()
+
+
+def blocker_category(blocker_type: str) -> str:
+ """Dashboard category of a blocker type: PIS | COFINS | RTC | OTHER."""
+ for prefix, category in FIELD_BY_BLOCKER_PREFIX:
+ if blocker_type.startswith(prefix):
+ return category
+ return "OTHER"
+
+
+def incidence_kind(value) -> str:
+ """NONE | MONOFASICA | SUBSTITUICAO | UNKNOWN (accent/case-insensitive, encoding tolerant). Anything the SGV does not define is UNKNOWN, never NONE."""
+ raw = _text(value)
+ text = re.sub(r"[^A-Z]", "", unicodedata.normalize("NFKD", raw).encode("ascii", "ignore").decode().upper())
+ if not raw or text == "NENHUMA":
+ return "NONE"
+ if text.startswith("MONOF"):
+ return "MONOFASICA"
+ if text.startswith("SUBST"):
+ return "SUBSTITUICAO"
+ return "UNKNOWN"
+
+
+def validate_cst(value) -> str:
+ code = _text(value)
+ if not code:
+ return "EMPTY"
+ return "VALID" if code in CST_DOMAIN else "INVALID"
+
+
+def cst_suggestion(value) -> str | None:
+ """A short numeric code that becomes an official CST once zero-padded to two digits ('4' -> '04'). Only a hint for the operator; never applied."""
+ code = _text(value)
+ return code.zfill(2) if code.isdigit() and len(code) == 1 and code.zfill(2) in CST_DOMAIN else None
+
+
+def validate_rate(value) -> str:
+ text = _text(value)
+ if not text:
+ return "EMPTY"
+ if not re.fullmatch(r"\d{1,3}([.,]\d{1,4})?", text):
+ return "INVALID"
+ return "VALID" if float(text.replace(",", ".")) <= 100 else "INVALID"
+
+
+# ------------------------------------------------------------------------------------------------- PIS / COFINS
+
+
+def pis_cofins_applicability(product: dict, crt) -> dict:
+ """{"state": NOT_APPLICABLE | REQUIRED | OPTIONAL | NEEDS_REVIEW, "basis": ...}.
+
+ NOT_APPLICABLE the product carries no product-level PIS/COFINS treatment at all (flag N/blank, incidence NENHUMA/blank, all six fields blank).
+ The tax is then settled elsewhere: inside the DAS (Simples family) or from the company default (regime normal).
+ REQUIRED the product itself claims a special treatment (flag S, or incidence MONOFASICA/SUBSTITUICAO): the exit CST (and a rate when the CST needs one) must be valid.
+ OPTIONAL no special claim, but some CST/rate was typed: it is not mandatory, yet whatever is there must be valid.
+ NEEDS_REVIEW the incidence text is not one the SGV defines (NENHUMA / MONOFASICA / SUBST.TRIBUTARIA): applicability cannot be decided.
+ """
+ flag = _text(product.get("PISCOFINS")).upper()
+ kind = incidence_kind(product.get("INCIDENCIA_PISCOFINS"))
+ has_values = any(_text(product.get(c)) for c in PIS_COFINS_FIELDS)
+ if kind == "UNKNOWN":
+ return {"state": "NEEDS_REVIEW", "basis": "INCIDENCE_NOT_DEFINED_BY_SGV"}
+ if flag == "S" or kind in ("MONOFASICA", "SUBSTITUICAO"):
+ return {"state": "REQUIRED", "basis": "PRODUCT_DECLARES_SPECIAL_TREATMENT"}
+ if has_values:
+ return {"state": "OPTIONAL", "basis": "VALUES_TYPED_WITHOUT_SPECIAL_CLAIM"}
+ return {"state": "NOT_APPLICABLE", "basis": "SIMPLES_DAS" if _text(crt) in SIMPLES_FAMILY else "COMPANY_DEFAULT"}
+
+
+def evaluate_pis_cofins(product: dict, crt) -> dict:
+ """{"applicability": {...}, "blockers": [(type, reason)]}. Entry and exit are separate; a missing OPTIONAL value never blocks."""
+ app = pis_cofins_applicability(product, crt)
+ blockers: list[tuple[str, str]] = []
+ if app["state"] in ("NOT_APPLICABLE", "NEEDS_REVIEW"):
+ return {"applicability": app, "blockers": blockers}
+ kind = incidence_kind(product.get("INCIDENCIA_PISCOFINS"))
+ flag = _text(product.get("PISCOFINS")).upper()
+ for tax in ("PIS", "COFINS"):
+ entry, exit_, rate = product.get(f"COD_ENTRADA_{tax}"), product.get(f"COD_SAIDA_{tax}"), product.get(f"ALIQ_{tax}")
+ entry_state, exit_state, rate_state = validate_cst(entry), validate_cst(exit_), validate_rate(rate)
+ if entry_state == "INVALID":
+ blockers.append((f"{tax}_CST_INVALID", "ENTRY" + (f":UNPADDED_USE_{cst_suggestion(entry)}" if cst_suggestion(entry) else "")))
+ if exit_state == "INVALID":
+ blockers.append((f"{tax}_CST_INVALID", "EXIT" + (f":UNPADDED_USE_{cst_suggestion(exit_)}" if cst_suggestion(exit_) else "")))
+ if app["state"] == "REQUIRED" and exit_state == "EMPTY":
+ blockers.append((f"{tax}_CST_MISSING", "EXIT"))
+ if rate_state == "INVALID":
+ blockers.append((f"{tax}_RATE_INVALID", "RATE"))
+ if exit_state == "VALID":
+ code = _text(exit_)
+ if code in RATE_REQUIRED_CST and rate_state == "EMPTY":
+ blockers.append((f"{tax}_RATE_MISSING", f"CST_{code}_NEEDS_RATE"))
+ if kind in INCIDENCE_CST and code not in INCIDENCE_CST[kind]:
+ blockers.append((f"{tax}_CST_INCIDENCE_MISMATCH", f"{kind}:CST_{code}"))
+ if kind == "NONE" and app["state"] == "REQUIRED" and code in RATE_NOT_APPLICABLE_CST - {"03"} and flag == "S":
+ blockers.append((f"{tax}_CST_INCIDENCE_MISMATCH", f"NENHUMA:CST_{code}"))
+ if flag == "N" and kind != "NONE":
+ blockers.append(("PIS_COFINS_FLAG_INCONSISTENT", "FLAG_N_WITH_SPECIAL_INCIDENCE"))
+ if flag == "S" and kind == "NONE" and not any(_text(product.get(c)) for c in PIS_COFINS_FIELDS):
+ blockers.append(("PIS_COFINS_FLAG_INCONSISTENT", "FLAG_S_WITHOUT_INCIDENCE_OR_CODES"))
+ return {"applicability": app, "blockers": _dedupe(blockers)}
+
+
+def _dedupe(items):
+ seen, out = set(), []
+ for item in items:
+ if item not in seen:
+ seen.add(item)
+ out.append(item)
+ return out
+
+
+# ------------------------------------------------------------------------------------------------- RTC
+
+
+def rtc_enabled(caps: dict) -> bool:
+ """RTC is in scope ONLY when the ERP is switched to the reform (PARAMETROS.USAR_REFORMA_TRIBUTARIA truthy) AND its schema can hold it."""
+ return bool(caps.get("HAS_PRODUCT_CCLASSTRIB") and caps.get("HAS_RTC_RELATIONSHIP") and caps.get("RTC_ENABLED_BY_PARAM"))
+
+
+def catalog_integrity(catalog: dict) -> dict:
+ """catalog: {"classes": {cClassTrib: cst_or_None}, "cst": {code, ...}, "duplicates": int}."""
+ classes, cst = catalog.get("classes", {}), set(catalog.get("cst", ()))
+ bad_format = [c for c in classes if not re.fullmatch(r"\d{6}", str(c))]
+ bad_cst_format = [c for c in cst if not re.fullmatch(r"\d{3}", str(c))]
+ orphans = [c for c, s in classes.items() if s is None or str(s) not in cst]
+ return {"classes": len(classes), "cst": len(cst), "orphan_classes": len(orphans), "bad_class_format": len(bad_format), "bad_cst_format": len(bad_cst_format),
+ "duplicates": int(catalog.get("duplicates", 0)), "valid": bool(classes and cst and not orphans and not bad_format and not bad_cst_format and not catalog.get("duplicates"))}
+
+
+def validate_cclasstrib(value, catalog: dict | None, today: date | None = None) -> str:
+ """EMPTY | INVALID_FORMAT | NOT_IN_CATALOG | EXPIRED | CST_MISSING | VALID (CST IBS/CBS derived through the catalog, never stored on the product)."""
+ code = _text(value)
+ if not code:
+ return "EMPTY"
+ if not re.fullmatch(r"\d{6}", code):
+ return "INVALID_FORMAT"
+ if not catalog or code not in catalog.get("classes", {}):
+ return "NOT_IN_CATALOG"
+ validity = (catalog.get("validity") or {}).get(code)
+ if validity and today is not None:
+ start, end = validity
+ if (start and start > today) or (end and end < today):
+ return "EXPIRED"
+ cst = catalog["classes"][code]
+ return "VALID" if cst is not None and str(cst) in set(catalog.get("cst", ())) else "CST_MISSING"
+
+
+def evaluate_rtc(product: dict, rtc: dict | None, today: date | None = None) -> dict:
+ """rtc: {"enabled": bool, "catalog": {...}|None, "catalog_valid": bool}. Disabled => NOT_APPLICABLE and NO blocker, whatever CCLASSTRIB holds."""
+ if not rtc or not rtc.get("enabled"):
+ return {"applicability": {"state": "NOT_APPLICABLE", "basis": "RTC_DISABLED_IN_ERP"}, "blockers": []}
+ app = {"state": "REQUIRED", "basis": "RTC_ENABLED_IN_ERP"}
+ if not rtc.get("catalog_valid"):
+ return {"applicability": app, "blockers": [("RTC_CATALOG_INVALID", "LOCAL_CATALOG_FAILED_INTEGRITY")]}
+ state = validate_cclasstrib(product.get("CCLASSTRIB"), rtc.get("catalog"), today)
+ mapping = {"EMPTY": "CCLASSTRIB_MISSING", "INVALID_FORMAT": "CCLASSTRIB_INVALID_FORMAT", "NOT_IN_CATALOG": "CCLASSTRIB_NOT_IN_CATALOG", "EXPIRED": "CCLASSTRIB_NOT_IN_CATALOG", "CST_MISSING": "RTC_CST_MISSING"}
+ return {"applicability": app, "blockers": [(mapping[state], state)] if state in mapping else []}
+
+
+# ------------------------------------------------------------------------------------------------- unified
+
+
+def evaluate_product(product: dict, company: dict | None, rtc: dict | None, today: date | None = None) -> dict:
+ """The single fiscal-completeness verdict for the fields this module owns.
+ Returns {"blockers": [{"type","category","reason"}], "applicability": {"PIS_COFINS": state, "RTC": state}, "mandatory_fields": [...], "needs_review": bool}."""
+ crt = (company or {}).get("crt")
+ pis = evaluate_pis_cofins(product, crt)
+ rtc_result = evaluate_rtc(product, rtc, today)
+ blockers = [{"type": kind, "category": blocker_category(kind), "reason": reason} for kind, reason in pis["blockers"] + rtc_result["blockers"]]
+ mandatory = []
+ if pis["applicability"]["state"] == "REQUIRED":
+ mandatory += ["COD_SAIDA_PIS", "COD_SAIDA_COFINS"]
+ if rtc_result["applicability"]["state"] == "REQUIRED":
+ mandatory.append("CCLASSTRIB")
+ return {"blockers": blockers, "applicability": {"PIS_COFINS": pis["applicability"]["state"], "RTC": rtc_result["applicability"]["state"]},
+ "basis": {"PIS_COFINS": pis["applicability"]["basis"], "RTC": rtc_result["applicability"]["basis"]}, "mandatory_fields": mandatory,
+ "needs_review": pis["applicability"]["state"] == "NEEDS_REVIEW"}
diff --git a/central/app/web.py b/central/app/web.py
index 20c6567..4886961 100644
--- a/central/app/web.py
+++ b/central/app/web.py
@@ -15,7 +15,7 @@ from .config import settings
from .blocker_resolution import BlockerResolutionUnavailable, _company_key, resolve_blocker_with_context
from .context_keys import ALLOWED_KEYS, ENUMS
from .installation_identity import reference_agent_version
-from .panel_presentation import expanded_scope_view, job_kind_label, job_status_label
+from .panel_presentation import job_kind_label, job_status_label, rtc_status_view
from .control_plane import blocker_detail, blocker_filter_summary, dashboard_summary, installation_view, jobs_status_summary, list_blockers, list_installations, list_jobs, list_panel_events
from .human_auth import create_session, current_session, require_csrf, verify_password
from .panel_presentation import format_ui_datetime
@@ -296,7 +296,7 @@ def router(session_factory):
if installation.processing_enabled and last_sync is not None:
next_sync = last_sync + timedelta(seconds=installation.sync_interval_seconds)
last_summary = session.scalar(select(Job.result_summary).where(Job.agent_id == agent.id, Job.status == "COMPLETED", Job.kind.in_(("INITIAL_SYNC", "SYNC_INCREMENTAL", "REPROCESS_ALL"))).order_by(Job.id.desc()).limit(1))
- return {"expanded": expanded_scope_view(last_summary), "enabled": bool(installation.processing_enabled), "interval_minutes": installation.sync_interval_seconds // 60,
+ return {"rtc": rtc_status_view(last_summary), "enabled": bool(installation.processing_enabled), "interval_minutes": installation.sync_interval_seconds // 60,
"last_sync": format_ui_datetime(last_sync) if last_sync else None, "next_sync": format_ui_datetime(next_sync) if next_sync else None,
"last_changed": installation.last_sync_changed, "last_job": {"kind": last_job.kind, "status": last_job.status, "id": last_job.id, "kind_label": job_kind_label(last_job.kind), "status_label": job_status_label(last_job.status)} if last_job else None}
diff --git a/central/templates/agent_detail.html b/central/templates/agent_detail.html
index 8cf0022..85a98c8 100644
--- a/central/templates/agent_detail.html
+++ b/central/templates/agent_detail.html
@@ -8,11 +8,7 @@
Reprocessar tudo
{% if ops.enabled %}{% else %}{% endif %}
-Processamento {{ "Ativo" if ops.enabled else "Pausado" }} · sincronização automática a cada {{ ops.interval_minutes }} minÚltima sincronização {{ ops.last_sync or "ainda não sincronizado" }}{% if ops.last_changed is not none %} · {{ ops.last_changed }} produto(s) alterado(s){% endif %} Sincronização agendada {{ ops.next_sync or ("aguardando o primeiro sync" if ops.enabled else "pausado") }} Último job {% if ops.last_job %}{{ ops.last_job.kind_label }} · {{ ops.last_job.status_label }}{% else %}nenhum{% endif %}
-{% if ops.expanded %}Cobertura fiscal Escopo anterior e escopo ampliado (cClassTrib e PIS/COFINS quando aplicáveis). Catálogo RTC local do ERP.
-Escopo anterior {{ ops.expanded.legacy.complete }} completos · {{ ops.expanded.legacy.safe_blocked }} bloqueados · {{ ops.expanded.legacy.failed }} falhas{% if ops.expanded.legacy_percent is not none %} · {{ ops.expanded.legacy_percent|string|replace(".", ",") }}%{% endif %}
-Escopo ampliado {{ ops.expanded.expanded.complete }} completos · {{ ops.expanded.expanded.safe_blocked }} bloqueados · {{ ops.expanded.expanded.failed }} falhas · {{ ops.expanded.expanded.unknown_applicability }} com aplicabilidade desconhecida
-Novos bloqueios {% for b in ops.expanded.blockers %}{{ b.label }} · {{ b.count }} {% else %}nenhum{% endfor %} {% endif %}
+Processamento {{ "Ativo" if ops.enabled else "Pausado" }} · sincronização automática a cada {{ ops.interval_minutes }} minÚltima sincronização {{ ops.last_sync or "ainda não sincronizado" }}{% if ops.last_changed is not none %} · {{ ops.last_changed }} produto(s) alterado(s){% endif %} Sincronização agendada {{ ops.next_sync or ("aguardando o primeiro sync" if ops.enabled else "pausado") }} {% if ops.rtc %}Reforma Tributária {{ ops.rtc.label }} {% endif %}Último job {% if ops.last_job %}{{ ops.last_job.kind_label }} · {{ ops.last_job.status_label }}{% else %}nenhum{% endif %}
{% for name,label in [("overview","Resumo"),("fiscal","Fiscal"),("blockers","Blockers"),("jobs","Jobs"),("events","Eventos")] %}{{ label }} {% endfor %}
Contexto da empresa Cliente {{ detail.client or "Não informado" }} Nome de exibição {{ detail.display_name or "Não informado" }} Hostname {{ detail.hostname or "Não informado" }} Empresa {{ detail.company_name or detail.company_code or "Não informado" }} UF {{ detail.company_uf or "Não informado" }} CRT {{ detail.company_crt or "Não informado" }} Versão do agent {{ detail.agent_version_display }} Último heartbeat {{ detail.last_seen_display }} · {{ detail.last_seen_age }} Firebird {{ detail.firebird_version or "Não informado" }} · ODS {{ detail.firebird_ods or "Não informado" }} IDs técnicos Agent {{ detail.technical_ids.agent_id }} · instalação {{ detail.technical_ids.installation_db_id }}
{% for event in events[:8] %}{% else %}Nenhum evento.
{% endfor %}
diff --git a/central/templates/blockers.html b/central/templates/blockers.html
index 5af3ade..1c04d80 100644
--- a/central/templates/blockers.html
+++ b/central/templates/blockers.html
@@ -15,7 +15,7 @@
{% if reason_code_filter %} {% endif %}
Cliente
Instalação / servidor
- CampoTodos {% for value in ["CFOP","ST","ICMS","CSOSN","NCM","OTHER"] %}{{ value if value!="OTHER" else "Outros" }} {% endfor %}
+ CampoTodos {% for value in ["NCM","CEST","CFOP","ST","ICMS","CSOSN","PIS","COFINS","RTC","OTHER"] %}{{ value if value!="OTHER" else "Outros" }} {% endfor %}
Tipo de blocker
StatusAbertos Resolvidos Todos
VisualizaçãoOperação ativa Histórico e inativos
@@ -26,7 +26,7 @@
{% if field_filter or reason_code_filter or scope_filter=="all" or status_filter!="OPEN" %}Filtros ativos: {% if scope_filter=="all" %}Histórico e inativos {% endif %}{% if field_filter %}Campo: {{ field_filter }} {% endif %}{% if reason_code_filter %}Motivo: {{ reason_code_filter }} {% endif %}{% if status_filter!="OPEN" %}Status: {{ "Resolvidos" if status_filter=="RESOLVED" else "Todos" }} {% endif %}
{% endif %}
-
Distribuição dos blockers filtrados
+
Distribuição dos blockers filtrados
Fila de blockers · {{ result.total }} resultados Cliente / servidor Produto Campo / tipo Prioridade Problema Última ocorrência Ação
{% for item in result["items"] %}{{ item.client or "Cliente não identificado" }} {{ item.hostname or item.display_name or "Instalação não informada" }} {{ item.product_code }} {{ item.description or "Sem descrição" }} {{ item.field or "Outros" }}{{ item.blocker_type_label }} {{ item.priority_label }} {{ item.reason_label }}{{ item.age }} {{ item.last_seen_display }}{{ item.retry_count }} tentativas Ver detalhes {% else %}Nenhum blocker corresponde aos filtros. Consultar histórico {% endfor %}
diff --git a/central/templates/dashboard.html b/central/templates/dashboard.html
index 7266740..744f9f4 100644
--- a/central/templates/dashboard.html
+++ b/central/templates/dashboard.html
@@ -25,7 +25,7 @@
- {% for key in ["CFOP","ST","ICMS","CSOSN","NCM","OTHER"] %}
+ {% for key in ["NCM","CEST","CFOP","ST","ICMS","CSOSN","PIS","COFINS","RTC","OTHER"] %}
{% if summary.open_blocker_breakdown[key] > 0 %}{{ key }} {{ summary.open_blocker_breakdown[key] }} →
{% else %}{{ key }} 0
{% endif %}
{% endfor %}
diff --git a/central/tests/test_control_panel_pages.py b/central/tests/test_control_panel_pages.py
index a7fd750..91bc87e 100644
--- a/central/tests/test_control_panel_pages.py
+++ b/central/tests/test_control_panel_pages.py
@@ -536,3 +536,42 @@ def test_agent_detail_processing_hierarchy_and_status_badges():
assert "Mantém o heartbeat, mas não executa jobs" in page and "Recalcula todos os produtos" in page
assert "Saúde técnica: " not in page and "Status fiscal: " not in page # context lives in the small label, not in the badge
assert 'Saúde técnica ' in page
+
+
+def _complete_sync(client, agent_id, fiscal_context):
+ with client._session_factory() as session:
+ session.add(Job(agent_id=agent_id, kind="SYNC_INCREMENTAL", status="COMPLETED", payload={}, result_summary={"fiscal_context": fiscal_context}, created_at=datetime(2026, 9, 22, 19, 0, tzinfo=timezone.utc),
+ finished_at=datetime(2026, 9, 22, 19, 5, tzinfo=timezone.utc)))
+ session.commit()
+
+
+def test_agent_detail_has_one_coverage_and_shows_rtc_status_only_when_reported():
+ client, agent_id, _ = _panel_client()
+ plain = client.get(f"/agents/{agent_id}").text
+ assert "Reforma Tributária" not in plain # older Agents do not report it
+ for text in (plain,):
+ assert "Escopo anterior" not in text and "Escopo ampliado" not in text and "aplicabilidade desconhecida" not in text
+ _complete_sync(client, agent_id, {"rtc_enabled": False, "rtc_supported": True, "rtc_catalog_valid": True})
+ off = client.get(f"/agents/{agent_id}").text
+ assert re.search(r"Reforma Tributária]*>Desativada ", off) and "Escopo anterior" not in off and "Escopo ampliado" not in off
+ assert "cClassTrib" not in off # RTC off never surfaces RTC as a problem
+ _complete_sync(client, agent_id, {"rtc_enabled": True, "rtc_supported": True, "rtc_catalog_valid": True})
+ on = client.get(f"/agents/{agent_id}").text
+ assert re.search(r"Reforma Tributária]*>Ativa ", on)
+
+
+def test_pis_cofins_and_rtc_blockers_appear_in_the_same_queue_and_dashboard_breakdown():
+ client, agent_id, _ = _panel_client()
+ with client._session_factory() as session:
+ installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
+ for code, field, reason in (("P-010", "PIS", "PIS_CST_INVALID"), ("P-011", "COFINS", "COFINS_CST_MISSING"), ("P-012", "RTC", "CCLASSTRIB_MISSING")):
+ upsert_blocker(session, installation.tenant_id, {"company": "Acme", "product_code": code, "description": "x", "blocker_type": "BLOCKED_FISCAL_DATA", "field": field, "reason_code": reason, "input_fingerprint": code},
+ installation_id=installation.id)
+ dash = client.get("/").text
+ for key in ("PIS", "COFINS", "RTC", "CEST"):
+ assert f"{key} " in dash
+ queue = client.get("/blockers?scope=active&status=OPEN").text
+ assert "CST PIS inválido" in queue and "CST COFINS ausente" in queue and "cClassTrib ausente" in queue
+ assert "fila RTC" not in queue.lower()
+ filtered = client.get("/blockers?scope=active&status=OPEN&field=RTC").text
+ assert "cClassTrib ausente" in filtered and "CST PIS inválido" not in filtered
diff --git a/central/tests/test_operational_view_models.py b/central/tests/test_operational_view_models.py
index 98525c3..ee46489 100644
--- a/central/tests/test_operational_view_models.py
+++ b/central/tests/test_operational_view_models.py
@@ -108,7 +108,7 @@ def test_dashboard_reconciles_five_health_states_and_counts_distinct_safe_produc
assert summary["agents_total"] == sum(summary["health_counts"].values())
assert summary["safe_blocked_products"] == 2
assert summary["open_blockers"] == 3
- assert summary["open_blocker_breakdown"] == {"CFOP": 1, "ST": 0, "ICMS": 1, "CSOSN": 0, "NCM": 1, "OTHER": 0}
+ assert summary["open_blocker_breakdown"] == {"NCM": 1, "CEST": 0, "CFOP": 1, "ST": 0, "ICMS": 1, "CSOSN": 0, "PIS": 0, "COFINS": 0, "RTC": 0, "OTHER": 0}
assert summary["online"] == 1 and summary["safe_blocked"] == 2
@@ -576,12 +576,11 @@ def test_remaining_technical_enums_have_human_labels():
assert blocker_reason_label("SOMETHING_NEW") == "Something new" # unknown codes degrade readably, never crash
-def test_expanded_scope_view_labels_and_absence():
- from central.app.panel_presentation import expanded_scope_view
- assert expanded_scope_view(None) is None and expanded_scope_view({"coverage": {}}) is None
- view = expanded_scope_view({"total_products": 10, "legacy_scope": {"complete": 8, "safe_blocked": 2, "failed": 0},
- "expanded_scope": {"complete": 1, "safe_blocked": 3, "failed": 0, "unknown_applicability": 6},
- "blockers_by_type": {"PIS_CST_INVALID": 1, "CCLASSTRIB_MISSING": 2}, "local_catalog_only": True})
- assert view["legacy_percent"] == 80.0 and view["expanded_percent"] == 10.0 and view["local_catalog_only"]
- assert {b["label"] for b in view["blockers"]} == {"CST PIS inválido", "cClassTrib ausente"}
- assert blocker_type_label("RTC_CST_INVALID") == "CST IBS/CBS inválido" and blocker_type_label("COFINS_RATE_INVALID") == "Alíquota COFINS inválida"
+def test_rtc_status_view_is_discreet_and_optional():
+ from central.app.panel_presentation import rtc_status_view
+ assert rtc_status_view(None) is None and rtc_status_view({"coverage": {}}) is None
+ assert rtc_status_view({"fiscal_context": {"rtc_enabled": False}})["label"] == "Desativada"
+ on = rtc_status_view({"fiscal_context": {"rtc_enabled": True, "rtc_supported": True, "rtc_catalog_valid": True}})
+ assert on["label"] == "Ativa" and on["enabled"] and on["catalog_valid"]
+ assert blocker_type_label("RTC_CST_MISSING") == "CST IBS/CBS ausente no catálogo" and blocker_type_label("COFINS_RATE_INVALID") == "Alíquota COFINS inválida"
+ assert blocker_type_label("PIS_CST_INCIDENCE_MISMATCH") == "CST PIS incompatível com a incidência"
diff --git a/central/tests/test_unified_fiscal_coverage.py b/central/tests/test_unified_fiscal_coverage.py
new file mode 100644
index 0000000..f8984fe
--- /dev/null
+++ b/central/tests/test_unified_fiscal_coverage.py
@@ -0,0 +1,131 @@
+from datetime import date
+
+from central.app.operational_state import classify_product_state, summarize_operational_states
+from central.app.unified_fiscal_coverage import (blocker_category, evaluate_product, incidence_kind, rtc_enabled, validate_cclasstrib)
+
+CATALOG = {"classes": {"000001": "000", "200003": "200", "410004": "410", "999999": None, "555555": "555"}, "cst": {"000", "200", "410"}, "duplicates": 0}
+ON = {"enabled": True, "catalog": CATALOG, "catalog_valid": True}
+OFF = {"enabled": False, "catalog": CATALOG, "catalog_valid": True}
+SIMPLES, NORMAL = {"crt": "1"}, {"crt": "3"}
+EMPTY = {"CODIGO": "1", "PISCOFINS": "N", "INCIDENCIA_PISCOFINS": "NENHUMA"}
+
+
+def kinds(result):
+ return {b["type"] for b in result["blockers"]}
+
+
+# ---------------------------------------------------------------- RTC on / off
+def test_rtc_off_ignores_cclasstrib_completely():
+ for value in (None, "", "12x", "999999", "000001"):
+ r = evaluate_product({**EMPTY, "CCLASSTRIB": value}, SIMPLES, OFF)
+ assert r["blockers"] == [] and r["applicability"]["RTC"] == "NOT_APPLICABLE" and "CCLASSTRIB" not in r["mandatory_fields"]
+ assert evaluate_product(EMPTY, SIMPLES, None)["blockers"] == []
+
+
+def test_rtc_on_requires_and_validates_cclasstrib():
+ assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, SIMPLES, ON)) == {"CCLASSTRIB_MISSING"}
+ assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "12x"}, SIMPLES, ON)) == {"CCLASSTRIB_INVALID_FORMAT"}
+ assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "123456"}, SIMPLES, ON)) == {"CCLASSTRIB_NOT_IN_CATALOG"}
+ assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "999999"}, SIMPLES, ON)) == {"RTC_CST_MISSING"} # class without CST
+ assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "555555"}, SIMPLES, ON)) == {"RTC_CST_MISSING"} # class points to a CST that is not in RTC_CST (orphan)
+ ok = evaluate_product({**EMPTY, "CCLASSTRIB": "200003"}, SIMPLES, ON)
+ assert ok["blockers"] == [] and ok["applicability"]["RTC"] == "REQUIRED" and "CCLASSTRIB" in ok["mandatory_fields"]
+
+
+def test_rtc_on_with_invalid_catalog_blocks_with_a_single_clear_reason():
+ r = evaluate_product({**EMPTY, "CCLASSTRIB": "200003"}, SIMPLES, {"enabled": True, "catalog": CATALOG, "catalog_valid": False})
+ assert kinds(r) == {"RTC_CATALOG_INVALID"}
+
+
+def test_rtc_vigencia_is_used_only_when_the_catalog_carries_it():
+ cat = {**CATALOG, "validity": {"200003": (date(2027, 1, 1), None), "410004": (date(2025, 1, 1), date(2025, 12, 31))}}
+ assert validate_cclasstrib("200003", cat, date(2026, 9, 24)) == "EXPIRED" and validate_cclasstrib("410004", cat, date(2026, 9, 24)) == "EXPIRED"
+ assert validate_cclasstrib("000001", cat, date(2026, 9, 24)) == "VALID" and validate_cclasstrib("200003", CATALOG, date(2026, 9, 24)) == "VALID"
+
+
+def test_rtc_enabled_needs_schema_and_erp_switch():
+ caps = {"HAS_PRODUCT_CCLASSTRIB": True, "HAS_RTC_RELATIONSHIP": True, "RTC_ENABLED_BY_PARAM": True}
+ assert rtc_enabled(caps) and not rtc_enabled({**caps, "RTC_ENABLED_BY_PARAM": False}) and not rtc_enabled({**caps, "HAS_RTC_RELATIONSHIP": False}) and not rtc_enabled({})
+
+
+# ---------------------------------------------------------------- PIS / COFINS applicability
+def test_empty_product_is_not_applicable_in_every_regime_and_never_blocks():
+ for company, basis in ((SIMPLES, "SIMPLES_DAS"), ({"crt": "2"}, "SIMPLES_DAS"), ({"crt": "4"}, "SIMPLES_DAS"), (NORMAL, "COMPANY_DEFAULT"), ({"crt": None}, "COMPANY_DEFAULT"), (None, "COMPANY_DEFAULT")):
+ r = evaluate_product(EMPTY, company, OFF)
+ assert r["applicability"]["PIS_COFINS"] == "NOT_APPLICABLE" and r["basis"]["PIS_COFINS"] == basis and r["blockers"] == []
+ assert evaluate_product({"CODIGO": "1"}, SIMPLES, OFF)["blockers"] == [] # columns absent in an old schema: same answer
+
+
+def test_declared_monophasic_product_requires_a_valid_exit_cst():
+ good = {**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA", "COD_SAIDA_PIS": "04", "COD_SAIDA_COFINS": "04"}
+ r = evaluate_product(good, SIMPLES, OFF)
+ assert r["blockers"] == [] and r["applicability"]["PIS_COFINS"] == "REQUIRED" and set(r["mandatory_fields"]) == {"COD_SAIDA_PIS", "COD_SAIDA_COFINS"}
+ assert kinds(evaluate_product({**good, "COD_SAIDA_PIS": None}, SIMPLES, OFF)) == {"PIS_CST_MISSING"}
+ assert kinds(evaluate_product({**good, "COD_SAIDA_COFINS": "01", "ALIQ_COFINS": "7,6"}, SIMPLES, OFF)) == {"COFINS_CST_INCIDENCE_MISMATCH"}
+
+
+def test_st_incidence_with_taxable_cst_is_a_mismatch_and_pis_cofins_are_independent():
+ row = {**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "SUBST.TRIBUTÁRIA", "COD_ENTRADA_PIS": "73", "COD_SAIDA_PIS": "01", "ALIQ_PIS": "0,65", "COD_ENTRADA_COFINS": "73", "COD_SAIDA_COFINS": "05"}
+ assert kinds(evaluate_product(row, NORMAL, OFF)) == {"PIS_CST_INCIDENCE_MISMATCH"} # only PIS is wrong; COFINS 05 is right
+
+
+def test_value_2_is_invalid_format_not_a_test_product_and_not_silently_ignored():
+ row = {**EMPTY, "COD_ENTRADA_PIS": "2", "COD_SAIDA_PIS": "2", "ALIQ_PIS": "2", "COD_ENTRADA_COFINS": "2", "COD_SAIDA_COFINS": "2", "ALIQ_COFINS": "2"}
+ r = evaluate_product(row, SIMPLES, OFF)
+ assert r["applicability"]["PIS_COFINS"] == "OPTIONAL" and kinds(r) == {"PIS_CST_INVALID", "COFINS_CST_INVALID"}
+ assert "PIS_RATE_INVALID" not in kinds(r) # rate 2 is a valid number; only the CST codes are malformed
+
+
+def test_rate_rules_follow_the_cst():
+ base = {**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "NENHUMA"}
+ assert kinds(evaluate_product({**base, "COD_SAIDA_PIS": "01", "COD_SAIDA_COFINS": "01"}, NORMAL, OFF)) == {"PIS_RATE_MISSING", "COFINS_RATE_MISSING"}
+ assert kinds(evaluate_product({**base, "COD_SAIDA_PIS": "01", "ALIQ_PIS": "1,65", "COD_SAIDA_COFINS": "01", "ALIQ_COFINS": "7,6"}, NORMAL, OFF)) == set()
+ assert kinds(evaluate_product({**base, "COD_SAIDA_PIS": "49", "COD_SAIDA_COFINS": "49"}, SIMPLES, OFF)) == set()
+ assert kinds(evaluate_product({**base, "COD_SAIDA_PIS": "49", "ALIQ_PIS": "abc", "COD_SAIDA_COFINS": "49"}, SIMPLES, OFF)) == {"PIS_RATE_INVALID"}
+ assert kinds(evaluate_product({**base, "COD_SAIDA_PIS": "49", "ALIQ_PIS": "150", "COD_SAIDA_COFINS": "49"}, SIMPLES, OFF)) == {"PIS_RATE_INVALID"}
+
+
+def test_inconsistent_flags_and_unknown_incidence():
+ assert "PIS_COFINS_FLAG_INCONSISTENT" in kinds(evaluate_product({**EMPTY, "PISCOFINS": "N", "INCIDENCIA_PISCOFINS": "MONOFÁSICA", "COD_SAIDA_PIS": "04", "COD_SAIDA_COFINS": "04"}, SIMPLES, OFF))
+ assert "PIS_COFINS_FLAG_INCONSISTENT" in kinds(evaluate_product({**EMPTY, "PISCOFINS": "S"}, SIMPLES, OFF))
+ unknown = evaluate_product({**EMPTY, "INCIDENCIA_PISCOFINS": "OUTRA COISA"}, SIMPLES, OFF)
+ assert unknown["needs_review"] and unknown["blockers"] == [] and unknown["applicability"]["PIS_COFINS"] == "NEEDS_REVIEW"
+
+
+def test_incidence_text_is_encoding_and_accent_tolerant():
+ for text in ("MONOFÁSICA", "MONOFASICA", "MONOF�SICA", "monofásica"):
+ assert incidence_kind(text) == "MONOFASICA"
+ assert incidence_kind("SUBST.TRIBUTÁRIA") == "SUBSTITUICAO" and incidence_kind(None) == "NONE" and incidence_kind(" nenhuma ") == "NONE"
+
+
+# ---------------------------------------------------------------- ONE definition of complete, ONE queue
+def test_blocker_categories_feed_the_single_dashboard_breakdown():
+ assert {blocker_category(t) for t in ("PIS_CST_INVALID", "PIS_COFINS_FLAG_INCONSISTENT")} == {"PIS"} and blocker_category("COFINS_RATE_MISSING") == "COFINS"
+ assert {blocker_category(t) for t in ("CCLASSTRIB_MISSING", "RTC_CST_MISSING", "RTC_CATALOG_INVALID")} == {"RTC"} and blocker_category("XYZ") == "OTHER"
+
+
+def test_unified_coverage_invariant_and_rtc_toggle_changes_only_rtc_products():
+ products = [{**EMPTY, "CODIGO": "1"}, {**EMPTY, "CODIGO": "2", "COD_SAIDA_PIS": "2"}, {**EMPTY, "CODIGO": "3", "CCLASSTRIB": "200003"}, {**EMPTY, "CODIGO": "4", "INCIDENCIA_PISCOFINS": "???"}]
+
+ def run(rtc):
+ states = [classify_product_state(p, {"status": "EXACT_NCM"}, fiscal=evaluate_product(p, SIMPLES, rtc)) for p in products]
+ return summarize_operational_states(states)
+ off, on = run(OFF), run(ON)
+ for s in (off, on):
+ assert s["TOTAL_PRODUCTS"] == s["COMPLETE_PRODUCTS"] + s["SAFE_BLOCKED"] + s["FAILED_TECHNICAL"] + s["UNCLASSIFIED"] == 4
+ assert off["COMPLETE_PRODUCTS"] == 2 and off["BLOCKED_FISCAL_DATA"] == 1 and off["UNCLASSIFIED"] == 1 # RTC off: cClassTrib never counts
+ assert on["COMPLETE_PRODUCTS"] == 1 and on["BLOCKED_FISCAL_DATA"] == 3 and on["UNCLASSIFIED"] == 0 # RTC on: only product 3 has a valid cClassTrib; a real blocker outranks NEEDS_REVIEW
+
+
+def test_fiscal_blockers_never_hide_or_override_a_decision_blocker():
+ fiscal = evaluate_product({**EMPTY, "COD_SAIDA_PIS": "2"}, SIMPLES, OFF)
+ state = classify_product_state({"CODIGO": "1"}, {"status": "INVALID_NCM"}, fiscal=fiscal)
+ assert state["state"] == "BLOCKED_MISSING_EVIDENCE" # the NCM blocker stays the primary state
+ assert classify_product_state({"CODIGO": "1"}, {"status": "EXACT_NCM"}, fiscal=None)["state"] == "COMPLETE"
+
+
+def test_unpadded_single_digit_cst_is_flagged_with_the_official_code_as_a_hint_only():
+ row = {**EMPTY, "COD_SAIDA_PIS": "4", "COD_SAIDA_COFINS": "4"}
+ r = evaluate_product(row, NORMAL, OFF)
+ assert kinds(r) == {"PIS_CST_INVALID", "COFINS_CST_INVALID"} and any("UNPADDED_USE_04" in b["reason"] for b in r["blockers"])
+ assert evaluate_product({**EMPTY, "COD_SAIDA_PIS": "049"}, NORMAL, OFF)["blockers"][0]["reason"] == "EXIT" # 3 digits: ambiguous, no hint
diff --git a/central/tests/test_zero_touch_hub.py b/central/tests/test_zero_touch_hub.py
index a71591e..84a4a59 100644
--- a/central/tests/test_zero_touch_hub.py
+++ b/central/tests/test_zero_touch_hub.py
@@ -194,3 +194,45 @@ def test_panel_controls_require_login():
anon = TestClient(create_app(database_probe=lambda: True, session_factory=factory, bootstrap_token=None))
for action in ("sync-now", "pause", "resume"):
assert anon.post(f"/agents/{agent_id}/{action}", data={}, follow_redirects=False).status_code == 303 # bounced to login, nothing executed
+
+
+def _fiscal_blocker(code, field, reason, fp):
+ return {"company": "", "product_code": code, "gtin": None, "description": "P" + code, "blocker_type": "BLOCKED_FISCAL_DATA", "field": field, "reason_code": reason, "evidence_summary": {}, "input_fingerprint": fp}
+
+
+def test_fiscal_blockers_share_the_single_queue_and_are_resolved_by_the_next_full_pass():
+ """PIS/COFINS/RTC blockers are ordinary blockers; when a later FULL pass no longer reports them (data fixed / RTC switched off) they close automatically."""
+ from central.app.models import BlockedProduct
+ client, engine = _client()
+ hb(client)
+ operational = {**OPERATIONAL, "COMPLETE": 519, "BLOCKED_MISSING_EVIDENCE": 0, "BLOCKED_CONFLICT": 0, "BLOCKED_FISCAL_DATA": 2, "SAFE_BLOCKED": 2, "TOTAL_PRODUCTS": 521}
+ first = {"operational": operational, "blocked_products": [_fiscal_blocker("10", "PIS", "PIS_CST_INVALID", "a"), _fiscal_blocker("11", "RTC", "CCLASSTRIB_MISSING", "b")], "resolved_products": [],
+ "fiscal_context": {"rtc_enabled": True, "evaluated_all": True}}
+ finish(client, engine, first)
+ with Session(engine) as s:
+ rows = s.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_(None))).all()
+ assert sorted((r.product_code, r.field) for r in rows) == [("10", "PIS"), ("11", "RTC")]
+ snap = s.scalars(select(AgentSnapshot)).one()
+ assert (snap.fiscally_complete, snap.safe_blocked, snap.total_products) == (519, 2, 521)
+ age_last_sync(engine, 4000)
+ hb(client)
+ second = {"operational": {**OPERATIONAL, "COMPLETE": 520, "BLOCKED_MISSING_EVIDENCE": 0, "BLOCKED_CONFLICT": 0, "BLOCKED_FISCAL_DATA": 1, "SAFE_BLOCKED": 1},
+ "blocked_products": [_fiscal_blocker("10", "PIS", "PIS_CST_INVALID", "a")], "resolved_products": [], "fiscal_context": {"rtc_enabled": False, "evaluated_all": True}}
+ finish(client, engine, second)
+ with Session(engine) as s:
+ open_rows = s.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_(None))).all()
+ assert [(r.product_code, r.field) for r in open_rows] == [("10", "PIS")] # RTC blocker closed because RTC is off now
+ closed = s.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_not(None))).all()
+ assert [(r.product_code, r.resolution_source) for r in closed] == [("11", "AUTO_REEVALUATION")]
+
+
+def test_partial_reprocess_never_closes_fiscal_blockers_of_other_products():
+ from central.app.models import BlockedProduct
+ client, engine = _client()
+ hb(client)
+ finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [_fiscal_blocker("10", "PIS", "PIS_CST_INVALID", "a")], "resolved_products": [], "fiscal_context": {"evaluated_all": True}})
+ age_last_sync(engine, 4000)
+ hb(client)
+ finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": {"evaluated_all": False}})
+ with Session(engine) as s:
+ assert len(s.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_(None))).all()) == 1
diff --git a/docs/pis_cofins_applicability.md b/docs/pis_cofins_applicability.md
new file mode 100644
index 0000000..1d9e768
--- /dev/null
+++ b/docs/pis_cofins_applicability.md
@@ -0,0 +1,52 @@
+# PIS/COFINS — aplicabilidade no cadastro de produto (SGV)
+
+Implementação: `central/app/unified_fiscal_coverage.py` (`pis_cofins_applicability`, `evaluate_pis_cofins`). Somente leitura; nenhum valor é preenchido.
+`ACCESSED_AT` de todas as fontes: **2026-09-24** (hashes em `docs/sources/SOURCES_MANIFEST_UNIFIED_FISCAL_V1.json` e `docs/sources/SOURCES_MANIFEST_V3.json`).
+
+## 1. O que o campo do produto é (e o que não é)
+
+Campos: `COD_ENTRADA_PIS`, `COD_SAIDA_PIS`, `ALIQ_PIS`, `COD_ENTRADA_COFINS`, `COD_SAIDA_COFINS`, `ALIQ_COFINS`, `PISCOFINS` (N/S), `INCIDENCIA_PISCOFINS` (NENHUMA / MONOFÁSICA / SUBST.TRIBUTÁRIA).
+
+- São *defaults de cadastro* (tela Produtos > Config. Impostos > CST PIS/COFINS; binding provado por DFM em `docs/SGV_PRODUCT_FISCAL_UI_MAP_V1.json`). Os campos CST usam um seletor com a lista oficial "NN - descrição" (2 dígitos).
+- O SGV copia esses defaults para os itens de documentos de entrada (SQL de inserção do item no executável: `COD_SAIDA_PIS, COD_ENTRADA_PIS, COD_SAIDA_COFINS, COD_ENTRADA_COFINS, ... cst_ipi`).
+- Não são a tributação efetiva do documento. **DOCUMENT_PIS_COFINS_SOURCE**: em `NFCE_ITENS.CST_PIS_COFINS` o CST **não vem do cadastro do produto**. Evidência (TESTESOFICIAIS): os 3.336 itens com CST `01` são todos de dezembro/2022 (dia 01 a 31, sequência contínua de notas) e independentes do produto (produtos com `PISCOFINS=N`, campos vazios); antes (jun–nov/2022) e depois (2023–2026) os itens saem com CST em branco; o produto com CST `04`/MONOFÁSICA cadastrado emitiu em branco. Nas 4 bases de clientes (~570 mil itens) 100% saem em branco, mesmo para produtos com CST `04` no cadastro (RINCAO). O código do emissor de NFC-e não está disponível localmente (o executável SGV 5.0.6/5.1.0.4 não contém `CST_PIS_COFINS`), então a regra exata do documento **não é comprovável** aqui; o que é comprovado é que ela não lê o cadastro do produto.
+
+## 2. Modelo de aplicabilidade (por produto)
+
+| Estado | Condição (dados do próprio produto) | Efeito |
+|---|---|---|
+| `NOT_APPLICABLE` | flag N/vazio, incidência NENHUMA/vazia, os 6 campos vazios | não entra na completude; sem blocker |
+| `REQUIRED` | flag `S`, ou incidência MONOFÁSICA / SUBST.TRIBUTÁRIA | CST de **saída** válido obrigatório; alíquota conforme CST |
+| `OPTIONAL` | sem alegação especial, mas algum CST/alíquota digitado | nada é obrigatório; o que existir tem de ser válido |
+| `NEEDS_REVIEW` | incidência fora do vocabulário do SGV | único caso não decidível → "Não classificado" |
+
+Base de `NOT_APPLICABLE`: `SIMPLES_DAS` para CRT 1/2/4; `COMPANY_DEFAULT` para CRT 3 e demais (o produto sem tratamento próprio herda o padrão da empresa).
+
+### Por que é assim (fontes)
+
+1. **Simples Nacional (CRT 1, 2, 4)** — SOURCE Planalto; DOCUMENT LC 123/2006 compilada; ARTICLES art. 13, IV (Cofins) e V (PIS/Pasep): recolhidos mensalmente no DAS; art. 18, §4º-A, I: o contribuinte deve segregar a receita sujeita à tributação monofásica. Logo o produto sem regime especial não tem PIS/COFINS de documento a calcular; só a monofasia exige segregação — e ela é *declarada* pelo produto (flag S / incidência).
+2. **Regime normal (CRT 3)** — Lei 9.718/1998 art. 2º-3º (cumulativo, faturamento), Lei 10.637/2002 art. 1º (PIS não cumulativo), Lei 10.833/2003 art. 1º (Cofins não cumulativa). O regime (cumulativo/não cumulativo) é fato da empresa e alimenta o padrão da empresa, não o produto; por isso o produto vazio é `NOT_APPLICABLE (COMPANY_DEFAULT)` e o perfil da empresa continua sendo pergunta de empresa (`OPERATOR_COMPANY_QUESTIONS_V1.md`).
+3. **Regimes especiais por NCM** — SPED EFD-Contribuições, tabelas 4.3.10/4.3.11 (monofásica, CST 02/03/04), 4.3.12 (ST, CST 05), 4.3.13 (alíquota zero, CST 06), 4.3.14 (isenção, CST 07), 4.3.15 (sem incidência, CST 08) — versões 2026 baixadas de `sped.rfb.gov.br/arquivo/download/{1638,5786,1642,1643,1646,1651}` (`SPED_PISCOFINS_TABLES_V3.json`; 4.3.13 v1.34 de 16/04/2026). Lei 10.147/2000 (monofasia de medicamentos/perfumaria). Servem para *validar coerência* (CST × incidência) e para o motor de correção de produto; **não** tornam obrigatório preencher um produto que não declara nada, porque no SGV o campo não alimenta o documento (item 1).
+4. **Domínio de CST** — SPED tabelas 4.3.3 (CST-PIS) e 4.3.4 (CST-COFINS): 01–09, 49, 50–56, 60–67, 70–75, 98, 99. O seletor do SGV grava 2 dígitos.
+5. **Documento fiscal** — NT 2025.002-RTC v1.51 (Portal NF-e, publicada 04/08/2026), Grupos Q (PIS) e S (COFINS): o grupo é obrigatório no item da NF-e (rejeições 745/748), com exceções por finalidade. Isso vale para o **documento**; não obriga o cadastro do produto a ter o default preenchido.
+
+## 3. Regras de validação (blockers)
+
+Entrada e saída são avaliadas separadamente; PIS e COFINS são independentes.
+
+| Blocker | Quando |
+|---|---|
+| `{PIS,COFINS}_CST_INVALID` | CST de entrada ou saída digitado que não é um código oficial de 2 dígitos (ex.: `2`, `4`, `049`). Dica no motivo quando um dígito vira código oficial com zero à esquerda (`4` → `04`); nunca aplicada |
+| `{PIS,COFINS}_CST_MISSING` | produto `REQUIRED` sem CST de saída |
+| `{PIS,COFINS}_RATE_MISSING` | CST de saída 01/02 sem alíquota |
+| `{PIS,COFINS}_RATE_INVALID` | alíquota não numérica ou > 100 |
+| `{PIS,COFINS}_CST_INCIDENCE_MISMATCH` | incidência MONOFÁSICA com CST fora de {02,03,04}; SUBST.TRIBUTÁRIA com CST ≠ 05 |
+| `PIS_COFINS_FLAG_INCONSISTENT` | flag N com incidência especial; flag S sem incidência e sem códigos |
+
+Alíquota: CST 01/02 exigem percentual; 03 (valor por unidade) e 04–09 não têm percentual; demais CSTs, opcional (validada se existir).
+
+## 4. Regimes no corpus SGV
+Encontrados: CRT 1 (Simples: CONSTRUCAO, MMMODAS, UNIAO, TESTESOFICIAIS) e CRT 3 (RINCAO, via EMISSOR). O motor não assume CRT 1: a base de `NOT_APPLICABLE` e as regras de validade dependem apenas dos dados do produto; o CRT só escolhe a *base* (DAS × padrão da empresa) e será usado pelos perfis de empresa.
+
+## 5. Campos ignorados quando não aplicáveis
+Produto `NOT_APPLICABLE`: os 6 campos, `PISCOFINS` e `INCIDENCIA_PISCOFINS` não contam. Candidatos a *write target* futuro (não usados nesta versão, sem escrita): `COD_SAIDA_PIS`, `COD_SAIDA_COFINS` (a partir das tabelas 4.3.10–4.3.15, já aplicadas na missão Local Completion V2 nas cópias autorizadas).
diff --git a/docs/rtc_pis_cofins_model.md b/docs/rtc_pis_cofins_model.md
index d96dfa9..ea5add2 100644
--- a/docs/rtc_pis_cofins_model.md
+++ b/docs/rtc_pis_cofins_model.md
@@ -1,60 +1,42 @@
-# Modelo RTC + PIS/COFINS no SGV (evidência: TESTESOFICIAIS.FDB)
+# Reforma Tributária (RTC) no cadastro de produto — modelo condicional
-Tudo abaixo é observação read-only. Nenhum valor é preenchido, nenhum write é habilitado (`FIREBIRD_WRITES=0`).
-Código: `agent/src/gti_fiscal_agent/rtc_pis_cofins.py` (capabilities, validadores, aplicabilidade, cobertura ampliada),
-integrado ao job de sync do Agent (`expanded_scope` no `result_summary`) e exibido no detalhe do Agent.
+Substitui o modelo anterior de "escopo ampliado". Não existe segundo escopo, segundo painel ou segunda fila: RTC é **uma condição** da definição única de completude (`docs/unified_fiscal_completeness.md`).
-## 1. Modelo real do RTC
+## Regra definitiva
+`RTC_ENABLED = PARAMETROS.USAR_REFORMA_TRIBUTARIA truthy (1/S/T/TRUE) E o schema comporta RTC`
+(`C000025.CCLASSTRIB`, `RTC_CCLASSTRIB(CCLASSTRIB, CSTIBSCBS)`, `RTC_CST(CSTIBSCBS)`).
-`C000025.CCLASSTRIB` (VARCHAR(6)) → `RTC_CCLASSTRIB.CCLASSTRIB` (CHAR(6), PK) → `RTC_CCLASSTRIB.CSTIBSCBS` (CHAR(3)) → `RTC_CST.CSTIBSCBS` (CHAR(3), PK).
+- `RTC_ENABLED=false` (parâmetro 0, nulo, ausente ou schema antigo): `CCLASSTRIB` e CST IBS/CBS **não entram** na completude; nenhum blocker RTC; o painel só mostra "Reforma Tributária: Desativada".
+- `RTC_ENABLED=true`: RTC passa a ser obrigatório para todo produto.
-- **Há FK física** nos dois elos: `FK_C000025_CCLASSTRIB` (RESTRICT/RESTRICT) e `FK_RTC_CCLASSTRIB_CST`.
-- Catálogo local: 145 cClassTrib (6 dígitos, únicos, 0 duplicados, 0 órfãos) para 18 CST IBS/CBS (3 dígitos); nenhum registro com `FIM_VIGENCIA`; `INICIO_VIGENCIA` de 2025-05-05 a 2025-11-19. Cada CST tem ao menos um cClassTrib.
-- Atributos do catálogo (não são valores do produto): `TIPO_ALIQUOTA`, `PRED_IBS`/`PRED_CBS` (redução), `IND_TRIB_REGULAR`, `IND_CREDPRES_OPER`, `IND_ESTORNO_CRED`, monofasia, `IND_NFE/NFCE/...` (documentos aplicáveis), `ANEXO`, `LINK`. `RTC_CST` descreve o CST (`IND_IBSCBS`, `IND_REDBC`, `IND_REDALIQ`, `IND_DIF`, `IND_CREDPRES_IBSZFM`...).
-- **Não existem no produto**: CST IBS/CBS, alíquotas IBS/CBS, cCredPres. O CST IBS/CBS só se obtém **do catálogo, a partir do cClassTrib informado**.
-- `PARAMETROS.USAR_REFORMA_TRIBUTARIA = 0`: o ERP não usa RTC neste banco. Documentos (`C000062`, `C000032`, `C000088`, `NFCE_ITENS`) não têm campos RTC.
-- `LOCAL_CATALOG_ONLY=true`: comparamos apenas com o catálogo do próprio banco. Não há dataset oficial integrado; o ponto de extensão é `catalog` em `expanded_scope_summary`/`validate_cclasstrib`.
+Não há estado "desconhecido": um parâmetro ilegível conta como não habilitado pelo ERP (fail-closed para *escopo*, sem inflar cobertura porque o campo não é exigido).
-## 2. PIS/COFINS no produto (`C000025`)
+## Validação com RTC ativa
+`C000025.CCLASSTRIB → RTC_CCLASSTRIB.CSTIBSCBS → RTC_CST` (o CST IBS/CBS **não** é coluna do produto; é derivado pela relação).
-| Campo | Tipo | Semântica observada |
-|---|---|---|
-| `COD_ENTRADA_PIS` / `COD_ENTRADA_COFINS` | VARCHAR(3) | CST de entrada (ex.: `73`); 2/521 preenchidos |
-| `COD_SAIDA_PIS` / `COD_SAIDA_COFINS` | VARCHAR(3) | CST de saída (ex.: `01`, `04`); 3/521 |
-| `ALIQ_PIS` / `ALIQ_COFINS` | VARCHAR(8) | percentual **textual** com vírgula (`0,65`, `3`); 2 não vazios |
-| `PISCOFINS` | VARCHAR(1) | flag S/N (N=519, S=2) |
-| `INCIDENCIA_PISCOFINS` | VARCHAR(30) | regime adicional: NENHUMA=519, MONOFÁSICA=1, SUBST.TRIBUTÁRIA=1 (coerente com o flag) |
+| Blocker (PT-BR) | Condição |
+|---|---|
+| `CCLASSTRIB_MISSING` cClassTrib ausente | vazio |
+| `CCLASSTRIB_INVALID_FORMAT` cClassTrib com formato inválido | não tem exatamente 6 dígitos |
+| `CCLASSTRIB_NOT_IN_CATALOG` cClassTrib fora do catálogo local | inexistente em `RTC_CCLASSTRIB`, ou fora da vigência (`INICIO_VIGENCIA` futura / `FIM_VIGENCIA` passada — colunas do próprio catálogo) |
+| `RTC_CST_MISSING` CST IBS/CBS ausente no catálogo | a classe não aponta CST ou o CST não existe em `RTC_CST` |
+| `RTC_CATALOG_INVALID` Catálogo RTC local inválido | catálogo com órfãos, duplicatas ou formatos inválidos (todo produto, enquanto RTC ativa) |
-- CST oficial PIS/COFINS tem 2 dígitos; um produto traz `2` em todos os campos (formato inválido, aparenta dado de teste) e é o único com flag inconsistente (`N` com códigos).
-- Não existe catálogo PIS/COFINS no SGV (nenhuma tabela com PIS/COFINS/CST no nome além de `RTC_CST`). Os códigos são texto livre.
-- **Produto × documento.** `C000062` (itens de saída) grava `COD_*_PIS/COFINS` e alíquotas por item (7 de 693 com `20`/`41`/`12`); `NFCE_ITENS.CST_PIS_COFINS` tem `01` em 3.336 itens, inclusive de produtos com `INCIDENCIA=NENHUMA` e sem código no produto. Conclusão: o documento **não** depende do cadastro do produto para o CST. `PRODUCT_DEFAULT_MODEL` = campos esparsos em `C000025` (2–3 produtos); `DOCUMENT_ITEM_MODEL` = campos por item; `CANONICAL_SOURCE_FOR_PRODUCT_DEFAULT` = `C000025` somente onde preenchido, sem fonte canônica para os demais.
+Integridade do catálogo: sem órfãos (classe → CST inexistente), sem duplicatas, classes com 6 dígitos, CST com 3 dígitos. TESTESOFICIAIS: 145 classes, 18 CSTs, 0 órfãos, 0 duplicatas.
-## 3. Aplicabilidade (conservadora)
+## O que NÃO é validado
+O schema do produto não modela alíquota IBS/CBS, valor IBS/CBS nem `cCredPres`. Não existem blockers desses campos e nenhum write target foi inventado. A validação do produto reflete só o que o SGV guarda como default: `CCLASSTRIB`.
-Quatro coisas separadas: SCHEMA_SUPPORT, DATA_PRESENT, APPLICABILITY, MANDATORYNESS. Só a última bloqueia, e só com aplicabilidade provada por dado local.
+## Fontes (ACCESSED_AT 2026-09-24)
+- NT 2025.002-RTC v1.51, Portal Nacional da NF-e (publicada 04/08/2026): campos IBS/CBS (grupo UB) obrigatórios em 2026; para emitentes CRT 1/2/4 as orientações "serão publicadas em NT futura" porque a tributação IBS/CBS/IS desses contribuintes ocorre a partir de 2027 (art. 348 da LC 214/2025).
+- LC 214/2025 (Planalto, compilada): art. 348 (2026: IBS/CBS compensados com PIS/Cofins; alíquotas de teste).
+- Catálogo local do ERP (`RTC_CCLASSTRIB`/`RTC_CST`, `PUBLICACAO`/`INICIO_VIGENCIA`) e cClassTrib oficial no Hub (`cClassTrib 2026-06-22.xlsx`).
+- `PARAMETROS.USAR_REFORMA_TRIBUTARIA`: parâmetro do próprio SGV (executável 5.1.0.4 o lê; o 5.0.6.124 não tem RTC).
-- **RTC**: `NOT_SUPPORTED` (schema sem o modelo) · `NOT_ENABLED_BY_ERP` (parâmetro do ERP = 0; não bloqueia nem conta como desconhecido) · `UNKNOWN_APPLICABILITY` (parâmetro ilegível) · `APPLICABLE` (parâmetro = 1 e modelo existe → cClassTrib passa a ser exigido/validado).
-- **PIS/COFINS**: `APPLICABLE` só quando o próprio produto traz evidência (algum código/alíquota, `PISCOFINS='S'` ou incidência especial). Produto vazio com `N`/`NENHUMA` é `UNKNOWN_APPLICABILITY`: não é completo nem bloqueado.
+Nota fiscal vs. regra do produto: para CRT 1 a legislação só exige IBS/CBS no documento a partir de 2027; a regra deste motor segue o **interruptor do ERP** — quando o cliente liga a reforma no SGV, o cadastro passa a ser cobrado.
-## 4. Validadores (determinísticos, sem preenchimento)
+## Achado de corpus (somente leitura)
+A cópia local `TESTESOFICIAIS.FDB` tem `USAR_REFORMA_TRIBUTARIA=1` e 0 produtos com `CCLASSTRIB`, enquanto o piloto confirmado pelo operador tem `0`. Nada foi alterado no arquivo; o resultado do piloto é reproduzido por simulação pura (`RTC_ENABLED=false`, `scripts/unified_coverage_e2e.py`).
-- cClassTrib: `EMPTY`, `INVALID_FORMAT` (≠ 6 dígitos), `NOT_IN_LOCAL_CATALOG`, `VALID_LOCAL`, `CST_MISSING`, `VALID_WITH_CST`.
-- CST PIS/COFINS: `EMPTY`, `VALID`, `INVALID_FORMAT` (fora do conjunto oficial 01–09, 49, 50–56, 60–67, 70–75, 98, 99). Alíquota: `EMPTY`, `VALID` (0–100, vírgula ou ponto), `INVALID_RATE`. Flag: `CONSISTENT`/`INCONSISTENT`/`EMPTY`.
-- Blockers específicos (rótulos PT-BR no painel): `CCLASSTRIB_MISSING/INVALID/NOT_IN_CATALOG`, `RTC_CST_INVALID`, `PIS|COFINS_CST_MISSING/INVALID`, `PIS|COFINS_RATE_INVALID`, `PIS_COFINS_FLAG_INCONSISTENT`. Eles ficam em `expanded_scope.blockers_by_type`, **separados** dos blockers legados (não alteram o invariante 495+26+0=521 do escopo anterior).
-
-## 5. Cobertura: legado × ampliada
-
-`legacy_scope` (LEGACY_SCOPE_COVERAGE) é preservado sem alteração. `expanded_scope` (EXPANDED_FISCAL_SCOPE_COVERAGE) só **acrescenta** condições: bloqueado/falha legado continua assim; completo legado vira completo ampliado, bloqueado (blocker específico) ou `unknown_applicability`. Invariante: soma = total.
-
-Resultado no TESTESOFICIAIS (521 produtos): legado 495 completos / 26 bloqueados / 0 falhas; ampliado 2 completos / 27 bloqueados / 0 falhas / 492 com aplicabilidade desconhecida; blockers novos: `PIS_CST_INVALID` 2, `COFINS_CST_INVALID` 2, `PIS_COFINS_FLAG_INCONSISTENT` 1 (mesmo produto). Os 492 desconhecidos **não** são defeitos: faltam evidências para dizer se o cadastro do produto deve trazer CST de PIS/COFINS.
-
-## 6. Limitações
-
-- Sem catálogo oficial RTC nem regra tributária: nenhuma correspondência é inventada.
-- A aplicabilidade de PIS/COFINS por regime (CRT 1 aqui) não está determinada por dado local confiável.
-- Vigência do cClassTrib não é usada como bloqueio (o catálogo local não tem `FIM_VIGENCIA`).
-
-## 7. Campos candidatos a write futuro (NÃO autorizado)
-
-`CANDIDATE`, sem write habilitado e sem autorização: `C000025.CCLASSTRIB`, `COD_ENTRADA_PIS`, `COD_SAIDA_PIS`, `ALIQ_PIS`, `COD_ENTRADA_COFINS`, `COD_SAIDA_COFINS`, `ALIQ_COFINS`.
-Não devem ser escritos: `RTC_CCLASSTRIB`, `RTC_CST` (catálogos), `PARAMETROS.USAR_REFORMA_TRIBUTARIA`, `PISCOFINS`/`INCIDENCIA_PISCOFINS` sem evidência de regime, campos de documentos fiscais, e qualquer CST IBS/CBS "inventado" no produto.
+## Testes
+RTC off/on, `CCLASSTRIB` vazio/válido/inválido/órfão/expirado, catálogo inválido: `central/tests/test_unified_fiscal_coverage.py`, `agent/tests/test_unified_fiscal.py`.
diff --git a/docs/runs/unified_coverage_e2e.json b/docs/runs/unified_coverage_e2e.json
new file mode 100644
index 0000000..c2c5895
--- /dev/null
+++ b/docs/runs/unified_coverage_e2e.json
@@ -0,0 +1,630 @@
+{
+ "database": "TESTESOFICIAIS",
+ "crt": "1",
+ "products": 521,
+ "fiscal_products_read": 521,
+ "BEFORE": {
+ "COMPLETE_PRODUCTS": 495,
+ "SAFE_BLOCKED_PRODUCTS": 26,
+ "COVERAGE_PERCENT": 95.01,
+ "UNKNOWN_APPLICABILITY_previous_model": 518
+ },
+ "runs": {
+ "ERP_AS_FOUND": {
+ "rtc_enabled": true,
+ "TOTAL_PRODUCTS": 521,
+ "COMPLETE_PRODUCTS": 0,
+ "SAFE_BLOCKED_PRODUCTS": 521,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 0.0,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 2,
+ "COFINS_BLOCKERS": 2,
+ "RTC_BLOCKERS": 521,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {
+ "CCLASSTRIB_MISSING": 521,
+ "COFINS_CST_INCIDENCE_MISMATCH": 1,
+ "COFINS_CST_INVALID": 1,
+ "PIS_CST_INCIDENCE_MISMATCH": 1,
+ "PIS_CST_INVALID": 1
+ },
+ "applicability": {
+ "NOT_APPLICABLE": 518,
+ "REQUIRED": 2,
+ "OPTIONAL": 1
+ },
+ "rtc_applicability": {
+ "REQUIRED": 521
+ },
+ "changed_products": 495,
+ "examples": [
+ {
+ "product": "000004",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000005",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000006",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000008",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000009",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000010",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000011",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000013",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000015",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000017",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000018",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000019",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000021",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000022",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000023",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000024",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000025",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000026",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000027",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000028",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000029",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000032",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000033",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000034",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000035",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000036",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000038",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000040",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000041",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000042",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ }
+ ],
+ "context": {
+ "rtc_enabled": true,
+ "rtc_supported": true,
+ "rtc_catalog_valid": true,
+ "rtc_param": 1,
+ "catalog_integrity": {
+ "classes": 145,
+ "cst": 18,
+ "orphan_classes": 0,
+ "bad_class_format": 0,
+ "bad_cst_format": 0,
+ "duplicates": 0,
+ "valid": true
+ },
+ "crt": "1",
+ "products_evaluated": 521
+ }
+ },
+ "RTC_OFF_SIMULATION": {
+ "rtc_enabled": false,
+ "TOTAL_PRODUCTS": 521,
+ "COMPLETE_PRODUCTS": 493,
+ "SAFE_BLOCKED_PRODUCTS": 28,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 94.63,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 2,
+ "COFINS_BLOCKERS": 2,
+ "RTC_BLOCKERS": 0,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {
+ "COFINS_CST_INCIDENCE_MISMATCH": 1,
+ "COFINS_CST_INVALID": 1,
+ "PIS_CST_INCIDENCE_MISMATCH": 1,
+ "PIS_CST_INVALID": 1
+ },
+ "applicability": {
+ "NOT_APPLICABLE": 518,
+ "REQUIRED": 2,
+ "OPTIONAL": 1
+ },
+ "rtc_applicability": {
+ "NOT_APPLICABLE": 521
+ },
+ "changed_products": 2,
+ "examples": [
+ {
+ "product": "000700",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INCIDENCE_MISMATCH",
+ "PIS_CST_INCIDENCE_MISMATCH"
+ ]
+ },
+ {
+ "product": "001434",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ }
+ ]
+ },
+ "RTC_ON_SIMULATION": {
+ "rtc_enabled": true,
+ "TOTAL_PRODUCTS": 521,
+ "COMPLETE_PRODUCTS": 0,
+ "SAFE_BLOCKED_PRODUCTS": 521,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 0.0,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 2,
+ "COFINS_BLOCKERS": 2,
+ "RTC_BLOCKERS": 521,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {
+ "CCLASSTRIB_MISSING": 521,
+ "COFINS_CST_INCIDENCE_MISMATCH": 1,
+ "COFINS_CST_INVALID": 1,
+ "PIS_CST_INCIDENCE_MISMATCH": 1,
+ "PIS_CST_INVALID": 1
+ },
+ "applicability": {
+ "NOT_APPLICABLE": 518,
+ "REQUIRED": 2,
+ "OPTIONAL": 1
+ },
+ "rtc_applicability": {
+ "REQUIRED": 521
+ },
+ "changed_products": 495,
+ "examples": [
+ {
+ "product": "000004",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000005",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000006",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000008",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000009",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000010",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000011",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000013",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000015",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000017",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000018",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000019",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000021",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000022",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000023",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000024",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000025",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000026",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000027",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000028",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000029",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000032",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000033",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000034",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000035",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000036",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000038",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000040",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000041",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000042",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ }
+ ]
+ }
+ },
+ "SAFETY": {
+ "FIREBIRD_WRITES": 0,
+ "WRITE_ATTEMPTS": 0,
+ "WRITE_SUCCESSES": 0,
+ "connection_mode": "READ COMMITTED READ ONLY (Agent reader); no write-capable connection opened"
+ }
+}
\ No newline at end of file
diff --git a/docs/runs/unified_coverage_e2e_CONSTRUCAO.json b/docs/runs/unified_coverage_e2e_CONSTRUCAO.json
new file mode 100644
index 0000000..8a2bcf3
--- /dev/null
+++ b/docs/runs/unified_coverage_e2e_CONSTRUCAO.json
@@ -0,0 +1,593 @@
+{
+ "mode": "FISCAL_ONLY (no Hub decision)",
+ "database": "CONSTRUCAO",
+ "crt": "1",
+ "products": 3712,
+ "fiscal_products_read": 3712,
+ "BEFORE": {
+ "COMPLETE_PRODUCTS": 3712,
+ "SAFE_BLOCKED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 100.0,
+ "UNKNOWN_APPLICABILITY_previous_model": 3712
+ },
+ "runs": {
+ "ERP_AS_FOUND": {
+ "rtc_enabled": true,
+ "TOTAL_PRODUCTS": 3712,
+ "COMPLETE_PRODUCTS": 0,
+ "SAFE_BLOCKED_PRODUCTS": 3712,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 0.0,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 0,
+ "COFINS_BLOCKERS": 0,
+ "RTC_BLOCKERS": 3712,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {
+ "CCLASSTRIB_MISSING": 3712
+ },
+ "applicability": {
+ "NOT_APPLICABLE": 3712
+ },
+ "rtc_applicability": {
+ "REQUIRED": 3712
+ },
+ "changed_products": 3712,
+ "examples": [
+ {
+ "product": "000001",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000002",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000003",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000004",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000005",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000006",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000007",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000008",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000009",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000010",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000011",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000012",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000013",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000014",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000015",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000016",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000017",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000018",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000019",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000020",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000021",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000022",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000023",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000024",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000025",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000026",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000027",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000028",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000029",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000030",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ }
+ ],
+ "context": {
+ "rtc_enabled": true,
+ "rtc_supported": true,
+ "rtc_catalog_valid": true,
+ "rtc_param": 1,
+ "catalog_integrity": {
+ "classes": 145,
+ "cst": 18,
+ "orphan_classes": 0,
+ "bad_class_format": 0,
+ "bad_cst_format": 0,
+ "duplicates": 0,
+ "valid": true
+ },
+ "crt": "1",
+ "products_evaluated": 3712
+ }
+ },
+ "RTC_OFF_SIMULATION": {
+ "rtc_enabled": false,
+ "TOTAL_PRODUCTS": 3712,
+ "COMPLETE_PRODUCTS": 3712,
+ "SAFE_BLOCKED_PRODUCTS": 0,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 100.0,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 0,
+ "COFINS_BLOCKERS": 0,
+ "RTC_BLOCKERS": 0,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {},
+ "applicability": {
+ "NOT_APPLICABLE": 3712
+ },
+ "rtc_applicability": {
+ "NOT_APPLICABLE": 3712
+ },
+ "changed_products": 0,
+ "examples": []
+ },
+ "RTC_ON_SIMULATION": {
+ "rtc_enabled": true,
+ "TOTAL_PRODUCTS": 3712,
+ "COMPLETE_PRODUCTS": 0,
+ "SAFE_BLOCKED_PRODUCTS": 3712,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 0.0,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 0,
+ "COFINS_BLOCKERS": 0,
+ "RTC_BLOCKERS": 3712,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {
+ "CCLASSTRIB_MISSING": 3712
+ },
+ "applicability": {
+ "NOT_APPLICABLE": 3712
+ },
+ "rtc_applicability": {
+ "REQUIRED": 3712
+ },
+ "changed_products": 3712,
+ "examples": [
+ {
+ "product": "000001",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000002",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000003",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000004",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000005",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000006",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000007",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000008",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000009",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000010",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000011",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000012",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000013",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000014",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000015",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000016",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000017",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000018",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000019",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000020",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000021",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000022",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000023",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000024",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000025",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000026",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000027",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000028",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000029",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000030",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ }
+ ]
+ }
+ },
+ "SAFETY": {
+ "FIREBIRD_WRITES": 0,
+ "WRITE_ATTEMPTS": 0,
+ "WRITE_SUCCESSES": 0,
+ "connection_mode": "READ COMMITTED READ ONLY (Agent reader); no write-capable connection opened"
+ }
+}
\ No newline at end of file
diff --git a/docs/runs/unified_coverage_e2e_MMMODAS.json b/docs/runs/unified_coverage_e2e_MMMODAS.json
new file mode 100644
index 0000000..cacf261
--- /dev/null
+++ b/docs/runs/unified_coverage_e2e_MMMODAS.json
@@ -0,0 +1,353 @@
+{
+ "mode": "FISCAL_ONLY (no Hub decision)",
+ "database": "MMMODAS",
+ "crt": "2",
+ "products": 20997,
+ "fiscal_products_read": 20997,
+ "BEFORE": {
+ "COMPLETE_PRODUCTS": 20997,
+ "SAFE_BLOCKED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 100.0,
+ "UNKNOWN_APPLICABILITY_previous_model": 20996
+ },
+ "runs": {
+ "ERP_AS_FOUND": {
+ "rtc_enabled": false,
+ "TOTAL_PRODUCTS": 20997,
+ "COMPLETE_PRODUCTS": 20997,
+ "SAFE_BLOCKED_PRODUCTS": 0,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 100.0,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 0,
+ "COFINS_BLOCKERS": 0,
+ "RTC_BLOCKERS": 0,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {},
+ "applicability": {
+ "OPTIONAL": 1,
+ "NOT_APPLICABLE": 20995
+ },
+ "rtc_applicability": {
+ "NOT_APPLICABLE": 20996
+ },
+ "changed_products": 0,
+ "examples": [],
+ "context": {
+ "rtc_enabled": false,
+ "rtc_supported": true,
+ "rtc_catalog_valid": true,
+ "rtc_param": 0,
+ "catalog_integrity": {
+ "classes": 145,
+ "cst": 18,
+ "orphan_classes": 0,
+ "bad_class_format": 0,
+ "bad_cst_format": 0,
+ "duplicates": 0,
+ "valid": true
+ },
+ "crt": "2",
+ "products_evaluated": 20997
+ }
+ },
+ "RTC_OFF_SIMULATION": {
+ "rtc_enabled": false,
+ "TOTAL_PRODUCTS": 20997,
+ "COMPLETE_PRODUCTS": 20997,
+ "SAFE_BLOCKED_PRODUCTS": 0,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 100.0,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 0,
+ "COFINS_BLOCKERS": 0,
+ "RTC_BLOCKERS": 0,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {},
+ "applicability": {
+ "OPTIONAL": 1,
+ "NOT_APPLICABLE": 20995
+ },
+ "rtc_applicability": {
+ "NOT_APPLICABLE": 20996
+ },
+ "changed_products": 0,
+ "examples": []
+ },
+ "RTC_ON_SIMULATION": {
+ "rtc_enabled": true,
+ "TOTAL_PRODUCTS": 20997,
+ "COMPLETE_PRODUCTS": 0,
+ "SAFE_BLOCKED_PRODUCTS": 20997,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 0.0,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 0,
+ "COFINS_BLOCKERS": 0,
+ "RTC_BLOCKERS": 20996,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {
+ "CCLASSTRIB_MISSING": 20996
+ },
+ "applicability": {
+ "OPTIONAL": 1,
+ "NOT_APPLICABLE": 20995
+ },
+ "rtc_applicability": {
+ "REQUIRED": 20996
+ },
+ "changed_products": 20997,
+ "examples": [
+ {
+ "product": "000002",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000003",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000006",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000008",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000011",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000013",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000014",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000016",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000017",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000018",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000019",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000020",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000022",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000023",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000024",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000025",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000026",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000027",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000028",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000029",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000030",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000031",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000032",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000033",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000034",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000038",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000039",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000040",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000041",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000042",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ }
+ ]
+ }
+ },
+ "SAFETY": {
+ "FIREBIRD_WRITES": 0,
+ "WRITE_ATTEMPTS": 0,
+ "WRITE_SUCCESSES": 0,
+ "connection_mode": "READ COMMITTED READ ONLY (Agent reader); no write-capable connection opened"
+ }
+}
\ No newline at end of file
diff --git a/docs/runs/unified_coverage_e2e_RINCAO.json b/docs/runs/unified_coverage_e2e_RINCAO.json
new file mode 100644
index 0000000..3db7a6f
--- /dev/null
+++ b/docs/runs/unified_coverage_e2e_RINCAO.json
@@ -0,0 +1,902 @@
+{
+ "database": "RINCAO",
+ "crt": null,
+ "products": 894,
+ "fiscal_products_read": 894,
+ "BEFORE": {
+ "COMPLETE_PRODUCTS": 894,
+ "SAFE_BLOCKED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 100.0,
+ "UNKNOWN_APPLICABILITY_previous_model": 815
+ },
+ "runs": {
+ "ERP_AS_FOUND": {
+ "rtc_enabled": false,
+ "TOTAL_PRODUCTS": 894,
+ "COMPLETE_PRODUCTS": 815,
+ "SAFE_BLOCKED_PRODUCTS": 79,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 91.16,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 79,
+ "COFINS_BLOCKERS": 79,
+ "RTC_BLOCKERS": 0,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {
+ "COFINS_CST_INVALID": 79,
+ "PIS_CST_INVALID": 79
+ },
+ "applicability": {
+ "NOT_APPLICABLE": 815,
+ "OPTIONAL": 79
+ },
+ "rtc_applicability": {
+ "NOT_APPLICABLE": 894
+ },
+ "changed_products": 79,
+ "examples": [
+ {
+ "product": "000139",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000140",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000141",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000142",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000143",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000144",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000145",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000146",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000147",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000148",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000149",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000150",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000156",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000157",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000158",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000159",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000160",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000161",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000165",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000166",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000167",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000168",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000169",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000170",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000172",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000173",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000174",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000175",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000176",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000177",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ }
+ ],
+ "context": {
+ "rtc_enabled": false,
+ "rtc_supported": true,
+ "rtc_catalog_valid": true,
+ "rtc_param": 0,
+ "catalog_integrity": {
+ "classes": 145,
+ "cst": 18,
+ "orphan_classes": 0,
+ "bad_class_format": 0,
+ "bad_cst_format": 0,
+ "duplicates": 0,
+ "valid": true
+ },
+ "crt": null,
+ "products_evaluated": 894
+ }
+ },
+ "RTC_OFF_SIMULATION": {
+ "rtc_enabled": false,
+ "TOTAL_PRODUCTS": 894,
+ "COMPLETE_PRODUCTS": 815,
+ "SAFE_BLOCKED_PRODUCTS": 79,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 91.16,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 79,
+ "COFINS_BLOCKERS": 79,
+ "RTC_BLOCKERS": 0,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {
+ "COFINS_CST_INVALID": 79,
+ "PIS_CST_INVALID": 79
+ },
+ "applicability": {
+ "NOT_APPLICABLE": 815,
+ "OPTIONAL": 79
+ },
+ "rtc_applicability": {
+ "NOT_APPLICABLE": 894
+ },
+ "changed_products": 79,
+ "examples": [
+ {
+ "product": "000139",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000140",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000141",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000142",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000143",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000144",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000145",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000146",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000147",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000148",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000149",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000150",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000156",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000157",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000158",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000159",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000160",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000161",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000165",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000166",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000167",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000168",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000169",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000170",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000172",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000173",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000174",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000175",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000176",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "000177",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID",
+ "PIS_CST_INVALID"
+ ]
+ }
+ ]
+ },
+ "RTC_ON_SIMULATION": {
+ "rtc_enabled": true,
+ "TOTAL_PRODUCTS": 894,
+ "COMPLETE_PRODUCTS": 0,
+ "SAFE_BLOCKED_PRODUCTS": 894,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 0.0,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 79,
+ "COFINS_BLOCKERS": 79,
+ "RTC_BLOCKERS": 894,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {
+ "CCLASSTRIB_MISSING": 894,
+ "COFINS_CST_INVALID": 79,
+ "PIS_CST_INVALID": 79
+ },
+ "applicability": {
+ "NOT_APPLICABLE": 815,
+ "OPTIONAL": 79
+ },
+ "rtc_applicability": {
+ "REQUIRED": 894
+ },
+ "changed_products": 894,
+ "examples": [
+ {
+ "product": "000001",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000004",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000005",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000006",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000007",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000008",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000009",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000010",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000011",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000012",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000013",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000014",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000015",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000016",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000017",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000018",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000019",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000020",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000021",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000022",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000023",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000024",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000025",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000026",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000027",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000028",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000029",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000030",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000031",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000032",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ }
+ ]
+ }
+ },
+ "SAFETY": {
+ "FIREBIRD_WRITES": 0,
+ "WRITE_ATTEMPTS": 0,
+ "WRITE_SUCCESSES": 0,
+ "connection_mode": "READ COMMITTED READ ONLY (Agent reader); no write-capable connection opened"
+ }
+}
\ No newline at end of file
diff --git a/docs/runs/unified_coverage_e2e_UNIAO.json b/docs/runs/unified_coverage_e2e_UNIAO.json
new file mode 100644
index 0000000..446c1c1
--- /dev/null
+++ b/docs/runs/unified_coverage_e2e_UNIAO.json
@@ -0,0 +1,392 @@
+{
+ "mode": "FISCAL_ONLY (no Hub decision)",
+ "database": "UNIAO",
+ "crt": "3",
+ "products": 8942,
+ "fiscal_products_read": 8942,
+ "BEFORE": {
+ "COMPLETE_PRODUCTS": 8942,
+ "SAFE_BLOCKED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 100.0,
+ "UNKNOWN_APPLICABILITY_previous_model": 8170
+ },
+ "runs": {
+ "ERP_AS_FOUND": {
+ "rtc_enabled": false,
+ "TOTAL_PRODUCTS": 8942,
+ "COMPLETE_PRODUCTS": 8940,
+ "SAFE_BLOCKED_PRODUCTS": 2,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 99.98,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 0,
+ "COFINS_BLOCKERS": 2,
+ "RTC_BLOCKERS": 0,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {
+ "COFINS_CST_INVALID": 2
+ },
+ "applicability": {
+ "NOT_APPLICABLE": 8170,
+ "OPTIONAL": 772
+ },
+ "rtc_applicability": {
+ "NOT_APPLICABLE": 8942
+ },
+ "changed_products": 2,
+ "examples": [
+ {
+ "product": "000304",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "003090",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID"
+ ]
+ }
+ ],
+ "context": {
+ "rtc_enabled": false,
+ "rtc_supported": true,
+ "rtc_catalog_valid": true,
+ "rtc_param": 0,
+ "catalog_integrity": {
+ "classes": 145,
+ "cst": 18,
+ "orphan_classes": 0,
+ "bad_class_format": 0,
+ "bad_cst_format": 0,
+ "duplicates": 0,
+ "valid": true
+ },
+ "crt": "3",
+ "products_evaluated": 8942
+ }
+ },
+ "RTC_OFF_SIMULATION": {
+ "rtc_enabled": false,
+ "TOTAL_PRODUCTS": 8942,
+ "COMPLETE_PRODUCTS": 8940,
+ "SAFE_BLOCKED_PRODUCTS": 2,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 99.98,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 0,
+ "COFINS_BLOCKERS": 2,
+ "RTC_BLOCKERS": 0,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {
+ "COFINS_CST_INVALID": 2
+ },
+ "applicability": {
+ "NOT_APPLICABLE": 8170,
+ "OPTIONAL": 772
+ },
+ "rtc_applicability": {
+ "NOT_APPLICABLE": 8942
+ },
+ "changed_products": 2,
+ "examples": [
+ {
+ "product": "000304",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID"
+ ]
+ },
+ {
+ "product": "003090",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "COFINS_CST_INVALID"
+ ]
+ }
+ ]
+ },
+ "RTC_ON_SIMULATION": {
+ "rtc_enabled": true,
+ "TOTAL_PRODUCTS": 8942,
+ "COMPLETE_PRODUCTS": 0,
+ "SAFE_BLOCKED_PRODUCTS": 8942,
+ "FAILED_PRODUCTS": 0,
+ "UNCLASSIFIED_PRODUCTS": 0,
+ "COVERAGE_PERCENT": 0.0,
+ "INVARIANT_PASS": true,
+ "PIS_BLOCKERS": 0,
+ "COFINS_BLOCKERS": 2,
+ "RTC_BLOCKERS": 8942,
+ "OTHER_BLOCKERS": 0,
+ "blockers_by_type": {
+ "CCLASSTRIB_MISSING": 8942,
+ "COFINS_CST_INVALID": 2
+ },
+ "applicability": {
+ "NOT_APPLICABLE": 8170,
+ "OPTIONAL": 772
+ },
+ "rtc_applicability": {
+ "REQUIRED": 8942
+ },
+ "changed_products": 8942,
+ "examples": [
+ {
+ "product": "000008",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000009",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000014",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000015",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000016",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000017",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000018",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000019",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000020",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000021",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000022",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000023",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000024",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000025",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000026",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000027",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000028",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000029",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000030",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000031",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000032",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000033",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000034",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000035",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000036",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000037",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000038",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000039",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000040",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ },
+ {
+ "product": "000041",
+ "BEFORE_STATUS": "COMPLETE",
+ "AFTER_STATUS": "BLOCKED_FISCAL_DATA",
+ "REASON": [
+ "CCLASSTRIB_MISSING"
+ ]
+ }
+ ]
+ }
+ },
+ "SAFETY": {
+ "FIREBIRD_WRITES": 0,
+ "WRITE_ATTEMPTS": 0,
+ "WRITE_SUCCESSES": 0,
+ "connection_mode": "READ COMMITTED READ ONLY (Agent reader); no write-capable connection opened"
+ }
+}
\ No newline at end of file
diff --git a/docs/sources/SOURCES_MANIFEST_UNIFIED_FISCAL_V1.json b/docs/sources/SOURCES_MANIFEST_UNIFIED_FISCAL_V1.json
new file mode 100644
index 0000000..386bffe
--- /dev/null
+++ b/docs/sources/SOURCES_MANIFEST_UNIFIED_FISCAL_V1.json
@@ -0,0 +1,70 @@
+{
+ "generated": "2026-09-24",
+ "items": [
+ {
+ "name": "LC 123/2006 (Simples Nacional), compilada",
+ "source_url": "https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp123.htm",
+ "accessed_at": "2026-09-24",
+ "sha256": "8c446540a798b1fe5cfff380cf1a736394ea5bfe0a8ce7351151dca77d02eee4",
+ "bytes": 1622252,
+ "note": "art. 13 IV/V (Cofins e PIS no DAS), art. 18 par. 4-A I (segregacao da receita monofasica). Obtida via Internet Archive (planalto recusa conexao direta)"
+ },
+ {
+ "name": "Lei 9.718/1998, compilada",
+ "source_url": "https://www.planalto.gov.br/ccivil_03/leis/l9718compilada.htm",
+ "accessed_at": "2026-09-24",
+ "sha256": "79b8c471f84ec9d702e6e6ef8cb28198143c0cbc4db59a14714c5c5d1b188906",
+ "bytes": 75858,
+ "note": "art. 2-3: PIS/Cofins sobre faturamento (regime cumulativo)"
+ },
+ {
+ "name": "Lei 10.637/2002, compilada",
+ "source_url": "https://www.planalto.gov.br/ccivil_03/leis/2002/l10637.htm",
+ "accessed_at": "2026-09-24",
+ "sha256": "eaa97c5448184815c96fb68ffcd6e136721d66965b609af60b6832fdd741382e",
+ "bytes": 238160,
+ "note": "art. 1: PIS/Pasep nao cumulativo"
+ },
+ {
+ "name": "Lei 10.833/2003, compilada",
+ "source_url": "https://www.planalto.gov.br/ccivil_03/leis/2003/l10.833.htm",
+ "accessed_at": "2026-09-24",
+ "sha256": "4d562e534472c620c404e55e56544b1b17ead935fd9fcc767fe5a19eb6062a3a",
+ "bytes": 533449,
+ "note": "art. 1: Cofins nao cumulativa"
+ },
+ {
+ "name": "Lei 10.147/2000, compilada",
+ "source_url": "https://www.planalto.gov.br/ccivil_03/leis/l10147.htm",
+ "accessed_at": "2026-09-24",
+ "sha256": "4b5d3c4c62ae967516b42be56c7cc8fb6315590fd98c4202c91fb2877f59c703",
+ "bytes": 35624,
+ "note": "incidencia monofasica (medicamentos, perfumaria/higiene)"
+ },
+ {
+ "name": "LC 214/2025, compilada",
+ "source_url": "https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp214.htm",
+ "accessed_at": "2026-09-24",
+ "sha256": "5c7d59b3c49e5b7c066c2ad28bedf32791193a676ec1a163bec14bea5fa2f680",
+ "bytes": 5400654,
+ "note": "art. 348: transicao 2026 (IBS/CBS compensados); Simples so a partir de 2027"
+ },
+ {
+ "name": "NT 2025.002-RTC v1.51 (NF-e/NFC-e IBS/CBS/IS)",
+ "source_url": "https://www.nfe.fazenda.gov.br/portal/exibirArquivo.aspx?conteudo=AKD/muSmiIY=",
+ "accessed_at": "2026-09-24",
+ "sha256": "a4aaaa181522b43cd90b502f8ccb9e4bdabea30fed838e2d0790be5ba77254a4",
+ "bytes": 2182361,
+ "note": "publicada 04/08/2026; CRT 1/2/4: orientacoes IBS/CBS em NT futura, tributacao so a partir de 2027 (art. 348 LC 214); grupos Q/S (PIS/COFINS) obrigatorios no documento (rejeicoes 745/748)"
+ },
+ {
+ "name": "NT 2026.002 v1.10a",
+ "source_url": "https://www.nfe.fazenda.gov.br/portal/exibirArquivo.aspx?conteudo=8zOzIahvn8I=",
+ "accessed_at": "2026-09-24",
+ "sha256": "705f771830565c209d815659347ac4ea251901c77064cc163ce5c93f311e6e64",
+ "bytes": 767698,
+ "note": "publicada 25/08/2026 (consultada; sem regra adicional de PIS/COFINS de produto)"
+ }
+ ],
+ "sped_tables": "docs/sources/SOURCES_MANIFEST_V3.json + SPED_PISCOFINS_TABLES_V3.json (4.3.3/4.3.4 CST domain; 4.3.10-4.3.15 special regimes)"
+}
\ No newline at end of file
diff --git a/docs/unified_fiscal_completeness.md b/docs/unified_fiscal_completeness.md
new file mode 100644
index 0000000..d7075e6
--- /dev/null
+++ b/docs/unified_fiscal_completeness.md
@@ -0,0 +1,64 @@
+# Completude fiscal unificada (UNIFIED_FISCAL_COVERAGE)
+
+Uma definição de "completo", um painel, uma fila de blockers, uma cobertura.
+
+## Definição
+```
+campos obrigatórios(produto, empresa) =
+ campos fiscais base aplicáveis (NCM, CEST, CFOP, ICMS/CST/CSOSN, IPI ... — decididos pelo motor de decisão do Hub)
+ + PIS/COFINS aplicáveis (docs/pis_cofins_applicability.md)
+ + RTC (cClassTrib → CST IBS/CBS) SOMENTE quando o ERP tem a Reforma Tributária ligada (docs/rtc_pis_cofins_model.md)
+```
+Campos `NOT_APPLICABLE` não contam contra a completude. Nenhum campo é exigido só porque a coluna existe.
+
+## Onde vive (nenhuma cópia paralela)
+| Peça | Arquivo |
+|---|---|
+| regras puras (PIS/COFINS, RTC, `evaluate_product`) | `central/app/unified_fiscal_coverage.py` |
+| estado do produto (`COMPLETE`/`CORRECTED`/`BLOCKED_*`/`BLOCKED_FISCAL_DATA`/`FAILED_TECHNICAL`/`NEEDS_REVIEW`) | `central/app/operational_state.py` (`classify_product_state(..., fiscal=)`) |
+| leitura do ERP (somente SELECT) | `agent/src/gti_fiscal_agent/unified_fiscal.py` |
+| integração no job do Agent | `agent/scripts/run_agent_service.py` (`_unified_fiscal`, `_fiscal_blocked_products`) |
+| snapshot / cobertura no Hub | `central/app/api.py` (`_apply_job_outcome` — inalterado: usa o mesmo `operational`) |
+| fila única + fechamento automático | `central/app/blocked_products.py` (`resolve_stale_fiscal_blockers`) |
+| painel | `central/app/panel_presentation.py`, `central/templates/{dashboard,blockers,agent_detail}.html` |
+
+## Estados finais do produto
+`COMPLETE` (+`CORRECTED`) · `SAFE_BLOCKED` (`BLOCKED_*`, incluindo `BLOCKED_FISCAL_DATA` para PIS/COFINS/RTC) · `FAILED_TECHNICAL` · `NEEDS_REVIEW` (não classificado; só incidência PIS/COFINS fora do vocabulário do SGV).
+
+Invariante: `TOTAL = COMPLETE + SAFE_BLOCKED + FAILED + UNCLASSIFIED` (o Hub calcula "não classificados" como `total − completos − bloqueados − falhas`, então a igualdade fecha por construção e é reverificada em `coverage_invariant_pass`).
+Precedência: um blocker de decisão (NCM, conflito, contexto) permanece o estado principal; blockers fiscais só rebaixam produto que seria completo. Um blocker real vale mais que `NEEDS_REVIEW`.
+
+## Fila única e fechamento
+Cada blocker fiscal é um blocker comum: `blocker_type=BLOCKED_FISCAL_DATA`, `field ∈ {PIS, COFINS, RTC}`, `reason_code` = tipo (ex.: `PIS_CST_INVALID`). A cada job de varredura completa (`fiscal_context.evaluated_all=true`) o Agent reenvia todos os blockers vigentes e o Hub fecha (`AUTO_REEVALUATION`) os blockers PIS/COFINS/RTC da instalação não reenviados (dado corrigido, ou RTC desligada). Reprocesso parcial não fecha nada de outros produtos. O fingerprint do blocker inclui os valores fiscais lidos.
+
+## Painel
+- Dashboard: um único bloco "Cobertura dos produtos" (Produtos, Completos, Bloqueados com segurança, Falhas técnicas, Não classificados, Cobertura fiscal). Breakdown de blockers: `NCM, CEST, CFOP, ST, ICMS, CSOSN, PIS, COFINS, RTC, OTHER` (categoria com 0 fica esmaecida).
+- Detalhe do Agent: a mesma cobertura + uma linha discreta "Reforma Tributária: Ativa/Desativada" (do último job). RTC desativada nunca aparece como problema. O bloco "Escopo anterior/ampliado" foi removido.
+- Removidos do código: `expanded_scope_summary`, `expanded_scope_view`, `LEGACY_SCOPE_COVERAGE`, `EXPANDED_SCOPE_COVERAGE`, `UNKNOWN_APPLICABILITY` (como estado em massa).
+
+## Segurança
+Somente leitura. `FIREBIRD_WRITES=0`: a avaliação abre uma conexão `READ COMMITTED READ ONLY`, executa apenas SELECT e nunca preenche `CCLASSTRIB`, PIS ou COFINS. Testes provam que só SELECT é emitido.
+
+## Validação no banco real (somente leitura)
+`scripts/unified_coverage_e2e.py` → `docs/runs/unified_coverage_e2e*.json`. TESTESOFICIAIS (521 produtos, CRT 1):
+
+| | Antes (só decisão) | Depois — RTC desligada (estado do piloto) |
+|---|---|---|
+| Completos | 495 (95,01%) | 493 (94,63%) |
+| Bloqueados | 26 | 28 |
+| Falhas / Não classificados | 0 / 0 | 0 / 0 |
+| RTC blockers | — | 0 |
+| PIS / COFINS blockers | — | 2 / 2 (2 produtos) |
+
+Aplicabilidade PIS/COFINS: 518 `NOT_APPLICABLE`, 2 `REQUIRED`, 1 `OPTIONAL`, 0 `NEEDS_REVIEW` (antes: 492 `UNKNOWN_APPLICABILITY`). Os 2 produtos bloqueados são legítimos: um com CST `2`/`2` (formato inválido) e um com incidência SUBST.TRIBUTÁRIA e CST 01 (incompatível com CST 05).
+
+### Corpus de clientes (RTC desligada, somente PIS/COFINS; `--fiscal-only`, sem decisão do Hub)
+| Banco | CRT | Produtos | NOT_APPLICABLE | OPTIONAL | Blockers PIS/COFINS | Cobertura da parte fiscal |
+|---|---|---|---|---|---|---|
+| CONSTRUCAO | 1 | 3.712 | 3.712 | 0 | 0 | 100% |
+| MMMODAS | 1 | 20.997 | 20.995 | 1 | 0 | 100% |
+| UNIAO | 1 | 8.942 | 8.170 | 772 | 2 produtos (COFINS `000`/`060`, estilo CST de ICMS) | 99,98% |
+| RINCAO | 3 | 894 | 815 | 79 | 79 produtos (78 com CST `4` sem zero à esquerda + 1) | 91,16% |
+| TESTESOFICIAIS | 1 | 521 | 518 | 1 | 2 produtos | 99,62% (94,63% com a decisão do Hub) |
+
+`OPTIONAL` = valor digitado sem flag/incidência especial. RINCAO: `4` deve ser `04` (o seletor do SGV grava 2 dígitos); o motivo do blocker traz a dica `UNPADDED_USE_04`, nunca aplicada.
diff --git a/scripts/unified_coverage_e2e.py b/scripts/unified_coverage_e2e.py
new file mode 100644
index 0000000..c3e7758
--- /dev/null
+++ b/scripts/unified_coverage_e2e.py
@@ -0,0 +1,95 @@
+"""READ-ONLY end-to-end of the UNIFIED fiscal coverage over a real Firebird database (default: TESTESOFICIAIS.FDB).
+
+ python scripts/unified_coverage_e2e.py [--db TESTESOFICIAIS] [--out docs/runs/unified_coverage_e2e.json]
+
+Reads the products with the Agent's own reader (SELECT only), classifies them through the Hub, then computes the operational states
+BEFORE (decision only) and AFTER (decision + PIS/COFINS + RTC when enabled). The ERP's RTC switch is reported as found; a what-if run
+with RTC forced ON / OFF uses the pure evaluator only (nothing is written, PARAMETROS is never touched).
+"""
+import argparse
+import json
+import os
+import sys
+from collections import Counter
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts")); sys.path.insert(0, str(ROOT / "agent" / "src"))
+from dotenv import load_dotenv # noqa: E402
+
+load_dotenv(ROOT / ".env")
+import local_completion as lc # noqa: E402
+from central.app.operational_state import classify_product_state, summarize_operational_states # noqa: E402
+from gti_fiscal_agent import firebird_reader, unified_fiscal # noqa: E402
+from gti_fiscal_agent.client import HubClient # noqa: E402
+from gti_fiscal_agent.runner import to_hub_product # noqa: E402
+
+PIS_COLS = ("COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS")
+
+
+def old_unknown(product):
+ """The previous model's UNKNOWN_APPLICABILITY: no product-level evidence of PIS/COFINS at all."""
+ text = lambda v: "" if v is None else str(v).strip() # noqa: E731
+ evidence = any(text(product.get(c)) for c in PIS_COLS) or text(product.get("PISCOFINS")).upper() == "S" or text(product.get("INCIDENCIA_PISCOFINS")).upper() not in ("", "NENHUMA")
+ return not evidence
+
+
+def main():
+ ap = argparse.ArgumentParser(); ap.add_argument("--db", default="TESTESOFICIAIS"); ap.add_argument("--out", default="docs/runs/unified_coverage_e2e.json"); ap.add_argument("--crt", default=None); ap.add_argument("--fiscal-only", action="store_true", help="skip the Hub classification (decisions = EXACT_NCM); measures only the PIS/COFINS + RTC contribution")
+ a = ap.parse_args()
+ path = lc.allowlist()[a.db]; env = lc.env_for(path)
+ products = firebird_reader.read_products(env)
+ connection = firebird_reader._connect(env) # READ COMMITTED READ ONLY (Agent reader)
+ try:
+ cursor = connection.cursor()
+ schema, _catalog, extras = unified_fiscal.load_rtc_context(cursor)
+ fiscal_products = unified_fiscal.read_products(cursor, schema)
+ cursor.execute("SELECT DISTINCT TRIM(CRT) FROM C000004"); crts = [r[0] for r in cursor.fetchall()]
+ finally:
+ connection.close()
+ crt = a.crt or (crts[0] if len(crts) == 1 else None)
+ client = HubClient(os.environ.get("HUB_URL", lc.HUB), os.environ["AGENT_DEV_TOKEN"])
+ decisions = [{"status": "EXACT_NCM"} for _ in products] if a.fiscal_only else client.classify_batch([to_hub_product(p) for p in products])
+ before_states = [classify_product_state(p, d) for p, d in zip(products, decisions)]
+ before = summarize_operational_states(before_states)
+ company = {"crt": crt}
+ runs = {}
+ for label, override in (("ERP_AS_FOUND", None), ("RTC_OFF_SIMULATION", False), ("RTC_ON_SIMULATION", True)):
+ conn = firebird_reader._connect(env)
+ try:
+ out = unified_fiscal.assess_from_connection(conn, company, rtc_override=override)
+ finally:
+ conn.close()
+ findings = out["findings"]
+ states = [classify_product_state(p, d, fiscal=findings.get(str(p["CODIGO"]))) for p, d in zip(products, decisions)]
+ summary = summarize_operational_states(states)
+ by_type, by_category = Counter(), Counter()
+ for f in findings.values():
+ for kind in {(b["type"], b["category"]) for b in f["blockers"]}:
+ by_type[kind[0]] += 1; by_category[kind[1]] += 1
+ changed = [{"product": s["product_code"], "BEFORE_STATUS": b["state"], "AFTER_STATUS": s["state"], "REASON": sorted({x["type"] for x in s["fiscal_blockers"]}) or ("NEEDS_REVIEW" if s["state"] == "NEEDS_REVIEW" else "")}
+ for b, s in zip(before_states, states) if b["state"] != s["state"]]
+ total = summary["TOTAL_PRODUCTS"]
+ runs[label] = {"rtc_enabled": out["context"]["rtc_enabled"], "TOTAL_PRODUCTS": total, "COMPLETE_PRODUCTS": summary["COMPLETE_PRODUCTS"], "SAFE_BLOCKED_PRODUCTS": summary["SAFE_BLOCKED"],
+ "FAILED_PRODUCTS": summary["FAILED_TECHNICAL"], "UNCLASSIFIED_PRODUCTS": summary["UNCLASSIFIED"],
+ "COVERAGE_PERCENT": round(100 * summary["COMPLETE_PRODUCTS"] / total, 2) if total else 0.0,
+ "INVARIANT_PASS": total == summary["COMPLETE_PRODUCTS"] + summary["SAFE_BLOCKED"] + summary["FAILED_TECHNICAL"] + summary["UNCLASSIFIED"],
+ "PIS_BLOCKERS": by_category["PIS"], "COFINS_BLOCKERS": by_category["COFINS"], "RTC_BLOCKERS": by_category["RTC"], "OTHER_BLOCKERS": by_category["OTHER"], "blockers_by_type": dict(sorted(by_type.items())),
+ "applicability": dict(Counter(f["applicability"]["PIS_COFINS"] for f in findings.values())), "rtc_applicability": dict(Counter(f["applicability"]["RTC"] for f in findings.values())),
+ "changed_products": len(changed), "examples": changed[:30]}
+ if label == "ERP_AS_FOUND":
+ runs[label]["context"] = {k: v for k, v in out["context"].items() if k != "capabilities"}
+ total = before["TOTAL_PRODUCTS"]
+ report = {"mode": "FISCAL_ONLY (no Hub decision)" if a.fiscal_only else "FULL (Hub decision + fiscal)", "database": a.db, "crt": crt, "products": len(products), "fiscal_products_read": len(fiscal_products),
+ "BEFORE": {"COMPLETE_PRODUCTS": before["COMPLETE_PRODUCTS"], "SAFE_BLOCKED_PRODUCTS": before["SAFE_BLOCKED"], "COVERAGE_PERCENT": round(100 * before["COMPLETE_PRODUCTS"] / total, 2),
+ "UNKNOWN_APPLICABILITY_previous_model": sum(1 for p in fiscal_products if old_unknown(p))}, "runs": runs,
+ "SAFETY": {"FIREBIRD_WRITES": 0, "WRITE_ATTEMPTS": 0, "WRITE_SUCCESSES": 0, "connection_mode": "READ COMMITTED READ ONLY (Agent reader); no write-capable connection opened"}}
+ Path(ROOT / a.out).write_text(json.dumps(report, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
+ print(json.dumps({k: report[k] for k in ("database", "crt", "products", "BEFORE")}, ensure_ascii=False))
+ for label, r in runs.items():
+ print(label, {k: r[k] for k in ("rtc_enabled", "TOTAL_PRODUCTS", "COMPLETE_PRODUCTS", "SAFE_BLOCKED_PRODUCTS", "FAILED_PRODUCTS", "UNCLASSIFIED_PRODUCTS", "COVERAGE_PERCENT", "INVARIANT_PASS", "PIS_BLOCKERS", "COFINS_BLOCKERS", "RTC_BLOCKERS", "OTHER_BLOCKERS")})
+ print(" applicability", r["applicability"], "blockers", r["blockers_by_type"])
+
+
+if __name__ == "__main__":
+ main()