feat: deterministic cClassTrib resolver + gated CCLASSTRIB auto-apply (default OFF), RTC panel summary; 1.1.9
- resolver (rules RTC-R0..R4/A1..A3/G1..G2) over the official cClassTrib table 2026-06-22 and LC 214/2025 annexes; local vs official catalog comparison - ONE root-cause RTC blocker per product (missing/unresolved/ambiguous/conflict/official mismatch/catalog outdated/evidence missing) - auto-apply: C000025.CCLASSTRIB only, three switches (FIREBIRD_WRITES + RTC_AUTO_APPLY_ENABLED + Hub authorization), per-product re-read and re-decision, never overwrites, rowcount 1, audit table rtc_write_audits (migration 0027) - copy-only write E2E (280/280 applied, only CCLASSTRIB changed, 2nd run 0 writes, stale decision 0 writes); real databases untouched - docs: cclasstrib_resolution, rtc_auto_apply, unified_fiscal_completeness Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -45,3 +45,4 @@ scripts/_*
|
||||
*.pkl
|
||||
.pytmp/
|
||||
sources_cache/legal/
|
||||
scratch_rtc_e2e/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "gti-fiscal-agent"
|
||||
version = "1.1.8"
|
||||
version = "1.1.9"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = ["httpx>=0.27,<1"]
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ sys.path.insert(0, str(AGENT_ROOT / "src"))
|
||||
sys.path.insert(0, str(PROJECT_ROOT)) # ships central/app engine modules read-only (central.app.*)
|
||||
|
||||
from gti_fiscal_agent.client import HubClient # noqa: E402
|
||||
from gti_fiscal_agent import firebird_reader as _firebird_reader, unified_fiscal as _unified # noqa: E402
|
||||
from gti_fiscal_agent import firebird_reader as _firebird_reader, rtc_auto_apply as _rtc_apply, unified_fiscal as _unified # noqa: E402
|
||||
from gti_fiscal_agent.firebird_reader import check_connection, read_products # noqa: E402
|
||||
from gti_fiscal_agent.pipeline import run_pipeline # noqa: E402
|
||||
from gti_fiscal_agent.runner import REFERENCE_SCHEMA_V1, product_fingerprint, to_hub_product # noqa: E402
|
||||
@@ -104,7 +104,10 @@ class Config:
|
||||
# (A legacy AGENT_JOBS_ENABLED line in an old agent.env is deliberately ignored.)
|
||||
self.agent_jobs_enabled = True
|
||||
self.poll_interval_seconds = float(env.get("POLL_INTERVAL_SECONDS", "5"))
|
||||
self.agent_version = env.get("AGENT_VERSION", "1.1.8")
|
||||
# RTC auto-apply is a SEPARATE switch (default OFF): FIREBIRD_WRITES alone never lets the Agent fill cClassTrib.
|
||||
self.rtc_auto_apply_enabled = env.get("RTC_AUTO_APPLY_ENABLED", "0").strip() == "1"
|
||||
self.rtc_auto_apply_max_per_job = int(env.get("RTC_AUTO_APPLY_MAX_PER_JOB", str(_rtc_apply.DEFAULT_MAX_PER_JOB)))
|
||||
self.agent_version = env.get("AGENT_VERSION", "1.1.9")
|
||||
self.schema_fingerprint = env.get("SCHEMA_FINGERPRINT", REFERENCE_SCHEMA_V1)
|
||||
self.state_db = env.get("AGENT_STATE_DB", str(AGENT_ROOT / "agent_state.sqlite"))
|
||||
# Pilot write authorization: both must be explicitly set by the installer for
|
||||
@@ -187,12 +190,12 @@ def open_write_connection(env, database_path, firebird_writes=True):
|
||||
)
|
||||
|
||||
|
||||
def _unified_fiscal(env, company):
|
||||
def _unified_fiscal(env, company, ncm_valid=None):
|
||||
"""PIS/COFINS applicability + RTC (only when the ERP has the reform on), read-only (SELECT only). Fail-closed: if it cannot be assessed the job fails
|
||||
instead of reporting a coverage that ignores these fields."""
|
||||
connection = _firebird_reader._connect(env)
|
||||
try:
|
||||
return _unified.assess_from_connection(connection, company)
|
||||
return _unified.assess_from_connection(connection, company, ncm_valid=ncm_valid)
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
@@ -210,6 +213,28 @@ def _fiscal_blocked_products(products, findings):
|
||||
return out
|
||||
|
||||
|
||||
def _rtc_auto_apply(env, config, job, products, fiscal, company):
|
||||
"""Report (always) and, only when all three gates are open, fill CCLASSTRIB for products whose class is PROVEN by a deterministic rule.
|
||||
Returns the summary stored in the job result; never raises for a single product."""
|
||||
gate = _rtc_apply.write_gate(firebird_writes=bool(getattr(config, "firebird_writes", False)), agent_enabled=bool(getattr(config, "rtc_auto_apply_enabled", False)),
|
||||
hub_authorized=bool((job.get("authorizations") or {}).get("rtc_auto_apply")))
|
||||
limit = int(getattr(config, "rtc_auto_apply_max_per_job", _rtc_apply.DEFAULT_MAX_PER_JOB))
|
||||
candidates, ready_total = _rtc_apply.select_candidates(fiscal["findings"], products, limit=limit)
|
||||
summary = {"gate": gate, "mode": "AUTO_APPLY" if gate["allowed"] else "REPORT_ONLY", "ready_total": ready_total, "max_per_job": limit, "write_attempts": 0, "write_successes": 0, "applied": [], "aborted": [], "aborted_reasons": {},
|
||||
"allowed_fields": list(_rtc_apply.ALLOWED_RTC_WRITE_FIELDS)}
|
||||
if not (gate["allowed"] and candidates and fiscal["context"].get("rtc_enabled")):
|
||||
return summary
|
||||
from datetime import datetime, timezone
|
||||
connection = open_write_connection(env, config.database_path)
|
||||
try:
|
||||
outcome = _rtc_apply.apply_candidates(connection, candidates, company=company or {}, decision={"local_catalog_hash": fiscal["context"].get("local_catalog_hash")},
|
||||
now=datetime.now(timezone.utc).isoformat(timespec="seconds"), job_id=job.get("job_id"))
|
||||
finally:
|
||||
connection.close()
|
||||
summary.update({k: outcome[k] for k in ("applied", "aborted", "aborted_reasons", "write_attempts", "write_successes")})
|
||||
return summary
|
||||
|
||||
|
||||
def execute_job(job, config, executor, state, env):
|
||||
"""Run the pipeline for one claimed job. Never raises for job-domain
|
||||
failures (bad Firebird read, classify error) — returns (result_summary, error)."""
|
||||
@@ -249,7 +274,7 @@ def execute_job(job, config, executor, state, env):
|
||||
if write_connection is not None:
|
||||
write_connection.close()
|
||||
|
||||
operational = coverage = fiscal_context = None
|
||||
operational = coverage = fiscal_context = rtc_apply = None
|
||||
fiscal = None
|
||||
if sync_kind:
|
||||
if cache:
|
||||
@@ -261,13 +286,17 @@ def execute_job(job, config, executor, state, env):
|
||||
all_decisions = decisions
|
||||
coverage = build_job_coverage(env, client, products, all_decisions)
|
||||
company = {"crt": ((coverage or {}).get("company_context") or {}).get("company_crt")}
|
||||
fiscal = _unified_fiscal(env, company)
|
||||
ncm_ok = {str(p["CODIGO"]): (d or {}).get("status") == "EXACT_NCM" for p, d in zip(products, all_decisions)}
|
||||
fiscal = _unified_fiscal(env, company, ncm_ok)
|
||||
rtc_apply = _rtc_auto_apply(env, config, job, products, fiscal, company)
|
||||
if rtc_apply["write_successes"]:
|
||||
fiscal = _unified_fiscal(env, company, ncm_ok) # coverage must reflect the fields that were just filled
|
||||
findings = fiscal["findings"]
|
||||
product_states = [classify_product_state(p, d, fiscal=findings.get(str(p["CODIGO"]))) for p, d in zip(products, all_decisions)]
|
||||
operational = summarize_operational_states(product_states)
|
||||
fiscal_context = {**fiscal["context"], "evaluated_all": kind != "REPROCESS_PRODUCTS"}
|
||||
else:
|
||||
fiscal = _unified_fiscal(env, None)
|
||||
fiscal = _unified_fiscal(env, None, {str(p["CODIGO"]): (d or {}).get("status") == "EXACT_NCM" for p, d in zip(to_process, decisions)})
|
||||
fiscal_blocked = _fiscal_blocked_products(products, fiscal["findings"])
|
||||
fiscal_codes = {b["product_code"] for b in fiscal_blocked}
|
||||
summary["blocked_products"] = summary["blocked_products"] + fiscal_blocked
|
||||
@@ -276,8 +305,9 @@ def execute_job(job, config, executor, state, env):
|
||||
"coverage": coverage,
|
||||
"operational": operational,
|
||||
"fiscal_context": fiscal_context,
|
||||
"rtc_auto_apply": rtc_apply,
|
||||
"fiscal_mode": getattr(config, "fiscal_mode", None),
|
||||
"release_version": env.get("AGENT_VERSION") or "1.1.8",
|
||||
"release_version": env.get("AGENT_VERSION") or "1.1.9",
|
||||
"products_total": len(products),
|
||||
"products_changed": len(to_process),
|
||||
"products_processed": summary["processed"],
|
||||
@@ -365,7 +395,7 @@ def run(config, env, *, max_iterations=None, stopper=None):
|
||||
"agent_version": config.agent_version, "last_job_id": last_job_id, "display_name": env.get("AGENT_DISPLAY_NAME", ""),
|
||||
"customer_code": env.get("CUSTOMER_CODE"), "hostname": env.get("AGENT_HOSTNAME") or os.environ.get("COMPUTERNAME"),
|
||||
"windows_version": env.get("WINDOWS_VERSION"), "company_code": config.company_id, "company_uf": config.company_uf,
|
||||
"database_path": config.database_path, "capabilities": {"heartbeat": True, "jobs": True, "local_logs": True},
|
||||
"database_path": config.database_path, "capabilities": {"heartbeat": True, "jobs": True, "local_logs": True, "rtc_auto_apply": True, "rtc_auto_apply_enabled": bool(getattr(config, "rtc_auto_apply_enabled", False))},
|
||||
"fiscal_engine_version": env.get("FISCAL_ENGINE_VERSION", config.agent_version), "dataset_versions": {},
|
||||
}
|
||||
if hasattr(client, "heartbeat_metadata"):
|
||||
@@ -435,7 +465,7 @@ def run(config, env, *, max_iterations=None, stopper=None):
|
||||
def enforce_startup_preflight(env):
|
||||
"""Full read-only preflight at startup (Hub, auth, Firebird, capabilities, company context, guards). Fail closed."""
|
||||
from gti_fiscal_agent.preflight import run_preflight
|
||||
report = run_preflight(env, agent_version=env.get("AGENT_VERSION") or "1.1.8")
|
||||
report = run_preflight(env, agent_version=env.get("AGENT_VERSION") or "1.1.9")
|
||||
logger.info("startup preflight: %s", json.dumps({k: report[k] for k in ("PREFLIGHT", "blockers", "fiscal_mode", "effective_firebird_writes")}, ensure_ascii=False))
|
||||
print(json.dumps({"PREFLIGHT": report["PREFLIGHT"], "blockers": report["blockers"]}, ensure_ascii=False))
|
||||
if report["PREFLIGHT"] != "PASS":
|
||||
|
||||
@@ -9,7 +9,7 @@ import os
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
AGENT_VERSION_DEFAULT = "1.1.8"
|
||||
AGENT_VERSION_DEFAULT = "1.1.9"
|
||||
REQUIRED_ENV = ("GTI_HUB_URL", "AGENT_TOKEN", "FIREBIRD_DATABASE_PATH", "FB_USER", "FB_PASSWORD")
|
||||
REQUIRED_CAPABILITIES = ("supports_ncm", "supports_ipi", "supports_cest")
|
||||
FISCAL_MODES = ("DRY_RUN", "AUTO_APPLY")
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
"""RTC auto-apply: fills C000025.CCLASSTRIB, and NOTHING else, when a deterministic rule proves the class.
|
||||
|
||||
Safety model (every gate must hold, default is OFF everywhere):
|
||||
FIREBIRD_WRITES effective (FISCAL_MODE=AUTO_APPLY + path/schema authorization) AND RTC_AUTO_APPLY_ENABLED (this Agent) AND Hub authorization (per installation)
|
||||
Write allowlist: ALLOWED_RTC_WRITE_FIELDS = ["CCLASSTRIB"]. One short transaction per product. Immediately before the UPDATE the product is re-read and
|
||||
the whole decision is re-derived from fresh data; if ANYTHING changed (RTC switch, catalog, NCM, current value, decision) that product is ABORTED.
|
||||
An existing value is never overwritten. Every applied change returns an audit record with the old value, so it can be reverted manually and auditably.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import date
|
||||
|
||||
from central.app.cclasstrib_resolver import compare_catalogs, local_catalog_hash, resolve_cclasstrib
|
||||
from central.app.unified_fiscal_coverage import rtc_enabled
|
||||
|
||||
from .unified_fiscal import detect_rtc_capabilities, load_rtc_context
|
||||
|
||||
ALLOWED_RTC_WRITE_FIELDS = ("CCLASSTRIB",)
|
||||
DEFAULT_MAX_PER_JOB = 500
|
||||
|
||||
|
||||
class RtcApplyAborted(Exception):
|
||||
"""The product is left untouched; `reason` is reported in the audit."""
|
||||
|
||||
def __init__(self, reason: str):
|
||||
super().__init__(reason)
|
||||
self.reason = reason
|
||||
|
||||
|
||||
def _text(value) -> str:
|
||||
return "" if value is None else str(value).strip()
|
||||
|
||||
|
||||
def _digits(value) -> str:
|
||||
return re.sub(r"\D", "", _text(value))
|
||||
|
||||
|
||||
def write_gate(*, firebird_writes: bool, agent_enabled: bool, hub_authorized: bool) -> dict:
|
||||
"""{"allowed": bool, "missing": [...]}: writes are possible only when all three switches are on."""
|
||||
missing = [name for name, on in (("FIREBIRD_WRITES", firebird_writes), ("RTC_AUTO_APPLY_ENABLED", agent_enabled), ("HUB_AUTHORIZATION", hub_authorized)) if not on]
|
||||
return {"allowed": not missing, "missing": missing, "firebird_writes": bool(firebird_writes), "agent_enabled": bool(agent_enabled), "hub_authorized": bool(hub_authorized)}
|
||||
|
||||
|
||||
def select_candidates(findings: dict, products: list[dict], *, limit: int = DEFAULT_MAX_PER_JOB) -> tuple[list[dict], int]:
|
||||
"""Products READY_TO_APPLY: empty CCLASSTRIB + a deterministic resolution, no other RTC problem. Returns (candidates up to `limit`, total ready)."""
|
||||
by_code = {str(p.get("CODIGO") or ""): p for p in products}
|
||||
ready = []
|
||||
for code, found in findings.items():
|
||||
res = found.get("rtc_resolution") or {}
|
||||
if not res.get("auto_apply_candidate"):
|
||||
continue
|
||||
rtc_types = {b["type"] for b in found.get("blockers", []) if b["category"] == "RTC"}
|
||||
if rtc_types != {"CCLASSTRIB_MISSING"}:
|
||||
continue
|
||||
p = by_code.get(code, {})
|
||||
if _text(p.get("CCLASSTRIB")):
|
||||
continue
|
||||
ready.append({"product_code": code, "proposed": res["resolved_cclasstrib"], "cst": res["resolved_cst_ibscbs"], "rule_id": res["rule_id"], "ncm": _digits(p.get("CLASSIFICACAO_FISCAL")), "confidence_type": res["confidence_type"]})
|
||||
return ready[:limit], len(ready)
|
||||
|
||||
|
||||
def apply_candidate(conn, candidate: dict, *, company: dict, decision: dict, now: str, job_id=None, today: date | None = None) -> dict:
|
||||
"""One product, one short transaction. `decision` = {"local_catalog_hash": ...} captured when the candidate was chosen. Raises RtcApplyAborted (nothing written)."""
|
||||
field = "CCLASSTRIB"
|
||||
assert field in ALLOWED_RTC_WRITE_FIELDS
|
||||
today = today or date.today()
|
||||
code, proposed = candidate["product_code"], candidate["proposed"]
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
schema, catalog, extras = load_rtc_context(cur) # fresh: the ERP is asked again, nothing cached
|
||||
caps = detect_rtc_capabilities(schema, param_value=extras["param_value"], catalog=catalog, physical_fk=extras["physical_fk"])
|
||||
if extras["param_conflict"] or not rtc_enabled(caps):
|
||||
raise RtcApplyAborted("RTC_NO_LONGER_ENABLED")
|
||||
if not caps["RTC_CATALOG_VALID"]:
|
||||
raise RtcApplyAborted("RTC_CATALOG_INVALID")
|
||||
local_rows = (catalog or {}).get("rows") or {}
|
||||
fresh_hash = local_catalog_hash(local_rows)
|
||||
if decision.get("local_catalog_hash") and fresh_hash != decision["local_catalog_hash"]:
|
||||
raise RtcApplyAborted("LOCAL_CATALOG_CHANGED_SINCE_DECISION")
|
||||
cur.execute(f"SELECT {field}, CLASSIFICACAO_FISCAL FROM C000025 WHERE CODIGO = ?", (code,))
|
||||
rows = cur.fetchall()
|
||||
if len(rows) != 1:
|
||||
raise RtcApplyAborted("PRODUCT_ROW_NOT_UNIQUE" if rows else "PRODUCT_NOT_FOUND")
|
||||
current, ncm = _text(rows[0][0]), _digits(rows[0][1])
|
||||
if current:
|
||||
raise RtcApplyAborted("VALUE_ALREADY_SET_NEVER_OVERWRITTEN" if current != proposed else "NO_WRITE_REQUIRED")
|
||||
if ncm != candidate["ncm"]:
|
||||
raise RtcApplyAborted("PRODUCT_CHANGED_NCM")
|
||||
compat = compare_catalogs(local_rows, today)
|
||||
resolution = resolve_cclasstrib({"CODIGO": code, "CLASSIFICACAO_FISCAL": ncm, "ncm_valid": True}, company, {"rtc_enabled": True, "local_catalog_valid": True}, local_rows, compat=compat, today=today)
|
||||
if not resolution.auto_apply_candidate or resolution.resolved_cclasstrib != proposed or resolution.rule_id != candidate["rule_id"]:
|
||||
raise RtcApplyAborted("DECISION_CHANGED")
|
||||
cur.execute(f"UPDATE C000025 SET {field} = ? WHERE CODIGO = ? AND ({field} IS NULL OR TRIM({field}) = '')", (proposed, code))
|
||||
if cur.rowcount != 1:
|
||||
conn.rollback()
|
||||
raise RtcApplyAborted("UPDATE_ROWCOUNT_NOT_ONE")
|
||||
conn.commit()
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"SELECT {field} FROM C000025 WHERE CODIGO = ?", (code,))
|
||||
after = _text(cur.fetchone()[0])
|
||||
conn.commit()
|
||||
if after != proposed:
|
||||
raise RuntimeError(f"post-write verification failed for {code}: {after!r} != {proposed!r}")
|
||||
official = compat["official_catalog"]
|
||||
return {"outcome": "APPLIED", "product_code": code, "field": field, "old_value": None, "new_value": proposed, "cst_ibscbs": resolution.resolved_cst_ibscbs, "rule_id": resolution.rule_id,
|
||||
"confidence_type": resolution.confidence_type, "evidence": {"inputs_used": resolution.inputs_used, "evidence": resolution.evidence[:5], "reason": resolution.reason},
|
||||
"official_source_version": {"cclasstrib_table": official["publication"], "cclasstrib_sha256": official["sha256"], "lc214_sha256": resolution.source_references[0]["sha256"]},
|
||||
"local_catalog_hash": fresh_hash, "timestamp": now, "job_id": job_id, "rowcount": 1, "verified_after": True}
|
||||
except RtcApplyAborted:
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
raise
|
||||
except Exception:
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
raise
|
||||
|
||||
|
||||
def apply_candidates(conn, candidates: list[dict], *, company: dict, decision: dict, now: str, job_id=None, today: date | None = None) -> dict:
|
||||
"""Applies each candidate independently; a failure or an abort never stops the rest and never leaves a partial change."""
|
||||
applied, aborted, reasons = [], [], {}
|
||||
for candidate in candidates:
|
||||
try:
|
||||
applied.append(apply_candidate(conn, candidate, company=company, decision=decision, now=now, job_id=job_id, today=today))
|
||||
except RtcApplyAborted as stop:
|
||||
aborted.append({"outcome": "ABORTED", "product_code": candidate["product_code"], "field": "CCLASSTRIB", "reason": stop.reason, "rule_id": candidate["rule_id"], "job_id": job_id, "timestamp": now})
|
||||
reasons[stop.reason] = reasons.get(stop.reason, 0) + 1
|
||||
except Exception as error: # noqa: BLE001 - technical failure of ONE product must not abort the batch
|
||||
aborted.append({"outcome": "FAILED", "product_code": candidate["product_code"], "field": "CCLASSTRIB", "reason": f"{type(error).__name__}", "rule_id": candidate["rule_id"], "job_id": job_id, "timestamp": now})
|
||||
reasons["TECHNICAL_FAILURE"] = reasons.get("TECHNICAL_FAILURE", 0) + 1
|
||||
return {"applied": applied, "aborted": aborted, "aborted_reasons": reasons, "write_attempts": len(candidates), "write_successes": len(applied)}
|
||||
@@ -11,12 +11,14 @@ import json
|
||||
from collections import Counter
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
from central.app.cclasstrib_resolver import compare_catalogs, local_catalog_hash, resolve_cclasstrib
|
||||
from central.app.rtc_findings import refine_rtc_finding, summarize_rtc
|
||||
from central.app.unified_fiscal_coverage import PIS_COFINS_FIELDS, catalog_integrity, evaluate_product, legal_effective_context, rtc_enabled
|
||||
|
||||
PRODUCT_TABLE = "C000025"
|
||||
RTC_CCLASSTRIB_TABLE = "RTC_CCLASSTRIB"
|
||||
RTC_CST_TABLE = "RTC_CST"
|
||||
PRODUCT_COLUMNS = ("CODIGO", "CCLASSTRIB") + PIS_COFINS_FIELDS + ("PISCOFINS", "INCIDENCIA_PISCOFINS")
|
||||
PRODUCT_COLUMNS = ("CODIGO", "CCLASSTRIB", "CLASSIFICACAO_FISCAL") + PIS_COFINS_FIELDS + ("PISCOFINS", "INCIDENCIA_PISCOFINS")
|
||||
TRUTHY = {"1", "S", "T", "TRUE"}
|
||||
|
||||
|
||||
@@ -67,13 +69,23 @@ def load_rtc_context(cursor) -> tuple[dict, dict | None, dict]:
|
||||
class_cols = schema.get(RTC_CCLASSTRIB_TABLE, set())
|
||||
if {"CCLASSTRIB", "CSTIBSCBS"} <= class_cols and "CSTIBSCBS" in schema.get(RTC_CST_TABLE, set()):
|
||||
validity = "INICIO_VIGENCIA" in class_cols and "FIM_VIGENCIA" in class_cols
|
||||
cursor.execute("SELECT TRIM(CCLASSTRIB), TRIM(CSTIBSCBS)" + (", INICIO_VIGENCIA, FIM_VIGENCIA" if validity else "") + " FROM RTC_CCLASSTRIB")
|
||||
extra = [c for c in ("TIPO_ALIQUOTA", "PRED_IBS", "PRED_CBS", "IND_NFE", "IND_NFCE") if c in class_cols]
|
||||
cursor.execute("SELECT TRIM(CCLASSTRIB), TRIM(CSTIBSCBS)" + (", INICIO_VIGENCIA, FIM_VIGENCIA" if validity else "") + "".join(f", {c}" for c in extra) + " FROM RTC_CCLASSTRIB")
|
||||
rows = cursor.fetchall()
|
||||
classes = {r[0]: (r[1] or None) for r in rows}
|
||||
cursor.execute("SELECT TRIM(CSTIBSCBS) FROM RTC_CST")
|
||||
catalog = {"classes": classes, "cst": {r[0] for r in cursor.fetchall()}, "duplicates": len(rows) - len(classes)}
|
||||
if validity:
|
||||
catalog["validity"] = {r[0]: (r[2], r[3]) for r in rows}
|
||||
offset = 4 if validity else 2
|
||||
catalog["rows"] = {r[0]: {"cst": r[1], "start": (r[2] if validity else None), "end": (r[3] if validity else None), **{c: r[offset + i] for i, c in enumerate(extra)}} for r in rows}
|
||||
for value in catalog["rows"].values():
|
||||
for key in ("TIPO_ALIQUOTA", "PRED_IBS", "PRED_CBS", "IND_NFE", "IND_NFCE"):
|
||||
if key in value:
|
||||
value[key.lower()] = value.pop(key)
|
||||
for flag in ("ind_nfe", "ind_nfce"):
|
||||
if flag in value:
|
||||
value[flag] = str(value[flag]).strip() in ("1", "True", "true")
|
||||
return schema, catalog, extras
|
||||
|
||||
|
||||
@@ -87,9 +99,10 @@ def read_products(cursor, schema: dict) -> list[dict]:
|
||||
return [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||
|
||||
|
||||
def assess_from_connection(conn, company: dict | None, *, today: date | None = None, rtc_override: bool | None = None) -> dict:
|
||||
def assess_from_connection(conn, company: dict | None, *, today: date | None = None, rtc_override: bool | None = None, ncm_valid: dict | None = None) -> dict:
|
||||
"""One read-only pass. Returns {"context": {...}, "findings": {product_code: evaluate_product(...)}}.
|
||||
rtc_override forces RTC on/off for simulations WITHOUT touching the ERP parameter (tests / what-if only)."""
|
||||
rtc_override forces RTC on/off for simulations WITHOUT touching the ERP parameter (tests / what-if only).
|
||||
ncm_valid: {product_code: bool} from the decision engine (EXACT_NCM); a product whose NCM is not confirmed never gets a cClassTrib proposal."""
|
||||
cursor = conn.cursor()
|
||||
schema, catalog, extras = load_rtc_context(cursor)
|
||||
caps = detect_rtc_capabilities(schema, param_value=extras["param_value"], catalog=catalog, physical_fk=extras["physical_fk"])
|
||||
@@ -102,8 +115,15 @@ def assess_from_connection(conn, company: dict | None, *, today: date | None = N
|
||||
warn_products: set = set()
|
||||
samples: list = []
|
||||
applicability: Counter = Counter()
|
||||
local_rows = (catalog or {}).get("rows") or {}
|
||||
compat = compare_catalogs(local_rows, today) if (enabled and local_rows) else None
|
||||
resolver_ctx = {"rtc_enabled": enabled, "local_catalog_valid": bool(caps["RTC_CATALOG_VALID"])}
|
||||
for p in products:
|
||||
found = evaluate_product(p, company, rtc, today)
|
||||
if enabled:
|
||||
code0 = str(p.get("CODIGO") or "")
|
||||
resolution = resolve_cclasstrib({**p, "ncm_valid": bool((ncm_valid or {}).get(code0, False))}, company, resolver_ctx, local_rows, compat=compat, today=today)
|
||||
refine_rtc_finding(found, p, resolution, compat["per_code"] if compat else None)
|
||||
found["input_fingerprint"] = hashlib.sha256(json.dumps({k: (None if v is None else str(v).strip()) for k, v in p.items()} | {"rtc": enabled}, sort_keys=True).encode()).hexdigest()
|
||||
code = str(p.get("CODIGO") or "")
|
||||
findings[code] = found
|
||||
@@ -119,5 +139,7 @@ def assess_from_connection(conn, company: dict | None, *, today: date | None = N
|
||||
"rtc_observed_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), "legal_effective_context": legal_effective_context((company or {}).get("crt")),
|
||||
"catalog_integrity": integrity, "crt": (company or {}).get("crt"), "capabilities": caps, "products_evaluated": len(products),
|
||||
"pis_cofins_applicability": dict(applicability),
|
||||
"rtc_resolution": summarize_rtc(findings, compat) if enabled else None, "local_catalog_hash": local_catalog_hash(local_rows) if local_rows else None,
|
||||
"official_catalog": (compat or {}).get("official_catalog"),
|
||||
"non_canonical_warnings": {"products": len(warn_products), "by_type": dict(sorted(warn_by_type.items())), "samples": samples}},
|
||||
"findings": findings}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
import re
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
|
||||
from central.app import rtc_reference_data as ref
|
||||
from central.app.cclasstrib_resolver import local_catalog_hash
|
||||
from gti_fiscal_agent import rtc_auto_apply as apply_mod
|
||||
from gti_fiscal_agent import unified_fiscal as uf
|
||||
|
||||
pytestmark = pytest.mark.real_unified_fiscal
|
||||
|
||||
TODAY = date(2026, 9, 24)
|
||||
NORMAL = {"crt": "3"}
|
||||
CLASS_COLS = ("CCLASSTRIB", "CSTIBSCBS", "INICIO_VIGENCIA", "FIM_VIGENCIA", "TIPO_ALIQUOTA", "PRED_IBS", "PRED_CBS", "IND_NFE", "IND_NFCE")
|
||||
SCHEMA = ([("C000025", c) for c in ("CODIGO", "CCLASSTRIB", "CLASSIFICACAO_FISCAL", "PISCOFINS", "INCIDENCIA_PISCOFINS")] + [("RTC_CCLASSTRIB", c) for c in CLASS_COLS] + [("RTC_CST", "CSTIBSCBS"), ("PARAMETROS", "USAR_REFORMA_TRIBUTARIA")])
|
||||
|
||||
|
||||
class FakeFirebird:
|
||||
"""A tiny SGV: PARAMETROS, the RTC catalog (from the official table) and C000025. Every statement is recorded; only SELECT and the single allowed UPDATE are understood."""
|
||||
|
||||
def __init__(self, products, param=(1,), catalog_tweak=None, skip=()):
|
||||
self.products = {c: dict(v) for c, v in products.items()} # code -> {"CCLASSTRIB":..., "CLASSIFICACAO_FISCAL":...}
|
||||
self.param = list(param)
|
||||
self.catalog = {r["code"]: {"cst": r["cst"], "start": r["start"], "end": r["end"], "tipo": r["tipo_aliquota"], "pred_ibs": r["p_red_ibs"], "pred_cbs": r["p_red_cbs"], "ind_nfe": r["ind_nfe"], "ind_nfce": r["ind_nfce"]}
|
||||
for r in ref.OFFICIAL_CCLASSTRIB if r["code"] not in skip and not r["end"]}
|
||||
for code, change in (catalog_tweak or {}).items():
|
||||
self.catalog[code] = {**self.catalog[code], **change}
|
||||
self.cst_table = {r["cst"] for r in ref.OFFICIAL_CCLASSTRIB} # RTC_CST is its own table: a class may point to a CST that is not in it
|
||||
self.statements, self.commits, self.rollbacks = [], 0, 0
|
||||
self._pending = {}
|
||||
|
||||
def cursor(self):
|
||||
return FakeCursor(self)
|
||||
|
||||
def commit(self):
|
||||
for code, value in self._pending.items():
|
||||
self.products[code]["CCLASSTRIB"] = value
|
||||
self._pending = {}
|
||||
self.commits += 1
|
||||
|
||||
def rollback(self):
|
||||
self._pending = {}
|
||||
self.rollbacks += 1
|
||||
|
||||
|
||||
class FakeCursor:
|
||||
def __init__(self, db):
|
||||
self.db, self._rows, self.rowcount = db, [], -1
|
||||
|
||||
def execute(self, sql, params=()):
|
||||
db = self.db
|
||||
db.statements.append((sql, tuple(params)))
|
||||
text = " ".join(sql.split())
|
||||
up = text.upper()
|
||||
if up.startswith("SELECT TRIM(RF.RDB$RELATION_NAME)"):
|
||||
self._rows = list(SCHEMA)
|
||||
elif up.startswith("SELECT FIRST 20 USAR_REFORMA_TRIBUTARIA"):
|
||||
self._rows = [(v,) for v in db.param]
|
||||
elif "RDB$RELATION_CONSTRAINTS" in up:
|
||||
self._rows = [(1,)]
|
||||
elif up.startswith("SELECT TRIM(CCLASSTRIB)") and "FROM RTC_CCLASSTRIB" in up:
|
||||
cols = [c.strip() for c in text[len("SELECT "):up.index(" FROM ")].split(",")]
|
||||
rows = []
|
||||
for code, r in db.catalog.items():
|
||||
mapping = {"CSTIBSCBS": r["cst"], "INICIO_VIGENCIA": date.fromisoformat(r["start"]) if r["start"] else None, "FIM_VIGENCIA": None, "TIPO_ALIQUOTA": r["tipo"], "PRED_IBS": r["pred_ibs"],
|
||||
"PRED_CBS": r["pred_cbs"], "IND_NFE": "1" if r["ind_nfe"] else "0", "IND_NFCE": "1" if r["ind_nfce"] else "0"}
|
||||
rows.append(tuple(code if c.startswith("TRIM(CCLASSTRIB") else mapping[re.sub(r"TRIM\((\w+)\)", r"\1", c)] for c in cols))
|
||||
self._rows = rows
|
||||
elif up.startswith("SELECT TRIM(CSTIBSCBS) FROM RTC_CST"):
|
||||
self._rows = [(c,) for c in sorted(db.cst_table)]
|
||||
elif up.startswith("SELECT CODIGO, CCLASSTRIB, CLASSIFICACAO_FISCAL"):
|
||||
self._rows = [(c, v["CCLASSTRIB"], v["CLASSIFICACAO_FISCAL"], "N", "NENHUMA") for c, v in sorted(db.products.items())]
|
||||
elif up.startswith("SELECT CCLASSTRIB, CLASSIFICACAO_FISCAL FROM C000025"):
|
||||
code = params[0]
|
||||
self._rows = [(db.products[code]["CCLASSTRIB"], db.products[code]["CLASSIFICACAO_FISCAL"])] if code in db.products else []
|
||||
if code in db.products and db.products[code].get("DUP"):
|
||||
self._rows = self._rows * 2
|
||||
elif up.startswith("SELECT CCLASSTRIB FROM C000025"):
|
||||
code = params[0]
|
||||
self._rows = [(db.products[code]["CCLASSTRIB"],)]
|
||||
elif up.startswith("UPDATE C000025 SET CCLASSTRIB = ? WHERE CODIGO = ?"):
|
||||
value, code = params
|
||||
product = db.products.get(code)
|
||||
hit = product is not None and not (product["CCLASSTRIB"] or "").strip()
|
||||
self.rowcount = (2 if product and product.get("DUP") else 1) if hit else 0
|
||||
if hit and self.rowcount == 1:
|
||||
db._pending[code] = value
|
||||
else:
|
||||
raise AssertionError(f"unexpected statement: {sql}")
|
||||
|
||||
def fetchall(self):
|
||||
return self._rows
|
||||
|
||||
def fetchone(self):
|
||||
return self._rows[0] if self._rows else None
|
||||
|
||||
|
||||
def product(ncm, value=None, **extra):
|
||||
return {"CCLASSTRIB": value, "CLASSIFICACAO_FISCAL": ncm, **extra}
|
||||
|
||||
|
||||
def assess(db, ncm_ok=None, company=NORMAL):
|
||||
return uf.assess_from_connection(db, company, today=TODAY, ncm_valid=ncm_ok if ncm_ok is not None else {c: True for c in db.products})
|
||||
|
||||
|
||||
def candidates_of(db, limit=500):
|
||||
out = assess(db)
|
||||
products = [{"CODIGO": c, "CCLASSTRIB": v["CCLASSTRIB"], "CLASSIFICACAO_FISCAL": v["CLASSIFICACAO_FISCAL"]} for c, v in db.products.items()]
|
||||
cands, total = apply_mod.select_candidates(out["findings"], products, limit=limit)
|
||||
return out, cands, total
|
||||
|
||||
|
||||
def run(db, cands, out, **kw):
|
||||
return apply_mod.apply_candidates(db, cands, company=NORMAL, decision={"local_catalog_hash": out["context"]["local_catalog_hash"]}, now="2026-09-24T20:00:00+00:00", job_id=7, today=TODAY, **kw)
|
||||
|
||||
|
||||
PRODUCTS = {"000001": product("10063021"), "000002": product("39235000"), "000003": product("15171000"), "000004": product("30049099"), "000005": product("39235000", "000001")}
|
||||
|
||||
|
||||
def test_gate_needs_all_three_switches_and_defaults_to_closed():
|
||||
assert apply_mod.write_gate(firebird_writes=True, agent_enabled=True, hub_authorized=True)["allowed"] is True
|
||||
for combo in ((False, True, True), (True, False, True), (True, True, False), (False, False, False)):
|
||||
g = apply_mod.write_gate(firebird_writes=combo[0], agent_enabled=combo[1], hub_authorized=combo[2])
|
||||
assert g["allowed"] is False and g["missing"]
|
||||
assert apply_mod.ALLOWED_RTC_WRITE_FIELDS == ("CCLASSTRIB",)
|
||||
|
||||
|
||||
def test_only_proven_products_become_candidates():
|
||||
db = FakeFirebird(PRODUCTS)
|
||||
out, cands, total = candidates_of(db)
|
||||
assert sorted((c["product_code"], c["proposed"], c["rule_id"]) for c in cands) == [("000001", "200003", "RTC-R3"), ("000002", "000001", "RTC-R4"), ("000003", "200003", "RTC-R3")]
|
||||
assert total == 3 and out["context"]["rtc_resolution"]["ready_to_apply"] == 3 and out["context"]["rtc_resolution"]["ambiguous"] == 1 # medicine chapter is ambiguous
|
||||
assert out["context"]["rtc_resolution"]["valid_existing"] == 1 # 000005 already has the right class
|
||||
assert "000004" not in {c["product_code"] for c in cands}
|
||||
|
||||
|
||||
def test_apply_writes_only_cclasstrib_with_audit_and_second_run_writes_nothing():
|
||||
db = FakeFirebird(PRODUCTS)
|
||||
out, cands, _ = candidates_of(db)
|
||||
result = run(db, cands, out)
|
||||
assert result["write_successes"] == 3 and result["write_attempts"] == 3 and result["aborted"] == []
|
||||
assert {c: db.products[c]["CCLASSTRIB"] for c in ("000001", "000002", "000003")} == {"000001": "200003", "000002": "000001", "000003": "200003"}
|
||||
updates = [s for s, _ in db.statements if s.upper().startswith("UPDATE")]
|
||||
assert len(updates) == 3 and all(re.fullmatch(r"UPDATE C000025 SET CCLASSTRIB = \? WHERE CODIGO = \? AND \(CCLASSTRIB IS NULL OR TRIM\(CCLASSTRIB\) = ''\)", u) for u in updates)
|
||||
assert not [s for s, _ in db.statements if re.match(r"(INSERT|DELETE|ALTER|DROP)", s.strip(), re.I)]
|
||||
audit = result["applied"][0]
|
||||
assert {"product_code", "field", "old_value", "new_value", "rule_id", "evidence", "official_source_version", "local_catalog_hash", "timestamp", "job_id", "outcome"} <= set(audit)
|
||||
assert audit["old_value"] is None and audit["field"] == "CCLASSTRIB" and audit["official_source_version"]["cclasstrib_sha256"] == ref.OFFICIAL_CATALOG["sha256"] and audit["job_id"] == 7
|
||||
out2, cands2, total2 = candidates_of(db) # idempotent: nothing left to write
|
||||
assert cands2 == [] and total2 == 0 and out2["context"]["rtc_resolution"]["valid_existing"] == 4
|
||||
before = len(db.statements)
|
||||
assert run(db, cands2, out2)["write_attempts"] == 0 and len(db.statements) == before
|
||||
|
||||
|
||||
def test_rtc_switched_off_between_decision_and_write_aborts_every_product():
|
||||
db = FakeFirebird(PRODUCTS)
|
||||
out, cands, _ = candidates_of(db)
|
||||
db.param = [0]
|
||||
result = run(db, cands, out)
|
||||
assert result["write_successes"] == 0 and result["aborted_reasons"] == {"RTC_NO_LONGER_ENABLED": 3} and all(p["CCLASSTRIB"] in (None, "000001") for p in db.products.values())
|
||||
|
||||
|
||||
def test_conflicting_parameter_rows_abort():
|
||||
db = FakeFirebird(PRODUCTS)
|
||||
out, cands, _ = candidates_of(db)
|
||||
db.param = [1, 0]
|
||||
assert run(db, cands, out)["aborted_reasons"] == {"RTC_NO_LONGER_ENABLED": 3}
|
||||
|
||||
|
||||
def test_catalog_changed_or_invalid_between_decision_and_write_aborts():
|
||||
db = FakeFirebird(PRODUCTS)
|
||||
out, cands, _ = candidates_of(db)
|
||||
db.catalog["200003"]["pred_ibs"] = "60"
|
||||
assert run(db, cands, out)["aborted_reasons"] == {"LOCAL_CATALOG_CHANGED_SINCE_DECISION": 3}
|
||||
db2 = FakeFirebird(PRODUCTS)
|
||||
out2, cands2, _ = candidates_of(db2)
|
||||
db2.catalog["999999"] = {**db2.catalog["000001"], "cst": "555"} # class pointing to a CST that does not exist -> invalid catalog
|
||||
invalid = apply_mod.apply_candidates(db2, cands2, company=NORMAL, decision={}, now="2026-09-24T20:00:00+00:00", job_id=7, today=TODAY) # no hash pinned: integrity check alone must stop it
|
||||
assert invalid["aborted_reasons"] == {"RTC_CATALOG_INVALID": 3}
|
||||
|
||||
|
||||
def test_product_changed_between_decision_and_write_is_left_alone():
|
||||
db = FakeFirebird(PRODUCTS)
|
||||
out, cands, _ = candidates_of(db)
|
||||
db.products["000001"]["CLASSIFICACAO_FISCAL"] = "30049099" # NCM edited in the ERP meanwhile
|
||||
db.products["000002"]["CCLASSTRIB"] = "000001" # operator filled it meanwhile (same value)
|
||||
db.products["000003"]["CCLASSTRIB"] = "200034" # ... or with a DIFFERENT value: never overwritten
|
||||
result = run(db, cands, out)
|
||||
assert result["aborted_reasons"] == {"PRODUCT_CHANGED_NCM": 1, "NO_WRITE_REQUIRED": 1, "VALUE_ALREADY_SET_NEVER_OVERWRITTEN": 1} and result["write_successes"] == 0
|
||||
assert db.products["000003"]["CCLASSTRIB"] == "200034" and not [s for s, _ in db.statements if s.upper().startswith("UPDATE")]
|
||||
|
||||
|
||||
def test_duplicate_code_rowcount_protection_and_isolation_of_failures():
|
||||
products = {**PRODUCTS, "000002": product("39235000", DUP=True)}
|
||||
db = FakeFirebird(products)
|
||||
out, cands, _ = candidates_of(db)
|
||||
result = run(db, cands, out)
|
||||
assert result["aborted_reasons"] == {"PRODUCT_ROW_NOT_UNIQUE": 1} and result["write_successes"] == 2 # the ambiguous row is skipped, the rest still applied
|
||||
assert db.products["000002"]["CCLASSTRIB"] is None
|
||||
|
||||
|
||||
def test_existing_different_valid_class_is_a_conflict_blocker_never_a_write():
|
||||
db = FakeFirebird({"000001": product("10063021", "000001")}) # rice already classified as fully taxed: differs from the proven Anexo I class
|
||||
out = assess(db)
|
||||
types = {b["type"] for b in out["findings"]["000001"]["blockers"]}
|
||||
assert types == {"CCLASSTRIB_CONFLICT"} and out["context"]["rtc_resolution"]["conflicts"] == 1
|
||||
_, cands, _ = candidates_of(db)
|
||||
assert cands == []
|
||||
|
||||
|
||||
def test_official_mismatch_of_an_existing_class_is_reported():
|
||||
db = FakeFirebird({"000001": product("39235000", "000001")}, catalog_tweak={"000001": {"ind_nfe": False, "ind_nfce": False}})
|
||||
types = {b["type"] for b in assess(db)["findings"]["000001"]["blockers"]}
|
||||
assert types == {"CCLASSTRIB_OFFICIAL_MISMATCH"}
|
||||
|
||||
|
||||
def test_batch_limit_bounds_the_blast_radius():
|
||||
db = FakeFirebird({f"{i:06d}": product("39235000") for i in range(1, 8)})
|
||||
out, cands, total = candidates_of(db, limit=3)
|
||||
assert len(cands) == 3 and total == 7
|
||||
|
||||
|
||||
def test_simples_never_produces_candidates_even_with_rtc_on():
|
||||
db = FakeFirebird(PRODUCTS)
|
||||
out = uf.assess_from_connection(db, {"crt": "1"}, today=TODAY, ncm_valid={c: True for c in PRODUCTS})
|
||||
assert out["context"]["rtc_resolution"]["ready_to_apply"] == 0
|
||||
kinds = {b["type"] for f in out["findings"].values() for b in f["blockers"]}
|
||||
assert "CCLASSTRIB_MISSING_UNRESOLVED" in kinds and "CCLASSTRIB_MISSING" not in kinds
|
||||
|
||||
|
||||
def test_rtc_off_has_no_resolution_and_no_candidates():
|
||||
db = FakeFirebird(PRODUCTS, param=(0,))
|
||||
out, cands, total = candidates_of(db)
|
||||
assert cands == [] and total == 0 and out["context"]["rtc_resolution"] is None
|
||||
|
||||
|
||||
def test_catalog_hash_is_recorded_for_audit():
|
||||
db = FakeFirebird(PRODUCTS)
|
||||
out = assess(db)
|
||||
schema, catalog, _ = uf.load_rtc_context(db.cursor())
|
||||
assert out["context"]["local_catalog_hash"] == local_catalog_hash(catalog["rows"]) and re.fullmatch(r"[0-9a-f]{64}", out["context"]["local_catalog_hash"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- job integration (gates)
|
||||
def _service():
|
||||
from test_run_agent_service import _config, service
|
||||
return service, _config
|
||||
|
||||
|
||||
def _fiscal_for(db):
|
||||
return assess(db)
|
||||
|
||||
|
||||
def test_job_reports_ready_products_but_writes_nothing_with_default_switches(monkeypatch):
|
||||
service, _config = _service()
|
||||
db = FakeFirebird(PRODUCTS)
|
||||
fiscal = _fiscal_for(db)
|
||||
monkeypatch.setattr(service, "open_write_connection", lambda *a, **k: (_ for _ in ()).throw(AssertionError("no write connection may be opened")))
|
||||
products = [{"CODIGO": c, "CCLASSTRIB": v["CCLASSTRIB"], "CLASSIFICACAO_FISCAL": v["CLASSIFICACAO_FISCAL"]} for c, v in db.products.items()]
|
||||
for cfg_kwargs, auth in (({}, False), ({"firebird_writes": True}, False), ({"firebird_writes": True, "rtc_auto_apply_enabled": True}, False), ({"rtc_auto_apply_enabled": True}, True), ({"firebird_writes": True}, True)):
|
||||
config = _config(**cfg_kwargs)
|
||||
summary = service._rtc_auto_apply({}, config, {"job_id": 1, "authorizations": {"rtc_auto_apply": auth}}, products, fiscal, NORMAL)
|
||||
assert summary["mode"] == "REPORT_ONLY" and summary["gate"]["allowed"] is False and summary["ready_total"] == 3 and summary["write_attempts"] == 0 and summary["applied"] == []
|
||||
|
||||
|
||||
def test_job_applies_only_when_all_three_switches_are_open(monkeypatch):
|
||||
service, _config = _service()
|
||||
db = FakeFirebird(PRODUCTS)
|
||||
fiscal = _fiscal_for(db)
|
||||
monkeypatch.setattr(service, "open_write_connection", lambda env, path: _Closeable(db))
|
||||
products = [{"CODIGO": c, "CCLASSTRIB": v["CCLASSTRIB"], "CLASSIFICACAO_FISCAL": v["CLASSIFICACAO_FISCAL"]} for c, v in db.products.items()]
|
||||
config = _config(firebird_writes=True, rtc_auto_apply_enabled=True)
|
||||
summary = service._rtc_auto_apply({}, config, {"job_id": 9, "authorizations": {"rtc_auto_apply": True}}, products, fiscal, NORMAL)
|
||||
assert summary["mode"] == "AUTO_APPLY" and summary["write_successes"] == 3 and summary["allowed_fields"] == ["CCLASSTRIB"] and all(a["job_id"] == 9 for a in summary["applied"])
|
||||
assert [db.products[c]["CCLASSTRIB"] for c in ("000001", "000002", "000003")] == ["200003", "000001", "200003"]
|
||||
|
||||
|
||||
def test_job_never_writes_when_the_erp_says_rtc_is_off_even_with_all_switches_open(monkeypatch):
|
||||
service, _config = _service()
|
||||
db = FakeFirebird(PRODUCTS, param=(0,))
|
||||
fiscal = _fiscal_for(db)
|
||||
monkeypatch.setattr(service, "open_write_connection", lambda *a, **k: (_ for _ in ()).throw(AssertionError("no write connection may be opened")))
|
||||
products = [{"CODIGO": c, "CCLASSTRIB": None, "CLASSIFICACAO_FISCAL": "39235000"} for c in db.products]
|
||||
summary = service._rtc_auto_apply({}, _config(firebird_writes=True, rtc_auto_apply_enabled=True), {"job_id": 1, "authorizations": {"rtc_auto_apply": True}}, products, fiscal, NORMAL)
|
||||
assert summary["write_attempts"] == 0 and summary["ready_total"] == 0
|
||||
|
||||
|
||||
class _Closeable:
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._db, name)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
@@ -69,14 +69,14 @@ def test_rtc_off_in_the_erp_is_out_of_scope_and_never_blocks():
|
||||
def test_rtc_on_in_the_erp_brings_cclasstrib_into_the_same_findings():
|
||||
out = uf.assess_from_connection(FakeConn(param=1), {"crt": "1"}, today=date(2026, 9, 24))
|
||||
assert out["context"]["rtc_enabled"] is True
|
||||
assert kinds(out["findings"]["1"]) == ["CCLASSTRIB_MISSING"] and kinds(out["findings"]["2"]) == []
|
||||
assert kinds(out["findings"]["3"]) == ["CCLASSTRIB_MISSING"] # one queue, one verdict per product
|
||||
assert kinds(out["findings"]["1"]) == ["CCLASSTRIB_MISSING_UNRESOLVED"] and kinds(out["findings"]["2"]) == []
|
||||
assert kinds(out["findings"]["3"]) == ["CCLASSTRIB_MISSING_UNRESOLVED"] # one queue, one verdict per product
|
||||
|
||||
|
||||
def test_simulation_flag_never_touches_the_erp_parameter():
|
||||
conn = FakeConn(param=0)
|
||||
on = uf.assess_from_connection(conn, {"crt": "1"}, rtc_override=True)
|
||||
assert on["context"]["rtc_enabled"] is True and kinds(on["findings"]["1"]) == ["CCLASSTRIB_MISSING"]
|
||||
assert on["context"]["rtc_enabled"] is True and kinds(on["findings"]["1"]) == ["CCLASSTRIB_MISSING_UNRESOLVED"]
|
||||
assert on["context"]["rtc_param"] == "0" and on["context"]["rtc_source"] == "SIMULATION" and not any("UPDATE" in s.upper() for s in conn.cur.statements)
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ def test_toggle_between_syncs_is_followed_in_both_directions_without_any_cache()
|
||||
assert first["context"]["rtc_enabled"] is False and not [b for f in first["findings"].values() for b in f["blockers"] if b["category"] == "RTC"]
|
||||
conn.cur.param = 1 # the operator switches the reform on in the ERP between two syncs
|
||||
second = uf.assess_from_connection(conn, {"crt": "1"})
|
||||
assert second["context"]["rtc_enabled"] is True and kinds(second["findings"]["1"]) == ["CCLASSTRIB_MISSING"]
|
||||
assert second["context"]["rtc_enabled"] is True and kinds(second["findings"]["1"]) == ["CCLASSTRIB_MISSING_UNRESOLVED"]
|
||||
assert second["findings"]["1"]["input_fingerprint"] != first["findings"]["1"]["input_fingerprint"] # a new blocker fingerprint: the old (RTC off) state is not reused
|
||||
conn.cur.param = 0 # and off again
|
||||
third = uf.assess_from_connection(conn, {"crt": "1"})
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""RTC auto-apply: per-installation authorization (default OFF) + immutable write audit.
|
||||
|
||||
Revision ID: 0027_rtc_auto_apply
|
||||
Revises: 0026_central_processing_control
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0027_rtc_auto_apply"
|
||||
down_revision = "0026_central_processing_control"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column("installations", sa.Column("rtc_auto_apply_authorized", sa.Boolean(), nullable=False, server_default="false"))
|
||||
op.create_table(
|
||||
"rtc_write_audits",
|
||||
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("agent_id", sa.Integer(), sa.ForeignKey("agents.id"), nullable=False, index=True),
|
||||
sa.Column("job_id", sa.Integer(), nullable=True, index=True),
|
||||
sa.Column("client", sa.String(255), nullable=True),
|
||||
sa.Column("product_code", sa.String(64), nullable=False, index=True),
|
||||
sa.Column("field", sa.String(32), nullable=False),
|
||||
sa.Column("outcome", sa.String(16), nullable=False, index=True),
|
||||
sa.Column("reason", sa.String(96), nullable=True),
|
||||
sa.Column("old_value", sa.String(64), nullable=True),
|
||||
sa.Column("new_value", sa.String(64), nullable=True),
|
||||
sa.Column("rule_id", sa.String(32), nullable=True),
|
||||
sa.Column("evidence", sa.JSON(), nullable=False, server_default="{}"),
|
||||
sa.Column("official_source_version", sa.JSON(), nullable=False, server_default="{}"),
|
||||
sa.Column("local_catalog_hash", sa.String(64), nullable=True),
|
||||
sa.Column("written_at", sa.String(40), nullable=True),
|
||||
sa.Column("recorded_at", sa.DateTime(timezone=True), server_default=sa.func.now(), index=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table("rtc_write_audits")
|
||||
op.drop_column("installations", "rtc_auto_apply_authorized")
|
||||
+6
-1
@@ -11,6 +11,7 @@ from .classification import ENGINE_VERSION, Classifier
|
||||
from .config import settings
|
||||
from .datasets.service import DatasetService
|
||||
from .jobs import SYNC_KINDS, claim_next_job, complete_job, create_job, fail_job, schedule_automatic_sync
|
||||
from .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 .context_keys import validate_entry
|
||||
from .models import AgentSnapshot, BlockedProduct, FiscalContextEntry, ClassificationCache, ClassificationRequest, ClassificationResult, Dataset, Installation, Job
|
||||
@@ -393,7 +394,9 @@ def router(session_factory, bootstrap_token):
|
||||
record_event(session, tenant_id=agent.tenant_id, installation_id=installation.id, agent_id=agent.id, severity="INFO", event_type="JOB_STARTED", message=f"Job {job.kind} started", context={"kind": job.kind}, job_id=job.id)
|
||||
session.commit()
|
||||
logger.info("job claimed job_id=%s agent_id=%s kind=%s", job.id, agent.id, job.kind)
|
||||
return _job_view(job)
|
||||
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
|
||||
return view
|
||||
finally: session.close()
|
||||
|
||||
@routes.get("/jobs/global")
|
||||
@@ -427,6 +430,8 @@ def router(session_factory, bootstrap_token):
|
||||
upsert_blocker(session, agent.tenant_id, blocker, installation_id=installation.id if installation else None)
|
||||
for product_code in result_summary.get("resolved_products", []):
|
||||
resolve_product_blockers(session, agent.tenant_id, product_code, installation_id=installation.id if installation else None)
|
||||
if installation is not None:
|
||||
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"))
|
||||
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")
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
"""Deterministic, auditable cClassTrib resolver (pure: no Firebird, no network, no probabilities).
|
||||
|
||||
resolve_cclasstrib(product, company, ctx, local_catalog) -> Resolution
|
||||
|
||||
Nothing here guesses. A class is proposed only when an EXPLICIT rule, backed by the official texts in `rtc_reference_data`
|
||||
(cClassTrib table 2026-06-22 and LC 214/2025 compiled through LC 227/2026), proves it. Everything else is AMBIGUOUS or INSUFFICIENT_EVIDENCE.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date
|
||||
|
||||
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")
|
||||
|
||||
# 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
|
||||
# Chapters where a reduced/special regime is defined by the NATURE of the good (not by a code list): in natura agriculture (art. 137), medicines (arts. 133/146),
|
||||
# fuels and biofuels (art. 172 ff), medical/accessibility devices and vehicles (arts. 131-132, 143-145, 149), books/periodicals (constitutional immunity),
|
||||
# gold and other special-regime goods kept out of the residual rule on purpose (conservative).
|
||||
SENSITIVE_CHAPTERS = frozenset([f"{n:02d}" for n in range(1, 15)] + ["27", "30", "49", "71", "87", "88", "89", "90"]) # 71 gold (financial-asset regime), 88/89 aircraft and vessels
|
||||
SPECIAL_PREFIXES = ("9619", "2207", "3826", "8703") # art. 147 (menstrual health), biofuel inputs, art. 149 (PcD cars)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Resolution:
|
||||
status: str
|
||||
rule_id: str
|
||||
reason: str
|
||||
resolved_cclasstrib: str | None = None
|
||||
resolved_cst_ibscbs: str | None = None
|
||||
confidence_type: str = "NONE"
|
||||
inputs_used: dict = field(default_factory=dict)
|
||||
evidence: list = field(default_factory=list)
|
||||
source_references: list = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def auto_apply_candidate(self) -> bool:
|
||||
return self.status in RESOLVED_STATUSES and bool(self.resolved_cclasstrib)
|
||||
|
||||
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}
|
||||
|
||||
|
||||
def _digits(value) -> str:
|
||||
return re.sub(r"\D", "", "" if value is None else str(value))
|
||||
|
||||
|
||||
def _official_by_code() -> dict:
|
||||
return {row["code"]: row for row in ref.OFFICIAL_CCLASSTRIB}
|
||||
|
||||
|
||||
def _num(value):
|
||||
try:
|
||||
return float(str(value).replace(",", "."))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------- catalog comparison
|
||||
|
||||
|
||||
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)."""
|
||||
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"
|
||||
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"
|
||||
else:
|
||||
problems = []
|
||||
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")
|
||||
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"]}}
|
||||
|
||||
|
||||
def local_catalog_hash(local_rows: dict) -> str:
|
||||
canon = json.dumps({c: {k: (None if v is None else str(v)) for k, v in sorted(r.items())} for c, r in sorted(local_rows.items())}, sort_keys=True)
|
||||
return hashlib.sha256(canon.encode()).hexdigest()
|
||||
|
||||
|
||||
def verify_annex_class_map() -> dict:
|
||||
"""Self-check of ANNEX_DEFAULT_CLASS against the official table: the class must exist, be a goods class in force, and be the ONLY official class for that annex
|
||||
usable in NF-e/NFC-e as the ordinary-sale default. Returns {annex: [problems]} (empty lists = verified)."""
|
||||
official = _official_by_code()
|
||||
numbers = {"I": "1", "VII": "7", "VIII": "8", "XV": "15"}
|
||||
out = {}
|
||||
for annex, code in ANNEX_DEFAULT_CLASS.items():
|
||||
row, problems = official.get(code), []
|
||||
if row is None:
|
||||
problems.append("CLASS_NOT_IN_OFFICIAL_TABLE")
|
||||
else:
|
||||
if str(row.get("anexo")) != numbers[annex]:
|
||||
problems.append("ANNEX_FIELD_DIFFERS")
|
||||
if not (row["ind_nfe"] or row["ind_nfce"]):
|
||||
problems.append("NOT_USABLE_IN_NFE_NFCE")
|
||||
siblings = [r["code"] for r in ref.OFFICIAL_CCLASSTRIB if str(r.get("anexo")) == numbers[annex] and r["code"] != code and (r["ind_nfe"] or r["ind_nfce"])]
|
||||
if siblings:
|
||||
problems.append("SIBLING_CLASSES:" + ",".join(siblings))
|
||||
out[annex] = problems
|
||||
return out
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------- annex lookup
|
||||
|
||||
|
||||
def annex_matches(ncm: str) -> list[dict]:
|
||||
"""Every annex item (any annex, revoked included) whose code list covers the NCM, with the item text and whether it is a clean (condition-free) item."""
|
||||
hits = []
|
||||
for roman, annex in ref.LC214_ANNEXES.items():
|
||||
for item in annex["items"]:
|
||||
for prefix in item["prefixes"]:
|
||||
if ncm.startswith(prefix):
|
||||
hits.append({"annex": roman, "item": item["item"], "prefix": prefix, "clean": item["clean"], "revoked": annex["revoked"], "text": item["description"][:160]})
|
||||
break
|
||||
return hits
|
||||
|
||||
|
||||
def _references(extra=None) -> list:
|
||||
refs = [{"document": ref.LC214_SOURCE["document"], "url": ref.LC214_SOURCE["url"], "sha256": ref.LC214_SOURCE["sha256"]},
|
||||
{"document": "cClassTrib (Portal Nacional NF-e)", "publication": ref.OFFICIAL_CATALOG["publication"], "sha256": ref.OFFICIAL_CATALOG["sha256"]}]
|
||||
return refs + (extra or [])
|
||||
|
||||
|
||||
def _validate_class(code: str, local_rows: dict, compat: dict, today: date):
|
||||
"""None when the class may be proposed, else the exact reason it may not."""
|
||||
official = _official_by_code().get(code)
|
||||
if official is None:
|
||||
return "CLASS_NOT_IN_OFFICIAL_TABLE"
|
||||
loc = local_rows.get(code)
|
||||
if loc is None:
|
||||
return "CLASS_NOT_IN_LOCAL_CATALOG"
|
||||
state = compat["per_code"].get(code)
|
||||
if state != "MATCH":
|
||||
return f"LOCAL_OFFICIAL_{state}"
|
||||
if official["start"] and date.fromisoformat(official["start"]) > today:
|
||||
return "CLASS_NOT_YET_IN_FORCE"
|
||||
if not (official["ind_nfe"] or official["ind_nfce"]):
|
||||
return "CLASS_NOT_USABLE_IN_NFE_NFCE"
|
||||
return None
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------- resolver
|
||||
|
||||
|
||||
def resolve_cclasstrib(product: dict, company: dict | None, ctx: dict, local_rows: dict | None, *, compat: dict | None = None, today: date | None = None) -> Resolution:
|
||||
"""product: {"CODIGO","CLASSIFICACAO_FISCAL","ncm_valid"(bool)}; company: {"crt"}; ctx: {"rtc_enabled": bool, "local_catalog_valid": bool}."""
|
||||
today = today or date.today()
|
||||
ncm = _digits(product.get("CLASSIFICACAO_FISCAL"))
|
||||
crt = str((company or {}).get("crt") or "").strip()
|
||||
used = {"ncm": ncm or None, "crt": crt or None, "rtc_enabled": bool(ctx.get("rtc_enabled"))}
|
||||
if not ctx.get("rtc_enabled"):
|
||||
return Resolution("NOT_APPLICABLE", "RTC-R0", "RTC_DISABLED_IN_ERP", inputs_used=used)
|
||||
if not ctx.get("local_catalog_valid"):
|
||||
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:
|
||||
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)
|
||||
local_rows = local_rows or {}
|
||||
compat = compat or compare_catalogs(local_rows, today)
|
||||
hits = annex_matches(ncm)
|
||||
annexes = sorted({h["annex"] for h in hits})
|
||||
if hits:
|
||||
if any(h["revoked"] for h in hits) and len(annexes) == 1:
|
||||
return Resolution("AMBIGUOUS", "RTC-A1", "LISTED_ONLY_IN_REVOKED_ANNEX", inputs_used=used, evidence=hits, source_references=_references())
|
||||
live = [h for h in hits if not h["revoked"]]
|
||||
live_annexes = sorted({h["annex"] for h in live})
|
||||
if len(live_annexes) > 1 or (len(live_annexes) == 1 and len(annexes) > 1):
|
||||
return Resolution("AMBIGUOUS", "RTC-A2", "LISTED_IN_MORE_THAN_ONE_ANNEX", inputs_used=used, evidence=hits, source_references=_references())
|
||||
annex = live_annexes[0] if live_annexes else annexes[0]
|
||||
clean = [h for h in live if h["clean"]]
|
||||
cls = ANNEX_DEFAULT_CLASS.get(annex)
|
||||
if not cls or not clean or len(clean) != len(live):
|
||||
return Resolution("AMBIGUOUS", "RTC-A1", f"ANNEX_{annex}_NEEDS_OPERATION_OR_CONDITION", inputs_used=used, evidence=hits, source_references=_references())
|
||||
problem = _validate_class(cls, local_rows, compat, today)
|
||||
if problem:
|
||||
return Resolution("INSUFFICIENT_EVIDENCE", "RTC-G2", problem, inputs_used=used, evidence=hits, source_references=_references())
|
||||
official = _official_by_code()[cls]
|
||||
return Resolution("RESOLVED_WITH_EXPLICIT_RULE", "RTC-R3", f"ANNEX_{annex}_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS", cls, official["cst"], "ANNEX_TEXT_MATCH", used, hits, _references())
|
||||
if ncm[:2] in SENSITIVE_CHAPTERS or ncm.startswith(SPECIAL_PREFIXES):
|
||||
return Resolution("AMBIGUOUS", "RTC-A3", "SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD", inputs_used=used, evidence=[{"chapter": ncm[:2]}], source_references=_references())
|
||||
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())
|
||||
@@ -15,7 +15,7 @@ EVENT_TYPES = {
|
||||
"AGENT_STARTED", "AGENT_STOPPED", "HEARTBEAT", "PREFLIGHT_PASS", "PREFLIGHT_FAIL", "JOB_CLAIMED", "JOB_STARTED",
|
||||
"JOB_COMPLETED", "JOB_FAILED", "PRODUCT_BLOCKED", "BLOCKER_RESOLVED", "DATASET_CHANGED", "COMPANY_CONTEXT_CHANGED",
|
||||
"SCHEMA_CHANGED", "WRITE_PLANNED", "WRITE_COMPLETED", "WRITE_FAILED", "CONNECTION_ERROR", "RETRY", "VERSION_CHANGED",
|
||||
"WINDOWS_INSTALL_COMPLETED", "WINDOWS_UNINSTALL_COMPLETED", "ADMIN_ACTION",
|
||||
"WINDOWS_INSTALL_COMPLETED", "WINDOWS_UNINSTALL_COMPLETED", "ADMIN_ACTION", "RTC_AUTO_APPLY",
|
||||
}
|
||||
SEVERITIES = {"INFO", "WARNING", "ERROR", "CRITICAL"}
|
||||
SECRET_RE = re.compile(r"(?i)(password|fb_password|token|api[_-]?key|secret|authorization|connection[_-]?string)\s*[:=]\s*[^,;\s]+")
|
||||
|
||||
@@ -282,6 +282,8 @@ class Installation(Base):
|
||||
sync_interval_seconds: Mapped[int] = mapped_column(Integer, default=1800, server_default="1800")
|
||||
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")
|
||||
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())
|
||||
|
||||
@@ -731,3 +733,26 @@ class ExternalProductCache(Base):
|
||||
normalized_data: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
cache_allowed: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
redistribution_allowed: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
|
||||
class RtcWriteAudit(Base):
|
||||
"""Append-only record of every attempted C000025.CCLASSTRIB write (applied or aborted). Holds the old value, so each change is revertible by hand."""
|
||||
__tablename__ = "rtc_write_audits"
|
||||
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)
|
||||
agent_id: Mapped[int] = mapped_column(ForeignKey("agents.id"), index=True)
|
||||
job_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
|
||||
client: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
product_code: Mapped[str] = mapped_column(String(64), index=True)
|
||||
field: Mapped[str] = mapped_column(String(32))
|
||||
outcome: Mapped[str] = mapped_column(String(16), index=True)
|
||||
reason: Mapped[str | None] = mapped_column(String(96), nullable=True)
|
||||
old_value: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
new_value: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
rule_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
evidence: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
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)
|
||||
recorded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
|
||||
@@ -161,7 +161,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):
|
||||
def rtc_status_view(summary, authorized=False):
|
||||
"""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
|
||||
@@ -174,6 +174,36 @@ def rtc_status_view(summary):
|
||||
except ValueError:
|
||||
observed = None
|
||||
source = context.get("rtc_source") or "PARAMETROS.USAR_REFORMA_TRIBUTARIA"
|
||||
return {"enabled": bool(context["rtc_enabled"]), "label": "Ativa" if context["rtc_enabled"] else "Desativada", "supported": bool(context.get("rtc_supported")),
|
||||
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")),
|
||||
"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 "")}
|
||||
|
||||
|
||||
def _rtc_products_view(resolution, enabled):
|
||||
"""Compact 'Produtos RTC' summary. None when RTC is off (RTC never shows up as a problem) or the Agent did not report a resolution."""
|
||||
if not enabled or not isinstance(resolution, dict) or not resolution.get("total"):
|
||||
return None
|
||||
total = int(resolution["total"])
|
||||
valid = int(resolution.get("valid_existing", 0))
|
||||
ready = int(resolution.get("ready_to_apply", 0))
|
||||
blocked = total - valid - ready
|
||||
pending = total - valid
|
||||
return {"total": total, "valid": valid, "ready": ready, "blocked": blocked, "pending": pending,
|
||||
"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")}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Persists the Agent's RTC auto-apply audit records (append-only) and mirrors a summary event for the panel."""
|
||||
from __future__ import annotations
|
||||
|
||||
from .events import record_event
|
||||
from .models import RtcWriteAudit
|
||||
|
||||
MAX_AUDITS_PER_JOB = 2000
|
||||
|
||||
|
||||
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."""
|
||||
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
|
||||
for item in records[:MAX_AUDITS_PER_JOB]:
|
||||
if not isinstance(item, dict) or item.get("field") != "CCLASSTRIB":
|
||||
continue
|
||||
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]),
|
||||
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")))
|
||||
stored += 1
|
||||
if item.get("outcome") == "APPLIED":
|
||||
applied += 1
|
||||
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()
|
||||
return stored
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Turns the deterministic cClassTrib resolution into the ONE root-cause RTC finding per product (pure).
|
||||
|
||||
`evaluate_product` (unified_fiscal_coverage) says WHETHER a product violates the RTC rule; this module says WHY and whether the gap is closable:
|
||||
the single RTC blocker of a product is refined to the cause that matters, never several blockers for the same root cause.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from .cclasstrib_resolver import 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_RULE_EVIDENCE_MISSING", "CCLASSTRIB_INVALID_FORMAT", "CCLASSTRIB_NOT_IN_CATALOG", "RTC_CST_MISSING", "RTC_CATALOG_INVALID")
|
||||
|
||||
|
||||
def _text(value) -> str:
|
||||
return "" if value is None else str(value).strip()
|
||||
|
||||
|
||||
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}
|
||||
|
||||
|
||||
def refine_rtc_finding(found: dict, product: dict, resolution: Resolution | None, compat_state: dict | None) -> dict:
|
||||
"""Mutates and returns `found` (an `evaluate_product` result). Only acts when RTC is REQUIRED for the product."""
|
||||
if found.get("applicability", {}).get("RTC") != "REQUIRED" or resolution is None:
|
||||
return found
|
||||
value = _text(product.get("CCLASSTRIB"))
|
||||
rtc_blockers = [b for b in found["blockers"] if b["category"] == "RTC"]
|
||||
kinds = {b["type"] for b in rtc_blockers}
|
||||
found["rtc_resolution"] = slim(resolution)
|
||||
if "RTC_CATALOG_INVALID" in kinds:
|
||||
return found # nothing can be proposed or compared against an invalid catalog
|
||||
others = [b for b in found["blockers"] if b["category"] != "RTC"]
|
||||
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 == "AMBIGUOUS":
|
||||
new = ("CCLASSTRIB_AMBIGUOUS", f"{resolution.rule_id}:{resolution.reason}")
|
||||
elif resolution.reason.startswith(RTC_CATALOG_REASONS):
|
||||
new = ("RTC_LOCAL_CATALOG_OUTDATED", 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]}]
|
||||
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"):
|
||||
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}"}]
|
||||
else:
|
||||
found["rtc_resolution"]["no_write_required"] = resolution.auto_apply_candidate and resolution.resolved_cclasstrib == value
|
||||
return found
|
||||
|
||||
|
||||
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": {}}
|
||||
for found in findings.values():
|
||||
out["total"] += 1
|
||||
if found.get("applicability", {}).get("RTC") != "REQUIRED":
|
||||
out["not_applicable"] += 1
|
||||
continue
|
||||
types = {b["type"] for b in found["blockers"] if b["category"] == "RTC"}
|
||||
res = found.get("rtc_resolution") or {}
|
||||
if res:
|
||||
out["by_rule"][res["rule_id"]] = out["by_rule"].get(res["rule_id"], 0) + 1
|
||||
out["by_reason"][res["reason"]] = out["by_reason"].get(res["reason"], 0) + 1
|
||||
if not types:
|
||||
out["valid_existing"] += 1
|
||||
elif "CCLASSTRIB_CONFLICT" in types:
|
||||
out["conflicts"] += 1
|
||||
elif "CCLASSTRIB_OFFICIAL_MISMATCH" in types:
|
||||
out["official_mismatch"] += 1
|
||||
elif res.get("auto_apply_candidate") and "CCLASSTRIB_MISSING" in types:
|
||||
out["ready_to_apply"] += 1
|
||||
elif "CCLASSTRIB_AMBIGUOUS" in types:
|
||||
out["ambiguous"] += 1
|
||||
elif types & {"CCLASSTRIB_MISSING_UNRESOLVED", "RTC_RULE_EVIDENCE_MISSING", "RTC_LOCAL_CATALOG_OUTDATED", "CCLASSTRIB_MISSING"}:
|
||||
out["insufficient"] += 1
|
||||
else:
|
||||
out["other_blocked"] += 1
|
||||
if compat:
|
||||
out["catalog_compatibility"] = {"compatibility": compat["compatibility"], "counts": compat["counts"], "expired_present_locally": compat.get("expired_present_locally", 0)}
|
||||
return out
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.8"
|
||||
GTI_FISCAL_VERSION = "1.1.9"
|
||||
API_COMPAT_MAJOR_MINOR = "1.0" # an Agent 1.0.x/1.1.x talks to any Hub 1.0.x
|
||||
|
||||
+27
-1
@@ -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), "enabled": bool(installation.processing_enabled), "interval_minutes": installation.sync_interval_seconds // 60,
|
||||
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,
|
||||
"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,6 +319,32 @@ 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."""
|
||||
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
|
||||
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")
|
||||
session.commit()
|
||||
return RedirectResponse(f"/agents/{agent_id}", status_code=303)
|
||||
finally: session.close()
|
||||
|
||||
@routes.post("/agents/{agent_id}/sync-now")
|
||||
async def sync_now(request: Request, agent_id: int):
|
||||
return await _installation_action(request, agent_id, "sync-now")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "gti-fiscal-hub"
|
||||
version = "1.1.8"
|
||||
version = "1.1.9"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"fastapi>=0.115,<1",
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
<form method="post" action="/agents/{{ detail.agent_id }}/sync-now" style="display:inline"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button type="submit" class="primary-action" {% if not ops.enabled %}disabled title="Processamento pausado: retome para sincronizar"{% else %}title="Verifica apenas as alterações desde a última sincronização"{% endif %}>Sincronizar agora</button></form>
|
||||
<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 %}<dt>Último job</dt><dd>{% if ops.last_job %}<a href="/jobs?scope=all&job_id={{ ops.last_job.id }}">{{ ops.last_job.kind_label }}</a> · {{ ops.last_job.status_label }}{% else %}nenhum{% endif %}</dd></dl></section>
|
||||
</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&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 }}&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>
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
from datetime import date
|
||||
|
||||
from central.app import rtc_reference_data as ref
|
||||
from central.app.cclasstrib_resolver import (ANNEX_DEFAULT_CLASS, annex_matches, compare_catalogs, local_catalog_hash, resolve_cclasstrib, verify_annex_class_map)
|
||||
|
||||
TODAY = date(2026, 9, 24)
|
||||
NORMAL, SIMPLES = {"crt": "3"}, {"crt": "1"}
|
||||
ON = {"rtc_enabled": True, "local_catalog_valid": True}
|
||||
|
||||
|
||||
def local_from_official(skip=(), tweak=None):
|
||||
rows = {}
|
||||
for r in ref.OFFICIAL_CCLASSTRIB:
|
||||
if r["code"] in skip:
|
||||
continue
|
||||
rows[r["code"]] = {"cst": r["cst"], "tipo_aliquota": r["tipo_aliquota"], "p_red_ibs": r["p_red_ibs"], "p_red_cbs": r["p_red_cbs"], "ind_nfe": r["ind_nfe"], "ind_nfce": r["ind_nfce"], "start": r["start"], "end": r["end"]}
|
||||
for code, change in (tweak or {}).items():
|
||||
rows[code] = {**rows[code], **change}
|
||||
return rows
|
||||
|
||||
|
||||
def product(ncm, valid=True, code="1"):
|
||||
return {"CODIGO": code, "CLASSIFICACAO_FISCAL": ncm, "ncm_valid": valid}
|
||||
|
||||
|
||||
def resolve(ncm, company=NORMAL, ctx=ON, local=None, valid=True):
|
||||
return resolve_cclasstrib(product(ncm, valid), company, ctx, local if local is not None else local_from_official(), today=TODAY)
|
||||
|
||||
|
||||
def test_annex_default_class_map_is_verified_against_the_official_table():
|
||||
assert verify_annex_class_map() == {annex: [] for annex in ANNEX_DEFAULT_CLASS}
|
||||
|
||||
|
||||
def test_reference_data_carries_provenance_and_the_expected_shape():
|
||||
assert len(ref.OFFICIAL_CCLASSTRIB) == 164 and ref.OFFICIAL_CATALOG["sha256"] and ref.LC214_SOURCE["sha256"] and ref.LC214_SOURCE["accessed_at"]
|
||||
assert {"I", "VII", "VIII", "IX", "XV"} <= set(ref.LC214_ANNEXES) and ref.LC214_ANNEXES["XIV"]["revoked"] is True
|
||||
assert len(ref.LC214_ANNEXES["I"]["items"]) == 26 and len(ref.LC214_ANNEXES["XV"]["items"]) == 6
|
||||
|
||||
|
||||
def test_rtc_off_is_not_applicable_and_never_a_candidate():
|
||||
r = resolve("10063021", ctx={"rtc_enabled": False, "local_catalog_valid": True})
|
||||
assert r.status == "NOT_APPLICABLE" and r.rule_id == "RTC-R0" and not r.auto_apply_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 resolve("10063021", company={"crt": None}).reason == "COMPANY_REGIME_UNKNOWN"
|
||||
|
||||
|
||||
def test_invalid_or_unconfirmed_ncm_is_insufficient():
|
||||
assert resolve("1234", valid=True).reason == "NO_VALID_NCM"
|
||||
assert resolve("10063021", valid=False).reason == "NO_VALID_NCM"
|
||||
|
||||
|
||||
def test_clean_annex_item_resolves_to_the_annex_default_class_with_evidence():
|
||||
r = resolve("10063021") # rice: Anexo I item 1 (subposicao 1006.30)
|
||||
assert r.status == "RESOLVED_WITH_EXPLICIT_RULE" and r.resolved_cclasstrib == "200003" and r.resolved_cst_ibscbs == "200" and r.rule_id == "RTC-R3" and r.auto_apply_candidate
|
||||
assert r.evidence[0]["annex"] == "I" and r.evidence[0]["clean"] is True and r.confidence_type == "ANNEX_TEXT_MATCH"
|
||||
assert r.source_references[0]["sha256"] == ref.LC214_SOURCE["sha256"] and r.inputs_used["ncm"] == "10063021"
|
||||
|
||||
|
||||
def test_conditional_annex_item_is_ambiguous_not_guessed():
|
||||
milk = next(i for i in ref.LC214_ANNEXES["I"]["items"] if not i["clean"] and i["prefixes"])
|
||||
ncm = milk["prefixes"][0].ljust(8, "0")
|
||||
r = resolve(ncm)
|
||||
assert r.status == "AMBIGUOUS" and r.rule_id == "RTC-A1" and not r.auto_apply_candidate and r.evidence[0]["clean"] is False
|
||||
|
||||
|
||||
def test_annex_without_a_unique_default_class_is_ambiguous():
|
||||
item = next(i for i in ref.LC214_ANNEXES["IV"]["items"] if i["clean"]) # medical devices: 60% class vs zero-rated public-buyer class
|
||||
r = resolve(item["prefixes"][0].ljust(8, "1"))
|
||||
assert r.status == "AMBIGUOUS" and "NEEDS_OPERATION_OR_CONDITION" in r.reason
|
||||
|
||||
|
||||
def test_revoked_annex_only_is_ambiguous():
|
||||
prefix = next(p for i in ref.LC214_ANNEXES["XIV"]["items"] for p in i["prefixes"] if len(p) == 8 and not annex_matches_other_than_xiv(p))
|
||||
assert resolve(prefix).rule_id == "RTC-A1" and resolve(prefix).reason == "LISTED_ONLY_IN_REVOKED_ANNEX"
|
||||
|
||||
|
||||
def annex_matches_other_than_xiv(prefix):
|
||||
return [h for h in annex_matches(prefix) if h["annex"] != "XIV"]
|
||||
|
||||
|
||||
def test_nature_based_chapters_are_ambiguous():
|
||||
for ncm in ("03028990", "30049099", "27101259", "49019900", "87032100", "90183100"):
|
||||
r = resolve(ncm)
|
||||
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():
|
||||
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") == []
|
||||
|
||||
|
||||
def test_a_class_missing_or_different_in_the_local_catalog_is_never_proposed():
|
||||
r = resolve("39235000", local=local_from_official(skip=("000001",)))
|
||||
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"}}))
|
||||
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"
|
||||
|
||||
|
||||
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 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
|
||||
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"
|
||||
|
||||
|
||||
def test_local_catalog_hash_is_stable_and_sensitive():
|
||||
a, b = local_from_official(), local_from_official(tweak={"200003": {"p_red_ibs": "60"}})
|
||||
assert local_catalog_hash(a) == local_catalog_hash(local_from_official()) and local_catalog_hash(a) != local_catalog_hash(b)
|
||||
|
||||
|
||||
def test_resolution_is_deterministic_and_auditable():
|
||||
a, b = resolve("10063021"), resolve("10063021")
|
||||
assert a.as_dict() == b.as_dict() and {"rule_id", "inputs_used", "source_references", "evidence", "reason"} <= set(a.as_dict())
|
||||
@@ -587,3 +587,45 @@ def test_rtc_status_shows_where_and_when_the_agent_observed_it():
|
||||
assert "linhas de PARAMETROS divergentes" in conflict and ">Desativada<" in conflict
|
||||
for legacy in ("Escopo anterior", "Escopo ampliado", "Legacy coverage", "Expanded coverage"):
|
||||
assert legacy not in conflict
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- RTC auto-apply authorization (central, default OFF)
|
||||
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)
|
||||
|
||||
|
||||
def test_rtc_auto_apply_authorization_is_off_by_default_and_needs_csrf_and_a_typed_confirmation():
|
||||
client, agent_id, _ = _panel_client()
|
||||
assert _installation(client, agent_id) is False
|
||||
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
|
||||
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"]
|
||||
|
||||
|
||||
def test_panel_shows_rtc_products_summary_and_the_pending_message_only_when_rtc_is_on():
|
||||
client, agent_id, _ = _panel_client()
|
||||
_complete_sync(client, agent_id, {"rtc_enabled": False, "rtc_supported": True, "rtc_resolution": None})
|
||||
off = client.get(f"/agents/{agent_id}").text
|
||||
assert "Produtos RTC" not in off and "cClassTrib pendente" not in off and "Preenchimento automático" not in off
|
||||
resolution = {"total": 521, "valid_existing": 0, "ready_to_apply": 280, "ambiguous": 215, "insufficient": 26, "catalog_compatibility": {"compatibility": "PARTIAL_LOCAL_OUTDATED"}}
|
||||
_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
|
||||
for legacy in ("Escopo anterior", "Escopo ampliado"):
|
||||
assert legacy not in on
|
||||
|
||||
@@ -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.8" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"]
|
||||
assert body["version"] == "1.1.9" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"]
|
||||
|
||||
|
||||
def test_production_refuses_the_dev_token(monkeypatch):
|
||||
|
||||
@@ -264,3 +264,46 @@ def test_rtc_on_then_off_and_pis_fix_leave_no_stale_open_blockers_and_never_dupl
|
||||
with Session(engine) as s:
|
||||
rows = s.scalars(select(BlockedProduct)).all()
|
||||
assert len(rows) == 2 and [(r.product_code, r.resolved_at is None) for r in rows] == [("20", True), ("21", False)]
|
||||
|
||||
|
||||
def test_job_carries_the_central_rtc_authorization_default_off_and_follows_the_installation():
|
||||
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
|
||||
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
|
||||
s.commit()
|
||||
age_last_sync(engine, 4000)
|
||||
hb(client)
|
||||
assert client.get("/v1/jobs/next", headers=H).json()["authorizations"] == {"rtc_auto_apply": True}
|
||||
|
||||
|
||||
def test_rtc_write_audits_are_persisted_with_server_side_identity_and_never_duplicated_per_job():
|
||||
from central.app.models import AgentEvent, RtcWriteAudit
|
||||
client, engine = _client()
|
||||
hb(client)
|
||||
applied = {"outcome": "APPLIED", "product_code": "10", "field": "CCLASSTRIB", "old_value": None, "new_value": "200003", "rule_id": "RTC-R3", "evidence": {"reason": "ANNEX_I_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS"},
|
||||
"official_source_version": {"cclasstrib_sha256": "ab" * 32}, "local_catalog_hash": "cd" * 32, "timestamp": "2026-09-24T20:00:00+00:00", "job_id": 999, "tenant_id": 12345, "agent_id": 777}
|
||||
aborted = {"outcome": "ABORTED", "product_code": "11", "field": "CCLASSTRIB", "reason": "RTC_NO_LONGER_ENABLED", "rule_id": "RTC-R4", "timestamp": "2026-09-24T20:00:01+00:00"}
|
||||
other_field = {"outcome": "APPLIED", "product_code": "12", "field": "IPI", "new_value": "1.0"} # anything but CCLASSTRIB is refused
|
||||
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"mode": "AUTO_APPLY", "applied": [applied, other_field], "aborted": [aborted], "aborted_reasons": {"RTC_NO_LONGER_ENABLED": 1}}})
|
||||
with Session(engine) as s:
|
||||
rows = s.scalars(select(RtcWriteAudit).order_by(RtcWriteAudit.id)).all()
|
||||
assert [(r.product_code, r.outcome, r.old_value, r.new_value, r.rule_id) for r in rows] == [("10", "APPLIED", None, "200003", "RTC-R3"), ("11", "ABORTED", None, None, "RTC-R4")]
|
||||
inst = s.scalars(select(Installation)).one()
|
||||
assert all(r.installation_id == inst.id and r.agent_id == inst.agent_id and r.tenant_id == inst.tenant_id for r in rows) # identity comes from the authenticated context, not the payload
|
||||
assert rows[0].local_catalog_hash == "cd" * 32 and rows[0].official_source_version["cclasstrib_sha256"] == "ab" * 32 and rows[0].job_id != 999
|
||||
events = [e for e in s.scalars(select(AgentEvent)).all() if e.event_type == "RTC_AUTO_APPLY"]
|
||||
assert len(events) == 1 and events[0].structured_context["applied"] == 1 and events[0].severity == "WARNING"
|
||||
|
||||
|
||||
def test_report_only_jobs_leave_no_audit_rows():
|
||||
from central.app.models import RtcWriteAudit
|
||||
client, engine = _client()
|
||||
hb(client)
|
||||
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() == []
|
||||
|
||||
@@ -19,4 +19,4 @@ FB_PASSWORD=
|
||||
FISCAL_MODE=DRY_RUN
|
||||
FIREBIRD_WRITES=0
|
||||
POLL_INTERVAL_SECONDS=5
|
||||
AGENT_VERSION=1.1.8
|
||||
AGENT_VERSION=1.1.9
|
||||
|
||||
@@ -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.8'
|
||||
'FISCAL_MODE=DRY_RUN', 'FIREBIRD_WRITES=0', 'POLL_INTERVAL_SECONDS=5', 'AGENT_VERSION=1.1.9'
|
||||
)
|
||||
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.8
|
||||
AGENT_VERSION=1.1.9
|
||||
|
||||
@@ -19,4 +19,4 @@ FB_PASSWORD=
|
||||
FISCAL_MODE=DRY_RUN
|
||||
FIREBIRD_WRITES=0
|
||||
POLL_INTERVAL_SECONDS=5
|
||||
AGENT_VERSION=1.1.8
|
||||
AGENT_VERSION=1.1.9
|
||||
|
||||
@@ -19,4 +19,4 @@ FB_PASSWORD=
|
||||
FISCAL_MODE=DRY_RUN
|
||||
FIREBIRD_WRITES=0
|
||||
POLL_INTERVAL_SECONDS=5
|
||||
AGENT_VERSION=1.1.8
|
||||
AGENT_VERSION=1.1.9
|
||||
|
||||
@@ -19,4 +19,4 @@ FB_PASSWORD=
|
||||
FISCAL_MODE=DRY_RUN
|
||||
FIREBIRD_WRITES=0
|
||||
POLL_INTERVAL_SECONDS=5
|
||||
AGENT_VERSION=1.1.8
|
||||
AGENT_VERSION=1.1.9
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# GTI Fiscal Hub — production environment (copy to .env, never commit .env)
|
||||
GTI_FISCAL_VERSION=1.1.8
|
||||
GTI_FISCAL_VERSION=1.1.9
|
||||
|
||||
# REQUIRED. URL-safe secret (used inside a connection URL): openssl rand -hex 24
|
||||
POSTGRES_PASSWORD=
|
||||
|
||||
@@ -36,7 +36,7 @@ services:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: central/Dockerfile
|
||||
image: gti-fiscal-hub:${GTI_FISCAL_VERSION:-1.1.8}
|
||||
image: gti-fiscal-hub:${GTI_FISCAL_VERSION:-1.1.9}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
|
||||
+1
-1
@@ -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.8
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy_hub.ps1 -PublishAgent -Version 1.1.9
|
||||
```
|
||||
Parâmetros: `-Version` (padrão `release/VERSION`), `-PublishAgent`, `-SkipTests`, `-DryRun`.
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Determinação segura do cClassTrib (resolver determinístico)
|
||||
|
||||
`central/app/cclasstrib_resolver.py` (puro, sem Firebird, sem rede, sem probabilidade) + `central/app/rtc_reference_data.py` (gerado por `scripts/build_rtc_reference_data.py`, com hash).
|
||||
`ACCESSED_AT`: **2026-09-24/25**. Nada aqui é chute: uma classe só é proposta quando uma regra **explícita**, ancorada nos textos oficiais, a prova.
|
||||
|
||||
## Fontes oficiais (SOURCE / DOCUMENT / VERSION / PUBLICATION / SHA256)
|
||||
| Fonte | Documento | Versão / publicação | SHA256 |
|
||||
|---|---|---|---|
|
||||
| Portal Nacional NF-e (ENCAT) | Tabela **cClassTrib** (164 classes, 18 CST) — `cClassTrib 2026-06-22.xlsx` | publicada 22/06/2026; no repo em `sources/raw/bootstrap_v1/` e no Hub (dataset `RTC_CCLASSTRIB` ACTIVE) | `1448cb63a41bdb67…` |
|
||||
| Planalto (cópia do Internet Archive; planalto.gov.br recusa conexão direta) | **LC 214/2025 compilada** (com LC 227/2026: Anexo XIV revogado) — arts. 125–152 (reduções), 172 (combustíveis), 348 (2026); Anexos I–XV | acessado 2026-09-24 | `5c7d59b3c49e5b7c…` |
|
||||
| Portal Nacional NF-e | **NT 2025.002-RTC v1.51** (04/08/2026) — para CRT 1/2/4 as orientações de IBS/CBS "serão publicadas em NT futura"; tributação a partir de 2027 (LC 214 art. 348) | `https://www.nfe.fazenda.gov.br/portal/exibirArquivo.aspx?conteudo=AKD/muSmiIY=` | ver `docs/sources/SOURCES_MANIFEST_UNIFIED_FISCAL_V1.json` |
|
||||
| SGV 5.1.0.4 (executável) | tela de produto: `cClassTrib` = `TRzDBButtonEdit` `DataField=CCLASSTRIB`; consulta `select * from RTC_CCLASSTRIB where … (IND_NFE=1 or IND_NFCE=1 or IND_CTE=1)`; junção do documento `C000025.CCLASSTRIB → RTC_CCLASSTRIB → RTC_CST` | binário local | — |
|
||||
|
||||
O portal NF-e foi rastreado (Documentos, Diversos, Atos): não expõe tabela cClassTrib mais nova que 22/06/2026. Não há endpoint oficial NCM→cClassTrib: o vínculo vem dos **Anexos da LC 214** (listas de NCM) + tabela oficial.
|
||||
|
||||
## O que o SGV faz (item 5) — DOCUMENT_CCLASSTRIB_SOURCE
|
||||
O SGV **não tem** regra produto→cClassTrib: o operador escolhe a classe pelo botão de busca (filtrada por `IND_NFE/IND_NFCE/IND_CTE=1`) e o documento usa `C000025.CCLASSTRIB` diretamente, juntando ao catálogo local e ao CST. Nenhuma trigger/procedure referencia RTC; `USAR_REFORMA_TRIBUTARIA` só liga/desliga. Consequência: qualquer resolução automática é regra **nossa**, portanto tem de ser provada e auditável. A classe proposta tem de passar no mesmo filtro do SGV (usável em NF-e/NFC-e).
|
||||
|
||||
## Catálogo local × oficial (item 4) — RTC_LOCAL_OFFICIAL_COMPATIBILITY
|
||||
Comparação por código (CST, reduções IBS/CBS, indicadores NF-e/NFC-e, vigência): `MATCH | LOCAL_ONLY | OFFICIAL_ONLY | MISMATCH | EXPIRED`. Nada é escrito no catálogo local.
|
||||
|
||||
| | TESTESOFICIAIS (= todos os bancos com RTC, mesmo catálogo) |
|
||||
|---|---|
|
||||
| MATCH | 131 |
|
||||
| OFFICIAL_ONLY (local desatualizado) | 19 (`000005, 200053, 200054, 221002-4, 400002, 410032-7, 550022-5, 620007, 820009`) |
|
||||
| MISMATCH | 11 — só `DOCUMENT_FLAGS` (IND_NFE/IND_NFCE): `200001, 200044, 410027, 410028, 410031, 550002, 620001-5` |
|
||||
| EXPIRED presentes no local | 3 (`220001-3`, fim 01/01/2026) |
|
||||
| LOCAL_ONLY | 0 |
|
||||
|
||||
Conclusão: **LOCAL_CATALOG_HAS_ENOUGH_METADATA_TO_RESOLVE = false**. O catálogo local (33 colunas: CST, tipo de alíquota, reduções, indicadores de documento, monofasia, `ANEXO`, `LINK`) descreve *a classe*, não contém NCM nem critério de produto: só o `ANEXO` liga classe a anexo. A determinação usa o catálogo local **para validar** e os Anexos da LC 214 **para decidir**. Incompatibilidade material ⇒ só a classe afetada é bloqueada (`RTC_LOCAL_CATALOG_OUTDATED` / `CCLASSTRIB_OFFICIAL_MISMATCH`); as classes que as regras usam (`000001, 200003, 200014, 200034, 200035`) são `MATCH`.
|
||||
|
||||
## Modelo de resolução e regras
|
||||
Premissa documentada: **venda ordinária no mercado interno** (default de produto). Classes que dependem do adquirente/operação (órgão público, exportação, ZFM/ALC, crédito presumido, diferimento…) não cabem num default de produto ⇒ `AMBIGUOUS`.
|
||||
|
||||
| Regra | Condição | Status |
|
||||
|---|---|---|
|
||||
| RTC-R0 | RTC desligada no ERP | `NOT_APPLICABLE` |
|
||||
| RTC-G1 | catálogo RTC local com falha de integridade | `INSUFFICIENT_EVIDENCE` |
|
||||
| RTC-R1 | Simples (CRT 1/2/4) ou regime desconhecido | `INSUFFICIENT_EVIDENCE` (NT 2025.002: orientação pendente) |
|
||||
| RTC-R2 | NCM sem confirmação de 8 dígitos válidos | `INSUFFICIENT_EVIDENCE` |
|
||||
| RTC-A1 | NCM em anexo revogado, em anexo sem classe-padrão única (IV, V, VI, IX, X, XI, XII, XIII…) ou sob texto condicional ("exceto", "em conformidade com…", "ressalvados…") | `AMBIGUOUS` |
|
||||
| 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-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`.
|
||||
Saída auditável: `status, resolved_cclasstrib, resolved_cst_ibscbs, reason, rule_id, confidence_type` (`ANNEX_TEXT_MATCH` | `EXCLUSION_OF_ALL_SPECIAL_REGIMES`; sem score), `inputs_used`, `evidence` (item do anexo, prefixo, texto), `source_references` (sha256).
|
||||
O anexo é lido do texto oficial por `scripts/build_rtc_reference_data.py` (item a item; `clean=False` quando há condição). `verify_annex_class_map()` prova contra a tabela oficial que cada classe-padrão existe, é a única classe do anexo usável em NF-e/NFC-e e tem o `ANEXO` esperado (o Anexo IX ficou de fora: a classe `515001` é irmã).
|
||||
|
||||
## Blockers RTC (uma causa raiz, um blocker por produto)
|
||||
`CCLASSTRIB_MISSING` (com `READY_TO_APPLY:<classe>:<regra>` quando a regra prova a classe) · `CCLASSTRIB_MISSING_UNRESOLVED` (cClassTrib não determinado) · `CCLASSTRIB_AMBIGUOUS` (cClassTrib ambíguo) · `RTC_RULE_EVIDENCE_MISSING` (evidência fiscal insuficiente) · `RTC_LOCAL_CATALOG_OUTDATED` (catálogo RTC local incompatível) · `CCLASSTRIB_OFFICIAL_MISMATCH` (classe existente diverge do oficial) · `CCLASSTRIB_CONFLICT` (valor existente ≠ regra determinística) + os de validação (`INVALID_FORMAT`, `NOT_IN_CATALOG`, `RTC_CST_MISSING`, `RTC_CATALOG_INVALID`). Todos vão para a mesma fila (categoria `RTC`).
|
||||
|
||||
## Dry-run real (somente leitura; `scripts/rtc_resolution_dryrun.py` → `docs/runs/rtc_resolution_dryrun.json`, amostra de 100 em `docs/runs/rtc_resolution_audit_sample.csv`)
|
||||
Estado como está hoje: TESTESOFICIAIS e CONSTRUCAO têm RTC ligada e regime Simples ⇒ **0 candidatos** (RTC-R1). Simulação "regime normal + RTC ligada" (o que as regras resolveriam quando existir orientação oficial para Simples):
|
||||
|
||||
| Banco | Produtos | Prontos (R3+R4) | Ambíguos | Insuficientes (NCM inválido) |
|
||||
|---|---|---|---|---|
|
||||
| TESTESOFICIAIS | 521 | 280 | 215 | 26 |
|
||||
| CONSTRUCAO | 3.712 | 2.725 | 973 | 14 |
|
||||
| MMMODAS | 20.996 | 20.831 | 49 | 116 |
|
||||
| RINCAO (CRT 3, RTC off no banco) | 894 | 530 | 364 | 0 |
|
||||
| UNIAO | 8.942 | 7.419 | 1.432 | 91 |
|
||||
|
||||
## 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`).
|
||||
@@ -0,0 +1,32 @@
|
||||
# Preenchimento automático do cClassTrib (RTC auto-apply)
|
||||
|
||||
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.**
|
||||
|
||||
## 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).
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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).
|
||||
|
||||
## 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.
|
||||
|
||||
## 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`.
|
||||
- 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.
|
||||
@@ -49,5 +49,8 @@ O fingerprint dos blockers inclui o estado RTC: 0→1 cria blockers novos; 1→0
|
||||
Estado atual do parâmetro nas cópias locais: `CONSTRUCAO=1`, `TESTESOFICIAIS=1`, `MMMODAS=0`, `RINCAO=0`, `UNIAO=0` (cada uma com **uma** linha em `PARAMETROS`, `ODS 11.2`, Firebird 2.5.9). O discovery de `TESTESOFICIAIS.FDB` registrou `0` às 15:25 de 24/09 e a leitura de hoje (mesmo arquivo, mesmas 521 linhas) retorna `1`. Causas descartadas: linha errada / várias linhas (há 1), cache do Hub (a leitura é direta no Firebird, o Agent lê ao vivo), outro arquivo (mesmo caminho canônico). Causa real comprovada: **o valor mudou no próprio banco depois do discovery** (mesmo arquivo, mesma linha única). O autor da mudança não é rastreável (o Firebird 2.5 não guarda histórico de valores e o SGV não tem tabela de log de parâmetros); a hipótese mais simples é a alteração do parâmetro "usar reforma tributária" no SGV 5.1.0.4, que é quem lê esse parâmetro. Por isso o Agent nunca reaproveita um estado anterior.
|
||||
Consequência medida (TESTESOFICIAIS, RTC ligada de fato): 521/521 produtos com `CCLASSTRIB_MISSING` ⇒ cobertura real 0%; com RTC desligada (estado do piloto) 94,82%.
|
||||
|
||||
## Determinação e preenchimento do cClassTrib
|
||||
Ver `docs/cclasstrib_resolution.md` (fontes oficiais, catálogo local × oficial, regras) e `docs/rtc_auto_apply.md` (auto-apply restrito a `CCLASSTRIB`, desligado por padrão).
|
||||
|
||||
## Testes
|
||||
RTC off/on, `CCLASSTRIB` vazio/válido/inválido/órfão/expirado, catálogo inválido: `central/tests/test_unified_fiscal_coverage.py`, `agent/tests/test_unified_fiscal.py`.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"database": "scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB (gbak restore of TESTESOFICIAIS)",
|
||||
"real_database_writes": 0,
|
||||
"real_database_write_attempts": 0,
|
||||
"copy_original_crt": [
|
||||
"1"
|
||||
],
|
||||
"copy_crt_for_experiment": "3",
|
||||
"copy_rtc_param": 1,
|
||||
"coverage_before": {
|
||||
"total": 521,
|
||||
"complete": 0,
|
||||
"blocked": 521,
|
||||
"unclassified": 0,
|
||||
"coverage_percent": 0.0
|
||||
},
|
||||
"resolution_before": {
|
||||
"total": 521,
|
||||
"valid_existing": 0,
|
||||
"ready_to_apply": 280,
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"IDEMPOTENT": true,
|
||||
"ONLY_CCLASSTRIB_CHANGED": true,
|
||||
"PASS": true
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
000001|000|Padrão|0|0|2026-01-01T00:00:00|||Situações tributadas integralmente pelo IBS e CBS.
|
||||
000002|000|Padrão|0|0|2026-01-01T00:00:00||90111|Exploração de via
|
||||
000003|000|Padrão|0|0|2026-01-01T00:00:00|||Regime automotivo - projetos incentivados (art. 311)
|
||||
000004|000|Padrão|0|0|2026-01-01T00:00:00|||Regime automotivo - projetos incentivados (art. 312)
|
||||
000005|000|Padrão|0|0|2026-01-01T00:00:00|||Operação com EAC destinado à mistura com gasolina A, mas com saída do biocombustível com destinação diversa
|
||||
010001|010|Uniforme setorial|0|0|2026-01-01T00:00:00|||Operações do FGTS não realizadas pela Caixa Econômica Federal
|
||||
010002|010|Uniforme setorial|0|0|2026-01-01T00:00:00|||Operações do serviço financeiro
|
||||
011001|011|Uniforme nacional (referência)|60|60|2026-01-01T00:00:00|||Planos de assistência funerária.
|
||||
011002|011|Uniforme nacional (referência)|60|60|2026-01-01T00:00:00|||Planos de assistência à saúde
|
||||
011003|011|Uniforme nacional (referência)|60|60|2026-01-01T00:00:00||92401|Intermediação de planos de assistência à saúde
|
||||
011004|011|Uniforme nacional (referência)|0|0|2026-01-01T00:00:00|||Concursos e prognósticos
|
||||
011005|011|Uniforme nacional (referência)|30|30|2026-01-01T00:00:00|||Planos de assistência à saúde de animais domésticos
|
||||
200001|200|Padrão|100|100|2026-01-01T00:00:00|||Serviços de transporte de bens até as zonas de processamento de exportação e bens exportados a partir das zonas de processamento de exportação
|
||||
200002|200|Padrão|100|100|2026-01-01T00:00:00|||Fornecimento ou importação para produtor rural não contribuinte ou TAC
|
||||
200003|200|Padrão|100|100|2026-01-01T00:00:00||1|Vendas de produtos destinados à alimentação humana (Anexo I)
|
||||
200004|200|Padrão|100|100|2026-01-01T00:00:00||12|Fornecimento de dispositivos médicos (Anexo XII)
|
||||
200005|200|Padrão|100|100|2026-01-01T00:00:00||4|Fornecimento de dispositivos médicos para órgãos da administração pública e entidades de saúde imunes (Anexo IV)
|
||||
200006|200|Padrão|100|100|2026-01-01T00:00:00|||Situação de emergência de saúde pública reconhecida pelo Poder público
|
||||
200007|200|Padrão|100|100|2026-01-01T00:00:00||13|Fornecimento dos dispositivos de acessibilidade próprios para pessoas com deficiência (Anexo XIII)
|
||||
200008|200|Padrão|100|100|2026-01-01T00:00:00||5|Fornecimento dos dispositivos de acessibilidade próprios para pessoas com deficiência adquiridos por órgãos da administração pública (Anexo V)
|
||||
200009|200|Padrão|100|100|2026-01-01T00:00:00|||Fornecimento dos medicamentos registrados na Anvisa
|
||||
200010|200|Padrão|100|100|2026-01-01T00:00:00|||Fornecimento dos medicamentos registrados na Anvisa, adquiridos por órgãos da administração pública
|
||||
200011|200|Padrão|100|100|2026-01-01T00:00:00||6|Fornecimento das composições para nutrição enteral e parenteral quando adquiridas por órgãos da administração pública (Anexo VI)
|
||||
200012|200|Padrão|100|100|2026-01-01T00:00:00|||Situação de emergência de saúde pública reconhecida pelo Poder público
|
||||
200013|200|Padrão|100|100|2026-01-01T00:00:00||91471|Fornecimento de tampões higiênicos, absorventes higiênicos internos ou externos
|
||||
200014|200|Padrão|100|100|2026-01-01T00:00:00||15|Fornecimento dos produtos hortícolas, frutas e ovos (Anexo XV)
|
||||
200015|200|Padrão|100|100|2026-01-01T00:00:00|||Venda de automóveis de passageiros de fabricação nacional adquiridos por motoristas profissionais ou pessoas com deficiência
|
||||
200016|200|Padrão|100|100|2026-01-01T00:00:00||91561|Prestação de serviços de pesquisa e desenvolvimento por Instituição Científica, Tecnológica e de Inovação (ICT)
|
||||
200017|200|Padrão|100|100|2026-01-01T00:00:00|||Operações relacionadas ao FGTS
|
||||
200018|200|Padrão|100|100|2026-01-01T00:00:00|||Operações de resseguro e retrocessão
|
||||
200019|200|Padrão|100|100|2026-01-01T00:00:00|||Importador dos serviços financeiros contribuinte
|
||||
200020|200|Padrão|100|100|2026-01-01T00:00:00|||Operação praticada por sociedades cooperativas optantes por regime específico do IBS e CBS
|
||||
200021|200|Padrão|100|100|2026-01-01T00:00:00|||Serviços de transporte público coletivo de passageiros ferroviário e hidroviário
|
||||
200022|200|Padrão|100|100|2026-01-01T00:00:00|||Operação originada fora da ZFM que destine bem material industrializado a contribuinte estabelecido na ZFM
|
||||
200023|200|Padrão|100|100|2026-01-01T00:00:00|||Operação realizada por indústria incentivada que destine bem material intermediário para outra indústria incentivada na ZFM
|
||||
200024|200|Padrão|100|100|2026-01-01T00:00:00|||Operação originada fora das Áreas de Livre Comércio destinadas a contribuinte estabelecido nas Áreas de Livre Comércio
|
||||
200025|200|Padrão|60|100|2026-01-01T00:00:00||93081|Fornecimento dos serviços de educação relacionados ao Programa Universidade para Todos (Prouni)
|
||||
200026|200|Padrão|80|80|2026-01-01T00:00:00||91581|Locação de imóveis localizados nas zonas reabilitadas
|
||||
200027|200|Padrão|70|70|2026-01-01T00:00:00||92611|Operações de locação, cessão onerosa e arrendamento de bens imóveis
|
||||
200028|200|Padrão|60|60|2026-01-01T00:00:00||2|Fornecimento dos serviços de educação (Anexo II)
|
||||
200029|200|Padrão|60|60|2026-01-01T00:00:00||3|Fornecimento dos serviços de saúde humana (Anexo III)
|
||||
200030|200|Padrão|60|60|2026-01-01T00:00:00||4|Venda dos dispositivos médicos (Anexo IV)
|
||||
200031|200|Padrão|60|60|2026-01-01T00:00:00||5|Fornecimento dos dispositivos de acessibilidade próprios para pessoas com deficiência (Anexo V)
|
||||
200032|200|Padrão|60|60|2026-01-01T00:00:00|||Fornecimento dos medicamentos registrados na Anvisa ou produzidos por farmácias de manipulação, ressalvados os medicamentos sujeitos à alíquota zero
|
||||
200033|200|Padrão|60|60|2026-01-01T00:00:00||6|Fornecimento das composições para nutrição enteral e parenteral (Anexo VI)
|
||||
200034|200|Padrão|60|60|2026-01-01T00:00:00||7|Fornecimento dos alimentos destinados ao consumo humano (Anexo VII)
|
||||
200035|200|Padrão|60|60|2026-01-01T00:00:00||8|Fornecimento dos produtos de higiene pessoal e limpeza (Anexo VIII)
|
||||
200036|200|Padrão|60|60|2026-01-01T00:00:00|||Fornecimento de produtos agropecuários, aquícolas, pesqueiros, florestais e extrativistas vegetais in natura
|
||||
200037|200|Padrão|60|60|2026-01-01T00:00:00|||Fornecimento de serviços ambientais de conservação ou recuperação da vegetação nativa
|
||||
200038|200|Padrão|60|60|2026-01-01T00:00:00||9|Fornecimento dos insumos agropecuários e aquícolas (Anexo IX)
|
||||
200039|200|Padrão|60|60|2026-01-01T00:00:00||10|Fornecimento dos bens e serviços relacionados com produções nacionais artísticas, culturais, de eventos, jornalísticas e audiovisuais (Anexo X)
|
||||
200040|200|Padrão|60|60|2026-01-01T00:00:00|||Fornecimento de serviços de comunicação institucional à administração pública
|
||||
200041|200|Padrão|60|60|2026-01-01T00:00:00||91411|Fornecimento de serviço de educação desportiva (art. 141. I)
|
||||
200042|200|Padrão|60|60|2026-01-01T00:00:00|||Fornecimento de serviço de gestão e exploração do desporto (art. 141. II)
|
||||
200043|200|Padrão|60|60|2026-01-01T00:00:00||11|Fornecimento à administração pública dos serviços e dos bens relativos à soberania (Anexo XI)
|
||||
200044|200|Padrão|60|60|2026-01-01T00:00:00||11|Operações e prestações de serviços de segurança da informação e segurança cibernética desenvolvidos por sociedade que tenha sócio brasileiro (Anexo XI)
|
||||
200045|200|Padrão|60|60|2026-01-01T00:00:00|||Operações relacionadas a projetos de reabilitação urbana de zonas históricas e de áreas críticas de recuperação e reconversão urbanística
|
||||
200046|200|Padrão|50|50|2026-01-01T00:00:00|||Operações com bens imóveis
|
||||
200047|200|Padrão|40|40|2026-01-01T00:00:00|||Bares e Restaurantes
|
||||
200048|200|Padrão|40|40|2026-01-01T00:00:00||92811|Hotelaria, Parques de Diversão e Parques Temáticos
|
||||
200049|200|Padrão|40|40|2026-01-01T00:00:00|||Transporte coletivo de passageiros rodoviário, ferroviário e hidroviário
|
||||
200050|200|Padrão|40|40|2026-01-01T00:00:00|||Serviços de transporte aéreo regional coletivo de passageiros ou de carga
|
||||
200051|200|Padrão|40|40|2026-01-01T00:00:00|||Agências de Turismo
|
||||
200052|200|Padrão|30|30|2026-01-01T00:00:00||91271|Prestação de serviços de profissões intelectuais
|
||||
200053|200|Padrão|100|100|2026-01-01T00:00:00|||Fornecimento de medicamentos registrados na Anvisa, quando classificados como soros ou vacinas
|
||||
200054|200|Padrão|100|100|2026-01-01T00:00:00|||Fornecimento de bem material pela cooperativa de produção agropecuária a associado não sujeito ao regime regular do IBS e da CBS
|
||||
220001|220|Fixa|0|0|2026-01-01T00:00:00|2026-01-01T00:00:00||Incorporação imobiliária submetida ao regime especial de tributação
|
||||
220002|220|Fixa|0|0|2026-01-01T00:00:00|2026-01-01T00:00:00||Incorporação imobiliária submetida ao regime especial de tributação
|
||||
220003|220|Fixa|0|0|2026-01-01T00:00:00|2026-01-01T00:00:00||Alienação de imóvel decorrente de parcelamento do solo
|
||||
221001|221|Fixa|0|0|2026-01-01T00:00:00||94871|Locação, cessão onerosa ou arrendamento de bem imóvel com alíquota sobre a receita bruta
|
||||
221002|221|Fixa|0|0|2026-01-01T00:00:00|||Incorporação imobiliária submetida ao regime especial de tributação
|
||||
221003|221|Fixa|0|0|2026-01-01T00:00:00|||Incorporação imobiliária submetida ao regime especial de tributação
|
||||
221004|221|Fixa|0|0|2026-01-01T00:00:00|||Alienação de imóvel decorrente de parcelamento do solo
|
||||
222001|222|Padrão|0|0|2026-01-01T00:00:00|||Transporte internacional de passageiros, caso os trechos de ida e volta sejam vendidos em conjunto
|
||||
400001|400|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimento de serviços de transporte público coletivo de passageiros rodoviário e metroviário
|
||||
400002|400|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimento de serviços de transporte público coletivo de passageiros rodoviário e metroviário com medição por quilômetro rodado
|
||||
410001|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimento de bonificações quando constem no documento fiscal e que não dependam de evento posterior
|
||||
410002|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Transferências entre estabelecimentos pertencentes ao mesmo contribuinte
|
||||
410003|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Doações sem contraprestação em benefício do doador
|
||||
410004|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Exportações de bens e serviços
|
||||
410005|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimentos realizados pela União, pelos Estados, pelo Distrito Federal e pelos Municípios
|
||||
410006|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimentos realizados por entidades religiosas e templos de qualquer culto
|
||||
410007|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimentos realizados por partidos políticos, entidades sindicais e instituições de educação e de assistência social
|
||||
410008|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimentos de livros, jornais, periódicos e do papel destinado a sua impressão
|
||||
410009|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimentos de fonogramas e videofonogramas musicais produzidos no Brasil
|
||||
410010|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimentos de serviço de comunicação nas modalidades de radiodifusão sonora e de sons e imagens de recepção livre e gratuita
|
||||
410011|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimentos de ouro, quando definido em lei como ativo financeiro ou instrumento cambial
|
||||
410012|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimento de condomínio edilício não optante pelo regime regular
|
||||
410013|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Exportações de combustíveis
|
||||
410014|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimento de produtor rural não contribuinte
|
||||
410015|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimento por transportador autônomo não contribuinte
|
||||
410016|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimento ou aquisição de resíduos sólidos
|
||||
410017|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Aquisição de bem móvel com crédito presumido sob condição de revenda realizada
|
||||
410018|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Operações relacionadas aos fundos garantidores e executores de políticas públicas
|
||||
410019|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Exclusão da gorjeta na base de cálculo no fornecimento de alimentação
|
||||
410020|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Exclusão do valor de intermediação na base de cálculo no fornecimento de alimentação
|
||||
410021|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Contribuição de que trata o art. 149-A da Constituição Federal
|
||||
410022|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Consolidação da propriedade do bem pelo credor
|
||||
410023|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Alienação de bens móveis ou imóveis que tenham sido objeto de garantia em que o prestador da garantia não seja contribuinte
|
||||
410024|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Consolidação da propriedade do bem pelo grupo de consórcio
|
||||
410025|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Alienação de bem que tenha sido objeto de garantia em que o prestador da garantia não seja contribuinte
|
||||
410026|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Doação com anulação de crédito
|
||||
410027|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Exportação de serviço ou de bem imaterial
|
||||
410028|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Operações com bens imóveis realizadas por pessoas físicas não consideradas contribuintes
|
||||
410029|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Operações acobertadas somente pelo ICMS
|
||||
410030|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Estorno de crédito por perecimento, deteriorização, roubo, furto ou extravio.
|
||||
410031|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimento em período anterior ao início de vigência de incidências de CBS e IBS
|
||||
410032|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Tributos incidentes na operação que não integram a base de cálculo do IBS e da CBS
|
||||
410033|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Operações de Fundos de Investimento Imobiliário (FII) e Fundos de Investimento nas Cadeias Produtivas do Agronegócio (Fiagro)
|
||||
410034|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Operações de fundos de investimento
|
||||
410035|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Fornecimento realizado por nanoempreendedor
|
||||
410036|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Descontos incondicionais
|
||||
410037|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Importação os bens materiais sem incidência de IBS e CBS
|
||||
410999|410|Sem alíquota|0|0|2026-01-01T00:00:00|||Operações não onerosas sem previsão de tributação, não especificadas anteriormente
|
||||
510001|510|Sem alíquota|0|0|2026-01-01T00:00:00||90281|Operações, sujeitas a diferimento, com energia elétrica, relativas à importação, geração, comercialização, distribuição e transmissão
|
||||
515001|515|Padrão|60|60|2026-01-01T00:00:00||9|Operações, sujeitas a diferimento, com insumos agropecuários e aquícolas (Anexo IX)
|
||||
550001|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Exportações de bens materiais
|
||||
550002|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Regime de Trânsito
|
||||
550003|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Regimes de Depósito (art. 85)
|
||||
550004|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Regimes de Depósito (art. 87)
|
||||
550005|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Regimes de Depósito (art. 87, Parágrafo único)
|
||||
550006|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Regimes de Permanência Temporária
|
||||
550007|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Regimes de Aperfeiçoamento
|
||||
550008|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Importação de bens para o Regime de Repetro-Temporário
|
||||
550009|550|Sem alíquota|0|0|2026-01-01T00:00:00|||GNL-Temporário
|
||||
550010|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Repetro-Permanente
|
||||
550011|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Repetro-Industrialização
|
||||
550012|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Repetro-Nacional
|
||||
550013|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Repetro-Entreposto
|
||||
550014|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Zona de Processamento de Exportação
|
||||
550015|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Regime Tributário para Incentivo à Modernização e à Ampliação da Estrutura Portuária
|
||||
550016|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Regime Especial de Incentivos para o Desenvolvimento da Infraestrutura
|
||||
550017|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Regime Tributário para Incentivo à Atividade Naval - Renaval (Art. 107, I)
|
||||
550018|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Desoneração da aquisição de bens de capital
|
||||
550019|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Importação de bem material por indústria incentivada para utilização na ZFM
|
||||
550020|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Áreas de livre comércio
|
||||
550021|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Industrialização destinada a exportações
|
||||
550022|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Regime Especial de Incentivos para a Produção de Hidrogênio de Baixa Emissão de Carbono (Rehidro)
|
||||
550023|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Operações com hidrocarbonetos líquidos derivados de petróleo não combustíveis ou de gás natural, inclusive nafta
|
||||
550024|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Regime Tributário para Incentivo à Atividade Naval - Renaval (Art. 107, II)
|
||||
550025|550|Sem alíquota|0|0|2026-01-01T00:00:00|||Regime Tributário para Incentivo à Atividade Naval - Renaval (Art. 107, III)
|
||||
620001|620|Uniforme setorial|0|0|2026-01-01T00:00:00||91721|Tributação monofásica sobre combustíveis
|
||||
620002|620|Uniforme setorial|0|0|2026-01-01T00:00:00||91722|Tributação monofásica com responsabilidade pela retenção sobre combustíveis
|
||||
620003|620|Uniforme setorial|0|0|2026-01-01T00:00:00||91723|Tributação monofásica com responsabilidade de retenção de tributos por terceiros
|
||||
620004|620|Uniforme setorial|0|0|2026-01-01T00:00:00||91724|Tributação monofásica sobre mistura de EAC com gasolina A em percentual superior ao obrigatório
|
||||
620005|620|Uniforme setorial|0|0|2026-01-01T00:00:00||91725|Tributação monofásica sobre mistura de EAC com gasolina A em percentual inferior ao obrigatório
|
||||
620006|620|Uniforme setorial|0|0|2026-01-01T00:00:00||91726|Tributação monofásica sobre combustíveis cobrada anteriormente
|
||||
620007|620|Sem alíquota|0|0|2026-01-01T00:00:00|||Perecimento, deteriorização, roubo, furto ou extravio no regime monofásico
|
||||
800001|800|Sem alíquota|0|0|2026-01-01T00:00:00|||Fusão, cisão ou incorporação
|
||||
800002|800|Sem alíquota|0|0|2026-01-01T00:00:00|||Transferência de crédito do associado, inclusive as cooperativas singulares
|
||||
810001|810|Sem alíquota|0|0|2026-01-01T00:00:00|||Crédito presumido de IBS sobre o valor apurado nos fornecimentos a partir da ZFM
|
||||
811001|811|Sem alíquota|0|0|2026-01-01T00:00:00|||Anulação de Crédito por Saídas Imunes/Isentas
|
||||
811002|811|Sem alíquota|0|0|2026-01-01T00:00:00|||Débitos de notas fiscais não processadas na apuração
|
||||
811003|811|Sem alíquota|0|0|2026-01-01T00:00:00|||Desenquadramento do Simples Nacional
|
||||
820001|820|Sem alíquota|0|0|2026-01-01T00:00:00||92341|Documento com informações de fornecimento de serviços de planos de assistência à saúde elencados no art. 234 da Lei Complementar nº214, de 2025
|
||||
820002|820|Sem alíquota|0|0|2026-01-01T00:00:00||92361|Documento com informações de fornecimento de serviços de planos de assistência funerária
|
||||
820003|820|Sem alíquota|0|0|2026-01-01T00:00:00||92431|Documento com informações de fornecimento de serviços de planos de assistência à saúde de animais domésticos
|
||||
820004|820|Sem alíquota|0|0|2026-01-01T00:00:00|||Documento com informações de prestação de serviços de consursos de prognósticos
|
||||
820005|820|Sem alíquota|0|0|2026-01-01T00:00:00|||Documento com informações de alienação de bens imóveis
|
||||
820006|820|Sem alíquota|0|0|2026-01-01T00:00:00||90111|Documento com informações de fornecimento de serviços de exploração de via
|
||||
820007|820|Sem alíquota|0|0|2026-01-01T00:00:00||91811|Documento com informações de fornecimento de serviços financeiros
|
||||
820008|820|Sem alíquota|0|0|2026-01-01T00:00:00|||Documento com informações de fornecimento de serviço continuado, mas com tributação realizada em fatura anterior
|
||||
820009|820|Sem alíquota|0|0|2026-01-01T00:00:00|||Cobrança relativa a fornecimentos declarados em outro documento
|
||||
830001|830|Sem alíquota|0|0|2026-01-01T00:00:00|||Documento com exclusão da BC da CBS e do IBS de energia elétrica fornecida pela distribuidora à UC
|
||||
@@ -0,0 +1,101 @@
|
||||
database,product,ncm,current_cclasstrib,proposed_cclasstrib,status,rule_id,reason,confidence_type,scenario
|
||||
TESTESOFICIAIS,000004,07069000,,,AMBIGUOUS,RTC-A1,ANNEX_XV_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
|
||||
TESTESOFICIAIS,000005,07069000,,,AMBIGUOUS,RTC-A1,ANNEX_XV_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
|
||||
TESTESOFICIAIS,000006,07069000,,,AMBIGUOUS,RTC-A1,ANNEX_XV_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
|
||||
TESTESOFICIAIS,000008,07069000,,,AMBIGUOUS,RTC-A1,ANNEX_XV_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
|
||||
TESTESOFICIAIS,000009,07069000,,,AMBIGUOUS,RTC-A1,ANNEX_XV_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
|
||||
TESTESOFICIAIS,000010,07069000,,,AMBIGUOUS,RTC-A1,ANNEX_XV_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
|
||||
TESTESOFICIAIS,000011,07069000,,,AMBIGUOUS,RTC-A1,ANNEX_XV_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
|
||||
TESTESOFICIAIS,000013,07069000,,,AMBIGUOUS,RTC-A1,ANNEX_XV_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
|
||||
TESTESOFICIAIS,000015,07133321,,,AMBIGUOUS,RTC-A3,SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
|
||||
TESTESOFICIAIS,000017,03061790,,,AMBIGUOUS,RTC-A1,ANNEX_VII_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
|
||||
TESTESOFICIAIS,000018,02071100,,,AMBIGUOUS,RTC-A1,ANNEX_I_NEEDS_OPERATION_OR_CONDITION,NONE,"WHATIF_NORMAL_ON (regime normal, RTC on)"
|
||||
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)"
|
||||
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)"
|
||||
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)"
|
||||
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)"
|
||||
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)"
|
||||
|
@@ -0,0 +1,941 @@
|
||||
{
|
||||
"mode": "READ_ONLY_DRY_RUN",
|
||||
"FIREBIRD_WRITES": 0,
|
||||
"WRITE_ATTEMPTS": 0,
|
||||
"WRITE_SUCCESSES": 0,
|
||||
"databases": {
|
||||
"TESTESOFICIAIS": {
|
||||
"crt": "1",
|
||||
"total_products": 521,
|
||||
"passes": {
|
||||
"AS_FOUND": {
|
||||
"crt": "1",
|
||||
"rtc_enabled": true,
|
||||
"products": 521,
|
||||
"resolution_summary": {
|
||||
"total": 521,
|
||||
"valid_existing": 0,
|
||||
"ready_to_apply": 0,
|
||||
"ambiguous": 0,
|
||||
"insufficient": 521,
|
||||
"conflicts": 0,
|
||||
"official_mismatch": 0,
|
||||
"not_applicable": 0,
|
||||
"other_blocked": 0,
|
||||
"by_rule": {
|
||||
"RTC-R1": 521
|
||||
},
|
||||
"by_reason": {
|
||||
"SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT": 521
|
||||
},
|
||||
"catalog_compatibility": {
|
||||
"compatibility": "MATERIAL_MISMATCH",
|
||||
"counts": {
|
||||
"MATCH": 131,
|
||||
"LOCAL_ONLY": 0,
|
||||
"OFFICIAL_ONLY": 19,
|
||||
"MISMATCH": 11,
|
||||
"EXPIRED": 3
|
||||
},
|
||||
"expired_present_locally": 3
|
||||
}
|
||||
},
|
||||
"by_status": {
|
||||
"INSUFFICIENT_EVIDENCE": 521
|
||||
},
|
||||
"rtc_blocker_types": {
|
||||
"CCLASSTRIB_MISSING_UNRESOLVED": 521
|
||||
},
|
||||
"AUTO_APPLY_CANDIDATES": 0,
|
||||
"context": {
|
||||
"rtc_param": "1",
|
||||
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
|
||||
"rtc_observed_at": "2026-09-25T00:11:38+00:00",
|
||||
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
|
||||
"official_catalog": {
|
||||
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
|
||||
"publication": "2026-06-22",
|
||||
"file": "cClassTrib 2026-06-22.xlsx"
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"WHATIF_RTC_ON": {
|
||||
"crt": "1",
|
||||
"rtc_enabled": true,
|
||||
"products": 521,
|
||||
"resolution_summary": {
|
||||
"total": 521,
|
||||
"valid_existing": 0,
|
||||
"ready_to_apply": 0,
|
||||
"ambiguous": 0,
|
||||
"insufficient": 521,
|
||||
"conflicts": 0,
|
||||
"official_mismatch": 0,
|
||||
"not_applicable": 0,
|
||||
"other_blocked": 0,
|
||||
"by_rule": {
|
||||
"RTC-R1": 521
|
||||
},
|
||||
"by_reason": {
|
||||
"SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT": 521
|
||||
},
|
||||
"catalog_compatibility": {
|
||||
"compatibility": "MATERIAL_MISMATCH",
|
||||
"counts": {
|
||||
"MATCH": 131,
|
||||
"LOCAL_ONLY": 0,
|
||||
"OFFICIAL_ONLY": 19,
|
||||
"MISMATCH": 11,
|
||||
"EXPIRED": 3
|
||||
},
|
||||
"expired_present_locally": 3
|
||||
}
|
||||
},
|
||||
"by_status": {
|
||||
"INSUFFICIENT_EVIDENCE": 521
|
||||
},
|
||||
"rtc_blocker_types": {
|
||||
"CCLASSTRIB_MISSING_UNRESOLVED": 521
|
||||
},
|
||||
"AUTO_APPLY_CANDIDATES": 0,
|
||||
"context": {
|
||||
"rtc_param": "1",
|
||||
"rtc_source": "SIMULATION",
|
||||
"rtc_observed_at": "2026-09-25T00:11:38+00:00",
|
||||
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
|
||||
"official_catalog": {
|
||||
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
|
||||
"publication": "2026-06-22",
|
||||
"file": "cClassTrib 2026-06-22.xlsx"
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"WHATIF_NORMAL_ON": {
|
||||
"crt": "3",
|
||||
"rtc_enabled": true,
|
||||
"products": 521,
|
||||
"resolution_summary": {
|
||||
"total": 521,
|
||||
"valid_existing": 0,
|
||||
"ready_to_apply": 280,
|
||||
"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
|
||||
},
|
||||
"by_reason": {
|
||||
"ANNEX_XV_NEEDS_OPERATION_OR_CONDITION": 14,
|
||||
"SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD": 10,
|
||||
"NO_VALID_NCM": 26,
|
||||
"ANNEX_VII_NEEDS_OPERATION_OR_CONDITION": 10,
|
||||
"ANNEX_I_NEEDS_OPERATION_OR_CONDITION": 14,
|
||||
"NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER": 203,
|
||||
"ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS": 75,
|
||||
"ANNEX_XV_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS": 2,
|
||||
"LISTED_IN_MORE_THAN_ONE_ANNEX": 164,
|
||||
"ANNEX_V_NEEDS_OPERATION_OR_CONDITION": 3
|
||||
},
|
||||
"catalog_compatibility": {
|
||||
"compatibility": "MATERIAL_MISMATCH",
|
||||
"counts": {
|
||||
"MATCH": 131,
|
||||
"LOCAL_ONLY": 0,
|
||||
"OFFICIAL_ONLY": 19,
|
||||
"MISMATCH": 11,
|
||||
"EXPIRED": 3
|
||||
},
|
||||
"expired_present_locally": 3
|
||||
}
|
||||
},
|
||||
"by_status": {
|
||||
"AMBIGUOUS": 215,
|
||||
"INSUFFICIENT_EVIDENCE": 26,
|
||||
"RESOLVED_WITH_EXPLICIT_RULE": 280
|
||||
},
|
||||
"rtc_blocker_types": {
|
||||
"CCLASSTRIB_AMBIGUOUS": 215,
|
||||
"RTC_RULE_EVIDENCE_MISSING": 26,
|
||||
"CCLASSTRIB_MISSING": 280
|
||||
},
|
||||
"AUTO_APPLY_CANDIDATES": 280,
|
||||
"context": {
|
||||
"rtc_param": "1",
|
||||
"rtc_source": "SIMULATION",
|
||||
"rtc_observed_at": "2026-09-25T00:11:38+00:00",
|
||||
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
|
||||
"official_catalog": {
|
||||
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
|
||||
"publication": "2026-06-22",
|
||||
"file": "cClassTrib 2026-06-22.xlsx"
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CONSTRUCAO": {
|
||||
"crt": "1",
|
||||
"total_products": 3712,
|
||||
"passes": {
|
||||
"AS_FOUND": {
|
||||
"crt": "1",
|
||||
"rtc_enabled": true,
|
||||
"products": 3712,
|
||||
"resolution_summary": {
|
||||
"total": 3712,
|
||||
"valid_existing": 0,
|
||||
"ready_to_apply": 0,
|
||||
"ambiguous": 0,
|
||||
"insufficient": 3712,
|
||||
"conflicts": 0,
|
||||
"official_mismatch": 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": 131,
|
||||
"LOCAL_ONLY": 0,
|
||||
"OFFICIAL_ONLY": 19,
|
||||
"MISMATCH": 11,
|
||||
"EXPIRED": 3
|
||||
},
|
||||
"expired_present_locally": 3
|
||||
}
|
||||
},
|
||||
"by_status": {
|
||||
"INSUFFICIENT_EVIDENCE": 3712
|
||||
},
|
||||
"rtc_blocker_types": {
|
||||
"CCLASSTRIB_MISSING_UNRESOLVED": 3712
|
||||
},
|
||||
"AUTO_APPLY_CANDIDATES": 0,
|
||||
"context": {
|
||||
"rtc_param": "1",
|
||||
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
|
||||
"rtc_observed_at": "2026-09-25T00:11:39+00:00",
|
||||
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
|
||||
"official_catalog": {
|
||||
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
|
||||
"publication": "2026-06-22",
|
||||
"file": "cClassTrib 2026-06-22.xlsx"
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"WHATIF_RTC_ON": {
|
||||
"crt": "1",
|
||||
"rtc_enabled": true,
|
||||
"products": 3712,
|
||||
"resolution_summary": {
|
||||
"total": 3712,
|
||||
"valid_existing": 0,
|
||||
"ready_to_apply": 0,
|
||||
"ambiguous": 0,
|
||||
"insufficient": 3712,
|
||||
"conflicts": 0,
|
||||
"official_mismatch": 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": 131,
|
||||
"LOCAL_ONLY": 0,
|
||||
"OFFICIAL_ONLY": 19,
|
||||
"MISMATCH": 11,
|
||||
"EXPIRED": 3
|
||||
},
|
||||
"expired_present_locally": 3
|
||||
}
|
||||
},
|
||||
"by_status": {
|
||||
"INSUFFICIENT_EVIDENCE": 3712
|
||||
},
|
||||
"rtc_blocker_types": {
|
||||
"CCLASSTRIB_MISSING_UNRESOLVED": 3712
|
||||
},
|
||||
"AUTO_APPLY_CANDIDATES": 0,
|
||||
"context": {
|
||||
"rtc_param": "1",
|
||||
"rtc_source": "SIMULATION",
|
||||
"rtc_observed_at": "2026-09-25T00:11:39+00:00",
|
||||
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
|
||||
"official_catalog": {
|
||||
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
|
||||
"publication": "2026-06-22",
|
||||
"file": "cClassTrib 2026-06-22.xlsx"
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"WHATIF_NORMAL_ON": {
|
||||
"crt": "3",
|
||||
"rtc_enabled": true,
|
||||
"products": 3712,
|
||||
"resolution_summary": {
|
||||
"total": 3712,
|
||||
"valid_existing": 0,
|
||||
"ready_to_apply": 2725,
|
||||
"ambiguous": 973,
|
||||
"insufficient": 14,
|
||||
"conflicts": 0,
|
||||
"official_mismatch": 0,
|
||||
"not_applicable": 0,
|
||||
"other_blocked": 0,
|
||||
"by_rule": {
|
||||
"RTC-R4": 2725,
|
||||
"RTC-A1": 255,
|
||||
"RTC-R2": 14,
|
||||
"RTC-A3": 682,
|
||||
"RTC-A2": 36
|
||||
},
|
||||
"by_reason": {
|
||||
"NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER": 2725,
|
||||
"ANNEX_IX_NEEDS_OPERATION_OR_CONDITION": 63,
|
||||
"NO_VALID_NCM": 14,
|
||||
"SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD": 682,
|
||||
"ANNEX_IV_NEEDS_OPERATION_OR_CONDITION": 179,
|
||||
"LISTED_IN_MORE_THAN_ONE_ANNEX": 36,
|
||||
"ANNEX_XI_NEEDS_OPERATION_OR_CONDITION": 8,
|
||||
"ANNEX_V_NEEDS_OPERATION_OR_CONDITION": 1,
|
||||
"ANNEX_XIII_NEEDS_OPERATION_OR_CONDITION": 4
|
||||
},
|
||||
"catalog_compatibility": {
|
||||
"compatibility": "MATERIAL_MISMATCH",
|
||||
"counts": {
|
||||
"MATCH": 131,
|
||||
"LOCAL_ONLY": 0,
|
||||
"OFFICIAL_ONLY": 19,
|
||||
"MISMATCH": 11,
|
||||
"EXPIRED": 3
|
||||
},
|
||||
"expired_present_locally": 3
|
||||
}
|
||||
},
|
||||
"by_status": {
|
||||
"RESOLVED_WITH_EXPLICIT_RULE": 2725,
|
||||
"AMBIGUOUS": 973,
|
||||
"INSUFFICIENT_EVIDENCE": 14
|
||||
},
|
||||
"rtc_blocker_types": {
|
||||
"CCLASSTRIB_MISSING": 2725,
|
||||
"CCLASSTRIB_AMBIGUOUS": 973,
|
||||
"RTC_RULE_EVIDENCE_MISSING": 14
|
||||
},
|
||||
"AUTO_APPLY_CANDIDATES": 2725,
|
||||
"context": {
|
||||
"rtc_param": "1",
|
||||
"rtc_source": "SIMULATION",
|
||||
"rtc_observed_at": "2026-09-25T00:11:40+00:00",
|
||||
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
|
||||
"official_catalog": {
|
||||
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
|
||||
"publication": "2026-06-22",
|
||||
"file": "cClassTrib 2026-06-22.xlsx"
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"MMMODAS": {
|
||||
"crt": "1",
|
||||
"total_products": 20997,
|
||||
"passes": {
|
||||
"AS_FOUND": {
|
||||
"crt": "1",
|
||||
"rtc_enabled": false,
|
||||
"products": 20996,
|
||||
"resolution_summary": null,
|
||||
"by_status": {
|
||||
"NOT_EVALUATED": 20996
|
||||
},
|
||||
"rtc_blocker_types": {},
|
||||
"AUTO_APPLY_CANDIDATES": 0,
|
||||
"context": {
|
||||
"rtc_param": "0",
|
||||
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
|
||||
"rtc_observed_at": "2026-09-25T00:11:43+00:00",
|
||||
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
|
||||
"official_catalog": null,
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"WHATIF_RTC_ON": {
|
||||
"crt": "1",
|
||||
"rtc_enabled": true,
|
||||
"products": 20996,
|
||||
"resolution_summary": {
|
||||
"total": 20996,
|
||||
"valid_existing": 0,
|
||||
"ready_to_apply": 0,
|
||||
"ambiguous": 0,
|
||||
"insufficient": 20996,
|
||||
"conflicts": 0,
|
||||
"official_mismatch": 0,
|
||||
"not_applicable": 0,
|
||||
"other_blocked": 0,
|
||||
"by_rule": {
|
||||
"RTC-R1": 20996
|
||||
},
|
||||
"by_reason": {
|
||||
"SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT": 20996
|
||||
},
|
||||
"catalog_compatibility": {
|
||||
"compatibility": "MATERIAL_MISMATCH",
|
||||
"counts": {
|
||||
"MATCH": 131,
|
||||
"LOCAL_ONLY": 0,
|
||||
"OFFICIAL_ONLY": 19,
|
||||
"MISMATCH": 11,
|
||||
"EXPIRED": 3
|
||||
},
|
||||
"expired_present_locally": 3
|
||||
}
|
||||
},
|
||||
"by_status": {
|
||||
"INSUFFICIENT_EVIDENCE": 20996
|
||||
},
|
||||
"rtc_blocker_types": {
|
||||
"CCLASSTRIB_MISSING_UNRESOLVED": 20996
|
||||
},
|
||||
"AUTO_APPLY_CANDIDATES": 0,
|
||||
"context": {
|
||||
"rtc_param": "0",
|
||||
"rtc_source": "SIMULATION",
|
||||
"rtc_observed_at": "2026-09-25T00:11:44+00:00",
|
||||
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
|
||||
"official_catalog": {
|
||||
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
|
||||
"publication": "2026-06-22",
|
||||
"file": "cClassTrib 2026-06-22.xlsx"
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"WHATIF_NORMAL_ON": {
|
||||
"crt": "3",
|
||||
"rtc_enabled": true,
|
||||
"products": 20996,
|
||||
"resolution_summary": {
|
||||
"total": 20996,
|
||||
"valid_existing": 0,
|
||||
"ready_to_apply": 20831,
|
||||
"ambiguous": 49,
|
||||
"insufficient": 116,
|
||||
"conflicts": 0,
|
||||
"official_mismatch": 0,
|
||||
"not_applicable": 0,
|
||||
"other_blocked": 0,
|
||||
"by_rule": {
|
||||
"RTC-R3": 1,
|
||||
"RTC-R4": 20830,
|
||||
"RTC-R2": 116,
|
||||
"RTC-A3": 48,
|
||||
"RTC-A1": 1
|
||||
},
|
||||
"by_reason": {
|
||||
"ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS": 1,
|
||||
"NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER": 20830,
|
||||
"NO_VALID_NCM": 116,
|
||||
"SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD": 48,
|
||||
"ANNEX_XV_NEEDS_OPERATION_OR_CONDITION": 1
|
||||
},
|
||||
"catalog_compatibility": {
|
||||
"compatibility": "MATERIAL_MISMATCH",
|
||||
"counts": {
|
||||
"MATCH": 131,
|
||||
"LOCAL_ONLY": 0,
|
||||
"OFFICIAL_ONLY": 19,
|
||||
"MISMATCH": 11,
|
||||
"EXPIRED": 3
|
||||
},
|
||||
"expired_present_locally": 3
|
||||
}
|
||||
},
|
||||
"by_status": {
|
||||
"RESOLVED_WITH_EXPLICIT_RULE": 20831,
|
||||
"INSUFFICIENT_EVIDENCE": 116,
|
||||
"AMBIGUOUS": 49
|
||||
},
|
||||
"rtc_blocker_types": {
|
||||
"CCLASSTRIB_MISSING": 20831,
|
||||
"RTC_RULE_EVIDENCE_MISSING": 116,
|
||||
"CCLASSTRIB_AMBIGUOUS": 49
|
||||
},
|
||||
"AUTO_APPLY_CANDIDATES": 20831,
|
||||
"context": {
|
||||
"rtc_param": "0",
|
||||
"rtc_source": "SIMULATION",
|
||||
"rtc_observed_at": "2026-09-25T00:11:49+00:00",
|
||||
"local_catalog_hash": "1da77aec27fbde88488d1edf562f3af315ab2b5d06efb2e0b2f1ed606585d459",
|
||||
"official_catalog": {
|
||||
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
|
||||
"publication": "2026-06-22",
|
||||
"file": "cClassTrib 2026-06-22.xlsx"
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"RINCAO": {
|
||||
"crt": "3",
|
||||
"total_products": 894,
|
||||
"passes": {
|
||||
"AS_FOUND": {
|
||||
"crt": "3",
|
||||
"rtc_enabled": false,
|
||||
"products": 894,
|
||||
"resolution_summary": null,
|
||||
"by_status": {
|
||||
"NOT_EVALUATED": 894
|
||||
},
|
||||
"rtc_blocker_types": {},
|
||||
"AUTO_APPLY_CANDIDATES": 0,
|
||||
"context": {
|
||||
"rtc_param": "0",
|
||||
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
|
||||
"rtc_observed_at": "2026-09-25T00:11:50+00:00",
|
||||
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
|
||||
"official_catalog": null,
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"WHATIF_RTC_ON": {
|
||||
"crt": "3",
|
||||
"rtc_enabled": true,
|
||||
"products": 894,
|
||||
"resolution_summary": {
|
||||
"total": 894,
|
||||
"valid_existing": 0,
|
||||
"ready_to_apply": 530,
|
||||
"ambiguous": 364,
|
||||
"insufficient": 0,
|
||||
"conflicts": 0,
|
||||
"official_mismatch": 0,
|
||||
"not_applicable": 0,
|
||||
"other_blocked": 0,
|
||||
"by_rule": {
|
||||
"RTC-R4": 467,
|
||||
"RTC-A1": 129,
|
||||
"RTC-A2": 234,
|
||||
"RTC-R3": 63,
|
||||
"RTC-A3": 1
|
||||
},
|
||||
"by_reason": {
|
||||
"NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER": 467,
|
||||
"ANNEX_X_NEEDS_OPERATION_OR_CONDITION": 109,
|
||||
"LISTED_IN_MORE_THAN_ONE_ANNEX": 234,
|
||||
"ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS": 59,
|
||||
"ANNEX_I_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS": 2,
|
||||
"ANNEX_XV_NEEDS_OPERATION_OR_CONDITION": 16,
|
||||
"SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD": 1,
|
||||
"ANNEX_XV_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS": 2,
|
||||
"ANNEX_I_NEEDS_OPERATION_OR_CONDITION": 3,
|
||||
"ANNEX_VII_NEEDS_OPERATION_OR_CONDITION": 1
|
||||
},
|
||||
"catalog_compatibility": {
|
||||
"compatibility": "MATERIAL_MISMATCH",
|
||||
"counts": {
|
||||
"MATCH": 131,
|
||||
"LOCAL_ONLY": 0,
|
||||
"OFFICIAL_ONLY": 19,
|
||||
"MISMATCH": 11,
|
||||
"EXPIRED": 3
|
||||
},
|
||||
"expired_present_locally": 3
|
||||
}
|
||||
},
|
||||
"by_status": {
|
||||
"RESOLVED_WITH_EXPLICIT_RULE": 530,
|
||||
"AMBIGUOUS": 364
|
||||
},
|
||||
"rtc_blocker_types": {
|
||||
"CCLASSTRIB_MISSING": 530,
|
||||
"CCLASSTRIB_AMBIGUOUS": 364
|
||||
},
|
||||
"AUTO_APPLY_CANDIDATES": 530,
|
||||
"context": {
|
||||
"rtc_param": "0",
|
||||
"rtc_source": "SIMULATION",
|
||||
"rtc_observed_at": "2026-09-25T00:11:50+00:00",
|
||||
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
|
||||
"official_catalog": {
|
||||
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
|
||||
"publication": "2026-06-22",
|
||||
"file": "cClassTrib 2026-06-22.xlsx"
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"WHATIF_NORMAL_ON": {
|
||||
"crt": "3",
|
||||
"rtc_enabled": true,
|
||||
"products": 894,
|
||||
"resolution_summary": {
|
||||
"total": 894,
|
||||
"valid_existing": 0,
|
||||
"ready_to_apply": 530,
|
||||
"ambiguous": 364,
|
||||
"insufficient": 0,
|
||||
"conflicts": 0,
|
||||
"official_mismatch": 0,
|
||||
"not_applicable": 0,
|
||||
"other_blocked": 0,
|
||||
"by_rule": {
|
||||
"RTC-R4": 467,
|
||||
"RTC-A1": 129,
|
||||
"RTC-A2": 234,
|
||||
"RTC-R3": 63,
|
||||
"RTC-A3": 1
|
||||
},
|
||||
"by_reason": {
|
||||
"NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER": 467,
|
||||
"ANNEX_X_NEEDS_OPERATION_OR_CONDITION": 109,
|
||||
"LISTED_IN_MORE_THAN_ONE_ANNEX": 234,
|
||||
"ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS": 59,
|
||||
"ANNEX_I_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS": 2,
|
||||
"ANNEX_XV_NEEDS_OPERATION_OR_CONDITION": 16,
|
||||
"SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD": 1,
|
||||
"ANNEX_XV_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS": 2,
|
||||
"ANNEX_I_NEEDS_OPERATION_OR_CONDITION": 3,
|
||||
"ANNEX_VII_NEEDS_OPERATION_OR_CONDITION": 1
|
||||
},
|
||||
"catalog_compatibility": {
|
||||
"compatibility": "MATERIAL_MISMATCH",
|
||||
"counts": {
|
||||
"MATCH": 131,
|
||||
"LOCAL_ONLY": 0,
|
||||
"OFFICIAL_ONLY": 19,
|
||||
"MISMATCH": 11,
|
||||
"EXPIRED": 3
|
||||
},
|
||||
"expired_present_locally": 3
|
||||
}
|
||||
},
|
||||
"by_status": {
|
||||
"RESOLVED_WITH_EXPLICIT_RULE": 530,
|
||||
"AMBIGUOUS": 364
|
||||
},
|
||||
"rtc_blocker_types": {
|
||||
"CCLASSTRIB_MISSING": 530,
|
||||
"CCLASSTRIB_AMBIGUOUS": 364
|
||||
},
|
||||
"AUTO_APPLY_CANDIDATES": 530,
|
||||
"context": {
|
||||
"rtc_param": "0",
|
||||
"rtc_source": "SIMULATION",
|
||||
"rtc_observed_at": "2026-09-25T00:11:50+00:00",
|
||||
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
|
||||
"official_catalog": {
|
||||
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
|
||||
"publication": "2026-06-22",
|
||||
"file": "cClassTrib 2026-06-22.xlsx"
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UNIAO": {
|
||||
"crt": "1",
|
||||
"total_products": 8942,
|
||||
"passes": {
|
||||
"AS_FOUND": {
|
||||
"crt": "1",
|
||||
"rtc_enabled": false,
|
||||
"products": 8942,
|
||||
"resolution_summary": null,
|
||||
"by_status": {
|
||||
"NOT_EVALUATED": 8942
|
||||
},
|
||||
"rtc_blocker_types": {},
|
||||
"AUTO_APPLY_CANDIDATES": 0,
|
||||
"context": {
|
||||
"rtc_param": "0",
|
||||
"rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA",
|
||||
"rtc_observed_at": "2026-09-25T00:11:52+00:00",
|
||||
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
|
||||
"official_catalog": null,
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"WHATIF_RTC_ON": {
|
||||
"crt": "1",
|
||||
"rtc_enabled": true,
|
||||
"products": 8942,
|
||||
"resolution_summary": {
|
||||
"total": 8942,
|
||||
"valid_existing": 0,
|
||||
"ready_to_apply": 0,
|
||||
"ambiguous": 0,
|
||||
"insufficient": 8942,
|
||||
"conflicts": 0,
|
||||
"official_mismatch": 0,
|
||||
"not_applicable": 0,
|
||||
"other_blocked": 0,
|
||||
"by_rule": {
|
||||
"RTC-R1": 8942
|
||||
},
|
||||
"by_reason": {
|
||||
"SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT": 8942
|
||||
},
|
||||
"catalog_compatibility": {
|
||||
"compatibility": "MATERIAL_MISMATCH",
|
||||
"counts": {
|
||||
"MATCH": 131,
|
||||
"LOCAL_ONLY": 0,
|
||||
"OFFICIAL_ONLY": 19,
|
||||
"MISMATCH": 11,
|
||||
"EXPIRED": 3
|
||||
},
|
||||
"expired_present_locally": 3
|
||||
}
|
||||
},
|
||||
"by_status": {
|
||||
"INSUFFICIENT_EVIDENCE": 8942
|
||||
},
|
||||
"rtc_blocker_types": {
|
||||
"CCLASSTRIB_MISSING_UNRESOLVED": 8942
|
||||
},
|
||||
"AUTO_APPLY_CANDIDATES": 0,
|
||||
"context": {
|
||||
"rtc_param": "0",
|
||||
"rtc_source": "SIMULATION",
|
||||
"rtc_observed_at": "2026-09-25T00:11:52+00:00",
|
||||
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
|
||||
"official_catalog": {
|
||||
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
|
||||
"publication": "2026-06-22",
|
||||
"file": "cClassTrib 2026-06-22.xlsx"
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"WHATIF_NORMAL_ON": {
|
||||
"crt": "3",
|
||||
"rtc_enabled": true,
|
||||
"products": 8942,
|
||||
"resolution_summary": {
|
||||
"total": 8942,
|
||||
"valid_existing": 0,
|
||||
"ready_to_apply": 7419,
|
||||
"ambiguous": 1432,
|
||||
"insufficient": 91,
|
||||
"conflicts": 0,
|
||||
"official_mismatch": 0,
|
||||
"not_applicable": 0,
|
||||
"other_blocked": 0,
|
||||
"by_rule": {
|
||||
"RTC-R4": 6430,
|
||||
"RTC-R3": 989,
|
||||
"RTC-A1": 821,
|
||||
"RTC-A3": 296,
|
||||
"RTC-R2": 91,
|
||||
"RTC-A2": 315
|
||||
},
|
||||
"by_reason": {
|
||||
"NCM_IN_NO_ANNEX_AND_NO_NATURE_BASED_CHAPTER": 6430,
|
||||
"ANNEX_VII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS": 113,
|
||||
"ANNEX_IX_NEEDS_OPERATION_OR_CONDITION": 174,
|
||||
"SPECIAL_REGIME_DEPENDS_ON_NATURE_OR_OPERATION_OF_THE_GOOD": 296,
|
||||
"ANNEX_I_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS": 262,
|
||||
"ANNEX_I_NEEDS_OPERATION_OR_CONDITION": 196,
|
||||
"ANNEX_VII_NEEDS_OPERATION_OR_CONDITION": 193,
|
||||
"NO_VALID_NCM": 91,
|
||||
"LISTED_IN_MORE_THAN_ONE_ANNEX": 315,
|
||||
"ANNEX_VIII_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS": 580,
|
||||
"ANNEX_X_NEEDS_OPERATION_OR_CONDITION": 22,
|
||||
"ANNEX_IV_NEEDS_OPERATION_OR_CONDITION": 156,
|
||||
"ANNEX_XV_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS": 34,
|
||||
"ANNEX_II_NEEDS_OPERATION_OR_CONDITION": 11,
|
||||
"ANNEX_XV_NEEDS_OPERATION_OR_CONDITION": 42,
|
||||
"ANNEX_V_NEEDS_OPERATION_OR_CONDITION": 12,
|
||||
"ANNEX_XI_NEEDS_OPERATION_OR_CONDITION": 12,
|
||||
"ANNEX_VI_NEEDS_OPERATION_OR_CONDITION": 3
|
||||
},
|
||||
"catalog_compatibility": {
|
||||
"compatibility": "MATERIAL_MISMATCH",
|
||||
"counts": {
|
||||
"MATCH": 131,
|
||||
"LOCAL_ONLY": 0,
|
||||
"OFFICIAL_ONLY": 19,
|
||||
"MISMATCH": 11,
|
||||
"EXPIRED": 3
|
||||
},
|
||||
"expired_present_locally": 3
|
||||
}
|
||||
},
|
||||
"by_status": {
|
||||
"RESOLVED_WITH_EXPLICIT_RULE": 7419,
|
||||
"AMBIGUOUS": 1432,
|
||||
"INSUFFICIENT_EVIDENCE": 91
|
||||
},
|
||||
"rtc_blocker_types": {
|
||||
"CCLASSTRIB_MISSING": 7419,
|
||||
"CCLASSTRIB_AMBIGUOUS": 1432,
|
||||
"RTC_RULE_EVIDENCE_MISSING": 91
|
||||
},
|
||||
"AUTO_APPLY_CANDIDATES": 7419,
|
||||
"context": {
|
||||
"rtc_param": "0",
|
||||
"rtc_source": "SIMULATION",
|
||||
"rtc_observed_at": "2026-09-25T00:11:54+00:00",
|
||||
"local_catalog_hash": "be8c5899fff3e236c6c77bbb887087c0a2db048aeff763a5823676c7b478a151",
|
||||
"official_catalog": {
|
||||
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
|
||||
"publication": "2026-06-22",
|
||||
"file": "cClassTrib 2026-06-22.xlsx"
|
||||
},
|
||||
"catalog_integrity": {
|
||||
"classes": 145,
|
||||
"cst": 18,
|
||||
"orphan_classes": 0,
|
||||
"bad_class_format": 0,
|
||||
"bad_cst_format": 0,
|
||||
"duplicates": 0,
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"official_catalog": {
|
||||
"file": "cClassTrib 2026-06-22.xlsx",
|
||||
"sha256": "1448cb63a41bdb67ea30b4e11f4dc200d9568542c6b9335b6cff87a4fd664654",
|
||||
"publication": "2026-06-22",
|
||||
"source": "Portal Nacional da NF-e (cClassTrib)",
|
||||
"parser": "rtc-cclasstrib-xlsx-v1"
|
||||
},
|
||||
"official_classes": 164,
|
||||
"lc214": {
|
||||
"document": "Lei Complementar 214/2025 compiled (through LC 227/2026)",
|
||||
"url": "https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp214.htm",
|
||||
"sha256": "5c7d59b3c49e5b7c066c2ad28bedf32791193a676ec1a163bec14bea5fa2f680",
|
||||
"accessed_at": "2026-09-24",
|
||||
"fetched_via": "Internet Archive copy of the official URL"
|
||||
},
|
||||
"annex_items_total_and_clean": {
|
||||
"I": [
|
||||
26,
|
||||
16
|
||||
],
|
||||
"II": [
|
||||
9,
|
||||
6
|
||||
],
|
||||
"III": [
|
||||
30,
|
||||
28
|
||||
],
|
||||
"IV": [
|
||||
105,
|
||||
98
|
||||
],
|
||||
"V": [
|
||||
3,
|
||||
3
|
||||
],
|
||||
"VI": [
|
||||
81,
|
||||
80
|
||||
],
|
||||
"VII": [
|
||||
17,
|
||||
7
|
||||
],
|
||||
"VIII": [
|
||||
7,
|
||||
7
|
||||
],
|
||||
"IX": [
|
||||
35,
|
||||
18
|
||||
],
|
||||
"X": [
|
||||
57,
|
||||
47
|
||||
],
|
||||
"XI": [
|
||||
2,
|
||||
0
|
||||
],
|
||||
"XII": [
|
||||
17,
|
||||
12
|
||||
],
|
||||
"XIII": [
|
||||
6,
|
||||
4
|
||||
],
|
||||
"XIV": [
|
||||
82,
|
||||
82
|
||||
],
|
||||
"XV": [
|
||||
6,
|
||||
4
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -51,3 +51,10 @@ Somente leitura. `FIREBIRD_WRITES=0`: a avaliação abre uma conexão `READ COMM
|
||||
| TESTESOFICIAIS | 1 | **ligada** | 521 | 0 | 521 (RTC) | 0 | 0% | 94,82% (494/521) |
|
||||
|
||||
Bancos além do piloto: medida só da parte fiscal (`--fiscal-only`, sem decisão do Hub). TESTESOFICIAIS: com a decisão do Hub (antes 495/521 = 95,01%). Bloqueios PIS/COFINS: TESTESOFICIAIS 1 produto (incidência SUBST.TRIBUTÁRIA com CST 01), RINCAO 1 produto (`049`/`000`), demais 0. Produtos com warning (não bloqueante): RINCAO 78 (`4`→`04`, não canônico), UNIAO 2 (CST `000`/`060` inválido, ignorado por ser Simples), TESTESOFICIAIS 1 (não canônico). Aplicabilidade: Simples ⇒ `NOT_APPLICABLE`; RINCAO ⇒ `OPTIONAL` (894); TESTESOFICIAIS 519 `NOT_APPLICABLE` + 2 `REQUIRED`.
|
||||
|
||||
|
||||
## RTC no motor único (cClassTrib) — atualização
|
||||
- **RTC ligada** ⇒ o Agent avalia cada produto com o resolver determinístico (`docs/cclasstrib_resolution.md`) e transforma o resultado em UM blocker de causa raiz por produto (mesma fila, categoria `RTC`): `CCLASSTRIB_MISSING` (pronto para preencher), `CCLASSTRIB_MISSING_UNRESOLVED`, `CCLASSTRIB_AMBIGUOUS`, `RTC_RULE_EVIDENCE_MISSING`, `RTC_LOCAL_CATALOG_OUTDATED`, `CCLASSTRIB_OFFICIAL_MISMATCH`, `CCLASSTRIB_CONFLICT`. **RTC desligada** ⇒ nenhuma avaliação, nenhum blocker, nada no painel além de "Desativada".
|
||||
- Cobertura continua UMA: produto só é completo quando o `CCLASSTRIB` é válido (ou seja, depois de preenchido pelo operador ou pelo auto-apply autorizado). Produtos "prontos para preenchimento" contam como bloqueados até a escrita.
|
||||
- O painel explica o 0%: "Reforma Tributária ativa — cClassTrib pendente em N produtos" + resumo Total / válidos / prontos / ambíguos-bloqueados.
|
||||
- Preenchimento automático: `docs/rtc_auto_apply.md` (desligado por padrão; três chaves; somente `CCLASSTRIB`; auditado).
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"artifact": "RELEASE_MANIFEST",
|
||||
"GTI_FISCAL_VERSION": "1.1.8",
|
||||
"GTI_FISCAL_VERSION": "1.1.9",
|
||||
"generated_at": "2026-09-24T14:13:15+00:00",
|
||||
"hub": {
|
||||
"version": "1.1.8",
|
||||
"version": "1.1.9",
|
||||
"api_compat": "1.0",
|
||||
"engine_version": "0.3.0",
|
||||
"decision_version": "mission-04c-ipi-ex-split"
|
||||
},
|
||||
"agent_version": "1.1.8",
|
||||
"agent_version": "1.1.9",
|
||||
"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": "0026_central_processing_control",
|
||||
"applied_revision": "0026_central_processing_control",
|
||||
"alembic_head_in_repo": "0027_rtc_auto_apply",
|
||||
"applied_revision": "0027_rtc_auto_apply",
|
||||
"applied_revision_source": "production Hub 10.0.10.117 (gti-fiscal-hub-fiscal-hub-1), verified 2026-09-24",
|
||||
"count": 26
|
||||
"count": 27
|
||||
},
|
||||
"datasets_active": [
|
||||
{
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.1.8
|
||||
1.1.9
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Build central/app/rtc_reference_data.py: the OFFICIAL cClassTrib catalog + the LC 214/2025 goods annexes as structured, hash-pinned reference data.
|
||||
|
||||
python scripts/build_rtc_reference_data.py
|
||||
|
||||
Inputs (all official, already downloaded/hashed):
|
||||
sources/raw/bootstrap_v1/cClassTrib 2026-06-22.xlsx Portal Nacional NF-e cClassTrib table (parsed with the Hub's own parser)
|
||||
sources_cache/wb_lc214.bin LC 214/2025 compiled (Planalto, through LC 227/2026), extracted to text
|
||||
|
||||
Nothing here decides anything: it only turns the published texts into data with provenance. Item texts that carry a condition/exception are
|
||||
marked `clean=False` so the resolver never treats them as a deterministic match.
|
||||
"""
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from central.app.datasets.bootstrap_v1_extra import parse_rtc_cclasstrib # noqa: E402
|
||||
|
||||
XLSX = ROOT / "sources" / "raw" / "bootstrap_v1" / "cClassTrib 2026-06-22.xlsx"
|
||||
LC214 = ROOT / "sources_cache" / "wb_lc214.bin"
|
||||
ROMAN = {"I": 1, "II": 2, "III": 3, "IV": 4, "V": 5, "VI": 6, "VII": 7, "VIII": 8, "IX": 9, "X": 10, "XI": 11, "XII": 12, "XIII": 13, "XIV": 14, "XV": 15}
|
||||
RESTRICTION = re.compile(r"exceto|ressalv|desde que|em conformidade|requisitos|somente|exclusiv|quando|observad|salvo|apenas|conforme|vedad|\ba\)|\bb\)|\bex\b|não", re.I)
|
||||
CODE = re.compile(r"\b(\d{2}\.\d{2}(?:\.\d{1,2}){0,2}|\d{4}(?:\.\d{1,2}){1,3})\b")
|
||||
|
||||
|
||||
def lc214_text():
|
||||
raw = LC214.read_bytes()
|
||||
text = raw.decode("latin-1", errors="replace")
|
||||
text = re.sub(r"<script.*?</script>|<style.*?</style>", "", text, flags=re.S)
|
||||
text = html.unescape(re.sub(r"<[^>]+>", " ", text))
|
||||
return raw, re.sub(r"[ \t\r\f\v]+", " ", text)
|
||||
|
||||
|
||||
def parse_annexes(text):
|
||||
heads = [(m.start(), m.group(1)) for m in re.finditer(r"\n[ \xa0]*ANEXO ([IVXL]+)[ \xa0]*\n", text)]
|
||||
blocks, seen = {}, set()
|
||||
for i, (pos, roman) in enumerate(heads):
|
||||
if roman in seen or roman not in ROMAN:
|
||||
continue
|
||||
# the goods annexes I..XV are the first occurrence of each numeral, before the Simples annexes (which repeat I..VII)
|
||||
seen.add(roman)
|
||||
end = heads[i + 1][0] if i + 1 < len(heads) else len(text)
|
||||
blocks[roman] = text[pos:end]
|
||||
out = {}
|
||||
for roman, block in blocks.items():
|
||||
head = " ".join(block[:600].split())
|
||||
items, current, expected = [], None, 1
|
||||
lines = [ln.strip() for ln in block.split("\n")]
|
||||
for idx, ln in enumerate(lines):
|
||||
if ln == str(expected):
|
||||
nxt = next((x for x in lines[idx + 1:idx + 8] if x), "")
|
||||
if nxt and not nxt.isdigit():
|
||||
current = {"item": expected, "text": []}
|
||||
items.append(current)
|
||||
expected += 1
|
||||
continue
|
||||
if current is not None and ln:
|
||||
current["text"].append(ln)
|
||||
parsed = []
|
||||
for it in items:
|
||||
desc = " ".join(" ".join(it["text"]).split())
|
||||
prefixes = sorted({re.sub(r"\D", "", c) for c in CODE.findall(desc)})
|
||||
clean = not RESTRICTION.search(desc) and bool(prefixes)
|
||||
parsed.append({"item": it["item"], "description": desc[:400], "prefixes": prefixes, "clean": clean})
|
||||
out[roman] = {"number": ROMAN[roman], "title": head[:200], "revoked": "Revogado" in head, "items": parsed}
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
payload = parse_rtc_cclasstrib(XLSX.read_bytes())
|
||||
official = []
|
||||
for e in payload.entries:
|
||||
ind = e["indicators"]
|
||||
official.append({"code": e["cclasstrib"], "cst": e["cst"], "name": (e["name"] or "")[:160], "tipo_aliquota": e["tipo_aliquota"], "p_red_ibs": e["p_red_ibs"], "p_red_cbs": e["p_red_cbs"],
|
||||
"start": (e["d_ini_vig"] or "")[:10] or None, "end": (e["d_fim_vig"] or "")[:10] or None, "anexo": e.get("anexo"), "ind_nfe": ind.get("indNFe") == "1", "ind_nfce": ind.get("indNFCe") == "1"})
|
||||
raw, text = lc214_text()
|
||||
annexes = parse_annexes(text)
|
||||
counts = {k: (len(v["items"]), sum(1 for i in v["items"] if i["clean"])) for k, v in annexes.items()}
|
||||
module = ROOT / "central" / "app" / "rtc_reference_data.py"
|
||||
catalog_meta = {"file": XLSX.name, "sha256": hashlib.sha256(XLSX.read_bytes()).hexdigest(), "publication": "2026-06-22", "source": "Portal Nacional da NF-e (cClassTrib)", "parser": payload.parser_version}
|
||||
lc_meta = {"document": "Lei Complementar 214/2025 compiled (through LC 227/2026)", "url": "https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp214.htm", "sha256": hashlib.sha256(raw).hexdigest(),
|
||||
"accessed_at": date.today().isoformat(), "fetched_via": "Internet Archive copy of the official URL"}
|
||||
header = '"""GENERATED by scripts/build_rtc_reference_data.py - do not edit. Official cClassTrib catalog and LC 214/2025 goods annexes with provenance."""' + "\n\n"
|
||||
body = "\n\n".join([
|
||||
"OFFICIAL_CATALOG = " + pformat(catalog_meta, width=160),
|
||||
"OFFICIAL_CCLASSTRIB = " + pformat(official, width=220),
|
||||
"LC214_SOURCE = " + pformat(lc_meta, width=160),
|
||||
"LC214_ANNEXES = " + pformat(annexes, width=220),
|
||||
]) + "\n"
|
||||
module.write_text(header + body, encoding="utf-8")
|
||||
(ROOT / "docs" / "sources" / "RTC_REFERENCE_MANIFEST_V1.json").write_text(json.dumps({"official_catalog": catalog_meta, "official_classes": len(official), "lc214": lc_meta, "annex_items_total_and_clean": counts}, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
print(len(official), "official classes;", "annex items (total, clean):", counts)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,140 @@
|
||||
"""COPY-ONLY write E2E for the 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.
|
||||
Writes go to the copy and nowhere else.
|
||||
"""
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts")); sys.path.insert(0, str(ROOT / "agent" / "src"))
|
||||
from dotenv import load_dotenv # noqa: E402
|
||||
|
||||
load_dotenv(ROOT / ".env")
|
||||
import fdb # noqa: E402
|
||||
import local_completion as lc # noqa: E402
|
||||
from central.app.operational_state import classify_product_state, summarize_operational_states # noqa: E402
|
||||
from gti_fiscal_agent import firebird_reader, unified_fiscal # noqa: E402
|
||||
|
||||
COPY = str((ROOT / "scratch_rtc_e2e" / "TESTESOFICIAIS_RTC_COPY.FDB").resolve())
|
||||
NCM_OK = set((ROOT / "docs" / "runs" / "ncm_valid_codes.txt").read_text().split())
|
||||
|
||||
|
||||
def guard(path):
|
||||
canon = os.path.normcase(os.path.abspath(path))
|
||||
if canon != os.path.normcase(COPY):
|
||||
raise SystemExit(f"REFUSED: {path} is not the disposable copy")
|
||||
real = {os.path.normcase(os.path.abspath(p)) for p in lc.allowlist().values()}
|
||||
if canon in real or "scratch_rtc_e2e" not in canon:
|
||||
raise SystemExit("REFUSED: real database")
|
||||
|
||||
|
||||
def connect(rw):
|
||||
kw = dict(database=COPY, user=os.environ["FB_USER"], password=os.environ["FB_PASSWORD"], fb_library_name=os.environ.get("FIREBIRD_CLIENT_LIBRARY"))
|
||||
if not rw:
|
||||
kw["isolation_level"] = fdb.ISOLATION_LEVEL_READ_COMMITED_RO
|
||||
return fdb.connect(**kw)
|
||||
|
||||
|
||||
def load_service():
|
||||
spec = importlib.util.spec_from_file_location("run_agent_service", ROOT / "agent" / "scripts" / "run_agent_service.py")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
def digits(v):
|
||||
return "".join(ch for ch in str(v or "") if ch.isdigit())
|
||||
|
||||
|
||||
def snapshot_table(cur, table, key, 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()}
|
||||
|
||||
|
||||
def coverage(env, company, ncm_valid, override=None):
|
||||
conn = firebird_reader._connect(env)
|
||||
try:
|
||||
out = unified_fiscal.assess_from_connection(conn, company, ncm_valid=ncm_valid, rtc_override=override)
|
||||
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)}
|
||||
|
||||
|
||||
def main():
|
||||
guard(COPY)
|
||||
service = load_service()
|
||||
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)
|
||||
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]
|
||||
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["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
|
||||
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)
|
||||
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"])
|
||||
(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])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,79 @@
|
||||
"""READ-ONLY dry-run of the deterministic cClassTrib resolver over real Firebird databases. Writes NOTHING to any database.
|
||||
|
||||
python scripts/rtc_resolution_dryrun.py --db TESTESOFICIAIS CONSTRUCAO ... [--out docs/runs/rtc_resolution_dryrun.json]
|
||||
|
||||
For each database, three passes over the same read-only snapshot:
|
||||
AS_FOUND the ERP's own RTC switch and the company's real CRT (what the Agent would report today)
|
||||
WHATIF_RTC_ON RTC forced ON (pure evaluator; PARAMETROS is never touched), real CRT
|
||||
WHATIF_NORMAL_ON RTC forced ON and regime normal (CRT 3): what the rules WOULD resolve once an official orientation exists for Simples
|
||||
NCM validity = the code exists in the active official NCM table exported from the Hub (docs/runs/ncm_valid_codes.txt).
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts")); sys.path.insert(0, str(ROOT / "agent" / "src"))
|
||||
from dotenv import load_dotenv # noqa: E402
|
||||
|
||||
load_dotenv(ROOT / ".env")
|
||||
import local_completion as lc # noqa: E402
|
||||
from gti_fiscal_agent import firebird_reader, unified_fiscal # noqa: E402
|
||||
from unified_coverage_e2e import resolved_crt # noqa: E402
|
||||
|
||||
NCM_OK = set((ROOT / "docs" / "runs" / "ncm_valid_codes.txt").read_text().split())
|
||||
|
||||
|
||||
def digits(value):
|
||||
return "".join(ch for ch in str(value or "") if ch.isdigit())
|
||||
|
||||
|
||||
def one_pass(env, company, override, ncm_valid):
|
||||
conn = firebird_reader._connect(env) # READ COMMITTED READ ONLY
|
||||
try:
|
||||
return unified_fiscal.assess_from_connection(conn, company, rtc_override=override, ncm_valid=ncm_valid)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(); ap.add_argument("--db", nargs="+", required=True); ap.add_argument("--out", default="docs/runs/rtc_resolution_dryrun.json"); ap.add_argument("--sample", type=int, default=100)
|
||||
a = ap.parse_args()
|
||||
report, sample = {}, []
|
||||
for name in a.db:
|
||||
path = lc.allowlist()[name]; env = lc.env_for(path)
|
||||
crt = resolved_crt(name, path)
|
||||
products = firebird_reader.read_products(env)
|
||||
ncm_valid = {str(p["CODIGO"]): digits(p.get("CLASSIFICACAO_FISCAL")) in NCM_OK for p in products}
|
||||
passes = {}
|
||||
for label, override, company in (("AS_FOUND", None, {"crt": crt}), ("WHATIF_RTC_ON", True, {"crt": crt}), ("WHATIF_NORMAL_ON", True, {"crt": "3"})):
|
||||
out = one_pass(env, company, override, ncm_valid)
|
||||
summ = out["context"].get("rtc_resolution")
|
||||
findings = out["findings"]
|
||||
by_status = Counter((f.get("rtc_resolution") or {}).get("status", "NOT_EVALUATED") for f in findings.values())
|
||||
conflicts = Counter(b["type"] for f in findings.values() for b in f["blockers"] if b["category"] == "RTC")
|
||||
passes[label] = {"crt": company["crt"], "rtc_enabled": out["context"]["rtc_enabled"], "products": len(findings), "resolution_summary": summ, "by_status": dict(by_status), "rtc_blocker_types": dict(conflicts),
|
||||
"AUTO_APPLY_CANDIDATES": sum(1 for f in findings.values() if (f.get("rtc_resolution") or {}).get("auto_apply_candidate") and not [b for b in f["blockers"] if b["type"] in ("CCLASSTRIB_CONFLICT",)])}
|
||||
if label == "WHATIF_NORMAL_ON" and len(sample) < a.sample:
|
||||
by_code = {str(p["CODIGO"]): p for p in products}
|
||||
for code, f in findings.items():
|
||||
res = f.get("rtc_resolution")
|
||||
if res and len(sample) < a.sample and (res["auto_apply_candidate"] or res["status"] == "AMBIGUOUS") and (len([s for s in sample if s["database"] == name]) < max(1, a.sample // len(a.db))):
|
||||
p = by_code.get(code, {})
|
||||
sample.append({"database": name, "product": code, "ncm": digits(p.get("CLASSIFICACAO_FISCAL")), "current_cclasstrib": (p.get("CCLASSTRIB") or "").strip() if p.get("CCLASSTRIB") else "", "proposed_cclasstrib": res["resolved_cclasstrib"] or "",
|
||||
"status": res["status"], "rule_id": res["rule_id"], "reason": res["reason"], "confidence_type": res["confidence_type"], "scenario": "WHATIF_NORMAL_ON (regime normal, RTC on)"})
|
||||
passes[label]["context"] = {k: out["context"][k] for k in ("rtc_param", "rtc_source", "rtc_observed_at", "local_catalog_hash", "official_catalog", "catalog_integrity")}
|
||||
report[name] = {"crt": crt, "total_products": len(products), "passes": passes}
|
||||
print(name, "crt", crt, {k: (v["rtc_enabled"], v["by_status"], v["AUTO_APPLY_CANDIDATES"]) for k, v in passes.items()}, flush=True)
|
||||
doc = {"mode": "READ_ONLY_DRY_RUN", "FIREBIRD_WRITES": 0, "WRITE_ATTEMPTS": 0, "WRITE_SUCCESSES": 0, "databases": report}
|
||||
(ROOT / a.out).write_text(json.dumps(doc, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
|
||||
with open(ROOT / "docs" / "runs" / "rtc_resolution_audit_sample.csv", "w", newline="", encoding="utf-8-sig") as fh:
|
||||
w = csv.DictWriter(fh, fieldnames=list(sample[0]) if sample else ["database"]); w.writeheader(); w.writerows(sample)
|
||||
print("sample rows:", len(sample))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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.8"
|
||||
assert v == GTI_FISCAL_VERSION == "1.1.9"
|
||||
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.8"' in (ROOT / "agent" / "scripts" / "run_agent_service.py").read_text(encoding="utf-8")
|
||||
assert '"AGENT_VERSION", "1.1.9"' 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"))
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""The RTC copy-write E2E may touch ONLY the disposable scratch copy; every real database path must be refused before any connection is opened."""
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _load():
|
||||
sys.path.insert(0, str(ROOT / "scripts")); sys.path.insert(0, str(ROOT / "agent" / "src"))
|
||||
spec = importlib.util.spec_from_file_location("rtc_copy_write_e2e", ROOT / "scripts" / "rtc_copy_write_e2e.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_guard_accepts_only_the_exact_scratch_copy_and_refuses_real_databases():
|
||||
module = _load()
|
||||
module.guard(module.COPY) # the copy itself is fine
|
||||
for path in (ROOT / "TESTESOFICIAIS.FDB", ROOT / "Banco" / "CONSTRUCAO.FDB", ROOT / "Banco" / "RINCAO.FDB", ROOT / "scratch_rtc_e2e" / "OTHER.FDB", Path("D:/PROJETOS/GTISISTEMAS/Banco/GTI.FDB")):
|
||||
with pytest.raises(SystemExit):
|
||||
module.guard(str(path))
|
||||
assert "scratch_rtc_e2e" in module.COPY
|
||||
|
||||
|
||||
def test_the_e2e_script_only_ever_connects_to_the_copy():
|
||||
text = (ROOT / "scripts" / "rtc_copy_write_e2e.py").read_text(encoding="utf-8")
|
||||
assert text.count("fdb.connect(") == 1 and "database=COPY" in text # the single connection helper is bound to the copy path
|
||||
Reference in New Issue
Block a user