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