fix: gate cClassTrib applicability by CRT and date

This commit is contained in:
gti
2026-09-25 17:47:04 -03:00
parent 95605c2ff5
commit 4a12640031
25 changed files with 498 additions and 72 deletions
+9 -4
View File
@@ -175,15 +175,20 @@ def rtc_status_view(summary, mode="DISABLED"):
except ValueError:
observed = None
source = context.get("rtc_source") or "PARAMETROS.USAR_REFORMA_TRIBUTARIA"
products = _rtc_products_view(context.get("rtc_resolution"), bool(context["rtc_enabled"]))
products = _rtc_products_view(context.get("rtc_resolution"), bool(context["rtc_enabled"]), context.get("cclasstrib_applicability"))
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 "")}
def _rtc_products_view(resolution, enabled):
def _rtc_products_view(resolution, enabled, cclasstrib_applicability=None):
"""Compact 'Produtos RTC' summary. None when RTC is off (RTC never shows up as a problem) or the Agent did not report a resolution."""
if not enabled or not isinstance(resolution, dict) or not resolution.get("total"):
if not enabled:
return None
if isinstance(cclasstrib_applicability, dict) and cclasstrib_applicability.get("state") == "NOT_APPLICABLE":
label = "cClassTrib: Aguardando confirmação do regime da empresa" if cclasstrib_applicability.get("basis") == "COMPANY_CRT_UNRESOLVED" else "cClassTrib: Não utilizado neste cliente"
return {"applicable": False, "label": label}
if not isinstance(resolution, dict) or not resolution.get("total"):
return None
total = int(resolution["total"])
valid = int(resolution.get("valid_existing", 0))
@@ -195,7 +200,7 @@ def _rtc_products_view(resolution, enabled):
other_reason_max = max((int(count or 0) for reason, count in reasons.items() if not str(reason).endswith("SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT")), default=0)
blocked = total - valid - ready
pending = total - valid
return {"total": total, "valid": valid, "ready": ready, "blocked": blocked, "pending": pending, "conflicts": conflicts, "catalog_incompatible": catalog, "suggestion_only": int(resolution.get("suggestion_only", 0)),
return {"applicable": True, "total": total, "valid": valid, "ready": ready, "blocked": blocked, "pending": pending, "conflicts": conflicts, "catalog_incompatible": catalog, "suggestion_only": int(resolution.get("suggestion_only", 0)),
"simple_orientation_count": simple_count, "simple_orientation_dominant": simple_count > 0 and simple_count > other_reason_max,
"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")}
+35 -8
View File
@@ -168,12 +168,38 @@ def _dedupe(items):
# ------------------------------------------------------------------------------------------------- RTC
CCLASSTRIB_EFFECTIVE_FROM = {
"3": date(2026, 8, 3),
"1": date(2027, 1, 1),
"2": date(2027, 1, 1),
"4": date(2027, 1, 1),
}
def rtc_enabled(caps: dict) -> bool:
"""RTC is in scope ONLY when the ERP is switched to the reform (PARAMETROS.USAR_REFORMA_TRIBUTARIA truthy) AND its schema can hold it."""
return bool(caps.get("HAS_PRODUCT_CCLASSTRIB") and caps.get("HAS_RTC_RELATIONSHIP") and caps.get("RTC_ENABLED_BY_PARAM"))
def cclasstrib_applicability(crt, rtc_enabled: bool, today: date | None = None) -> dict:
"""Company-level cClassTrib requiredness from live CRT and its effective date.
The ERP RTC switch scopes the feature; it does not make cClassTrib mandatory
for every company/product. Unknown regimes fail closed to NOT_APPLICABLE.
"""
today = today or date.today()
regime = _text(crt)
if not rtc_enabled:
return {"state": "NOT_APPLICABLE", "basis": "RTC_DISABLED_IN_ERP", "effective_from": None, "source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA"}
effective_from = CCLASSTRIB_EFFECTIVE_FROM.get(regime)
if effective_from is None:
return {"state": "NOT_APPLICABLE", "basis": "COMPANY_CRT_UNRESOLVED", "effective_from": None, "source": "C000004.CRT"}
if today < effective_from:
basis = "SIMPLES_CCLASSTRIB_NOT_EFFECTIVE" if regime in SIMPLES_FAMILY else "CCLASSTRIB_NOT_YET_EFFECTIVE"
return {"state": "NOT_APPLICABLE", "basis": basis, "effective_from": effective_from.isoformat(), "source": "C000004.CRT + RTC effective-date rules"}
return {"state": "REQUIRED", "basis": "CCLASSTRIB_EFFECTIVE_FOR_CRT", "effective_from": effective_from.isoformat(), "source": "C000004.CRT + RTC effective-date rules"}
def catalog_integrity(catalog: dict) -> dict:
"""catalog: {"classes": {cClassTrib: cst_or_None}, "cst": {code, ...}, "duplicates": int}."""
classes, cst = catalog.get("classes", {}), set(catalog.get("cst", ()))
@@ -202,11 +228,13 @@ def validate_cclasstrib(value, catalog: dict | None, today: date | None = None)
return "VALID" if cst is not None and str(cst) in set(catalog.get("cst", ())) else "CST_MISSING"
def evaluate_rtc(product: dict, rtc: dict | None, today: date | None = None) -> dict:
"""rtc: {"enabled": bool, "catalog": {...}|None, "catalog_valid": bool}. Disabled => NOT_APPLICABLE and NO blocker, whatever CCLASSTRIB holds."""
if not rtc or not rtc.get("enabled"):
return {"applicability": {"state": "NOT_APPLICABLE", "basis": "RTC_DISABLED_IN_ERP"}, "blockers": []}
app = {"state": "REQUIRED", "basis": "RTC_ENABLED_IN_ERP"}
def evaluate_rtc(product: dict, rtc: dict | None, today: date | None = None, crt=None) -> dict:
"""Validate cClassTrib only when RTC is on and the company's regime/date gate applies."""
app = rtc.get("cclasstrib_applicability") if isinstance(rtc, dict) else None
if not isinstance(app, dict):
app = cclasstrib_applicability(crt, bool(rtc and rtc.get("enabled")), today)
if app["state"] != "REQUIRED":
return {"applicability": app, "blockers": []}
if not rtc.get("catalog_valid"):
return {"applicability": app, "blockers": [("RTC_CATALOG_INVALID", "LOCAL_CATALOG_FAILED_INTEGRITY")]}
state = validate_cclasstrib(product.get("CCLASSTRIB"), rtc.get("catalog"), today)
@@ -222,7 +250,7 @@ def evaluate_product(product: dict, company: dict | None, rtc: dict | None, toda
Returns {"blockers": [{"type","category","reason"}], "applicability": {"PIS_COFINS": state, "RTC": state}, "mandatory_fields": [...], "needs_review": bool}."""
crt = (company or {}).get("crt")
pis = evaluate_pis_cofins(product, crt)
rtc_result = evaluate_rtc(product, rtc, today)
rtc_result = evaluate_rtc(product, rtc, today, crt)
blockers = [{"type": kind, "category": blocker_category(kind), "reason": reason} for kind, reason in pis["blockers"] + rtc_result["blockers"]]
warnings = [{"type": kind, "reason": reason} for kind, reason in pis["warnings"]]
mandatory = []
@@ -236,8 +264,7 @@ 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 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.
"""Summarize the broad IBS/CBS document timeline; cClassTrib requiredness has its own CRT/date gate above.
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,
+1 -1
View File
@@ -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.2.4"
GTI_FISCAL_VERSION = "1.2.5"
API_COMPAT_MAJOR_MINOR = "1.0" # an Agent 1.0.x/1.1.x talks to any Hub 1.0.x
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "gti-fiscal-hub"
version = "1.2.4"
version = "1.2.5"
requires-python = ">=3.10"
dependencies = [
"fastapi>=0.115,<1",
+1 -1
View File
@@ -8,7 +8,7 @@
<form method="post" action="/agents/{{ detail.agent_id }}/sync-now" style="display:inline"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button type="submit" class="primary-action" {% if not ops.enabled %}disabled title="Processamento pausado: retome para sincronizar"{% else %}title="Verifica apenas as alterações desde a última sincronização"{% endif %}>Sincronizar agora</button></form>
<a class="botao secondary" href="/agents/{{ detail.agent_id }}/reprocess-confirmation" title="Recalcula todos os produtos (pede confirmação)">Reprocessar tudo</a>
{% if ops.enabled %}<form method="post" action="/agents/{{ detail.agent_id }}/pause" style="display:inline"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button type="submit" class="caution" title="Mantém o heartbeat, mas não executa jobs">Pausar processamento</button></form>{% else %}<form method="post" action="/agents/{{ detail.agent_id }}/resume" style="display:inline"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button type="submit" class="primary-action" title="Volta a executar jobs">Retomar processamento</button></form>{% endif %}
</div></div><dl class="facts"><dt>Processamento</dt><dd><span class="badge {{ '' if ops.enabled else 'critical' }}">{{ "Ativo" if ops.enabled else "Pausado" }}</span> · sincronização automática a cada {{ ops.interval_minutes }} min</dd><dt>Última sincronização</dt><dd>{{ ops.last_sync or "ainda não sincronizado" }}{% if ops.last_changed is not none %} · {{ ops.last_changed }} produto(s) alterado(s){% endif %}</dd><dt>Sincronização agendada</dt><dd>{{ ops.next_sync or ("aguardando o primeiro sync" if ops.enabled else "pausado") }}</dd>{% if ops.rtc %}<dt>Reforma Tributária</dt><dd><span class="badge {{ "" if ops.rtc.enabled else "muted" }}" title="{{ ops.rtc.title }}">{{ ops.rtc.label }}</span></dd>{% endif %}{% 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 %}
</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 and not ops.rtc.products.applicable %}<dt>cClassTrib</dt><dd>{{ ops.rtc.products.label }}</dd>{% elif 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 %} · <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&amp;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>
+3 -1
View File
@@ -5,7 +5,9 @@
<p class="muted">{{ explanation }}</p>{% if not privileged %}<p class="muted"><strong>Somente leitura para o seu perfil:</strong> alterar para aprovação/automático, aprovar, aplicar e reverter exige administrador do GTI Gerenciador.</p>{% endif %}</div></div>
{% set rtc = ops.rtc %}
{% if rtc and rtc.products %}
{% if rtc and rtc.products and not rtc.products.applicable %}
<p class="muted" role="status">{{ rtc.products.label }}</p>
{% elif 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>
+14
View File
@@ -690,6 +690,20 @@ def test_panel_shows_rtc_products_summary_and_the_pending_message_only_when_rtc_
for legacy in ("Escopo anterior", "Escopo ampliado"):
assert legacy not in on
_complete_sync(client, agent_id, {
"rtc_enabled": True,
"rtc_supported": True,
"rtc_catalog_valid": True,
"cclasstrib_applicability": {"state": "NOT_APPLICABLE", "basis": "SIMPLES_CCLASSTRIB_NOT_EFFECTIVE"},
"rtc_resolution": {"total": 521, "valid_existing": 0, "ready_to_apply": 0, "insufficient": 521},
})
not_applicable = client.get(f"/agents/{agent_id}").text
assert "cClassTrib: Não utilizado neste cliente" in not_applicable
assert "cClassTrib pendente em 521 produtos" not in not_applicable
rtc_page = client.get(f"/agents/{agent_id}/rtc").text
assert "cClassTrib: Não utilizado neste cliente" in rtc_page
assert "cClassTrib pendente em 521 produtos" not in rtc_page
def test_rtc_control_page_shows_human_labels_and_a_catalog_column_never_raw_enums():
from central.app.panel_presentation import abort_reason_label, audit_outcome_label, candidate_status_label, catalog_state_label, blocker_reason_label, blocker_type_label, rtc_rule_label
+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.2.4" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"]
assert body["version"] == "1.2.5" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"]
def test_production_refuses_the_dev_token(monkeypatch):
@@ -606,3 +606,18 @@ def test_rtc_status_view_is_discreet_and_optional():
assert on["label"] == "Ativa" and on["enabled"] and on["catalog_valid"]
assert blocker_type_label("RTC_CST_MISSING") == "CST IBS/CBS ausente no catálogo" and blocker_type_label("COFINS_RATE_INVALID") == "Alíquota COFINS inválida"
assert blocker_type_label("PIS_CST_INCIDENCE_MISMATCH") == "CST PIS incompatível com a incidência"
def test_rtc_status_view_hides_product_pending_when_cclasstrib_is_not_applicable():
from central.app.panel_presentation import rtc_status_view
result = rtc_status_view({"fiscal_context": {
"rtc_enabled": True,
"rtc_resolution": {"total": 521, "valid_existing": 0, "ready_to_apply": 0, "insufficient": 521},
"cclasstrib_applicability": {"state": "NOT_APPLICABLE", "basis": "SIMPLES_CCLASSTRIB_NOT_EFFECTIVE"},
}})
assert result["enabled"] is True
assert result["products"]["applicable"] is False
assert result["products"]["label"] == "cClassTrib: Não utilizado neste cliente"
assert "pendente" not in result["products"].get("headline", "").lower()
+56 -10
View File
@@ -29,17 +29,17 @@ def test_rtc_off_ignores_cclasstrib_completely():
def test_rtc_on_requires_and_validates_cclasstrib():
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, SIMPLES, ON)) == {"CCLASSTRIB_MISSING"}
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "12x"}, SIMPLES, ON)) == {"CCLASSTRIB_INVALID_FORMAT"}
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "123456"}, SIMPLES, ON)) == {"CCLASSTRIB_NOT_IN_CATALOG"}
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "999999"}, SIMPLES, ON)) == {"RTC_CST_MISSING"} # class without CST
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "555555"}, SIMPLES, ON)) == {"RTC_CST_MISSING"} # class points to a CST that is not in RTC_CST (orphan)
ok = evaluate_product({**EMPTY, "CCLASSTRIB": "200003"}, SIMPLES, ON)
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, NORMAL, ON)) == {"CCLASSTRIB_MISSING"}
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "12x"}, NORMAL, ON)) == {"CCLASSTRIB_INVALID_FORMAT"}
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "123456"}, NORMAL, ON)) == {"CCLASSTRIB_NOT_IN_CATALOG"}
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "999999"}, NORMAL, ON)) == {"RTC_CST_MISSING"} # class without CST
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "555555"}, NORMAL, ON)) == {"RTC_CST_MISSING"} # class points to a CST that is not in RTC_CST (orphan)
ok = evaluate_product({**EMPTY, "CCLASSTRIB": "200003"}, NORMAL, ON)
assert ok["blockers"] == [] and ok["applicability"]["RTC"] == "REQUIRED" and "CCLASSTRIB" in ok["mandatory_fields"]
def test_rtc_on_with_invalid_catalog_blocks_with_a_single_clear_reason():
r = evaluate_product({**EMPTY, "CCLASSTRIB": "200003"}, SIMPLES, {"enabled": True, "catalog": CATALOG, "catalog_valid": False})
r = evaluate_product({**EMPTY, "CCLASSTRIB": "200003"}, NORMAL, {"enabled": True, "catalog": CATALOG, "catalog_valid": False})
assert kinds(r) == {"RTC_CATALOG_INVALID"}
@@ -54,13 +54,59 @@ def test_rtc_enabled_needs_schema_and_erp_switch():
assert rtc_enabled(caps) and not rtc_enabled({**caps, "RTC_ENABLED_BY_PARAM": False}) and not rtc_enabled({**caps, "HAS_RTC_RELATIONSHIP": False}) and not rtc_enabled({})
def test_cclasstrib_is_not_applicable_for_simples_before_its_effective_date_even_when_rtc_is_on():
result = evaluate_product({**EMPTY, "CCLASSTRIB": None}, SIMPLES, ON, date(2026, 9, 25))
assert result["applicability"]["RTC"] == "NOT_APPLICABLE"
assert result["basis"]["RTC"] == "SIMPLES_CCLASSTRIB_NOT_EFFECTIVE"
assert "CCLASSTRIB" not in result["mandatory_fields"]
assert not [blocker for blocker in result["blockers"] if blocker["category"] == "RTC"]
def test_not_applicable_cclasstrib_ignores_any_existing_value():
for value in (None, "", "12x", "999999", "200003"):
result = evaluate_product({**EMPTY, "CCLASSTRIB": value}, SIMPLES, ON, date(2026, 9, 25))
assert result["applicability"]["RTC"] == "NOT_APPLICABLE"
assert "CCLASSTRIB" not in result["mandatory_fields"]
assert not [blocker for blocker in result["blockers"] if blocker["category"] == "RTC"]
def test_cclasstrib_becomes_applicable_on_each_regime_effective_date():
for crt in ("1", "2", "4"):
before = evaluate_product({**EMPTY, "CCLASSTRIB": None}, {"crt": crt}, ON, date(2026, 12, 31))
effective = evaluate_product({**EMPTY, "CCLASSTRIB": None}, {"crt": crt}, ON, date(2027, 1, 1))
assert before["applicability"]["RTC"] == "NOT_APPLICABLE"
assert "CCLASSTRIB_MISSING" in kinds(effective)
normal_before = evaluate_product({**EMPTY, "CCLASSTRIB": None}, NORMAL, ON, date(2026, 8, 2))
normal_effective = evaluate_product({**EMPTY, "CCLASSTRIB": None}, NORMAL, ON, date(2026, 8, 3))
assert normal_before["applicability"]["RTC"] == "NOT_APPLICABLE"
assert "CCLASSTRIB_MISSING" in kinds(normal_effective)
def test_cclasstrib_is_not_applicable_when_company_regime_is_unresolved():
for company in (None, {"crt": None}, {"crt": "9"}):
result = evaluate_product({**EMPTY, "CCLASSTRIB": None}, company, ON, date(2026, 9, 25))
assert result["applicability"]["RTC"] == "NOT_APPLICABLE"
assert not [blocker for blocker in result["blockers"] if blocker["category"] == "RTC"]
def test_non_applicable_cclasstrib_does_not_mask_other_fiscal_blockers():
product = {**EMPTY, "CCLASSTRIB": None, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}
result = evaluate_product(product, SIMPLES, ON, date(2026, 9, 25))
assert kinds(result) == {"PIS_CST_MISSING", "COFINS_CST_MISSING"}
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 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()
# Global RTC support does not make Simples cClassTrib mandatory before its effective date.
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, SIMPLES, ON, date(2026, 9, 25))) == set()
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, NORMAL, ON, date(2026, 9, 25))) == {"CCLASSTRIB_MISSING"}
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, SIMPLES, OFF, date(2026, 9, 25))) == set()
# ---------------------------------------------------------------- PIS / COFINS applicability is NOT circular
@@ -184,7 +230,7 @@ def test_unified_coverage_invariant_and_rtc_toggle_changes_only_rtc_products():
for s in (off, on):
assert s["TOTAL_PRODUCTS"] == s["COMPLETE_PRODUCTS"] + s["SAFE_BLOCKED"] + s["FAILED_TECHNICAL"] + s["UNCLASSIFIED"] == 4
assert off["COMPLETE_PRODUCTS"] == 2 and off["BLOCKED_FISCAL_DATA"] == 1 and off["UNCLASSIFIED"] == 1 # RTC off: cClassTrib never counts
assert on["COMPLETE_PRODUCTS"] == 1 and on["BLOCKED_FISCAL_DATA"] == 3 and on["UNCLASSIFIED"] == 0 # RTC on: only product 3 has a valid cClassTrib; a real blocker outranks NEEDS_REVIEW
assert on["COMPLETE_PRODUCTS"] == 2 and on["BLOCKED_FISCAL_DATA"] == 1 and on["UNCLASSIFIED"] == 1 # RTC on, but Simples cClassTrib is not effective yet
def test_fiscal_blockers_never_hide_or_override_a_decision_blocker():