From 88f4318072b83299f345a7f7dc80d9951fa37d90 Mon Sep 17 00:00:00 2001 From: gti Date: Thu, 24 Sep 2026 17:46:45 -0300 Subject: [PATCH] fix: RTC observed live with provenance, non-circular PIS/COFINS applicability, canonical CST normalization (1.1.8) - RTC: Agent reads ALL PARAMETROS rows every sync (conflict => off), reports rtc_source/rtc_observed_at/rtc_param_rows; legal_effective_context is informational only - PIS/COFINS: applicability from CRT + PISCOFINS flag + incidence only (never from empty fields); Simples w/o special treatment NOT_APPLICABLE, taxed regime with explicit N OPTIONAL, unreadable declaration NEEDS_REVIEW - CST: one digit with an official zero-padded form is VALID_AFTER_NORMALIZATION (non-blocking warning), nothing is written to the ERP - tests: RTC toggle 0->1->0 across syncs (Agent + Hub stale closing), non-circularity, normalization, panel provenance Co-Authored-By: Claude Sonnet 5 --- agent/src/gti_fiscal_agent/unified_fiscal.py | 39 +- agent/tests/test_unified_fiscal.py | 54 +- agent/tests/test_zero_touch_agent.py | 23 + central/app/panel_presentation.py | 13 +- central/app/unified_fiscal_coverage.py | 93 ++- central/templates/agent_detail.html | 2 +- central/tests/test_control_panel_pages.py | 12 + central/tests/test_unified_fiscal_coverage.py | 116 ++- central/tests/test_zero_touch_hub.py | 28 + docs/pis_cofins_applicability.md | 58 +- docs/rtc_pis_cofins_model.md | 15 +- docs/runs/unified_coverage_e2e.json | 115 +-- .../runs/unified_coverage_e2e_CONSTRUCAO.json | 33 +- docs/runs/unified_coverage_e2e_MMMODAS.json | 46 +- docs/runs/unified_coverage_e2e_RINCAO.json | 667 +++--------------- docs/runs/unified_coverage_e2e_UNIAO.json | 128 ++-- docs/unified_fiscal_completeness.md | 31 +- scripts/unified_coverage_e2e.py | 20 +- 18 files changed, 679 insertions(+), 814 deletions(-) diff --git a/agent/src/gti_fiscal_agent/unified_fiscal.py b/agent/src/gti_fiscal_agent/unified_fiscal.py index 6835d37..67df3bb 100644 --- a/agent/src/gti_fiscal_agent/unified_fiscal.py +++ b/agent/src/gti_fiscal_agent/unified_fiscal.py @@ -8,9 +8,10 @@ from __future__ import annotations import hashlib import json -from datetime import date +from collections import Counter +from datetime import date, datetime, timezone -from central.app.unified_fiscal_coverage import PIS_COFINS_FIELDS, catalog_integrity, evaluate_product, rtc_enabled +from central.app.unified_fiscal_coverage import PIS_COFINS_FIELDS, catalog_integrity, evaluate_product, legal_effective_context, rtc_enabled PRODUCT_TABLE = "C000025" RTC_CCLASSTRIB_TABLE = "RTC_CCLASSTRIB" @@ -44,12 +45,17 @@ def load_rtc_context(cursor) -> tuple[dict, dict | None, dict]: 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} + extras: dict = {"param_value": None, "param_rows": 0, "param_conflict": False, "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] + # ALL rows are read: the source of truth is what THIS installation's database says NOW (never a cached or Hub-side value). + cursor.execute("SELECT FIRST 20 USAR_REFORMA_TRIBUTARIA FROM PARAMETROS") + values = [None if r[0] is None else str(r[0]).strip().upper() for r in cursor.fetchall()] + extras["param_rows"] = len(values) + if len(set(values)) > 1: + extras["param_conflict"] = True # rows disagree: never guess "on" (RTC stays out of scope, and the conflict is reported) + elif values: + extras["param_value"] = values[0] except Exception: # noqa: BLE001 - unreadable parameter => RTC stays off (not enabled by the ERP) extras["param_value"] = None try: @@ -92,11 +98,26 @@ def assess_from_connection(conn, company: dict | None, *, today: date | None = N today = today or date.today() products = read_products(cursor, schema) findings = {} + warn_by_type: Counter = Counter() + warn_products: set = set() + samples: list = [] + applicability: Counter = Counter() 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 + code = str(p.get("CODIGO") or "") + findings[code] = found + applicability[found["applicability"]["PIS_COFINS"]] += 1 + for w in found.get("warnings", []): + warn_by_type[w["type"]] += 1 + warn_products.add(code) + if len(samples) < 10: + samples.append({"product_code": code, **w}) 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)}, + return {"context": {"rtc_enabled": enabled, "erp_rtc_enabled": enabled, "rtc_supported": caps["HAS_RTC_RELATIONSHIP"], "rtc_catalog_valid": caps["RTC_CATALOG_VALID"], "rtc_param": extras["param_value"], + "rtc_source": "SIMULATION" if rtc_override is not None else "PARAMETROS.USAR_REFORMA_TRIBUTARIA", "rtc_param_rows": extras["param_rows"], "rtc_param_conflict": extras["param_conflict"], + "rtc_observed_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), "legal_effective_context": legal_effective_context((company or {}).get("crt")), + "catalog_integrity": integrity, "crt": (company or {}).get("crt"), "capabilities": caps, "products_evaluated": len(products), + "pis_cofins_applicability": dict(applicability), + "non_canonical_warnings": {"products": len(warn_products), "by_type": dict(sorted(warn_by_type.items())), "samples": samples}}, "findings": findings} diff --git a/agent/tests/test_unified_fiscal.py b/agent/tests/test_unified_fiscal.py index d75bb2c..7908ea2 100644 --- a/agent/tests/test_unified_fiscal.py +++ b/agent/tests/test_unified_fiscal.py @@ -16,8 +16,8 @@ CLASSES = [("000001", "000", date(2025, 5, 5), None), ("200003", "200", date(202 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 __init__(self, param=0, params=None, schema=SCHEMA_ROWS, products=PRODUCTS, classes=CLASSES, cst=(("000",), ("200",))): + self.param, self.params, self.schema, self.products, self.classes, self.cst, self.statements, self._rows = param, params, schema, products, classes, cst, [], [] def execute(self, sql, *args): self.statements.append(sql) @@ -26,7 +26,7 @@ class FakeCursor: if "FROM RDB$RELATION_FIELDS" in text: self._rows = list(self.schema) elif "FROM PARAMETROS" in text: - self._rows = [(self.param,)] + self._rows = [(v,) for v in (self.params if self.params is not None else [self.param])] elif "RDB$RELATION_CONSTRAINTS" in text: self._rows = [(1,)] elif "FROM RTC_CCLASSTRIB" in text: @@ -62,7 +62,7 @@ def test_rtc_off_in_the_erp_is_out_of_scope_and_never_blocks(): 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 kinds(out["findings"]["1"]) == [] and kinds(out["findings"]["2"]) == [] and kinds(out["findings"]["3"]) == [] # Simples + explicit "no special treatment": not applicable, typed "2" is only a warning assert all(s.strip().upper().startswith("SELECT") for s in conn.cur.statements) # read-only: SELECT statements only @@ -70,19 +70,19 @@ 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 + assert kinds(out["findings"]["3"]) == ["CCLASSTRIB_MISSING"] # 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) + assert on["context"]["rtc_param"] == "0" and on["context"]["rtc_source"] == "SIMULATION" and not any("UPDATE" in s.upper() for s in conn.cur.statements) 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")] + rows = [("1", None, "N", "NENHUMA"), ("2", "049", "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"] @@ -108,3 +108,43 @@ def test_input_fingerprint_changes_with_the_fiscal_values_and_the_rtc_switch(): 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) + + +def test_rtc_state_is_read_from_this_database_now_with_provenance_and_timestamp(): + out = uf.assess_from_connection(FakeConn(param=1), {"crt": "1"}) + ctx = out["context"] + assert ctx["rtc_enabled"] is True and ctx["erp_rtc_enabled"] is True and ctx["rtc_source"] == "PARAMETROS.USAR_REFORMA_TRIBUTARIA" and ctx["rtc_param"] == "1" and ctx["rtc_param_rows"] == 1 + assert ctx["rtc_param_conflict"] is False and ctx["rtc_observed_at"].endswith("+00:00") + assert ctx["legal_effective_context"]["informational_only"] is True and ctx["legal_effective_context"]["crt"] == "1" + + +def test_toggle_between_syncs_is_followed_in_both_directions_without_any_cache(): + conn = FakeConn(param=0) + first = uf.assess_from_connection(conn, {"crt": "1"}) + assert first["context"]["rtc_enabled"] is False and not [b for f in first["findings"].values() for b in f["blockers"] if b["category"] == "RTC"] + conn.cur.param = 1 # the operator switches the reform on in the ERP between two syncs + second = uf.assess_from_connection(conn, {"crt": "1"}) + assert second["context"]["rtc_enabled"] is True and kinds(second["findings"]["1"]) == ["CCLASSTRIB_MISSING"] + assert second["findings"]["1"]["input_fingerprint"] != first["findings"]["1"]["input_fingerprint"] # a new blocker fingerprint: the old (RTC off) state is not reused + conn.cur.param = 0 # and off again + third = uf.assess_from_connection(conn, {"crt": "1"}) + assert third["context"]["rtc_enabled"] is False and not [b for f in third["findings"].values() for b in f["blockers"] if b["category"] == "RTC"] + assert third["findings"]["1"]["input_fingerprint"] == first["findings"]["1"]["input_fingerprint"] + + +def test_conflicting_or_repeated_parameter_rows_never_guess_on(): + same = uf.assess_from_connection(FakeConn(params=[1, 1]), {"crt": "1"})["context"] + assert same["rtc_enabled"] is True and same["rtc_param_rows"] == 2 and same["rtc_param_conflict"] is False + conflict = uf.assess_from_connection(FakeConn(params=[1, 0]), {"crt": "1"}) + assert conflict["context"]["rtc_enabled"] is False and conflict["context"]["rtc_param_conflict"] is True + assert not [b for f in conflict["findings"].values() for b in f["blockers"] if b["category"] == "RTC"] + assert uf.assess_from_connection(FakeConn(params=[]), {"crt": "1"})["context"]["rtc_enabled"] is False + + +def test_non_canonical_cst_warnings_are_reported_without_blocking(): + rows = [("1", None, None, "4", None, None, "4", None, "N", "NENHUMA"), ("2", None, None, None, None, None, None, None, "N", "NENHUMA")] + out = uf.assess_from_connection(FakeConn(products=rows), {"crt": "3"}) + assert kinds(out["findings"]["1"]) == [] and kinds(out["findings"]["2"]) == [] + w = out["context"]["non_canonical_warnings"] + assert w["products"] == 1 and w["by_type"] == {"CST_COFINS_NON_CANONICAL_FORMAT": 1, "CST_PIS_NON_CANONICAL_FORMAT": 1} and w["samples"][0]["product_code"] == "1" + assert out["context"]["pis_cofins_applicability"] == {"OPTIONAL": 2} diff --git a/agent/tests/test_zero_touch_agent.py b/agent/tests/test_zero_touch_agent.py index 2f513cf..729ad62 100644 --- a/agent/tests/test_zero_touch_agent.py +++ b/agent/tests/test_zero_touch_agent.py @@ -185,3 +185,26 @@ def test_partial_reprocess_does_not_claim_a_full_fiscal_evaluation(env_setup, mo hub, state = env_setup result, _ = run_job("REPROCESS_PRODUCTS", [product("1")], state, monkeypatch) assert result["fiscal_context"] is None and result["operational"] is None + + +def test_rtc_toggle_between_two_syncs_is_followed_by_the_next_job(env_setup, monkeypatch): + """The Agent asks the database EVERY sync: 0 -> 1 makes the next job report RTC blockers, 1 -> 0 makes the following job report none (Hub then closes the old ones).""" + from gti_fiscal_agent import unified_fiscal + switch = {"on": False} + + def assess(conn, company, **kw): + found = _findings(**{"1": [("CCLASSTRIB_MISSING", "RTC")]}) if switch["on"] else {} + return {"context": {"rtc_enabled": switch["on"], "rtc_supported": True, "rtc_catalog_valid": True, "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA", "rtc_observed_at": "2026-09-24T20:00:00+00:00"}, "findings": found} + monkeypatch.setattr(unified_fiscal, "assess_from_connection", assess) + hub, state = env_setup + products = [product("1"), product("2")] + off1, _ = run_job("INITIAL_SYNC", products, state, monkeypatch) + switch["on"] = True + on, _ = run_job("SYNC_INCREMENTAL", products, state, monkeypatch) # nothing changed in C000025: the delta cache must not hide the RTC change + switch["on"] = False + off2, _ = run_job("SYNC_INCREMENTAL", products, state, monkeypatch) + assert off1["blocked_products"] == [] and off1["fiscal_context"]["rtc_enabled"] is False + assert [b["field"] for b in on["blocked_products"]] == ["RTC"] and on["fiscal_context"]["rtc_enabled"] is True and on["operational"]["BLOCKED_FISCAL_DATA"] == 1 + assert on["fiscal_context"]["rtc_observed_at"] and on["fiscal_context"]["rtc_source"] == "PARAMETROS.USAR_REFORMA_TRIBUTARIA" + assert off2["blocked_products"] == [] and off2["fiscal_context"]["rtc_enabled"] is False and off2["fiscal_context"]["evaluated_all"] is True + assert off2["operational"]["COMPLETE_PRODUCTS"] == 2 diff --git a/central/app/panel_presentation.py b/central/app/panel_presentation.py index d2239ee..f63d3ec 100644 --- a/central/app/panel_presentation.py +++ b/central/app/panel_presentation.py @@ -162,9 +162,18 @@ def priority_label(value): 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).""" + """Discreet Reforma Tributaria status from the last completed job's `fiscal_context`, exactly as the Agent OBSERVED it in that installation's own database. + 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 + observed = None + if context.get("rtc_observed_at"): + try: + observed = format_ui_datetime(datetime.fromisoformat(str(context["rtc_observed_at"]).replace("Z", "+00:00"))) + except ValueError: + observed = None + source = context.get("rtc_source") or "PARAMETROS.USAR_REFORMA_TRIBUTARIA" 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"))} + "catalog_valid": bool(context.get("rtc_catalog_valid")), "observed_at": observed, "conflict": bool(context.get("rtc_param_conflict")), + "title": f"Lido no banco do cliente ({source})" + (f" em {observed}" if observed else "") + (" - linhas de PARAMETROS divergentes: RTC tratada como desativada" if context.get("rtc_param_conflict") else "")} diff --git a/central/app/unified_fiscal_coverage.py b/central/app/unified_fiscal_coverage.py index fea3c17..f113694 100644 --- a/central/app/unified_fiscal_coverage.py +++ b/central/app/unified_fiscal_coverage.py @@ -54,17 +54,25 @@ def incidence_kind(value) -> str: return "UNKNOWN" +def canonical_cst(value) -> str | None: + """The official 2-digit CST this text stands for, or None. Exactly two official digits, or ONE digit whose zero-padded form is an official code + ("4" -> "04"). Nothing else is ever normalised ("049", "000", "2x" stay invalid).""" + code = _text(value) + if code in CST_DOMAIN: + return code + if len(code) == 1 and code.isdigit() and code.zfill(2) in CST_DOMAIN: + return code.zfill(2) + return None + + def validate_cst(value) -> str: + """EMPTY | VALID | VALID_AFTER_NORMALIZATION | INVALID.""" 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 + if code in CST_DOMAIN: + return "VALID" + return "VALID_AFTER_NORMALIZATION" if canonical_cst(code) else "INVALID" def validate_rate(value) -> str: @@ -82,56 +90,71 @@ def validate_rate(value) -> str: 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. + Decided ONLY from signals that are independent of the six CST/rate fields - an empty field is a data state, never proof of "not applicable": + * the company regime (CRT); + * the product's own declaration: PISCOFINS flag and INCIDENCIA_PISCOFINS; + * official rules (LC 123/2006 art. 13 IV/V and art. 18 par. 4-A I; Leis 9.718/98, 10.637/02, 10.833/03; SPED 4.3.x for the CST domain/semantics). + + REQUIRED the product declares a special treatment (flag S, or incidence MONOFASICA / SUBSTITUICAO): the exit CST must be valid, whatever the fields hold. + NOT_APPLICABLE Simples family (CRT 1/2/4) with no special treatment declared: PIS/Cofins are collected inside the DAS (LC 123 art. 13), so the product carries no + obligation. The same verdict holds whether the fields are empty or typed. + OPTIONAL taxed regime (CRT 3 or other) AND the product explicitly declares "no special treatment" (flag N + incidence NENHUMA): the tax applies but the + company default covers it, so the product default is not mandatory; anything typed must still be valid. + NEEDS_REVIEW the declaration cannot be read: incidence text outside the SGV vocabulary, or a taxed regime with a blank flag/incidence. """ 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) + raw_incidence = _text(product.get("INCIDENCIA_PISCOFINS")) + kind = incidence_kind(raw_incidence) 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"} + if _text(crt) in SIMPLES_FAMILY: + return {"state": "NOT_APPLICABLE", "basis": "SIMPLES_DAS_NO_SPECIAL_TREATMENT"} + if flag == "N" and raw_incidence: + return {"state": "OPTIONAL", "basis": "TAXED_REGIME_COMPANY_DEFAULT_COVERS"} + return {"state": "NEEDS_REVIEW", "basis": "NO_DECLARATION_IN_TAXED_REGIME"} def evaluate_pis_cofins(product: dict, crt) -> dict: - """{"applicability": {...}, "blockers": [(type, reason)]}. Entry and exit are separate; a missing OPTIONAL value never blocks.""" + """{"applicability": {...}, "blockers": [(type, reason)], "warnings": [(type, reason)]}. Entry and exit are separate; PIS and COFINS independent. + Blockers exist only where the field applies (REQUIRED / OPTIONAL). Non-blocking warnings report data-quality facts (non-canonical or ignored values).""" app = pis_cofins_applicability(product, crt) blockers: list[tuple[str, str]] = [] - if app["state"] in ("NOT_APPLICABLE", "NEEDS_REVIEW"): - return {"applicability": app, "blockers": blockers} + warnings: list[tuple[str, str]] = [] kind = incidence_kind(product.get("INCIDENCIA_PISCOFINS")) flag = _text(product.get("PISCOFINS")).upper() + applies = app["state"] in ("REQUIRED", "OPTIONAL") 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 ""))) + for slot, value, state in (("ENTRY", entry, entry_state), ("EXIT", exit_, exit_state)): + if state == "VALID_AFTER_NORMALIZATION": + warnings.append((f"CST_{tax}_NON_CANONICAL_FORMAT", f"{slot}:{_text(value)}->{canonical_cst(value)}")) + elif state == "INVALID": + if applies: + blockers.append((f"{tax}_CST_INVALID", slot)) + else: + warnings.append((f"CST_{tax}_INVALID_IGNORED", f"{slot}:{_text(value)}")) + if not applies: + continue 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_) + code = canonical_cst(exit_) + if code: 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": + if applies and 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): + if applies and 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)} + return {"applicability": app, "blockers": _dedupe(blockers), "warnings": _dedupe(warnings)} def _dedupe(items): @@ -201,11 +224,21 @@ def evaluate_product(product: dict, company: dict | None, rtc: dict | None, toda 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"]] + warnings = [{"type": kind, "reason": reason} for kind, reason in pis["warnings"]] 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, + "basis": {"PIS_COFINS": pis["applicability"]["basis"], "RTC": rtc_result["applicability"]["basis"]}, "mandatory_fields": mandatory, "warnings": warnings, "needs_review": pis["applicability"]["state"] == "NEEDS_REVIEW"} + + +def legal_effective_context(crt) -> dict: + """INFORMATIONAL ONLY (never a blocker, never part of completeness). The operational rule stays ERP_RTC_ENABLED; this records what the legislation + says about IBS/CBS in documents for the company regime so a difference in timing can be explained without inventing a rule.""" + simples = _text(crt) in SIMPLES_FAMILY + return {"crt": _text(crt) or None, "informational_only": True, + "ibs_cbs_in_documents": "FROM_2027_FOR_SIMPLES_ORIENTATION_PENDING" if simples else ("MANDATORY_FROM_2026-01-01" if _text(crt) == "3" else "UNKNOWN_REGIME"), + "sources": ["NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT; IBS/CBS taxation of these taxpayers only from 2027", "LC 214/2025 art. 348"]} diff --git a/central/templates/agent_detail.html b/central/templates/agent_detail.html index 85a98c8..2af1a4c 100644 --- a/central/templates/agent_detail.html +++ b/central/templates/agent_detail.html @@ -8,7 +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") }}
{% 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 %}
+
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/tests/test_control_panel_pages.py b/central/tests/test_control_panel_pages.py index 91bc87e..1a53e1c 100644 --- a/central/tests/test_control_panel_pages.py +++ b/central/tests/test_control_panel_pages.py @@ -575,3 +575,15 @@ def test_pis_cofins_and_rtc_blockers_appear_in_the_same_queue_and_dashboard_brea 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 + + +def test_rtc_status_shows_where_and_when_the_agent_observed_it(): + client, agent_id, _ = _panel_client() + _complete_sync(client, agent_id, {"rtc_enabled": True, "rtc_supported": True, "rtc_catalog_valid": True, "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA", "rtc_observed_at": "2026-09-24T20:00:00+00:00"}) + text = client.get(f"/agents/{agent_id}").text + assert 'title="Lido no banco do cliente (PARAMETROS.USAR_REFORMA_TRIBUTARIA) em ' in text and ">Ativa<" in text + _complete_sync(client, agent_id, {"rtc_enabled": False, "rtc_param_conflict": True, "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA"}) + conflict = client.get(f"/agents/{agent_id}").text + assert "linhas de PARAMETROS divergentes" in conflict and ">Desativada<" in conflict + for legacy in ("Escopo anterior", "Escopo ampliado", "Legacy coverage", "Expanded coverage"): + assert legacy not in conflict diff --git a/central/tests/test_unified_fiscal_coverage.py b/central/tests/test_unified_fiscal_coverage.py index f8984fe..476184c 100644 --- a/central/tests/test_unified_fiscal_coverage.py +++ b/central/tests/test_unified_fiscal_coverage.py @@ -1,19 +1,25 @@ 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) +from central.app.unified_fiscal_coverage import (blocker_category, canonical_cst, evaluate_product, incidence_kind, legal_effective_context, pis_cofins_applicability, rtc_enabled, + validate_cclasstrib, validate_cst) 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"} +FIELDS = ("COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS") def kinds(result): return {b["type"] for b in result["blockers"]} +def warns(result): + return {w["type"] for w in result["warnings"]} + + # ---------------------------------------------------------------- RTC on / off def test_rtc_off_ignores_cclasstrib_completely(): for value in (None, "", "12x", "999999", "000001"): @@ -48,14 +54,85 @@ def test_rtc_enabled_needs_schema_and_erp_switch(): 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_legal_context_is_informational_and_never_creates_a_blocker(): + simples, normal = legal_effective_context("1"), legal_effective_context("3") + assert simples["informational_only"] and "2027" in simples["ibs_cbs_in_documents"] and normal["ibs_cbs_in_documents"] == "MANDATORY_FROM_2026-01-01" + assert legal_effective_context(None)["ibs_cbs_in_documents"] == "UNKNOWN_REGIME" + # the operational rule is the ERP switch only: a Simples company with RTC on is validated, one with RTC off is not, whatever the legal timing says + assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, SIMPLES, ON)) == {"CCLASSTRIB_MISSING"} and kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, SIMPLES, OFF)) == set() +# ---------------------------------------------------------------- PIS / COFINS applicability is NOT circular +def test_applicability_never_depends_on_whether_the_six_fields_are_empty(): + typed = {f: "49" if "COD" in f else "0" for f in FIELDS} + for crt in ("1", "2", "4", "3", None): + for base in (EMPTY, {"CODIGO": "1", "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, {"CODIGO": "1", "PISCOFINS": "", "INCIDENCIA_PISCOFINS": ""}): + empty_state = pis_cofins_applicability(base, crt) + filled_state = pis_cofins_applicability({**base, **typed}, crt) + assert empty_state == filled_state, (crt, base) # same verdict with the fields empty or typed: emptiness proves nothing + + +def test_empty_fields_with_applicable_context_is_a_missing_blocker(): + for crt in ("1", "3"): + r = evaluate_product({**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, {"crt": crt}, OFF) + assert r["applicability"]["PIS_COFINS"] == "REQUIRED" and kinds(r) == {"PIS_CST_MISSING", "COFINS_CST_MISSING"} + + +def test_empty_fields_with_provably_not_applicable_context_is_not_applicable(): + for crt in ("1", "2", "4"): + r = evaluate_product(EMPTY, {"crt": crt}, OFF) + assert r["applicability"]["PIS_COFINS"] == "NOT_APPLICABLE" and r["basis"]["PIS_COFINS"] == "SIMPLES_DAS_NO_SPECIAL_TREATMENT" and r["blockers"] == [] and not r["needs_review"] + assert evaluate_product({"CODIGO": "1"}, SIMPLES, OFF)["blockers"] == [] # columns absent in an old schema, Simples: nothing to require + + +def test_empty_fields_with_insufficient_context_need_review_and_never_default_to_complete(): + for company in (NORMAL, {"crt": None}, None): + r = evaluate_product({"CODIGO": "1", "PISCOFINS": "", "INCIDENCIA_PISCOFINS": ""}, company, OFF) + assert r["applicability"]["PIS_COFINS"] == "NEEDS_REVIEW" and r["needs_review"] and r["blockers"] == [] + state = classify_product_state({"CODIGO": "1"}, {"status": "EXACT_NCM"}, fiscal=r)["state"] + assert state == "NEEDS_REVIEW" # counted as UNCLASSIFIED, never inflating coverage + + +def test_taxed_regime_with_explicit_no_special_treatment_is_optional_and_typed_values_must_be_valid(): + r = evaluate_product(EMPTY, NORMAL, OFF) + assert r["applicability"]["PIS_COFINS"] == "OPTIONAL" and r["blockers"] == [] + assert kinds(evaluate_product({**EMPTY, "COD_SAIDA_PIS": "2x"}, NORMAL, OFF)) == {"PIS_CST_INVALID"} + + +def test_unknown_incidence_text_needs_review_in_every_regime(): + for crt in ("1", "3"): + r = evaluate_product({**EMPTY, "INCIDENCIA_PISCOFINS": "OUTRA COISA"}, {"crt": crt}, OFF) + assert r["applicability"]["PIS_COFINS"] == "NEEDS_REVIEW" and r["needs_review"] and r["blockers"] == [] + + +def test_simples_typed_garbage_is_ignored_as_a_warning_not_a_blocker(): + 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"] == "NOT_APPLICABLE" and r["blockers"] == [] + assert warns(r) == {"CST_PIS_NON_CANONICAL_FORMAT", "CST_COFINS_NON_CANONICAL_FORMAT"} # "2" is one digit of official "02": normalisable + bad = evaluate_product({**EMPTY, "COD_ENTRADA_COFINS": "000", "COD_SAIDA_COFINS": "060"}, SIMPLES, OFF) + assert bad["blockers"] == [] and warns(bad) == {"CST_COFINS_INVALID_IGNORED"} + + +# ---------------------------------------------------------------- CST normalisation +def test_canonical_cst_accepts_only_one_digit_with_an_official_zero_padded_form(): + assert canonical_cst("4") == "04" and canonical_cst("04") == "04" and canonical_cst(" 4 ") == "04" and canonical_cst("9") == "09" and canonical_cst("1") == "01" + for text in ("0", "049", "000", "060", "2x", "x", "", None, "10", "12", "00"): + assert canonical_cst(text) is None, text + assert validate_cst("4") == "VALID_AFTER_NORMALIZATION" and validate_cst("04") == "VALID" and validate_cst("049") == "INVALID" and validate_cst("") == "EMPTY" + + +def test_unpadded_cst_is_semantically_valid_warns_and_is_used_canonically_for_the_rules(): + row = {**EMPTY, "COD_SAIDA_PIS": "4", "COD_SAIDA_COFINS": "4"} + r = evaluate_product(row, NORMAL, OFF) + assert r["blockers"] == [] and warns(r) == {"CST_PIS_NON_CANONICAL_FORMAT", "CST_COFINS_NON_CANONICAL_FORMAT"} + mono = {**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA", "COD_SAIDA_PIS": "4", "COD_SAIDA_COFINS": "4"} + assert kinds(evaluate_product(mono, NORMAL, OFF)) == set() # "4" -> "04" is compatible with MONOFASICA + assert kinds(evaluate_product({**mono, "COD_SAIDA_PIS": "1", "ALIQ_PIS": "1,65"}, NORMAL, OFF)) == {"PIS_CST_INCIDENCE_MISMATCH"} # "1" -> "01" is not + assert kinds(evaluate_product({**EMPTY, "COD_SAIDA_PIS": "049"}, NORMAL, OFF)) == {"PIS_CST_INVALID"} # ambiguous: still a blocker + + +# ---------------------------------------------------------------- PIS / COFINS rules 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) @@ -69,13 +146,6 @@ def test_st_incidence_with_taxable_cst_is_a_mismatch_and_pis_cofins_are_independ 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"} @@ -85,17 +155,15 @@ def test_rate_rules_follow_the_cst(): 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(): +def test_inconsistent_flags(): 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" + assert incidence_kind("SUBST.TRIBUTÁRIA") == "SUBSTITUICAO" and incidence_kind(None) == "NONE" and incidence_kind(" nenhuma ") == "NONE" and incidence_kind("???") == "UNKNOWN" # ---------------------------------------------------------------- ONE definition of complete, ONE queue @@ -105,7 +173,8 @@ def test_blocker_categories_feed_the_single_dashboard_breakdown(): 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": "???"}] + products = [{**EMPTY, "CODIGO": "1"}, {**EMPTY, "CODIGO": "2", "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, {**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] @@ -118,14 +187,7 @@ def test_unified_coverage_invariant_and_rtc_toggle_changes_only_rtc_products(): def test_fiscal_blockers_never_hide_or_override_a_decision_blocker(): - fiscal = evaluate_product({**EMPTY, "COD_SAIDA_PIS": "2"}, SIMPLES, OFF) + fiscal = evaluate_product({**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, 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 84a4a59..26f87a8 100644 --- a/central/tests/test_zero_touch_hub.py +++ b/central/tests/test_zero_touch_hub.py @@ -236,3 +236,31 @@ def test_partial_reprocess_never_closes_fiscal_blockers_of_other_products(): 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 + + +def test_rtc_on_then_off_and_pis_fix_leave_no_stale_open_blockers_and_never_duplicate(): + from central.app.models import BlockedProduct + client, engine = _client() + hb(client) + rtc_missing = _fiscal_blocker("20", "RTC", "CCLASSTRIB_MISSING", "r1") + pis_invalid = _fiscal_blocker("21", "PIS", "PIS_CST_INVALID", "p1") + op = {**OPERATIONAL, "BLOCKED_MISSING_EVIDENCE": 0, "BLOCKED_CONFLICT": 0} + finish(client, engine, {"operational": {**op, "COMPLETE": 519, "BLOCKED_FISCAL_DATA": 2, "SAFE_BLOCKED": 2}, "blocked_products": [rtc_missing, pis_invalid, dict(rtc_missing)], "resolved_products": [], + "fiscal_context": {"rtc_enabled": True, "evaluated_all": True}}) + with Session(engine) as s: + assert len(s.scalars(select(BlockedProduct)).all()) == 2 # the repeated blocker is one row + age_last_sync(engine, 4000) + hb(client) + # RTC switched off in the ERP AND the PIS value was normalised/fixed: the next full pass reports nothing + finish(client, engine, {"operational": {**op, "COMPLETE": 521}, "blocked_products": [], "resolved_products": [], "fiscal_context": {"rtc_enabled": False, "evaluated_all": True}}) + with Session(engine) as s: + rows = s.scalars(select(BlockedProduct)).all() + assert [r.resolved_at is not None for r in rows] == [True, True] and {r.resolution_source for r in rows} == {"AUTO_REEVALUATION"} + assert s.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_(None))).all() == [] + age_last_sync(engine, 4000) + hb(client) + # RTC comes back on: the SAME blocker (same fingerprint) reopens instead of being duplicated + finish(client, engine, {"operational": {**op, "COMPLETE": 520, "BLOCKED_FISCAL_DATA": 1, "SAFE_BLOCKED": 1}, "blocked_products": [rtc_missing], "resolved_products": [], "fiscal_context": {"rtc_enabled": True, "evaluated_all": True}}) + with Session(engine) as s: + rows = s.scalars(select(BlockedProduct)).all() + assert len(rows) == 2 and [(r.product_code, r.resolved_at is None) for r in rows] == [("20", True), ("21", False)] diff --git a/docs/pis_cofins_applicability.md b/docs/pis_cofins_applicability.md index 1d9e768..5bb9d4b 100644 --- a/docs/pis_cofins_applicability.md +++ b/docs/pis_cofins_applicability.md @@ -1,52 +1,46 @@ # 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`). +Implementação: `central/app/unified_fiscal_coverage.py` (`pis_cofins_applicability`, `evaluate_pis_cofins`, `canonical_cst`). Somente leitura; nenhum valor é preenchido nem normalizado no ERP. +`ACCESSED_AT` das fontes: **2026-09-24** (hashes em `docs/sources/SOURCES_MANIFEST_UNIFIED_FISCAL_V1.json` e `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. +- São *defaults de cadastro* (tela Produtos > Config. Impostos > CST PIS/COFINS; binding 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). +- 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**. TESTESOFICIAIS: os 3.336 itens com CST `01` são todos de dezembro/2022 (dias 01–31, notas contíguas), independentes do produto (`PISCOFINS=N`, campos vazios); antes e depois os itens saem em branco; o produto com CST `04` cadastrado emitiu em branco. Nas 4 bases de clientes (~570 mil itens) 100% saem em branco. O emissor de NFC-e não existe localmente (o executável SGV não contém `CST_PIS_COFINS`), então a regra exata do documento **não é comprovável** aqui. -## 2. Modelo de aplicabilidade (por produto) +## 2. Modelo de aplicabilidade — não circular +A aplicabilidade usa **somente sinais independentes dos seis campos**: regime da empresa (CRT), flag `PISCOFINS`, `INCIDENCIA_PISCOFINS` e as regras oficiais abaixo. **Campo vazio é estado de dado, nunca prova de não aplicabilidade**: o veredito é idêntico com os campos vazios ou preenchidos (teste `test_applicability_never_depends_on_whether_the_six_fields_are_empty`). -| Estado | Condição (dados do próprio produto) | Efeito | +| Estado | Condição | 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" | +| `REQUIRED` | flag `S`, ou incidência MONOFÁSICA / SUBST.TRIBUTÁRIA (declaração própria do produto) | CST de **saída** válido obrigatório (vazio ⇒ `*_CST_MISSING`); alíquota conforme o CST | +| `NOT_APPLICABLE` | CRT 1/2/4 (Simples) **sem** tratamento especial declarado | não entra na completude; valor digitado vira só *warning* | +| `OPTIONAL` | regime tributado (CRT 3 ou outro) **e** declaração explícita "sem tratamento especial" (flag `N` + incidência NENHUMA) | nada obrigatório; o que existir tem de ser válido | +| `NEEDS_REVIEW` | incidência fora do vocabulário do SGV, ou regime tributado com flag/incidência em branco (declaração ilegível) | ú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 quê (fontes): Simples — LC 123/2006 art. 13, IV e V (Cofins e PIS no DAS) e art. 18, §4º-A, I (segregação da receita monofásica): o produto sem tratamento especial não tem PIS/COFINS de documento a calcular. Regime normal — Lei 9.718/1998 arts. 2º–3º (cumulativo), Lei 10.637/2002 art. 1º (PIS) e Lei 10.833/2003 art. 1º (Cofins) (não cumulativo): o tributo incide, mas o perfil (cumulativo/não cumulativo) é fato da empresa; o default do produto é opcional porque o padrão vem da empresa. Regimes especiais por NCM (SPED 4.3.10–4.3.15, Lei 10.147/2000) servem para *coerência de CST* e para o motor de correção — não tornam obrigatório um produto que não declara nada, porque o SGV não usa o campo na emissão e a extração das tabelas por NCM é parcial. Documento — NT 2025.002-RTC v1.51, Grupos Q/S: o grupo é obrigatório no item da NF-e (rejeições 745/748), o que vale para o documento e não para o cadastro. -### 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. +## 3. CST: normalização canônica (somente leitura) +`canonical_cst`: aceita **exatamente** 2 dígitos oficiais (SPED 4.3.3/4.3.4: 01–09, 49, 50–56, 60–67, 70–75, 98, 99) ou **um** dígito cujo preenchimento com zero à esquerda seja um código oficial (`4` → `04`). Nada mais é normalizado (`049`, `000`, `060`, `2x`, `0` continuam inválidos). +- `VALID_AFTER_NORMALIZATION`: semanticamente válido para a cobertura e para as regras (CST × incidência, alíquota); gera *warning* não bloqueante `CST_PIS_NON_CANONICAL_FORMAT` / `CST_COFINS_NON_CANONICAL_FORMAT` (contagem e amostras em `fiscal_context.non_canonical_warnings`). +- Nada é escrito no Firebird; escrever `04` seria decisão futura. +## 4. Blockers (só onde o campo se aplica: REQUIRED/OPTIONAL) | 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_INVALID` | CST digitado que não é canônico nem normalizável | | `{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_MISSING` | CST de saída (canônico) 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}_CST_INCIDENCE_MISMATCH` | 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). +Warnings (nunca bloqueiam): `CST_*_NON_CANONICAL_FORMAT`, `CST_*_INVALID_IGNORED` (valor inválido digitado em produto `NOT_APPLICABLE`). -## 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. Regimes no corpus SGV +CRT 1 (CONSTRUCAO, MMMODAS, UNIAO, TESTESOFICIAIS) e CRT 3 (RINCAO, via EMISSOR). Nenhuma regra assume CRT 1. -## 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). +## 6. Campos ignorados quando não aplicáveis / write targets candidatos +Produto `NOT_APPLICABLE`: os 6 campos, `PISCOFINS` e `INCIDENCIA_PISCOFINS` não contam. Candidatos futuros (sem escrita nesta versão): `COD_SAIDA_PIS`, `COD_SAIDA_COFINS` e a normalização `4→04`. diff --git a/docs/rtc_pis_cofins_model.md b/docs/rtc_pis_cofins_model.md index ea5add2..f017490 100644 --- a/docs/rtc_pis_cofins_model.md +++ b/docs/rtc_pis_cofins_model.md @@ -35,8 +35,19 @@ O schema do produto não modela alíquota IBS/CBS, valor IBS/CBS nem `cCredPres` 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. -## 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`). +## Fonte da verdade (ERP_RTC_ENABLED) +Quem decide é o **Agent**, lendo o banco real daquela instalação **a cada varredura** (nunca uma decisão guardada no Hub, nunca cache local): +`SELECT FIRST 20 USAR_REFORMA_TRIBUTARIA FROM PARAMETROS` — todas as linhas. Uma linha (ou linhas iguais) truthy ⇒ ativa; linhas divergentes ⇒ **não** ativa e `rtc_param_conflict=true` (nunca "adivinha" ligado); parâmetro ausente/nulo ⇒ desativada. +O estado observado vai em `fiscal_context` do resultado do job e aparece no painel com proveniência (tooltip): `rtc_enabled`/`erp_rtc_enabled`, `rtc_source` (`PARAMETROS.USAR_REFORMA_TRIBUTARIA`), `rtc_param`, `rtc_param_rows`, `rtc_param_conflict`, `rtc_observed_at` (UTC), `rtc_supported`, `rtc_catalog_valid`. +O fingerprint dos blockers inclui o estado RTC: 0→1 cria blockers novos; 1→0 faz a varredura seguinte não reenviar os RTC e o Hub os fecha (`AUTO_REEVALUATION`). + +## ERP_RTC_ENABLED × LEGAL_EFFECTIVE_CONTEXT (registrados separadamente) +- `ERP_RTC_ENABLED` é a regra operacional do produto e a única que entra na completude. +- `legal_effective_context(crt)` é **informativo** (`informational_only=true`, nunca blocker): para CRT 1/2/4 a NT 2025.002 v1.51 diz que as orientações de IBS/CBS em documentos virão em NT futura e que a tributação desses contribuintes ocorre a partir de 2027 (LC 214/2025 art. 348); para CRT 3 o preenchimento é obrigatório desde 01/01/2026. Uma empresa Simples com o interruptor ligado no SGV é validada; uma com o interruptor desligado não é, seja qual for o calendário legal. + +## Achado de corpus e causa da divergência de RTC (somente leitura) +Estado atual do parâmetro nas cópias locais: `CONSTRUCAO=1`, `TESTESOFICIAIS=1`, `MMMODAS=0`, `RINCAO=0`, `UNIAO=0` (cada uma com **uma** linha em `PARAMETROS`, `ODS 11.2`, Firebird 2.5.9). O discovery de `TESTESOFICIAIS.FDB` registrou `0` às 15:25 de 24/09 e a leitura de hoje (mesmo arquivo, mesmas 521 linhas) retorna `1`. Causas descartadas: linha errada / várias linhas (há 1), cache do Hub (a leitura é direta no Firebird, o Agent lê ao vivo), outro arquivo (mesmo caminho canônico). Causa real comprovada: **o valor mudou no próprio banco depois do discovery** (mesmo arquivo, mesma linha única). O autor da mudança não é rastreável (o Firebird 2.5 não guarda histórico de valores e o SGV não tem tabela de log de parâmetros); a hipótese mais simples é a alteração do parâmetro "usar reforma tributária" no SGV 5.1.0.4, que é quem lê esse parâmetro. Por isso o Agent nunca reaproveita um estado anterior. +Consequência medida (TESTESOFICIAIS, RTC ligada de fato): 521/521 produtos com `CCLASSTRIB_MISSING` ⇒ cobertura real 0%; com RTC desligada (estado do piloto) 94,82%. ## 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 index c2c5895..1e402b4 100644 --- a/docs/runs/unified_coverage_e2e.json +++ b/docs/runs/unified_coverage_e2e.json @@ -1,4 +1,5 @@ { + "mode": "FULL (Hub decision + fiscal)", "database": "TESTESOFICIAIS", "crt": "1", "products": 521, @@ -19,21 +20,18 @@ "UNCLASSIFIED_PRODUCTS": 0, "COVERAGE_PERCENT": 0.0, "INVARIANT_PASS": true, - "PIS_BLOCKERS": 2, - "COFINS_BLOCKERS": 2, + "PIS_BLOCKERS": 1, + "COFINS_BLOCKERS": 1, "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 + "PIS_CST_INCIDENCE_MISMATCH": 1 }, "applicability": { - "NOT_APPLICABLE": 518, - "REQUIRED": 2, - "OPTIONAL": 1 + "NOT_APPLICABLE": 519, + "REQUIRED": 2 }, "rtc_applicability": { "REQUIRED": 521 @@ -281,11 +279,26 @@ ] } ], + "non_canonical_warnings": 1, "context": { "rtc_enabled": true, + "erp_rtc_enabled": true, "rtc_supported": true, "rtc_catalog_valid": true, - "rtc_param": 1, + "rtc_param": "1", + "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA", + "rtc_param_rows": 1, + "rtc_param_conflict": false, + "rtc_observed_at": "2026-09-24T20:40:30+00:00", + "legal_effective_context": { + "crt": "1", + "informational_only": true, + "ibs_cbs_in_documents": "FROM_2027_FOR_SIMPLES_ORIENTATION_PENDING", + "sources": [ + "NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT; IBS/CBS taxation of these taxpayers only from 2027", + "LC 214/2025 art. 348" + ] + }, "catalog_integrity": { "classes": 145, "cst": 18, @@ -296,37 +309,67 @@ "valid": true }, "crt": "1", - "products_evaluated": 521 + "products_evaluated": 521, + "pis_cofins_applicability": { + "NOT_APPLICABLE": 519, + "REQUIRED": 2 + }, + "non_canonical_warnings": { + "products": 1, + "by_type": { + "CST_COFINS_NON_CANONICAL_FORMAT": 2, + "CST_PIS_NON_CANONICAL_FORMAT": 2 + }, + "samples": [ + { + "product_code": "001434", + "type": "CST_PIS_NON_CANONICAL_FORMAT", + "reason": "ENTRY:2->02" + }, + { + "product_code": "001434", + "type": "CST_PIS_NON_CANONICAL_FORMAT", + "reason": "EXIT:2->02" + }, + { + "product_code": "001434", + "type": "CST_COFINS_NON_CANONICAL_FORMAT", + "reason": "ENTRY:2->02" + }, + { + "product_code": "001434", + "type": "CST_COFINS_NON_CANONICAL_FORMAT", + "reason": "EXIT:2->02" + } + ] + } } }, "RTC_OFF_SIMULATION": { "rtc_enabled": false, "TOTAL_PRODUCTS": 521, - "COMPLETE_PRODUCTS": 493, - "SAFE_BLOCKED_PRODUCTS": 28, + "COMPLETE_PRODUCTS": 494, + "SAFE_BLOCKED_PRODUCTS": 27, "FAILED_PRODUCTS": 0, "UNCLASSIFIED_PRODUCTS": 0, - "COVERAGE_PERCENT": 94.63, + "COVERAGE_PERCENT": 94.82, "INVARIANT_PASS": true, - "PIS_BLOCKERS": 2, - "COFINS_BLOCKERS": 2, + "PIS_BLOCKERS": 1, + "COFINS_BLOCKERS": 1, "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 + "PIS_CST_INCIDENCE_MISMATCH": 1 }, "applicability": { - "NOT_APPLICABLE": 518, - "REQUIRED": 2, - "OPTIONAL": 1 + "NOT_APPLICABLE": 519, + "REQUIRED": 2 }, "rtc_applicability": { "NOT_APPLICABLE": 521 }, - "changed_products": 2, + "changed_products": 1, "examples": [ { "product": "000700", @@ -336,17 +379,9 @@ "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" - ] } - ] + ], + "non_canonical_warnings": 1 }, "RTC_ON_SIMULATION": { "rtc_enabled": true, @@ -357,21 +392,18 @@ "UNCLASSIFIED_PRODUCTS": 0, "COVERAGE_PERCENT": 0.0, "INVARIANT_PASS": true, - "PIS_BLOCKERS": 2, - "COFINS_BLOCKERS": 2, + "PIS_BLOCKERS": 1, + "COFINS_BLOCKERS": 1, "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 + "PIS_CST_INCIDENCE_MISMATCH": 1 }, "applicability": { - "NOT_APPLICABLE": 518, - "REQUIRED": 2, - "OPTIONAL": 1 + "NOT_APPLICABLE": 519, + "REQUIRED": 2 }, "rtc_applicability": { "REQUIRED": 521 @@ -618,7 +650,8 @@ "CCLASSTRIB_MISSING" ] } - ] + ], + "non_canonical_warnings": 1 } }, "SAFETY": { diff --git a/docs/runs/unified_coverage_e2e_CONSTRUCAO.json b/docs/runs/unified_coverage_e2e_CONSTRUCAO.json index 8a2bcf3..fddb360 100644 --- a/docs/runs/unified_coverage_e2e_CONSTRUCAO.json +++ b/docs/runs/unified_coverage_e2e_CONSTRUCAO.json @@ -276,11 +276,26 @@ ] } ], + "non_canonical_warnings": 0, "context": { "rtc_enabled": true, + "erp_rtc_enabled": true, "rtc_supported": true, "rtc_catalog_valid": true, - "rtc_param": 1, + "rtc_param": "1", + "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA", + "rtc_param_rows": 1, + "rtc_param_conflict": false, + "rtc_observed_at": "2026-09-24T20:40:41+00:00", + "legal_effective_context": { + "crt": "1", + "informational_only": true, + "ibs_cbs_in_documents": "FROM_2027_FOR_SIMPLES_ORIENTATION_PENDING", + "sources": [ + "NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT; IBS/CBS taxation of these taxpayers only from 2027", + "LC 214/2025 art. 348" + ] + }, "catalog_integrity": { "classes": 145, "cst": 18, @@ -291,7 +306,15 @@ "valid": true }, "crt": "1", - "products_evaluated": 3712 + "products_evaluated": 3712, + "pis_cofins_applicability": { + "NOT_APPLICABLE": 3712 + }, + "non_canonical_warnings": { + "products": 0, + "by_type": {}, + "samples": [] + } } }, "RTC_OFF_SIMULATION": { @@ -315,7 +338,8 @@ "NOT_APPLICABLE": 3712 }, "changed_products": 0, - "examples": [] + "examples": [], + "non_canonical_warnings": 0 }, "RTC_ON_SIMULATION": { "rtc_enabled": true, @@ -581,7 +605,8 @@ "CCLASSTRIB_MISSING" ] } - ] + ], + "non_canonical_warnings": 0 } }, "SAFETY": { diff --git a/docs/runs/unified_coverage_e2e_MMMODAS.json b/docs/runs/unified_coverage_e2e_MMMODAS.json index cacf261..039ba78 100644 --- a/docs/runs/unified_coverage_e2e_MMMODAS.json +++ b/docs/runs/unified_coverage_e2e_MMMODAS.json @@ -1,7 +1,7 @@ { "mode": "FISCAL_ONLY (no Hub decision)", "database": "MMMODAS", - "crt": "2", + "crt": "1", "products": 20997, "fiscal_products_read": 20997, "BEFORE": { @@ -26,19 +26,33 @@ "OTHER_BLOCKERS": 0, "blockers_by_type": {}, "applicability": { - "OPTIONAL": 1, - "NOT_APPLICABLE": 20995 + "NOT_APPLICABLE": 20996 }, "rtc_applicability": { "NOT_APPLICABLE": 20996 }, "changed_products": 0, "examples": [], + "non_canonical_warnings": 0, "context": { "rtc_enabled": false, + "erp_rtc_enabled": false, "rtc_supported": true, "rtc_catalog_valid": true, - "rtc_param": 0, + "rtc_param": "0", + "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA", + "rtc_param_rows": 1, + "rtc_param_conflict": false, + "rtc_observed_at": "2026-09-24T20:40:49+00:00", + "legal_effective_context": { + "crt": "1", + "informational_only": true, + "ibs_cbs_in_documents": "FROM_2027_FOR_SIMPLES_ORIENTATION_PENDING", + "sources": [ + "NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT; IBS/CBS taxation of these taxpayers only from 2027", + "LC 214/2025 art. 348" + ] + }, "catalog_integrity": { "classes": 145, "cst": 18, @@ -48,8 +62,16 @@ "duplicates": 0, "valid": true }, - "crt": "2", - "products_evaluated": 20997 + "crt": "1", + "products_evaluated": 20997, + "pis_cofins_applicability": { + "NOT_APPLICABLE": 20997 + }, + "non_canonical_warnings": { + "products": 0, + "by_type": {}, + "samples": [] + } } }, "RTC_OFF_SIMULATION": { @@ -67,14 +89,14 @@ "OTHER_BLOCKERS": 0, "blockers_by_type": {}, "applicability": { - "OPTIONAL": 1, - "NOT_APPLICABLE": 20995 + "NOT_APPLICABLE": 20996 }, "rtc_applicability": { "NOT_APPLICABLE": 20996 }, "changed_products": 0, - "examples": [] + "examples": [], + "non_canonical_warnings": 0 }, "RTC_ON_SIMULATION": { "rtc_enabled": true, @@ -93,8 +115,7 @@ "CCLASSTRIB_MISSING": 20996 }, "applicability": { - "OPTIONAL": 1, - "NOT_APPLICABLE": 20995 + "NOT_APPLICABLE": 20996 }, "rtc_applicability": { "REQUIRED": 20996 @@ -341,7 +362,8 @@ "CCLASSTRIB_MISSING" ] } - ] + ], + "non_canonical_warnings": 0 } }, "SAFETY": { diff --git a/docs/runs/unified_coverage_e2e_RINCAO.json b/docs/runs/unified_coverage_e2e_RINCAO.json index 3db7a6f..a4ec1d9 100644 --- a/docs/runs/unified_coverage_e2e_RINCAO.json +++ b/docs/runs/unified_coverage_e2e_RINCAO.json @@ -1,6 +1,7 @@ { + "mode": "FISCAL_ONLY (no Hub decision)", "database": "RINCAO", - "crt": null, + "crt": "3", "products": 894, "fiscal_products_read": 894, "BEFORE": { @@ -13,292 +14,30 @@ "ERP_AS_FOUND": { "rtc_enabled": false, "TOTAL_PRODUCTS": 894, - "COMPLETE_PRODUCTS": 815, - "SAFE_BLOCKED_PRODUCTS": 79, + "COMPLETE_PRODUCTS": 893, + "SAFE_BLOCKED_PRODUCTS": 1, "FAILED_PRODUCTS": 0, "UNCLASSIFIED_PRODUCTS": 0, - "COVERAGE_PERCENT": 91.16, + "COVERAGE_PERCENT": 99.89, "INVARIANT_PASS": true, - "PIS_BLOCKERS": 79, - "COFINS_BLOCKERS": 79, + "PIS_BLOCKERS": 1, + "COFINS_BLOCKERS": 1, "RTC_BLOCKERS": 0, "OTHER_BLOCKERS": 0, "blockers_by_type": { - "COFINS_CST_INVALID": 79, - "PIS_CST_INVALID": 79 + "COFINS_CST_INVALID": 1, + "PIS_CST_INVALID": 1 }, "applicability": { - "NOT_APPLICABLE": 815, - "OPTIONAL": 79 + "OPTIONAL": 894 }, "rtc_applicability": { "NOT_APPLICABLE": 894 }, - "changed_products": 79, + "changed_products": 1, "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", + "product": "000866", "BEFORE_STATUS": "COMPLETE", "AFTER_STATUS": "BLOCKED_FISCAL_DATA", "REASON": [ @@ -307,11 +46,26 @@ ] } ], + "non_canonical_warnings": 78, "context": { "rtc_enabled": false, + "erp_rtc_enabled": false, "rtc_supported": true, "rtc_catalog_valid": true, - "rtc_param": 0, + "rtc_param": "0", + "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA", + "rtc_param_rows": 1, + "rtc_param_conflict": false, + "rtc_observed_at": "2026-09-24T20:40:58+00:00", + "legal_effective_context": { + "crt": "3", + "informational_only": true, + "ibs_cbs_in_documents": "MANDATORY_FROM_2026-01-01", + "sources": [ + "NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT; IBS/CBS taxation of these taxpayers only from 2027", + "LC 214/2025 art. 348" + ] + }, "catalog_integrity": { "classes": 145, "cst": 18, @@ -321,299 +75,99 @@ "duplicates": 0, "valid": true }, - "crt": null, - "products_evaluated": 894 + "crt": "3", + "products_evaluated": 894, + "pis_cofins_applicability": { + "OPTIONAL": 894 + }, + "non_canonical_warnings": { + "products": 78, + "by_type": { + "CST_COFINS_NON_CANONICAL_FORMAT": 78, + "CST_PIS_NON_CANONICAL_FORMAT": 78 + }, + "samples": [ + { + "product_code": "000139", + "type": "CST_PIS_NON_CANONICAL_FORMAT", + "reason": "EXIT:4->04" + }, + { + "product_code": "000139", + "type": "CST_COFINS_NON_CANONICAL_FORMAT", + "reason": "EXIT:4->04" + }, + { + "product_code": "000140", + "type": "CST_PIS_NON_CANONICAL_FORMAT", + "reason": "EXIT:4->04" + }, + { + "product_code": "000140", + "type": "CST_COFINS_NON_CANONICAL_FORMAT", + "reason": "EXIT:4->04" + }, + { + "product_code": "000141", + "type": "CST_PIS_NON_CANONICAL_FORMAT", + "reason": "EXIT:4->04" + }, + { + "product_code": "000141", + "type": "CST_COFINS_NON_CANONICAL_FORMAT", + "reason": "EXIT:4->04" + }, + { + "product_code": "000142", + "type": "CST_PIS_NON_CANONICAL_FORMAT", + "reason": "EXIT:4->04" + }, + { + "product_code": "000142", + "type": "CST_COFINS_NON_CANONICAL_FORMAT", + "reason": "EXIT:4->04" + }, + { + "product_code": "000143", + "type": "CST_PIS_NON_CANONICAL_FORMAT", + "reason": "EXIT:4->04" + }, + { + "product_code": "000143", + "type": "CST_COFINS_NON_CANONICAL_FORMAT", + "reason": "EXIT:4->04" + } + ] + } } }, "RTC_OFF_SIMULATION": { "rtc_enabled": false, "TOTAL_PRODUCTS": 894, - "COMPLETE_PRODUCTS": 815, - "SAFE_BLOCKED_PRODUCTS": 79, + "COMPLETE_PRODUCTS": 893, + "SAFE_BLOCKED_PRODUCTS": 1, "FAILED_PRODUCTS": 0, "UNCLASSIFIED_PRODUCTS": 0, - "COVERAGE_PERCENT": 91.16, + "COVERAGE_PERCENT": 99.89, "INVARIANT_PASS": true, - "PIS_BLOCKERS": 79, - "COFINS_BLOCKERS": 79, + "PIS_BLOCKERS": 1, + "COFINS_BLOCKERS": 1, "RTC_BLOCKERS": 0, "OTHER_BLOCKERS": 0, "blockers_by_type": { - "COFINS_CST_INVALID": 79, - "PIS_CST_INVALID": 79 + "COFINS_CST_INVALID": 1, + "PIS_CST_INVALID": 1 }, "applicability": { - "NOT_APPLICABLE": 815, - "OPTIONAL": 79 + "OPTIONAL": 894 }, "rtc_applicability": { "NOT_APPLICABLE": 894 }, - "changed_products": 79, + "changed_products": 1, "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", + "product": "000866", "BEFORE_STATUS": "COMPLETE", "AFTER_STATUS": "BLOCKED_FISCAL_DATA", "REASON": [ @@ -621,7 +175,8 @@ "PIS_CST_INVALID" ] } - ] + ], + "non_canonical_warnings": 78 }, "RTC_ON_SIMULATION": { "rtc_enabled": true, @@ -632,18 +187,17 @@ "UNCLASSIFIED_PRODUCTS": 0, "COVERAGE_PERCENT": 0.0, "INVARIANT_PASS": true, - "PIS_BLOCKERS": 79, - "COFINS_BLOCKERS": 79, + "PIS_BLOCKERS": 1, + "COFINS_BLOCKERS": 1, "RTC_BLOCKERS": 894, "OTHER_BLOCKERS": 0, "blockers_by_type": { "CCLASSTRIB_MISSING": 894, - "COFINS_CST_INVALID": 79, - "PIS_CST_INVALID": 79 + "COFINS_CST_INVALID": 1, + "PIS_CST_INVALID": 1 }, "applicability": { - "NOT_APPLICABLE": 815, - "OPTIONAL": 79 + "OPTIONAL": 894 }, "rtc_applicability": { "REQUIRED": 894 @@ -890,7 +444,8 @@ "CCLASSTRIB_MISSING" ] } - ] + ], + "non_canonical_warnings": 78 } }, "SAFETY": { diff --git a/docs/runs/unified_coverage_e2e_UNIAO.json b/docs/runs/unified_coverage_e2e_UNIAO.json index 446c1c1..8102413 100644 --- a/docs/runs/unified_coverage_e2e_UNIAO.json +++ b/docs/runs/unified_coverage_e2e_UNIAO.json @@ -1,7 +1,7 @@ { "mode": "FISCAL_ONLY (no Hub decision)", "database": "UNIAO", - "crt": "3", + "crt": "1", "products": 8942, "fiscal_products_read": 8942, "BEFORE": { @@ -14,50 +14,45 @@ "ERP_AS_FOUND": { "rtc_enabled": false, "TOTAL_PRODUCTS": 8942, - "COMPLETE_PRODUCTS": 8940, - "SAFE_BLOCKED_PRODUCTS": 2, + "COMPLETE_PRODUCTS": 8942, + "SAFE_BLOCKED_PRODUCTS": 0, "FAILED_PRODUCTS": 0, "UNCLASSIFIED_PRODUCTS": 0, - "COVERAGE_PERCENT": 99.98, + "COVERAGE_PERCENT": 100.0, "INVARIANT_PASS": true, "PIS_BLOCKERS": 0, - "COFINS_BLOCKERS": 2, + "COFINS_BLOCKERS": 0, "RTC_BLOCKERS": 0, "OTHER_BLOCKERS": 0, - "blockers_by_type": { - "COFINS_CST_INVALID": 2 - }, + "blockers_by_type": {}, "applicability": { - "NOT_APPLICABLE": 8170, - "OPTIONAL": 772 + "NOT_APPLICABLE": 8942 }, "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" - ] - } - ], + "changed_products": 0, + "examples": [], + "non_canonical_warnings": 2, "context": { "rtc_enabled": false, + "erp_rtc_enabled": false, "rtc_supported": true, "rtc_catalog_valid": true, - "rtc_param": 0, + "rtc_param": "0", + "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA", + "rtc_param_rows": 1, + "rtc_param_conflict": false, + "rtc_observed_at": "2026-09-24T20:41:07+00:00", + "legal_effective_context": { + "crt": "1", + "informational_only": true, + "ibs_cbs_in_documents": "FROM_2027_FOR_SIMPLES_ORIENTATION_PENDING", + "sources": [ + "NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT; IBS/CBS taxation of these taxpayers only from 2027", + "LC 214/2025 art. 348" + ] + }, "catalog_integrity": { "classes": 145, "cst": 18, @@ -67,52 +62,54 @@ "duplicates": 0, "valid": true }, - "crt": "3", - "products_evaluated": 8942 + "crt": "1", + "products_evaluated": 8942, + "pis_cofins_applicability": { + "NOT_APPLICABLE": 8942 + }, + "non_canonical_warnings": { + "products": 2, + "by_type": { + "CST_COFINS_INVALID_IGNORED": 2 + }, + "samples": [ + { + "product_code": "000304", + "type": "CST_COFINS_INVALID_IGNORED", + "reason": "ENTRY:000" + }, + { + "product_code": "003090", + "type": "CST_COFINS_INVALID_IGNORED", + "reason": "EXIT:060" + } + ] + } } }, "RTC_OFF_SIMULATION": { "rtc_enabled": false, "TOTAL_PRODUCTS": 8942, - "COMPLETE_PRODUCTS": 8940, - "SAFE_BLOCKED_PRODUCTS": 2, + "COMPLETE_PRODUCTS": 8942, + "SAFE_BLOCKED_PRODUCTS": 0, "FAILED_PRODUCTS": 0, "UNCLASSIFIED_PRODUCTS": 0, - "COVERAGE_PERCENT": 99.98, + "COVERAGE_PERCENT": 100.0, "INVARIANT_PASS": true, "PIS_BLOCKERS": 0, - "COFINS_BLOCKERS": 2, + "COFINS_BLOCKERS": 0, "RTC_BLOCKERS": 0, "OTHER_BLOCKERS": 0, - "blockers_by_type": { - "COFINS_CST_INVALID": 2 - }, + "blockers_by_type": {}, "applicability": { - "NOT_APPLICABLE": 8170, - "OPTIONAL": 772 + "NOT_APPLICABLE": 8942 }, "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" - ] - } - ] + "changed_products": 0, + "examples": [], + "non_canonical_warnings": 2 }, "RTC_ON_SIMULATION": { "rtc_enabled": true, @@ -124,16 +121,14 @@ "COVERAGE_PERCENT": 0.0, "INVARIANT_PASS": true, "PIS_BLOCKERS": 0, - "COFINS_BLOCKERS": 2, + "COFINS_BLOCKERS": 0, "RTC_BLOCKERS": 8942, "OTHER_BLOCKERS": 0, "blockers_by_type": { - "CCLASSTRIB_MISSING": 8942, - "COFINS_CST_INVALID": 2 + "CCLASSTRIB_MISSING": 8942 }, "applicability": { - "NOT_APPLICABLE": 8170, - "OPTIONAL": 772 + "NOT_APPLICABLE": 8942 }, "rtc_applicability": { "REQUIRED": 8942 @@ -380,7 +375,8 @@ "CCLASSTRIB_MISSING" ] } - ] + ], + "non_canonical_warnings": 2 } }, "SAFETY": { diff --git a/docs/unified_fiscal_completeness.md b/docs/unified_fiscal_completeness.md index d7075e6..9db7afb 100644 --- a/docs/unified_fiscal_completeness.md +++ b/docs/unified_fiscal_completeness.md @@ -39,26 +39,15 @@ Cada blocker fiscal é um blocker comum: `blocker_type=BLOCKED_FISCAL_DATA`, `fi ## 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): +## Validação nos bancos reais (somente leitura, cinco bancos) +`scripts/unified_coverage_e2e.py` → `docs/runs/unified_coverage_e2e*.json`. RTC lida **do banco como está hoje**; "RTC desligada" é simulação pura do avaliador (o parâmetro nunca é tocado). -| | 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) | +| Banco | CRT | RTC no banco | Produtos | Completos | Bloqueados | Não class. | Cobertura (como está) | Cobertura com RTC off | +|---|---|---|---|---|---|---|---|---| +| CONSTRUCAO | 1 | **ligada** | 3.712 | 0 | 3.712 (RTC) | 0 | 0% | 100% | +| MMMODAS | 1 | desligada | 20.997 | 20.997 | 0 | 0 | 100% | 100% | +| RINCAO | 3 | desligada | 894 | 893 | 1 | 0 | 99,89% (antes da normalização: 91,16%) | 99,89% | +| UNIAO | 1 | desligada | 8.942 | 8.942 | 0 | 0 | 100% | 100% | +| TESTESOFICIAIS | 1 | **ligada** | 521 | 0 | 521 (RTC) | 0 | 0% | 94,82% (494/521) | -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. +Bancos além do piloto: medida só da parte fiscal (`--fiscal-only`, sem decisão do Hub). TESTESOFICIAIS: com a decisão do Hub (antes 495/521 = 95,01%). Bloqueios PIS/COFINS: TESTESOFICIAIS 1 produto (incidência SUBST.TRIBUTÁRIA com CST 01), RINCAO 1 produto (`049`/`000`), demais 0. Produtos com warning (não bloqueante): RINCAO 78 (`4`→`04`, não canônico), UNIAO 2 (CST `000`/`060` inválido, ignorado por ser Simples), TESTESOFICIAIS 1 (não canônico). Aplicabilidade: Simples ⇒ `NOT_APPLICABLE`; RINCAO ⇒ `OPTIONAL` (894); TESTESOFICIAIS 519 `NOT_APPLICABLE` + 2 `REQUIRED`. diff --git a/scripts/unified_coverage_e2e.py b/scripts/unified_coverage_e2e.py index c3e7758..8af5fba 100644 --- a/scripts/unified_coverage_e2e.py +++ b/scripts/unified_coverage_e2e.py @@ -34,6 +34,18 @@ def old_unknown(product): return not evidence +def resolved_crt(name, path): + """Company regime exactly as the Agent resolves it (deterministic company-context resolver).""" + from gti_fiscal_agent import company_context as cc + conn = lc.connect(path) + try: + cur = conn.cursor() + result = cc.resolve(cc.collect_evidence(cur), cc.config_from_env(lc._cfg_env(name))) + return result.get("company_crt") if result.get("status") == "PASS" else None + finally: + conn.close() + + 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() @@ -47,7 +59,7 @@ def main(): 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) + crt = a.crt or resolved_crt(a.db, path) 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)] @@ -76,9 +88,9 @@ def main(): "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]} + "changed_products": len(changed), "examples": changed[:30], "non_canonical_warnings": out["context"]["non_canonical_warnings"]["products"]} if label == "ERP_AS_FOUND": - runs[label]["context"] = {k: v for k, v in out["context"].items() if k != "capabilities"} + runs[label]["context"] = {k: v for k, v in out["context"].items() if k not in ("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), @@ -87,7 +99,7 @@ def main(): 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(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", "non_canonical_warnings")}) print(" applicability", r["applicability"], "blockers", r["blockers_by_type"])