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:
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user