feat: central RTC write modes (DISABLED/DRY_RUN/APPROVAL/ENABLED), R4 suggestion-only, preview/approval, per-record rollback, bulk blocker upsert; 1.2.0

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-24 22:46:05 -03:00
co-authored by Claude Sonnet 5
parent 3c10422e81
commit 42dc172763
50 changed files with 2948 additions and 915 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "gti-fiscal-agent"
version = "1.1.9"
version = "1.2.0"
requires-python = ">=3.10"
dependencies = ["httpx>=0.27,<1"]
+36 -20
View File
@@ -105,9 +105,8 @@ class Config:
self.agent_jobs_enabled = True
self.poll_interval_seconds = float(env.get("POLL_INTERVAL_SECONDS", "5"))
# RTC auto-apply is a SEPARATE switch (default OFF): FIREBIRD_WRITES alone never lets the Agent fill cClassTrib.
self.rtc_auto_apply_enabled = env.get("RTC_AUTO_APPLY_ENABLED", "0").strip() == "1"
self.rtc_auto_apply_max_per_job = int(env.get("RTC_AUTO_APPLY_MAX_PER_JOB", str(_rtc_apply.DEFAULT_MAX_PER_JOB)))
self.agent_version = env.get("AGENT_VERSION", "1.1.9")
self.agent_version = env.get("AGENT_VERSION", "1.2.0")
self.schema_fingerprint = env.get("SCHEMA_FINGERPRINT", REFERENCE_SCHEMA_V1)
self.state_db = env.get("AGENT_STATE_DB", str(AGENT_ROOT / "agent_state.sqlite"))
# Pilot write authorization: both must be explicitly set by the installer for
@@ -208,30 +207,47 @@ def _fiscal_blocked_products(products, findings):
found = findings.get(code) or {}
for blocker in found.get("blockers") or []:
out.append({"company": "", "product_code": code, "gtin": product.get("GTIN") or product.get("EAN_GTIN"), "description": product.get("PRODUTO"), "blocker_type": "BLOCKED_FISCAL_DATA",
"field": blocker["category"], "reason_code": blocker["type"], "evidence_summary": {"reason": blocker["reason"], "applicability": found.get("applicability")},
"field": blocker["category"], "reason_code": blocker["type"], "evidence_summary": {"reason": blocker["reason"], "applicability": found.get("applicability"), **({"human": blocker["human"]} if blocker.get("human") else {})},
"input_fingerprint": found.get("input_fingerprint", "")})
return out
def _rtc_auto_apply(env, config, job, products, fiscal, company):
"""Report (always) and, only when all three gates are open, fill CCLASSTRIB for products whose class is PROVEN by a deterministic rule.
Returns the summary stored in the job result; never raises for a single product."""
gate = _rtc_apply.write_gate(firebird_writes=bool(getattr(config, "firebird_writes", False)), agent_enabled=bool(getattr(config, "rtc_auto_apply_enabled", False)),
hub_authorized=bool((job.get("authorizations") or {}).get("rtc_auto_apply")))
limit = int(getattr(config, "rtc_auto_apply_max_per_job", _rtc_apply.DEFAULT_MAX_PER_JOB))
candidates, ready_total = _rtc_apply.select_candidates(fiscal["findings"], products, limit=limit)
summary = {"gate": gate, "mode": "AUTO_APPLY" if gate["allowed"] else "REPORT_ONLY", "ready_total": ready_total, "max_per_job": limit, "write_attempts": 0, "write_successes": 0, "applied": [], "aborted": [], "aborted_reasons": {},
"allowed_fields": list(_rtc_apply.ALLOWED_RTC_WRITE_FIELDS)}
if not (gate["allowed"] and candidates and fiscal["context"].get("rtc_enabled")):
return summary
"""Preview (always) and, only when the Hub-delivered mode permits it, fill CCLASSTRIB for products whose class is PROVEN by an AUTO_APPLY_SAFE rule; also executes
Hub-requested rollbacks. Authorization arrives ONLY inside the authenticated job (`authorizations`); there is no local switch. Never raises for a single product."""
from datetime import datetime, timezone
auth = job.get("authorizations") or {}
mode = auth.get("rtc_write_mode") if auth.get("rtc_write_mode") in _rtc_apply.MODES else "DISABLED"
approved = {str(c) for c in (auth.get("rtc_approved_codes") or [])}
limit = int(getattr(config, "rtc_auto_apply_max_per_job", _rtc_apply.DEFAULT_MAX_PER_JOB))
rtc_on = bool(fiscal["context"].get("rtc_enabled"))
all_candidates, ready_total = _rtc_apply.select_candidates(fiscal["findings"], products, limit=_rtc_apply.MAX_PREVIEW_CANDIDATES)
gate = _rtc_apply.write_gate(mode=mode, capability=True, approved=bool(approved))
summary = {"gate": gate, "mode": mode if mode != "DISABLED" else "REPORT_ONLY", "hub_mode": mode, "capability": _rtc_apply.CAPABILITY, "ready_total": ready_total, "max_per_job": limit, "write_attempts": 0, "write_successes": 0,
"applied": [], "aborted": [], "aborted_reasons": {}, "rollbacks": [], "allowed_fields": list(_rtc_apply.ALLOWED_RTC_WRITE_FIELDS),
"preview": [{k: c[k] for k in ("product_code", "description", "ncm", "current", "proposed", "cst", "rule_id", "basis")} for c in all_candidates] if mode != "DISABLED" and rtc_on else [],
"preview_truncated": ready_total > len(all_candidates)}
if not rtc_on:
return summary
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
rollbacks = [r for r in (auth.get("rtc_rollbacks") or []) if isinstance(r, dict)]
if mode == "ENABLED":
wanted = all_candidates[:limit]
elif mode == "APPROVAL_REQUIRED":
wanted = [c for c in all_candidates if c["product_code"] in approved][:limit]
else:
wanted = []
if not (rollbacks or (gate["allowed"] and wanted)):
return summary
connection = open_write_connection(env, config.database_path)
try:
outcome = _rtc_apply.apply_candidates(connection, candidates, company=company or {}, decision={"local_catalog_hash": fiscal["context"].get("local_catalog_hash")},
now=datetime.now(timezone.utc).isoformat(timespec="seconds"), job_id=job.get("job_id"))
if rollbacks:
summary["rollbacks"] = _rtc_apply.rollback_records(connection, rollbacks, now=now, job_id=job.get("job_id"))["rollbacks"]
if gate["allowed"] and wanted:
outcome = _rtc_apply.apply_candidates(connection, wanted, company=company or {}, decision={"local_catalog_hash": fiscal["context"].get("local_catalog_hash"), "origin": f"HUB_MODE:{mode}"}, now=now, job_id=job.get("job_id"))
summary.update({k: outcome[k] for k in ("applied", "aborted", "aborted_reasons", "write_attempts", "write_successes")})
finally:
connection.close()
summary.update({k: outcome[k] for k in ("applied", "aborted", "aborted_reasons", "write_attempts", "write_successes")})
return summary
@@ -289,7 +305,7 @@ def execute_job(job, config, executor, state, env):
ncm_ok = {str(p["CODIGO"]): (d or {}).get("status") == "EXACT_NCM" for p, d in zip(products, all_decisions)}
fiscal = _unified_fiscal(env, company, ncm_ok)
rtc_apply = _rtc_auto_apply(env, config, job, products, fiscal, company)
if rtc_apply["write_successes"]:
if rtc_apply["write_successes"] or any(r.get("outcome") == "ROLLED_BACK" for r in rtc_apply["rollbacks"]):
fiscal = _unified_fiscal(env, company, ncm_ok) # coverage must reflect the fields that were just filled
findings = fiscal["findings"]
product_states = [classify_product_state(p, d, fiscal=findings.get(str(p["CODIGO"]))) for p, d in zip(products, all_decisions)]
@@ -307,7 +323,7 @@ def execute_job(job, config, executor, state, env):
"fiscal_context": fiscal_context,
"rtc_auto_apply": rtc_apply,
"fiscal_mode": getattr(config, "fiscal_mode", None),
"release_version": env.get("AGENT_VERSION") or "1.1.9",
"release_version": env.get("AGENT_VERSION") or "1.2.0",
"products_total": len(products),
"products_changed": len(to_process),
"products_processed": summary["processed"],
@@ -395,7 +411,7 @@ def run(config, env, *, max_iterations=None, stopper=None):
"agent_version": config.agent_version, "last_job_id": last_job_id, "display_name": env.get("AGENT_DISPLAY_NAME", ""),
"customer_code": env.get("CUSTOMER_CODE"), "hostname": env.get("AGENT_HOSTNAME") or os.environ.get("COMPUTERNAME"),
"windows_version": env.get("WINDOWS_VERSION"), "company_code": config.company_id, "company_uf": config.company_uf,
"database_path": config.database_path, "capabilities": {"heartbeat": True, "jobs": True, "local_logs": True, "rtc_auto_apply": True, "rtc_auto_apply_enabled": bool(getattr(config, "rtc_auto_apply_enabled", False))},
"database_path": config.database_path, "capabilities": {"heartbeat": True, "jobs": True, "local_logs": True, "RTC_CCLASSTRIB_WRITE_V1": True},
"fiscal_engine_version": env.get("FISCAL_ENGINE_VERSION", config.agent_version), "dataset_versions": {},
}
if hasattr(client, "heartbeat_metadata"):
@@ -465,7 +481,7 @@ def run(config, env, *, max_iterations=None, stopper=None):
def enforce_startup_preflight(env):
"""Full read-only preflight at startup (Hub, auth, Firebird, capabilities, company context, guards). Fail closed."""
from gti_fiscal_agent.preflight import run_preflight
report = run_preflight(env, agent_version=env.get("AGENT_VERSION") or "1.1.9")
report = run_preflight(env, agent_version=env.get("AGENT_VERSION") or "1.2.0")
logger.info("startup preflight: %s", json.dumps({k: report[k] for k in ("PREFLIGHT", "blockers", "fiscal_mode", "effective_firebird_writes")}, ensure_ascii=False))
print(json.dumps({"PREFLIGHT": report["PREFLIGHT"], "blockers": report["blockers"]}, ensure_ascii=False))
if report["PREFLIGHT"] != "PASS":
+1 -1
View File
@@ -9,7 +9,7 @@ import os
import sqlite3
from pathlib import Path
AGENT_VERSION_DEFAULT = "1.1.9"
AGENT_VERSION_DEFAULT = "1.2.0"
REQUIRED_ENV = ("GTI_HUB_URL", "AGENT_TOKEN", "FIREBIRD_DATABASE_PATH", "FB_USER", "FB_PASSWORD")
REQUIRED_CAPABILITIES = ("supports_ncm", "supports_ipi", "supports_cest")
FISCAL_MODES = ("DRY_RUN", "AUTO_APPLY")
+84 -14
View File
@@ -1,23 +1,26 @@
"""RTC auto-apply: fills C000025.CCLASSTRIB, and NOTHING else, when a deterministic rule proves the class.
Safety model (every gate must hold, default is OFF everywhere):
FIREBIRD_WRITES effective (FISCAL_MODE=AUTO_APPLY + path/schema authorization) AND RTC_AUTO_APPLY_ENABLED (this Agent) AND Hub authorization (per installation)
Write allowlist: ALLOWED_RTC_WRITE_FIELDS = ["CCLASSTRIB"]. One short transaction per product. Immediately before the UPDATE the product is re-read and
the whole decision is re-derived from fresh data; if ANYTHING changed (RTC switch, catalog, NCM, current value, decision) that product is ABORTED.
An existing value is never overwritten. Every applied change returns an audit record with the old value, so it can be reverted manually and auditably.
Safety model (defense in depth, every gate must hold, default is OFF): the Hub sets a per-installation mode DISABLED / DRY_RUN / APPROVAL_REQUIRED / ENABLED and
delivers it inside an authenticated job. The Agent only exposes the restricted capability RTC_CCLASSTRIB_WRITE_V1; there is no local switch to edit (no agent.env, no FIREBIRD_WRITES).
Write allowlist: ALLOWED_RTC_WRITE_FIELDS = ["CCLASSTRIB"] on C000025 (no free SQL exists). One short transaction per product. Immediately before the UPDATE the product,
company, RTC parameter and local catalog are re-read and the whole decision is re-derived; if ANYTHING changed that product is ABORTED and nothing is written.
An existing value is never overwritten. Every applied change returns an audit record with the old value; ROLLBACK restores it only if the value still equals what was written.
"""
from __future__ import annotations
import re
from datetime import date
from central.app.cclasstrib_resolver import compare_catalogs, local_catalog_hash, resolve_cclasstrib
from central.app.cclasstrib_resolver import RULE_VERSION, compare_catalogs, local_catalog_hash, resolve_cclasstrib
from central.app.unified_fiscal_coverage import rtc_enabled
from .unified_fiscal import detect_rtc_capabilities, load_rtc_context
ALLOWED_RTC_WRITE_FIELDS = ("CCLASSTRIB",)
DEFAULT_MAX_PER_JOB = 500
CAPABILITY = "RTC_CCLASSTRIB_WRITE_V1"
MODES = ("DISABLED", "DRY_RUN", "APPROVAL_REQUIRED", "ENABLED")
MAX_PREVIEW_CANDIDATES = 5000
class RtcApplyAborted(Exception):
@@ -36,10 +39,22 @@ def _digits(value) -> str:
return re.sub(r"\D", "", _text(value))
def write_gate(*, firebird_writes: bool, agent_enabled: bool, hub_authorized: bool) -> dict:
"""{"allowed": bool, "missing": [...]}: writes are possible only when all three switches are on."""
missing = [name for name, on in (("FIREBIRD_WRITES", firebird_writes), ("RTC_AUTO_APPLY_ENABLED", agent_enabled), ("HUB_AUTHORIZATION", hub_authorized)) if not on]
return {"allowed": not missing, "missing": missing, "firebird_writes": bool(firebird_writes), "agent_enabled": bool(agent_enabled), "hub_authorized": bool(hub_authorized)}
def write_gate(*, mode: str, capability: bool = True, approved: bool = True) -> dict:
"""{"allowed": bool, "missing": [...]}: a real write needs the capability, an authenticated Hub mode that permits writing (ENABLED, or APPROVAL_REQUIRED for approved products)."""
mode = mode if mode in MODES else "DISABLED"
missing = []
if not capability:
missing.append("CAPABILITY")
if mode not in ("ENABLED", "APPROVAL_REQUIRED"):
missing.append("HUB_MODE_" + mode)
elif mode == "APPROVAL_REQUIRED" and not approved:
missing.append("HUB_APPROVAL")
return {"allowed": not missing, "missing": missing, "mode": mode, "capability": bool(capability)}
def candidate_fingerprint(code, ncm, description, current) -> str:
import hashlib
return hashlib.sha256("|".join((_text(code), _digits(ncm), _text(description), _text(current))).encode("utf-8")).hexdigest()[:24]
def select_candidates(findings: dict, products: list[dict], *, limit: int = DEFAULT_MAX_PER_JOB) -> tuple[list[dict], int]:
@@ -56,7 +71,8 @@ def select_candidates(findings: dict, products: list[dict], *, limit: int = DEFA
p = by_code.get(code, {})
if _text(p.get("CCLASSTRIB")):
continue
ready.append({"product_code": code, "proposed": res["resolved_cclasstrib"], "cst": res["resolved_cst_ibscbs"], "rule_id": res["rule_id"], "ncm": _digits(p.get("CLASSIFICACAO_FISCAL")), "confidence_type": res["confidence_type"]})
ready.append({"product_code": code, "proposed": res["resolved_cclasstrib"], "cst": res["resolved_cst_ibscbs"], "rule_id": res["rule_id"], "ncm": _digits(p.get("CLASSIFICACAO_FISCAL")), "confidence_type": res["confidence_type"], "description": _text(p.get("PRODUTO"))[:255], "current": "", "basis": f'{res["rule_id"]}:{res["confidence_type"]}',
"fingerprint": candidate_fingerprint(code, p.get("CLASSIFICACAO_FISCAL"), p.get("PRODUTO"), "")})
return ready[:limit], len(ready)
@@ -78,7 +94,14 @@ def apply_candidate(conn, candidate: dict, *, company: dict, decision: dict, now
fresh_hash = local_catalog_hash(local_rows)
if decision.get("local_catalog_hash") and fresh_hash != decision["local_catalog_hash"]:
raise RtcApplyAborted("LOCAL_CATALOG_CHANGED_SINCE_DECISION")
cur.execute(f"SELECT {field}, CLASSIFICACAO_FISCAL FROM C000025 WHERE CODIGO = ?", (code,))
cur.execute("SELECT DISTINCT TRIM(CRT) FROM C000004")
crts = {_text(r[0]) for r in cur.fetchall()}
expected_crt = _text((company or {}).get("crt"))
if not expected_crt or crts != {expected_crt}:
raise RtcApplyAborted("COMPANY_CONTEXT_CHANGED")
if expected_crt != "3":
raise RtcApplyAborted("REGIME_NOT_NORMAL")
cur.execute(f"SELECT {field}, CLASSIFICACAO_FISCAL, PRODUTO FROM C000025 WHERE CODIGO = ?", (code,))
rows = cur.fetchall()
if len(rows) != 1:
raise RtcApplyAborted("PRODUCT_ROW_NOT_UNIQUE" if rows else "PRODUCT_NOT_FOUND")
@@ -87,6 +110,8 @@ def apply_candidate(conn, candidate: dict, *, company: dict, decision: dict, now
raise RtcApplyAborted("VALUE_ALREADY_SET_NEVER_OVERWRITTEN" if current != proposed else "NO_WRITE_REQUIRED")
if ncm != candidate["ncm"]:
raise RtcApplyAborted("PRODUCT_CHANGED_NCM")
if candidate.get("fingerprint") and candidate_fingerprint(code, ncm, rows[0][2], current) != candidate["fingerprint"]:
raise RtcApplyAborted("PRODUCT_CHANGED_SINCE_DECISION")
compat = compare_catalogs(local_rows, today)
resolution = resolve_cclasstrib({"CODIGO": code, "CLASSIFICACAO_FISCAL": ncm, "ncm_valid": True}, company, {"rtc_enabled": True, "local_catalog_valid": True}, local_rows, compat=compat, today=today)
if not resolution.auto_apply_candidate or resolution.resolved_cclasstrib != proposed or resolution.rule_id != candidate["rule_id"]:
@@ -106,7 +131,8 @@ def apply_candidate(conn, candidate: dict, *, company: dict, decision: dict, now
return {"outcome": "APPLIED", "product_code": code, "field": field, "old_value": None, "new_value": proposed, "cst_ibscbs": resolution.resolved_cst_ibscbs, "rule_id": resolution.rule_id,
"confidence_type": resolution.confidence_type, "evidence": {"inputs_used": resolution.inputs_used, "evidence": resolution.evidence[:5], "reason": resolution.reason},
"official_source_version": {"cclasstrib_table": official["publication"], "cclasstrib_sha256": official["sha256"], "lc214_sha256": resolution.source_references[0]["sha256"]},
"local_catalog_hash": fresh_hash, "timestamp": now, "job_id": job_id, "rowcount": 1, "verified_after": True}
"local_catalog_hash": fresh_hash, "timestamp": now, "job_id": job_id, "rowcount": 1, "verified_after": True, "action": "APPLY", "rule_version": RULE_VERSION,
"company_crt": expected_crt, "description_snapshot": _text(rows[0][2])[:255], "rtc_status": "ENABLED", "local_catalog_state": (compat.get("per_code") or {}).get(proposed), "authorization_origin": decision.get("origin")}
except RtcApplyAborted:
try:
conn.rollback()
@@ -128,9 +154,53 @@ def apply_candidates(conn, candidates: list[dict], *, company: dict, decision: d
try:
applied.append(apply_candidate(conn, candidate, company=company, decision=decision, now=now, job_id=job_id, today=today))
except RtcApplyAborted as stop:
aborted.append({"outcome": "ABORTED", "product_code": candidate["product_code"], "field": "CCLASSTRIB", "reason": stop.reason, "rule_id": candidate["rule_id"], "job_id": job_id, "timestamp": now})
aborted.append({"outcome": "ABORTED", "product_code": candidate["product_code"], "field": "CCLASSTRIB", "reason": stop.reason, "rule_id": candidate["rule_id"], "job_id": job_id, "timestamp": now, "action": "APPLY", "authorization_origin": decision.get("origin")})
reasons[stop.reason] = reasons.get(stop.reason, 0) + 1
except Exception as error: # noqa: BLE001 - technical failure of ONE product must not abort the batch
aborted.append({"outcome": "FAILED", "product_code": candidate["product_code"], "field": "CCLASSTRIB", "reason": f"{type(error).__name__}", "rule_id": candidate["rule_id"], "job_id": job_id, "timestamp": now})
reasons["TECHNICAL_FAILURE"] = reasons.get("TECHNICAL_FAILURE", 0) + 1
return {"applied": applied, "aborted": aborted, "aborted_reasons": reasons, "write_attempts": len(candidates), "write_successes": len(applied)}
def rollback_one(conn, item: dict, *, now: str, job_id=None) -> dict:
"""Restores C000025.CCLASSTRIB to empty for ONE previously applied audit record, only if the value still equals what was written. No free SQL: the only statement is fixed."""
field = "CCLASSTRIB"
assert field in ALLOWED_RTC_WRITE_FIELDS
code, written = _text(item.get("product_code")), _text(item.get("written_value"))
base = {"product_code": code, "field": field, "job_id": job_id, "timestamp": now, "action": "ROLLBACK", "rollback_of": item.get("audit_id"), "old_value": written, "new_value": None, "rule_id": item.get("rule_id")}
if not code or not written or item.get("restore_value"):
return {**base, "outcome": "ABORTED", "reason": "ROLLBACK_REQUEST_INVALID"}
cur = conn.cursor()
try:
cur.execute(f"SELECT {field} FROM C000025 WHERE CODIGO = ?", (code,))
rows = cur.fetchall()
if len(rows) != 1:
conn.rollback()
return {**base, "outcome": "ABORTED", "reason": "PRODUCT_ROW_NOT_UNIQUE" if rows else "PRODUCT_NOT_FOUND"}
current = _text(rows[0][0])
if current != written:
conn.rollback()
return {**base, "outcome": "ABORTED", "reason": "ROLLBACK_CONFLICT_VALUE_CHANGED" if current else "ALREADY_EMPTY"}
cur.execute(f"UPDATE C000025 SET {field} = NULL WHERE CODIGO = ? AND TRIM({field}) = ?", (code, written))
if cur.rowcount != 1:
conn.rollback()
return {**base, "outcome": "ABORTED", "reason": "UPDATE_ROWCOUNT_NOT_ONE"}
conn.commit()
cur = conn.cursor()
cur.execute(f"SELECT {field} FROM C000025 WHERE CODIGO = ?", (code,))
after = _text(cur.fetchone()[0])
conn.commit()
if after:
raise RuntimeError("post-rollback verification failed")
return {**base, "outcome": "ROLLED_BACK", "rowcount": 1, "verified_after": True}
except Exception as error: # noqa: BLE001
try:
conn.rollback()
except Exception: # noqa: BLE001
pass
return {**base, "outcome": "FAILED", "reason": type(error).__name__}
def rollback_records(conn, items: list[dict], *, now: str, job_id=None) -> dict:
results = [rollback_one(conn, item, now=now, job_id=job_id) for item in items]
return {"rollbacks": results, "rolled_back": sum(1 for r in results if r["outcome"] == "ROLLED_BACK")}
+134 -30
View File
@@ -27,6 +27,7 @@ class FakeFirebird:
for code, change in (catalog_tweak or {}).items():
self.catalog[code] = {**self.catalog[code], **change}
self.cst_table = {r["cst"] for r in ref.OFFICIAL_CCLASSTRIB} # RTC_CST is its own table: a class may point to a CST that is not in it
self.crt = ("3",) # C000004.CRT rows
self.statements, self.commits, self.rollbacks = [], 0, 0
self._pending = {}
@@ -71,14 +72,23 @@ class FakeCursor:
self._rows = [(c,) for c in sorted(db.cst_table)]
elif up.startswith("SELECT CODIGO, CCLASSTRIB, CLASSIFICACAO_FISCAL"):
self._rows = [(c, v["CCLASSTRIB"], v["CLASSIFICACAO_FISCAL"], "N", "NENHUMA") for c, v in sorted(db.products.items())]
elif up.startswith("SELECT CCLASSTRIB, CLASSIFICACAO_FISCAL FROM C000025"):
elif up.startswith("SELECT DISTINCT TRIM(CRT) FROM C000004"):
self._rows = [(c,) for c in db.crt]
elif up.startswith("SELECT CCLASSTRIB, CLASSIFICACAO_FISCAL, PRODUTO FROM C000025"):
code = params[0]
self._rows = [(db.products[code]["CCLASSTRIB"], db.products[code]["CLASSIFICACAO_FISCAL"])] if code in db.products else []
self._rows = [(db.products[code]["CCLASSTRIB"], db.products[code]["CLASSIFICACAO_FISCAL"], db.products[code].get("PRODUTO", ""))] if code in db.products else []
if code in db.products and db.products[code].get("DUP"):
self._rows = self._rows * 2
elif up.startswith("SELECT CCLASSTRIB FROM C000025"):
code = params[0]
self._rows = [(db.products[code]["CCLASSTRIB"],)]
elif up.startswith("UPDATE C000025 SET CCLASSTRIB = NULL WHERE CODIGO = ? AND TRIM(CCLASSTRIB) = ?"):
code, written = params
product = db.products.get(code)
hit = product is not None and (product["CCLASSTRIB"] or "").strip() == written
self.rowcount = 1 if hit else 0
if hit:
db._pending[code] = None
elif up.startswith("UPDATE C000025 SET CCLASSTRIB = ? WHERE CODIGO = ?"):
value, code = params
product = db.products.get(code)
@@ -115,21 +125,22 @@ def run(db, cands, out, **kw):
return apply_mod.apply_candidates(db, cands, company=NORMAL, decision={"local_catalog_hash": out["context"]["local_catalog_hash"]}, now="2026-09-24T20:00:00+00:00", job_id=7, today=TODAY, **kw)
PRODUCTS = {"000001": product("10063021"), "000002": product("39235000"), "000003": product("15171000"), "000004": product("30049099"), "000005": product("39235000", "000001")}
PRODUCTS = {"000001": product("10063021"), "000002": product("04090000"), "000003": product("15171000"), "000004": product("30049099"), "000005": product("39235000", "000001")}
def test_gate_needs_all_three_switches_and_defaults_to_closed():
assert apply_mod.write_gate(firebird_writes=True, agent_enabled=True, hub_authorized=True)["allowed"] is True
for combo in ((False, True, True), (True, False, True), (True, True, False), (False, False, False)):
g = apply_mod.write_gate(firebird_writes=combo[0], agent_enabled=combo[1], hub_authorized=combo[2])
def test_gate_is_driven_by_the_central_mode_only_and_defaults_to_closed():
assert apply_mod.write_gate(mode="ENABLED")["allowed"] is True
assert apply_mod.write_gate(mode="APPROVAL_REQUIRED", approved=True)["allowed"] is True
for kwargs in ({"mode": "DISABLED"}, {"mode": "DRY_RUN"}, {"mode": "APPROVAL_REQUIRED", "approved": False}, {"mode": "ENABLED", "capability": False}, {"mode": "bogus"}, {"mode": ""}):
g = apply_mod.write_gate(**kwargs)
assert g["allowed"] is False and g["missing"]
assert apply_mod.ALLOWED_RTC_WRITE_FIELDS == ("CCLASSTRIB",)
assert apply_mod.ALLOWED_RTC_WRITE_FIELDS == ("CCLASSTRIB",) and apply_mod.CAPABILITY == "RTC_CCLASSTRIB_WRITE_V1"
def test_only_proven_products_become_candidates():
db = FakeFirebird(PRODUCTS)
out, cands, total = candidates_of(db)
assert sorted((c["product_code"], c["proposed"], c["rule_id"]) for c in cands) == [("000001", "200003", "RTC-R3"), ("000002", "000001", "RTC-R4"), ("000003", "200003", "RTC-R3")]
assert sorted((c["product_code"], c["proposed"], c["rule_id"]) for c in cands) == [("000001", "200003", "RTC-R3"), ("000002", "200034", "RTC-R3"), ("000003", "200003", "RTC-R3")]
assert total == 3 and out["context"]["rtc_resolution"]["ready_to_apply"] == 3 and out["context"]["rtc_resolution"]["ambiguous"] == 1 # medicine chapter is ambiguous
assert out["context"]["rtc_resolution"]["valid_existing"] == 1 # 000005 already has the right class
assert "000004" not in {c["product_code"] for c in cands}
@@ -140,7 +151,7 @@ def test_apply_writes_only_cclasstrib_with_audit_and_second_run_writes_nothing()
out, cands, _ = candidates_of(db)
result = run(db, cands, out)
assert result["write_successes"] == 3 and result["write_attempts"] == 3 and result["aborted"] == []
assert {c: db.products[c]["CCLASSTRIB"] for c in ("000001", "000002", "000003")} == {"000001": "200003", "000002": "000001", "000003": "200003"}
assert {c: db.products[c]["CCLASSTRIB"] for c in ("000001", "000002", "000003")} == {"000001": "200003", "000002": "200034", "000003": "200003"}
updates = [s for s, _ in db.statements if s.upper().startswith("UPDATE")]
assert len(updates) == 3 and all(re.fullmatch(r"UPDATE C000025 SET CCLASSTRIB = \? WHERE CODIGO = \? AND \(CCLASSTRIB IS NULL OR TRIM\(CCLASSTRIB\) = ''\)", u) for u in updates)
assert not [s for s, _ in db.statements if re.match(r"(INSERT|DELETE|ALTER|DROP)", s.strip(), re.I)]
@@ -184,7 +195,7 @@ def test_product_changed_between_decision_and_write_is_left_alone():
db = FakeFirebird(PRODUCTS)
out, cands, _ = candidates_of(db)
db.products["000001"]["CLASSIFICACAO_FISCAL"] = "30049099" # NCM edited in the ERP meanwhile
db.products["000002"]["CCLASSTRIB"] = "000001" # operator filled it meanwhile (same value)
db.products["000002"]["CCLASSTRIB"] = "200034" # operator filled it meanwhile (same value)
db.products["000003"]["CCLASSTRIB"] = "200034" # ... or with a DIFFERENT value: never overwritten
result = run(db, cands, out)
assert result["aborted_reasons"] == {"PRODUCT_CHANGED_NCM": 1, "NO_WRITE_REQUIRED": 1, "VALUE_ALREADY_SET_NEVER_OVERWRITTEN": 1} and result["write_successes"] == 0
@@ -192,7 +203,7 @@ def test_product_changed_between_decision_and_write_is_left_alone():
def test_duplicate_code_rowcount_protection_and_isolation_of_failures():
products = {**PRODUCTS, "000002": product("39235000", DUP=True)}
products = {**PRODUCTS, "000002": product("04090000", DUP=True)}
db = FakeFirebird(products)
out, cands, _ = candidates_of(db)
result = run(db, cands, out)
@@ -216,7 +227,7 @@ def test_official_mismatch_of_an_existing_class_is_reported():
def test_batch_limit_bounds_the_blast_radius():
db = FakeFirebird({f"{i:06d}": product("39235000") for i in range(1, 8)})
db = FakeFirebird({f"{i:06d}": product("04090000") for i in range(1, 8)})
out, cands, total = candidates_of(db, limit=3)
assert len(cands) == 3 and total == 7
@@ -252,40 +263,110 @@ def _fiscal_for(db):
return assess(db)
def test_job_reports_ready_products_but_writes_nothing_with_default_switches(monkeypatch):
def _products_of(db):
return [{"CODIGO": c, "CCLASSTRIB": v["CCLASSTRIB"], "CLASSIFICACAO_FISCAL": v["CLASSIFICACAO_FISCAL"], "PRODUTO": v.get("PRODUTO", "")} for c, v in db.products.items()]
def _no_write(service, monkeypatch):
monkeypatch.setattr(service, "open_write_connection", lambda *a, **k: (_ for _ in ()).throw(AssertionError("no write connection may be opened")))
def test_job_without_a_central_mode_or_with_disabled_or_dry_run_never_writes(monkeypatch):
service, _config = _service()
db = FakeFirebird(PRODUCTS)
fiscal = _fiscal_for(db)
monkeypatch.setattr(service, "open_write_connection", lambda *a, **k: (_ for _ in ()).throw(AssertionError("no write connection may be opened")))
products = [{"CODIGO": c, "CCLASSTRIB": v["CCLASSTRIB"], "CLASSIFICACAO_FISCAL": v["CLASSIFICACAO_FISCAL"]} for c, v in db.products.items()]
for cfg_kwargs, auth in (({}, False), ({"firebird_writes": True}, False), ({"firebird_writes": True, "rtc_auto_apply_enabled": True}, False), ({"rtc_auto_apply_enabled": True}, True), ({"firebird_writes": True}, True)):
config = _config(**cfg_kwargs)
summary = service._rtc_auto_apply({}, config, {"job_id": 1, "authorizations": {"rtc_auto_apply": auth}}, products, fiscal, NORMAL)
assert summary["mode"] == "REPORT_ONLY" and summary["gate"]["allowed"] is False and summary["ready_total"] == 3 and summary["write_attempts"] == 0 and summary["applied"] == []
_no_write(service, monkeypatch)
for auth in ({}, {"rtc_write_mode": "DISABLED"}, {"rtc_write_mode": "DRY_RUN"}, {"rtc_write_mode": "bogus"}, {"rtc_write_mode": "APPROVAL_REQUIRED", "rtc_approved_codes": []}, {"rtc_auto_apply": True}):
summary = service._rtc_auto_apply({}, _config(), {"job_id": 1, "authorizations": auth}, _products_of(db), fiscal, NORMAL)
assert summary["gate"]["allowed"] is False and summary["ready_total"] == 3 and summary["write_attempts"] == 0 and summary["applied"] == []
off = service._rtc_auto_apply({}, _config(), {"job_id": 1, "authorizations": {"rtc_write_mode": "DISABLED"}}, _products_of(db), fiscal, NORMAL)
dry = service._rtc_auto_apply({}, _config(), {"job_id": 1, "authorizations": {"rtc_write_mode": "DRY_RUN"}}, _products_of(db), fiscal, NORMAL)
assert off["preview"] == [] and off["mode"] == "REPORT_ONLY"
assert [c["product_code"] for c in dry["preview"]] == ["000001", "000002", "000003"] and all(c["rule_id"] == "RTC-R3" for c in dry["preview"]) and dry["capability"] == "RTC_CCLASSTRIB_WRITE_V1"
def test_job_applies_only_when_all_three_switches_are_open(monkeypatch):
def test_r4_suggestions_are_never_in_the_preview_and_never_written(monkeypatch):
service, _config = _service()
db = FakeFirebird({"000001": product("39235000"), "000002": product("39235000")})
fiscal = _fiscal_for(db)
_no_write(service, monkeypatch)
summary = service._rtc_auto_apply({}, _config(), {"job_id": 1, "authorizations": {"rtc_write_mode": "ENABLED"}}, _products_of(db), fiscal, NORMAL)
assert summary["ready_total"] == 0 and summary["preview"] == [] and summary["write_attempts"] == 0
def test_enabled_mode_applies_without_any_local_switch(monkeypatch):
service, _config = _service()
db = FakeFirebird(PRODUCTS)
fiscal = _fiscal_for(db)
monkeypatch.setattr(service, "open_write_connection", lambda env, path: _Closeable(db))
products = [{"CODIGO": c, "CCLASSTRIB": v["CCLASSTRIB"], "CLASSIFICACAO_FISCAL": v["CLASSIFICACAO_FISCAL"]} for c, v in db.products.items()]
config = _config(firebird_writes=True, rtc_auto_apply_enabled=True)
summary = service._rtc_auto_apply({}, config, {"job_id": 9, "authorizations": {"rtc_auto_apply": True}}, products, fiscal, NORMAL)
assert summary["mode"] == "AUTO_APPLY" and summary["write_successes"] == 3 and summary["allowed_fields"] == ["CCLASSTRIB"] and all(a["job_id"] == 9 for a in summary["applied"])
assert [db.products[c]["CCLASSTRIB"] for c in ("000001", "000002", "000003")] == ["200003", "000001", "200003"]
summary = service._rtc_auto_apply({}, _config(), {"job_id": 9, "authorizations": {"rtc_write_mode": "ENABLED"}}, _products_of(db), fiscal, NORMAL) # default config: no firebird_writes, no env switch
assert summary["mode"] == "ENABLED" and summary["write_successes"] == 3 and summary["allowed_fields"] == ["CCLASSTRIB"] and all(a["job_id"] == 9 and a["authorization_origin"] == "HUB_MODE:ENABLED" for a in summary["applied"])
assert [db.products[c]["CCLASSTRIB"] for c in ("000001", "000002", "000003")] == ["200003", "200034", "200003"]
a = summary["applied"][0]
assert a["rule_version"] and a["company_crt"] == "3" and a["action"] == "APPLY" and a["rtc_status"] == "ENABLED" and a["local_catalog_state"] == "MATCH_SAFE" and a["old_value"] is None
def test_job_never_writes_when_the_erp_says_rtc_is_off_even_with_all_switches_open(monkeypatch):
def test_approval_required_writes_only_the_approved_products(monkeypatch):
service, _config = _service()
db = FakeFirebird(PRODUCTS)
fiscal = _fiscal_for(db)
monkeypatch.setattr(service, "open_write_connection", lambda env, path: _Closeable(db))
summary = service._rtc_auto_apply({}, _config(), {"job_id": 9, "authorizations": {"rtc_write_mode": "APPROVAL_REQUIRED", "rtc_approved_codes": ["000002"]}}, _products_of(db), fiscal, NORMAL)
assert summary["write_successes"] == 1 and [db.products[c]["CCLASSTRIB"] for c in ("000001", "000002", "000003")] == [None, "200034", None]
def test_job_never_writes_when_the_erp_says_rtc_is_off_even_in_enabled_mode(monkeypatch):
service, _config = _service()
db = FakeFirebird(PRODUCTS, param=(0,))
fiscal = _fiscal_for(db)
monkeypatch.setattr(service, "open_write_connection", lambda *a, **k: (_ for _ in ()).throw(AssertionError("no write connection may be opened")))
products = [{"CODIGO": c, "CCLASSTRIB": None, "CLASSIFICACAO_FISCAL": "39235000"} for c in db.products]
summary = service._rtc_auto_apply({}, _config(firebird_writes=True, rtc_auto_apply_enabled=True), {"job_id": 1, "authorizations": {"rtc_auto_apply": True}}, products, fiscal, NORMAL)
_no_write(service, monkeypatch)
products = [{"CODIGO": c, "CCLASSTRIB": None, "CLASSIFICACAO_FISCAL": "04090000"} for c in db.products]
summary = service._rtc_auto_apply({}, _config(), {"job_id": 1, "authorizations": {"rtc_write_mode": "ENABLED"}}, products, fiscal, NORMAL)
assert summary["write_attempts"] == 0 and summary["ready_total"] == 0
def test_company_regime_or_fingerprint_change_aborts_before_the_update():
db = FakeFirebird(PRODUCTS)
out, cands, _ = candidates_of(db)
db.crt = ("1",) # the company was switched to Simples between decision and write
res = run(db, cands, out)
assert res["write_successes"] == 0 and res["aborted_reasons"] == {"COMPANY_CONTEXT_CHANGED": 3}
db.crt = ("3", "1") # ambiguous company context is also refused
assert run(db, cands, out)["aborted_reasons"] == {"COMPANY_CONTEXT_CHANGED": 3}
db.crt = ("3",)
db.products["000001"]["PRODUTO"] = "RENAMED AFTER THE DECISION"
res = run(db, cands, out)
assert res["aborted_reasons"] == {"PRODUCT_CHANGED_SINCE_DECISION": 1} and res["write_successes"] == 2 and db.products["000001"]["CCLASSTRIB"] is None
assert not any("UPDATE" in q.upper() and "C000004" in q.upper() for q, _ in db.statements)
def test_rollback_restores_only_when_the_value_still_equals_the_written_one_and_reapply_works():
db = FakeFirebird(PRODUCTS)
out, cands, _ = candidates_of(db)
first = run(db, cands, out)
assert first["write_successes"] == 3
items = [{"audit_id": i + 1, "product_code": a["product_code"], "written_value": a["new_value"], "rule_id": a["rule_id"]} for i, a in enumerate(first["applied"])]
db.products["000002"]["CCLASSTRIB"] = "200035" # an operator changed it afterwards
res = apply_mod.rollback_records(db, items, now="2026-09-24T21:00:00+00:00", job_id=8)
by = {r["product_code"]: r for r in res["rollbacks"]}
assert res["rolled_back"] == 2 and by["000002"]["outcome"] == "ABORTED" and by["000002"]["reason"] == "ROLLBACK_CONFLICT_VALUE_CHANGED" and db.products["000002"]["CCLASSTRIB"] == "200035"
assert by["000001"]["outcome"] == "ROLLED_BACK" and by["000001"]["rollback_of"] == 1 and by["000001"]["old_value"] == "200003" and by["000001"]["new_value"] is None and db.products["000001"]["CCLASSTRIB"] is None
again = apply_mod.rollback_records(db, items, now="2026-09-24T21:05:00+00:00", job_id=9)["rollbacks"]
assert [r["reason"] for r in again if r["product_code"] == "000001"] == ["ALREADY_EMPTY"]
out2, cands2, _ = candidates_of(db) # reapply: the emptied products are candidates again, the changed one is not
assert sorted(c["product_code"] for c in cands2) == ["000001", "000003"] and run(db, cands2, out2)["write_successes"] == 2
def test_rollback_uses_one_fixed_statement_and_rejects_malformed_requests():
db = FakeFirebird(PRODUCTS)
bad = apply_mod.rollback_records(db, [{"audit_id": 1, "product_code": "000001", "written_value": ""}, {"audit_id": 2, "product_code": "000001", "written_value": "200003", "restore_value": "999999"}], now="t")
assert [r["reason"] for r in bad["rollbacks"]] == ["ROLLBACK_REQUEST_INVALID"] * 2 and db.statements == []
db.products["000001"]["CCLASSTRIB"] = "200003"
apply_mod.rollback_records(db, [{"audit_id": 3, "product_code": "000001", "written_value": "200003"}], now="t")
writes = [q for q, _ in db.statements if q.strip().upper().startswith(("UPDATE", "INSERT", "DELETE"))]
assert writes == ["UPDATE C000025 SET CCLASSTRIB = NULL WHERE CODIGO = ? AND TRIM(CCLASSTRIB) = ?"]
class _Closeable:
def __init__(self, db):
self._db = db
@@ -295,3 +376,26 @@ class _Closeable:
def close(self):
pass
def test_firebird_unavailable_for_the_write_connection_fails_the_job_closed_with_zero_writes(monkeypatch):
service, _config = _service()
db = FakeFirebird(PRODUCTS)
fiscal = _fiscal_for(db)
monkeypatch.setattr(service, "open_write_connection", lambda *a, **k: (_ for _ in ()).throw(OSError("Firebird unavailable")))
import pytest as _pytest
with _pytest.raises(OSError):
service._rtc_auto_apply({}, _config(), {"job_id": 1, "authorizations": {"rtc_write_mode": "ENABLED"}}, _products_of(db), fiscal, NORMAL)
assert all(v["CCLASSTRIB"] is None for c, v in db.products.items() if c != "000005") and not [q for q, _ in db.statements if q.upper().startswith("UPDATE")]
def test_connection_is_closed_even_when_a_product_fails_mid_batch(monkeypatch):
service, _config = _service()
db = FakeFirebird({**PRODUCTS, "000002": product("04090000", DUP=True)})
fiscal = _fiscal_for(db)
closed = []
conn = _Closeable(db)
conn.close = lambda: closed.append(True)
monkeypatch.setattr(service, "open_write_connection", lambda env, path: conn)
summary = service._rtc_auto_apply({}, _config(), {"job_id": 1, "authorizations": {"rtc_write_mode": "ENABLED"}}, _products_of(db), fiscal, NORMAL)
assert closed == [True] and summary["write_successes"] == 2 and summary["aborted_reasons"]
@@ -0,0 +1,47 @@
"""RTC central write modes per installation, candidate preview/approval table, richer audit + rollback tracking.
Revision ID: 0028_rtc_write_modes
Revises: 0027_rtc_auto_apply
"""
from alembic import op
import sqlalchemy as sa
revision = "0028_rtc_write_modes"
down_revision = "0027_rtc_auto_apply"
branch_labels = None
depends_on = None
def upgrade():
op.add_column("installations", sa.Column("rtc_write_mode", sa.String(24), nullable=False, server_default="DISABLED"))
op.execute("UPDATE installations SET rtc_write_mode = 'ENABLED' WHERE rtc_auto_apply_authorized") # keeps an existing explicit authorization
for name, kind in (("rule_version", sa.String(32)), ("company_crt", sa.String(8)), ("authorization_origin", sa.String(48)), ("description_snapshot", sa.String(255)),
("installation_uuid", sa.String(64)), ("rtc_status", sa.String(24)), ("action", sa.String(16)), ("rollback_state", sa.String(16)), ("rollback_of", sa.Integer()),
("local_catalog_state", sa.String(32))):
op.add_column("rtc_write_audits", sa.Column(name, kind, nullable=True))
op.create_index("ix_rtc_write_audits_rollback_of", "rtc_write_audits", ["rollback_of"])
op.create_table(
"rtc_candidates",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("tenant_id", sa.Integer(), sa.ForeignKey("tenants.id"), nullable=False, index=True),
sa.Column("installation_id", sa.Integer(), sa.ForeignKey("installations.id"), nullable=False, index=True),
sa.Column("product_code", sa.String(64), nullable=False),
sa.Column("description", sa.String(255), nullable=True),
sa.Column("ncm", sa.String(16), nullable=True),
sa.Column("current_value", sa.String(16), nullable=True),
sa.Column("proposed", sa.String(16), nullable=False),
sa.Column("cst_ibscbs", sa.String(8), nullable=True),
sa.Column("rule_id", sa.String(32), nullable=False),
sa.Column("basis", sa.String(255), nullable=True),
sa.Column("status", sa.String(16), nullable=False, server_default="PENDING", index=True),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("installation_id", "product_code", name="uq_rtc_candidate_product"),
)
def downgrade():
op.drop_table("rtc_candidates")
op.drop_index("ix_rtc_write_audits_rollback_of", table_name="rtc_write_audits")
for name in ("rule_version", "company_crt", "authorization_origin", "description_snapshot", "installation_uuid", "rtc_status", "action", "rollback_state", "rollback_of", "local_catalog_state"):
op.drop_column("rtc_write_audits", name)
op.drop_column("installations", "rtc_write_mode")
+5 -6
View File
@@ -11,8 +11,8 @@ from .classification import ENGINE_VERSION, Classifier
from .config import settings
from .datasets.service import DatasetService
from .jobs import SYNC_KINDS, claim_next_job, complete_job, create_job, fail_job, schedule_automatic_sync
from .rtc_audit import record_rtc_write_audits
from .blocked_products import list_blockers, resolve_blocker, resolve_product_blockers, resolve_stale_fiscal_blockers, upsert_blocker
from .rtc_audit import job_authorizations, record_rtc_write_audits
from .blocked_products import list_blockers, resolve_blocker, resolve_product_blockers, resolve_stale_fiscal_blockers, upsert_blocker, upsert_blockers_bulk
from .context_keys import validate_entry
from .models import AgentSnapshot, BlockedProduct, FiscalContextEntry, ClassificationCache, ClassificationRequest, ClassificationResult, Dataset, Installation, Job
from .events import record_event
@@ -395,7 +395,7 @@ def router(session_factory, bootstrap_token):
session.commit()
logger.info("job claimed job_id=%s agent_id=%s kind=%s", job.id, agent.id, job.kind)
view = _job_view(job)
view["authorizations"] = {"rtc_auto_apply": bool(installation is not None and installation.rtc_auto_apply_authorized)} # decided centrally, per installation, default OFF
view["authorizations"] = job_authorizations(session, installation) if installation is not None else {"rtc_write_mode": "DISABLED"} # decided centrally, per installation, default OFF
return view
finally: session.close()
@@ -426,12 +426,11 @@ def router(session_factory, bootstrap_token):
result_summary = payload.get("result_summary", {})
installation = session.scalar(select(Installation).where(Installation.agent_id == agent.id))
pass_started = datetime.now(timezone.utc)
for blocker in result_summary.get("blocked_products", []):
upsert_blocker(session, agent.tenant_id, blocker, installation_id=installation.id if installation else None)
upsert_blockers_bulk(session, agent.tenant_id, list(result_summary.get("blocked_products", [])), installation_id=installation.id if installation else None)
for product_code in result_summary.get("resolved_products", []):
resolve_product_blockers(session, agent.tenant_id, product_code, installation_id=installation.id if installation else None)
if installation is not None:
record_rtc_write_audits(session, tenant_id=agent.tenant_id, installation=installation, agent=agent, job_id=job_id, rtc_auto_apply=result_summary.get("rtc_auto_apply"))
record_rtc_write_audits(session, tenant_id=agent.tenant_id, installation=installation, agent=agent, job_id=job_id, rtc_auto_apply=result_summary.get("rtc_auto_apply"), evaluated_all=bool((result_summary.get("fiscal_context") or {}).get("evaluated_all")))
if installation is not None and (result_summary.get("fiscal_context") or {}).get("evaluated_all"):
resolve_stale_fiscal_blockers(session, agent.tenant_id, installation.id, pass_started)
if not complete_job(session, job, result_summary): raise HTTPException(409, "job is not RUNNING")
+32 -6
View File
@@ -7,19 +7,26 @@ from .models import BlockedProduct
from .operational_state import blocker_fingerprint
def upsert_blocker(session, tenant_id: int, payload: dict, *, installation_id: int | None = None) -> BlockedProduct:
def _fingerprint_of(tenant_id: int, payload: dict, installation_id: int | None) -> str:
supplied_fingerprint = payload.get("blocker_fingerprint")
if installation_id is None:
fingerprint = supplied_fingerprint or blocker_fingerprint(payload.get("tenant", str(tenant_id)), payload.get("company", ""), payload.get("product_code", ""), payload.get("field", ""), payload.get("reason_code", ""), payload.get("input_fingerprint", ""))
return supplied_fingerprint or blocker_fingerprint(payload.get("tenant", str(tenant_id)), payload.get("company", ""), payload.get("product_code", ""), payload.get("field", ""), payload.get("reason_code", ""), payload.get("input_fingerprint", ""))
elif supplied_fingerprint:
fingerprint = blocker_fingerprint(supplied_fingerprint, "", "", "", "", "", installation_id=installation_id)
return blocker_fingerprint(supplied_fingerprint, "", "", "", "", "", installation_id=installation_id)
else:
fingerprint = blocker_fingerprint(payload.get("tenant", str(tenant_id)), payload.get("company", ""), payload.get("product_code", ""), payload.get("field", ""), payload.get("reason_code", ""), payload.get("input_fingerprint", ""), installation_id=installation_id)
row = session.scalar(select(BlockedProduct).where(BlockedProduct.blocker_fingerprint == fingerprint))
return blocker_fingerprint(payload.get("tenant", str(tenant_id)), payload.get("company", ""), payload.get("product_code", ""), payload.get("field", ""), payload.get("reason_code", ""), payload.get("input_fingerprint", ""), installation_id=installation_id)
def upsert_blocker(session, tenant_id: int, payload: dict, *, installation_id: int | None = None, commit: bool = True, existing: dict | None = None) -> BlockedProduct:
"""`existing` ({fingerprint: row}) and `commit=False` let a caller upsert thousands of blockers with one lookup per chunk and one commit (see upsert_blockers_bulk)."""
fingerprint = _fingerprint_of(tenant_id, payload, installation_id)
row = existing.get(fingerprint) if existing is not None else session.scalar(select(BlockedProduct).where(BlockedProduct.blocker_fingerprint == fingerprint))
now = datetime.now(timezone.utc)
if row is None:
row = BlockedProduct(tenant_id=tenant_id, installation_id=installation_id, company=payload.get("company", ""), product_code=str(payload.get("product_code", "")), gtin=payload.get("gtin"), description=payload.get("description"), blocker_type=payload.get("blocker_type", "BLOCKED_MISSING_EVIDENCE"), field=payload.get("field", ""), reason_code=payload.get("reason_code", ""), evidence_summary=payload.get("evidence_summary") or {}, input_fingerprint=payload.get("input_fingerprint", ""), blocker_fingerprint=fingerprint, first_seen_at=now, last_seen_at=now)
session.add(row)
if existing is not None:
existing[fingerprint] = row
else:
row.last_seen_at = now
row.evidence_summary = payload.get("evidence_summary") or row.evidence_summary
@@ -27,10 +34,29 @@ def upsert_blocker(session, tenant_id: int, payload: dict, *, installation_id: i
if row.installation_id is None and installation_id is not None:
row.installation_id = installation_id
row.resolved_at = None
session.commit()
if commit:
session.commit()
return row
BULK_CHUNK = 1000
def upsert_blockers_bulk(session, tenant_id: int, payloads: list, *, installation_id: int | None = None) -> int:
"""Same semantics as `upsert_blocker` per item, but O(chunks) statements instead of O(products): one IN-lookup per chunk, one commit at the end."""
n = 0
for start in range(0, len(payloads), BULK_CHUNK):
chunk = [p for p in payloads[start:start + BULK_CHUNK] if isinstance(p, dict)]
prints = {_fingerprint_of(tenant_id, p, installation_id) for p in chunk}
existing = {r.blocker_fingerprint: r for r in session.scalars(select(BlockedProduct).where(BlockedProduct.blocker_fingerprint.in_(prints)))} if prints else {}
for payload in chunk:
upsert_blocker(session, tenant_id, payload, installation_id=installation_id, commit=False, existing=existing)
n += 1
session.flush()
session.commit()
return n
def list_blockers(session, tenant_id: int, *, input_fingerprint: str | None = None, blocker_type: str | None = None, field: str | None = None, installation_id: int | None = None):
query = select(BlockedProduct).where(BlockedProduct.tenant_id == tenant_id, BlockedProduct.resolved_at.is_(None))
if installation_id is not None:
+72 -31
View File
@@ -8,17 +8,27 @@ Nothing here guesses. A class is proposed only when an EXPLICIT rule, backed by
Operation assumption (documented, product-level default): an ordinary domestic sale of the good. Operation-dependent classes (buyer is a public body,
exports, ZFM/ALC, credit presumed, ...) cannot be carried by a product default; when the annex offers such alternatives the answer is AMBIGUOUS.
Rule classes (what a rule is ALLOWED to do)
AUTO_APPLY_SAFE positive, explicit evidence: may be written to the ERP (only when every runtime gate also holds)
VALIDATION_ONLY used to validate/explain, never proposes a value
SUGGESTION_ONLY diagnostic suggestion shown to a human; NEVER authorizes an UPDATE
BLOCKED_BY_EVIDENCE the evidence is missing/ambiguous: safe-block with a human reason
Rules
RTC-R0 RTC disabled in the ERP -> NOT_APPLICABLE
RTC-R1 Simples family (CRT 1/2/4) -> INSUFFICIENT_EVIDENCE (NT 2025.002 v1.51: orientation for these taxpayers pending)
RTC-R2 no valid 8-digit NCM (decision engine did not confirm) -> INSUFFICIENT_EVIDENCE
RTC-A1 NCM listed in a REVOKED annex or an annex without a unique product-default class, or under a conditional item text -> AMBIGUOUS
RTC-A2 NCM listed in more than one annex -> AMBIGUOUS
RTC-A3 NCM in a chapter whose special regime depends on the nature/operation of the good, not on a code list -> AMBIGUOUS
RTC-R3 NCM listed in exactly one annex whose product-default class is unique, item text has no condition -> RESOLVED_WITH_EXPLICIT_RULE
RTC-R4 NCM listed in NO annex and in no nature-based chapter -> RESOLVED_WITH_EXPLICIT_RULE 000001 (integral taxation, LC 214 art. 4 general rule)
A proposed class must additionally be present in the local ERP catalog, MATCH the official table, be in force today, be usable in NF-e/NFC-e (the SGV's own
lookup filter) and carry a CST that exists; otherwise the answer degrades to INSUFFICIENT_EVIDENCE with the exact reason.
RTC-R0 RTC disabled in the ERP VALIDATION_ONLY -> NOT_APPLICABLE
RTC-G1 local RTC catalog failed integrity BLOCKED_BY_EVIDENCE -> INSUFFICIENT_EVIDENCE
RTC-R1 regime without an official rule (Simples family, unknown) BLOCKED_BY_EVIDENCE -> INSUFFICIENT_EVIDENCE
RTC-R2 no valid 8-digit NCM (decision engine did not confirm) BLOCKED_BY_EVIDENCE -> INSUFFICIENT_EVIDENCE
RTC-A1 annex without a unique product-default class / conditional text / revoked annex -> AMBIGUOUS
RTC-A2 NCM listed in more than one annex -> AMBIGUOUS
RTC-A3 chapter whose regime depends on the nature/operation of the good -> AMBIGUOUS
RTC-R3 NCM listed in EXACTLY ONE annex with a unique default class, condition-free item, regime normal (CRT 3)
AUTO_APPLY_SAFE -> RESOLVED_WITH_EXPLICIT_RULE
RTC-R4 NCM in NO annex and in no nature-based chapter: full taxation is the residual reading of LC 214 art. 4
SUGGESTION_ONLY -> SUGGESTION_ONLY (000001). Absence of a special regime is NOT positive evidence: never written.
RTC-G2 proposed class missing/different in the local SGV catalog, out of force or unusable in NF-e/NFC-e BLOCKED_BY_EVIDENCE -> INSUFFICIENT_EVIDENCE (RTC_LOCAL_CATALOG_INCOMPATIBLE)
A proposed class must additionally be MATCH_SAFE against the official table (local SGV catalog confirms every attribute that matters), in force today, usable in
NF-e/NFC-e (the SGV's own lookup filter) and carry a CST that exists; otherwise the answer degrades with the exact reason.
"""
from __future__ import annotations
@@ -32,7 +42,12 @@ from . import rtc_reference_data as ref
SIMPLES_FAMILY = frozenset({"1", "2", "4"})
RESOLVED_STATUSES = ("RESOLVED_DETERMINISTIC", "RESOLVED_WITH_EXPLICIT_RULE")
STATUSES = RESOLVED_STATUSES + ("AMBIGUOUS", "INSUFFICIENT_EVIDENCE", "NOT_APPLICABLE")
STATUSES = RESOLVED_STATUSES + ("SUGGESTION_ONLY", "AMBIGUOUS", "INSUFFICIENT_EVIDENCE", "NOT_APPLICABLE")
RULE_VERSION = "rtc-rules-v2"
AUTO_APPLY_SAFE, VALIDATION_ONLY, SUGGESTION_ONLY, BLOCKED_BY_EVIDENCE = "AUTO_APPLY_SAFE", "VALIDATION_ONLY", "SUGGESTION_ONLY", "BLOCKED_BY_EVIDENCE"
RULE_CLASSES = {"RTC-R0": VALIDATION_ONLY, "RTC-G1": BLOCKED_BY_EVIDENCE, "RTC-R1": BLOCKED_BY_EVIDENCE, "RTC-R2": BLOCKED_BY_EVIDENCE, "RTC-A1": BLOCKED_BY_EVIDENCE, "RTC-A2": BLOCKED_BY_EVIDENCE,
"RTC-A3": BLOCKED_BY_EVIDENCE, "RTC-R3": AUTO_APPLY_SAFE, "RTC-R4": SUGGESTION_ONLY, "RTC-G2": BLOCKED_BY_EVIDENCE}
HUMAN_REASONS = {"SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT": "Enquadramento RTC depende de regra ainda não determinável com segurança.", "COMPANY_REGIME_UNKNOWN": "Regime tributário da empresa não determinado."}
# Goods annexes whose product-default class is unique in the official table (verified by `verify_annex_class_map`).
ANNEX_DEFAULT_CLASS = {"I": "200003", "VII": "200034", "VIII": "200035", "XV": "200014"} # IX is out: 515001 (deferral with the same inputs) is a sibling class
@@ -55,14 +70,23 @@ class Resolution:
evidence: list = field(default_factory=list)
source_references: list = field(default_factory=list)
@property
def rule_class(self) -> str:
return RULE_CLASSES.get(self.rule_id, BLOCKED_BY_EVIDENCE)
@property
def auto_apply_candidate(self) -> bool:
return self.status in RESOLVED_STATUSES and bool(self.resolved_cclasstrib)
"""Only an AUTO_APPLY_SAFE rule with a proven class may ever become a write. Suggestions never do."""
return self.rule_class == AUTO_APPLY_SAFE and self.status in RESOLVED_STATUSES and bool(self.resolved_cclasstrib)
@property
def suggested_cclasstrib(self) -> str | None:
return self.resolved_cclasstrib if self.status == "SUGGESTION_ONLY" else None
def as_dict(self) -> dict:
return {"status": self.status, "rule_id": self.rule_id, "reason": self.reason, "resolved_cclasstrib": self.resolved_cclasstrib, "resolved_cst_ibscbs": self.resolved_cst_ibscbs,
"confidence_type": self.confidence_type, "inputs_used": self.inputs_used, "evidence": self.evidence, "source_references": self.source_references,
"auto_apply_candidate": self.auto_apply_candidate}
return {"status": self.status, "rule_id": self.rule_id, "rule_class": self.rule_class, "rule_version": RULE_VERSION, "reason": self.reason, "resolved_cclasstrib": self.resolved_cclasstrib,
"resolved_cst_ibscbs": self.resolved_cst_ibscbs, "confidence_type": self.confidence_type, "inputs_used": self.inputs_used, "evidence": self.evidence,
"source_references": self.source_references, "auto_apply_candidate": self.auto_apply_candidate}
def _digits(value) -> str:
@@ -85,34 +109,50 @@ def _num(value):
def compare_catalogs(local_rows: dict, today: date | None = None) -> dict:
"""local_rows: {code: {cst, tipo_aliquota, p_red_ibs, p_red_cbs, ind_nfe, ind_nfce, start, end}} read from the ERP. Never writes anywhere.
Per code: MATCH | LOCAL_ONLY | OFFICIAL_ONLY | MISMATCH | EXPIRED (official table is the reference)."""
Per code (the official table is the reference):
MATCH_SAFE every attribute the local table carries that matters to a product default agrees (CST, reductions, NF-e/NFC-e usability)
MATCH_WITH_NON_MATERIAL_DIFFERENCE only a scheduled local end date (class still in force today) differs
MISMATCH CST, reductions or NF-e/NFC-e usability differ
EXPIRED the official class ended
MISSING_LOCAL official class absent from the SGV catalog (local outdated)
MISSING_OFFICIAL local class the official table does not know"""
today = today or date.today()
official = _official_by_code()
result, details = {}, {}
for code in sorted(set(local_rows) | set(official)):
loc, off = local_rows.get(code), official.get(code)
if off is None:
result[code] = "LOCAL_ONLY"
result[code] = "MISSING_OFFICIAL"
elif off.get("end") and date.fromisoformat(off["end"]) < today:
result[code] = "EXPIRED"
details[code] = ["OFFICIAL_END_PASSED"] + (["PRESENT_LOCALLY"] if loc is not None else [])
elif loc is None:
result[code] = "OFFICIAL_ONLY"
result[code] = "MISSING_LOCAL"
else:
problems = []
problems, soft, info = [], [], []
if str(loc.get("cst") or "").strip() != off["cst"]:
problems.append("CST")
if "p_red_ibs" in loc and "p_red_cbs" in loc and (_num(loc.get("p_red_ibs")) != _num(off["p_red_ibs"]) or _num(loc.get("p_red_cbs")) != _num(off["p_red_cbs"])):
problems.append("REDUCTION") # only attributes the local table actually carries are compared
if "ind_nfe" in loc and "ind_nfce" in loc and (bool(loc.get("ind_nfe")) != off["ind_nfe"] or bool(loc.get("ind_nfce")) != off["ind_nfce"]):
problems.append("DOCUMENT_FLAGS")
result[code] = "MISMATCH" if problems else "MATCH"
if problems:
details[code] = problems
counts = {k: sum(1 for v in result.values() if v == k) for k in ("MATCH", "LOCAL_ONLY", "OFFICIAL_ONLY", "MISMATCH", "EXPIRED")}
material = counts["MISMATCH"] + counts["LOCAL_ONLY"] # a local class the official table contradicts or does not know
expired_present = sum(1 for c, d in details.items() if "PRESENT_LOCALLY" in d)
compat = "FULL" if counts["OFFICIAL_ONLY"] == 0 and expired_present == 0 and material == 0 else ("PARTIAL_LOCAL_OUTDATED" if material == 0 else "MATERIAL_MISMATCH")
start, end = str(loc.get("start") or "")[:10], str(loc.get("end") or "")[:10]
if start and date.fromisoformat(start) > today:
problems.append("LOCAL_NOT_IN_FORCE_YET")
elif start and off.get("start") and start != off["start"]:
info.append("VIGENCY_START_DIFFERS_BOTH_IN_FORCE") # informational: the class is in force today on both sides, so the emission effect is identical
if end and date.fromisoformat(end) < today:
problems.append("LOCAL_CLASS_ENDED")
elif end:
soft.append("LOCAL_END_SCHEDULED") # the class still works today; the local table schedules an end the official one does not
result[code] = "MISMATCH" if problems else ("MATCH_WITH_NON_MATERIAL_DIFFERENCE" if soft else "MATCH_SAFE")
if problems or soft or info:
details[code] = problems + soft + info
keys = ("MATCH_SAFE", "MATCH_WITH_NON_MATERIAL_DIFFERENCE", "MISMATCH", "EXPIRED", "MISSING_LOCAL", "MISSING_OFFICIAL")
counts = {k: sum(1 for v in result.values() if v == k) for k in keys}
material = counts["MISMATCH"] + counts["MISSING_OFFICIAL"] # a local class the official table contradicts or does not know
expired_present = sum(1 for d in details.values() if "PRESENT_LOCALLY" in d)
compat = "FULL" if counts["MISSING_LOCAL"] == 0 and expired_present == 0 and material == 0 else ("PARTIAL_LOCAL_OUTDATED" if material == 0 else "MATERIAL_MISMATCH")
return {"per_code": result, "details": details, "counts": counts, "expired_present_locally": expired_present, "compatibility": compat,
"official_catalog": {"sha256": ref.OFFICIAL_CATALOG["sha256"], "publication": ref.OFFICIAL_CATALOG["publication"], "file": ref.OFFICIAL_CATALOG["file"]}}
@@ -174,7 +214,7 @@ def _validate_class(code: str, local_rows: dict, compat: dict, today: date):
if loc is None:
return "CLASS_NOT_IN_LOCAL_CATALOG"
state = compat["per_code"].get(code)
if state != "MATCH":
if state != "MATCH_SAFE":
return f"LOCAL_OFFICIAL_{state}"
if official["start"] and date.fromisoformat(official["start"]) > today:
return "CLASS_NOT_YET_IN_FORCE"
@@ -198,8 +238,9 @@ def resolve_cclasstrib(product: dict, company: dict | None, ctx: dict, local_row
return Resolution("INSUFFICIENT_EVIDENCE", "RTC-G1", "LOCAL_RTC_CATALOG_INVALID", inputs_used=used)
if crt in SIMPLES_FAMILY:
return Resolution("INSUFFICIENT_EVIDENCE", "RTC-R1", "SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT", inputs_used=used,
source_references=_references([{"document": "NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026)", "note": "IBS/CBS orientations for CRT 1/2/4 pending; taxation from 2027 (LC 214 art. 348)"}]))
if not crt:
source_references=_references([{"document": "NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026)", "note": "IBS/CBS orientations for CRT 1/2/4 pending"},
{"document": "Ato Conjunto RFB/CGIBS n 4/2026 (DOU 31/07/2026)", "note": "art. 1 par. 1: DF-e obligation for Simples optantes starts 2027-01-01"}]))
if crt != "3":
return Resolution("INSUFFICIENT_EVIDENCE", "RTC-R1", "COMPANY_REGIME_UNKNOWN", inputs_used=used)
if len(ncm) != 8 or product.get("ncm_valid") is not True:
return Resolution("INSUFFICIENT_EVIDENCE", "RTC-R2", "NO_VALID_NCM", inputs_used=used)
@@ -229,5 +270,5 @@ def resolve_cclasstrib(product: dict, company: dict | None, ctx: dict, local_row
problem = _validate_class("000001", local_rows, compat, today)
if problem:
return Resolution("INSUFFICIENT_EVIDENCE", "RTC-G2", problem, inputs_used=used, source_references=_references())
return Resolution("RESOLVED_WITH_EXPLICIT_RULE", "RTC-R4", "NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER", "000001", "000", "EXCLUSION_OF_ALL_SPECIAL_REGIMES", used,
[{"annex_hits": 0, "chapter": ncm[:2]}], _references())
return Resolution("SUGGESTION_ONLY", "RTC-R4", "NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER", "000001", "000", "EXCLUSION_OF_ALL_SPECIAL_REGIMES", used,
[{"annex_hits": 0, "chapter": ncm[:2], "note": "absence of a special regime is not positive evidence: suggestion only, never written"}], _references())
+31 -1
View File
@@ -283,7 +283,8 @@ class Installation(Base):
last_sync_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
last_sync_changed: Mapped[int | None] = mapped_column(Integer, nullable=True)
# RTC auto-apply is authorized centrally per installation and is OFF until an operator explicitly turns it on (the Agent has its own second switch).
rtc_auto_apply_authorized: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
rtc_auto_apply_authorized: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") # legacy (0027); rtc_write_mode is the source of truth
rtc_write_mode: Mapped[str] = mapped_column(String(24), default="DISABLED", server_default="DISABLED") # DISABLED | DRY_RUN | APPROVAL_REQUIRED | ENABLED
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
@@ -755,4 +756,33 @@ class RtcWriteAudit(Base):
official_source_version: Mapped[dict] = mapped_column(JSON, default=dict)
local_catalog_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
written_at: Mapped[str | None] = mapped_column(String(40), nullable=True)
rule_version: Mapped[str | None] = mapped_column(String(32), nullable=True)
company_crt: Mapped[str | None] = mapped_column(String(8), nullable=True)
authorization_origin: Mapped[str | None] = mapped_column(String(48), nullable=True)
description_snapshot: Mapped[str | None] = mapped_column(String(255), nullable=True)
installation_uuid: Mapped[str | None] = mapped_column(String(64), nullable=True)
rtc_status: Mapped[str | None] = mapped_column(String(24), nullable=True)
local_catalog_state: Mapped[str | None] = mapped_column(String(32), nullable=True)
action: Mapped[str | None] = mapped_column(String(16), nullable=True) # APPLY | ROLLBACK
rollback_state: Mapped[str | None] = mapped_column(String(16), nullable=True) # REQUESTED | DONE | BLOCKED (on the original APPLY record)
rollback_of: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
recorded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
class RtcCandidate(Base):
"""Server-side preview of the products the Agent could fill (R3 only). Rows are proposals; only the Agent's re-derivation at write time decides."""
__tablename__ = "rtc_candidates"
__table_args__ = (UniqueConstraint("installation_id", "product_code", name="uq_rtc_candidate_product"),)
id: Mapped[int] = mapped_column(primary_key=True)
tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id"), index=True)
installation_id: Mapped[int] = mapped_column(ForeignKey("installations.id"), index=True)
product_code: Mapped[str] = mapped_column(String(64))
description: Mapped[str | None] = mapped_column(String(255), nullable=True)
ncm: Mapped[str | None] = mapped_column(String(16), nullable=True)
current_value: Mapped[str | None] = mapped_column(String(16), nullable=True)
proposed: Mapped[str] = mapped_column(String(16))
cst_ibscbs: Mapped[str | None] = mapped_column(String(8), nullable=True)
rule_id: Mapped[str] = mapped_column(String(32))
basis: Mapped[str | None] = mapped_column(String(255), nullable=True)
status: Mapped[str] = mapped_column(String(16), default="PENDING", server_default="PENDING", index=True) # PENDING | APPROVED | REJECTED | APPLIED | STALE
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
+17 -17
View File
@@ -121,7 +121,8 @@ def job_kind_label(value):
_FISCAL_LABELS = {
"CCLASSTRIB_MISSING": "cClassTrib ausente", "CCLASSTRIB_INVALID_FORMAT": "cClassTrib com formato inválido", "CCLASSTRIB_NOT_IN_CATALOG": "cClassTrib fora do catálogo local",
"CCLASSTRIB_MISSING": "cClassTrib ausente", "CCLASSTRIB_MISSING_UNRESOLVED": "cClassTrib não determinado", "CCLASSTRIB_AMBIGUOUS": "cClassTrib ambíguo", "CCLASSTRIB_CONFLICT": "cClassTrib conflitante",
"CCLASSTRIB_OFFICIAL_MISMATCH": "cClassTrib diverge da tabela oficial", "RTC_LOCAL_CATALOG_INCOMPATIBLE": "Catálogo RTC incompatível", "RTC_RULE_EVIDENCE_MISSING": "Regra fiscal insuficiente", "CCLASSTRIB_INVALID_FORMAT": "cClassTrib com formato inválido", "CCLASSTRIB_NOT_IN_CATALOG": "cClassTrib fora do catálogo local",
"RTC_CST_MISSING": "CST IBS/CBS ausente no catálogo", "RTC_CATALOG_INVALID": "Catálogo RTC local inválido",
"PIS_CST_MISSING": "CST PIS ausente", "PIS_CST_INVALID": "CST PIS inválido", "PIS_RATE_MISSING": "Alíquota PIS ausente", "PIS_RATE_INVALID": "Alíquota PIS inválida", "PIS_CST_INCIDENCE_MISMATCH": "CST PIS incompatível com a incidência",
"COFINS_CST_MISSING": "CST COFINS ausente", "COFINS_CST_INVALID": "CST COFINS inválido", "COFINS_RATE_MISSING": "Alíquota COFINS ausente", "COFINS_RATE_INVALID": "Alíquota COFINS inválida", "COFINS_CST_INCIDENCE_MISMATCH": "CST COFINS incompatível com a incidência",
@@ -161,7 +162,7 @@ def priority_label(value):
return _human_label(value, {"CRITICAL": "Crítica", "HIGH": "Alta", "NORMAL": "Média", "MEDIUM": "Média", "LOW": "Baixa"})
def rtc_status_view(summary, authorized=False):
def rtc_status_view(summary, mode="DISABLED"):
"""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
@@ -175,7 +176,7 @@ def rtc_status_view(summary, authorized=False):
observed = None
source = context.get("rtc_source") or "PARAMETROS.USAR_REFORMA_TRIBUTARIA"
products = _rtc_products_view(context.get("rtc_resolution"), bool(context["rtc_enabled"]))
return {"products": products, "apply": _rtc_apply_view(summary.get("rtc_auto_apply"), authorized), "enabled": bool(context["rtc_enabled"]), "label": "Ativa" if context["rtc_enabled"] else "Desativada", "supported": bool(context.get("rtc_supported")),
return {"products": products, "apply": _rtc_apply_view(summary.get("rtc_auto_apply"), mode), "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")), "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 "")}
@@ -187,23 +188,22 @@ def _rtc_products_view(resolution, enabled):
total = int(resolution["total"])
valid = int(resolution.get("valid_existing", 0))
ready = int(resolution.get("ready_to_apply", 0))
conflicts = int(resolution.get("conflicts", 0)) + int(resolution.get("official_mismatch", 0))
catalog = int(resolution.get("catalog_incompatible", 0))
blocked = total - valid - ready
pending = total - valid
return {"total": total, "valid": valid, "ready": ready, "blocked": blocked, "pending": pending,
return {"total": total, "valid": valid, "ready": ready, "blocked": blocked, "pending": pending, "conflicts": conflicts, "catalog_incompatible": catalog, "suggestion_only": int(resolution.get("suggestion_only", 0)),
"headline": f"Reforma Tributária ativa — cClassTrib pendente em {pending} produto{'s' if pending != 1 else ''}" if pending else "Reforma Tributária ativa — todos os produtos com cClassTrib válido",
"catalog": (resolution.get("catalog_compatibility") or {}).get("compatibility")}
def _rtc_apply_view(report, authorized):
"""State of the (default OFF) automatic fill, for the operator. Never claims a write happened unless the Agent reported it."""
gate = (report or {}).get("gate") or {}
applied = len((report or {}).get("applied") or [])
missing = gate.get("missing") or []
labels = {"FIREBIRD_WRITES": "escrita no Firebird desligada no Agent", "RTC_AUTO_APPLY_ENABLED": "chave RTC_AUTO_APPLY_ENABLED desligada no Agent", "HUB_AUTHORIZATION": "sem autorização no Hub"}
if gate.get("allowed"):
state = "Ativo"
elif authorized:
state = "Autorizado no Hub, aguardando o Agent (" + "; ".join(labels.get(m, m) for m in missing if m != "HUB_AUTHORIZATION") + ")" if any(m != "HUB_AUTHORIZATION" for m in missing) else "Autorizado no Hub"
else:
state = "Desativado"
return {"authorized": bool(authorized), "state": state, "applied_last_job": applied, "aborted_last_job": len((report or {}).get("aborted") or []), "mode": (report or {}).get("mode")}
RTC_MODE_LABELS = {"DISABLED": "Desativada", "DRY_RUN": "Dry-run", "APPROVAL_REQUIRED": "Aprovação manual", "ENABLED": "Automática"}
RTC_MODE_EXPLANATION = "Somente cClassTrib com regra fiscal determinística e catálogo validado pode ser preenchido automaticamente."
def _rtc_apply_view(report, mode):
"""State of the (default DISABLED) centrally controlled fill. Never claims a write happened unless the Agent reported it."""
mode = mode if mode in RTC_MODE_LABELS else "DISABLED"
report = report or {}
return {"mode": mode, "state": RTC_MODE_LABELS[mode], "explanation": RTC_MODE_EXPLANATION, "applied_last_job": len(report.get("applied") or []), "aborted_last_job": len(report.get("aborted") or []),
"ready_total": report.get("ready_total"), "capability": report.get("capability"), "authorized": mode in ("ENABLED", "APPROVAL_REQUIRED")}
+82 -15
View File
@@ -1,36 +1,103 @@
"""Persists the Agent's RTC auto-apply audit records (append-only) and mirrors a summary event for the panel."""
"""Persists the Agent's RTC write audit records (append-only), the candidate preview and the rollback bookkeeping; mirrors a summary event for the panel."""
from __future__ import annotations
from sqlalchemy import select, update
from .events import record_event
from .models import RtcWriteAudit
from .models import RtcCandidate, RtcWriteAudit
MAX_AUDITS_PER_JOB = 2000
MAX_CANDIDATES = 25000
WRITE_MODES = ("DISABLED", "DRY_RUN", "APPROVAL_REQUIRED", "ENABLED")
MAX_APPROVED_PER_JOB = 500
MAX_ROLLBACKS_PER_JOB = 500
def record_rtc_write_audits(session, *, tenant_id: int, installation, agent, job_id: int | None, rtc_auto_apply: dict | None) -> int:
"""Stores every applied/aborted write of the job. Never trusts the Agent for identity: tenant/installation/agent come from the authenticated context."""
def _clip(value, size):
return None if value is None else str(value)[:size]
def job_authorizations(session, installation) -> dict:
"""What the authenticated job carries to the Agent. Decided centrally per installation; the Agent has no local switch."""
mode = installation.rtc_write_mode if installation.rtc_write_mode in WRITE_MODES else "DISABLED"
out = {"rtc_write_mode": mode, "rtc_approved_codes": [], "rtc_rollbacks": []}
if mode == "APPROVAL_REQUIRED":
out["rtc_approved_codes"] = list(session.scalars(select(RtcCandidate.product_code).where(RtcCandidate.installation_id == installation.id, RtcCandidate.status == "APPROVED").limit(MAX_APPROVED_PER_JOB)))
for audit in session.scalars(select(RtcWriteAudit).where(RtcWriteAudit.installation_id == installation.id, RtcWriteAudit.action == "APPLY", RtcWriteAudit.outcome == "APPLIED",
RtcWriteAudit.rollback_state == "REQUESTED").limit(MAX_ROLLBACKS_PER_JOB)):
out["rtc_rollbacks"].append({"audit_id": audit.id, "product_code": audit.product_code, "written_value": audit.new_value, "rule_id": audit.rule_id})
return out
def _store_preview(session, tenant_id, installation, summary, evaluated_all):
preview = summary.get("preview")
if not isinstance(preview, list) or installation.rtc_write_mode == "DISABLED":
return
existing = {c.product_code: c for c in session.scalars(select(RtcCandidate).where(RtcCandidate.installation_id == installation.id))}
seen = set()
for item in preview[:MAX_CANDIDATES]:
code = _clip(item.get("product_code"), 64)
if not code or not item.get("proposed"):
continue
seen.add(code)
row = existing.get(code)
if row is None:
session.add(RtcCandidate(tenant_id=tenant_id, installation_id=installation.id, product_code=code, description=_clip(item.get("description"), 255), ncm=_clip(item.get("ncm"), 16),
current_value=_clip(item.get("current") or None, 16), proposed=_clip(item["proposed"], 16), cst_ibscbs=_clip(item.get("cst"), 8), rule_id=_clip(item.get("rule_id"), 32),
basis=_clip(item.get("basis"), 255), status="PENDING"))
else:
if row.status in ("APPLIED", "STALE"):
row.status = "PENDING" # empty again (e.g. after a rollback) or newly eligible
if row.proposed != item["proposed"]:
row.proposed = _clip(item["proposed"], 16)
if row.status == "APPROVED":
row.status = "PENDING" # the proposal changed: the earlier approval does not carry over
row.description, row.ncm, row.cst_ibscbs = _clip(item.get("description"), 255), _clip(item.get("ncm"), 16), _clip(item.get("cst"), 8)
row.rule_id, row.basis = _clip(item.get("rule_id"), 32), _clip(item.get("basis"), 255)
if evaluated_all and not summary.get("preview_truncated"):
for code, row in existing.items():
if code not in seen and row.status in ("PENDING", "APPROVED"):
row.status = "STALE"
def record_rtc_write_audits(session, *, tenant_id: int, installation, agent, job_id: int | None, rtc_auto_apply: dict | None, evaluated_all: bool = False) -> int:
"""Stores every applied/aborted/rolled-back write of the job. Never trusts the Agent for identity: tenant/installation/agent come from the authenticated context."""
if not isinstance(rtc_auto_apply, dict):
return 0
records = list(rtc_auto_apply.get("applied") or []) + list(rtc_auto_apply.get("aborted") or [])
stored = applied = aborted = 0
_store_preview(session, tenant_id, installation, rtc_auto_apply, evaluated_all)
records = list(rtc_auto_apply.get("applied") or []) + list(rtc_auto_apply.get("aborted") or []) + list(rtc_auto_apply.get("rollbacks") or [])
stored = applied = aborted = rolled = 0
for item in records[:MAX_AUDITS_PER_JOB]:
if not isinstance(item, dict) or item.get("field") != "CCLASSTRIB":
continue
action = "ROLLBACK" if item.get("action") == "ROLLBACK" else "APPLY"
outcome = str(item.get("outcome", ""))[:16]
rollback_of = item.get("rollback_of") if action == "ROLLBACK" else None
code = str(item.get("product_code", ""))[:64]
session.add(RtcWriteAudit(
tenant_id=tenant_id, installation_id=installation.id, agent_id=agent.id, job_id=job_id, client=getattr(installation, "display_name", None),
product_code=str(item.get("product_code", ""))[:64], field="CCLASSTRIB", outcome=str(item.get("outcome", ""))[:16], reason=(item.get("reason") or None) and str(item["reason"])[:96],
old_value=(None if item.get("old_value") is None else str(item["old_value"])[:64]), new_value=(None if item.get("new_value") is None else str(item["new_value"])[:64]),
product_code=code, field="CCLASSTRIB", outcome=outcome, reason=(item.get("reason") or None) and str(item["reason"])[:96],
old_value=_clip(item.get("old_value"), 64), new_value=_clip(item.get("new_value"), 64),
rule_id=(item.get("rule_id") or None) and str(item["rule_id"])[:32], evidence=item.get("evidence") or {}, official_source_version=item.get("official_source_version") or {},
local_catalog_hash=item.get("local_catalog_hash"), written_at=item.get("timestamp")))
local_catalog_hash=item.get("local_catalog_hash"), written_at=item.get("timestamp"), rule_version=_clip(item.get("rule_version"), 32), company_crt=_clip(item.get("company_crt"), 8),
authorization_origin=_clip(item.get("authorization_origin"), 48), description_snapshot=_clip(item.get("description_snapshot"), 255), installation_uuid=_clip(getattr(installation, "installation_id", None), 64),
rtc_status=_clip(item.get("rtc_status"), 24), local_catalog_state=_clip(item.get("local_catalog_state"), 32), action=action, rollback_of=rollback_of if isinstance(rollback_of, int) else None))
stored += 1
if item.get("outcome") == "APPLIED":
candidate_of = (RtcCandidate.installation_id == installation.id) & (RtcCandidate.product_code == code)
if action == "ROLLBACK":
if isinstance(rollback_of, int):
session.execute(update(RtcWriteAudit).where(RtcWriteAudit.id == rollback_of, RtcWriteAudit.installation_id == installation.id).values(rollback_state="DONE" if outcome == "ROLLED_BACK" else "BLOCKED"))
if outcome == "ROLLED_BACK":
rolled += 1
session.execute(update(RtcCandidate).where(candidate_of).values(status="PENDING"))
elif outcome == "APPLIED":
applied += 1
session.execute(update(RtcCandidate).where(candidate_of).values(status="APPLIED"))
else:
aborted += 1
if stored:
record_event(session, tenant_id=tenant_id, installation_id=installation.id, agent_id=agent.id, severity="WARNING" if applied else "INFO", event_type="RTC_AUTO_APPLY", job_id=job_id,
message=f"cClassTrib preenchido em {applied} produto(s); {aborted} abortado(s)",
context={"applied": applied, "aborted": aborted, "reasons": rtc_auto_apply.get("aborted_reasons") or {}, "mode": rtc_auto_apply.get("mode")})
if stored:
session.commit()
record_event(session, tenant_id=tenant_id, installation_id=installation.id, agent_id=agent.id, severity="WARNING" if (applied or rolled) else "INFO", event_type="RTC_AUTO_APPLY", job_id=job_id,
message=f"cClassTrib: {applied} preenchido(s), {rolled} revertido(s), {aborted} abortado(s)",
context={"applied": applied, "rolled_back": rolled, "aborted": aborted, "reasons": rtc_auto_apply.get("aborted_reasons") or {}, "mode": rtc_auto_apply.get("mode")})
session.commit()
return stored
+22 -10
View File
@@ -5,12 +5,13 @@ the single RTC blocker of a product is refined to the cause that matters, never
"""
from __future__ import annotations
from .cclasstrib_resolver import Resolution
from .cclasstrib_resolver import HUMAN_REASONS, Resolution
RTC_CATALOG_REASONS = ("LOCAL_OFFICIAL_", "CLASS_NOT_IN_LOCAL_CATALOG", "CLASS_NOT_IN_OFFICIAL_TABLE", "CLASS_NOT_YET_IN_FORCE", "CLASS_NOT_USABLE_IN_NFE_NFCE", "LOCAL_RTC_CATALOG_INVALID")
EVIDENCE_REASONS = ("NO_VALID_NCM", "COMPANY_REGIME_UNKNOWN")
RTC_TYPES = ("CCLASSTRIB_MISSING", "CCLASSTRIB_MISSING_UNRESOLVED", "CCLASSTRIB_AMBIGUOUS", "CCLASSTRIB_CONFLICT", "CCLASSTRIB_OFFICIAL_MISMATCH", "RTC_LOCAL_CATALOG_OUTDATED",
RTC_TYPES = ("CCLASSTRIB_MISSING", "CCLASSTRIB_MISSING_UNRESOLVED", "CCLASSTRIB_AMBIGUOUS", "CCLASSTRIB_CONFLICT", "CCLASSTRIB_OFFICIAL_MISMATCH", "RTC_LOCAL_CATALOG_INCOMPATIBLE",
"RTC_RULE_EVIDENCE_MISSING", "CCLASSTRIB_INVALID_FORMAT", "CCLASSTRIB_NOT_IN_CATALOG", "RTC_CST_MISSING", "RTC_CATALOG_INVALID")
BAD_LOCAL_STATES = ("MISMATCH", "EXPIRED", "MISSING_OFFICIAL")
def _text(value) -> str:
@@ -19,8 +20,9 @@ def _text(value) -> str:
def slim(resolution: Resolution) -> dict:
"""Small, audit-grade view kept in the job result (full evidence stays in the resolver output)."""
return {"status": resolution.status, "rule_id": resolution.rule_id, "reason": resolution.reason, "resolved_cclasstrib": resolution.resolved_cclasstrib,
"resolved_cst_ibscbs": resolution.resolved_cst_ibscbs, "confidence_type": resolution.confidence_type, "auto_apply_candidate": resolution.auto_apply_candidate}
return {"status": resolution.status, "rule_id": resolution.rule_id, "rule_class": resolution.rule_class, "reason": resolution.reason, "resolved_cclasstrib": resolution.resolved_cclasstrib,
"resolved_cst_ibscbs": resolution.resolved_cst_ibscbs, "confidence_type": resolution.confidence_type, "auto_apply_candidate": resolution.auto_apply_candidate,
"suggested_cclasstrib": resolution.suggested_cclasstrib}
def refine_rtc_finding(found: dict, product: dict, resolution: Resolution | None, compat_state: dict | None) -> dict:
@@ -37,19 +39,24 @@ def refine_rtc_finding(found: dict, product: dict, resolution: Resolution | None
if "CCLASSTRIB_MISSING" in kinds:
if resolution.auto_apply_candidate:
new = ("CCLASSTRIB_MISSING", f"READY_TO_APPLY:{resolution.resolved_cclasstrib}:{resolution.rule_id}")
elif resolution.status == "SUGGESTION_ONLY":
new = ("CCLASSTRIB_MISSING_UNRESOLVED", f"{resolution.rule_id}:SUGGESTION_ONLY:{resolution.resolved_cclasstrib}")
elif resolution.status == "AMBIGUOUS":
new = ("CCLASSTRIB_AMBIGUOUS", f"{resolution.rule_id}:{resolution.reason}")
elif resolution.reason.startswith(RTC_CATALOG_REASONS):
new = ("RTC_LOCAL_CATALOG_OUTDATED", resolution.reason)
new = ("RTC_LOCAL_CATALOG_INCOMPATIBLE", resolution.reason)
elif resolution.reason in EVIDENCE_REASONS:
new = ("RTC_RULE_EVIDENCE_MISSING", resolution.reason)
else:
new = ("CCLASSTRIB_MISSING_UNRESOLVED", f"{resolution.rule_id}:{resolution.reason}")
found["blockers"] = others + [{"type": new[0], "category": "RTC", "reason": new[1]}]
blocker = {"type": new[0], "category": "RTC", "reason": new[1]}
if resolution.reason in HUMAN_REASONS:
blocker["human"] = HUMAN_REASONS[resolution.reason]
found["blockers"] = others + [blocker]
return found
if not rtc_blockers and value: # a valid existing class: compare, never overwrite
state = (compat_state or {}).get(value)
if state in ("MISMATCH", "EXPIRED", "LOCAL_ONLY"):
if state in BAD_LOCAL_STATES:
found["blockers"] = others + [{"type": "CCLASSTRIB_OFFICIAL_MISMATCH", "category": "RTC", "reason": f"LOCAL_VS_OFFICIAL:{state}"}]
elif resolution.auto_apply_candidate and resolution.resolved_cclasstrib != value:
found["blockers"] = others + [{"type": "CCLASSTRIB_CONFLICT", "category": "RTC", "reason": f"HAS:{value};RULE:{resolution.rule_id}:{resolution.resolved_cclasstrib}"}]
@@ -59,8 +66,9 @@ def refine_rtc_finding(found: dict, product: dict, resolution: Resolution | None
def summarize_rtc(findings: dict, compat: dict | None = None) -> dict:
"""Counts for the panel and the audit: how many products already have a valid class, how many are ready to be filled, how many are blocked and why."""
out = {"total": 0, "valid_existing": 0, "ready_to_apply": 0, "ambiguous": 0, "insufficient": 0, "conflicts": 0, "official_mismatch": 0, "not_applicable": 0, "other_blocked": 0, "by_rule": {}, "by_reason": {}}
"""Counts for the panel and the audit: valid, auto-resolvable (safe), suggested (diagnostic only), blocked and conflicting, catalog-incompatible."""
out = {"total": 0, "valid_existing": 0, "ready_to_apply": 0, "suggestion_only": 0, "ambiguous": 0, "insufficient": 0, "conflicts": 0, "official_mismatch": 0, "catalog_incompatible": 0,
"not_applicable": 0, "other_blocked": 0, "by_rule": {}, "by_reason": {}}
for found in findings.values():
out["total"] += 1
if found.get("applicability", {}).get("RTC") != "REQUIRED":
@@ -77,11 +85,15 @@ def summarize_rtc(findings: dict, compat: dict | None = None) -> dict:
out["conflicts"] += 1
elif "CCLASSTRIB_OFFICIAL_MISMATCH" in types:
out["official_mismatch"] += 1
elif "RTC_LOCAL_CATALOG_INCOMPATIBLE" in types or "RTC_CATALOG_INVALID" in types:
out["catalog_incompatible"] += 1
elif res.get("auto_apply_candidate") and "CCLASSTRIB_MISSING" in types:
out["ready_to_apply"] += 1
elif res.get("status") == "SUGGESTION_ONLY":
out["suggestion_only"] += 1
elif "CCLASSTRIB_AMBIGUOUS" in types:
out["ambiguous"] += 1
elif types & {"CCLASSTRIB_MISSING_UNRESOLVED", "RTC_RULE_EVIDENCE_MISSING", "RTC_LOCAL_CATALOG_OUTDATED", "CCLASSTRIB_MISSING"}:
elif types & {"CCLASSTRIB_MISSING_UNRESOLVED", "RTC_RULE_EVIDENCE_MISSING", "CCLASSTRIB_MISSING"}:
out["insufficient"] += 1
else:
out["other_blocked"] += 1
+6 -4
View File
@@ -236,9 +236,11 @@ def evaluate_product(product: dict, company: dict | None, rtc: dict | None, toda
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."""
"""INFORMATIONAL ONLY (never a blocker, never part of completeness). The operational rule stays ERP_RTC_ENABLED; this records what the official acts say about the
obligation to issue DF-e with IBS/CBS for the company regime, so a timing difference can be explained without inventing a rule.
Source: Ato Conjunto RFB/CGIBS n 4/2026 (DOU 31/07/2026) art. 1: NF-e (55) and NFC-e (65) from 2026-08-03; par. 1: Simples optantes from 2027-01-01."""
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"]}
"ibs_cbs_in_documents": "MANDATORY_FROM_2027-01-01" if simples else ("MANDATORY_FROM_2026-08-03" if _text(crt) == "3" else "UNKNOWN_REGIME"),
"cclasstrib_orientation": "PENDING_OFFICIAL_NT" if simples else ("TABLE_AND_LC214_ANNEXES" if _text(crt) == "3" else "UNKNOWN_REGIME"),
"sources": ["Ato Conjunto RFB/CGIBS n 4/2026 (DOU 31/07/2026) art. 1 I, II and par. 1", "NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT", "LC 214/2025 art. 348"]}
+1 -1
View File
@@ -4,5 +4,5 @@ kept identical to this) and what an AgentRelease row's `version` column holds
once published — never a second, independent version number. Fiscal semantics
are versioned separately (ENGINE_VERSION/DECISION_VERSION in classification.py,
DECISION_ENGINE_VERSION etc.) and are NOT changed by a release bump."""
GTI_FISCAL_VERSION = "1.1.9"
GTI_FISCAL_VERSION = "1.2.0"
API_COMPAT_MAJOR_MINOR = "1.0" # an Agent 1.0.x/1.1.x talks to any Hub 1.0.x
+102 -21
View File
@@ -8,18 +8,18 @@ from urllib.parse import parse_qs, urlencode
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import select
from sqlalchemy import func, select, update
from . import agent_distribution, client_sync, config, enrollment, sso
from .config import settings
from .blocker_resolution import BlockerResolutionUnavailable, _company_key, resolve_blocker_with_context
from .context_keys import ALLOWED_KEYS, ENUMS
from .installation_identity import reference_agent_version
from .panel_presentation import job_kind_label, job_status_label, rtc_status_view
from .panel_presentation import RTC_MODE_EXPLANATION, RTC_MODE_LABELS, job_kind_label, job_status_label, rtc_status_view
from .control_plane import blocker_detail, blocker_filter_summary, dashboard_summary, installation_view, jobs_status_summary, list_blockers, list_installations, list_jobs, list_panel_events
from .human_auth import create_session, current_session, require_csrf, verify_password
from .panel_presentation import format_ui_datetime
from .models import AdminUser, Agent, AgentEnrollment, AgentRelease, AgentSnapshot, BlockedProduct, FiscalContextEntry, Installation, Job, Tenant
from .models import AdminUser, Agent, AgentEnrollment, AgentRelease, AgentSnapshot, BlockedProduct, FiscalContextEntry, Installation, Job, RtcCandidate, RtcWriteAudit, Tenant
from .jobs import create_job
from .audit import record_audit
@@ -296,7 +296,7 @@ def router(session_factory):
if installation.processing_enabled and last_sync is not None:
next_sync = last_sync + timedelta(seconds=installation.sync_interval_seconds)
last_summary = session.scalar(select(Job.result_summary).where(Job.agent_id == agent.id, Job.status == "COMPLETED", Job.kind.in_(("INITIAL_SYNC", "SYNC_INCREMENTAL", "REPROCESS_ALL"))).order_by(Job.id.desc()).limit(1))
return {"rtc": rtc_status_view(last_summary, bool(installation.rtc_auto_apply_authorized)), "rtc_authorized": bool(installation.rtc_auto_apply_authorized), "enabled": bool(installation.processing_enabled), "interval_minutes": installation.sync_interval_seconds // 60,
return {"rtc": rtc_status_view(last_summary, installation.rtc_write_mode), "rtc_mode": installation.rtc_write_mode, "enabled": bool(installation.processing_enabled), "interval_minutes": installation.sync_interval_seconds // 60,
"last_sync": format_ui_datetime(last_sync) if last_sync else None, "next_sync": format_ui_datetime(next_sync) if next_sync else None,
"last_changed": installation.last_sync_changed, "last_job": {"kind": last_job.kind, "status": last_job.status, "id": last_job.id, "kind_label": job_kind_label(last_job.kind), "status_label": job_status_label(last_job.status)} if last_job else None}
@@ -319,30 +319,111 @@ def router(session_factory):
return RedirectResponse(f"/agents/{agent_id}?tab=jobs", status_code=303)
finally: session.close()
@routes.post("/agents/{agent_id}/rtc-auto-apply")
async def rtc_auto_apply_authorization(request: Request, agent_id: int):
"""Central authorization of the automatic cClassTrib fill (default OFF). Enabling requires an explicit typed confirmation; every change is audited."""
def _rtc_target(session, agent_id):
agent = session.get(Agent, agent_id)
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id)) if agent else None
return agent, installation
@routes.get("/agents/{agent_id}/rtc")
async def rtc_control_page(request: Request, agent_id: int):
"""Central cClassTrib control: write mode, candidate preview (server-side pagination), approvals and audit/rollback."""
session, auth_data, redirect = page_auth(request)
if redirect: return redirect
try:
agent, installation = _rtc_target(session, agent_id)
if not agent or not installation: return HTMLResponse("Agent not found", status_code=404)
q = request.query_params
status = q.get("status") or "PENDING"
page = max(1, int(q.get("page", "1") or 1)) if str(q.get("page", "1")).isdigit() else 1
size = 50
base = select(RtcCandidate).where(RtcCandidate.installation_id == installation.id)
counts = dict(session.execute(select(RtcCandidate.status, func.count()).where(RtcCandidate.installation_id == installation.id).group_by(RtcCandidate.status)).all())
shown = base.where(RtcCandidate.status == status) if status in ("PENDING", "APPROVED", "REJECTED", "APPLIED", "STALE") else base
total = int(session.scalar(select(func.count()).select_from(shown.subquery())) or 0)
rows = list(session.scalars(shown.order_by(RtcCandidate.product_code).offset((page - 1) * size).limit(size)))
audits = list(session.scalars(select(RtcWriteAudit).where(RtcWriteAudit.installation_id == installation.id, RtcWriteAudit.outcome.in_(("APPLIED", "ROLLED_BACK"))).order_by(RtcWriteAudit.id.desc()).limit(25)))
ops = _ops_view(session, installation, agent)
return render(request, "rtc_control.html", {"agent_id": agent_id, "installation": installation, "mode": installation.rtc_write_mode, "modes": RTC_MODE_LABELS, "explanation": RTC_MODE_EXPLANATION, "ops": ops,
"rows": rows, "counts": counts, "status": status, "page": page, "pages": max(1, -(-total // size)), "total": total, "audits": audits})
finally: session.close()
@routes.post("/agents/{agent_id}/rtc-mode")
async def rtc_write_mode(request: Request, agent_id: int):
"""Central authorization of the cClassTrib fill, per installation. Default DISABLED; ENABLED needs a typed confirmation; every change is audited."""
session, auth_data, redirect = page_auth(request)
if redirect: return redirect
try:
values = await form_values(request); web_session, admin = auth_data
require_csrf(web_session, values.get("csrf_token"))
agent = session.get(Agent, agent_id)
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id)) if agent else None
agent, installation = _rtc_target(session, agent_id)
if not agent or not installation: return HTMLResponse("Agent not found", status_code=404)
decision = values.get("decision")
if decision == "authorize":
if (values.get("confirm") or "").strip().upper() != "PREENCHER":
return HTMLResponse("Digite PREENCHER para confirmar a autorização", status_code=422)
installation.rtc_auto_apply_authorized = True
elif decision == "revoke":
installation.rtc_auto_apply_authorized = False
else:
return HTMLResponse("decisão inválida", status_code=422)
record_audit(session, admin_user_id=admin.id, tenant_id=installation.tenant_id, action=f"RTC_AUTO_APPLY_{decision.upper()}", target_type="installation", target_id=installation.installation_id,
context={"authorized": installation.rtc_auto_apply_authorized}, severity="WARNING")
mode = (values.get("mode") or "").strip().upper()
if mode not in RTC_MODE_LABELS:
return HTMLResponse("modo inválido", status_code=422)
if mode == "ENABLED" and (values.get("confirm") or "").strip().upper() != "PREENCHER":
return HTMLResponse("Digite PREENCHER para confirmar o modo automático", status_code=422)
previous = installation.rtc_write_mode
installation.rtc_write_mode = mode
installation.rtc_auto_apply_authorized = mode == "ENABLED"
record_audit(session, admin_user_id=admin.id, tenant_id=installation.tenant_id, action="RTC_WRITE_MODE", target_type="installation", target_id=installation.installation_id,
context={"from": previous, "to": mode}, severity="WARNING")
session.commit()
return RedirectResponse(f"/agents/{agent_id}", status_code=303)
return RedirectResponse(f"/agents/{agent_id}/rtc", status_code=303)
finally: session.close()
@routes.post("/agents/{agent_id}/rtc-candidates")
async def rtc_candidate_action(request: Request, agent_id: int):
session, auth_data, redirect = page_auth(request)
if redirect: return redirect
try:
values = await form_values(request); web_session, admin = auth_data
require_csrf(web_session, values.get("csrf_token"))
agent, installation = _rtc_target(session, agent_id)
if not agent or not installation: return HTMLResponse("Agent not found", status_code=404)
action = values.get("action")
scope = RtcCandidate.installation_id == installation.id
ids = [int(x) for x in parse_qs((await request.body()).decode()).get("ids", []) if x.isdigit()]
select_all = values.get("all") == "1" or request.query_params.get("all") == "1"
changed = 0
if action in ("approve", "reject"):
new, old = ("APPROVED", ("PENDING",)) if action == "approve" else ("REJECTED", ("PENDING", "APPROVED"))
cond = scope & RtcCandidate.status.in_(old) & (RtcCandidate.id.in_(ids) if not select_all else RtcCandidate.status.in_(old))
changed = session.execute(update(RtcCandidate).where(cond).values(status=new)).rowcount
record_audit(session, admin_user_id=admin.id, tenant_id=installation.tenant_id, action=f"RTC_CANDIDATES_{action.upper()}", target_type="installation", target_id=installation.installation_id, context={"changed": changed, "all": select_all}, severity="WARNING")
elif action == "apply_approved":
if installation.rtc_write_mode != "APPROVAL_REQUIRED":
return HTMLResponse("Disponível apenas no modo Aprovação manual", status_code=409)
codes = list(session.scalars(select(RtcCandidate.product_code).where(scope, RtcCandidate.status == "APPROVED").order_by(RtcCandidate.product_code).limit(500)))
if codes:
job = create_job(session, agent, "REPROCESS_PRODUCTS", {"codigos": codes, "reason": "RTC_APPLY_APPROVED", "requested_by": admin.id})
record_audit(session, admin_user_id=admin.id, tenant_id=installation.tenant_id, action="RTC_APPLY_APPROVED", target_type="installation", target_id=installation.installation_id, context={"job_id": job.id, "products": len(codes)}, severity="WARNING")
else:
return HTMLResponse("ação inválida", status_code=422)
session.commit()
return RedirectResponse(f"/agents/{agent_id}/rtc", status_code=303)
finally: session.close()
@routes.post("/agents/{agent_id}/rtc-rollback")
async def rtc_rollback_request(request: Request, agent_id: int):
"""Requests the rollback of ONE audit record. The Agent executes it only if the value in the ERP still equals what was written; no free SQL exists."""
session, auth_data, redirect = page_auth(request)
if redirect: return redirect
try:
values = await form_values(request); web_session, admin = auth_data
require_csrf(web_session, values.get("csrf_token"))
agent, installation = _rtc_target(session, agent_id)
if not agent or not installation: return HTMLResponse("Agent not found", status_code=404)
if (values.get("confirm") or "").strip().upper() != "REVERTER":
return HTMLResponse("Digite REVERTER para confirmar", status_code=422)
audit = session.get(RtcWriteAudit, int(values.get("audit_id") or 0))
if audit is None or audit.installation_id != installation.id or audit.action != "APPLY" or audit.outcome != "APPLIED" or audit.rollback_state in ("REQUESTED", "DONE"):
return HTMLResponse("registro não reversível", status_code=409)
audit.rollback_state = "REQUESTED"
job = create_job(session, agent, "REPROCESS_PRODUCTS", {"codigos": [audit.product_code], "reason": "RTC_ROLLBACK", "requested_by": admin.id})
record_audit(session, admin_user_id=admin.id, tenant_id=installation.tenant_id, action="RTC_ROLLBACK_REQUESTED", target_type="installation", target_id=installation.installation_id,
context={"audit_id": audit.id, "product_code": audit.product_code, "job_id": job.id}, severity="WARNING")
session.commit()
return RedirectResponse(f"/agents/{agent_id}/rtc", status_code=303)
finally: session.close()
@routes.post("/agents/{agent_id}/sync-now")
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "gti-fiscal-hub"
version = "1.1.9"
version = "1.2.0"
requires-python = ">=3.10"
dependencies = [
"fastapi>=0.115,<1",
+1 -2
View File
@@ -9,8 +9,7 @@
<a class="botao secondary" href="/agents/{{ detail.agent_id }}/reprocess-confirmation" title="Recalcula todos os produtos (pede confirmação)">Reprocessar tudo</a>
{% if ops.enabled %}<form method="post" action="/agents/{{ detail.agent_id }}/pause" style="display:inline"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button type="submit" class="caution" title="Mantém o heartbeat, mas não executa jobs">Pausar processamento</button></form>{% else %}<form method="post" action="/agents/{{ detail.agent_id }}/resume" style="display:inline"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button type="submit" class="primary-action" title="Volta a executar jobs">Retomar processamento</button></form>{% endif %}
</div></div><dl class="facts"><dt>Processamento</dt><dd><span class="badge {{ '' if ops.enabled else 'critical' }}">{{ "Ativo" if ops.enabled else "Pausado" }}</span> · sincronização automática a cada {{ ops.interval_minutes }} min</dd><dt>Última sincronização</dt><dd>{{ ops.last_sync or "ainda não sincronizado" }}{% if ops.last_changed is not none %} · {{ ops.last_changed }} produto(s) alterado(s){% endif %}</dd><dt>Sincronização agendada</dt><dd>{{ ops.next_sync or ("aguardando o primeiro sync" if ops.enabled else "pausado") }}</dd>{% if ops.rtc %}<dt>Reforma Tributária</dt><dd><span class="badge {{ "" if ops.rtc.enabled else "muted" }}" title="{{ ops.rtc.title }}">{{ ops.rtc.label }}</span></dd>{% endif %}{% if ops.rtc and ops.rtc.enabled %}{% if ops.rtc.products %}<dt>Produtos RTC</dt><dd><b>{{ ops.rtc.products.headline }}</b><br><small>Total {{ ops.rtc.products.total }} · com cClassTrib válido {{ ops.rtc.products.valid }} · prontos para preenchimento {{ ops.rtc.products.ready }} · ambíguos/bloqueados {{ ops.rtc.products.blocked }}</small></dd>{% endif %}
<dt>Preenchimento automático</dt><dd>{{ ops.rtc.apply.state }}{% if ops.rtc.apply.applied_last_job %} · {{ ops.rtc.apply.applied_last_job }} preenchido(s) no último job{% endif %}
<form method="post" action="/agents/{{ detail.agent_id }}/rtc-auto-apply" style="display:inline"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">{% if ops.rtc_authorized %}<input type="hidden" name="decision" value="revoke"><button type="submit" class="caution">Revogar autorização</button>{% else %}<input type="hidden" name="decision" value="authorize"><input type="text" name="confirm" placeholder="digite PREENCHER" aria-label="Confirmar autorização" size="14"><button type="submit" class="secondary" title="Autoriza o Hub; o Agent ainda precisa ter a escrita e a chave RTC_AUTO_APPLY_ENABLED ligadas">Autorizar</button>{% endif %}</form></dd>{% endif %}<dt>Último job</dt><dd>{% if ops.last_job %}<a href="/jobs?scope=all&amp;job_id={{ ops.last_job.id }}">{{ ops.last_job.kind_label }}</a> · {{ ops.last_job.status_label }}{% else %}nenhum{% endif %}</dd></dl></section>
<dt>Preenchimento automático</dt><dd>{{ ops.rtc.apply.state }}{% if ops.rtc.apply.applied_last_job %} · {{ ops.rtc.apply.applied_last_job }} preenchido(s) no último job{% endif %} · <a href="/agents/{{ detail.agent_id }}/rtc">Controle e candidatos cClassTrib</a></dd>{% endif %}<dt>Último job</dt><dd>{% if ops.last_job %}<a href="/jobs?scope=all&amp;job_id={{ ops.last_job.id }}">{{ ops.last_job.kind_label }}</a> · {{ ops.last_job.status_label }}{% else %}nenhum{% endif %}</dd></dl></section>
<nav class="tabs" aria-label="Seções da instalação">{% for name,label in [("overview","Resumo"),("fiscal","Fiscal"),("blockers","Blockers"),("jobs","Jobs"),("events","Eventos")] %}<a class="{% if tab==name %}active{% endif %}" href="?tab={{ name }}">{{ label }}</a>{% endfor %}</nav>
<section class="split"><article class="panel"><div class="panel-title"><h2>Contexto da empresa</h2></div><dl class="facts"><dt>Cliente</dt><dd>{{ detail.client or "Não informado" }}</dd><dt>Nome de exibição</dt><dd>{{ detail.display_name or "Não informado" }}</dd><dt>Hostname</dt><dd>{{ detail.hostname or "Não informado" }}</dd><dt>Empresa</dt><dd>{{ detail.company_name or detail.company_code or "Não informado" }}</dd><dt>UF</dt><dd>{{ detail.company_uf or "Não informado" }}</dd><dt>CRT</dt><dd>{{ detail.company_crt or "Não informado" }}</dd><dt>Versão do agent</dt><dd>{{ detail.agent_version_display }}</dd><dt>Último heartbeat</dt><dd title="{{ detail.last_seen_at }}">{{ detail.last_seen_display }} · {{ detail.last_seen_age }}</dd><dt>Firebird</dt><dd>{{ detail.firebird_version or "Não informado" }} · ODS {{ detail.firebird_ods or "Não informado" }}</dd><dt>IDs técnicos</dt><dd>Agent {{ detail.technical_ids.agent_id }} · instalação {{ detail.technical_ids.installation_db_id }}</dd></dl></article>
<article class="panel"><div class="panel-title"><h2>Eventos recentes</h2><a href="#eventos">Ver eventos</a></div>{% for event in events[:8] %}<div class="event-row"><span class="event-time">{{ event.occurred_display }}</span><span><b>{{ event.event_type }}</b><small>{{ event.message }}{% if event.job_id %} · <a href="/jobs?job_id={{ event.job_id }}">Job {{ event.job_id }}</a>{% endif %}{% if event.product_code %} · <a href="/blockers?agent_id={{ detail.agent_id }}&amp;product_code={{ event.product_code|urlencode }}">Produto {{ event.product_code }}</a>{% endif %}</small></span><em class="severity {{ event.severity|lower }}">Severidade: {{ event.severity }}</em></div>{% else %}<p class="empty-state">Nenhum evento.</p>{% endfor %}</article></section>
+41
View File
@@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block title %}cClassTrib | GTI Fiscal{% endblock %}
{% block content %}
<div class="detail-head"><div><p class="eyebrow"><a href="/agents/{{ agent_id }}">← Instalação</a> / REFORMA TRIBUTÁRIA</p><h1>Preenchimento de cClassTrib</h1>
<p class="muted">{{ explanation }}</p></div></div>
{% set rtc = ops.rtc %}
{% if rtc and rtc.products %}
<section class="cards" aria-label="Resumo cClassTrib">
<article class="metric"><span>Produtos</span><strong>{{ rtc.products.total }}</strong></article>
<article class="metric"><span>cClassTrib válido</span><strong>{{ rtc.products.valid }}</strong></article>
<article class="metric"><span>Auto-resolvíveis</span><strong>{{ rtc.products.ready }}</strong></article>
<article class="metric"><span>Bloqueados</span><strong>{{ rtc.products.blocked }}</strong></article>
<article class="metric"><span>Conflitos</span><strong>{{ rtc.products.conflicts }}</strong></article>
<article class="metric"><span>Catálogo incompatível</span><strong>{{ rtc.products.catalog_incompatible }}</strong></article>
</section>
{% endif %}
<section class="panel"><div class="panel-title"><div><h2>Modo de preenchimento</h2><p class="muted">Decidido centralmente para esta instalação. Padrão: Desativada. Nenhuma configuração local no servidor do cliente.</p></div></div>
<form method="post" action="/agents/{{ agent_id }}/rtc-mode"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
{% for key, label in modes.items() %}<label style="display:block"><input type="radio" name="mode" value="{{ key }}" {% if key == mode %}checked{% endif %}> {{ label }}
{% if key == "DISABLED" %}— apenas informa{% elif key == "DRY_RUN" %}— mostra os candidatos, não escreve{% elif key == "APPROVAL_REQUIRED" %}— escreve só o que for aprovado abaixo{% else %}— preenche os candidatos determinísticos{% endif %}</label>{% endfor %}
<p><input type="text" name="confirm" placeholder="digite PREENCHER (modo automático)" aria-label="Confirmar modo automático" size="30"> <button type="submit" class="primary-action">Salvar modo</button></p></form></section>
<section class="panel"><div class="panel-title"><div><h2>Candidatos</h2><p class="muted">{{ total }} produto(s) neste filtro.</p></div></div>
<nav class="tabs" aria-label="Filtro de candidatos">{% for st, label in [("PENDING","Pendentes"),("APPROVED","Aprovados"),("REJECTED","Rejeitados"),("APPLIED","Aplicados"),("STALE","Obsoletos")] %}<a class="{% if status==st %}active{% endif %}" href="?status={{ st }}">{{ label }} ({{ counts.get(st, 0) }})</a>{% endfor %}</nav>
<form method="post" action="/agents/{{ agent_id }}/rtc-candidates"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<table><thead><tr><th></th><th>Produto</th><th>NCM</th><th>Atual</th><th>Proposto</th><th>CST IBS/CBS</th><th>Regra</th><th>Base</th><th>Status</th></tr></thead><tbody>
{% for c in rows %}<tr><td><input type="checkbox" name="ids" value="{{ c.id }}" aria-label="Selecionar {{ c.product_code }}"></td><td>{{ c.product_code }} · {{ c.description or "" }}</td><td>{{ c.ncm }}</td><td>{{ c.current_value or "vazio" }}</td><td>{{ c.proposed }}</td><td>{{ c.cst_ibscbs }}</td><td>{{ c.rule_id }}</td><td>{{ c.basis }}</td><td>{{ c.status }}</td></tr>
{% else %}<tr><td colspan="9" class="muted">Nenhum candidato neste filtro.</td></tr>{% endfor %}</tbody></table>
<p>Página {{ page }} de {{ pages }}{% if page > 1 %} · <a href="?status={{ status }}&amp;page={{ page - 1 }}">anterior</a>{% endif %}{% if page < pages %} · <a href="?status={{ status }}&amp;page={{ page + 1 }}">próxima</a>{% endif %}</p>
<p><button type="submit" name="action" value="approve" class="secondary">Aprovar selecionados</button> <button type="submit" name="action" value="reject" class="caution">Rejeitar selecionados</button>
<button type="submit" name="action" value="approve" class="secondary" formaction="/agents/{{ agent_id }}/rtc-candidates?all=1">Aprovar todos os pendentes</button>
{% if mode == "APPROVAL_REQUIRED" %}<button type="submit" name="action" value="apply_approved" class="primary-action">Aplicar aprovados agora</button>{% endif %}</p></form></section>
<section class="panel"><div class="panel-title"><div><h2>Auditoria e reversão</h2><p class="muted">Cada preenchimento pode ser revertido individualmente, desde que o valor no ERP ainda seja o gravado.</p></div></div>
<table><thead><tr><th>#</th><th>Produto</th><th>Ação</th><th>Resultado</th><th>Antes → Depois</th><th>Regra</th><th>Origem</th><th>Reversão</th></tr></thead><tbody>
{% for a in audits %}<tr><td>{{ a.id }}</td><td>{{ a.product_code }} · {{ a.description_snapshot or "" }}</td><td>{{ a.action or "APPLY" }}</td><td>{{ a.outcome }}{% if a.reason %} ({{ a.reason }}){% endif %}</td><td>{{ a.old_value or "vazio" }} → {{ a.new_value or "vazio" }}</td><td>{{ a.rule_id }}</td><td>{{ a.authorization_origin }}</td>
<td>{% if a.action == "APPLY" and a.outcome == "APPLIED" and not a.rollback_state %}<form method="post" action="/agents/{{ agent_id }}/rtc-rollback" style="display:inline"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="audit_id" value="{{ a.id }}"><input type="text" name="confirm" placeholder="digite REVERTER" size="12" aria-label="Confirmar reversão"><button type="submit" class="caution">Reverter</button></form>{% else %}{{ a.rollback_state or "" }}{% endif %}</td></tr>
{% else %}<tr><td colspan="8" class="muted">Nenhum preenchimento registrado.</td></tr>{% endfor %}</tbody></table></section>
{% endblock %}
+34 -10
View File
@@ -44,7 +44,7 @@ def test_rtc_off_is_not_applicable_and_never_a_candidate():
def test_simples_is_insufficient_until_the_official_orientation_exists():
r = resolve("10063021", company=SIMPLES)
assert r.status == "INSUFFICIENT_EVIDENCE" and r.rule_id == "RTC-R1" and not r.auto_apply_candidate and r.source_references[-1]["document"].startswith("NT 2025.002")
assert r.status == "INSUFFICIENT_EVIDENCE" and r.rule_id == "RTC-R1" and not r.auto_apply_candidate and r.source_references[-1]["document"].startswith("Ato Conjunto RFB/CGIBS") and r.rule_class == "BLOCKED_BY_EVIDENCE"
assert resolve("10063021", company={"crt": None}).reason == "COMPANY_REGIME_UNKNOWN"
@@ -88,35 +88,59 @@ def test_nature_based_chapters_are_ambiguous():
assert r.status == "AMBIGUOUS" and r.rule_id in ("RTC-A3", "RTC-A1", "RTC-A2"), ncm
def test_residual_ncm_outside_every_annex_and_sensitive_chapter_is_integral_taxation():
def test_residual_ncm_outside_every_annex_is_only_a_suggestion_and_never_a_write_candidate():
r = resolve("39235000") # plastic closures: no annex, no nature-based regime
assert r.status == "RESOLVED_WITH_EXPLICIT_RULE" and r.resolved_cclasstrib == "000001" and r.resolved_cst_ibscbs == "000" and r.rule_id == "RTC-R4" and r.confidence_type == "EXCLUSION_OF_ALL_SPECIAL_REGIMES"
assert r.auto_apply_candidate and annex_matches("39235000") == []
assert r.status == "SUGGESTION_ONLY" and r.resolved_cclasstrib == "000001" and r.suggested_cclasstrib == "000001" and r.rule_id == "RTC-R4" and r.rule_class == "SUGGESTION_ONLY"
assert r.auto_apply_candidate is False and annex_matches("39235000") == [] and r.confidence_type == "EXCLUSION_OF_ALL_SPECIAL_REGIMES"
assert "not positive evidence" in r.evidence[0]["note"] and r.as_dict()["auto_apply_candidate"] is False
def test_only_auto_apply_safe_rules_can_authorize_a_write():
from central.app.cclasstrib_resolver import RULE_CLASSES
assert [rid for rid, cls in RULE_CLASSES.items() if cls == "AUTO_APPLY_SAFE"] == ["RTC-R3"]
assert RULE_CLASSES["RTC-R4"] == "SUGGESTION_ONLY" and RULE_CLASSES["RTC-R0"] == "VALIDATION_ONLY"
assert {c for c in RULE_CLASSES.values()} == {"AUTO_APPLY_SAFE", "VALIDATION_ONLY", "SUGGESTION_ONLY", "BLOCKED_BY_EVIDENCE"}
def test_regime_other_than_normal_never_reaches_a_positive_rule():
for company in ({"crt": "2"}, {"crt": "4"}, {"crt": "9"}, {"crt": ""}):
assert not resolve("10063021", company=company).auto_apply_candidate
def test_a_class_missing_or_different_in_the_local_catalog_is_never_proposed():
r = resolve("39235000", local=local_from_official(skip=("000001",)))
r = resolve("10063021", local=local_from_official(skip=("200003",)))
assert r.status == "INSUFFICIENT_EVIDENCE" and r.reason == "CLASS_NOT_IN_LOCAL_CATALOG" and not r.auto_apply_candidate
r2 = resolve("39235000", local=local_from_official(tweak={"000001": {"cst": "010"}}))
r2 = resolve("10063021", local=local_from_official(tweak={"200003": {"cst": "010"}}))
assert r2.reason == "LOCAL_OFFICIAL_MISMATCH" and not r2.auto_apply_candidate
r3 = resolve("10063021", local=local_from_official(tweak={"200003": {"ind_nfe": False, "ind_nfce": False}}))
assert r3.reason == "LOCAL_OFFICIAL_MISMATCH"
def test_invalid_local_catalog_blocks_every_proposal():
assert resolve("39235000", ctx={"rtc_enabled": True, "local_catalog_valid": False}).reason == "LOCAL_RTC_CATALOG_INVALID"
assert resolve("10063021", ctx={"rtc_enabled": True, "local_catalog_valid": False}).reason == "LOCAL_RTC_CATALOG_INVALID"
def test_catalog_comparison_classifies_every_code_and_flags_material_problems():
full = compare_catalogs(local_from_official(), TODAY)
assert full["counts"]["MATCH"] == 161 and full["counts"]["EXPIRED"] == 3 and full["compatibility"] == "PARTIAL_LOCAL_OUTDATED" # 220001-3 ended on 2026-01-01
assert full["counts"]["MATCH_SAFE"] == 161 and full["counts"]["EXPIRED"] == 3 and full["compatibility"] == "PARTIAL_LOCAL_OUTDATED" # 220001-3 ended on 2026-01-01
assert compare_catalogs(local_from_official(skip=("220001", "220002", "220003")), TODAY)["compatibility"] == "FULL"
outdated = compare_catalogs(local_from_official(skip=("000005", "200053")), TODAY)
assert outdated["counts"]["OFFICIAL_ONLY"] == 2 and outdated["compatibility"] == "PARTIAL_LOCAL_OUTDATED" and outdated["counts"]["MISMATCH"] == 0
assert outdated["counts"]["MISSING_LOCAL"] == 2 and outdated["compatibility"] == "PARTIAL_LOCAL_OUTDATED" and outdated["counts"]["MISMATCH"] == 0
local = local_from_official(tweak={"200003": {"p_red_ibs": "60"}})
local["999999"] = {"cst": "000", "p_red_ibs": "0", "p_red_cbs": "0", "ind_nfe": True, "ind_nfce": True}
bad = compare_catalogs(local, TODAY)
assert bad["per_code"]["200003"] == "MISMATCH" and bad["per_code"]["999999"] == "LOCAL_ONLY" and bad["compatibility"] == "MATERIAL_MISMATCH"
assert bad["per_code"]["200003"] == "MISMATCH" and bad["per_code"]["999999"] == "MISSING_OFFICIAL" and bad["compatibility"] == "MATERIAL_MISMATCH"
def test_non_material_differences_are_classified_but_not_treated_as_a_mismatch():
earlier = compare_catalogs(local_from_official(tweak={"200003": {"start": "2025-05-05"}}), TODAY) # SGV imports an earlier start: both in force today => same emission effect
assert earlier["per_code"]["200003"] == "MATCH_SAFE" and earlier["details"]["200003"] == ["VIGENCY_START_DIFFERS_BOTH_IN_FORCE"]
assert compare_catalogs(local_from_official(tweak={"200003": {"start": "2027-01-01"}}), TODAY)["per_code"]["200003"] == "MISMATCH"
assert compare_catalogs(local_from_official(tweak={"200003": {"end": "2026-01-01"}}), TODAY)["per_code"]["200003"] == "MISMATCH"
local = local_from_official(tweak={"200003": {"end": "2027-12-31"}}) # a scheduled local end the official table does not have
c = compare_catalogs(local, TODAY)
assert c["per_code"]["200003"] == "MATCH_WITH_NON_MATERIAL_DIFFERENCE" and c["counts"]["MISMATCH"] == 0
assert resolve("10063021", local=local).status == "INSUFFICIENT_EVIDENCE" # auto-apply requires MATCH_SAFE: a non-material difference still blocks the WRITE
def test_local_catalog_hash_is_stable_and_sensitive():
+76 -16
View File
@@ -594,27 +594,87 @@ def _installation(client, agent_id):
from central.app.models import Installation as Inst
with client._session_factory() as session:
row = session.scalar(select(Inst).where(Inst.agent_id == agent_id))
return bool(row.rtc_auto_apply_authorized)
return row.rtc_write_mode
def test_rtc_auto_apply_authorization_is_off_by_default_and_needs_csrf_and_a_typed_confirmation():
def test_rtc_write_mode_is_disabled_by_default_and_needs_csrf_valid_mode_and_a_typed_confirmation_for_automatic():
client, agent_id, _ = _panel_client()
assert _installation(client, agent_id) is False
assert _installation(client, agent_id) == "DISABLED"
csrf = client.cookies.get("gti_csrf")
url = f"/agents/{agent_id}/rtc-auto-apply"
assert client.post(url, data={"csrf_token": "wrong", "decision": "authorize", "confirm": "PREENCHER"}).status_code == 403
assert client.post(url, data={"csrf_token": csrf, "decision": "authorize"}).status_code == 422
assert client.post(url, data={"csrf_token": csrf, "decision": "authorize", "confirm": "sim"}).status_code == 422
assert _installation(client, agent_id) is False
assert client.post(url, data={"csrf_token": csrf, "decision": "authorize", "confirm": "preencher"}, follow_redirects=False).status_code == 303
assert _installation(client, agent_id) is True
assert client.post(url, data={"csrf_token": csrf, "decision": "revoke"}, follow_redirects=False).status_code == 303
assert _installation(client, agent_id) is False
assert client.post(url, data={"csrf_token": csrf, "decision": "other"}).status_code == 422
url = f"/agents/{agent_id}/rtc-mode"
assert client.post(url, data={"csrf_token": "wrong", "mode": "DRY_RUN"}).status_code == 403
assert client.post(url, data={"csrf_token": csrf, "mode": "ENABLED"}).status_code == 422
assert client.post(url, data={"csrf_token": csrf, "mode": "ENABLED", "confirm": "sim"}).status_code == 422
assert client.post(url, data={"csrf_token": csrf, "mode": "bogus"}).status_code == 422
assert _installation(client, agent_id) == "DISABLED"
for mode, extra in (("DRY_RUN", {}), ("APPROVAL_REQUIRED", {}), ("ENABLED", {"confirm": "preencher"}), ("DISABLED", {})):
assert client.post(url, data={"csrf_token": csrf, "mode": mode, **extra}, follow_redirects=False).status_code == 303
assert _installation(client, agent_id) == mode
from central.app.models import AuditEvent
with client._session_factory() as session:
actions = [a.action for a in session.scalars(select(AuditEvent).order_by(AuditEvent.id)).all() if a.action.startswith("RTC_AUTO_APPLY")]
assert actions == ["RTC_AUTO_APPLY_AUTHORIZE", "RTC_AUTO_APPLY_REVOKE"]
actions = [a.action for a in session.scalars(select(AuditEvent).order_by(AuditEvent.id)).all() if a.action.startswith("RTC_")]
assert actions == ["RTC_WRITE_MODE"] * 4
def _seed_rtc(client, agent_id, n=120):
from central.app.models import Installation as Inst, RtcCandidate, RtcWriteAudit
with client._session_factory() as session:
inst = session.scalar(select(Inst).where(Inst.agent_id == agent_id))
inst.rtc_write_mode = "APPROVAL_REQUIRED"
for i in range(n):
session.add(RtcCandidate(tenant_id=inst.tenant_id, installation_id=inst.id, product_code=f"C{i:04d}", description=f"PRODUTO {i}", ncm="10063021", proposed="200003", cst_ibscbs="200", rule_id="RTC-R3", basis="RTC-R3:ANNEX", status="PENDING"))
audit = RtcWriteAudit(tenant_id=inst.tenant_id, installation_id=inst.id, agent_id=agent_id, product_code="A1", field="CCLASSTRIB", outcome="APPLIED", old_value=None, new_value="200003", rule_id="RTC-R3", action="APPLY")
session.add(audit)
session.commit()
return audit.id
def test_rtc_control_page_paginates_candidates_on_the_server_and_shows_modes_counts_and_audit():
client, agent_id, _ = _panel_client()
_seed_rtc(client, agent_id)
page1 = client.get(f"/agents/{agent_id}/rtc").text
assert "Somente cClassTrib com regra fiscal determinística e catálogo validado pode ser preenchido automaticamente." in page1
for label in ("Desativada", "Dry-run", "Aprovação manual", "Automática"):
assert label in page1
assert "Pendentes (120)" in page1 and "C0000" in page1 and "C0049" in page1 and "C0050" not in page1 and "Página 1 de 3" in page1 and "Aplicar aprovados agora" in page1
page3 = client.get(f"/agents/{agent_id}/rtc?page=3").text
assert "C0119" in page3 and "C0099" not in page3 and "Página 3 de 3" in page3
assert "Reverter" in page3
def test_rtc_candidate_actions_approve_reject_apply_and_rollback_are_csrf_protected_and_scoped():
from central.app.models import Job, RtcCandidate, RtcWriteAudit
client, agent_id, _ = _panel_client()
audit_id = _seed_rtc(client, agent_id, n=10)
csrf = client.cookies.get("gti_csrf")
url = f"/agents/{agent_id}/rtc-candidates"
assert client.post(url, data={"csrf_token": "wrong", "action": "approve"}).status_code == 403
with client._session_factory() as session:
ids = [c.id for c in session.scalars(select(RtcCandidate).order_by(RtcCandidate.id)).all()]
assert client.post(url, content=f"csrf_token={csrf}&action=approve&ids={ids[0]}&ids={ids[1]}", headers={"content-type": "application/x-www-form-urlencoded"}, follow_redirects=False).status_code == 303
assert client.post(f"{url}?all=1", data={"csrf_token": csrf, "action": "approve"}, follow_redirects=False).status_code == 303
assert client.post(url, content=f"csrf_token={csrf}&action=reject&ids={ids[2]}", headers={"content-type": "application/x-www-form-urlencoded"}, follow_redirects=False).status_code == 303
with client._session_factory() as session:
counts = {c.product_code: c.status for c in session.scalars(select(RtcCandidate)).all()}
assert list(counts.values()).count("REJECTED") == 1 and list(counts.values()).count("APPROVED") == 9
jobs_before = len(session.scalars(select(Job)).all())
assert client.post(url, data={"csrf_token": csrf, "action": "apply_approved"}, follow_redirects=False).status_code == 303
with client._session_factory() as session:
job = session.scalars(select(Job).order_by(Job.id.desc())).first()
assert len(session.scalars(select(Job)).all()) == jobs_before + 1 and job.kind == "REPROCESS_PRODUCTS" and job.payload["reason"] == "RTC_APPLY_APPROVED" and len(job.payload["codigos"]) == 9
assert client.post(url, data={"csrf_token": csrf, "action": "nope"}).status_code == 422
roll = f"/agents/{agent_id}/rtc-rollback"
assert client.post(roll, data={"csrf_token": csrf, "audit_id": audit_id}).status_code == 422 # typed confirmation required
assert client.post(roll, data={"csrf_token": csrf, "audit_id": audit_id, "confirm": "reverter"}, follow_redirects=False).status_code == 303
assert client.post(roll, data={"csrf_token": csrf, "audit_id": audit_id, "confirm": "REVERTER"}).status_code == 409 # already requested: no duplicate
with client._session_factory() as session:
assert session.get(RtcWriteAudit, audit_id).rollback_state == "REQUESTED"
def test_apply_approved_is_refused_outside_the_approval_mode():
client, agent_id, _ = _panel_client()
csrf = client.cookies.get("gti_csrf")
assert client.post(f"/agents/{agent_id}/rtc-candidates", data={"csrf_token": csrf, "action": "apply_approved"}).status_code == 409
def test_panel_shows_rtc_products_summary_and_the_pending_message_only_when_rtc_is_on():
@@ -626,6 +686,6 @@ def test_panel_shows_rtc_products_summary_and_the_pending_message_only_when_rtc_
_complete_sync(client, agent_id, {"rtc_enabled": True, "rtc_supported": True, "rtc_catalog_valid": True, "rtc_resolution": resolution})
on = client.get(f"/agents/{agent_id}").text
assert "Reforma Tributária ativa — cClassTrib pendente em 521 produtos" in on and "prontos para preenchimento 280" in on and "ambíguos/bloqueados 241" in on
assert "Preenchimento automático" in on and "Desativado" in on and "digite PREENCHER" in on
assert "Preenchimento automático" in on and "Desativada" in on and f"/agents/{agent_id}/rtc" in on
for legacy in ("Escopo anterior", "Escopo ampliado"):
assert legacy not in on
+1 -1
View File
@@ -17,7 +17,7 @@ def test_health_is_unavailable_when_database_is_down():
def test_version_endpoint_reports_release_and_fiscal_versions():
body = TestClient(create_app(database_probe=lambda: True)).get("/version").json()
assert body["version"] == "1.1.9" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"]
assert body["version"] == "1.2.0" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"]
def test_production_refuses_the_dev_token(monkeypatch):
+87
View File
@@ -0,0 +1,87 @@
"""Security review of the centrally controlled cClassTrib write: who can change the mode, what the Agent can and cannot decide, tenant scoping, no secrets in audits."""
from sqlalchemy import select
from sqlalchemy.orm import Session
from central.app.models import AuditEvent, Installation, RtcCandidate, RtcWriteAudit
from central.tests.test_control_panel_pages import _panel_client, _seed_rtc
from central.tests.test_zero_touch_hub import H, OPERATIONAL, _client, age_last_sync, finish, hb
def test_new_rtc_routes_require_an_operator_session():
client, agent_id, _ = _panel_client()
client.cookies.clear()
for method, path in (("get", f"/agents/{agent_id}/rtc"), ("post", f"/agents/{agent_id}/rtc-mode"), ("post", f"/agents/{agent_id}/rtc-candidates"), ("post", f"/agents/{agent_id}/rtc-rollback")):
response = getattr(client, method)(path, follow_redirects=False)
assert response.status_code in (303, 401, 403), (path, response.status_code)
assert "login" in response.headers.get("location", "login") or response.status_code in (401, 403)
def test_default_mode_is_disabled_and_the_agent_cannot_choose_it():
client, engine = _client()
hb(client)
with Session(engine) as s:
assert s.scalars(select(Installation)).one().rtc_write_mode == "DISABLED"
job = client.get("/v1/jobs/next", headers=H).json()
assert job["authorizations"]["rtc_write_mode"] == "DISABLED"
# a forged result cannot change the mode, approve candidates or request rollbacks: none of those are read from the payload
forged = {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_write_mode": "ENABLED", "authorizations": {"rtc_write_mode": "ENABLED", "rtc_approved_codes": ["X"]},
"rtc_auto_apply": {"mode": "ENABLED", "hub_mode": "ENABLED", "preview": [{"product_code": "X", "proposed": "200003", "rule_id": "RTC-R3", "status": "APPROVED"}]}}
client.post(f"/v1/jobs/{job['job_id']}/complete", json={"result_summary": forged}, headers=H)
with Session(engine) as s:
assert s.scalars(select(Installation)).one().rtc_write_mode == "DISABLED"
assert s.scalars(select(RtcCandidate)).all() == [] # DISABLED stores no preview at all
def test_preview_from_the_agent_never_arrives_approved_and_never_approves_itself():
client, engine = _client()
hb(client)
with Session(engine) as s:
s.scalars(select(Installation)).one().rtc_write_mode = "APPROVAL_REQUIRED"
s.commit()
item = {"product_code": "X1", "description": "D", "ncm": "10063021", "current": "", "proposed": "200003", "cst": "200", "rule_id": "RTC-R3", "basis": "b", "status": "APPROVED"}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": {"evaluated_all": True}, "rtc_auto_apply": {"preview": [item]}})
with Session(engine) as s:
assert [c.status for c in s.scalars(select(RtcCandidate)).all()] == ["PENDING"]
age_last_sync(engine, 4000); hb(client)
assert client.get("/v1/jobs/next", headers=H).json()["authorizations"]["rtc_approved_codes"] == []
def test_a_rollback_result_cannot_touch_another_installations_audit():
client, engine = _client()
hb(client)
with Session(engine) as s:
inst = s.scalars(select(Installation)).one()
foreign = RtcWriteAudit(tenant_id=inst.tenant_id, installation_id=inst.id + 999, agent_id=inst.agent_id, product_code="F", field="CCLASSTRIB", outcome="APPLIED", new_value="200003", action="APPLY", rollback_state="REQUESTED")
# the FK is not enforced by the SQLite test DB; the scoping is what is under test
s.add(foreign); s.commit(); foreign_id = foreign.id
done = {"outcome": "ROLLED_BACK", "product_code": "F", "field": "CCLASSTRIB", "old_value": "200003", "new_value": None, "action": "ROLLBACK", "rollback_of": foreign_id}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"rollbacks": [done]}})
with Session(engine) as s:
assert s.get(RtcWriteAudit, foreign_id).rollback_state == "REQUESTED" # untouched
def test_rollback_and_candidate_actions_are_scoped_to_the_operators_installation_and_audited():
client, agent_id, _ = _panel_client()
_seed_rtc(client, agent_id, n=3)
csrf = client.cookies.get("gti_csrf")
with client._session_factory() as s:
other = RtcWriteAudit(tenant_id=1, installation_id=987654, agent_id=agent_id, product_code="Z", field="CCLASSTRIB", outcome="APPLIED", new_value="200003", action="APPLY")
s.add(other); s.commit(); other_id = other.id
assert client.post(f"/agents/{agent_id}/rtc-rollback", data={"csrf_token": csrf, "audit_id": other_id, "confirm": "REVERTER"}).status_code == 409
client.post(f"/agents/{agent_id}/rtc-candidates?all=1", data={"csrf_token": csrf, "action": "approve"}, follow_redirects=False)
with client._session_factory() as s:
actions = [a.action for a in s.scalars(select(AuditEvent)).all()]
assert "RTC_CANDIDATES_APPROVE" in actions
def test_audit_rows_never_carry_credentials_or_tokens():
client, engine = _client()
hb(client)
applied = {"outcome": "APPLIED", "product_code": "10", "field": "CCLASSTRIB", "old_value": None, "new_value": "200003", "rule_id": "RTC-R3", "action": "APPLY", "password": "p", "token": "t",
"evidence": {"reason": "r"}}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"applied": [applied]}})
with Session(engine) as s:
row = s.scalars(select(RtcWriteAudit)).one()
blob = repr([getattr(row, c.name) for c in RtcWriteAudit.__table__.columns])
assert "password" not in blob.lower() and "'t'" not in blob and "Bearer" not in blob
assert not any("pass" in c.name or "token" in c.name or "secret" in c.name for c in RtcWriteAudit.__table__.columns)
@@ -56,7 +56,8 @@ def test_rtc_enabled_needs_schema_and_erp_switch():
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 simples["informational_only"] and simples["ibs_cbs_in_documents"] == "MANDATORY_FROM_2027-01-01" and normal["ibs_cbs_in_documents"] == "MANDATORY_FROM_2026-08-03"
assert simples["cclasstrib_orientation"] == "PENDING_OFFICIAL_NT" and any("Ato Conjunto RFB/CGIBS n 4/2026" in x for x in simples["sources"])
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()
+61 -3
View File
@@ -270,15 +270,15 @@ def test_job_carries_the_central_rtc_authorization_default_off_and_follows_the_i
client, engine = _client()
hb(client)
job = client.get("/v1/jobs/next", headers=H).json()
assert job["authorizations"] == {"rtc_auto_apply": False} # never on by default
assert job["authorizations"] == {"rtc_write_mode": "DISABLED", "rtc_approved_codes": [], "rtc_rollbacks": []} # never on by default
client.post(f"/v1/jobs/{job['job_id']}/complete", json={"result_summary": {}}, headers=H)
with Session(engine) as s:
inst = s.scalars(select(Installation)).one()
inst.rtc_auto_apply_authorized = True
inst.rtc_write_mode = "ENABLED"
s.commit()
age_last_sync(engine, 4000)
hb(client)
assert client.get("/v1/jobs/next", headers=H).json()["authorizations"] == {"rtc_auto_apply": True}
assert client.get("/v1/jobs/next", headers=H).json()["authorizations"]["rtc_write_mode"] == "ENABLED"
def test_rtc_write_audits_are_persisted_with_server_side_identity_and_never_duplicated_per_job():
@@ -307,3 +307,61 @@ def test_report_only_jobs_leave_no_audit_rows():
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"mode": "REPORT_ONLY", "applied": [], "aborted": [], "ready_total": 280}})
with Session(engine) as s:
assert s.scalars(select(RtcWriteAudit)).all() == []
def _set_mode(engine, mode):
with Session(engine) as s:
s.scalars(select(Installation)).one().rtc_write_mode = mode
s.commit()
def test_preview_is_stored_only_when_the_central_mode_wants_it_and_approvals_travel_in_the_next_job():
from central.app.models import RtcCandidate
client, engine = _client()
hb(client)
preview = [{"product_code": f"P{i}", "description": f"ITEM {i}", "ncm": "10063021", "current": "", "proposed": "200003", "cst": "200", "rule_id": "RTC-R3", "basis": "RTC-R3:ANNEX"} for i in range(3)]
ctx = {"rtc_enabled": True, "evaluated_all": True}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": ctx, "rtc_auto_apply": {"mode": "REPORT_ONLY", "preview": preview, "preview_truncated": False}})
with Session(engine) as s:
assert s.scalars(select(RtcCandidate)).all() == [] # DISABLED: nothing is stored
_set_mode(engine, "APPROVAL_REQUIRED")
age_last_sync(engine, 4000); hb(client)
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": ctx, "rtc_auto_apply": {"mode": "APPROVAL_REQUIRED", "preview": preview, "preview_truncated": False}})
with Session(engine) as s:
rows = s.scalars(select(RtcCandidate).order_by(RtcCandidate.product_code)).all()
assert [(r.product_code, r.status, r.proposed) for r in rows] == [("P0", "PENDING", "200003"), ("P1", "PENDING", "200003"), ("P2", "PENDING", "200003")]
rows[1].status = "APPROVED"
s.commit()
age_last_sync(engine, 4000); hb(client)
job = client.get("/v1/jobs/next", headers=H).json()
assert job["authorizations"]["rtc_approved_codes"] == ["P1"] # only what a human approved
client.post(f"/v1/jobs/{job['job_id']}/complete", json={"result_summary": {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": ctx,
"rtc_auto_apply": {"mode": "APPROVAL_REQUIRED", "preview": [p for p in preview if p["product_code"] != "P2"], "preview_truncated": False,
"applied": [{"outcome": "APPLIED", "product_code": "P1", "field": "CCLASSTRIB", "old_value": None, "new_value": "200003", "rule_id": "RTC-R3", "action": "APPLY", "rule_version": "rtc-rules-v2",
"company_crt": "3", "authorization_origin": "HUB_MODE:APPROVAL_REQUIRED", "description_snapshot": "ITEM 1", "rtc_status": "ENABLED", "local_catalog_state": "MATCH_SAFE"}]}}}, headers=H)
with Session(engine) as s:
status = {r.product_code: r.status for r in s.scalars(select(RtcCandidate)).all()}
assert status == {"P0": "PENDING", "P1": "APPLIED", "P2": "STALE"} # applied is recorded; a product no longer eligible on a full evaluation becomes STALE
from central.app.models import RtcWriteAudit
audit = s.scalars(select(RtcWriteAudit)).one()
assert (audit.rule_version, audit.company_crt, audit.authorization_origin, audit.description_snapshot, audit.action, audit.local_catalog_state) == ("rtc-rules-v2", "3", "HUB_MODE:APPROVAL_REQUIRED", "ITEM 1", "APPLY", "MATCH_SAFE")
assert audit.installation_uuid
def test_rollback_request_travels_in_the_job_and_its_result_closes_the_original_audit():
from central.app.models import RtcWriteAudit
client, engine = _client()
hb(client)
applied = {"outcome": "APPLIED", "product_code": "P1", "field": "CCLASSTRIB", "old_value": None, "new_value": "200003", "rule_id": "RTC-R3", "action": "APPLY"}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"applied": [applied]}})
with Session(engine) as s:
audit = s.scalars(select(RtcWriteAudit)).one(); audit.rollback_state = "REQUESTED"; audit_id = audit.id
s.commit()
age_last_sync(engine, 4000); hb(client)
job = client.get("/v1/jobs/next", headers=H).json()
assert job["authorizations"]["rtc_rollbacks"] == [{"audit_id": audit_id, "product_code": "P1", "written_value": "200003", "rule_id": "RTC-R3"}]
done = {"outcome": "ROLLED_BACK", "product_code": "P1", "field": "CCLASSTRIB", "old_value": "200003", "new_value": None, "rule_id": "RTC-R3", "action": "ROLLBACK", "rollback_of": audit_id}
client.post(f"/v1/jobs/{job['job_id']}/complete", json={"result_summary": {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"rollbacks": [done]}}}, headers=H)
with Session(engine) as s:
rows = s.scalars(select(RtcWriteAudit).order_by(RtcWriteAudit.id)).all()
assert [(r.action, r.outcome, r.rollback_state, r.rollback_of) for r in rows] == [("APPLY", "APPLIED", "DONE", None), ("ROLLBACK", "ROLLED_BACK", None, audit_id)]
+1 -1
View File
@@ -19,4 +19,4 @@ FB_PASSWORD=
FISCAL_MODE=DRY_RUN
FIREBIRD_WRITES=0
POLL_INTERVAL_SECONDS=5
AGENT_VERSION=1.1.9
AGENT_VERSION=1.2.0
+1 -1
View File
@@ -164,7 +164,7 @@ switch ($Command) {
"GTI_HUB_URL=$hub", "AGENT_TOKEN=$token", "AGENT_ID=$agentId",
"COMPANY_CODE=$code", "COMPANY_CNPJ=$cnpj", "COMPANY_CRT_SOURCE=$crtSource",
"FIREBIRD_DATABASE_PATH=$db", "FB_USER=$fbUser", "FB_PASSWORD=$fbPass",
'FISCAL_MODE=DRY_RUN', 'FIREBIRD_WRITES=0', 'POLL_INTERVAL_SECONDS=5', 'AGENT_VERSION=1.1.9'
'FISCAL_MODE=DRY_RUN', 'FIREBIRD_WRITES=0', 'POLL_INTERVAL_SECONDS=5', 'AGENT_VERSION=1.2.0'
)
Set-Content -Path $EnvFile -Value $lines -Encoding UTF8
}
@@ -19,4 +19,4 @@ FB_PASSWORD=
FISCAL_MODE=DRY_RUN
FIREBIRD_WRITES=0
POLL_INTERVAL_SECONDS=5
AGENT_VERSION=1.1.9
AGENT_VERSION=1.2.0
@@ -19,4 +19,4 @@ FB_PASSWORD=
FISCAL_MODE=DRY_RUN
FIREBIRD_WRITES=0
POLL_INTERVAL_SECONDS=5
AGENT_VERSION=1.1.9
AGENT_VERSION=1.2.0
@@ -19,4 +19,4 @@ FB_PASSWORD=
FISCAL_MODE=DRY_RUN
FIREBIRD_WRITES=0
POLL_INTERVAL_SECONDS=5
AGENT_VERSION=1.1.9
AGENT_VERSION=1.2.0
@@ -19,4 +19,4 @@ FB_PASSWORD=
FISCAL_MODE=DRY_RUN
FIREBIRD_WRITES=0
POLL_INTERVAL_SECONDS=5
AGENT_VERSION=1.1.9
AGENT_VERSION=1.2.0
+1 -1
View File
@@ -1,5 +1,5 @@
# GTI Fiscal Hub — production environment (copy to .env, never commit .env)
GTI_FISCAL_VERSION=1.1.9
GTI_FISCAL_VERSION=1.2.0
# REQUIRED. URL-safe secret (used inside a connection URL): openssl rand -hex 24
POSTGRES_PASSWORD=
+1 -1
View File
@@ -36,7 +36,7 @@ services:
build:
context: ../..
dockerfile: central/Dockerfile
image: gti-fiscal-hub:${GTI_FISCAL_VERSION:-1.1.9}
image: gti-fiscal-hub:${GTI_FISCAL_VERSION:-1.2.0}
restart: unless-stopped
depends_on:
postgres:
+8
View File
@@ -534,3 +534,11 @@ See `docs/PRE_AI_LOCAL_COMPLETION_V3_REPORT.md` for measured values.
Remaining drift is naming, column order, extra DB defaults, redundant indexes.
- `deploy/hub/*.sh` are forced to LF (`.gitattributes`); `backup.sh` + restore
into a scratch database were exercised on production.
## RTC / cClassTrib closure (2026-09-24/25) — see docs/rtc_auto_apply.md, docs/cclasstrib_resolution.md, docs/unified_fiscal_completeness.md
- Unified fiscal coverage (PIS/COFINS applicability + conditional RTC) is one definition/queue/panel; RTC is ON only if the ERP says so (`PARAMETROS.USAR_REFORMA_TRIBUTARIA`, read live).
- cClassTrib resolver (`central/app/cclasstrib_resolver.py`, `rtc-rules-v2`): every rule is AUTO_APPLY_SAFE / VALIDATION_ONLY / SUGGESTION_ONLY / BLOCKED_BY_EVIDENCE. Only **RTC-R3** (LC 214 annex I/VII/VIII/XV, clean item, CRT 3) authorizes a write; **RTC-R4 is suggestion-only**. Simples: SAFE_BLOCKED (no official orientation; Ato Conjunto RFB/CGIBS 4/2026: Simples from 2027-01-01). Catalog states per class: MATCH_SAFE / MATCH_WITH_NON_MATERIAL_DIFFERENCE / MISMATCH / EXPIRED / MISSING_LOCAL / MISSING_OFFICIAL; write only for MATCH_SAFE.
- Write is **centrally authorized per installation** (`installations.rtc_write_mode`: DISABLED default / DRY_RUN / APPROVAL_REQUIRED / ENABLED; migration 0028), delivered only inside the authenticated job (`authorizations`). The Agent has no local switch, only the capability `RTC_CCLASSTRIB_WRITE_V1`; allowlist = `C000025.CCLASSTRIB`.
- Preview/approval (`rtc_candidates`), audit (`rtc_write_audits`, append-only, rich fields) and per-record rollback (value must still equal the written one) live in the Hub; panel page `/agents/{id}/rtc`.
- Blocker persistence is bulk (`upsert_blockers_bulk`); 21k products persist in ~5 s (was 43 s).
- Proofs: `scripts/rtc_copy_write_e2e.py` (copy only; restores from .fbk; 324 tables hashed), `scripts/scale_hub_test.py`, `docs/runs/*`. Real client DBs: 0 writes. Agent 2 stays DISABLED.
+1 -1
View File
@@ -16,7 +16,7 @@ A variável de ambiente `GTI_SSH_PASSWORD`, se existir, tem prioridade. A senha
## Deploy
```
powershell -ExecutionPolicy Bypass -File .\scripts\deploy_hub.ps1
powershell -ExecutionPolicy Bypass -File .\scripts\deploy_hub.ps1 -PublishAgent -Version 1.1.9
powershell -ExecutionPolicy Bypass -File .\scripts\deploy_hub.ps1 -PublishAgent -Version 1.2.0
```
Parâmetros: `-Version` (padrão `release/VERSION`), `-PublishAgent`, `-SkipTests`, `-DryRun`.
+5 -1
View File
@@ -42,7 +42,7 @@ Premissa documentada: **venda ordinária no mercado interno** (default de produt
| RTC-A2 | NCM listado em mais de um anexo | `AMBIGUOUS` |
| RTC-A3 | capítulos 01–14, 27, 30, 49, 71, 87, 88, 89, 90 e posições 9619, 2207, 3826, 8703: regime definido pela natureza/operação do bem | `AMBIGUOUS` |
| RTC-R3 | NCM em **exatamente um** anexo com classe-padrão única (I→`200003`, VII→`200034`, VIII→`200035`, XV→`200014`) e item sem condição | `RESOLVED_WITH_EXPLICIT_RULE` |
| RTC-R4 | NCM em **nenhum** anexo e fora dos capítulos acima ⇒ tributação integral (regra geral, LC 214 art. 4) | `RESOLVED_WITH_EXPLICIT_RULE` → `000001` |
| RTC-R4 | NCM em **nenhum** anexo e fora dos capítulos acima ⇒ tributação integral (regra geral, LC 214 art. 4). **Exclusão residual não é evidência positiva** | `SUGGESTION_ONLY` → `000001` (nunca candidato de escrita) |
| RTC-G2 | classe proposta ausente/diferente no catálogo local, fora de vigência ou inutilizável em NF-e/NFC-e | `INSUFFICIENT_EVIDENCE` |
`RESOLVED_DETERMINISTIC` (regra sem premissa de operação) não é usado: toda resolução carrega a premissa "venda ordinária" e por isso é `RESOLVED_WITH_EXPLICIT_RULE`.
@@ -65,3 +65,7 @@ Estado como está hoje: TESTESOFICIAIS e CONSTRUCAO têm RTC ligada e regime Sim
## Limitações
Simples fica bloqueado até haver orientação oficial (ou decisão explícita de empresa, não implementada); operações especiais não são cobertas pelo default de produto; a extração dos anexos é textual (itens condicionais viram ambíguos de propósito); o catálogo oficial usado é o de 22/06/2026; sem autorização, nada é escrito (ver `docs/rtc_auto_apply.md`).
## Classes de regra (rtc-rules-v2)
`AUTO_APPLY_SAFE` (única hoje: **RTC-R3**, CRT 3, anexo único, item limpo, classe `MATCH_SAFE`) · `VALIDATION_ONLY` (R0 RTC desligada, valida valor existente) · `SUGGESTION_ONLY` (**R4**) · `BLOCKED_BY_EVIDENCE` (R1 Simples/regime desconhecido, R2 NCM inválido, A1–A3 ambíguo, G1/G2 catálogo). Escrita só com evidência POSITIVA explícita: nunca por similaridade, frequência, LLM ou "não achei nada mais". Vigência local vs oficial: início diferente com a classe em vigor nos dois lados é informativo (`MATCH_SAFE`); fim local agendado = `MATCH_WITH_NON_MATERIAL_DIFFERENCE` (não escreve); início futuro ou fim vencido = `MISMATCH`. Fontes: `docs/sources/RTC_OFFICIAL_ACTS_MANIFEST_V2.json`. Escrita/aprovação/rollback: `docs/rtc_auto_apply.md`.
+26 -21
View File
@@ -1,32 +1,37 @@
# Preenchimento automático do cClassTrib (RTC auto-apply)
# cClassTrib — preenchimento controlado centralmente (RTC_CCLASSTRIB_WRITE_V1)
Código: `agent/src/gti_fiscal_agent/rtc_auto_apply.py` · integração `agent/scripts/run_agent_service.py::_rtc_auto_apply` · Hub `central/app/rtc_audit.py`, migração `0027_rtc_auto_apply`, rota `POST /agents/{id}/rtc-auto-apply`.
**Estado de entrega: mecanismo pronto, DESLIGADO em todo lugar. Nenhuma escrita em banco real foi feita ou é possível sem as três chaves abaixo.**
O Agent **nunca** decide sozinho escrever no ERP. Não há chave local (`agent.env`, `FIREBIRD_WRITES`, PowerShell, RDP): a autorização vem **somente** dentro do job autenticado enviado pelo Hub. O Agent expõe apenas a capability restrita `RTC_CCLASSTRIB_WRITE_V1` (heartbeat).
## Escopo de escrita
Uma coluna: `C000025.CCLASSTRIB`. `ALLOWED_RTC_WRITE_FIELDS = ["CCLASSTRIB"]`; o SQL é uma constante (`UPDATE C000025 SET CCLASSTRIB = ? WHERE CODIGO = ? AND (CCLASSTRIB IS NULL OR TRIM(CCLASSTRIB) = '')`). Não há write genérico de produto, nem escrita em NFCE/itens/catálogo RTC.
Só candidatos `RESOLVED_*` (ver `docs/cclasstrib_resolution.md`) com `CCLASSTRIB` vazio e nenhum outro problema RTC. Nunca sobrescreve: valor igual ⇒ `NO_WRITE_REQUIRED`; valor diferente ⇒ blocker `CCLASSTRIB_CONFLICT` (a política de correção precisa de aprovação futura).
## Modos (por instalação, painel: Agent → "Controle e candidatos cClassTrib")
| Modo | Efeito |
|---|---|
| `DISABLED` (padrão) | só informa; nada de preview, nada de escrita |
| `DRY_RUN` | calcula e envia o preview dos candidatos; zero escrita |
| `APPROVAL_REQUIRED` | escreve só os produtos aprovados por um humano no painel (lote de até 500 por job) |
| `ENABLED` | escreve os candidatos determinísticos (até `RTC_AUTO_APPLY_MAX_PER_JOB`, padrão 500, por job); exige digitar `PREENCHER` |
## Três chaves (todas necessárias; padrão = fechadas)
1. `FIREBIRD_WRITES` efetivo no Agent (`FISCAL_MODE=AUTO_APPLY` + caminho/esquema autorizados — as travas já existentes);
2. `RTC_AUTO_APPLY_ENABLED=1` no `agent.env` (chave própria; `RTC_AUTO_APPLY_MAX_PER_JOB`, padrão 500, limita o raio de ação);
3. autorização do **Hub** por instalação (`installations.rtc_auto_apply_authorized`, padrão `false`), entregue com cada job (`authorizations.rtc_auto_apply`) e alterada só pelo painel: botão "Autorizar" exige CSRF e digitar `PREENCHER`; toda mudança gera `audit_events` (`RTC_AUTO_APPLY_AUTHORIZE/REVOKE`).
Sem as três: modo `REPORT_ONLY` (conta candidatos, abre nenhuma conexão de escrita). O Agent reporta a capability `rtc_auto_apply` no heartbeat.
Texto do painel: "Somente cClassTrib com regra fiscal determinística e catálogo validado pode ser preenchido automaticamente."
Mudança de modo = evento de auditoria administrativa (`RTC_WRITE_MODE`). Migration `0028_rtc_write_modes` (padrão `DISABLED`; instalações que já tinham a autorização 0027 viram `ENABLED`).
## Proteção contra decisão velha (por produto, uma transação curta)
Imediatamente antes do UPDATE o Agent **relê tudo do banco**: parâmetro `USAR_REFORMA_TRIBUTARIA` (todas as linhas; divergência ⇒ aborta), integridade do catálogo, hash do catálogo (deve ser o da decisão), a linha do produto (única; NCM igual ao da decisão; `CCLASSTRIB` ainda vazio) e **re-deriva** a resolução com dados novos (mesma classe e mesma regra). Qualquer diferença ⇒ `ABORTED` daquele produto (`RTC_NO_LONGER_ENABLED`, `RTC_CATALOG_INVALID`, `LOCAL_CATALOG_CHANGED_SINCE_DECISION`, `PRODUCT_CHANGED_NCM`, `VALUE_ALREADY_SET_NEVER_OVERWRITTEN`, `NO_WRITE_REQUIRED`, `DECISION_CHANGED`, `PRODUCT_ROW_NOT_UNIQUE`, `UPDATE_ROWCOUNT_NOT_ONE`). `rowcount` deve ser 1; releitura confirma o valor; a FK física `C000025.CCLASSTRIB → RTC_CCLASSTRIB` é a segunda barreira. Falha de um produto não interrompe os demais.
Idempotência: com o valor correto gravado o produto deixa de ser candidato (`valid_existing`); segunda execução = 0 escritas.
## O que pode ser escrito
Somente `C000025.CCLASSTRIB` (allowlist `ALLOWED_RTC_WRITE_FIELDS`), somente quando **todos** valem: RTC ligada no ERP; regra `AUTO_APPLY_SAFE` (hoje só **RTC-R3**: NCM em exatamente um anexo I/VII/VIII/XV da LC 214, item limpo, CRT 3, uma única classe); classe `MATCH_SAFE` no catálogo local vs oficial; CST válido; empresa relida (`C000004.CRT` = único valor esperado e = 3); RTC/parâmetro/catálogo relidos; produto inalterado (NCM + descrição + valor, fingerprint); valor atual vazio. **R4 (exclusão residual → 000001) é só sugestão e nunca vira candidato.** Nunca sobrescreve: valor igual → `NO_WRITE_REQUIRED`; diferente → `CCLASSTRIB_CONFLICT` (bloqueio). Qualquer gate falho → zero escrita para aquele produto.
## Auditoria e rollback lógico
Cada tentativa vira uma linha em `rtc_write_audits` (append-only): `tenant/installation/agent` (do contexto autenticado, nunca do payload), `client`, `job_id`, `product_code`, `field`, `outcome` (`APPLIED|ABORTED|FAILED`), `reason`, **`old_value`** (sempre nulo hoje: só se preenche campo vazio), `new_value`, `rule_id`, `evidence` (regra, entradas, item do anexo), `official_source_version` (data + sha256 da tabela cClassTrib e sha256 da LC 214), `local_catalog_hash`, `written_at`. Um resumo vai como evento `RTC_AUTO_APPLY` (severidade WARNING quando houve escrita). Reverter = `UPDATE C000025 SET CCLASSTRIB = NULL` para os `product_code` do `job_id` (não há botão de rollback nesta versão).
Sequência por produto (uma transação curta): reler tudo → recalcular a decisão → conferir fingerprint → `UPDATE C000025 SET CCLASSTRIB = ? WHERE CODIGO = ? AND (CCLASSTRIB IS NULL OR TRIM(CCLASSTRIB)='')` → rowcount = 1 → reler → commit → auditoria.
## Painel
Mesmo painel, mesma fila. No detalhe do Agent, com RTC ativa: "Reforma Tributária ativa — cClassTrib pendente em N produtos", "Produtos RTC: total · válidos · prontos para preenchimento · ambíguos/bloqueados", estado do preenchimento automático ("Desativado / Autorizado no Hub, aguardando o Agent (…) / Ativo") e o formulário de autorização. RTC desligada não mostra nada disto.
## Preview e aprovação
O Agent envia os candidatos (até 5.000 por job; `preview_truncated` sinaliza excesso) → tabela `rtc_candidates` (PENDING/APPROVED/REJECTED/APPLIED/STALE). O painel pagina no servidor (50/página), filtra por status, aprova/rejeita selecionados ou todos os pendentes e dispara "Aplicar aprovados agora". Uma proposta que muda invalida a aprovação anterior; produto que deixa de ser elegível numa avaliação completa vira `STALE`.
## Auditoria (append-only, `rtc_write_audits`)
cliente, agent, `installation_uuid`, produto, snapshot da descrição, campo, valor antigo/novo, regra + versão (`rtc-rules-v2`), evidência, hashes/versões das fontes oficiais, hash e estado (`MATCH_SAFE`) do catálogo local, status RTC, CRT da empresa, timestamp, job, origem da autorização (`HUB_MODE:<modo>`), resultado, ação (`APPLY`/`ROLLBACK`). Identidade vem do contexto autenticado, nunca do payload. Sem segredos.
## Rollback por registro
Painel → "Reverter" (digitar `REVERTER`) marca o registro `REQUESTED` e cria um job; o Hub entrega `{audit_id, product_code, written_value}` no job. O Agent só restaura (`SET CCLASSTRIB = NULL`) se o valor atual **ainda for igual ao gravado**; senão `ROLLBACK_CONFLICT_VALUE_CHANGED` (nada muda). Um único statement fixo, sem SQL livre. O registro original vira `DONE`/`BLOCKED`; o produto volta a ser candidato.
## Validação
- Testes: `agent/tests/test_rtc_auto_apply.py` (gates, candidatos, escrita restrita, idempotência, RTC desligada entre decisão e escrita, catálogo alterado/inválido, NCM alterado, valor existente, duplicata, limite por job), `central/tests/test_zero_touch_hub.py`, `central/tests/test_control_panel_pages.py`, `tests/test_rtc_copy_write_guard.py`.
- **E2E de escrita em CÓPIA descartável** (`scripts/rtc_copy_write_e2e.py`; cópia `scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB` via gbak; o script recusa qualquer outro caminho): empresa da cópia trocada para regime normal (CRT 3) como experimento; 280 candidatos → 280 escritas verificadas (`000001`×203, `200034`×75, `200014`×2), somente a coluna `CCLASSTRIB` mudou (diff da tabela inteira), 0 órfãos de FK/CST, catálogo RTC local intacto (145 linhas antes e depois), cobertura da cópia 0% → 53,55%; decisão velha (RTC desligada na cópia entre decisão e escrita) → 280 abortos, 0 escritas; segunda execução → 0 candidatos, 0 escritas. Resultado em `docs/runs/rtc_copy_write_e2e.json`.
- Testes: `agent/tests/test_rtc_auto_apply.py`, `central/tests/test_zero_touch_hub.py`, `test_control_panel_pages.py`, `test_rtc_security.py`, `test_cclasstrib_resolver.py`.
- **E2E em CÓPIA** (`scripts/rtc_copy_write_e2e.py`, `docs/runs/rtc_copy_write_e2e.json`): restaura a cópia do `.fbk`; DISABLED/DRY_RUN/decisão obsoleta = 0 escrita; aprovação de 5 → exatamente 5; ENABLED → 77 (total); reler; FK/CST órfãos 0; segunda execução 0; rollback de 3 + 1 conflito recusado; reaplicação de 3; idempotência 0; hash de 324 tabelas: `NON_TARGET_COLUMNS_CHANGED=0`, `CATALOG_CHANGED=0`, `PARAMETERS_CHANGED=0`, `DOCUMENT_TABLES_CHANGED=0`, nenhuma outra tabela alterada.
- Escala (`scripts/scale_hub_test.py`, 21.000 produtos, payload 7,96 MB): persistência 43 s → 5,5 s (upsert em lote; regime estável 64 statements); painel < 0,6 s.
- Bancos reais: 0 escritas, 0 tentativas.
## Limitações
Simples continua bloqueado (sem orientação oficial); `CCLASSTRIB_CONFLICT` só reporta; não há rollback automático; o texto dos anexos é interpretado de forma conservadora (condição ⇒ ambíguo). Para o piloto real: escolher uma instalação em regime normal com RTC ligada, ligar as três chaves e revisar a amostra de auditoria antes.
Simples continua bloqueado (sem orientação oficial: NT 2025.002 v1.51; Ato Conjunto RFB/CGIBS 4/2026 fixa a obrigação de Simples em 2027-01-01). Regras além de R3 exigem evidência positiva nova. O Agent 2 permanece em `DISABLED`.
+226 -184
View File
@@ -1,12 +1,14 @@
{
"database": "scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB (gbak restore of TESTESOFICIAIS)",
"database": "scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB (restored from its .fbk at start)",
"real_database_writes": 0,
"real_database_write_attempts": 0,
"mechanism": "job authorizations only (no env switches)",
"copy_original_crt": [
"1"
],
"copy_crt_for_experiment": "3",
"copy_rtc_param": 1,
"copy_crt_for_experiment": "3",
"tables_hashed": 324,
"coverage_before": {
"total": 521,
"complete": 0,
@@ -14,14 +16,61 @@
"unclassified": 0,
"coverage_percent": 0.0
},
"disabled_run": {
"write_attempts": 0,
"preview": 0,
"ready_total": 77
},
"dry_run": {
"write_attempts": 0,
"preview": 77,
"ready_total": 77,
"rules": [
"RTC-R3"
],
"sample": [
{
"product_code": "000024",
"description": "COXINHA ( 6 UNID )",
"ncm": "19022000",
"current": "",
"proposed": "200034",
"cst": "200",
"rule_id": "RTC-R3",
"basis": "RTC-R3:ANNEX_TEXT_MATCH"
},
{
"product_code": "000027",
"description": "RISOLE DE CAMARAO",
"ncm": "19022000",
"current": "",
"proposed": "200034",
"cst": "200",
"rule_id": "RTC-R3",
"basis": "RTC-R3:ANNEX_TEXT_MATCH"
},
{
"product_code": "000028",
"description": "SALGADOS (MIXTO 10 UND)",
"ncm": "19022000",
"current": "",
"proposed": "200034",
"cst": "200",
"rule_id": "RTC-R3",
"basis": "RTC-R3:ANNEX_TEXT_MATCH"
}
]
},
"resolution_before": {
"total": 521,
"valid_existing": 0,
"ready_to_apply": 280,
"ready_to_apply": 77,
"suggestion_only": 203,
"ambiguous": 215,
"insufficient": 26,
"conflicts": 0,
"official_mismatch": 0,
"catalog_incompatible": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
@@ -35,196 +84,189 @@
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH": 131,
"LOCAL_ONLY": 0,
"OFFICIAL_ONLY": 19,
"MATCH_SAFE": 131,
"MATCH_WITH_NON_MATERIAL_DIFFERENCE": 0,
"MISMATCH": 11,
"EXPIRED": 3
"EXPIRED": 3,
"MISSING_LOCAL": 19,
"MISSING_OFFICIAL": 0
},
"expired_present_locally": 3
}
},
"stale_decision_run": {
"write_attempts": 280,
"write_successes": 0,
"aborted_reasons": {
"RTC_NO_LONGER_ENABLED": 280
}
},
"first_run": {
"mode": "AUTO_APPLY",
"ready_total": 280,
"write_attempts": 280,
"write_successes": 280,
"aborted_reasons": {},
"audit_sample": [
{
"outcome": "APPLIED",
"product_code": "000023",
"field": "CCLASSTRIB",
"old_value": null,
"new_value": "000001",
"cst_ibscbs": "000",
"rule_id": "RTC-R4",
"confidence_type": "EXCLUSION_OF_ALL_SPECIAL_REGIMES",
"evidence": {
"inputs_used": {
"ncm": "16025000",
"crt": "3",
"rtc_enabled": true
},
"evidence": [
{
"annex_hits": 0,
"chapter": "16"
}
],
"reason": "NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER"
},
"official_source_version": {
"cclasstrib_table": "2026-06-22",
"cclasstrib_sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
"lc214_sha256": "5c7d59b3c49e5b7c066c2ad28bedf32791193a676ec1a163bec14bea5fa2f680"
},
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
"timestamp": "2026-09-25T00:07:47+00:00",
"job_id": 424242,
"rowcount": 1,
"verified_after": true
},
{
"outcome": "APPLIED",
"product_code": "000024",
"field": "CCLASSTRIB",
"old_value": null,
"new_value": "200034",
"cst_ibscbs": "200",
"rule_id": "RTC-R3",
"confidence_type": "ANNEX_TEXT_MATCH",
"evidence": {
"inputs_used": {
"ncm": "19022000",
"crt": "3",
"rtc_enabled": true
},
"evidence": [
{
"annex": "VII",
"item": 9,
"prefix": "19022000",
"clean": true,
"revoked": false,
"text": "Massas alimentícias dos códigos 1902.20.00 e 1902.30.00 da NCM/SH"
}
],
"reason": "ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS"
},
"official_source_version": {
"cclasstrib_table": "2026-06-22",
"cclasstrib_sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
"lc214_sha256": "5c7d59b3c49e5b7c066c2ad28bedf32791193a676ec1a163bec14bea5fa2f680"
},
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
"timestamp": "2026-09-25T00:07:47+00:00",
"job_id": 424242,
"rowcount": 1,
"verified_after": true
},
{
"outcome": "APPLIED",
"product_code": "000027",
"field": "CCLASSTRIB",
"old_value": null,
"new_value": "200034",
"cst_ibscbs": "200",
"rule_id": "RTC-R3",
"confidence_type": "ANNEX_TEXT_MATCH",
"evidence": {
"inputs_used": {
"ncm": "19022000",
"crt": "3",
"rtc_enabled": true
},
"evidence": [
{
"annex": "VII",
"item": 9,
"prefix": "19022000",
"clean": true,
"revoked": false,
"text": "Massas alimentícias dos códigos 1902.20.00 e 1902.30.00 da NCM/SH"
}
],
"reason": "ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS"
},
"official_source_version": {
"cclasstrib_table": "2026-06-22",
"cclasstrib_sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
"lc214_sha256": "5c7d59b3c49e5b7c066c2ad28bedf32791193a676ec1a163bec14bea5fa2f680"
},
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
"timestamp": "2026-09-25T00:07:47+00:00",
"job_id": 424242,
"rowcount": 1,
"verified_after": true
}
]
},
"rows_changed": 280,
"columns_changed": [
"CCLASSTRIB"
],
"fk_orphans": 0,
"cst_orphans": 0,
"written_by_class": {
"000001": 203,
"200014": 2,
"200034": 75
},
"catalog_rows_unchanged": 145,
"coverage_after": {
"total": 521,
"complete": 279,
"blocked": 242,
"unclassified": 0,
"coverage_percent": 53.55
},
"resolution_after": {
"total": 521,
"valid_existing": 280,
"ready_to_apply": 0,
"ambiguous": 215,
"insufficient": 26,
"conflicts": 0,
"official_mismatch": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
"RTC-A1": 41,
"RTC-A3": 10,
"RTC-R2": 26,
"RTC-R4": 203,
"RTC-R3": 77,
"RTC-A2": 164
},
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH": 131,
"LOCAL_ONLY": 0,
"OFFICIAL_ONLY": 19,
"MISMATCH": 11,
"EXPIRED": 3
},
"expired_present_locally": 3
}
},
"second_run": {
"ready_total": 0,
"write_attempts": 0,
"write_successes": 0
},
"approval_run": {
"without_approval_attempts": 0,
"approved": 5,
"write_successes": 5,
"only_approved": true
},
"enabled_jobs": [
{
"write_attempts": 72,
"write_successes": 72,
"aborted": {}
},
{
"write_attempts": 0,
"write_successes": 0,
"aborted": {}
}
],
"total_applied": 77,
"audit_sample": [
{
"outcome": "APPLIED",
"product_code": "000024",
"field": "CCLASSTRIB",
"old_value": null,
"new_value": "200034",
"cst_ibscbs": "200",
"rule_id": "RTC-R3",
"confidence_type": "ANNEX_TEXT_MATCH",
"evidence": {
"inputs_used": {
"ncm": "19022000",
"crt": "3",
"rtc_enabled": true
},
"evidence": [
{
"annex": "VII",
"item": 9,
"prefix": "19022000",
"clean": true,
"revoked": false,
"text": "Massas alimentícias dos códigos 1902.20.00 e 1902.30.00 da NCM/SH"
}
],
"reason": "ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS"
},
"official_source_version": {
"cclasstrib_table": "2026-06-22",
"cclasstrib_sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
"lc214_sha256": "5c7d59b3c49e5b7c066c2ad28bedf32791193a676ec1a163bec14bea5fa2f680"
},
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
"timestamp": "2026-09-25T01:06:30+00:00",
"job_id": 2,
"rowcount": 1,
"verified_after": true,
"action": "APPLY",
"rule_version": "rtc-rules-v2",
"company_crt": "3",
"description_snapshot": "COXINHA ( 6 UNID )",
"rtc_status": "ENABLED",
"local_catalog_state": "MATCH_SAFE",
"authorization_origin": "HUB_MODE:APPROVAL_REQUIRED"
},
{
"outcome": "APPLIED",
"product_code": "000027",
"field": "CCLASSTRIB",
"old_value": null,
"new_value": "200034",
"cst_ibscbs": "200",
"rule_id": "RTC-R3",
"confidence_type": "ANNEX_TEXT_MATCH",
"evidence": {
"inputs_used": {
"ncm": "19022000",
"crt": "3",
"rtc_enabled": true
},
"evidence": [
{
"annex": "VII",
"item": 9,
"prefix": "19022000",
"clean": true,
"revoked": false,
"text": "Massas alimentícias dos códigos 1902.20.00 e 1902.30.00 da NCM/SH"
}
],
"reason": "ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS"
},
"official_source_version": {
"cclasstrib_table": "2026-06-22",
"cclasstrib_sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
"lc214_sha256": "5c7d59b3c49e5b7c066c2ad28bedf32791193a676ec1a163bec14bea5fa2f680"
},
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
"timestamp": "2026-09-25T01:06:30+00:00",
"job_id": 2,
"rowcount": 1,
"verified_after": true,
"action": "APPLY",
"rule_version": "rtc-rules-v2",
"company_crt": "3",
"description_snapshot": "RISOLE DE CAMARAO",
"rtc_status": "ENABLED",
"local_catalog_state": "MATCH_SAFE",
"authorization_origin": "HUB_MODE:APPROVAL_REQUIRED"
}
],
"audit_fields_complete": true,
"rows_changed": 77,
"fk_orphans": 0,
"cst_orphans": 0,
"coverage_after": {
"total": 521,
"complete": 77,
"blocked": 444,
"unclassified": 0,
"coverage_percent": 14.78
},
"second_run": {
"write_attempts": 0,
"write_successes": 0,
"ready_total": 0
},
"rollback": {
"rolled_back": 3,
"conflict": [
"ABORTED",
"ROLLBACK_CONFLICT_VALUE_CHANGED"
],
"records": [
"ROLLED_BACK",
"ROLLED_BACK",
"ROLLED_BACK",
"ABORTED"
],
"reread": {
"000024": "",
"000027": "",
"000028": "",
"000032": "200035"
}
},
"reapply": {
"write_successes": 3,
"codes": [
"000024",
"000027",
"000028"
],
"expected": [
"000024",
"000027",
"000028"
]
},
"idempotency_after_reapply": {
"write_attempts": 0,
"write_successes": 0
},
"NON_TARGET_COLUMNS_CHANGED": 0,
"CATALOG_CHANGED": 0,
"PARAMETERS_CHANGED": 0,
"DOCUMENT_TABLES_CHANGED": 0,
"OTHER_TABLES_CHANGED": 0,
"changed_tables": [],
"IDEMPOTENT": true,
"ONLY_CCLASSTRIB_CHANGED": true,
"PASS": true
}
+63 -63
View File
@@ -13,89 +13,89 @@ TESTESOFICIAIS,000018,02071100,,,AMBIGUOUS,RTC-A1,ANNEX_I_NEEDS_OPERATION_OR_CON
TESTESOFICIAIS,000019,07133321,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
TESTESOFICIAIS,000021,07089000,,,AMBIGUOUS,RTC-A1,ANNEX_XV_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
TESTESOFICIAIS,000022,07089000,,,AMBIGUOUS,RTC-A1,ANNEX_XV_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
TESTESOFICIAIS,000023,16025000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
TESTESOFICIAIS,000024,19022000,,200034,RESOLVED_WITH_EXPLICIT_RULE,RTC-R3,ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS,ANNEX_TEXT_MATCH,"WHATIF_NORMAL_ON (regime normal, RTC on)"
TESTESOFICIAIS,000025,04061010,,,AMBIGUOUS,RTC-A1,ANNEX_I_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
TESTESOFICIAIS,000026,03055100,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
TESTESOFICIAIS,000027,19022000,,200034,RESOLVED_WITH_EXPLICIT_RULE,RTC-R3,ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS,ANNEX_TEXT_MATCH,"WHATIF_NORMAL_ON (regime normal, RTC on)"
TESTESOFICIAIS,000028,19022000,,200034,RESOLVED_WITH_EXPLICIT_RULE,RTC-R3,ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS,ANNEX_TEXT_MATCH,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000001,72142000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000002,72142000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000003,72142000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000004,72142000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000005,72142000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000006,72142000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000007,72142000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000008,25232910,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000009,25232910,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000010,25232910,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000011,68118200,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000012,72142000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000013,68101100,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000014,68101100,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000015,40139000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000016,40139000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
TESTESOFICIAIS,000029,04089100,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000017,30045020,,,AMBIGUOUS,RTC-A1,ANNEX_IX_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000018,56075090,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000019,56075090,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000020,56075090,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000075,38089199,,,AMBIGUOUS,RTC-A1,ANNEX_IX_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000076,38089119,,,AMBIGUOUS,RTC-A1,ANNEX_IX_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000077,38089119,,,AMBIGUOUS,RTC-A1,ANNEX_IX_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000078,38089996,,,AMBIGUOUS,RTC-A1,ANNEX_IX_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000086,90303390,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000092,87141000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000093,87141000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000094,87141000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000095,87141000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000096,87141000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000097,87141000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000108,87141000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000109,87141000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000113,87141000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000114,87141000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000115,87141000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000116,87141000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000117,87141000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
CONSTRUCAO,000118,87141000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000002,19022000,,200034,RESOLVED_WITH_EXPLICIT_RULE,RTC-R3,ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS,ANNEX_TEXT_MATCH,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000003,61046300,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000006,61046300,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000008,61046200,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000011,61091000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000013,61051000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000014,61051000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000016,61051000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000017,62111100,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000018,62111100,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000019,62034200,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000020,42021220,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000022,42021900,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000023,65040090,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000024,61091000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000025,61091000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000026,61061000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000027,62064000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000028,62046200,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000029,62034300,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000001,21050090,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000729,90041000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000730,90041000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000731,90031990,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000732,90041000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000733,90041000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000735,90041000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,000736,90041000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,001248,71171900,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,001259,71171900,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,001260,71171900,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,001261,71171900,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,001262,71132000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,001263,71171900,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,001264,71132000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,001265,71132000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,001266,71132000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,001267,71132000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,001270,71132000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
MMMODAS,001272,71132000,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000004,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000005,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000006,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000007,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000008,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000009,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000010,18063210,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000011,18063210,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000012,18063210,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000013,18063210,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000014,18063210,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000015,18063210,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000016,18063210,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000017,21069090,,,AMBIGUOUS,RTC-A2,LISTED_IN_MORE_THAN_ONE_ANNEX,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000018,18063210,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000019,18063210,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000020,21069090,,,AMBIGUOUS,RTC-A2,LISTED_IN_MORE_THAN_ONE_ANNEX,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000021,18063210,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000022,18063210,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000008,33059000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000009,21039021,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000026,21069090,,,AMBIGUOUS,RTC-A2,LISTED_IN_MORE_THAN_ONE_ANNEX,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000027,21069090,,,AMBIGUOUS,RTC-A2,LISTED_IN_MORE_THAN_ONE_ANNEX,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000032,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000033,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000034,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000035,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000036,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000037,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000038,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000039,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000040,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
RINCAO,000041,17041000,,,AMBIGUOUS,RTC-A1,ANNEX_X_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000014,19059010,,200034,RESOLVED_WITH_EXPLICIT_RULE,RTC-R3,ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS,ANNEX_TEXT_MATCH,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000015,95051000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000016,21032010,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000017,95030099,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000018,38089999,,,AMBIGUOUS,RTC-A1,ANNEX_IX_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000019,38089999,,,AMBIGUOUS,RTC-A1,ANNEX_IX_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000020,38089999,,,AMBIGUOUS,RTC-A1,ANNEX_IX_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000021,95051000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000022,22021000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000023,19053100,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000024,82041100,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000025,04089900,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000026,15171000,,200003,RESOLVED_WITH_EXPLICIT_RULE,RTC-R3,ANNEX_I_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS,ANNEX_TEXT_MATCH,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000027,82054000,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000028,04090000,,200034,RESOLVED_WITH_EXPLICIT_RULE,RTC-R3,ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS,ANNEX_TEXT_MATCH,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000029,19053100,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000030,04022120,,,AMBIGUOUS,RTC-A1,ANNEX_I_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000031,19053100,,000001,RESOLVED_WITH_EXPLICIT_RULE,RTC-R4,NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER,EXCLUSION_OF_ALL_SPECIAL_REGIMES,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000032,19019090,,200003,RESOLVED_WITH_EXPLICIT_RULE,RTC-R3,ANNEX_I_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS,ANNEX_TEXT_MATCH,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000034,20081100,,,AMBIGUOUS,RTC-A1,ANNEX_VII_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000035,15121911,,,AMBIGUOUS,RTC-A1,ANNEX_VII_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000046,20089900,,200034,RESOLVED_WITH_EXPLICIT_RULE,RTC-R3,ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS,ANNEX_TEXT_MATCH,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000047,23023010,,,AMBIGUOUS,RTC-A1,ANNEX_IX_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000050,20089900,,200034,RESOLVED_WITH_EXPLICIT_RULE,RTC-R3,ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS,ANNEX_TEXT_MATCH,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000052,20089900,,200034,RESOLVED_WITH_EXPLICIT_RULE,RTC-R3,ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS,ANNEX_TEXT_MATCH,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000054,20057000,,,AMBIGUOUS,RTC-A1,ANNEX_VII_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000056,20081100,,,AMBIGUOUS,RTC-A1,ANNEX_VII_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000058,15121911,,,AMBIGUOUS,RTC-A1,ANNEX_VII_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000064,02101200,,,AMBIGUOUS,RTC-A2,LISTED_IN_MORE_THAN_ONE_ANNEX,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
UNIAO,000077,20089900,,200034,RESOLVED_WITH_EXPLICIT_RULE,RTC-R3,ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS,ANNEX_TEXT_MATCH,"WHATIF_NORMAL_ON (regime normal, RTC on)"
1 database product ncm current_cclasstrib proposed_cclasstrib status rule_id reason confidence_type scenario
13 TESTESOFICIAIS 000019 07133321 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
14 TESTESOFICIAIS 000021 07089000 AMBIGUOUS RTC-A1 ANNEX_XV_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
15 TESTESOFICIAIS 000022 07089000 AMBIGUOUS RTC-A1 ANNEX_XV_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
TESTESOFICIAIS 000023 16025000 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
16 TESTESOFICIAIS 000024 19022000 200034 RESOLVED_WITH_EXPLICIT_RULE RTC-R3 ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS ANNEX_TEXT_MATCH WHATIF_NORMAL_ON (regime normal, RTC on)
17 TESTESOFICIAIS 000025 04061010 AMBIGUOUS RTC-A1 ANNEX_I_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
18 TESTESOFICIAIS 000026 03055100 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
19 TESTESOFICIAIS 000027 19022000 200034 RESOLVED_WITH_EXPLICIT_RULE RTC-R3 ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS ANNEX_TEXT_MATCH WHATIF_NORMAL_ON (regime normal, RTC on)
20 TESTESOFICIAIS 000028 19022000 200034 RESOLVED_WITH_EXPLICIT_RULE RTC-R3 ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS ANNEX_TEXT_MATCH WHATIF_NORMAL_ON (regime normal, RTC on)
21 CONSTRUCAO TESTESOFICIAIS 000001 000029 72142000 04089100 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000002 72142000 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000003 72142000 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000004 72142000 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000005 72142000 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000006 72142000 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000007 72142000 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000008 25232910 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000009 25232910 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000010 25232910 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000011 68118200 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000012 72142000 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000013 68101100 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000014 68101100 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000015 40139000 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
CONSTRUCAO 000016 40139000 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
22 CONSTRUCAO 000017 30045020 AMBIGUOUS RTC-A1 ANNEX_IX_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
23 CONSTRUCAO 000018 000075 56075090 38089199 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A1 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER ANNEX_IX_NEEDS_OPERATION_OR_CONDITION EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
24 CONSTRUCAO 000019 000076 56075090 38089119 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A1 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER ANNEX_IX_NEEDS_OPERATION_OR_CONDITION EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
25 CONSTRUCAO 000020 000077 56075090 38089119 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A1 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER ANNEX_IX_NEEDS_OPERATION_OR_CONDITION EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
26 CONSTRUCAO 000078 38089996 AMBIGUOUS RTC-A1 ANNEX_IX_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
27 CONSTRUCAO 000086 90303390 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
28 CONSTRUCAO 000092 87141000 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
29 CONSTRUCAO 000093 87141000 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
30 CONSTRUCAO 000094 87141000 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
31 CONSTRUCAO 000095 87141000 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
32 CONSTRUCAO 000096 87141000 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
33 CONSTRUCAO 000097 87141000 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
34 CONSTRUCAO 000108 87141000 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
35 CONSTRUCAO 000109 87141000 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
36 CONSTRUCAO 000113 87141000 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
37 CONSTRUCAO 000114 87141000 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
38 CONSTRUCAO 000115 87141000 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
39 CONSTRUCAO 000116 87141000 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
40 CONSTRUCAO 000117 87141000 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
41 CONSTRUCAO 000118 87141000 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
42 MMMODAS 000002 19022000 200034 RESOLVED_WITH_EXPLICIT_RULE RTC-R3 ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS ANNEX_TEXT_MATCH WHATIF_NORMAL_ON (regime normal, RTC on)
43 MMMODAS 000003 000729 61046300 90041000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
44 MMMODAS 000006 000730 61046300 90041000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
45 MMMODAS 000008 000731 61046200 90031990 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
46 MMMODAS 000011 000732 61091000 90041000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
47 MMMODAS 000013 000733 61051000 90041000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
48 MMMODAS 000014 000735 61051000 90041000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
49 MMMODAS 000016 000736 61051000 90041000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
50 MMMODAS 000017 001248 62111100 71171900 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
51 MMMODAS 000018 001259 62111100 71171900 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
52 MMMODAS 000019 001260 62034200 71171900 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
53 MMMODAS 000020 001261 42021220 71171900 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
54 MMMODAS 000022 001262 42021900 71132000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
55 MMMODAS 000023 001263 65040090 71171900 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
56 MMMODAS 000024 001264 61091000 71132000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
57 MMMODAS 000025 001265 61091000 71132000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
58 MMMODAS 000026 001266 61061000 71132000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
59 MMMODAS 000027 001267 62064000 71132000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
60 MMMODAS 000028 001270 62046200 71132000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
61 MMMODAS 000029 001272 62034300 71132000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
RINCAO 000001 21050090 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
62 RINCAO 000004 17041000 AMBIGUOUS RTC-A1 ANNEX_X_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
63 RINCAO 000005 17041000 AMBIGUOUS RTC-A1 ANNEX_X_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
64 RINCAO 000006 17041000 AMBIGUOUS RTC-A1 ANNEX_X_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
65 RINCAO 000007 17041000 AMBIGUOUS RTC-A1 ANNEX_X_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
66 RINCAO 000008 17041000 AMBIGUOUS RTC-A1 ANNEX_X_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
67 RINCAO 000009 17041000 AMBIGUOUS RTC-A1 ANNEX_X_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
RINCAO 000010 18063210 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
RINCAO 000011 18063210 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
RINCAO 000012 18063210 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
RINCAO 000013 18063210 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
RINCAO 000014 18063210 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
RINCAO 000015 18063210 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
RINCAO 000016 18063210 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
68 RINCAO 000017 21069090 AMBIGUOUS RTC-A2 LISTED_IN_MORE_THAN_ONE_ANNEX NONE WHATIF_NORMAL_ON (regime normal, RTC on)
RINCAO 000018 18063210 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
RINCAO 000019 18063210 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
69 RINCAO 000020 21069090 AMBIGUOUS RTC-A2 LISTED_IN_MORE_THAN_ONE_ANNEX NONE WHATIF_NORMAL_ON (regime normal, RTC on)
70 RINCAO 000021 000026 18063210 21069090 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A2 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER LISTED_IN_MORE_THAN_ONE_ANNEX EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
71 RINCAO 000022 000027 18063210 21069090 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A2 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER LISTED_IN_MORE_THAN_ONE_ANNEX EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
72 UNIAO RINCAO 000008 000032 33059000 17041000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A1 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER ANNEX_X_NEEDS_OPERATION_OR_CONDITION EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
73 UNIAO RINCAO 000009 000033 21039021 17041000 000001 RESOLVED_WITH_EXPLICIT_RULE AMBIGUOUS RTC-R4 RTC-A1 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER ANNEX_X_NEEDS_OPERATION_OR_CONDITION EXCLUSION_OF_ALL_SPECIAL_REGIMES NONE WHATIF_NORMAL_ON (regime normal, RTC on)
74 RINCAO 000034 17041000 AMBIGUOUS RTC-A1 ANNEX_X_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
75 RINCAO 000035 17041000 AMBIGUOUS RTC-A1 ANNEX_X_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
76 RINCAO 000036 17041000 AMBIGUOUS RTC-A1 ANNEX_X_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
77 RINCAO 000037 17041000 AMBIGUOUS RTC-A1 ANNEX_X_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
78 RINCAO 000038 17041000 AMBIGUOUS RTC-A1 ANNEX_X_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
79 RINCAO 000039 17041000 AMBIGUOUS RTC-A1 ANNEX_X_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
80 RINCAO 000040 17041000 AMBIGUOUS RTC-A1 ANNEX_X_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
81 RINCAO 000041 17041000 AMBIGUOUS RTC-A1 ANNEX_X_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
82 UNIAO 000014 19059010 200034 RESOLVED_WITH_EXPLICIT_RULE RTC-R3 ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS ANNEX_TEXT_MATCH WHATIF_NORMAL_ON (regime normal, RTC on)
UNIAO 000015 95051000 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
UNIAO 000016 21032010 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
UNIAO 000017 95030099 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
83 UNIAO 000018 38089999 AMBIGUOUS RTC-A1 ANNEX_IX_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
84 UNIAO 000019 38089999 AMBIGUOUS RTC-A1 ANNEX_IX_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
85 UNIAO 000020 38089999 AMBIGUOUS RTC-A1 ANNEX_IX_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
UNIAO 000021 95051000 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
UNIAO 000022 22021000 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
UNIAO 000023 19053100 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
UNIAO 000024 82041100 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
86 UNIAO 000025 04089900 AMBIGUOUS RTC-A3 SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD NONE WHATIF_NORMAL_ON (regime normal, RTC on)
87 UNIAO 000026 15171000 200003 RESOLVED_WITH_EXPLICIT_RULE RTC-R3 ANNEX_I_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS ANNEX_TEXT_MATCH WHATIF_NORMAL_ON (regime normal, RTC on)
UNIAO 000027 82054000 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
88 UNIAO 000028 04090000 200034 RESOLVED_WITH_EXPLICIT_RULE RTC-R3 ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS ANNEX_TEXT_MATCH WHATIF_NORMAL_ON (regime normal, RTC on)
UNIAO 000029 19053100 000001 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER EXCLUSION_OF_ALL_SPECIAL_REGIMES WHATIF_NORMAL_ON (regime normal, RTC on)
89 UNIAO 000030 04022120 AMBIGUOUS RTC-A1 ANNEX_I_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
90 UNIAO 000031 000032 19053100 19019090 000001 200003 RESOLVED_WITH_EXPLICIT_RULE RTC-R4 RTC-R3 NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER ANNEX_I_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS EXCLUSION_OF_ALL_SPECIAL_REGIMES ANNEX_TEXT_MATCH WHATIF_NORMAL_ON (regime normal, RTC on)
91 UNIAO 000034 20081100 AMBIGUOUS RTC-A1 ANNEX_VII_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
92 UNIAO 000035 15121911 AMBIGUOUS RTC-A1 ANNEX_VII_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
93 UNIAO 000046 20089900 200034 RESOLVED_WITH_EXPLICIT_RULE RTC-R3 ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS ANNEX_TEXT_MATCH WHATIF_NORMAL_ON (regime normal, RTC on)
94 UNIAO 000047 23023010 AMBIGUOUS RTC-A1 ANNEX_IX_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
95 UNIAO 000050 20089900 200034 RESOLVED_WITH_EXPLICIT_RULE RTC-R3 ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS ANNEX_TEXT_MATCH WHATIF_NORMAL_ON (regime normal, RTC on)
96 UNIAO 000052 20089900 200034 RESOLVED_WITH_EXPLICIT_RULE RTC-R3 ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS ANNEX_TEXT_MATCH WHATIF_NORMAL_ON (regime normal, RTC on)
97 UNIAO 000054 20057000 AMBIGUOUS RTC-A1 ANNEX_VII_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
98 UNIAO 000056 20081100 AMBIGUOUS RTC-A1 ANNEX_VII_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
99 UNIAO 000058 15121911 AMBIGUOUS RTC-A1 ANNEX_VII_NEEDS_OPERATION_OR_CONDITION NONE WHATIF_NORMAL_ON (regime normal, RTC on)
100 UNIAO 000064 02101200 AMBIGUOUS RTC-A2 LISTED_IN_MORE_THAN_ONE_ANNEX NONE WHATIF_NORMAL_ON (regime normal, RTC on)
101 UNIAO 000077 20089900 200034 RESOLVED_WITH_EXPLICIT_RULE RTC-R3 ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS ANNEX_TEXT_MATCH WHATIF_NORMAL_ON (regime normal, RTC on)
+144 -98
View File
@@ -16,10 +16,12 @@
"total": 521,
"valid_existing": 0,
"ready_to_apply": 0,
"suggestion_only": 0,
"ambiguous": 0,
"insufficient": 521,
"conflicts": 0,
"official_mismatch": 0,
"catalog_incompatible": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
@@ -31,11 +33,12 @@
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH": 131,
"LOCAL_ONLY": 0,
"OFFICIAL_ONLY": 19,
"MATCH_SAFE": 131,
"MATCH_WITH_NON_MATERIAL_DIFFERENCE": 0,
"MISMATCH": 11,
"EXPIRED": 3
"EXPIRED": 3,
"MISSING_LOCAL": 19,
"MISSING_OFFICIAL": 0
},
"expired_present_locally": 3
}
@@ -50,7 +53,7 @@
"context": {
"rtc_param": "1",
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
"rtc_observed_at": "2026-09-25T00:11:38+00:00",
"rtc_observed_at": "2026-09-25T01:07:28+00:00",
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
"official_catalog": {
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
@@ -76,10 +79,12 @@
"total": 521,
"valid_existing": 0,
"ready_to_apply": 0,
"suggestion_only": 0,
"ambiguous": 0,
"insufficient": 521,
"conflicts": 0,
"official_mismatch": 0,
"catalog_incompatible": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
@@ -91,11 +96,12 @@
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH": 131,
"LOCAL_ONLY": 0,
"OFFICIAL_ONLY": 19,
"MATCH_SAFE": 131,
"MATCH_WITH_NON_MATERIAL_DIFFERENCE": 0,
"MISMATCH": 11,
"EXPIRED": 3
"EXPIRED": 3,
"MISSING_LOCAL": 19,
"MISSING_OFFICIAL": 0
},
"expired_present_locally": 3
}
@@ -110,7 +116,7 @@
"context": {
"rtc_param": "1",
"rtc_source": "SIMULATION",
"rtc_observed_at": "2026-09-25T00:11:38+00:00",
"rtc_observed_at": "2026-09-25T01:07:28+00:00",
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
"official_catalog": {
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
@@ -135,11 +141,13 @@
"resolution_summary": {
"total": 521,
"valid_existing": 0,
"ready_to_apply": 280,
"ready_to_apply": 77,
"suggestion_only": 203,
"ambiguous": 215,
"insufficient": 26,
"conflicts": 0,
"official_mismatch": 0,
"catalog_incompatible": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
@@ -165,11 +173,12 @@
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH": 131,
"LOCAL_ONLY": 0,
"OFFICIAL_ONLY": 19,
"MATCH_SAFE": 131,
"MATCH_WITH_NON_MATERIAL_DIFFERENCE": 0,
"MISMATCH": 11,
"EXPIRED": 3
"EXPIRED": 3,
"MISSING_LOCAL": 19,
"MISSING_OFFICIAL": 0
},
"expired_present_locally": 3
}
@@ -177,18 +186,20 @@
"by_status": {
"AMBIGUOUS": 215,
"INSUFFICIENT_EVIDENCE": 26,
"RESOLVED_WITH_EXPLICIT_RULE": 280
"SUGGESTION_ONLY": 203,
"RESOLVED_WITH_EXPLICIT_RULE": 77
},
"rtc_blocker_types": {
"CCLASSTRIB_AMBIGUOUS": 215,
"RTC_RULE_EVIDENCE_MISSING": 26,
"CCLASSTRIB_MISSING": 280
"CCLASSTRIB_MISSING_UNRESOLVED": 203,
"CCLASSTRIB_MISSING": 77
},
"AUTO_APPLY_CANDIDATES": 280,
"AUTO_APPLY_CANDIDATES": 77,
"context": {
"rtc_param": "1",
"rtc_source": "SIMULATION",
"rtc_observed_at": "2026-09-25T00:11:38+00:00",
"rtc_observed_at": "2026-09-25T01:07:29+00:00",
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
"official_catalog": {
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
@@ -220,10 +231,12 @@
"total": 3712,
"valid_existing": 0,
"ready_to_apply": 0,
"suggestion_only": 0,
"ambiguous": 0,
"insufficient": 3712,
"conflicts": 0,
"official_mismatch": 0,
"catalog_incompatible": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
@@ -235,11 +248,12 @@
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH": 131,
"LOCAL_ONLY": 0,
"OFFICIAL_ONLY": 19,
"MATCH_SAFE": 131,
"MATCH_WITH_NON_MATERIAL_DIFFERENCE": 0,
"MISMATCH": 11,
"EXPIRED": 3
"EXPIRED": 3,
"MISSING_LOCAL": 19,
"MISSING_OFFICIAL": 0
},
"expired_present_locally": 3
}
@@ -254,7 +268,7 @@
"context": {
"rtc_param": "1",
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
"rtc_observed_at": "2026-09-25T00:11:39+00:00",
"rtc_observed_at": "2026-09-25T01:07:29+00:00",
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
"official_catalog": {
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
@@ -280,10 +294,12 @@
"total": 3712,
"valid_existing": 0,
"ready_to_apply": 0,
"suggestion_only": 0,
"ambiguous": 0,
"insufficient": 3712,
"conflicts": 0,
"official_mismatch": 0,
"catalog_incompatible": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
@@ -295,11 +311,12 @@
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH": 131,
"LOCAL_ONLY": 0,
"OFFICIAL_ONLY": 19,
"MATCH_SAFE": 131,
"MATCH_WITH_NON_MATERIAL_DIFFERENCE": 0,
"MISMATCH": 11,
"EXPIRED": 3
"EXPIRED": 3,
"MISSING_LOCAL": 19,
"MISSING_OFFICIAL": 0
},
"expired_present_locally": 3
}
@@ -314,7 +331,7 @@
"context": {
"rtc_param": "1",
"rtc_source": "SIMULATION",
"rtc_observed_at": "2026-09-25T00:11:39+00:00",
"rtc_observed_at": "2026-09-25T01:07:30+00:00",
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
"official_catalog": {
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
@@ -339,11 +356,13 @@
"resolution_summary": {
"total": 3712,
"valid_existing": 0,
"ready_to_apply": 2725,
"ready_to_apply": 0,
"suggestion_only": 2725,
"ambiguous": 973,
"insufficient": 14,
"conflicts": 0,
"official_mismatch": 0,
"catalog_incompatible": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
@@ -367,30 +386,31 @@
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH": 131,
"LOCAL_ONLY": 0,
"OFFICIAL_ONLY": 19,
"MATCH_SAFE": 131,
"MATCH_WITH_NON_MATERIAL_DIFFERENCE": 0,
"MISMATCH": 11,
"EXPIRED": 3
"EXPIRED": 3,
"MISSING_LOCAL": 19,
"MISSING_OFFICIAL": 0
},
"expired_present_locally": 3
}
},
"by_status": {
"RESOLVED_WITH_EXPLICIT_RULE": 2725,
"SUGGESTION_ONLY": 2725,
"AMBIGUOUS": 973,
"INSUFFICIENT_EVIDENCE": 14
},
"rtc_blocker_types": {
"CCLASSTRIB_MISSING": 2725,
"CCLASSTRIB_MISSING_UNRESOLVED": 2725,
"CCLASSTRIB_AMBIGUOUS": 973,
"RTC_RULE_EVIDENCE_MISSING": 14
},
"AUTO_APPLY_CANDIDATES": 2725,
"AUTO_APPLY_CANDIDATES": 0,
"context": {
"rtc_param": "1",
"rtc_source": "SIMULATION",
"rtc_observed_at": "2026-09-25T00:11:40+00:00",
"rtc_observed_at": "2026-09-25T01:07:30+00:00",
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
"official_catalog": {
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
@@ -427,7 +447,7 @@
"context": {
"rtc_param": "0",
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
"rtc_observed_at": "2026-09-25T00:11:43+00:00",
"rtc_observed_at": "2026-09-25T01:07:33+00:00",
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
"official_catalog": null,
"catalog_integrity": {
@@ -449,10 +469,12 @@
"total": 20996,
"valid_existing": 0,
"ready_to_apply": 0,
"suggestion_only": 0,
"ambiguous": 0,
"insufficient": 20996,
"conflicts": 0,
"official_mismatch": 0,
"catalog_incompatible": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
@@ -464,11 +486,12 @@
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH": 131,
"LOCAL_ONLY": 0,
"OFFICIAL_ONLY": 19,
"MATCH_SAFE": 131,
"MATCH_WITH_NON_MATERIAL_DIFFERENCE": 0,
"MISMATCH": 11,
"EXPIRED": 3
"EXPIRED": 3,
"MISSING_LOCAL": 19,
"MISSING_OFFICIAL": 0
},
"expired_present_locally": 3
}
@@ -483,7 +506,7 @@
"context": {
"rtc_param": "0",
"rtc_source": "SIMULATION",
"rtc_observed_at": "2026-09-25T00:11:44+00:00",
"rtc_observed_at": "2026-09-25T01:07:35+00:00",
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
"official_catalog": {
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
@@ -508,54 +531,59 @@
"resolution_summary": {
"total": 20996,
"valid_existing": 0,
"ready_to_apply": 20831,
"ambiguous": 49,
"ready_to_apply": 1,
"suggestion_only": 20565,
"ambiguous": 314,
"insufficient": 116,
"conflicts": 0,
"official_mismatch": 0,
"catalog_incompatible": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
"RTC-R3": 1,
"RTC-R4": 20830,
"RTC-R4": 20565,
"RTC-R2": 116,
"RTC-A3": 48,
"RTC-A3": 313,
"RTC-A1": 1
},
"by_reason": {
"ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS": 1,
"NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER": 20830,
"NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER": 20565,
"NO_VALID_NCM": 116,
"SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD": 48,
"SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD": 313,
"ANNEX_XV_NEEDS_OPERATION_OR_CONDITION": 1
},
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH": 131,
"LOCAL_ONLY": 0,
"OFFICIAL_ONLY": 19,
"MATCH_SAFE": 131,
"MATCH_WITH_NON_MATERIAL_DIFFERENCE": 0,
"MISMATCH": 11,
"EXPIRED": 3
"EXPIRED": 3,
"MISSING_LOCAL": 19,
"MISSING_OFFICIAL": 0
},
"expired_present_locally": 3
}
},
"by_status": {
"RESOLVED_WITH_EXPLICIT_RULE": 20831,
"RESOLVED_WITH_EXPLICIT_RULE": 1,
"SUGGESTION_ONLY": 20565,
"INSUFFICIENT_EVIDENCE": 116,
"AMBIGUOUS": 49
"AMBIGUOUS": 314
},
"rtc_blocker_types": {
"CCLASSTRIB_MISSING": 20831,
"CCLASSTRIB_MISSING": 1,
"CCLASSTRIB_MISSING_UNRESOLVED": 20565,
"RTC_RULE_EVIDENCE_MISSING": 116,
"CCLASSTRIB_AMBIGUOUS": 49
"CCLASSTRIB_AMBIGUOUS": 314
},
"AUTO_APPLY_CANDIDATES": 20831,
"AUTO_APPLY_CANDIDATES": 1,
"context": {
"rtc_param": "0",
"rtc_source": "SIMULATION",
"rtc_observed_at": "2026-09-25T00:11:49+00:00",
"rtc_observed_at": "2026-09-25T01:07:39+00:00",
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
"official_catalog": {
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
@@ -592,7 +620,7 @@
"context": {
"rtc_param": "0",
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
"rtc_observed_at": "2026-09-25T00:11:50+00:00",
"rtc_observed_at": "2026-09-25T01:07:40+00:00",
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
"official_catalog": null,
"catalog_integrity": {
@@ -613,11 +641,13 @@
"resolution_summary": {
"total": 894,
"valid_existing": 0,
"ready_to_apply": 530,
"ready_to_apply": 63,
"suggestion_only": 467,
"ambiguous": 364,
"insufficient": 0,
"conflicts": 0,
"official_mismatch": 0,
"catalog_incompatible": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
@@ -642,28 +672,31 @@
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH": 131,
"LOCAL_ONLY": 0,
"OFFICIAL_ONLY": 19,
"MATCH_SAFE": 131,
"MATCH_WITH_NON_MATERIAL_DIFFERENCE": 0,
"MISMATCH": 11,
"EXPIRED": 3
"EXPIRED": 3,
"MISSING_LOCAL": 19,
"MISSING_OFFICIAL": 0
},
"expired_present_locally": 3
}
},
"by_status": {
"RESOLVED_WITH_EXPLICIT_RULE": 530,
"AMBIGUOUS": 364
"SUGGESTION_ONLY": 467,
"AMBIGUOUS": 364,
"RESOLVED_WITH_EXPLICIT_RULE": 63
},
"rtc_blocker_types": {
"CCLASSTRIB_MISSING": 530,
"CCLASSTRIB_AMBIGUOUS": 364
"CCLASSTRIB_MISSING_UNRESOLVED": 467,
"CCLASSTRIB_AMBIGUOUS": 364,
"CCLASSTRIB_MISSING": 63
},
"AUTO_APPLY_CANDIDATES": 530,
"AUTO_APPLY_CANDIDATES": 63,
"context": {
"rtc_param": "0",
"rtc_source": "SIMULATION",
"rtc_observed_at": "2026-09-25T00:11:50+00:00",
"rtc_observed_at": "2026-09-25T01:07:41+00:00",
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
"official_catalog": {
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
@@ -688,11 +721,13 @@
"resolution_summary": {
"total": 894,
"valid_existing": 0,
"ready_to_apply": 530,
"ready_to_apply": 63,
"suggestion_only": 467,
"ambiguous": 364,
"insufficient": 0,
"conflicts": 0,
"official_mismatch": 0,
"catalog_incompatible": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
@@ -717,28 +752,31 @@
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH": 131,
"LOCAL_ONLY": 0,
"OFFICIAL_ONLY": 19,
"MATCH_SAFE": 131,
"MATCH_WITH_NON_MATERIAL_DIFFERENCE": 0,
"MISMATCH": 11,
"EXPIRED": 3
"EXPIRED": 3,
"MISSING_LOCAL": 19,
"MISSING_OFFICIAL": 0
},
"expired_present_locally": 3
}
},
"by_status": {
"RESOLVED_WITH_EXPLICIT_RULE": 530,
"AMBIGUOUS": 364
"SUGGESTION_ONLY": 467,
"AMBIGUOUS": 364,
"RESOLVED_WITH_EXPLICIT_RULE": 63
},
"rtc_blocker_types": {
"CCLASSTRIB_MISSING": 530,
"CCLASSTRIB_AMBIGUOUS": 364
"CCLASSTRIB_MISSING_UNRESOLVED": 467,
"CCLASSTRIB_AMBIGUOUS": 364,
"CCLASSTRIB_MISSING": 63
},
"AUTO_APPLY_CANDIDATES": 530,
"AUTO_APPLY_CANDIDATES": 63,
"context": {
"rtc_param": "0",
"rtc_source": "SIMULATION",
"rtc_observed_at": "2026-09-25T00:11:50+00:00",
"rtc_observed_at": "2026-09-25T01:07:41+00:00",
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
"official_catalog": {
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
@@ -775,7 +813,7 @@
"context": {
"rtc_param": "0",
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
"rtc_observed_at": "2026-09-25T00:11:52+00:00",
"rtc_observed_at": "2026-09-25T01:07:42+00:00",
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
"official_catalog": null,
"catalog_integrity": {
@@ -797,10 +835,12 @@
"total": 8942,
"valid_existing": 0,
"ready_to_apply": 0,
"suggestion_only": 0,
"ambiguous": 0,
"insufficient": 8942,
"conflicts": 0,
"official_mismatch": 0,
"catalog_incompatible": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
@@ -812,11 +852,12 @@
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH": 131,
"LOCAL_ONLY": 0,
"OFFICIAL_ONLY": 19,
"MATCH_SAFE": 131,
"MATCH_WITH_NON_MATERIAL_DIFFERENCE": 0,
"MISMATCH": 11,
"EXPIRED": 3
"EXPIRED": 3,
"MISSING_LOCAL": 19,
"MISSING_OFFICIAL": 0
},
"expired_present_locally": 3
}
@@ -831,7 +872,7 @@
"context": {
"rtc_param": "0",
"rtc_source": "SIMULATION",
"rtc_observed_at": "2026-09-25T00:11:52+00:00",
"rtc_observed_at": "2026-09-25T01:07:43+00:00",
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
"official_catalog": {
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
@@ -856,11 +897,13 @@
"resolution_summary": {
"total": 8942,
"valid_existing": 0,
"ready_to_apply": 7419,
"ready_to_apply": 989,
"suggestion_only": 6430,
"ambiguous": 1432,
"insufficient": 91,
"conflicts": 0,
"official_mismatch": 0,
"catalog_incompatible": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
@@ -894,30 +937,33 @@
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH": 131,
"LOCAL_ONLY": 0,
"OFFICIAL_ONLY": 19,
"MATCH_SAFE": 131,
"MATCH_WITH_NON_MATERIAL_DIFFERENCE": 0,
"MISMATCH": 11,
"EXPIRED": 3
"EXPIRED": 3,
"MISSING_LOCAL": 19,
"MISSING_OFFICIAL": 0
},
"expired_present_locally": 3
}
},
"by_status": {
"RESOLVED_WITH_EXPLICIT_RULE": 7419,
"SUGGESTION_ONLY": 6430,
"RESOLVED_WITH_EXPLICIT_RULE": 989,
"AMBIGUOUS": 1432,
"INSUFFICIENT_EVIDENCE": 91
},
"rtc_blocker_types": {
"CCLASSTRIB_MISSING": 7419,
"CCLASSTRIB_MISSING_UNRESOLVED": 6430,
"CCLASSTRIB_MISSING": 989,
"CCLASSTRIB_AMBIGUOUS": 1432,
"RTC_RULE_EVIDENCE_MISSING": 91
},
"AUTO_APPLY_CANDIDATES": 7419,
"AUTO_APPLY_CANDIDATES": 989,
"context": {
"rtc_param": "0",
"rtc_source": "SIMULATION",
"rtc_observed_at": "2026-09-25T00:11:54+00:00",
"rtc_observed_at": "2026-09-25T01:07:45+00:00",
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
"official_catalog": {
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
+48
View File
@@ -0,0 +1,48 @@
{
"products": 21000,
"result_payload_bytes": 8342480,
"result_payload_mb": 7.96,
"persist_first_pass": {
"status": 200,
"seconds": 5.46,
"sql_statements": 26043,
"statements_per_product": 1.24
},
"persist_steady_state": {
"status": 200,
"seconds": 3.45,
"sql_statements": 64,
"statements_per_product": 0.0
},
"rows": {
"blocked": 21000,
"rtc_candidates": 5000
},
"panel_pages": {
"dashboard": {
"status": 200,
"seconds": 0.209,
"html_kb": 10.3
},
"agent_detail": {
"status": 200,
"seconds": 0.196,
"html_kb": 10.7
},
"rtc_control_p1": {
"status": 200,
"seconds": 0.036,
"html_kb": 18.6
},
"rtc_control_p50": {
"status": 200,
"seconds": 0.017,
"html_kb": 18.9
},
"blockers": {
"status": 200,
"seconds": 0.228,
"html_kb": 40.2
}
}
}
+462
View File
@@ -0,0 +1,462 @@
{
"mode": "FULL (Hub decision + fiscal)",
"database": "RINCAO",
"crt": "3",
"products": 894,
"fiscal_products_read": 894,
"BEFORE": {
"COMPLETE_PRODUCTS": 894,
"SAFE_BLOCKED_PRODUCTS": 0,
"COVERAGE_PERCENT": 100.0,
"UNKNOWN_APPLICABILITY_previous_model": 815
},
"runs": {
"ERP_AS_FOUND": {
"rtc_enabled": false,
"TOTAL_PRODUCTS": 894,
"COMPLETE_PRODUCTS": 893,
"SAFE_BLOCKED_PRODUCTS": 1,
"FAILED_PRODUCTS": 0,
"UNCLASSIFIED_PRODUCTS": 0,
"COVERAGE_PERCENT": 99.89,
"INVARIANT_PASS": true,
"PIS_BLOCKERS": 1,
"COFINS_BLOCKERS": 1,
"RTC_BLOCKERS": 0,
"OTHER_BLOCKERS": 0,
"blockers_by_type": {
"COFINS_CST_INVALID": 1,
"PIS_CST_INVALID": 1
},
"applicability": {
"OPTIONAL": 894
},
"rtc_applicability": {
"NOT_APPLICABLE": 894
},
"changed_products": 1,
"examples": [
{
"product": "000866",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"COFINS_CST_INVALID",
"PIS_CST_INVALID"
]
}
],
"non_canonical_warnings": 78,
"context": {
"rtc_enabled": false,
"erp_rtc_enabled": false,
"rtc_supported": true,
"rtc_catalog_valid": true,
"rtc_param": "0",
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
"rtc_param_rows": 1,
"rtc_param_conflict": false,
"rtc_observed_at": "2026-09-25T01:22:08+00:00",
"legal_effective_context": {
"crt": "3",
"informational_only": true,
"ibs_cbs_in_documents": "MANDATORY_FROM_2026-08-03",
"cclasstrib_orientation": "TABLE_AND_LC214_ANNEXES",
"sources": [
"Ato Conjunto RFB/CGIBS n 4/2026 (DOU 31/07/2026) art. 1 I, II and par. 1",
"NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT",
"LC 214/2025 art. 348"
]
},
"catalog_integrity": {
"classes": 145,
"cst": 18,
"orphan_classes": 0,
"bad_class_format": 0,
"bad_cst_format": 0,
"duplicates": 0,
"valid": true
},
"crt": "3",
"products_evaluated": 894,
"pis_cofins_applicability": {
"OPTIONAL": 894
},
"rtc_resolution": null,
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
"official_catalog": null,
"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": 893,
"SAFE_BLOCKED_PRODUCTS": 1,
"FAILED_PRODUCTS": 0,
"UNCLASSIFIED_PRODUCTS": 0,
"COVERAGE_PERCENT": 99.89,
"INVARIANT_PASS": true,
"PIS_BLOCKERS": 1,
"COFINS_BLOCKERS": 1,
"RTC_BLOCKERS": 0,
"OTHER_BLOCKERS": 0,
"blockers_by_type": {
"COFINS_CST_INVALID": 1,
"PIS_CST_INVALID": 1
},
"applicability": {
"OPTIONAL": 894
},
"rtc_applicability": {
"NOT_APPLICABLE": 894
},
"changed_products": 1,
"examples": [
{
"product": "000866",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"COFINS_CST_INVALID",
"PIS_CST_INVALID"
]
}
],
"non_canonical_warnings": 78
},
"RTC_ON_SIMULATION": {
"rtc_enabled": true,
"TOTAL_PRODUCTS": 894,
"COMPLETE_PRODUCTS": 0,
"SAFE_BLOCKED_PRODUCTS": 894,
"FAILED_PRODUCTS": 0,
"UNCLASSIFIED_PRODUCTS": 0,
"COVERAGE_PERCENT": 0.0,
"INVARIANT_PASS": true,
"PIS_BLOCKERS": 1,
"COFINS_BLOCKERS": 1,
"RTC_BLOCKERS": 894,
"OTHER_BLOCKERS": 0,
"blockers_by_type": {
"COFINS_CST_INVALID": 1,
"PIS_CST_INVALID": 1,
"RTC_RULE_EVIDENCE_MISSING": 894
},
"applicability": {
"OPTIONAL": 894
},
"rtc_applicability": {
"REQUIRED": 894
},
"changed_products": 894,
"examples": [
{
"product": "000001",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000004",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000005",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000006",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000007",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000008",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000009",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000010",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000011",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000012",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000013",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000014",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000015",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000016",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000017",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000018",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000019",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000020",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000021",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000022",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000023",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000024",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000025",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000026",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000027",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000028",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000029",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000030",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000031",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
},
{
"product": "000032",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"RTC_RULE_EVIDENCE_MISSING"
]
}
],
"non_canonical_warnings": 78
}
},
"SAFETY": {
"FIREBIRD_WRITES": 0,
"WRITE_ATTEMPTS": 0,
"WRITE_SUCCESSES": 0,
"connection_mode": "READ COMMITTED READ ONLY (Agent reader); no write-capable connection opened"
}
}
+393
View File
@@ -0,0 +1,393 @@
{
"mode": "FULL (Hub decision + fiscal)",
"database": "UNIAO",
"crt": "1",
"products": 8942,
"fiscal_products_read": 8942,
"BEFORE": {
"COMPLETE_PRODUCTS": 8851,
"SAFE_BLOCKED_PRODUCTS": 91,
"COVERAGE_PERCENT": 98.98,
"UNKNOWN_APPLICABILITY_previous_model": 8170
},
"runs": {
"ERP_AS_FOUND": {
"rtc_enabled": false,
"TOTAL_PRODUCTS": 8942,
"COMPLETE_PRODUCTS": 8851,
"SAFE_BLOCKED_PRODUCTS": 91,
"FAILED_PRODUCTS": 0,
"UNCLASSIFIED_PRODUCTS": 0,
"COVERAGE_PERCENT": 98.98,
"INVARIANT_PASS": true,
"PIS_BLOCKERS": 0,
"COFINS_BLOCKERS": 0,
"RTC_BLOCKERS": 0,
"OTHER_BLOCKERS": 0,
"blockers_by_type": {},
"applicability": {
"NOT_APPLICABLE": 8942
},
"rtc_applicability": {
"NOT_APPLICABLE": 8942
},
"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_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
"rtc_param_rows": 1,
"rtc_param_conflict": false,
"rtc_observed_at": "2026-09-25T01:27:07+00:00",
"legal_effective_context": {
"crt": "1",
"informational_only": true,
"ibs_cbs_in_documents": "MANDATORY_FROM_2027-01-01",
"cclasstrib_orientation": "PENDING_OFFICIAL_NT",
"sources": [
"Ato Conjunto RFB/CGIBS n 4/2026 (DOU 31/07/2026) art. 1 I, II and par. 1",
"NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT",
"LC 214/2025 art. 348"
]
},
"catalog_integrity": {
"classes": 145,
"cst": 18,
"orphan_classes": 0,
"bad_class_format": 0,
"bad_cst_format": 0,
"duplicates": 0,
"valid": true
},
"crt": "1",
"products_evaluated": 8942,
"pis_cofins_applicability": {
"NOT_APPLICABLE": 8942
},
"rtc_resolution": null,
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
"official_catalog": null,
"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": 8851,
"SAFE_BLOCKED_PRODUCTS": 91,
"FAILED_PRODUCTS": 0,
"UNCLASSIFIED_PRODUCTS": 0,
"COVERAGE_PERCENT": 98.98,
"INVARIANT_PASS": true,
"PIS_BLOCKERS": 0,
"COFINS_BLOCKERS": 0,
"RTC_BLOCKERS": 0,
"OTHER_BLOCKERS": 0,
"blockers_by_type": {},
"applicability": {
"NOT_APPLICABLE": 8942
},
"rtc_applicability": {
"NOT_APPLICABLE": 8942
},
"changed_products": 0,
"examples": [],
"non_canonical_warnings": 2
},
"RTC_ON_SIMULATION": {
"rtc_enabled": true,
"TOTAL_PRODUCTS": 8942,
"COMPLETE_PRODUCTS": 0,
"SAFE_BLOCKED_PRODUCTS": 8942,
"FAILED_PRODUCTS": 0,
"UNCLASSIFIED_PRODUCTS": 0,
"COVERAGE_PERCENT": 0.0,
"INVARIANT_PASS": true,
"PIS_BLOCKERS": 0,
"COFINS_BLOCKERS": 0,
"RTC_BLOCKERS": 8942,
"OTHER_BLOCKERS": 0,
"blockers_by_type": {
"CCLASSTRIB_MISSING_UNRESOLVED": 8942
},
"applicability": {
"NOT_APPLICABLE": 8942
},
"rtc_applicability": {
"REQUIRED": 8942
},
"changed_products": 8851,
"examples": [
{
"product": "000008",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000009",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000014",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000015",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000016",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000017",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000018",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000019",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000020",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000021",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000022",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000023",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000024",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000025",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000026",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000027",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000028",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000029",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000030",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000031",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000032",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000033",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000034",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000035",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000036",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000037",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000038",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000039",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000040",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000041",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
}
],
"non_canonical_warnings": 2
}
},
"SAFETY": {
"FIREBIRD_WRITES": 0,
"WRITE_ATTEMPTS": 0,
"WRITE_SUCCESSES": 0,
"connection_mode": "READ COMMITTED READ ONLY (Agent reader); no write-capable connection opened"
}
}
+260 -266
View File
@@ -1,49 +1,70 @@
{
"mode": "FULL (Hub decision + fiscal)",
"database": "TESTESOFICIAIS",
"database": "CONSTRUCAO",
"crt": "1",
"products": 521,
"fiscal_products_read": 521,
"products": 3712,
"fiscal_products_read": 3712,
"BEFORE": {
"COMPLETE_PRODUCTS": 495,
"SAFE_BLOCKED_PRODUCTS": 26,
"COVERAGE_PERCENT": 95.01,
"UNKNOWN_APPLICABILITY_previous_model": 518
"COMPLETE_PRODUCTS": 3698,
"SAFE_BLOCKED_PRODUCTS": 14,
"COVERAGE_PERCENT": 99.62,
"UNKNOWN_APPLICABILITY_previous_model": 3712
},
"runs": {
"ERP_AS_FOUND": {
"rtc_enabled": true,
"TOTAL_PRODUCTS": 521,
"TOTAL_PRODUCTS": 3712,
"COMPLETE_PRODUCTS": 0,
"SAFE_BLOCKED_PRODUCTS": 521,
"SAFE_BLOCKED_PRODUCTS": 3712,
"FAILED_PRODUCTS": 0,
"UNCLASSIFIED_PRODUCTS": 0,
"COVERAGE_PERCENT": 0.0,
"INVARIANT_PASS": true,
"PIS_BLOCKERS": 1,
"COFINS_BLOCKERS": 1,
"RTC_BLOCKERS": 521,
"PIS_BLOCKERS": 0,
"COFINS_BLOCKERS": 0,
"RTC_BLOCKERS": 3712,
"OTHER_BLOCKERS": 0,
"blockers_by_type": {
"CCLASSTRIB_MISSING": 521,
"COFINS_CST_INCIDENCE_MISMATCH": 1,
"PIS_CST_INCIDENCE_MISMATCH": 1
"CCLASSTRIB_MISSING_UNRESOLVED": 3712
},
"applicability": {
"NOT_APPLICABLE": 519,
"REQUIRED": 2
"NOT_APPLICABLE": 3712
},
"rtc_applicability": {
"REQUIRED": 521
"REQUIRED": 3712
},
"changed_products": 495,
"changed_products": 3698,
"examples": [
{
"product": "000001",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000002",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000003",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000004",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -51,7 +72,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -59,7 +80,15 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000007",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -67,7 +96,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -75,7 +104,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -83,7 +112,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -91,7 +120,15 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000012",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -99,7 +136,15 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000014",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -107,7 +152,15 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000016",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -115,7 +168,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -123,7 +176,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -131,7 +184,15 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000020",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -139,7 +200,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -147,7 +208,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -155,7 +216,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -163,7 +224,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -171,7 +232,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -179,7 +240,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -187,7 +248,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -195,7 +256,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -203,83 +264,19 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000032",
"product": "000030",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000033",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000034",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000035",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000036",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000038",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000040",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000041",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000042",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
}
],
"non_canonical_warnings": 1,
"non_canonical_warnings": 0,
"context": {
"rtc_enabled": true,
"erp_rtc_enabled": true,
@@ -289,13 +286,15 @@
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
"rtc_param_rows": 1,
"rtc_param_conflict": false,
"rtc_observed_at": "2026-09-24T20:40:30+00:00",
"rtc_observed_at": "2026-09-25T01:33:41+00:00",
"legal_effective_context": {
"crt": "1",
"informational_only": true,
"ibs_cbs_in_documents": "FROM_2027_FOR_SIMPLES_ORIENTATION_PENDING",
"ibs_cbs_in_documents": "MANDATORY_FROM_2027-01-01",
"cclasstrib_orientation": "PENDING_OFFICIAL_NT",
"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",
"Ato Conjunto RFB/CGIBS n 4/2026 (DOU 31/07/2026) art. 1 I, II and par. 1",
"NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT",
"LC 214/2025 art. 348"
]
},
@@ -309,113 +308,132 @@
"valid": true
},
"crt": "1",
"products_evaluated": 521,
"products_evaluated": 3712,
"pis_cofins_applicability": {
"NOT_APPLICABLE": 519,
"REQUIRED": 2
"NOT_APPLICABLE": 3712
},
"rtc_resolution": {
"total": 3712,
"valid_existing": 0,
"ready_to_apply": 0,
"suggestion_only": 0,
"ambiguous": 0,
"insufficient": 3712,
"conflicts": 0,
"official_mismatch": 0,
"catalog_incompatible": 0,
"not_applicable": 0,
"other_blocked": 0,
"by_rule": {
"RTC-R1": 3712
},
"by_reason": {
"SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT": 3712
},
"catalog_compatibility": {
"compatibility": "MATERIAL_MISMATCH",
"counts": {
"MATCH_SAFE": 131,
"MATCH_WITH_NON_MATERIAL_DIFFERENCE": 0,
"MISMATCH": 11,
"EXPIRED": 3,
"MISSING_LOCAL": 19,
"MISSING_OFFICIAL": 0
},
"expired_present_locally": 3
}
},
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
"official_catalog": {
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
"publication": "2026-06-22",
"file": "cClassTrib 2026-06-22.xlsx"
},
"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"
}
]
"products": 0,
"by_type": {},
"samples": []
}
}
},
"RTC_OFF_SIMULATION": {
"rtc_enabled": false,
"TOTAL_PRODUCTS": 521,
"COMPLETE_PRODUCTS": 494,
"SAFE_BLOCKED_PRODUCTS": 27,
"TOTAL_PRODUCTS": 3712,
"COMPLETE_PRODUCTS": 3698,
"SAFE_BLOCKED_PRODUCTS": 14,
"FAILED_PRODUCTS": 0,
"UNCLASSIFIED_PRODUCTS": 0,
"COVERAGE_PERCENT": 94.82,
"COVERAGE_PERCENT": 99.62,
"INVARIANT_PASS": true,
"PIS_BLOCKERS": 1,
"COFINS_BLOCKERS": 1,
"PIS_BLOCKERS": 0,
"COFINS_BLOCKERS": 0,
"RTC_BLOCKERS": 0,
"OTHER_BLOCKERS": 0,
"blockers_by_type": {
"COFINS_CST_INCIDENCE_MISMATCH": 1,
"PIS_CST_INCIDENCE_MISMATCH": 1
},
"blockers_by_type": {},
"applicability": {
"NOT_APPLICABLE": 519,
"REQUIRED": 2
"NOT_APPLICABLE": 3712
},
"rtc_applicability": {
"NOT_APPLICABLE": 521
"NOT_APPLICABLE": 3712
},
"changed_products": 1,
"examples": [
{
"product": "000700",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"COFINS_CST_INCIDENCE_MISMATCH",
"PIS_CST_INCIDENCE_MISMATCH"
]
}
],
"non_canonical_warnings": 1
"changed_products": 0,
"examples": [],
"non_canonical_warnings": 0
},
"RTC_ON_SIMULATION": {
"rtc_enabled": true,
"TOTAL_PRODUCTS": 521,
"TOTAL_PRODUCTS": 3712,
"COMPLETE_PRODUCTS": 0,
"SAFE_BLOCKED_PRODUCTS": 521,
"SAFE_BLOCKED_PRODUCTS": 3712,
"FAILED_PRODUCTS": 0,
"UNCLASSIFIED_PRODUCTS": 0,
"COVERAGE_PERCENT": 0.0,
"INVARIANT_PASS": true,
"PIS_BLOCKERS": 1,
"COFINS_BLOCKERS": 1,
"RTC_BLOCKERS": 521,
"PIS_BLOCKERS": 0,
"COFINS_BLOCKERS": 0,
"RTC_BLOCKERS": 3712,
"OTHER_BLOCKERS": 0,
"blockers_by_type": {
"CCLASSTRIB_MISSING": 521,
"COFINS_CST_INCIDENCE_MISMATCH": 1,
"PIS_CST_INCIDENCE_MISMATCH": 1
"CCLASSTRIB_MISSING_UNRESOLVED": 3712
},
"applicability": {
"NOT_APPLICABLE": 519,
"REQUIRED": 2
"NOT_APPLICABLE": 3712
},
"rtc_applicability": {
"REQUIRED": 521
"REQUIRED": 3712
},
"changed_products": 495,
"changed_products": 3698,
"examples": [
{
"product": "000001",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000002",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000003",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000004",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -423,7 +441,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -431,7 +449,15 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000007",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -439,7 +465,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -447,7 +473,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -455,7 +481,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -463,7 +489,15 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000012",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -471,7 +505,15 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000014",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -479,7 +521,15 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000016",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -487,7 +537,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -495,7 +545,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -503,7 +553,15 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000020",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -511,7 +569,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -519,7 +577,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -527,7 +585,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -535,7 +593,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -543,7 +601,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -551,7 +609,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -559,7 +617,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -567,7 +625,7 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
@@ -575,83 +633,19 @@
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
},
{
"product": "000032",
"product": "000030",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000033",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000034",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000035",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000036",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000038",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000040",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000041",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
]
},
{
"product": "000042",
"BEFORE_STATUS": "COMPLETE",
"AFTER_STATUS": "BLOCKED_FISCAL_DATA",
"REASON": [
"CCLASSTRIB_MISSING"
"CCLASSTRIB_MISSING_UNRESOLVED"
]
}
],
"non_canonical_warnings": 1
"non_canonical_warnings": 0
}
},
"SAFETY": {
@@ -0,0 +1,95 @@
{
"purpose": "Official acts consulted for the cClassTrib determination (closure mission). Authority order: legislation > Portal NF-e > Receita > SPED. Local files are ignored by git; hashes pin exactly what was read.",
"authority_order": [
"LC 214/2025 and amendments (LC 227/2026)",
"Portal Nacional da NF-e (NT 2025.002-RTC v1.51, NT 2026.00x, cClassTrib table)",
"Receita Federal / CGIBS Atos Conjuntos (1/2025, 4/2026)",
"SPED"
],
"findings": {
"simples_cclasstrib_orientation": "NONE_OFFICIAL_YET (NT 2025.002 v1.51 defers CRT 1/2/4; Ato Conjunto RFB/CGIBS 4/2026 sets the Simples obligation from 2027-01-01)",
"nfe_nfce_ibs_cbs_obligation": "from 2026-08-03 (Ato Conjunto RFB/CGIBS 4/2026 art. 1)"
},
"files": [
{
"local_file": "sources_cache/legal/ato_conj_1_2025.bin",
"sha256": "5354059c82c78902bd870ee241c068ecbe177cc36c3486867824d63195d257b7",
"bytes": 132565,
"published_name": "ATO CONJUNTO RFB_CGIBS Nº 1 DE 22 DE DEZEMBRO DE 2025.pdf",
"content_type": "application/pdf",
"accessed_http_date": "Fri, 25 Sep 2026 00:41:10 GMT"
},
{
"local_file": "sources_cache/legal/ato_conj_4_2026.bin",
"sha256": "64ab779b13ad3df47530bf9a77a07cd42402ebd5b2cff5ede403b0169c0f7b99",
"bytes": 119007,
"published_name": "ATO CONJUNTO RFB_CGIBS Nº 4 DE 30 DE julho DE 2026.pdf",
"content_type": "application/pdf",
"accessed_http_date": "Fri, 25 Sep 2026 00:41:09 GMT"
},
{
"local_file": "sources_cache/legal/ato_tec_1_2026.bin",
"sha256": "b67ba6228b7f9b4d55e982187d8d89ea680b56e4e97753f20f569ab29a71b67f",
"bytes": 107050,
"published_name": "ATO TÉCNICO CONJUNTO RFB_CGIBS Nº 1 DE 31 DE JULHO DE 2026.pdf",
"content_type": "application/pdf",
"accessed_http_date": "Fri, 25 Sep 2026 00:41:08 GMT"
},
{
"local_file": "sources_cache/legal/ato_tec_2_2026.bin",
"sha256": "11cb025e36ac93168a61f35dd47c469c344ff65dd88be6bf45cb34826dba73e1",
"bytes": 165111,
"published_name": "ATO TECNICO CONJUNTO RFB_SUFIS_CGIBS_DIRETORIA-EXECUTIVA N 2 DE 21 DE AGOSTO DE 2026.PDF",
"content_type": "application/pdf",
"accessed_http_date": "Fri, 25 Sep 2026 00:41:07 GMT"
},
{
"local_file": "sources_cache/legal/nt2025_002_v151.bin",
"sha256": "a4aaaa181522b43cd90b502f8ccb9e4bdabea30fed838e2d0790be5ba77254a4",
"bytes": 2182361,
"published_name": "NT_2025.002_v1.51_RTC.pdf",
"content_type": "application/pdf",
"accessed_http_date": "Thu, 24 Sep 2026 19:04:50 GMT"
},
{
"local_file": "sources_cache/legal/nt2026_001.bin",
"sha256": "703310f95bff855aad9a4a7004515ce29ffff343334c53f0c45e9b32390296cd",
"bytes": 1010960,
"published_name": "NT_2026.001_v1.02b - PAA NFe.pdf",
"content_type": "application/pdf",
"accessed_http_date": "Fri, 25 Sep 2026 00:41:41 GMT"
},
{
"local_file": "sources_cache/legal/nt2026_002_v110a.bin",
"sha256": "705f771830565c209d815659347ac4ea251901c77064cc163ce5c93f311e6e64",
"bytes": 767698,
"published_name": "NT_2026.002_v1.10a_DANFE_Simpl_Tp2_Emi_Offline_Autoriz_Alerta_v2.pdf",
"content_type": "application/pdf",
"accessed_http_date": "Thu, 24 Sep 2026 19:04:51 GMT"
},
{
"local_file": "sources_cache/legal/nt2026_006.bin",
"sha256": "47a65abde315fed1e6a8c6085a67ca483935cb57877ae5715d365377a725e05d",
"bytes": 767681,
"published_name": "NT_2026.006_v1.00_RTC_VinculacaoPagamento[2775].pdf",
"content_type": "application/pdf",
"accessed_http_date": "Thu, 24 Sep 2026 19:04:52 GMT"
},
{
"local_file": "sources_cache/legal/nt2026_007.bin",
"sha256": "559fcd7d1b495549099498ff5c1ba4165f65d4b5ad5b34dbe802869d4619d917",
"bytes": 733482,
"published_name": "NT2026.007_v1.00 - Emissão de NF-e sem IE e RV LCC.pdf",
"content_type": "application/pdf",
"accessed_http_date": "Fri, 25 Sep 2026 00:41:40 GMT"
},
{
"local_file": "sources_cache/legal/nt2026_009.bin",
"sha256": "50b7dec40bba8928b4d544feb4fdbd8c8bac16e37c99d926ddb2834cf246f15d",
"bytes": 310342,
"published_name": "NT2026.009_v1.00_Alteracao_RV_CFOP.pdf",
"content_type": "application/pdf",
"accessed_http_date": "Thu, 24 Sep 2026 19:04:53 GMT"
}
]
}
+6 -6
View File
@@ -1,14 +1,14 @@
{
"artifact": "RELEASE_MANIFEST",
"GTI_FISCAL_VERSION": "1.1.9",
"GTI_FISCAL_VERSION": "1.2.0",
"generated_at": "2026-09-24T14:13:15+00:00",
"hub": {
"version": "1.1.9",
"version": "1.2.0",
"api_compat": "1.0",
"engine_version": "0.3.0",
"decision_version": "mission-04c-ipi-ex-split"
},
"agent_version": "1.1.9",
"agent_version": "1.2.0",
"fiscal_engine": {
"ENGINE_VERSION": "0.3.0",
"DECISION_VERSION": "mission-04c-ipi-ex-split",
@@ -17,10 +17,10 @@
"ICMS_CONTEXT_ENGINE_VERSION": "icms-context-v1"
},
"migrations": {
"alembic_head_in_repo": "0027_rtc_auto_apply",
"applied_revision": "0027_rtc_auto_apply",
"alembic_head_in_repo": "0028_rtc_write_modes",
"applied_revision": "0028_rtc_write_modes",
"applied_revision_source": "production Hub 10.0.10.117 (gti-fiscal-hub-fiscal-hub-1), verified 2026-09-24",
"count": 27
"count": 28
},
"datasets_active": [
{
+1 -1
View File
@@ -1 +1 @@
1.1.9
1.2.0
+135 -52
View File
@@ -1,19 +1,23 @@
"""COPY-ONLY write E2E for the RTC auto-apply (C000025.CCLASSTRIB).
"""COPY-ONLY write E2E for the centrally controlled RTC auto-apply (C000025.CCLASSTRIB).
python scripts/rtc_copy_write_e2e.py
Runs ONLY against scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB (a gbak restore of TESTESOFICIAIS). It refuses any other path, and refuses every real database.
Scenario (all through the Agent's own code path `run_agent_service._rtc_auto_apply`):
1. the copy's company is switched to regime normal (C000004.CRT='3'): a disposable experiment, so the rules can act (the real company is Simples => 0 candidates);
2. assess -> candidates; 3. apply with all three gates open; 4. re-read, FK/CST checks, coverage; 5. ONLY CCLASSTRIB changed (whole-table diff);
6. second run: 0 candidates, 0 writes; 7. stale-decision abort (RTC switched off on the copy between decision and write) writes nothing.
Runs ONLY against scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB (restored from its own .fbk at the start, so every run is reproducible). It refuses any other path and every
real database. Everything goes through the Agent's own code path `run_agent_service._rtc_auto_apply`, driven ONLY by the authenticated job `authorizations` (no env switches):
0 restore + company -> regime normal (documented experiment on the copy); 1 whole-database baseline hashes;
2 DISABLED (0 writes, no preview) / DRY_RUN (preview, 0 writes) / stale decision (RTC off between decision and write => 0 writes);
3 APPROVAL_REQUIRED with 5 approved products (only those 5 written) / then ENABLED until nothing is left;
4 reread, FK + CST integrity, coverage; 5 second run = 0 writes;
6 rollback of 3 audit records (+1 conflict: value changed by an operator => refused); 7 reread, reapply, idempotency;
8 invariants: NON_TARGET_COLUMNS_CHANGED, CATALOG_CHANGED, PARAMETERS_CHANGED, DOCUMENT_TABLES_CHANGED, OTHER_TABLES_CHANGED all 0.
Writes go to the copy and nowhere else.
"""
import hashlib
import importlib.util
import json
import os
import subprocess
import sys
from datetime import date
from pathlib import Path
from types import SimpleNamespace
@@ -27,7 +31,9 @@ from central.app.operational_state import classify_product_state, summarize_oper
from gti_fiscal_agent import firebird_reader, unified_fiscal # noqa: E402
COPY = str((ROOT / "scratch_rtc_e2e" / "TESTESOFICIAIS_RTC_COPY.FDB").resolve())
FBK = str((ROOT / "scratch_rtc_e2e" / "TESTESOFICIAIS_RTC_COPY.fbk").resolve())
NCM_OK = set((ROOT / "docs" / "runs" / "ncm_valid_codes.txt").read_text().split())
DOC_TABLE_HINTS = ("NFCE", "NFE", "NF_", "CUPOM", "VENDA", "CTE", "MDFE")
def guard(path):
@@ -58,82 +64,159 @@ def digits(v):
return "".join(ch for ch in str(v or "") if ch.isdigit())
def snapshot_table(cur, table, key, skip_cols=()):
def restore_copy():
guard(COPY)
gbak = Path(os.environ["FIREBIRD_CLIENT_LIBRARY"]).parent / "gbak.exe"
done = subprocess.run([str(gbak), "-c", "-rep", "-n", "-user", os.environ["FB_USER"], "-pas", os.environ["FB_PASSWORD"], FBK, COPY], capture_output=True, text=True, timeout=600)
if done.returncode != 0:
raise SystemExit(f"restore failed: {done.stderr[-300:]}")
def tables(cur):
cur.execute("SELECT TRIM(RDB$RELATION_NAME) FROM RDB$RELATIONS WHERE COALESCE(RDB$SYSTEM_FLAG, 0) = 0 AND RDB$VIEW_BLR IS NULL AND RDB$EXTERNAL_FILE IS NULL ORDER BY 1")
return [r[0] for r in cur.fetchall()]
def table_hash(cur, table, skip_cols=()):
cur.execute(f"SELECT * FROM {table}")
cols = [d[0] for d in cur.description]
ki = cols.index(key)
return cols, {r[ki]: {c: v for c, v in zip(cols, r) if c not in skip_cols} for r in cur.fetchall()}
keep = [i for i, c in enumerate(cols) if c not in skip_cols]
digest = hashlib.sha256()
rows = 0
for row in cur.fetchall():
digest.update(repr([row[i] for i in keep]).encode("utf-8", "replace"))
rows += 1
return f"{rows}:{digest.hexdigest()[:20]}"
def coverage(env, company, ncm_valid, override=None):
def baseline():
conn = connect(False); cur = conn.cursor()
out = {t: table_hash(cur, t, skip_cols=("CCLASSTRIB",) if t == "C000025" else ()) for t in tables(cur)}
cur.execute("SELECT CODIGO, CCLASSTRIB FROM C000025")
values = {str(r[0]).strip(): (r[1] or "").strip() for r in cur.fetchall()}
conn.close()
return out, values
def diff_tables(before, after):
return sorted(t for t in before if before[t] != after.get(t))
def coverage(env, company, ncm_valid):
conn = firebird_reader._connect(env)
try:
out = unified_fiscal.assess_from_connection(conn, company, ncm_valid=ncm_valid, rtc_override=override)
out = unified_fiscal.assess_from_connection(conn, company, ncm_valid=ncm_valid)
finally:
conn.close()
products = firebird_reader.read_products(env)
states = [classify_product_state(p, {"status": "EXACT_NCM"}, fiscal=out["findings"].get(str(p["CODIGO"]))) for p in products]
s = summarize_operational_states(states)
return out, {"total": s["TOTAL_PRODUCTS"], "complete": s["COMPLETE_PRODUCTS"], "blocked": s["SAFE_BLOCKED"], "unclassified": s["UNCLASSIFIED"], "coverage_percent": round(100 * s["COMPLETE_PRODUCTS"] / s["TOTAL_PRODUCTS"], 2)}
return out, products, {"total": s["TOTAL_PRODUCTS"], "complete": s["COMPLETE_PRODUCTS"], "blocked": s["SAFE_BLOCKED"], "unclassified": s["UNCLASSIFIED"], "coverage_percent": round(100 * s["COMPLETE_PRODUCTS"] / s["TOTAL_PRODUCTS"], 2)}
def main():
load_dotenv(ROOT / ".env") # credentials only when the script runs, never as an import side effect
guard(COPY)
service = load_service()
restore_copy()
env = {"FIREBIRD_DATABASE_PATH": COPY, "FB_USER": os.environ["FB_USER"], "FB_PASSWORD": os.environ["FB_PASSWORD"], "FIREBIRD_CLIENT_LIBRARY": os.environ.get("FIREBIRD_CLIENT_LIBRARY")}
report = {"database": "scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB (gbak restore of TESTESOFICIAIS)", "real_database_writes": 0, "real_database_write_attempts": 0}
# ---- 1. company -> regime normal on the COPY (documented experiment)
report = {"database": "scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB (restored from its .fbk at start)", "real_database_writes": 0, "real_database_write_attempts": 0, "mechanism": "job authorizations only (no env switches)"}
rw = connect(True); cur = rw.cursor()
cur.execute("SELECT DISTINCT TRIM(CRT) FROM C000004"); report["copy_original_crt"] = [r[0] for r in cur.fetchall()]
cur.execute("UPDATE C000004 SET CRT = '3'"); rw.commit()
cur.execute("SELECT DISTINCT TRIM(CRT) FROM C000004"); company = {"crt": cur.fetchone()[0]}
report["copy_crt_for_experiment"] = company["crt"]
_, before_c25 = snapshot_table(cur, "C000025", "CODIGO")
cur.execute("SELECT USAR_REFORMA_TRIBUTARIA FROM PARAMETROS"); report["copy_rtc_param"] = cur.fetchone()[0]
cur.execute("SELECT CODIGO, CLASSIFICACAO_FISCAL FROM C000025"); ncm_valid = {str(c).strip(): digits(n) in NCM_OK for c, n in cur.fetchall()}
rw.commit(); rw.close()
ncm_valid = {str(c).strip(): digits(v["CLASSIFICACAO_FISCAL"]) in NCM_OK for c, v in before_c25.items()}
# ---- 2. assess (as found on the copy)
out, cov_before = coverage(env, company, ncm_valid)
report["copy_crt_for_experiment"] = company["crt"]
base, base_values = baseline()
report["tables_hashed"] = len(base)
config = SimpleNamespace(rtc_auto_apply_max_per_job=500, database_path=COPY) # NOTE: no firebird_writes, no rtc switch: the Agent has none
def run_job(mode, approved=(), rollbacks=(), job_id=1):
out, products, _ = coverage(env, company, ncm_valid)
job = {"job_id": job_id, "authorizations": {"rtc_write_mode": mode, "rtc_approved_codes": list(approved), "rtc_rollbacks": list(rollbacks)}}
return service._rtc_auto_apply(env, config, job, products, out, company), out
_, _, cov_before = coverage(env, company, ncm_valid)
report["coverage_before"] = cov_before
report["resolution_before"] = {k: v for k, v in out["context"]["rtc_resolution"].items() if k != "by_reason"}
products = firebird_reader.read_products(env)
config = SimpleNamespace(firebird_writes=True, rtc_auto_apply_enabled=True, rtc_auto_apply_max_per_job=500, database_path=COPY)
job = {"job_id": 424242, "authorizations": {"rtc_auto_apply": True}}
# ---- 7 (first, on purpose): stale decision - RTC switched off on the copy between decision and write => nothing is written
rw = connect(True); c = rw.cursor(); c.execute("UPDATE PARAMETROS SET USAR_REFORMA_TRIBUTARIA = 0"); rw.commit(); rw.close()
stale = service._rtc_auto_apply(env, config, job, products, out, company)
report["stale_decision_run"] = {"write_attempts": stale["write_attempts"], "write_successes": stale["write_successes"], "aborted_reasons": stale["aborted_reasons"]}
rw = connect(True); c = rw.cursor(); c.execute("UPDATE PARAMETROS SET USAR_REFORMA_TRIBUTARIA = 1"); rw.commit(); rw.close()
# ---- 3. apply
first = service._rtc_auto_apply(env, config, job, products, out, company)
report["first_run"] = {"mode": first["mode"], "ready_total": first["ready_total"], "write_attempts": first["write_attempts"], "write_successes": first["write_successes"], "aborted_reasons": first["aborted_reasons"],
"audit_sample": first["applied"][:3]}
# ---- 4/5. re-read, FK, whole-table diff
# ---- 2. DISABLED / DRY_RUN / stale decision: zero writes
disabled, _ = run_job("DISABLED")
dry, out0 = run_job("DRY_RUN")
report["disabled_run"] = {"write_attempts": disabled["write_attempts"], "preview": len(disabled["preview"]), "ready_total": disabled["ready_total"]}
report["dry_run"] = {"write_attempts": dry["write_attempts"], "preview": len(dry["preview"]), "ready_total": dry["ready_total"], "rules": sorted({c["rule_id"] for c in dry["preview"]}), "sample": dry["preview"][:3]}
report["resolution_before"] = {k: v for k, v in out0["context"]["rtc_resolution"].items() if k != "by_reason"}
assert base == baseline()[0], "DISABLED/DRY_RUN wrote something"
rw = connect(True); c = rw.cursor(); c.execute("UPDATE PARAMETROS SET USAR_REFORMA_TRIBUTARIA = 0"); rw.commit()
stale, _ = run_job("ENABLED")
c.execute("UPDATE PARAMETROS SET USAR_REFORMA_TRIBUTARIA = 1"); rw.commit(); rw.close()
report["stale_decision_run"] = {"write_attempts": stale["write_attempts"], "write_successes": stale["write_successes"]}
# ---- 3. APPROVAL_REQUIRED (5 approved) then ENABLED
preview_codes = [c["product_code"] for c in dry["preview"]]
approved = preview_codes[:5]
unapproved = run_job("APPROVAL_REQUIRED", approved=[])[0]
part, _ = run_job("APPROVAL_REQUIRED", approved=approved, job_id=2)
report["approval_run"] = {"without_approval_attempts": unapproved["write_attempts"], "approved": len(approved), "write_successes": part["write_successes"], "only_approved": sorted(a["product_code"] for a in part["applied"]) == sorted(approved)}
applied = list(part["applied"])
jobs = 0
while True:
jobs += 1
step, _ = run_job("ENABLED", job_id=100 + jobs)
applied += step["applied"]
report.setdefault("enabled_jobs", []).append({"write_attempts": step["write_attempts"], "write_successes": step["write_successes"], "aborted": step["aborted_reasons"]})
if not step["write_attempts"] or jobs >= 200:
break
report["total_applied"] = len(applied)
report["audit_sample"] = applied[:2]
report["audit_fields_complete"] = all(a.get(k) is not None for a in applied for k in ("rule_id", "rule_version", "company_crt", "authorization_origin", "description_snapshot", "timestamp", "job_id", "local_catalog_hash", "official_source_version"))
# ---- 4. reread + integrity + coverage
mid, mid_values = baseline()
report["rows_changed"] = sum(1 for k, v in mid_values.items() if base_values.get(k) != v)
ro = connect(False); cur = ro.cursor()
cols, after_c25 = snapshot_table(cur, "C000025", "CODIGO")
changed = {code: [c for c in cols if before_c25[code][c] != row[c]] for code, row in after_c25.items() if any(before_c25[code][c] != row[c] for c in cols)}
report["rows_changed"] = len(changed)
report["columns_changed"] = sorted({c for v in changed.values() for c in v})
cur.execute("SELECT COUNT(*) FROM C000025 p WHERE p.CCLASSTRIB IS NOT NULL AND NOT EXISTS (SELECT 1 FROM RTC_CCLASSTRIB c WHERE c.CCLASSTRIB = p.CCLASSTRIB)"); report["fk_orphans"] = cur.fetchone()[0]
cur.execute("SELECT COUNT(*) FROM C000025 p JOIN RTC_CCLASSTRIB c ON c.CCLASSTRIB = p.CCLASSTRIB WHERE NOT EXISTS (SELECT 1 FROM RTC_CST s WHERE s.CSTIBSCBS = c.CSTIBSCBS)"); report["cst_orphans"] = cur.fetchone()[0]
cur.execute("SELECT CCLASSTRIB, COUNT(*) FROM C000025 WHERE CCLASSTRIB IS NOT NULL GROUP BY 1"); report["written_by_class"] = {r[0].strip(): r[1] for r in cur.fetchall()}
cur.execute("SELECT COUNT(*) FROM RTC_CCLASSTRIB"); report["catalog_rows_unchanged"] = cur.fetchone()[0]
ro.close()
out2, cov_after = coverage(env, company, ncm_valid)
_, _, cov_after = coverage(env, company, ncm_valid)
report["coverage_after"] = cov_after
report["resolution_after"] = {k: v for k, v in out2["context"]["rtc_resolution"].items() if k != "by_reason"}
# ---- 6. second run
products2 = firebird_reader.read_products(env)
second = service._rtc_auto_apply(env, config, job, products2, out2, company)
report["second_run"] = {"ready_total": second["ready_total"], "write_attempts": second["write_attempts"], "write_successes": second["write_successes"]}
report["IDEMPOTENT"] = second["write_attempts"] == 0 and second["write_successes"] == 0
report["ONLY_CCLASSTRIB_CHANGED"] = report["columns_changed"] in ([], ["CCLASSTRIB"])
report["PASS"] = bool(report["IDEMPOTENT"] and report["ONLY_CCLASSTRIB_CHANGED"] and report["fk_orphans"] == 0 and report["cst_orphans"] == 0 and first["write_successes"] == first["write_attempts"] > 0
and report["stale_decision_run"]["write_successes"] == 0 and report["rows_changed"] == first["write_successes"])
# ---- 5. second run
second, _ = run_job("ENABLED", job_id=300)
report["second_run"] = {"write_attempts": second["write_attempts"], "write_successes": second["write_successes"], "ready_total": second["ready_total"]}
# ---- 6. rollback of 3 records + 1 conflict
print(json.dumps({k: report[k] for k in ("disabled_run","dry_run","stale_decision_run","approval_run","enabled_jobs","coverage_before","resolution_before")}, default=str)[:2500]) if len(applied) < 4 else None
victims = applied[:4]
rw = connect(True); c = rw.cursor(); c.execute("UPDATE C000025 SET CCLASSTRIB = '200035' WHERE CODIGO = ?", (victims[3]["product_code"],)); rw.commit(); rw.close() # an operator changed it afterwards
requests = [{"audit_id": 900 + i, "product_code": v["product_code"], "written_value": v["new_value"], "rule_id": v["rule_id"]} for i, v in enumerate(victims)]
roll, _ = run_job("DISABLED", rollbacks=requests, job_id=400) # rollback needs the central request, not a write mode
outcomes = {r["product_code"]: (r["outcome"], r.get("reason")) for r in roll["rollbacks"]}
report["rollback"] = {"rolled_back": sum(1 for o, _ in outcomes.values() if o == "ROLLED_BACK"), "conflict": outcomes[victims[3]["product_code"]], "records": [outcomes[v["product_code"]][0] for v in victims]}
ro = connect(False); cur = ro.cursor()
cur.execute("SELECT CODIGO, CCLASSTRIB FROM C000025 WHERE CODIGO IN (%s)" % ",".join("?" * 4), tuple(v["product_code"] for v in victims))
after_roll = {str(r[0]).strip(): (r[1] or "").strip() for r in cur.fetchall()}
ro.close()
report["rollback"]["reread"] = {k: v for k, v in after_roll.items()}
# ---- 7. reapply + idempotency
again, _ = run_job("ENABLED", job_id=500)
report["reapply"] = {"write_successes": again["write_successes"], "codes": sorted(a["product_code"] for a in again["applied"]), "expected": sorted(v["product_code"] for v in victims[:3])}
third, _ = run_job("ENABLED", job_id=501)
report["idempotency_after_reapply"] = {"write_attempts": third["write_attempts"], "write_successes": third["write_successes"]}
# ---- 8. invariants
final, final_values = baseline()
changed_tables = diff_tables(base, final)
report["NON_TARGET_COLUMNS_CHANGED"] = 1 if "C000025" in changed_tables else 0
report["CATALOG_CHANGED"] = len([t for t in changed_tables if t.startswith("RTC_")])
report["PARAMETERS_CHANGED"] = len([t for t in changed_tables if t == "PARAMETROS"])
report["DOCUMENT_TABLES_CHANGED"] = len([t for t in changed_tables if any(h in t for h in DOC_TABLE_HINTS)])
report["OTHER_TABLES_CHANGED"] = len(changed_tables)
report["changed_tables"] = changed_tables
report["IDEMPOTENT"] = second["write_attempts"] == 0 and third["write_attempts"] == 0
report["PASS"] = bool(report["IDEMPOTENT"] and report["fk_orphans"] == 0 and report["cst_orphans"] == 0 and not changed_tables and report["total_applied"] > 5
and report["disabled_run"]["write_attempts"] == 0 and report["dry_run"]["write_attempts"] == 0 and report["dry_run"]["rules"] == ["RTC-R3"]
and report["stale_decision_run"]["write_successes"] == 0 and report["approval_run"]["without_approval_attempts"] == 0 and report["approval_run"]["only_approved"]
and report["rows_changed"] == report["total_applied"] and report["rollback"]["rolled_back"] == 3 and report["rollback"]["conflict"][0] == "ABORTED"
and report["rollback"]["reread"][victims[3]["product_code"]] == "200035" and all(report["rollback"]["reread"][v["product_code"]] == "" for v in victims[:3])
and report["reapply"]["codes"] == report["reapply"]["expected"] and report["audit_fields_complete"])
(ROOT / "docs" / "runs" / "rtc_copy_write_e2e.json").write_text(json.dumps(report, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
print(json.dumps({k: report[k] for k in ("copy_original_crt", "copy_rtc_param", "coverage_before", "coverage_after", "first_run", "stale_decision_run", "rows_changed", "columns_changed", "fk_orphans", "cst_orphans", "second_run", "IDEMPOTENT", "ONLY_CCLASSTRIB_CHANGED", "PASS")},
ensure_ascii=False, default=str)[:3000])
print(json.dumps({k: report[k] for k in report if k not in ("audit_sample", "coverage_before", "resolution_before")}, ensure_ascii=False, default=str)[:3500])
if __name__ == "__main__":
+88
View File
@@ -0,0 +1,88 @@
"""SCALE test of the Hub side for a ~21k-product installation (synthetic payloads shaped like the real MMMODAS run).
python scripts/scale_hub_test.py [--products 21000] [--out docs/runs/scale_hub_test.json]
Measures: job-result payload size, Hub persistence (blockers upsert + RTC preview + audits) with SQL statement counts (N+1 check),
and the panel pages that read the data (dashboard, agent detail, RTC control, blockers). In-process SQLite: timings are a conservative
upper bound for the PostgreSQL production database only for CPU-bound work; statement counts are engine-independent.
"""
import argparse
import logging
import json
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from sqlalchemy import event, select # noqa: E402
from sqlalchemy.orm import Session # noqa: E402
from central.tests import test_zero_touch_hub as zt # noqa: E402
logging.disable(logging.CRITICAL) # the Hub logs each job result; at 21k products that is megabytes of noise
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--products", type=int, default=21000)
parser.add_argument("--out", default=str(ROOT / "docs" / "runs" / "scale_hub_test.json"))
args = parser.parse_args()
n = args.products
client, engine = zt._client()
counter = {"n": 0}
event.listen(engine, "before_cursor_execute", lambda *a, **k: counter.__setitem__("n", counter["n"] + 1))
zt.hb(client)
with Session(engine) as s:
from central.app.models import Installation
s.scalars(select(Installation)).one().rtc_write_mode = "APPROVAL_REQUIRED"
s.commit()
blocked = [{"company": "", "product_code": f"{i:06d}", "gtin": None, "description": f"PRODUTO DE TESTE {i}", "blocker_type": "BLOCKED_FISCAL_DATA", "field": "RTC", "reason_code": "CCLASSTRIB_MISSING_UNRESOLVED",
"evidence_summary": {"reason": "RTC-R4:SUGGESTION_ONLY:000001", "human": "cClassTrib sem regra determinística"}, "input_fingerprint": f"fp{i:08d}"} for i in range(n)]
preview = [{"product_code": f"{i:06d}", "description": f"PRODUTO DE TESTE {i}", "ncm": "10063021", "current": "", "proposed": "200003", "cst": "200", "rule_id": "RTC-R3", "basis": "RTC-R3:ANNEX"} for i in range(5000)]
operational = {**zt.OPERATIONAL, "TOTAL_PRODUCTS": n, "COMPLETE": 0, "SAFE_BLOCKED": n, "BLOCKED_MISSING_EVIDENCE": n}
summary = {"operational": operational, "products_processed": n, "products_changed": n, "blocked_products": blocked, "resolved_products": [],
"fiscal_context": {"rtc_enabled": True, "evaluated_all": True, "rtc_resolution": {"total": n, "valid_existing": 0, "ready_to_apply": 5000, "ambiguous": 100, "catalog_incompatible": 0}},
"rtc_auto_apply": {"mode": "APPROVAL_REQUIRED", "ready_total": 5000, "preview": preview, "preview_truncated": False, "applied": [], "aborted": [], "rollbacks": []}}
payload = json.dumps({"result_summary": summary})
report = {"products": n, "result_payload_bytes": len(payload), "result_payload_mb": round(len(payload) / 1048576, 2)}
job = client.get("/v1/jobs/next", headers=zt.H).json()
counter["n"] = 0
t0 = time.perf_counter()
response = client.post(f"/v1/jobs/{job['job_id']}/complete", content=payload, headers={**zt.H, "content-type": "application/json"})
report["persist_first_pass"] = {"status": response.status_code, "seconds": round(time.perf_counter() - t0, 2), "sql_statements": counter["n"], "statements_per_product": round(counter["n"] / n, 2)}
# a second full evaluation with the same content (steady state: every 30 min per installation)
zt.age_last_sync(engine, 4000); zt.hb(client)
job = client.get("/v1/jobs/next", headers=zt.H).json()
counter["n"] = 0
t0 = time.perf_counter()
response = client.post(f"/v1/jobs/{job['job_id']}/complete", content=payload, headers={**zt.H, "content-type": "application/json"})
report["persist_steady_state"] = {"status": response.status_code, "seconds": round(time.perf_counter() - t0, 2), "sql_statements": counter["n"], "statements_per_product": round(counter["n"] / n, 2)}
with Session(engine) as s:
from central.app.models import BlockedProduct, RtcCandidate
report["rows"] = {"blocked": len(s.scalars(select(BlockedProduct.id)).all()), "rtc_candidates": len(s.scalars(select(RtcCandidate.id)).all())}
# panel pages (authenticated operator session)
from central.tests.test_control_panel_pages import _panel_client
pc, agent_id, _ = _panel_client()
import central.tests.test_control_panel_pages as cp
cp._seed_rtc(pc, agent_id, n=5000)
from central.app.blocked_products import upsert_blocker
from central.app.models import Installation as Inst
with pc._session_factory() as s:
inst = s.scalar(select(Inst).where(Inst.agent_id == agent_id))
for b in blocked:
upsert_blocker(s, inst.tenant_id, b, installation_id=inst.id)
s.commit()
pages = {}
for name, url in (("dashboard", "/"), ("agent_detail", f"/agents/{agent_id}"), ("rtc_control_p1", f"/agents/{agent_id}/rtc"), ("rtc_control_p50", f"/agents/{agent_id}/rtc?page=50"), ("blockers", "/blockers")):
counter["n"] = 0
t0 = time.perf_counter()
r = pc.get(url)
pages[name] = {"status": r.status_code, "seconds": round(time.perf_counter() - t0, 3), "html_kb": round(len(r.content) / 1024, 1)}
report["panel_pages"] = pages
Path(args.out).write_text(json.dumps(report, indent=1), encoding="utf-8")
print(json.dumps(report))
if __name__ == "__main__":
main()
+2 -2
View File
@@ -14,11 +14,11 @@ sys.path.insert(0, str(ROOT))
def test_versions_are_coherent_across_hub_agent_and_packages():
from central.app.version import GTI_FISCAL_VERSION
v = (ROOT / "release" / "VERSION").read_text(encoding="utf-8").strip()
assert v == GTI_FISCAL_VERSION == "1.1.9"
assert v == GTI_FISCAL_VERSION == "1.2.0"
pep = v.replace("-rc", "rc")
for pp in ("central/pyproject.toml", "agent/pyproject.toml"):
assert f'version = "{pep}"' in (ROOT / pp).read_text(encoding="utf-8")
assert '"AGENT_VERSION", "1.1.9"' in (ROOT / "agent" / "scripts" / "run_agent_service.py").read_text(encoding="utf-8")
assert '"AGENT_VERSION", "1.2.0"' in (ROOT / "agent" / "scripts" / "run_agent_service.py").read_text(encoding="utf-8")
manifest = ROOT / "release" / "RELEASE_MANIFEST.json"
if manifest.exists():
m = json.loads(manifest.read_text(encoding="utf-8"))