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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-24 22:46:05 -03:00
co-authored by Claude Sonnet 5
parent 3c10422e81
commit 42dc172763
50 changed files with 2948 additions and 915 deletions
+34 -10
View File
@@ -44,7 +44,7 @@ def test_rtc_off_is_not_applicable_and_never_a_candidate():
def test_simples_is_insufficient_until_the_official_orientation_exists():
r = resolve("10063021", company=SIMPLES)
assert r.status == "INSUFFICIENT_EVIDENCE" and r.rule_id == "RTC-R1" and not r.auto_apply_candidate and r.source_references[-1]["document"].startswith("NT 2025.002")
assert r.status == "INSUFFICIENT_EVIDENCE" and r.rule_id == "RTC-R1" and not r.auto_apply_candidate and r.source_references[-1]["document"].startswith("Ato Conjunto RFB/CGIBS") and r.rule_class == "BLOCKED_BY_EVIDENCE"
assert resolve("10063021", company={"crt": None}).reason == "COMPANY_REGIME_UNKNOWN"
@@ -88,35 +88,59 @@ def test_nature_based_chapters_are_ambiguous():
assert r.status == "AMBIGUOUS" and r.rule_id in ("RTC-A3", "RTC-A1", "RTC-A2"), ncm
def test_residual_ncm_outside_every_annex_and_sensitive_chapter_is_integral_taxation():
def test_residual_ncm_outside_every_annex_is_only_a_suggestion_and_never_a_write_candidate():
r = resolve("39235000") # plastic closures: no annex, no nature-based regime
assert r.status == "RESOLVED_WITH_EXPLICIT_RULE" and r.resolved_cclasstrib == "000001" and r.resolved_cst_ibscbs == "000" and r.rule_id == "RTC-R4" and r.confidence_type == "EXCLUSION_OF_ALL_SPECIAL_REGIMES"
assert r.auto_apply_candidate and annex_matches("39235000") == []
assert r.status == "SUGGESTION_ONLY" and r.resolved_cclasstrib == "000001" and r.suggested_cclasstrib == "000001" and r.rule_id == "RTC-R4" and r.rule_class == "SUGGESTION_ONLY"
assert r.auto_apply_candidate is False and annex_matches("39235000") == [] and r.confidence_type == "EXCLUSION_OF_ALL_SPECIAL_REGIMES"
assert "not positive evidence" in r.evidence[0]["note"] and r.as_dict()["auto_apply_candidate"] is False
def test_only_auto_apply_safe_rules_can_authorize_a_write():
from central.app.cclasstrib_resolver import RULE_CLASSES
assert [rid for rid, cls in RULE_CLASSES.items() if cls == "AUTO_APPLY_SAFE"] == ["RTC-R3"]
assert RULE_CLASSES["RTC-R4"] == "SUGGESTION_ONLY" and RULE_CLASSES["RTC-R0"] == "VALIDATION_ONLY"
assert {c for c in RULE_CLASSES.values()} == {"AUTO_APPLY_SAFE", "VALIDATION_ONLY", "SUGGESTION_ONLY", "BLOCKED_BY_EVIDENCE"}
def test_regime_other_than_normal_never_reaches_a_positive_rule():
for company in ({"crt": "2"}, {"crt": "4"}, {"crt": "9"}, {"crt": ""}):
assert not resolve("10063021", company=company).auto_apply_candidate
def test_a_class_missing_or_different_in_the_local_catalog_is_never_proposed():
r = resolve("39235000", local=local_from_official(skip=("000001",)))
r = resolve("10063021", local=local_from_official(skip=("200003",)))
assert r.status == "INSUFFICIENT_EVIDENCE" and r.reason == "CLASS_NOT_IN_LOCAL_CATALOG" and not r.auto_apply_candidate
r2 = resolve("39235000", local=local_from_official(tweak={"000001": {"cst": "010"}}))
r2 = resolve("10063021", local=local_from_official(tweak={"200003": {"cst": "010"}}))
assert r2.reason == "LOCAL_OFFICIAL_MISMATCH" and not r2.auto_apply_candidate
r3 = resolve("10063021", local=local_from_official(tweak={"200003": {"ind_nfe": False, "ind_nfce": False}}))
assert r3.reason == "LOCAL_OFFICIAL_MISMATCH"
def test_invalid_local_catalog_blocks_every_proposal():
assert resolve("39235000", ctx={"rtc_enabled": True, "local_catalog_valid": False}).reason == "LOCAL_RTC_CATALOG_INVALID"
assert resolve("10063021", ctx={"rtc_enabled": True, "local_catalog_valid": False}).reason == "LOCAL_RTC_CATALOG_INVALID"
def test_catalog_comparison_classifies_every_code_and_flags_material_problems():
full = compare_catalogs(local_from_official(), TODAY)
assert full["counts"]["MATCH"] == 161 and full["counts"]["EXPIRED"] == 3 and full["compatibility"] == "PARTIAL_LOCAL_OUTDATED" # 220001-3 ended on 2026-01-01
assert full["counts"]["MATCH_SAFE"] == 161 and full["counts"]["EXPIRED"] == 3 and full["compatibility"] == "PARTIAL_LOCAL_OUTDATED" # 220001-3 ended on 2026-01-01
assert compare_catalogs(local_from_official(skip=("220001", "220002", "220003")), TODAY)["compatibility"] == "FULL"
outdated = compare_catalogs(local_from_official(skip=("000005", "200053")), TODAY)
assert outdated["counts"]["OFFICIAL_ONLY"] == 2 and outdated["compatibility"] == "PARTIAL_LOCAL_OUTDATED" and outdated["counts"]["MISMATCH"] == 0
assert outdated["counts"]["MISSING_LOCAL"] == 2 and outdated["compatibility"] == "PARTIAL_LOCAL_OUTDATED" and outdated["counts"]["MISMATCH"] == 0
local = local_from_official(tweak={"200003": {"p_red_ibs": "60"}})
local["999999"] = {"cst": "000", "p_red_ibs": "0", "p_red_cbs": "0", "ind_nfe": True, "ind_nfce": True}
bad = compare_catalogs(local, TODAY)
assert bad["per_code"]["200003"] == "MISMATCH" and bad["per_code"]["999999"] == "LOCAL_ONLY" and bad["compatibility"] == "MATERIAL_MISMATCH"
assert bad["per_code"]["200003"] == "MISMATCH" and bad["per_code"]["999999"] == "MISSING_OFFICIAL" and bad["compatibility"] == "MATERIAL_MISMATCH"
def test_non_material_differences_are_classified_but_not_treated_as_a_mismatch():
earlier = compare_catalogs(local_from_official(tweak={"200003": {"start": "2025-05-05"}}), TODAY) # SGV imports an earlier start: both in force today => same emission effect
assert earlier["per_code"]["200003"] == "MATCH_SAFE" and earlier["details"]["200003"] == ["VIGENCY_START_DIFFERS_BOTH_IN_FORCE"]
assert compare_catalogs(local_from_official(tweak={"200003": {"start": "2027-01-01"}}), TODAY)["per_code"]["200003"] == "MISMATCH"
assert compare_catalogs(local_from_official(tweak={"200003": {"end": "2026-01-01"}}), TODAY)["per_code"]["200003"] == "MISMATCH"
local = local_from_official(tweak={"200003": {"end": "2027-12-31"}}) # a scheduled local end the official table does not have
c = compare_catalogs(local, TODAY)
assert c["per_code"]["200003"] == "MATCH_WITH_NON_MATERIAL_DIFFERENCE" and c["counts"]["MISMATCH"] == 0
assert resolve("10063021", local=local).status == "INSUFFICIENT_EVIDENCE" # auto-apply requires MATCH_SAFE: a non-material difference still blocks the WRITE
def test_local_catalog_hash_is_stable_and_sensitive():
+76 -16
View File
@@ -594,27 +594,87 @@ def _installation(client, agent_id):
from central.app.models import Installation as Inst
with client._session_factory() as session:
row = session.scalar(select(Inst).where(Inst.agent_id == agent_id))
return bool(row.rtc_auto_apply_authorized)
return row.rtc_write_mode
def test_rtc_auto_apply_authorization_is_off_by_default_and_needs_csrf_and_a_typed_confirmation():
def test_rtc_write_mode_is_disabled_by_default_and_needs_csrf_valid_mode_and_a_typed_confirmation_for_automatic():
client, agent_id, _ = _panel_client()
assert _installation(client, agent_id) is False
assert _installation(client, agent_id) == "DISABLED"
csrf = client.cookies.get("gti_csrf")
url = f"/agents/{agent_id}/rtc-auto-apply"
assert client.post(url, data={"csrf_token": "wrong", "decision": "authorize", "confirm": "PREENCHER"}).status_code == 403
assert client.post(url, data={"csrf_token": csrf, "decision": "authorize"}).status_code == 422
assert client.post(url, data={"csrf_token": csrf, "decision": "authorize", "confirm": "sim"}).status_code == 422
assert _installation(client, agent_id) is False
assert client.post(url, data={"csrf_token": csrf, "decision": "authorize", "confirm": "preencher"}, follow_redirects=False).status_code == 303
assert _installation(client, agent_id) is True
assert client.post(url, data={"csrf_token": csrf, "decision": "revoke"}, follow_redirects=False).status_code == 303
assert _installation(client, agent_id) is False
assert client.post(url, data={"csrf_token": csrf, "decision": "other"}).status_code == 422
url = f"/agents/{agent_id}/rtc-mode"
assert client.post(url, data={"csrf_token": "wrong", "mode": "DRY_RUN"}).status_code == 403
assert client.post(url, data={"csrf_token": csrf, "mode": "ENABLED"}).status_code == 422
assert client.post(url, data={"csrf_token": csrf, "mode": "ENABLED", "confirm": "sim"}).status_code == 422
assert client.post(url, data={"csrf_token": csrf, "mode": "bogus"}).status_code == 422
assert _installation(client, agent_id) == "DISABLED"
for mode, extra in (("DRY_RUN", {}), ("APPROVAL_REQUIRED", {}), ("ENABLED", {"confirm": "preencher"}), ("DISABLED", {})):
assert client.post(url, data={"csrf_token": csrf, "mode": mode, **extra}, follow_redirects=False).status_code == 303
assert _installation(client, agent_id) == mode
from central.app.models import AuditEvent
with client._session_factory() as session:
actions = [a.action for a in session.scalars(select(AuditEvent).order_by(AuditEvent.id)).all() if a.action.startswith("RTC_AUTO_APPLY")]
assert actions == ["RTC_AUTO_APPLY_AUTHORIZE", "RTC_AUTO_APPLY_REVOKE"]
actions = [a.action for a in session.scalars(select(AuditEvent).order_by(AuditEvent.id)).all() if a.action.startswith("RTC_")]
assert actions == ["RTC_WRITE_MODE"] * 4
def _seed_rtc(client, agent_id, n=120):
from central.app.models import Installation as Inst, RtcCandidate, RtcWriteAudit
with client._session_factory() as session:
inst = session.scalar(select(Inst).where(Inst.agent_id == agent_id))
inst.rtc_write_mode = "APPROVAL_REQUIRED"
for i in range(n):
session.add(RtcCandidate(tenant_id=inst.tenant_id, installation_id=inst.id, product_code=f"C{i:04d}", description=f"PRODUTO {i}", ncm="10063021", proposed="200003", cst_ibscbs="200", rule_id="RTC-R3", basis="RTC-R3:ANNEX", status="PENDING"))
audit = RtcWriteAudit(tenant_id=inst.tenant_id, installation_id=inst.id, agent_id=agent_id, product_code="A1", field="CCLASSTRIB", outcome="APPLIED", old_value=None, new_value="200003", rule_id="RTC-R3", action="APPLY")
session.add(audit)
session.commit()
return audit.id
def test_rtc_control_page_paginates_candidates_on_the_server_and_shows_modes_counts_and_audit():
client, agent_id, _ = _panel_client()
_seed_rtc(client, agent_id)
page1 = client.get(f"/agents/{agent_id}/rtc").text
assert "Somente cClassTrib com regra fiscal determinística e catálogo validado pode ser preenchido automaticamente." in page1
for label in ("Desativada", "Dry-run", "Aprovação manual", "Automática"):
assert label in page1
assert "Pendentes (120)" in page1 and "C0000" in page1 and "C0049" in page1 and "C0050" not in page1 and "Página 1 de 3" in page1 and "Aplicar aprovados agora" in page1
page3 = client.get(f"/agents/{agent_id}/rtc?page=3").text
assert "C0119" in page3 and "C0099" not in page3 and "Página 3 de 3" in page3
assert "Reverter" in page3
def test_rtc_candidate_actions_approve_reject_apply_and_rollback_are_csrf_protected_and_scoped():
from central.app.models import Job, RtcCandidate, RtcWriteAudit
client, agent_id, _ = _panel_client()
audit_id = _seed_rtc(client, agent_id, n=10)
csrf = client.cookies.get("gti_csrf")
url = f"/agents/{agent_id}/rtc-candidates"
assert client.post(url, data={"csrf_token": "wrong", "action": "approve"}).status_code == 403
with client._session_factory() as session:
ids = [c.id for c in session.scalars(select(RtcCandidate).order_by(RtcCandidate.id)).all()]
assert client.post(url, content=f"csrf_token={csrf}&action=approve&ids={ids[0]}&ids={ids[1]}", headers={"content-type": "application/x-www-form-urlencoded"}, follow_redirects=False).status_code == 303
assert client.post(f"{url}?all=1", data={"csrf_token": csrf, "action": "approve"}, follow_redirects=False).status_code == 303
assert client.post(url, content=f"csrf_token={csrf}&action=reject&ids={ids[2]}", headers={"content-type": "application/x-www-form-urlencoded"}, follow_redirects=False).status_code == 303
with client._session_factory() as session:
counts = {c.product_code: c.status for c in session.scalars(select(RtcCandidate)).all()}
assert list(counts.values()).count("REJECTED") == 1 and list(counts.values()).count("APPROVED") == 9
jobs_before = len(session.scalars(select(Job)).all())
assert client.post(url, data={"csrf_token": csrf, "action": "apply_approved"}, follow_redirects=False).status_code == 303
with client._session_factory() as session:
job = session.scalars(select(Job).order_by(Job.id.desc())).first()
assert len(session.scalars(select(Job)).all()) == jobs_before + 1 and job.kind == "REPROCESS_PRODUCTS" and job.payload["reason"] == "RTC_APPLY_APPROVED" and len(job.payload["codigos"]) == 9
assert client.post(url, data={"csrf_token": csrf, "action": "nope"}).status_code == 422
roll = f"/agents/{agent_id}/rtc-rollback"
assert client.post(roll, data={"csrf_token": csrf, "audit_id": audit_id}).status_code == 422 # typed confirmation required
assert client.post(roll, data={"csrf_token": csrf, "audit_id": audit_id, "confirm": "reverter"}, follow_redirects=False).status_code == 303
assert client.post(roll, data={"csrf_token": csrf, "audit_id": audit_id, "confirm": "REVERTER"}).status_code == 409 # already requested: no duplicate
with client._session_factory() as session:
assert session.get(RtcWriteAudit, audit_id).rollback_state == "REQUESTED"
def test_apply_approved_is_refused_outside_the_approval_mode():
client, agent_id, _ = _panel_client()
csrf = client.cookies.get("gti_csrf")
assert client.post(f"/agents/{agent_id}/rtc-candidates", data={"csrf_token": csrf, "action": "apply_approved"}).status_code == 409
def test_panel_shows_rtc_products_summary_and_the_pending_message_only_when_rtc_is_on():
@@ -626,6 +686,6 @@ def test_panel_shows_rtc_products_summary_and_the_pending_message_only_when_rtc_
_complete_sync(client, agent_id, {"rtc_enabled": True, "rtc_supported": True, "rtc_catalog_valid": True, "rtc_resolution": resolution})
on = client.get(f"/agents/{agent_id}").text
assert "Reforma Tributária ativa — cClassTrib pendente em 521 produtos" in on and "prontos para preenchimento 280" in on and "ambíguos/bloqueados 241" in on
assert "Preenchimento automático" in on and "Desativado" in on and "digite PREENCHER" in on
assert "Preenchimento automático" in on and "Desativada" in on and f"/agents/{agent_id}/rtc" in on
for legacy in ("Escopo anterior", "Escopo ampliado"):
assert legacy not in on
+1 -1
View File
@@ -17,7 +17,7 @@ def test_health_is_unavailable_when_database_is_down():
def test_version_endpoint_reports_release_and_fiscal_versions():
body = TestClient(create_app(database_probe=lambda: True)).get("/version").json()
assert body["version"] == "1.1.9" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"]
assert body["version"] == "1.2.0" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"]
def test_production_refuses_the_dev_token(monkeypatch):
+87
View File
@@ -0,0 +1,87 @@
"""Security review of the centrally controlled cClassTrib write: who can change the mode, what the Agent can and cannot decide, tenant scoping, no secrets in audits."""
from sqlalchemy import select
from sqlalchemy.orm import Session
from central.app.models import AuditEvent, Installation, RtcCandidate, RtcWriteAudit
from central.tests.test_control_panel_pages import _panel_client, _seed_rtc
from central.tests.test_zero_touch_hub import H, OPERATIONAL, _client, age_last_sync, finish, hb
def test_new_rtc_routes_require_an_operator_session():
client, agent_id, _ = _panel_client()
client.cookies.clear()
for method, path in (("get", f"/agents/{agent_id}/rtc"), ("post", f"/agents/{agent_id}/rtc-mode"), ("post", f"/agents/{agent_id}/rtc-candidates"), ("post", f"/agents/{agent_id}/rtc-rollback")):
response = getattr(client, method)(path, follow_redirects=False)
assert response.status_code in (303, 401, 403), (path, response.status_code)
assert "login" in response.headers.get("location", "login") or response.status_code in (401, 403)
def test_default_mode_is_disabled_and_the_agent_cannot_choose_it():
client, engine = _client()
hb(client)
with Session(engine) as s:
assert s.scalars(select(Installation)).one().rtc_write_mode == "DISABLED"
job = client.get("/v1/jobs/next", headers=H).json()
assert job["authorizations"]["rtc_write_mode"] == "DISABLED"
# a forged result cannot change the mode, approve candidates or request rollbacks: none of those are read from the payload
forged = {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_write_mode": "ENABLED", "authorizations": {"rtc_write_mode": "ENABLED", "rtc_approved_codes": ["X"]},
"rtc_auto_apply": {"mode": "ENABLED", "hub_mode": "ENABLED", "preview": [{"product_code": "X", "proposed": "200003", "rule_id": "RTC-R3", "status": "APPROVED"}]}}
client.post(f"/v1/jobs/{job['job_id']}/complete", json={"result_summary": forged}, headers=H)
with Session(engine) as s:
assert s.scalars(select(Installation)).one().rtc_write_mode == "DISABLED"
assert s.scalars(select(RtcCandidate)).all() == [] # DISABLED stores no preview at all
def test_preview_from_the_agent_never_arrives_approved_and_never_approves_itself():
client, engine = _client()
hb(client)
with Session(engine) as s:
s.scalars(select(Installation)).one().rtc_write_mode = "APPROVAL_REQUIRED"
s.commit()
item = {"product_code": "X1", "description": "D", "ncm": "10063021", "current": "", "proposed": "200003", "cst": "200", "rule_id": "RTC-R3", "basis": "b", "status": "APPROVED"}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": {"evaluated_all": True}, "rtc_auto_apply": {"preview": [item]}})
with Session(engine) as s:
assert [c.status for c in s.scalars(select(RtcCandidate)).all()] == ["PENDING"]
age_last_sync(engine, 4000); hb(client)
assert client.get("/v1/jobs/next", headers=H).json()["authorizations"]["rtc_approved_codes"] == []
def test_a_rollback_result_cannot_touch_another_installations_audit():
client, engine = _client()
hb(client)
with Session(engine) as s:
inst = s.scalars(select(Installation)).one()
foreign = RtcWriteAudit(tenant_id=inst.tenant_id, installation_id=inst.id + 999, agent_id=inst.agent_id, product_code="F", field="CCLASSTRIB", outcome="APPLIED", new_value="200003", action="APPLY", rollback_state="REQUESTED")
# the FK is not enforced by the SQLite test DB; the scoping is what is under test
s.add(foreign); s.commit(); foreign_id = foreign.id
done = {"outcome": "ROLLED_BACK", "product_code": "F", "field": "CCLASSTRIB", "old_value": "200003", "new_value": None, "action": "ROLLBACK", "rollback_of": foreign_id}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"rollbacks": [done]}})
with Session(engine) as s:
assert s.get(RtcWriteAudit, foreign_id).rollback_state == "REQUESTED" # untouched
def test_rollback_and_candidate_actions_are_scoped_to_the_operators_installation_and_audited():
client, agent_id, _ = _panel_client()
_seed_rtc(client, agent_id, n=3)
csrf = client.cookies.get("gti_csrf")
with client._session_factory() as s:
other = RtcWriteAudit(tenant_id=1, installation_id=987654, agent_id=agent_id, product_code="Z", field="CCLASSTRIB", outcome="APPLIED", new_value="200003", action="APPLY")
s.add(other); s.commit(); other_id = other.id
assert client.post(f"/agents/{agent_id}/rtc-rollback", data={"csrf_token": csrf, "audit_id": other_id, "confirm": "REVERTER"}).status_code == 409
client.post(f"/agents/{agent_id}/rtc-candidates?all=1", data={"csrf_token": csrf, "action": "approve"}, follow_redirects=False)
with client._session_factory() as s:
actions = [a.action for a in s.scalars(select(AuditEvent)).all()]
assert "RTC_CANDIDATES_APPROVE" in actions
def test_audit_rows_never_carry_credentials_or_tokens():
client, engine = _client()
hb(client)
applied = {"outcome": "APPLIED", "product_code": "10", "field": "CCLASSTRIB", "old_value": None, "new_value": "200003", "rule_id": "RTC-R3", "action": "APPLY", "password": "p", "token": "t",
"evidence": {"reason": "r"}}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"applied": [applied]}})
with Session(engine) as s:
row = s.scalars(select(RtcWriteAudit)).one()
blob = repr([getattr(row, c.name) for c in RtcWriteAudit.__table__.columns])
assert "password" not in blob.lower() and "'t'" not in blob and "Bearer" not in blob
assert not any("pass" in c.name or "token" in c.name or "secret" in c.name for c in RtcWriteAudit.__table__.columns)
@@ -56,7 +56,8 @@ def test_rtc_enabled_needs_schema_and_erp_switch():
def test_legal_context_is_informational_and_never_creates_a_blocker():
simples, normal = legal_effective_context("1"), legal_effective_context("3")
assert simples["informational_only"] and "2027" in simples["ibs_cbs_in_documents"] and normal["ibs_cbs_in_documents"] == "MANDATORY_FROM_2026-01-01"
assert simples["informational_only"] and simples["ibs_cbs_in_documents"] == "MANDATORY_FROM_2027-01-01" and normal["ibs_cbs_in_documents"] == "MANDATORY_FROM_2026-08-03"
assert simples["cclasstrib_orientation"] == "PENDING_OFFICIAL_NT" and any("Ato Conjunto RFB/CGIBS n 4/2026" in x for x in simples["sources"])
assert legal_effective_context(None)["ibs_cbs_in_documents"] == "UNKNOWN_REGIME"
# the operational rule is the ERP switch only: a Simples company with RTC on is validated, one with RTC off is not, whatever the legal timing says
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, SIMPLES, ON)) == {"CCLASSTRIB_MISSING"} and kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, SIMPLES, OFF)) == set()
+61 -3
View File
@@ -270,15 +270,15 @@ def test_job_carries_the_central_rtc_authorization_default_off_and_follows_the_i
client, engine = _client()
hb(client)
job = client.get("/v1/jobs/next", headers=H).json()
assert job["authorizations"] == {"rtc_auto_apply": False} # never on by default
assert job["authorizations"] == {"rtc_write_mode": "DISABLED", "rtc_approved_codes": [], "rtc_rollbacks": []} # never on by default
client.post(f"/v1/jobs/{job['job_id']}/complete", json={"result_summary": {}}, headers=H)
with Session(engine) as s:
inst = s.scalars(select(Installation)).one()
inst.rtc_auto_apply_authorized = True
inst.rtc_write_mode = "ENABLED"
s.commit()
age_last_sync(engine, 4000)
hb(client)
assert client.get("/v1/jobs/next", headers=H).json()["authorizations"] == {"rtc_auto_apply": True}
assert client.get("/v1/jobs/next", headers=H).json()["authorizations"]["rtc_write_mode"] == "ENABLED"
def test_rtc_write_audits_are_persisted_with_server_side_identity_and_never_duplicated_per_job():
@@ -307,3 +307,61 @@ def test_report_only_jobs_leave_no_audit_rows():
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"mode": "REPORT_ONLY", "applied": [], "aborted": [], "ready_total": 280}})
with Session(engine) as s:
assert s.scalars(select(RtcWriteAudit)).all() == []
def _set_mode(engine, mode):
with Session(engine) as s:
s.scalars(select(Installation)).one().rtc_write_mode = mode
s.commit()
def test_preview_is_stored_only_when_the_central_mode_wants_it_and_approvals_travel_in_the_next_job():
from central.app.models import RtcCandidate
client, engine = _client()
hb(client)
preview = [{"product_code": f"P{i}", "description": f"ITEM {i}", "ncm": "10063021", "current": "", "proposed": "200003", "cst": "200", "rule_id": "RTC-R3", "basis": "RTC-R3:ANNEX"} for i in range(3)]
ctx = {"rtc_enabled": True, "evaluated_all": True}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": ctx, "rtc_auto_apply": {"mode": "REPORT_ONLY", "preview": preview, "preview_truncated": False}})
with Session(engine) as s:
assert s.scalars(select(RtcCandidate)).all() == [] # DISABLED: nothing is stored
_set_mode(engine, "APPROVAL_REQUIRED")
age_last_sync(engine, 4000); hb(client)
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": ctx, "rtc_auto_apply": {"mode": "APPROVAL_REQUIRED", "preview": preview, "preview_truncated": False}})
with Session(engine) as s:
rows = s.scalars(select(RtcCandidate).order_by(RtcCandidate.product_code)).all()
assert [(r.product_code, r.status, r.proposed) for r in rows] == [("P0", "PENDING", "200003"), ("P1", "PENDING", "200003"), ("P2", "PENDING", "200003")]
rows[1].status = "APPROVED"
s.commit()
age_last_sync(engine, 4000); hb(client)
job = client.get("/v1/jobs/next", headers=H).json()
assert job["authorizations"]["rtc_approved_codes"] == ["P1"] # only what a human approved
client.post(f"/v1/jobs/{job['job_id']}/complete", json={"result_summary": {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": ctx,
"rtc_auto_apply": {"mode": "APPROVAL_REQUIRED", "preview": [p for p in preview if p["product_code"] != "P2"], "preview_truncated": False,
"applied": [{"outcome": "APPLIED", "product_code": "P1", "field": "CCLASSTRIB", "old_value": None, "new_value": "200003", "rule_id": "RTC-R3", "action": "APPLY", "rule_version": "rtc-rules-v2",
"company_crt": "3", "authorization_origin": "HUB_MODE:APPROVAL_REQUIRED", "description_snapshot": "ITEM 1", "rtc_status": "ENABLED", "local_catalog_state": "MATCH_SAFE"}]}}}, headers=H)
with Session(engine) as s:
status = {r.product_code: r.status for r in s.scalars(select(RtcCandidate)).all()}
assert status == {"P0": "PENDING", "P1": "APPLIED", "P2": "STALE"} # applied is recorded; a product no longer eligible on a full evaluation becomes STALE
from central.app.models import RtcWriteAudit
audit = s.scalars(select(RtcWriteAudit)).one()
assert (audit.rule_version, audit.company_crt, audit.authorization_origin, audit.description_snapshot, audit.action, audit.local_catalog_state) == ("rtc-rules-v2", "3", "HUB_MODE:APPROVAL_REQUIRED", "ITEM 1", "APPLY", "MATCH_SAFE")
assert audit.installation_uuid
def test_rollback_request_travels_in_the_job_and_its_result_closes_the_original_audit():
from central.app.models import RtcWriteAudit
client, engine = _client()
hb(client)
applied = {"outcome": "APPLIED", "product_code": "P1", "field": "CCLASSTRIB", "old_value": None, "new_value": "200003", "rule_id": "RTC-R3", "action": "APPLY"}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"applied": [applied]}})
with Session(engine) as s:
audit = s.scalars(select(RtcWriteAudit)).one(); audit.rollback_state = "REQUESTED"; audit_id = audit.id
s.commit()
age_last_sync(engine, 4000); hb(client)
job = client.get("/v1/jobs/next", headers=H).json()
assert job["authorizations"]["rtc_rollbacks"] == [{"audit_id": audit_id, "product_code": "P1", "written_value": "200003", "rule_id": "RTC-R3"}]
done = {"outcome": "ROLLED_BACK", "product_code": "P1", "field": "CCLASSTRIB", "old_value": "200003", "new_value": None, "rule_id": "RTC-R3", "action": "ROLLBACK", "rollback_of": audit_id}
client.post(f"/v1/jobs/{job['job_id']}/complete", json={"result_summary": {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"rollbacks": [done]}}}, headers=H)
with Session(engine) as s:
rows = s.scalars(select(RtcWriteAudit).order_by(RtcWriteAudit.id)).all()
assert [(r.action, r.outcome, r.rollback_state, r.rollback_of) for r in rows] == [("APPLY", "APPLIED", "DONE", None), ("ROLLBACK", "ROLLED_BACK", None, audit_id)]