From b15dbea213ca22faa14dda87a0456ae0fa13c201 Mon Sep 17 00:00:00 2001 From: gti Date: Thu, 24 Sep 2026 16:39:53 -0300 Subject: [PATCH] feat: unified fiscal coverage - PIS/COFINS applicability + conditional RTC in one definition, one queue, one panel - central/app/unified_fiscal_coverage.py: pure rules (PIS/COFINS NOT_APPLICABLE/REQUIRED/OPTIONAL/NEEDS_REVIEW, RTC only when the ERP switch is on) - agent: read-only loader (unified_fiscal.py) replaces the legacy/expanded module; findings feed the single operational state, coverage and blocker queue - hub: BLOCKED_FISCAL_DATA blockers (PIS/COFINS/RTC) share the queue; stale ones close on the next full pass; dashboard breakdown gains CEST/PIS/COFINS/RTC - panel: legacy/expanded coverage block removed; discreet 'Reforma Tributaria: Ativa/Desativada' - docs: unified_fiscal_completeness, pis_cofins_applicability, rtc_pis_cofins_model + official source manifests; read-only E2E script Co-Authored-By: Claude Sonnet 5 --- .gitignore | 1 + agent/scripts/run_agent_service.py | 53 +- agent/src/gti_fiscal_agent/rtc_pis_cofins.py | 277 ------ agent/src/gti_fiscal_agent/unified_fiscal.py | 102 ++ agent/tests/conftest.py | 21 + agent/tests/test_rtc_pis_cofins.py | 134 --- agent/tests/test_unified_fiscal.py | 110 +++ agent/tests/test_zero_touch_agent.py | 39 + central/app/api.py | 5 +- central/app/blocked_products.py | 19 +- central/app/control_plane.py | 2 +- central/app/operational_state.py | 15 +- central/app/panel_presentation.py | 31 +- central/app/unified_fiscal_coverage.py | 211 ++++ central/app/web.py | 4 +- central/templates/agent_detail.html | 6 +- central/templates/blockers.html | 4 +- central/templates/dashboard.html | 2 +- central/tests/test_control_panel_pages.py | 39 + central/tests/test_operational_view_models.py | 19 +- central/tests/test_unified_fiscal_coverage.py | 131 +++ central/tests/test_zero_touch_hub.py | 42 + docs/pis_cofins_applicability.md | 52 + docs/rtc_pis_cofins_model.md | 78 +- docs/runs/unified_coverage_e2e.json | 630 ++++++++++++ .../runs/unified_coverage_e2e_CONSTRUCAO.json | 593 ++++++++++++ docs/runs/unified_coverage_e2e_MMMODAS.json | 353 +++++++ docs/runs/unified_coverage_e2e_RINCAO.json | 902 ++++++++++++++++++ docs/runs/unified_coverage_e2e_UNIAO.json | 392 ++++++++ .../SOURCES_MANIFEST_UNIFIED_FISCAL_V1.json | 70 ++ docs/unified_fiscal_completeness.md | 64 ++ scripts/unified_coverage_e2e.py | 95 ++ 32 files changed, 3979 insertions(+), 517 deletions(-) delete mode 100644 agent/src/gti_fiscal_agent/rtc_pis_cofins.py create mode 100644 agent/src/gti_fiscal_agent/unified_fiscal.py delete mode 100644 agent/tests/test_rtc_pis_cofins.py create mode 100644 agent/tests/test_unified_fiscal.py create mode 100644 central/app/unified_fiscal_coverage.py create mode 100644 central/tests/test_unified_fiscal_coverage.py create mode 100644 docs/pis_cofins_applicability.md create mode 100644 docs/runs/unified_coverage_e2e.json create mode 100644 docs/runs/unified_coverage_e2e_CONSTRUCAO.json create mode 100644 docs/runs/unified_coverage_e2e_MMMODAS.json create mode 100644 docs/runs/unified_coverage_e2e_RINCAO.json create mode 100644 docs/runs/unified_coverage_e2e_UNIAO.json create mode 100644 docs/sources/SOURCES_MANIFEST_UNIFIED_FISCAL_V1.json create mode 100644 docs/unified_fiscal_completeness.md create mode 100644 scripts/unified_coverage_e2e.py 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 %}

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 }}

Eventos recentes

Ver eventos
{% for event in events[:8] %}
{{ event.occurred_display }}{{ event.event_type }}{{ event.message }}{% if event.job_id %} · Job {{ event.job_id }}{% endif %}{% if event.product_code %} · Produto {{ event.product_code }}{% endif %}Severidade: {{ event.severity }}
{% 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 %} - + @@ -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

{% for key in ["CFOP","ST","ICMS","CSOSN","NCM","OTHER"] %}{% if summary.blocker_breakdown[key] > 0 %}{{ key }}{{ summary.blocker_breakdown[key] }}{% else %}
{{ key }}0
{% endif %}{% endfor %}
+

Distribuição dos blockers filtrados

{% for key in ["NCM","CEST","CFOP","ST","ICMS","CSOSN","PIS","COFINS","RTC","OTHER"] %}{% if summary.blocker_breakdown[key] > 0 %}{{ key }}{{ summary.blocker_breakdown[key] }}{% else %}
{{ key }}0
{% endif %}{% endfor %}
{% for item in result["items"] %}{% else %}{% endfor %}
Fila de blockers · {{ result.total }} resultados
Cliente / servidorProdutoCampo / tipoPrioridadeProblemaÚltima ocorrênciaAção
{{ 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 }} tentativasVer detalhes
Nenhum blocker corresponde aos filtros. Consultar histórico
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 @@

Breakdown dos blockers

Ver blockers
- {% 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()