fix: RTC observed live with provenance, non-circular PIS/COFINS applicability, canonical CST normalization (1.1.8)
- RTC: Agent reads ALL PARAMETROS rows every sync (conflict => off), reports rtc_source/rtc_observed_at/rtc_param_rows; legal_effective_context is informational only - PIS/COFINS: applicability from CRT + PISCOFINS flag + incidence only (never from empty fields); Simples w/o special treatment NOT_APPLICABLE, taxed regime with explicit N OPTIONAL, unreadable declaration NEEDS_REVIEW - CST: one digit with an official zero-padded form is VALID_AFTER_NORMALIZATION (non-blocking warning), nothing is written to the ERP - tests: RTC toggle 0->1->0 across syncs (Agent + Hub stale closing), non-circularity, normalization, panel provenance Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -162,9 +162,18 @@ def priority_label(value):
|
||||
|
||||
|
||||
def rtc_status_view(summary):
|
||||
"""Discreet Reforma Tributária status from the last completed job's `fiscal_context`. None when the Agent did not report it (older Agents)."""
|
||||
"""Discreet Reforma Tributaria status from the last completed job's `fiscal_context`, exactly as the Agent OBSERVED it in that installation's own database.
|
||||
None when the Agent did not report it (older Agents)."""
|
||||
context = (summary or {}).get("fiscal_context") if isinstance(summary, dict) else None
|
||||
if not isinstance(context, dict) or "rtc_enabled" not in context:
|
||||
return None
|
||||
observed = None
|
||||
if context.get("rtc_observed_at"):
|
||||
try:
|
||||
observed = format_ui_datetime(datetime.fromisoformat(str(context["rtc_observed_at"]).replace("Z", "+00:00")))
|
||||
except ValueError:
|
||||
observed = None
|
||||
source = context.get("rtc_source") or "PARAMETROS.USAR_REFORMA_TRIBUTARIA"
|
||||
return {"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"))}
|
||||
"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 "")}
|
||||
|
||||
@@ -54,17 +54,25 @@ def incidence_kind(value) -> str:
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
def canonical_cst(value) -> str | None:
|
||||
"""The official 2-digit CST this text stands for, or None. Exactly two official digits, or ONE digit whose zero-padded form is an official code
|
||||
("4" -> "04"). Nothing else is ever normalised ("049", "000", "2x" stay invalid)."""
|
||||
code = _text(value)
|
||||
if code in CST_DOMAIN:
|
||||
return code
|
||||
if len(code) == 1 and code.isdigit() and code.zfill(2) in CST_DOMAIN:
|
||||
return code.zfill(2)
|
||||
return None
|
||||
|
||||
|
||||
def validate_cst(value) -> str:
|
||||
"""EMPTY | VALID | VALID_AFTER_NORMALIZATION | INVALID."""
|
||||
code = _text(value)
|
||||
if not code:
|
||||
return "EMPTY"
|
||||
return "VALID" if code in CST_DOMAIN else "INVALID"
|
||||
|
||||
|
||||
def cst_suggestion(value) -> str | None:
|
||||
"""A short numeric code that becomes an official CST once zero-padded to two digits ('4' -> '04'). Only a hint for the operator; never applied."""
|
||||
code = _text(value)
|
||||
return code.zfill(2) if code.isdigit() and len(code) == 1 and code.zfill(2) in CST_DOMAIN else None
|
||||
if code in CST_DOMAIN:
|
||||
return "VALID"
|
||||
return "VALID_AFTER_NORMALIZATION" if canonical_cst(code) else "INVALID"
|
||||
|
||||
|
||||
def validate_rate(value) -> str:
|
||||
@@ -82,56 +90,71 @@ def validate_rate(value) -> str:
|
||||
def pis_cofins_applicability(product: dict, crt) -> dict:
|
||||
"""{"state": NOT_APPLICABLE | REQUIRED | OPTIONAL | NEEDS_REVIEW, "basis": ...}.
|
||||
|
||||
NOT_APPLICABLE the product carries no product-level PIS/COFINS treatment at all (flag N/blank, incidence NENHUMA/blank, all six fields blank).
|
||||
The tax is then settled elsewhere: inside the DAS (Simples family) or from the company default (regime normal).
|
||||
REQUIRED the product itself claims a special treatment (flag S, or incidence MONOFASICA/SUBSTITUICAO): the exit CST (and a rate when the CST needs one) must be valid.
|
||||
OPTIONAL no special claim, but some CST/rate was typed: it is not mandatory, yet whatever is there must be valid.
|
||||
NEEDS_REVIEW the incidence text is not one the SGV defines (NENHUMA / MONOFASICA / SUBST.TRIBUTARIA): applicability cannot be decided.
|
||||
Decided ONLY from signals that are independent of the six CST/rate fields - an empty field is a data state, never proof of "not applicable":
|
||||
* the company regime (CRT);
|
||||
* the product's own declaration: PISCOFINS flag and INCIDENCIA_PISCOFINS;
|
||||
* official rules (LC 123/2006 art. 13 IV/V and art. 18 par. 4-A I; Leis 9.718/98, 10.637/02, 10.833/03; SPED 4.3.x for the CST domain/semantics).
|
||||
|
||||
REQUIRED the product declares a special treatment (flag S, or incidence MONOFASICA / SUBSTITUICAO): the exit CST must be valid, whatever the fields hold.
|
||||
NOT_APPLICABLE Simples family (CRT 1/2/4) with no special treatment declared: PIS/Cofins are collected inside the DAS (LC 123 art. 13), so the product carries no
|
||||
obligation. The same verdict holds whether the fields are empty or typed.
|
||||
OPTIONAL taxed regime (CRT 3 or other) AND the product explicitly declares "no special treatment" (flag N + incidence NENHUMA): the tax applies but the
|
||||
company default covers it, so the product default is not mandatory; anything typed must still be valid.
|
||||
NEEDS_REVIEW the declaration cannot be read: incidence text outside the SGV vocabulary, or a taxed regime with a blank flag/incidence.
|
||||
"""
|
||||
flag = _text(product.get("PISCOFINS")).upper()
|
||||
kind = incidence_kind(product.get("INCIDENCIA_PISCOFINS"))
|
||||
has_values = any(_text(product.get(c)) for c in PIS_COFINS_FIELDS)
|
||||
raw_incidence = _text(product.get("INCIDENCIA_PISCOFINS"))
|
||||
kind = incidence_kind(raw_incidence)
|
||||
if kind == "UNKNOWN":
|
||||
return {"state": "NEEDS_REVIEW", "basis": "INCIDENCE_NOT_DEFINED_BY_SGV"}
|
||||
if flag == "S" or kind in ("MONOFASICA", "SUBSTITUICAO"):
|
||||
return {"state": "REQUIRED", "basis": "PRODUCT_DECLARES_SPECIAL_TREATMENT"}
|
||||
if has_values:
|
||||
return {"state": "OPTIONAL", "basis": "VALUES_TYPED_WITHOUT_SPECIAL_CLAIM"}
|
||||
return {"state": "NOT_APPLICABLE", "basis": "SIMPLES_DAS" if _text(crt) in SIMPLES_FAMILY else "COMPANY_DEFAULT"}
|
||||
if _text(crt) in SIMPLES_FAMILY:
|
||||
return {"state": "NOT_APPLICABLE", "basis": "SIMPLES_DAS_NO_SPECIAL_TREATMENT"}
|
||||
if flag == "N" and raw_incidence:
|
||||
return {"state": "OPTIONAL", "basis": "TAXED_REGIME_COMPANY_DEFAULT_COVERS"}
|
||||
return {"state": "NEEDS_REVIEW", "basis": "NO_DECLARATION_IN_TAXED_REGIME"}
|
||||
|
||||
|
||||
def evaluate_pis_cofins(product: dict, crt) -> dict:
|
||||
"""{"applicability": {...}, "blockers": [(type, reason)]}. Entry and exit are separate; a missing OPTIONAL value never blocks."""
|
||||
"""{"applicability": {...}, "blockers": [(type, reason)], "warnings": [(type, reason)]}. Entry and exit are separate; PIS and COFINS independent.
|
||||
Blockers exist only where the field applies (REQUIRED / OPTIONAL). Non-blocking warnings report data-quality facts (non-canonical or ignored values)."""
|
||||
app = pis_cofins_applicability(product, crt)
|
||||
blockers: list[tuple[str, str]] = []
|
||||
if app["state"] in ("NOT_APPLICABLE", "NEEDS_REVIEW"):
|
||||
return {"applicability": app, "blockers": blockers}
|
||||
warnings: list[tuple[str, str]] = []
|
||||
kind = incidence_kind(product.get("INCIDENCIA_PISCOFINS"))
|
||||
flag = _text(product.get("PISCOFINS")).upper()
|
||||
applies = app["state"] in ("REQUIRED", "OPTIONAL")
|
||||
for tax in ("PIS", "COFINS"):
|
||||
entry, exit_, rate = product.get(f"COD_ENTRADA_{tax}"), product.get(f"COD_SAIDA_{tax}"), product.get(f"ALIQ_{tax}")
|
||||
entry_state, exit_state, rate_state = validate_cst(entry), validate_cst(exit_), validate_rate(rate)
|
||||
if entry_state == "INVALID":
|
||||
blockers.append((f"{tax}_CST_INVALID", "ENTRY" + (f":UNPADDED_USE_{cst_suggestion(entry)}" if cst_suggestion(entry) else "")))
|
||||
if exit_state == "INVALID":
|
||||
blockers.append((f"{tax}_CST_INVALID", "EXIT" + (f":UNPADDED_USE_{cst_suggestion(exit_)}" if cst_suggestion(exit_) else "")))
|
||||
for slot, value, state in (("ENTRY", entry, entry_state), ("EXIT", exit_, exit_state)):
|
||||
if state == "VALID_AFTER_NORMALIZATION":
|
||||
warnings.append((f"CST_{tax}_NON_CANONICAL_FORMAT", f"{slot}:{_text(value)}->{canonical_cst(value)}"))
|
||||
elif state == "INVALID":
|
||||
if applies:
|
||||
blockers.append((f"{tax}_CST_INVALID", slot))
|
||||
else:
|
||||
warnings.append((f"CST_{tax}_INVALID_IGNORED", f"{slot}:{_text(value)}"))
|
||||
if not applies:
|
||||
continue
|
||||
if app["state"] == "REQUIRED" and exit_state == "EMPTY":
|
||||
blockers.append((f"{tax}_CST_MISSING", "EXIT"))
|
||||
if rate_state == "INVALID":
|
||||
blockers.append((f"{tax}_RATE_INVALID", "RATE"))
|
||||
if exit_state == "VALID":
|
||||
code = _text(exit_)
|
||||
code = canonical_cst(exit_)
|
||||
if code:
|
||||
if code in RATE_REQUIRED_CST and rate_state == "EMPTY":
|
||||
blockers.append((f"{tax}_RATE_MISSING", f"CST_{code}_NEEDS_RATE"))
|
||||
if kind in INCIDENCE_CST and code not in INCIDENCE_CST[kind]:
|
||||
blockers.append((f"{tax}_CST_INCIDENCE_MISMATCH", f"{kind}:CST_{code}"))
|
||||
if kind == "NONE" and app["state"] == "REQUIRED" and code in RATE_NOT_APPLICABLE_CST - {"03"} and flag == "S":
|
||||
blockers.append((f"{tax}_CST_INCIDENCE_MISMATCH", f"NENHUMA:CST_{code}"))
|
||||
if flag == "N" and kind != "NONE":
|
||||
if applies and flag == "N" and kind != "NONE":
|
||||
blockers.append(("PIS_COFINS_FLAG_INCONSISTENT", "FLAG_N_WITH_SPECIAL_INCIDENCE"))
|
||||
if flag == "S" and kind == "NONE" and not any(_text(product.get(c)) for c in PIS_COFINS_FIELDS):
|
||||
if applies and flag == "S" and kind == "NONE" and not any(_text(product.get(c)) for c in PIS_COFINS_FIELDS):
|
||||
blockers.append(("PIS_COFINS_FLAG_INCONSISTENT", "FLAG_S_WITHOUT_INCIDENCE_OR_CODES"))
|
||||
return {"applicability": app, "blockers": _dedupe(blockers)}
|
||||
return {"applicability": app, "blockers": _dedupe(blockers), "warnings": _dedupe(warnings)}
|
||||
|
||||
|
||||
def _dedupe(items):
|
||||
@@ -201,11 +224,21 @@ def evaluate_product(product: dict, company: dict | None, rtc: dict | None, toda
|
||||
pis = evaluate_pis_cofins(product, crt)
|
||||
rtc_result = evaluate_rtc(product, rtc, today)
|
||||
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 = []
|
||||
if pis["applicability"]["state"] == "REQUIRED":
|
||||
mandatory += ["COD_SAIDA_PIS", "COD_SAIDA_COFINS"]
|
||||
if rtc_result["applicability"]["state"] == "REQUIRED":
|
||||
mandatory.append("CCLASSTRIB")
|
||||
return {"blockers": blockers, "applicability": {"PIS_COFINS": pis["applicability"]["state"], "RTC": rtc_result["applicability"]["state"]},
|
||||
"basis": {"PIS_COFINS": pis["applicability"]["basis"], "RTC": rtc_result["applicability"]["basis"]}, "mandatory_fields": mandatory,
|
||||
"basis": {"PIS_COFINS": pis["applicability"]["basis"], "RTC": rtc_result["applicability"]["basis"]}, "mandatory_fields": mandatory, "warnings": warnings,
|
||||
"needs_review": pis["applicability"]["state"] == "NEEDS_REVIEW"}
|
||||
|
||||
|
||||
def legal_effective_context(crt) -> dict:
|
||||
"""INFORMATIONAL ONLY (never a blocker, never part of completeness). The operational rule stays ERP_RTC_ENABLED; this records what the legislation
|
||||
says about IBS/CBS in documents for the company regime so a difference in timing can be explained without inventing a rule."""
|
||||
simples = _text(crt) in SIMPLES_FAMILY
|
||||
return {"crt": _text(crt) or None, "informational_only": True,
|
||||
"ibs_cbs_in_documents": "FROM_2027_FOR_SIMPLES_ORIENTATION_PENDING" if simples else ("MANDATORY_FROM_2026-01-01" if _text(crt) == "3" else "UNKNOWN_REGIME"),
|
||||
"sources": ["NT 2025.002-RTC v1.51 (Portal NF-e, 04/08/2026): CRT 1/2/4 orientations in a future NT; IBS/CBS taxation of these taxpayers only from 2027", "LC 214/2025 art. 348"]}
|
||||
|
||||
@@ -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" }}">{{ ops.rtc.label }}</span></dd>{% endif %}<dt>Último job</dt><dd>{% if ops.last_job %}<a href="/jobs?scope=all&job_id={{ ops.last_job.id }}">{{ ops.last_job.kind_label }}</a> · {{ ops.last_job.status_label }}{% else %}nenhum{% endif %}</dd></dl></section>
|
||||
</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 %}<dt>Último job</dt><dd>{% if ops.last_job %}<a href="/jobs?scope=all&job_id={{ ops.last_job.id }}">{{ ops.last_job.kind_label }}</a> · {{ ops.last_job.status_label }}{% else %}nenhum{% endif %}</dd></dl></section>
|
||||
<nav class="tabs" aria-label="Seções da instalação">{% for name,label in [("overview","Resumo"),("fiscal","Fiscal"),("blockers","Blockers"),("jobs","Jobs"),("events","Eventos")] %}<a class="{% if tab==name %}active{% endif %}" href="?tab={{ name }}">{{ label }}</a>{% endfor %}</nav>
|
||||
<section class="split"><article class="panel"><div class="panel-title"><h2>Contexto da empresa</h2></div><dl class="facts"><dt>Cliente</dt><dd>{{ detail.client or "Não informado" }}</dd><dt>Nome de exibição</dt><dd>{{ detail.display_name or "Não informado" }}</dd><dt>Hostname</dt><dd>{{ detail.hostname or "Não informado" }}</dd><dt>Empresa</dt><dd>{{ detail.company_name or detail.company_code or "Não informado" }}</dd><dt>UF</dt><dd>{{ detail.company_uf or "Não informado" }}</dd><dt>CRT</dt><dd>{{ detail.company_crt or "Não informado" }}</dd><dt>Versão do agent</dt><dd>{{ detail.agent_version_display }}</dd><dt>Último heartbeat</dt><dd title="{{ detail.last_seen_at }}">{{ detail.last_seen_display }} · {{ detail.last_seen_age }}</dd><dt>Firebird</dt><dd>{{ detail.firebird_version or "Não informado" }} · ODS {{ detail.firebird_ods or "Não informado" }}</dd><dt>IDs técnicos</dt><dd>Agent {{ detail.technical_ids.agent_id }} · instalação {{ detail.technical_ids.installation_db_id }}</dd></dl></article>
|
||||
<article class="panel"><div class="panel-title"><h2>Eventos recentes</h2><a href="#eventos">Ver eventos</a></div>{% for event in events[:8] %}<div class="event-row"><span class="event-time">{{ event.occurred_display }}</span><span><b>{{ event.event_type }}</b><small>{{ event.message }}{% if event.job_id %} · <a href="/jobs?job_id={{ event.job_id }}">Job {{ event.job_id }}</a>{% endif %}{% if event.product_code %} · <a href="/blockers?agent_id={{ detail.agent_id }}&product_code={{ event.product_code|urlencode }}">Produto {{ event.product_code }}</a>{% endif %}</small></span><em class="severity {{ event.severity|lower }}">Severidade: {{ event.severity }}</em></div>{% else %}<p class="empty-state">Nenhum evento.</p>{% endfor %}</article></section>
|
||||
|
||||
@@ -575,3 +575,15 @@ def test_pis_cofins_and_rtc_blockers_appear_in_the_same_queue_and_dashboard_brea
|
||||
assert "fila RTC" not in queue.lower()
|
||||
filtered = client.get("/blockers?scope=active&status=OPEN&field=RTC").text
|
||||
assert "cClassTrib ausente" in filtered and "CST PIS inválido" not in filtered
|
||||
|
||||
|
||||
def test_rtc_status_shows_where_and_when_the_agent_observed_it():
|
||||
client, agent_id, _ = _panel_client()
|
||||
_complete_sync(client, agent_id, {"rtc_enabled": True, "rtc_supported": True, "rtc_catalog_valid": True, "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA", "rtc_observed_at": "2026-09-24T20:00:00+00:00"})
|
||||
text = client.get(f"/agents/{agent_id}").text
|
||||
assert 'title="Lido no banco do cliente (PARAMETROS.USAR_REFORMA_TRIBUTARIA) em ' in text and ">Ativa<" in text
|
||||
_complete_sync(client, agent_id, {"rtc_enabled": False, "rtc_param_conflict": True, "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA"})
|
||||
conflict = client.get(f"/agents/{agent_id}").text
|
||||
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
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
from datetime import date
|
||||
|
||||
from central.app.operational_state import classify_product_state, summarize_operational_states
|
||||
from central.app.unified_fiscal_coverage import (blocker_category, evaluate_product, incidence_kind, rtc_enabled, validate_cclasstrib)
|
||||
from central.app.unified_fiscal_coverage import (blocker_category, canonical_cst, evaluate_product, incidence_kind, legal_effective_context, pis_cofins_applicability, rtc_enabled,
|
||||
validate_cclasstrib, validate_cst)
|
||||
|
||||
CATALOG = {"classes": {"000001": "000", "200003": "200", "410004": "410", "999999": None, "555555": "555"}, "cst": {"000", "200", "410"}, "duplicates": 0}
|
||||
ON = {"enabled": True, "catalog": CATALOG, "catalog_valid": True}
|
||||
OFF = {"enabled": False, "catalog": CATALOG, "catalog_valid": True}
|
||||
SIMPLES, NORMAL = {"crt": "1"}, {"crt": "3"}
|
||||
EMPTY = {"CODIGO": "1", "PISCOFINS": "N", "INCIDENCIA_PISCOFINS": "NENHUMA"}
|
||||
FIELDS = ("COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS")
|
||||
|
||||
|
||||
def kinds(result):
|
||||
return {b["type"] for b in result["blockers"]}
|
||||
|
||||
|
||||
def warns(result):
|
||||
return {w["type"] for w in result["warnings"]}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- RTC on / off
|
||||
def test_rtc_off_ignores_cclasstrib_completely():
|
||||
for value in (None, "", "12x", "999999", "000001"):
|
||||
@@ -48,14 +54,85 @@ 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({})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- PIS / COFINS applicability
|
||||
def test_empty_product_is_not_applicable_in_every_regime_and_never_blocks():
|
||||
for company, basis in ((SIMPLES, "SIMPLES_DAS"), ({"crt": "2"}, "SIMPLES_DAS"), ({"crt": "4"}, "SIMPLES_DAS"), (NORMAL, "COMPANY_DEFAULT"), ({"crt": None}, "COMPANY_DEFAULT"), (None, "COMPANY_DEFAULT")):
|
||||
r = evaluate_product(EMPTY, company, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "NOT_APPLICABLE" and r["basis"]["PIS_COFINS"] == basis and r["blockers"] == []
|
||||
assert evaluate_product({"CODIGO": "1"}, SIMPLES, OFF)["blockers"] == [] # columns absent in an old schema: same answer
|
||||
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 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()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- PIS / COFINS applicability is NOT circular
|
||||
def test_applicability_never_depends_on_whether_the_six_fields_are_empty():
|
||||
typed = {f: "49" if "COD" in f else "0" for f in FIELDS}
|
||||
for crt in ("1", "2", "4", "3", None):
|
||||
for base in (EMPTY, {"CODIGO": "1", "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, {"CODIGO": "1", "PISCOFINS": "", "INCIDENCIA_PISCOFINS": ""}):
|
||||
empty_state = pis_cofins_applicability(base, crt)
|
||||
filled_state = pis_cofins_applicability({**base, **typed}, crt)
|
||||
assert empty_state == filled_state, (crt, base) # same verdict with the fields empty or typed: emptiness proves nothing
|
||||
|
||||
|
||||
def test_empty_fields_with_applicable_context_is_a_missing_blocker():
|
||||
for crt in ("1", "3"):
|
||||
r = evaluate_product({**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, {"crt": crt}, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "REQUIRED" and kinds(r) == {"PIS_CST_MISSING", "COFINS_CST_MISSING"}
|
||||
|
||||
|
||||
def test_empty_fields_with_provably_not_applicable_context_is_not_applicable():
|
||||
for crt in ("1", "2", "4"):
|
||||
r = evaluate_product(EMPTY, {"crt": crt}, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "NOT_APPLICABLE" and r["basis"]["PIS_COFINS"] == "SIMPLES_DAS_NO_SPECIAL_TREATMENT" and r["blockers"] == [] and not r["needs_review"]
|
||||
assert evaluate_product({"CODIGO": "1"}, SIMPLES, OFF)["blockers"] == [] # columns absent in an old schema, Simples: nothing to require
|
||||
|
||||
|
||||
def test_empty_fields_with_insufficient_context_need_review_and_never_default_to_complete():
|
||||
for company in (NORMAL, {"crt": None}, None):
|
||||
r = evaluate_product({"CODIGO": "1", "PISCOFINS": "", "INCIDENCIA_PISCOFINS": ""}, company, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "NEEDS_REVIEW" and r["needs_review"] and r["blockers"] == []
|
||||
state = classify_product_state({"CODIGO": "1"}, {"status": "EXACT_NCM"}, fiscal=r)["state"]
|
||||
assert state == "NEEDS_REVIEW" # counted as UNCLASSIFIED, never inflating coverage
|
||||
|
||||
|
||||
def test_taxed_regime_with_explicit_no_special_treatment_is_optional_and_typed_values_must_be_valid():
|
||||
r = evaluate_product(EMPTY, NORMAL, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "OPTIONAL" and r["blockers"] == []
|
||||
assert kinds(evaluate_product({**EMPTY, "COD_SAIDA_PIS": "2x"}, NORMAL, OFF)) == {"PIS_CST_INVALID"}
|
||||
|
||||
|
||||
def test_unknown_incidence_text_needs_review_in_every_regime():
|
||||
for crt in ("1", "3"):
|
||||
r = evaluate_product({**EMPTY, "INCIDENCIA_PISCOFINS": "OUTRA COISA"}, {"crt": crt}, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "NEEDS_REVIEW" and r["needs_review"] and r["blockers"] == []
|
||||
|
||||
|
||||
def test_simples_typed_garbage_is_ignored_as_a_warning_not_a_blocker():
|
||||
row = {**EMPTY, "COD_ENTRADA_PIS": "2", "COD_SAIDA_PIS": "2", "ALIQ_PIS": "2", "COD_ENTRADA_COFINS": "2", "COD_SAIDA_COFINS": "2", "ALIQ_COFINS": "2"}
|
||||
r = evaluate_product(row, SIMPLES, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "NOT_APPLICABLE" and r["blockers"] == []
|
||||
assert warns(r) == {"CST_PIS_NON_CANONICAL_FORMAT", "CST_COFINS_NON_CANONICAL_FORMAT"} # "2" is one digit of official "02": normalisable
|
||||
bad = evaluate_product({**EMPTY, "COD_ENTRADA_COFINS": "000", "COD_SAIDA_COFINS": "060"}, SIMPLES, OFF)
|
||||
assert bad["blockers"] == [] and warns(bad) == {"CST_COFINS_INVALID_IGNORED"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- CST normalisation
|
||||
def test_canonical_cst_accepts_only_one_digit_with_an_official_zero_padded_form():
|
||||
assert canonical_cst("4") == "04" and canonical_cst("04") == "04" and canonical_cst(" 4 ") == "04" and canonical_cst("9") == "09" and canonical_cst("1") == "01"
|
||||
for text in ("0", "049", "000", "060", "2x", "x", "", None, "10", "12", "00"):
|
||||
assert canonical_cst(text) is None, text
|
||||
assert validate_cst("4") == "VALID_AFTER_NORMALIZATION" and validate_cst("04") == "VALID" and validate_cst("049") == "INVALID" and validate_cst("") == "EMPTY"
|
||||
|
||||
|
||||
def test_unpadded_cst_is_semantically_valid_warns_and_is_used_canonically_for_the_rules():
|
||||
row = {**EMPTY, "COD_SAIDA_PIS": "4", "COD_SAIDA_COFINS": "4"}
|
||||
r = evaluate_product(row, NORMAL, OFF)
|
||||
assert r["blockers"] == [] and warns(r) == {"CST_PIS_NON_CANONICAL_FORMAT", "CST_COFINS_NON_CANONICAL_FORMAT"}
|
||||
mono = {**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA", "COD_SAIDA_PIS": "4", "COD_SAIDA_COFINS": "4"}
|
||||
assert kinds(evaluate_product(mono, NORMAL, OFF)) == set() # "4" -> "04" is compatible with MONOFASICA
|
||||
assert kinds(evaluate_product({**mono, "COD_SAIDA_PIS": "1", "ALIQ_PIS": "1,65"}, NORMAL, OFF)) == {"PIS_CST_INCIDENCE_MISMATCH"} # "1" -> "01" is not
|
||||
assert kinds(evaluate_product({**EMPTY, "COD_SAIDA_PIS": "049"}, NORMAL, OFF)) == {"PIS_CST_INVALID"} # ambiguous: still a blocker
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- PIS / COFINS rules
|
||||
def test_declared_monophasic_product_requires_a_valid_exit_cst():
|
||||
good = {**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA", "COD_SAIDA_PIS": "04", "COD_SAIDA_COFINS": "04"}
|
||||
r = evaluate_product(good, SIMPLES, OFF)
|
||||
@@ -69,13 +146,6 @@ def test_st_incidence_with_taxable_cst_is_a_mismatch_and_pis_cofins_are_independ
|
||||
assert kinds(evaluate_product(row, NORMAL, OFF)) == {"PIS_CST_INCIDENCE_MISMATCH"} # only PIS is wrong; COFINS 05 is right
|
||||
|
||||
|
||||
def test_value_2_is_invalid_format_not_a_test_product_and_not_silently_ignored():
|
||||
row = {**EMPTY, "COD_ENTRADA_PIS": "2", "COD_SAIDA_PIS": "2", "ALIQ_PIS": "2", "COD_ENTRADA_COFINS": "2", "COD_SAIDA_COFINS": "2", "ALIQ_COFINS": "2"}
|
||||
r = evaluate_product(row, SIMPLES, OFF)
|
||||
assert r["applicability"]["PIS_COFINS"] == "OPTIONAL" and kinds(r) == {"PIS_CST_INVALID", "COFINS_CST_INVALID"}
|
||||
assert "PIS_RATE_INVALID" not in kinds(r) # rate 2 is a valid number; only the CST codes are malformed
|
||||
|
||||
|
||||
def test_rate_rules_follow_the_cst():
|
||||
base = {**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "NENHUMA"}
|
||||
assert kinds(evaluate_product({**base, "COD_SAIDA_PIS": "01", "COD_SAIDA_COFINS": "01"}, NORMAL, OFF)) == {"PIS_RATE_MISSING", "COFINS_RATE_MISSING"}
|
||||
@@ -85,17 +155,15 @@ def test_rate_rules_follow_the_cst():
|
||||
assert kinds(evaluate_product({**base, "COD_SAIDA_PIS": "49", "ALIQ_PIS": "150", "COD_SAIDA_COFINS": "49"}, SIMPLES, OFF)) == {"PIS_RATE_INVALID"}
|
||||
|
||||
|
||||
def test_inconsistent_flags_and_unknown_incidence():
|
||||
def test_inconsistent_flags():
|
||||
assert "PIS_COFINS_FLAG_INCONSISTENT" in kinds(evaluate_product({**EMPTY, "PISCOFINS": "N", "INCIDENCIA_PISCOFINS": "MONOFÁSICA", "COD_SAIDA_PIS": "04", "COD_SAIDA_COFINS": "04"}, SIMPLES, OFF))
|
||||
assert "PIS_COFINS_FLAG_INCONSISTENT" in kinds(evaluate_product({**EMPTY, "PISCOFINS": "S"}, SIMPLES, OFF))
|
||||
unknown = evaluate_product({**EMPTY, "INCIDENCIA_PISCOFINS": "OUTRA COISA"}, SIMPLES, OFF)
|
||||
assert unknown["needs_review"] and unknown["blockers"] == [] and unknown["applicability"]["PIS_COFINS"] == "NEEDS_REVIEW"
|
||||
|
||||
|
||||
def test_incidence_text_is_encoding_and_accent_tolerant():
|
||||
for text in ("MONOFÁSICA", "MONOFASICA", "MONOF�SICA", "monofásica"):
|
||||
assert incidence_kind(text) == "MONOFASICA"
|
||||
assert incidence_kind("SUBST.TRIBUTÁRIA") == "SUBSTITUICAO" and incidence_kind(None) == "NONE" and incidence_kind(" nenhuma ") == "NONE"
|
||||
assert incidence_kind("SUBST.TRIBUTÁRIA") == "SUBSTITUICAO" and incidence_kind(None) == "NONE" and incidence_kind(" nenhuma ") == "NONE" and incidence_kind("???") == "UNKNOWN"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- ONE definition of complete, ONE queue
|
||||
@@ -105,7 +173,8 @@ def test_blocker_categories_feed_the_single_dashboard_breakdown():
|
||||
|
||||
|
||||
def test_unified_coverage_invariant_and_rtc_toggle_changes_only_rtc_products():
|
||||
products = [{**EMPTY, "CODIGO": "1"}, {**EMPTY, "CODIGO": "2", "COD_SAIDA_PIS": "2"}, {**EMPTY, "CODIGO": "3", "CCLASSTRIB": "200003"}, {**EMPTY, "CODIGO": "4", "INCIDENCIA_PISCOFINS": "???"}]
|
||||
products = [{**EMPTY, "CODIGO": "1"}, {**EMPTY, "CODIGO": "2", "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, {**EMPTY, "CODIGO": "3", "CCLASSTRIB": "200003"},
|
||||
{**EMPTY, "CODIGO": "4", "INCIDENCIA_PISCOFINS": "???"}]
|
||||
|
||||
def run(rtc):
|
||||
states = [classify_product_state(p, {"status": "EXACT_NCM"}, fiscal=evaluate_product(p, SIMPLES, rtc)) for p in products]
|
||||
@@ -118,14 +187,7 @@ def test_unified_coverage_invariant_and_rtc_toggle_changes_only_rtc_products():
|
||||
|
||||
|
||||
def test_fiscal_blockers_never_hide_or_override_a_decision_blocker():
|
||||
fiscal = evaluate_product({**EMPTY, "COD_SAIDA_PIS": "2"}, SIMPLES, OFF)
|
||||
fiscal = evaluate_product({**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, SIMPLES, OFF)
|
||||
state = classify_product_state({"CODIGO": "1"}, {"status": "INVALID_NCM"}, fiscal=fiscal)
|
||||
assert state["state"] == "BLOCKED_MISSING_EVIDENCE" # the NCM blocker stays the primary state
|
||||
assert classify_product_state({"CODIGO": "1"}, {"status": "EXACT_NCM"}, fiscal=None)["state"] == "COMPLETE"
|
||||
|
||||
|
||||
def test_unpadded_single_digit_cst_is_flagged_with_the_official_code_as_a_hint_only():
|
||||
row = {**EMPTY, "COD_SAIDA_PIS": "4", "COD_SAIDA_COFINS": "4"}
|
||||
r = evaluate_product(row, NORMAL, OFF)
|
||||
assert kinds(r) == {"PIS_CST_INVALID", "COFINS_CST_INVALID"} and any("UNPADDED_USE_04" in b["reason"] for b in r["blockers"])
|
||||
assert evaluate_product({**EMPTY, "COD_SAIDA_PIS": "049"}, NORMAL, OFF)["blockers"][0]["reason"] == "EXIT" # 3 digits: ambiguous, no hint
|
||||
|
||||
@@ -236,3 +236,31 @@ def test_partial_reprocess_never_closes_fiscal_blockers_of_other_products():
|
||||
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": {"evaluated_all": False}})
|
||||
with Session(engine) as s:
|
||||
assert len(s.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_(None))).all()) == 1
|
||||
|
||||
|
||||
def test_rtc_on_then_off_and_pis_fix_leave_no_stale_open_blockers_and_never_duplicate():
|
||||
from central.app.models import BlockedProduct
|
||||
client, engine = _client()
|
||||
hb(client)
|
||||
rtc_missing = _fiscal_blocker("20", "RTC", "CCLASSTRIB_MISSING", "r1")
|
||||
pis_invalid = _fiscal_blocker("21", "PIS", "PIS_CST_INVALID", "p1")
|
||||
op = {**OPERATIONAL, "BLOCKED_MISSING_EVIDENCE": 0, "BLOCKED_CONFLICT": 0}
|
||||
finish(client, engine, {"operational": {**op, "COMPLETE": 519, "BLOCKED_FISCAL_DATA": 2, "SAFE_BLOCKED": 2}, "blocked_products": [rtc_missing, pis_invalid, dict(rtc_missing)], "resolved_products": [],
|
||||
"fiscal_context": {"rtc_enabled": True, "evaluated_all": True}})
|
||||
with Session(engine) as s:
|
||||
assert len(s.scalars(select(BlockedProduct)).all()) == 2 # the repeated blocker is one row
|
||||
age_last_sync(engine, 4000)
|
||||
hb(client)
|
||||
# RTC switched off in the ERP AND the PIS value was normalised/fixed: the next full pass reports nothing
|
||||
finish(client, engine, {"operational": {**op, "COMPLETE": 521}, "blocked_products": [], "resolved_products": [], "fiscal_context": {"rtc_enabled": False, "evaluated_all": True}})
|
||||
with Session(engine) as s:
|
||||
rows = s.scalars(select(BlockedProduct)).all()
|
||||
assert [r.resolved_at is not None for r in rows] == [True, True] and {r.resolution_source for r in rows} == {"AUTO_REEVALUATION"}
|
||||
assert s.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_(None))).all() == []
|
||||
age_last_sync(engine, 4000)
|
||||
hb(client)
|
||||
# RTC comes back on: the SAME blocker (same fingerprint) reopens instead of being duplicated
|
||||
finish(client, engine, {"operational": {**op, "COMPLETE": 520, "BLOCKED_FISCAL_DATA": 1, "SAFE_BLOCKED": 1}, "blocked_products": [rtc_missing], "resolved_products": [], "fiscal_context": {"rtc_enabled": True, "evaluated_all": True}})
|
||||
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)]
|
||||
|
||||
Reference in New Issue
Block a user