feat: deterministic cClassTrib resolver + gated CCLASSTRIB auto-apply (default OFF), RTC panel summary; 1.1.9
- resolver (rules RTC-R0..R4/A1..A3/G1..G2) over the official cClassTrib table 2026-06-22 and LC 214/2025 annexes; local vs official catalog comparison - ONE root-cause RTC blocker per product (missing/unresolved/ambiguous/conflict/official mismatch/catalog outdated/evidence missing) - auto-apply: C000025.CCLASSTRIB only, three switches (FIREBIRD_WRITES + RTC_AUTO_APPLY_ENABLED + Hub authorization), per-product re-read and re-decision, never overwrites, rowcount 1, audit table rtc_write_audits (migration 0027) - copy-only write E2E (280/280 applied, only CCLASSTRIB changed, 2nd run 0 writes, stale decision 0 writes); real databases untouched - docs: cclasstrib_resolution, rtc_auto_apply, unified_fiscal_completeness Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
from datetime import date
|
||||
|
||||
from central.app import rtc_reference_data as ref
|
||||
from central.app.cclasstrib_resolver import (ANNEX_DEFAULT_CLASS, annex_matches, compare_catalogs, local_catalog_hash, resolve_cclasstrib, verify_annex_class_map)
|
||||
|
||||
TODAY = date(2026, 9, 24)
|
||||
NORMAL, SIMPLES = {"crt": "3"}, {"crt": "1"}
|
||||
ON = {"rtc_enabled": True, "local_catalog_valid": True}
|
||||
|
||||
|
||||
def local_from_official(skip=(), tweak=None):
|
||||
rows = {}
|
||||
for r in ref.OFFICIAL_CCLASSTRIB:
|
||||
if r["code"] in skip:
|
||||
continue
|
||||
rows[r["code"]] = {"cst": r["cst"], "tipo_aliquota": r["tipo_aliquota"], "p_red_ibs": r["p_red_ibs"], "p_red_cbs": r["p_red_cbs"], "ind_nfe": r["ind_nfe"], "ind_nfce": r["ind_nfce"], "start": r["start"], "end": r["end"]}
|
||||
for code, change in (tweak or {}).items():
|
||||
rows[code] = {**rows[code], **change}
|
||||
return rows
|
||||
|
||||
|
||||
def product(ncm, valid=True, code="1"):
|
||||
return {"CODIGO": code, "CLASSIFICACAO_FISCAL": ncm, "ncm_valid": valid}
|
||||
|
||||
|
||||
def resolve(ncm, company=NORMAL, ctx=ON, local=None, valid=True):
|
||||
return resolve_cclasstrib(product(ncm, valid), company, ctx, local if local is not None else local_from_official(), today=TODAY)
|
||||
|
||||
|
||||
def test_annex_default_class_map_is_verified_against_the_official_table():
|
||||
assert verify_annex_class_map() == {annex: [] for annex in ANNEX_DEFAULT_CLASS}
|
||||
|
||||
|
||||
def test_reference_data_carries_provenance_and_the_expected_shape():
|
||||
assert len(ref.OFFICIAL_CCLASSTRIB) == 164 and ref.OFFICIAL_CATALOG["sha256"] and ref.LC214_SOURCE["sha256"] and ref.LC214_SOURCE["accessed_at"]
|
||||
assert {"I", "VII", "VIII", "IX", "XV"} <= set(ref.LC214_ANNEXES) and ref.LC214_ANNEXES["XIV"]["revoked"] is True
|
||||
assert len(ref.LC214_ANNEXES["I"]["items"]) == 26 and len(ref.LC214_ANNEXES["XV"]["items"]) == 6
|
||||
|
||||
|
||||
def test_rtc_off_is_not_applicable_and_never_a_candidate():
|
||||
r = resolve("10063021", ctx={"rtc_enabled": False, "local_catalog_valid": True})
|
||||
assert r.status == "NOT_APPLICABLE" and r.rule_id == "RTC-R0" and not r.auto_apply_candidate
|
||||
|
||||
|
||||
def test_simples_is_insufficient_until_the_official_orientation_exists():
|
||||
r = resolve("10063021", company=SIMPLES)
|
||||
assert r.status == "INSUFFICIENT_EVIDENCE" and r.rule_id == "RTC-R1" and not r.auto_apply_candidate and r.source_references[-1]["document"].startswith("NT 2025.002")
|
||||
assert resolve("10063021", company={"crt": None}).reason == "COMPANY_REGIME_UNKNOWN"
|
||||
|
||||
|
||||
def test_invalid_or_unconfirmed_ncm_is_insufficient():
|
||||
assert resolve("1234", valid=True).reason == "NO_VALID_NCM"
|
||||
assert resolve("10063021", valid=False).reason == "NO_VALID_NCM"
|
||||
|
||||
|
||||
def test_clean_annex_item_resolves_to_the_annex_default_class_with_evidence():
|
||||
r = resolve("10063021") # rice: Anexo I item 1 (subposicao 1006.30)
|
||||
assert r.status == "RESOLVED_WITH_EXPLICIT_RULE" and r.resolved_cclasstrib == "200003" and r.resolved_cst_ibscbs == "200" and r.rule_id == "RTC-R3" and r.auto_apply_candidate
|
||||
assert r.evidence[0]["annex"] == "I" and r.evidence[0]["clean"] is True and r.confidence_type == "ANNEX_TEXT_MATCH"
|
||||
assert r.source_references[0]["sha256"] == ref.LC214_SOURCE["sha256"] and r.inputs_used["ncm"] == "10063021"
|
||||
|
||||
|
||||
def test_conditional_annex_item_is_ambiguous_not_guessed():
|
||||
milk = next(i for i in ref.LC214_ANNEXES["I"]["items"] if not i["clean"] and i["prefixes"])
|
||||
ncm = milk["prefixes"][0].ljust(8, "0")
|
||||
r = resolve(ncm)
|
||||
assert r.status == "AMBIGUOUS" and r.rule_id == "RTC-A1" and not r.auto_apply_candidate and r.evidence[0]["clean"] is False
|
||||
|
||||
|
||||
def test_annex_without_a_unique_default_class_is_ambiguous():
|
||||
item = next(i for i in ref.LC214_ANNEXES["IV"]["items"] if i["clean"]) # medical devices: 60% class vs zero-rated public-buyer class
|
||||
r = resolve(item["prefixes"][0].ljust(8, "1"))
|
||||
assert r.status == "AMBIGUOUS" and "NEEDS_OPERATION_OR_CONDITION" in r.reason
|
||||
|
||||
|
||||
def test_revoked_annex_only_is_ambiguous():
|
||||
prefix = next(p for i in ref.LC214_ANNEXES["XIV"]["items"] for p in i["prefixes"] if len(p) == 8 and not annex_matches_other_than_xiv(p))
|
||||
assert resolve(prefix).rule_id == "RTC-A1" and resolve(prefix).reason == "LISTED_ONLY_IN_REVOKED_ANNEX"
|
||||
|
||||
|
||||
def annex_matches_other_than_xiv(prefix):
|
||||
return [h for h in annex_matches(prefix) if h["annex"] != "XIV"]
|
||||
|
||||
|
||||
def test_nature_based_chapters_are_ambiguous():
|
||||
for ncm in ("03028990", "30049099", "27101259", "49019900", "87032100", "90183100"):
|
||||
r = resolve(ncm)
|
||||
assert r.status == "AMBIGUOUS" and r.rule_id in ("RTC-A3", "RTC-A1", "RTC-A2"), ncm
|
||||
|
||||
|
||||
def test_residual_ncm_outside_every_annex_and_sensitive_chapter_is_integral_taxation():
|
||||
r = resolve("39235000") # plastic closures: no annex, no nature-based regime
|
||||
assert r.status == "RESOLVED_WITH_EXPLICIT_RULE" and r.resolved_cclasstrib == "000001" and r.resolved_cst_ibscbs == "000" and r.rule_id == "RTC-R4" and r.confidence_type == "EXCLUSION_OF_ALL_SPECIAL_REGIMES"
|
||||
assert r.auto_apply_candidate and annex_matches("39235000") == []
|
||||
|
||||
|
||||
def test_a_class_missing_or_different_in_the_local_catalog_is_never_proposed():
|
||||
r = resolve("39235000", local=local_from_official(skip=("000001",)))
|
||||
assert r.status == "INSUFFICIENT_EVIDENCE" and r.reason == "CLASS_NOT_IN_LOCAL_CATALOG" and not r.auto_apply_candidate
|
||||
r2 = resolve("39235000", local=local_from_official(tweak={"000001": {"cst": "010"}}))
|
||||
assert r2.reason == "LOCAL_OFFICIAL_MISMATCH" and not r2.auto_apply_candidate
|
||||
r3 = resolve("10063021", local=local_from_official(tweak={"200003": {"ind_nfe": False, "ind_nfce": False}}))
|
||||
assert r3.reason == "LOCAL_OFFICIAL_MISMATCH"
|
||||
|
||||
|
||||
def test_invalid_local_catalog_blocks_every_proposal():
|
||||
assert resolve("39235000", ctx={"rtc_enabled": True, "local_catalog_valid": False}).reason == "LOCAL_RTC_CATALOG_INVALID"
|
||||
|
||||
|
||||
def test_catalog_comparison_classifies_every_code_and_flags_material_problems():
|
||||
full = compare_catalogs(local_from_official(), TODAY)
|
||||
assert full["counts"]["MATCH"] == 161 and full["counts"]["EXPIRED"] == 3 and full["compatibility"] == "PARTIAL_LOCAL_OUTDATED" # 220001-3 ended on 2026-01-01
|
||||
assert compare_catalogs(local_from_official(skip=("220001", "220002", "220003")), TODAY)["compatibility"] == "FULL"
|
||||
outdated = compare_catalogs(local_from_official(skip=("000005", "200053")), TODAY)
|
||||
assert outdated["counts"]["OFFICIAL_ONLY"] == 2 and outdated["compatibility"] == "PARTIAL_LOCAL_OUTDATED" and outdated["counts"]["MISMATCH"] == 0
|
||||
local = local_from_official(tweak={"200003": {"p_red_ibs": "60"}})
|
||||
local["999999"] = {"cst": "000", "p_red_ibs": "0", "p_red_cbs": "0", "ind_nfe": True, "ind_nfce": True}
|
||||
bad = compare_catalogs(local, TODAY)
|
||||
assert bad["per_code"]["200003"] == "MISMATCH" and bad["per_code"]["999999"] == "LOCAL_ONLY" and bad["compatibility"] == "MATERIAL_MISMATCH"
|
||||
|
||||
|
||||
def test_local_catalog_hash_is_stable_and_sensitive():
|
||||
a, b = local_from_official(), local_from_official(tweak={"200003": {"p_red_ibs": "60"}})
|
||||
assert local_catalog_hash(a) == local_catalog_hash(local_from_official()) and local_catalog_hash(a) != local_catalog_hash(b)
|
||||
|
||||
|
||||
def test_resolution_is_deterministic_and_auditable():
|
||||
a, b = resolve("10063021"), resolve("10063021")
|
||||
assert a.as_dict() == b.as_dict() and {"rule_id", "inputs_used", "source_references", "evidence", "reason"} <= set(a.as_dict())
|
||||
@@ -587,3 +587,45 @@ def test_rtc_status_shows_where_and_when_the_agent_observed_it():
|
||||
assert "linhas de PARAMETROS divergentes" in conflict and ">Desativada<" in conflict
|
||||
for legacy in ("Escopo anterior", "Escopo ampliado", "Legacy coverage", "Expanded coverage"):
|
||||
assert legacy not in conflict
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- RTC auto-apply authorization (central, default OFF)
|
||||
def _installation(client, agent_id):
|
||||
from central.app.models import Installation as Inst
|
||||
with client._session_factory() as session:
|
||||
row = session.scalar(select(Inst).where(Inst.agent_id == agent_id))
|
||||
return bool(row.rtc_auto_apply_authorized)
|
||||
|
||||
|
||||
def test_rtc_auto_apply_authorization_is_off_by_default_and_needs_csrf_and_a_typed_confirmation():
|
||||
client, agent_id, _ = _panel_client()
|
||||
assert _installation(client, agent_id) is False
|
||||
csrf = client.cookies.get("gti_csrf")
|
||||
url = f"/agents/{agent_id}/rtc-auto-apply"
|
||||
assert client.post(url, data={"csrf_token": "wrong", "decision": "authorize", "confirm": "PREENCHER"}).status_code == 403
|
||||
assert client.post(url, data={"csrf_token": csrf, "decision": "authorize"}).status_code == 422
|
||||
assert client.post(url, data={"csrf_token": csrf, "decision": "authorize", "confirm": "sim"}).status_code == 422
|
||||
assert _installation(client, agent_id) is False
|
||||
assert client.post(url, data={"csrf_token": csrf, "decision": "authorize", "confirm": "preencher"}, follow_redirects=False).status_code == 303
|
||||
assert _installation(client, agent_id) is True
|
||||
assert client.post(url, data={"csrf_token": csrf, "decision": "revoke"}, follow_redirects=False).status_code == 303
|
||||
assert _installation(client, agent_id) is False
|
||||
assert client.post(url, data={"csrf_token": csrf, "decision": "other"}).status_code == 422
|
||||
from central.app.models import AuditEvent
|
||||
with client._session_factory() as session:
|
||||
actions = [a.action for a in session.scalars(select(AuditEvent).order_by(AuditEvent.id)).all() if a.action.startswith("RTC_AUTO_APPLY")]
|
||||
assert actions == ["RTC_AUTO_APPLY_AUTHORIZE", "RTC_AUTO_APPLY_REVOKE"]
|
||||
|
||||
|
||||
def test_panel_shows_rtc_products_summary_and_the_pending_message_only_when_rtc_is_on():
|
||||
client, agent_id, _ = _panel_client()
|
||||
_complete_sync(client, agent_id, {"rtc_enabled": False, "rtc_supported": True, "rtc_resolution": None})
|
||||
off = client.get(f"/agents/{agent_id}").text
|
||||
assert "Produtos RTC" not in off and "cClassTrib pendente" not in off and "Preenchimento automático" not in off
|
||||
resolution = {"total": 521, "valid_existing": 0, "ready_to_apply": 280, "ambiguous": 215, "insufficient": 26, "catalog_compatibility": {"compatibility": "PARTIAL_LOCAL_OUTDATED"}}
|
||||
_complete_sync(client, agent_id, {"rtc_enabled": True, "rtc_supported": True, "rtc_catalog_valid": True, "rtc_resolution": resolution})
|
||||
on = client.get(f"/agents/{agent_id}").text
|
||||
assert "Reforma Tributária ativa — cClassTrib pendente em 521 produtos" in on and "prontos para preenchimento 280" in on and "ambíguos/bloqueados 241" in on
|
||||
assert "Preenchimento automático" in on and "Desativado" in on and "digite PREENCHER" in on
|
||||
for legacy in ("Escopo anterior", "Escopo ampliado"):
|
||||
assert legacy not in on
|
||||
|
||||
@@ -17,7 +17,7 @@ def test_health_is_unavailable_when_database_is_down():
|
||||
|
||||
def test_version_endpoint_reports_release_and_fiscal_versions():
|
||||
body = TestClient(create_app(database_probe=lambda: True)).get("/version").json()
|
||||
assert body["version"] == "1.1.8" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"]
|
||||
assert body["version"] == "1.1.9" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"]
|
||||
|
||||
|
||||
def test_production_refuses_the_dev_token(monkeypatch):
|
||||
|
||||
@@ -264,3 +264,46 @@ def test_rtc_on_then_off_and_pis_fix_leave_no_stale_open_blockers_and_never_dupl
|
||||
with Session(engine) as s:
|
||||
rows = s.scalars(select(BlockedProduct)).all()
|
||||
assert len(rows) == 2 and [(r.product_code, r.resolved_at is None) for r in rows] == [("20", True), ("21", False)]
|
||||
|
||||
|
||||
def test_job_carries_the_central_rtc_authorization_default_off_and_follows_the_installation():
|
||||
client, engine = _client()
|
||||
hb(client)
|
||||
job = client.get("/v1/jobs/next", headers=H).json()
|
||||
assert job["authorizations"] == {"rtc_auto_apply": False} # never on by default
|
||||
client.post(f"/v1/jobs/{job['job_id']}/complete", json={"result_summary": {}}, headers=H)
|
||||
with Session(engine) as s:
|
||||
inst = s.scalars(select(Installation)).one()
|
||||
inst.rtc_auto_apply_authorized = True
|
||||
s.commit()
|
||||
age_last_sync(engine, 4000)
|
||||
hb(client)
|
||||
assert client.get("/v1/jobs/next", headers=H).json()["authorizations"] == {"rtc_auto_apply": True}
|
||||
|
||||
|
||||
def test_rtc_write_audits_are_persisted_with_server_side_identity_and_never_duplicated_per_job():
|
||||
from central.app.models import AgentEvent, RtcWriteAudit
|
||||
client, engine = _client()
|
||||
hb(client)
|
||||
applied = {"outcome": "APPLIED", "product_code": "10", "field": "CCLASSTRIB", "old_value": None, "new_value": "200003", "rule_id": "RTC-R3", "evidence": {"reason": "ANNEX_I_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS"},
|
||||
"official_source_version": {"cclasstrib_sha256": "ab" * 32}, "local_catalog_hash": "cd" * 32, "timestamp": "2026-09-24T20:00:00+00:00", "job_id": 999, "tenant_id": 12345, "agent_id": 777}
|
||||
aborted = {"outcome": "ABORTED", "product_code": "11", "field": "CCLASSTRIB", "reason": "RTC_NO_LONGER_ENABLED", "rule_id": "RTC-R4", "timestamp": "2026-09-24T20:00:01+00:00"}
|
||||
other_field = {"outcome": "APPLIED", "product_code": "12", "field": "IPI", "new_value": "1.0"} # anything but CCLASSTRIB is refused
|
||||
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"mode": "AUTO_APPLY", "applied": [applied, other_field], "aborted": [aborted], "aborted_reasons": {"RTC_NO_LONGER_ENABLED": 1}}})
|
||||
with Session(engine) as s:
|
||||
rows = s.scalars(select(RtcWriteAudit).order_by(RtcWriteAudit.id)).all()
|
||||
assert [(r.product_code, r.outcome, r.old_value, r.new_value, r.rule_id) for r in rows] == [("10", "APPLIED", None, "200003", "RTC-R3"), ("11", "ABORTED", None, None, "RTC-R4")]
|
||||
inst = s.scalars(select(Installation)).one()
|
||||
assert all(r.installation_id == inst.id and r.agent_id == inst.agent_id and r.tenant_id == inst.tenant_id for r in rows) # identity comes from the authenticated context, not the payload
|
||||
assert rows[0].local_catalog_hash == "cd" * 32 and rows[0].official_source_version["cclasstrib_sha256"] == "ab" * 32 and rows[0].job_id != 999
|
||||
events = [e for e in s.scalars(select(AgentEvent)).all() if e.event_type == "RTC_AUTO_APPLY"]
|
||||
assert len(events) == 1 and events[0].structured_context["applied"] == 1 and events[0].severity == "WARNING"
|
||||
|
||||
|
||||
def test_report_only_jobs_leave_no_audit_rows():
|
||||
from central.app.models import RtcWriteAudit
|
||||
client, engine = _client()
|
||||
hb(client)
|
||||
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"mode": "REPORT_ONLY", "applied": [], "aborted": [], "ready_total": 280}})
|
||||
with Session(engine) as s:
|
||||
assert s.scalars(select(RtcWriteAudit)).all() == []
|
||||
|
||||
Reference in New Issue
Block a user