feat: unified fiscal coverage - PIS/COFINS applicability + conditional RTC in one definition, one queue, one panel
- central/app/unified_fiscal_coverage.py: pure rules (PIS/COFINS NOT_APPLICABLE/REQUIRED/OPTIONAL/NEEDS_REVIEW, RTC only when the ERP switch is on) - agent: read-only loader (unified_fiscal.py) replaces the legacy/expanded module; findings feed the single operational state, coverage and blocker queue - hub: BLOCKED_FISCAL_DATA blockers (PIS/COFINS/RTC) share the queue; stale ones close on the next full pass; dashboard breakdown gains CEST/PIS/COFINS/RTC - panel: legacy/expanded coverage block removed; discreet 'Reforma Tributaria: Ativa/Desativada' - docs: unified_fiscal_completeness, pis_cofins_applicability, rtc_pis_cofins_model + official source manifests; read-only E2E script Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,8 +30,8 @@ sys.path.insert(0, str(AGENT_ROOT / "src"))
|
||||
sys.path.insert(0, str(PROJECT_ROOT)) # ships central/app engine modules read-only (central.app.*)
|
||||
|
||||
from gti_fiscal_agent.client import HubClient # noqa: E402
|
||||
from gti_fiscal_agent.firebird_reader import _connect as _open_read_only, check_connection, read_products # noqa: E402
|
||||
from gti_fiscal_agent.rtc_pis_cofins import assess_from_connection # noqa: E402
|
||||
from gti_fiscal_agent import firebird_reader as _firebird_reader, unified_fiscal as _unified # noqa: E402
|
||||
from gti_fiscal_agent.firebird_reader import check_connection, read_products # noqa: E402
|
||||
from gti_fiscal_agent.pipeline import run_pipeline # noqa: E402
|
||||
from gti_fiscal_agent.runner import REFERENCE_SCHEMA_V1, product_fingerprint, to_hub_product # noqa: E402
|
||||
from central.app.operational_state import classify_product_state, summarize_operational_states # noqa: E402
|
||||
@@ -187,16 +187,27 @@ def open_write_connection(env, database_path, firebird_writes=True):
|
||||
)
|
||||
|
||||
|
||||
def _expanded_scope(env, legacy_states):
|
||||
"""RTC + PIS/COFINS assessment (read-only, SELECT only). Isolated: a failure here never fails the job or changes legacy coverage."""
|
||||
def _unified_fiscal(env, company):
|
||||
"""PIS/COFINS applicability + RTC (only when the ERP has the reform on), read-only (SELECT only). Fail-closed: if it cannot be assessed the job fails
|
||||
instead of reporting a coverage that ignores these fields."""
|
||||
connection = _firebird_reader._connect(env)
|
||||
try:
|
||||
connection = _open_read_only(env)
|
||||
try:
|
||||
return assess_from_connection(connection, legacy_states)
|
||||
finally:
|
||||
connection.close()
|
||||
except Exception as error: # noqa: BLE001
|
||||
return {"scope_version": "expanded-v1", "unavailable": type(error).__name__}
|
||||
return _unified.assess_from_connection(connection, company)
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def _fiscal_blocked_products(products, findings):
|
||||
"""One queue entry per (product, blocker). Same shape as every other blocker; `field` is the dashboard category (PIS/COFINS/RTC)."""
|
||||
out = []
|
||||
for product in products:
|
||||
code = str(product.get("CODIGO") or "")
|
||||
found = findings.get(code) or {}
|
||||
for blocker in found.get("blockers") or []:
|
||||
out.append({"company": "", "product_code": code, "gtin": product.get("GTIN") or product.get("EAN_GTIN"), "description": product.get("PRODUTO"), "blocker_type": "BLOCKED_FISCAL_DATA",
|
||||
"field": blocker["category"], "reason_code": blocker["type"], "evidence_summary": {"reason": blocker["reason"], "applicability": found.get("applicability")},
|
||||
"input_fingerprint": found.get("input_fingerprint", "")})
|
||||
return out
|
||||
|
||||
|
||||
def execute_job(job, config, executor, state, env):
|
||||
@@ -238,7 +249,8 @@ def execute_job(job, config, executor, state, env):
|
||||
if write_connection is not None:
|
||||
write_connection.close()
|
||||
|
||||
operational = coverage = expanded_scope = None
|
||||
operational = coverage = fiscal_context = None
|
||||
fiscal = None
|
||||
if sync_kind:
|
||||
if cache:
|
||||
for product, decision in zip(to_process, decisions):
|
||||
@@ -247,14 +259,23 @@ def execute_job(job, config, executor, state, env):
|
||||
all_decisions = [state.cached_decision(p["CODIGO"]) or {} for p in products]
|
||||
else:
|
||||
all_decisions = decisions
|
||||
product_states = [classify_product_state(p, d) for p, d in zip(products, all_decisions)]
|
||||
operational = summarize_operational_states(product_states)
|
||||
coverage = build_job_coverage(env, client, products, all_decisions)
|
||||
expanded_scope = _expanded_scope(env, {s["product_code"]: s["state"] for s in product_states})
|
||||
company = {"crt": ((coverage or {}).get("company_context") or {}).get("company_crt")}
|
||||
fiscal = _unified_fiscal(env, company)
|
||||
findings = fiscal["findings"]
|
||||
product_states = [classify_product_state(p, d, fiscal=findings.get(str(p["CODIGO"]))) for p, d in zip(products, all_decisions)]
|
||||
operational = summarize_operational_states(product_states)
|
||||
fiscal_context = {**fiscal["context"], "evaluated_all": kind != "REPROCESS_PRODUCTS"}
|
||||
else:
|
||||
fiscal = _unified_fiscal(env, None)
|
||||
fiscal_blocked = _fiscal_blocked_products(products, fiscal["findings"])
|
||||
fiscal_codes = {b["product_code"] for b in fiscal_blocked}
|
||||
summary["blocked_products"] = summary["blocked_products"] + fiscal_blocked
|
||||
summary["resolved_products"] = [c for c in summary["resolved_products"] if str(c) not in fiscal_codes]
|
||||
result_summary = {
|
||||
"coverage": coverage,
|
||||
"operational": operational,
|
||||
"expanded_scope": expanded_scope,
|
||||
"fiscal_context": fiscal_context,
|
||||
"fiscal_mode": getattr(config, "fiscal_mode", None),
|
||||
"release_version": env.get("AGENT_VERSION") or "1.1.7",
|
||||
"products_total": len(products),
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
"""READ-ONLY RTC (cClassTrib / CST IBS-CBS) and PIS/COFINS assessment of SGV products.
|
||||
|
||||
Everything here is observation: capability detection, catalog integrity, deterministic validators, applicability and an
|
||||
EXPANDED coverage view that sits next to (never replaces) the legacy scope coverage. Nothing writes to Firebird, nothing
|
||||
fills a value, and no fiscal rule is invented: where local data cannot prove that a field applies, the answer is
|
||||
UNKNOWN_APPLICABILITY, not a blocker.
|
||||
|
||||
Vocabulary kept apart on purpose: SCHEMA_SUPPORT (the column exists) / DATA_PRESENT (it has a value) /
|
||||
APPLICABILITY (the field applies to this product) / MANDATORYNESS (a missing value is a defect). Only the last one
|
||||
may block, and only when applicability is proven from local, reliable data.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
LOCAL_CATALOG_ONLY = True # the RTC catalog used is the one stored in the client's own SGV database; no official dataset is integrated here
|
||||
|
||||
PRODUCT_TABLE = "C000025"
|
||||
RTC_CCLASSTRIB_TABLE = "RTC_CCLASSTRIB"
|
||||
RTC_CST_TABLE = "RTC_CST"
|
||||
PIS_COFINS_COLUMNS = ("COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS", "PISCOFINS", "INCIDENCIA_PISCOFINS")
|
||||
PRODUCT_COLUMNS = ("CODIGO", "CCLASSTRIB") + PIS_COFINS_COLUMNS
|
||||
|
||||
# Official CST PIS/COFINS codes (2 digits). Used for FORMAT/membership only, never to pick a value.
|
||||
OFFICIAL_PIS_COFINS_CST = frozenset([f"{n:02d}" for n in (*range(1, 10), 49, *range(50, 57), *range(60, 68), *range(70, 76), 98, 99)])
|
||||
|
||||
# ---------------------------------------------------------------- capabilities -------------------------------------------------
|
||||
|
||||
|
||||
def detect_rtc_capabilities(schema: dict, *, param_value=None, catalog: dict | None = None, physical_fk: bool | None = None) -> dict:
|
||||
"""schema: {TABLE: {COLUMN, ...}}. Fail-closed: anything not proven is False. Old databases without RTC simply report False."""
|
||||
product_cols = set(schema.get(PRODUCT_TABLE, ()))
|
||||
class_cols = set(schema.get(RTC_CCLASSTRIB_TABLE, ()))
|
||||
cst_cols = set(schema.get(RTC_CST_TABLE, ()))
|
||||
has_product = "CCLASSTRIB" in product_cols
|
||||
has_class = {"CCLASSTRIB", "CSTIBSCBS"} <= class_cols
|
||||
has_cst = "CSTIBSCBS" in cst_cols
|
||||
has_relationship = bool(has_product and has_class and has_cst) # column-level relationship: product -> class -> cst
|
||||
caps = {
|
||||
"HAS_PRODUCT_CCLASSTRIB": has_product,
|
||||
"HAS_RTC_CCLASSTRIB_TABLE": has_class,
|
||||
"HAS_RTC_CST_TABLE": has_cst,
|
||||
"HAS_RTC_RELATIONSHIP": has_relationship,
|
||||
"HAS_PHYSICAL_RTC_FK": bool(physical_fk),
|
||||
"RTC_ENABLED_BY_PARAM": None if param_value is None else str(param_value).strip() in {"1", "S", "T", "TRUE"},
|
||||
"RTC_CATALOG_VALID": False,
|
||||
"LOCAL_CATALOG_ONLY": LOCAL_CATALOG_ONLY,
|
||||
}
|
||||
if catalog is not None and has_class and has_cst:
|
||||
caps["RTC_CATALOG_VALID"] = catalog_integrity(catalog)["valid"]
|
||||
return caps
|
||||
|
||||
|
||||
def catalog_integrity(catalog: dict) -> dict:
|
||||
"""catalog: {"classes": {cClassTrib: cst_or_None}, "cst": {code, ...}, "duplicates": int}. Pure."""
|
||||
classes, cst = catalog.get("classes", {}), set(catalog.get("cst", ()))
|
||||
bad_format = [c for c in classes if not re.fullmatch(r"\d{6}", str(c))]
|
||||
bad_cst_format = [c for c in cst if not re.fullmatch(r"\d{3}", str(c))]
|
||||
orphans = [c for c, s in classes.items() if s is None or str(s) not in cst]
|
||||
return {"classes": len(classes), "cst": len(cst), "orphan_classes": len(orphans), "bad_class_format": len(bad_format),
|
||||
"bad_cst_format": len(bad_cst_format), "duplicates": int(catalog.get("duplicates", 0)),
|
||||
"valid": bool(classes and cst and not orphans and not bad_format and not bad_cst_format and not catalog.get("duplicates"))}
|
||||
|
||||
# ---------------------------------------------------------------- validators ---------------------------------------------------
|
||||
|
||||
|
||||
def _text(value) -> str:
|
||||
return "" if value is None else str(value).strip()
|
||||
|
||||
|
||||
def validate_cclasstrib(value, catalog: dict | None) -> str:
|
||||
"""EMPTY | INVALID_FORMAT | NOT_IN_LOCAL_CATALOG | VALID_LOCAL | CST_MISSING | VALID_WITH_CST (deterministic, no inference)."""
|
||||
code = _text(value)
|
||||
if not code:
|
||||
return "EMPTY"
|
||||
if not re.fullmatch(r"\d{6}", code):
|
||||
return "INVALID_FORMAT"
|
||||
if not catalog or code not in catalog.get("classes", {}):
|
||||
return "NOT_IN_LOCAL_CATALOG"
|
||||
if not catalog.get("cst"):
|
||||
return "VALID_LOCAL" # code exists, but there is no CST table to verify its CST
|
||||
cst = catalog["classes"][code]
|
||||
return "VALID_WITH_CST" if cst is not None and str(cst) in catalog["cst"] else "CST_MISSING"
|
||||
|
||||
|
||||
def validate_pis_cofins_cst(value) -> str:
|
||||
code = _text(value)
|
||||
if not code:
|
||||
return "EMPTY"
|
||||
return "VALID" if code in OFFICIAL_PIS_COFINS_CST else "INVALID_FORMAT"
|
||||
|
||||
|
||||
def validate_pis_cofins_rate(value) -> str:
|
||||
text = _text(value)
|
||||
if not text:
|
||||
return "EMPTY"
|
||||
if not re.fullmatch(r"\d{1,3}([.,]\d{1,4})?", text):
|
||||
return "INVALID_RATE"
|
||||
return "VALID" if float(text.replace(",", ".")) <= 100 else "INVALID_RATE"
|
||||
|
||||
|
||||
def validate_pis_cofins(product: dict) -> dict:
|
||||
"""Field-level validation for one product row. Flag consistency compares the ERP's own flag/incidence with its own codes."""
|
||||
out = {}
|
||||
for tax in ("PIS", "COFINS"):
|
||||
out[f"{tax}_ENTRY_CST"] = validate_pis_cofins_cst(product.get(f"COD_ENTRADA_{tax}"))
|
||||
out[f"{tax}_EXIT_CST"] = validate_pis_cofins_cst(product.get(f"COD_SAIDA_{tax}"))
|
||||
out[f"{tax}_RATE"] = validate_pis_cofins_rate(product.get(f"ALIQ_{tax}"))
|
||||
flag = _text(product.get("PISCOFINS")).upper()
|
||||
incidence = _text(product.get("INCIDENCIA_PISCOFINS")).upper()
|
||||
has_codes = any(_text(product.get(c)) for c in PIS_COFINS_COLUMNS[:6])
|
||||
special = incidence not in ("", "NENHUMA")
|
||||
inconsistent = (flag == "N" and (special or has_codes)) or (flag == "S" and incidence in ("", "NENHUMA"))
|
||||
out["FLAG_CONSISTENCY"] = "INCONSISTENT" if inconsistent else ("EMPTY" if not flag else "CONSISTENT")
|
||||
return out
|
||||
|
||||
# ---------------------------------------------------------------- applicability -----------------------------------------------
|
||||
|
||||
|
||||
def rtc_applicability(caps: dict) -> str:
|
||||
"""APPLICABLE only when the ERP itself is switched to the reform (PARAMETROS.USAR_REFORMA_TRIBUTARIA) AND the model exists.
|
||||
NOT_ENABLED_BY_ERP when the ERP says it does not use RTC; NOT_SUPPORTED when the schema cannot hold it; else UNKNOWN."""
|
||||
if not (caps.get("HAS_PRODUCT_CCLASSTRIB") and caps.get("HAS_RTC_RELATIONSHIP")):
|
||||
return "NOT_SUPPORTED"
|
||||
enabled = caps.get("RTC_ENABLED_BY_PARAM")
|
||||
if enabled is None:
|
||||
return "UNKNOWN_APPLICABILITY"
|
||||
return "APPLICABLE" if enabled else "NOT_ENABLED_BY_ERP"
|
||||
|
||||
|
||||
def pis_cofins_applicability(product: dict, schema_ok: bool) -> str:
|
||||
"""Product-level PIS/COFINS defaults are treated as applicable only when the product itself carries evidence of them
|
||||
(a code, or a special incidence/flag). A product with no data and flag N / NENHUMA gives NO proof either way (documents
|
||||
take their CST from elsewhere), so it is UNKNOWN_APPLICABILITY - not complete and not blocked."""
|
||||
if not schema_ok:
|
||||
return "NOT_SUPPORTED"
|
||||
evidence = any(_text(product.get(c)) for c in PIS_COFINS_COLUMNS[:6]) or _text(product.get("PISCOFINS")).upper() == "S" \
|
||||
or _text(product.get("INCIDENCIA_PISCOFINS")).upper() not in ("", "NENHUMA")
|
||||
return "APPLICABLE" if evidence else "UNKNOWN_APPLICABILITY"
|
||||
|
||||
# ---------------------------------------------------------------- blockers + expanded coverage ------------------------------
|
||||
|
||||
|
||||
def expanded_findings(product: dict, caps: dict, catalog: dict | None) -> dict:
|
||||
"""{"blockers": [(TYPE, reason)], "unknown": [area], "notes": [...]}: specific blockers only where applicability is proven."""
|
||||
blockers, unknown = [], []
|
||||
rtc = rtc_applicability(caps)
|
||||
if rtc == "APPLICABLE":
|
||||
state = validate_cclasstrib(product.get("CCLASSTRIB"), catalog)
|
||||
if state == "EMPTY":
|
||||
blockers.append(("CCLASSTRIB_MISSING", "EMPTY"))
|
||||
elif state == "INVALID_FORMAT":
|
||||
blockers.append(("CCLASSTRIB_INVALID", state))
|
||||
elif state == "NOT_IN_LOCAL_CATALOG":
|
||||
blockers.append(("CCLASSTRIB_NOT_IN_CATALOG", state))
|
||||
elif state == "CST_MISSING":
|
||||
blockers.append(("RTC_CST_INVALID", state))
|
||||
elif rtc == "UNKNOWN_APPLICABILITY":
|
||||
unknown.append("RTC")
|
||||
schema_ok = caps.get("_PIS_COFINS_SCHEMA", False)
|
||||
pis_cofins = pis_cofins_applicability(product, schema_ok)
|
||||
if pis_cofins == "APPLICABLE":
|
||||
checks = validate_pis_cofins(product)
|
||||
for tax in ("PIS", "COFINS"):
|
||||
for slot, label in (("ENTRY_CST", "ENTRY"), ("EXIT_CST", "EXIT")):
|
||||
result = checks[f"{tax}_{slot}"]
|
||||
if result == "INVALID_FORMAT":
|
||||
blockers.append((f"{tax}_CST_INVALID", f"{label}:{result}"))
|
||||
if checks[f"{tax}_RATE"] == "INVALID_RATE":
|
||||
blockers.append((f"{tax}_RATE_INVALID", "INVALID_RATE"))
|
||||
exit_state = checks[f"{tax}_EXIT_CST"]
|
||||
if exit_state == "EMPTY" and checks[f"{tax}_RATE"] != "EMPTY":
|
||||
blockers.append((f"{tax}_CST_MISSING", "EXIT:EMPTY_WITH_RATE"))
|
||||
if checks["FLAG_CONSISTENCY"] == "INCONSISTENT":
|
||||
blockers.append(("PIS_COFINS_FLAG_INCONSISTENT", "FLAG_VS_DATA"))
|
||||
elif pis_cofins == "UNKNOWN_APPLICABILITY":
|
||||
unknown.append("PIS_COFINS")
|
||||
return {"blockers": blockers, "unknown": unknown, "rtc_applicability": rtc, "pis_cofins_applicability": pis_cofins}
|
||||
|
||||
|
||||
def expanded_scope_summary(products: list[dict], legacy_states: dict, caps: dict, catalog: dict | None) -> dict:
|
||||
"""Legacy coverage is reported unchanged (LEGACY_SCOPE_COVERAGE); EXPANDED_FISCAL_SCOPE_COVERAGE adds RTC + PIS/COFINS.
|
||||
legacy_states: {product_code: state} from classify_product_state (COMPLETE/CORRECTED/BLOCKED_*/FAILED_TECHNICAL)."""
|
||||
legacy = {"complete": 0, "safe_blocked": 0, "failed": 0}
|
||||
expanded = {"complete": 0, "safe_blocked": 0, "failed": 0, "unknown_applicability": 0}
|
||||
blockers: dict[str, int] = {}
|
||||
blocked_rows = []
|
||||
caps = dict(caps)
|
||||
for product in products:
|
||||
code = str(product.get("CODIGO") or "")
|
||||
state = legacy_states.get(code, "COMPLETE")
|
||||
if state == "FAILED_TECHNICAL":
|
||||
legacy["failed"] += 1
|
||||
expanded["failed"] += 1
|
||||
continue
|
||||
if state.startswith("BLOCKED_"):
|
||||
legacy["safe_blocked"] += 1
|
||||
expanded["safe_blocked"] += 1 # blocked in the legacy scope stays blocked: the expanded scope only ADDS conditions
|
||||
continue
|
||||
legacy["complete"] += 1
|
||||
found = expanded_findings(product, caps, catalog)
|
||||
if found["blockers"]:
|
||||
expanded["safe_blocked"] += 1
|
||||
for kind, reason in found["blockers"]:
|
||||
blockers[kind] = blockers.get(kind, 0) + 1
|
||||
if len(blocked_rows) < 200:
|
||||
blocked_rows.append({"product_code": code, "type": kind, "reason": reason})
|
||||
elif found["unknown"]:
|
||||
expanded["unknown_applicability"] += 1
|
||||
else:
|
||||
expanded["complete"] += 1
|
||||
total = len(products)
|
||||
return {"scope_version": "expanded-v1", "local_catalog_only": LOCAL_CATALOG_ONLY, "total_products": total,
|
||||
"legacy_scope": legacy, "expanded_scope": expanded, "blockers_by_type": dict(sorted(blockers.items())),
|
||||
"blocker_samples": blocked_rows, "invariant_pass": sum(expanded.values()) == total and sum(legacy.values()) == total,
|
||||
"capabilities": {k: v for k, v in caps.items() if not k.startswith("_")}}
|
||||
|
||||
# ---------------------------------------------------------------- read-only loader -------------------------------------------
|
||||
|
||||
|
||||
def load_rtc_context(cursor) -> tuple[dict, dict | None, dict]:
|
||||
"""Read schema + catalog + ERP parameter through an already open READ ONLY cursor (SELECT only).
|
||||
Returns (schema, catalog|None, extras). Never raises for a database that lacks RTC."""
|
||||
schema: dict[str, set] = {}
|
||||
cursor.execute("SELECT TRIM(rf.RDB$RELATION_NAME), TRIM(rf.RDB$FIELD_NAME) FROM RDB$RELATION_FIELDS rf WHERE rf.RDB$RELATION_NAME IN ('C000025','RTC_CCLASSTRIB','RTC_CST','PARAMETROS')")
|
||||
for table, column in cursor.fetchall():
|
||||
schema.setdefault(table, set()).add(column)
|
||||
extras: dict = {"param_value": None, "physical_fk": False}
|
||||
if "USAR_REFORMA_TRIBUTARIA" in schema.get("PARAMETROS", ()):
|
||||
try:
|
||||
cursor.execute("SELECT FIRST 1 USAR_REFORMA_TRIBUTARIA FROM PARAMETROS")
|
||||
row = cursor.fetchone()
|
||||
extras["param_value"] = None if row is None or row[0] is None else row[0]
|
||||
except Exception: # noqa: BLE001 - unreadable parameter => applicability stays unknown
|
||||
extras["param_value"] = None
|
||||
try:
|
||||
cursor.execute("SELECT COUNT(*) FROM RDB$RELATION_CONSTRAINTS WHERE RDB$RELATION_NAME = 'C000025' AND RDB$CONSTRAINT_TYPE = 'FOREIGN KEY' AND RDB$CONSTRAINT_NAME CONTAINING 'CCLASSTRIB'")
|
||||
extras["physical_fk"] = cursor.fetchone()[0] > 0
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
catalog = None
|
||||
if {"CCLASSTRIB", "CSTIBSCBS"} <= schema.get(RTC_CCLASSTRIB_TABLE, set()) and "CSTIBSCBS" in schema.get(RTC_CST_TABLE, set()):
|
||||
cursor.execute("SELECT TRIM(CCLASSTRIB), TRIM(CSTIBSCBS) FROM RTC_CCLASSTRIB")
|
||||
rows = cursor.fetchall()
|
||||
classes = {c: (s or None) for c, s in rows}
|
||||
cursor.execute("SELECT TRIM(CSTIBSCBS) FROM RTC_CST")
|
||||
cst = {r[0] for r in cursor.fetchall()}
|
||||
catalog = {"classes": classes, "cst": cst, "duplicates": len(rows) - len(classes)}
|
||||
return schema, catalog, extras
|
||||
|
||||
|
||||
def read_products_rtc_pis_cofins(cursor, schema: dict) -> list[dict]:
|
||||
"""SELECT of only the columns that exist (old schemas keep working)."""
|
||||
have = schema.get(PRODUCT_TABLE, set())
|
||||
columns = [c for c in PRODUCT_COLUMNS if c in have]
|
||||
if "CODIGO" not in columns:
|
||||
return []
|
||||
cursor.execute(f"SELECT {', '.join(columns)} FROM {PRODUCT_TABLE} ORDER BY CODIGO")
|
||||
return [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||
|
||||
|
||||
def assess_from_connection(conn, legacy_states: dict) -> dict:
|
||||
"""One read-only pass: capabilities -> catalog integrity -> expanded scope. Any failure degrades to an explicit unavailable
|
||||
marker; it never raises into the job and never touches the database beyond SELECT."""
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
schema, catalog, extras = load_rtc_context(cursor)
|
||||
caps = detect_rtc_capabilities(schema, param_value=extras["param_value"], catalog=catalog, physical_fk=extras["physical_fk"])
|
||||
caps["_PIS_COFINS_SCHEMA"] = all(c in schema.get(PRODUCT_TABLE, ()) for c in PIS_COFINS_COLUMNS[:6])
|
||||
products = read_products_rtc_pis_cofins(cursor, schema)
|
||||
summary = expanded_scope_summary(products, legacy_states, caps, catalog)
|
||||
summary["catalog_integrity"] = catalog_integrity(catalog) if catalog else None
|
||||
summary["capabilities"]["PIS_COFINS_SCHEMA_VALID"] = caps["_PIS_COFINS_SCHEMA"]
|
||||
summary["data_present"] = {c: sum(1 for p in products if _text(p.get(c))) for c in ("CCLASSTRIB",) + PIS_COFINS_COLUMNS if any(c in p for p in products[:1])}
|
||||
return summary
|
||||
except Exception as error: # noqa: BLE001
|
||||
return {"scope_version": "expanded-v1", "unavailable": type(error).__name__}
|
||||
@@ -0,0 +1,102 @@
|
||||
"""READ-ONLY ERP loader for the unified fiscal completeness (PIS/COFINS applicability + RTC when the ERP has the reform on).
|
||||
|
||||
The rules live in central/app/unified_fiscal_coverage.py (pure). This module only observes the ERP through an already open
|
||||
READ ONLY connection (SELECT only): schema capabilities, RTC catalog, PARAMETROS.USAR_REFORMA_TRIBUTARIA and the product columns.
|
||||
Nothing is written and no value is ever filled in.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import date
|
||||
|
||||
from central.app.unified_fiscal_coverage import PIS_COFINS_FIELDS, catalog_integrity, evaluate_product, rtc_enabled
|
||||
|
||||
PRODUCT_TABLE = "C000025"
|
||||
RTC_CCLASSTRIB_TABLE = "RTC_CCLASSTRIB"
|
||||
RTC_CST_TABLE = "RTC_CST"
|
||||
PRODUCT_COLUMNS = ("CODIGO", "CCLASSTRIB") + PIS_COFINS_FIELDS + ("PISCOFINS", "INCIDENCIA_PISCOFINS")
|
||||
TRUTHY = {"1", "S", "T", "TRUE"}
|
||||
|
||||
|
||||
def detect_rtc_capabilities(schema: dict, *, param_value=None, catalog: dict | None = None, physical_fk: bool | None = None) -> dict:
|
||||
"""schema: {TABLE: {COLUMN, ...}}. Fail-closed: anything not proven is False. Old databases without RTC simply report False."""
|
||||
product_cols = set(schema.get(PRODUCT_TABLE, ()))
|
||||
class_cols = set(schema.get(RTC_CCLASSTRIB_TABLE, ()))
|
||||
cst_cols = set(schema.get(RTC_CST_TABLE, ()))
|
||||
has_product = "CCLASSTRIB" in product_cols
|
||||
has_class = {"CCLASSTRIB", "CSTIBSCBS"} <= class_cols
|
||||
has_cst = "CSTIBSCBS" in cst_cols
|
||||
caps = {
|
||||
"HAS_PRODUCT_CCLASSTRIB": has_product, "HAS_RTC_CCLASSTRIB_TABLE": has_class, "HAS_RTC_CST_TABLE": has_cst,
|
||||
"HAS_RTC_RELATIONSHIP": bool(has_product and has_class and has_cst), "HAS_PHYSICAL_RTC_FK": bool(physical_fk),
|
||||
"RTC_ENABLED_BY_PARAM": False if param_value is None else str(param_value).strip().upper() in TRUTHY,
|
||||
"RTC_CATALOG_VALID": False,
|
||||
}
|
||||
if catalog is not None and has_class and has_cst:
|
||||
caps["RTC_CATALOG_VALID"] = catalog_integrity(catalog)["valid"]
|
||||
return caps
|
||||
|
||||
|
||||
def load_rtc_context(cursor) -> tuple[dict, dict | None, dict]:
|
||||
"""Schema + catalog + ERP parameter through a READ ONLY cursor. Never raises for a database that lacks RTC."""
|
||||
schema: dict[str, set] = {}
|
||||
cursor.execute("SELECT TRIM(rf.RDB$RELATION_NAME), TRIM(rf.RDB$FIELD_NAME) FROM RDB$RELATION_FIELDS rf WHERE rf.RDB$RELATION_NAME IN ('C000025','RTC_CCLASSTRIB','RTC_CST','PARAMETROS')")
|
||||
for table, column in cursor.fetchall():
|
||||
schema.setdefault(table, set()).add(column)
|
||||
extras: dict = {"param_value": None, "physical_fk": False}
|
||||
if "USAR_REFORMA_TRIBUTARIA" in schema.get("PARAMETROS", ()):
|
||||
try:
|
||||
cursor.execute("SELECT FIRST 1 USAR_REFORMA_TRIBUTARIA FROM PARAMETROS")
|
||||
row = cursor.fetchone()
|
||||
extras["param_value"] = None if row is None or row[0] is None else row[0]
|
||||
except Exception: # noqa: BLE001 - unreadable parameter => RTC stays off (not enabled by the ERP)
|
||||
extras["param_value"] = None
|
||||
try:
|
||||
cursor.execute("SELECT COUNT(*) FROM RDB$RELATION_CONSTRAINTS WHERE RDB$RELATION_NAME = 'C000025' AND RDB$CONSTRAINT_TYPE = 'FOREIGN KEY' AND RDB$CONSTRAINT_NAME CONTAINING 'CCLASSTRIB'")
|
||||
extras["physical_fk"] = cursor.fetchone()[0] > 0
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
catalog = None
|
||||
class_cols = schema.get(RTC_CCLASSTRIB_TABLE, set())
|
||||
if {"CCLASSTRIB", "CSTIBSCBS"} <= class_cols and "CSTIBSCBS" in schema.get(RTC_CST_TABLE, set()):
|
||||
validity = "INICIO_VIGENCIA" in class_cols and "FIM_VIGENCIA" in class_cols
|
||||
cursor.execute("SELECT TRIM(CCLASSTRIB), TRIM(CSTIBSCBS)" + (", INICIO_VIGENCIA, FIM_VIGENCIA" if validity else "") + " FROM RTC_CCLASSTRIB")
|
||||
rows = cursor.fetchall()
|
||||
classes = {r[0]: (r[1] or None) for r in rows}
|
||||
cursor.execute("SELECT TRIM(CSTIBSCBS) FROM RTC_CST")
|
||||
catalog = {"classes": classes, "cst": {r[0] for r in cursor.fetchall()}, "duplicates": len(rows) - len(classes)}
|
||||
if validity:
|
||||
catalog["validity"] = {r[0]: (r[2], r[3]) for r in rows}
|
||||
return schema, catalog, extras
|
||||
|
||||
|
||||
def read_products(cursor, schema: dict) -> list[dict]:
|
||||
"""SELECT of only the columns that exist (old schemas keep working)."""
|
||||
have = schema.get(PRODUCT_TABLE, set())
|
||||
columns = [c for c in PRODUCT_COLUMNS if c in have]
|
||||
if "CODIGO" not in columns:
|
||||
return []
|
||||
cursor.execute(f"SELECT {', '.join(columns)} FROM {PRODUCT_TABLE} ORDER BY CODIGO")
|
||||
return [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||
|
||||
|
||||
def assess_from_connection(conn, company: dict | None, *, today: date | None = None, rtc_override: bool | None = None) -> dict:
|
||||
"""One read-only pass. Returns {"context": {...}, "findings": {product_code: evaluate_product(...)}}.
|
||||
rtc_override forces RTC on/off for simulations WITHOUT touching the ERP parameter (tests / what-if only)."""
|
||||
cursor = conn.cursor()
|
||||
schema, catalog, extras = load_rtc_context(cursor)
|
||||
caps = detect_rtc_capabilities(schema, param_value=extras["param_value"], catalog=catalog, physical_fk=extras["physical_fk"])
|
||||
enabled = rtc_enabled(caps) if rtc_override is None else bool(rtc_override and caps["HAS_RTC_RELATIONSHIP"])
|
||||
rtc = {"enabled": enabled, "catalog": catalog, "catalog_valid": caps["RTC_CATALOG_VALID"]}
|
||||
today = today or date.today()
|
||||
products = read_products(cursor, schema)
|
||||
findings = {}
|
||||
for p in products:
|
||||
found = evaluate_product(p, company, rtc, today)
|
||||
found["input_fingerprint"] = hashlib.sha256(json.dumps({k: (None if v is None else str(v).strip()) for k, v in p.items()} | {"rtc": enabled}, sort_keys=True).encode()).hexdigest()
|
||||
findings[str(p.get("CODIGO") or "")] = found
|
||||
integrity = catalog_integrity(catalog) if catalog else None
|
||||
return {"context": {"rtc_enabled": enabled, "rtc_supported": caps["HAS_RTC_RELATIONSHIP"], "rtc_catalog_valid": caps["RTC_CATALOG_VALID"], "rtc_param": extras["param_value"],
|
||||
"catalog_integrity": integrity, "crt": (company or {}).get("crt"), "capabilities": caps, "products_evaluated": len(products)},
|
||||
"findings": findings}
|
||||
@@ -1,3 +1,24 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_real_firebird_for_unified_fiscal(monkeypatch, request):
|
||||
"""The unified fiscal assessment opens its own READ ONLY connection; unit tests have no Firebird, so it is neutral here unless a test opts in."""
|
||||
if request.node.get_closest_marker("real_unified_fiscal"):
|
||||
return
|
||||
from gti_fiscal_agent import firebird_reader, unified_fiscal
|
||||
|
||||
class _Conn:
|
||||
def close(self):
|
||||
pass
|
||||
monkeypatch.setattr(firebird_reader, "_connect", lambda env: _Conn())
|
||||
monkeypatch.setattr(unified_fiscal, "assess_from_connection", lambda conn, company, **kw: {"context": {"rtc_enabled": False, "rtc_supported": False, "rtc_catalog_valid": False, "products_evaluated": 0}, "findings": {}})
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line("markers", "real_unified_fiscal: use the real unified fiscal assessment (with a fake cursor) instead of the neutral stub")
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
"""RTC (cClassTrib / CST IBS-CBS) and PIS/COFINS: capabilities, validators, applicability, legacy vs expanded coverage. Read-only."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
||||
|
||||
from gti_fiscal_agent import rtc_pis_cofins as r # noqa: E402
|
||||
|
||||
FULL_SCHEMA = {"C000025": {"CODIGO", "CCLASSTRIB", *r.PIS_COFINS_COLUMNS}, "RTC_CCLASSTRIB": {"CCLASSTRIB", "CSTIBSCBS"}, "RTC_CST": {"CSTIBSCBS"}}
|
||||
CATALOG = {"classes": {"000001": "000", "200003": "200", "999999": None}, "cst": {"000", "200"}, "duplicates": 0}
|
||||
|
||||
|
||||
def caps(schema=FULL_SCHEMA, param=0, catalog=CATALOG):
|
||||
c = r.detect_rtc_capabilities(schema, param_value=param, catalog=catalog, physical_fk=True)
|
||||
c["_PIS_COFINS_SCHEMA"] = all(col in schema.get("C000025", ()) for col in r.PIS_COFINS_COLUMNS[:6])
|
||||
return c
|
||||
|
||||
|
||||
def test_database_without_rtc_fails_closed_and_is_not_blocked():
|
||||
c = caps({"C000025": {"CODIGO"}}, param=None, catalog=None)
|
||||
assert not any(c[k] for k in ("HAS_PRODUCT_CCLASSTRIB", "HAS_RTC_CCLASSTRIB_TABLE", "HAS_RTC_CST_TABLE", "HAS_RTC_RELATIONSHIP", "RTC_CATALOG_VALID"))
|
||||
assert r.rtc_applicability(c) == "NOT_SUPPORTED"
|
||||
found = r.expanded_findings({"CODIGO": 1}, c, None)
|
||||
assert found["blockers"] == [] and found["unknown"] == []
|
||||
|
||||
|
||||
def test_partial_rtc_column_without_catalog_tables_is_not_a_relationship():
|
||||
c = caps({"C000025": {"CODIGO", "CCLASSTRIB"}}, param=1, catalog=None)
|
||||
assert c["HAS_PRODUCT_CCLASSTRIB"] and not c["HAS_RTC_RELATIONSHIP"] and not c["RTC_CATALOG_VALID"]
|
||||
assert r.rtc_applicability(c) == "NOT_SUPPORTED"
|
||||
|
||||
|
||||
def test_full_model_and_valid_catalog_capabilities():
|
||||
good = {"classes": {"000001": "000"}, "cst": {"000"}, "duplicates": 0}
|
||||
c = caps(catalog=good)
|
||||
assert c["HAS_RTC_RELATIONSHIP"] and c["RTC_CATALOG_VALID"] and c["HAS_PHYSICAL_RTC_FK"] and c["RTC_ENABLED_BY_PARAM"] is False and c["LOCAL_CATALOG_ONLY"]
|
||||
assert not caps(catalog=CATALOG)["RTC_CATALOG_VALID"] # a class pointing to a missing CST makes the catalog invalid
|
||||
|
||||
|
||||
def test_cclasstrib_validator_states():
|
||||
assert r.validate_cclasstrib(None, CATALOG) == "EMPTY" and r.validate_cclasstrib(" ", CATALOG) == "EMPTY"
|
||||
assert r.validate_cclasstrib("12A456", CATALOG) == "INVALID_FORMAT" and r.validate_cclasstrib("12345", CATALOG) == "INVALID_FORMAT"
|
||||
assert r.validate_cclasstrib("123456", CATALOG) == "NOT_IN_LOCAL_CATALOG"
|
||||
assert r.validate_cclasstrib("200003", CATALOG) == "VALID_WITH_CST"
|
||||
assert r.validate_cclasstrib("999999", CATALOG) == "CST_MISSING"
|
||||
assert r.validate_cclasstrib("000001", {"classes": {"000001": None}, "cst": set()}) == "VALID_LOCAL"
|
||||
assert r.validate_cclasstrib("000001", None) == "NOT_IN_LOCAL_CATALOG"
|
||||
|
||||
|
||||
def test_pis_cofins_validators():
|
||||
assert r.validate_pis_cofins_cst("") == "EMPTY" and r.validate_pis_cofins_cst(None) == "EMPTY"
|
||||
assert r.validate_pis_cofins_cst("01") == "VALID" and r.validate_pis_cofins_cst("73") == "VALID"
|
||||
assert r.validate_pis_cofins_cst("2") == "INVALID_FORMAT" and r.validate_pis_cofins_cst("10") == "INVALID_FORMAT"
|
||||
assert r.validate_pis_cofins_rate("0,65") == "VALID" and r.validate_pis_cofins_rate("3") == "VALID" and r.validate_pis_cofins_rate("") == "EMPTY"
|
||||
assert r.validate_pis_cofins_rate("abc") == "INVALID_RATE" and r.validate_pis_cofins_rate("150") == "INVALID_RATE"
|
||||
ok = r.validate_pis_cofins({"PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFASICA", "COD_SAIDA_PIS": "04", "COD_SAIDA_COFINS": "04"})
|
||||
assert ok["FLAG_CONSISTENCY"] == "CONSISTENT"
|
||||
bad = r.validate_pis_cofins({"PISCOFINS": "N", "INCIDENCIA_PISCOFINS": "NENHUMA", "COD_SAIDA_PIS": "01"})
|
||||
assert bad["FLAG_CONSISTENCY"] == "INCONSISTENT"
|
||||
|
||||
|
||||
def test_empty_pis_cofins_is_unknown_applicability_not_a_blocker():
|
||||
found = r.expanded_findings({"CODIGO": 1, "PISCOFINS": "N", "INCIDENCIA_PISCOFINS": "NENHUMA"}, caps(), CATALOG)
|
||||
assert found["blockers"] == [] and found["unknown"] == ["PIS_COFINS"] and found["rtc_applicability"] == "NOT_ENABLED_BY_ERP"
|
||||
|
||||
|
||||
def test_populated_pis_cofins_is_applicable_and_valid_data_completes():
|
||||
row = {"CODIGO": 1, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "SUBST.TRIBUTARIA", "COD_ENTRADA_PIS": "73", "COD_SAIDA_PIS": "01", "ALIQ_PIS": "0,65",
|
||||
"COD_ENTRADA_COFINS": "73", "COD_SAIDA_COFINS": "01", "ALIQ_COFINS": "3"}
|
||||
found = r.expanded_findings(row, caps(), CATALOG)
|
||||
assert found["blockers"] == [] and found["unknown"] == [] and found["pis_cofins_applicability"] == "APPLICABLE"
|
||||
|
||||
|
||||
def test_invalid_values_where_applicable_create_specific_blockers():
|
||||
row = {"CODIGO": 2, "PISCOFINS": "N", "INCIDENCIA_PISCOFINS": "NENHUMA", "COD_ENTRADA_PIS": "2", "COD_SAIDA_PIS": "2", "ALIQ_PIS": "2",
|
||||
"COD_ENTRADA_COFINS": "2", "COD_SAIDA_COFINS": "2", "ALIQ_COFINS": "x"}
|
||||
kinds = {k for k, _ in r.expanded_findings(row, caps(), CATALOG)["blockers"]}
|
||||
assert {"PIS_CST_INVALID", "COFINS_CST_INVALID", "COFINS_RATE_INVALID", "PIS_COFINS_FLAG_INCONSISTENT"} <= kinds
|
||||
|
||||
|
||||
def test_rtc_becomes_applicable_only_when_the_erp_enables_it():
|
||||
on = caps(param=1)
|
||||
assert r.rtc_applicability(on) == "APPLICABLE"
|
||||
assert {k for k, _ in r.expanded_findings({"CODIGO": 1}, on, CATALOG)["blockers"]} == {"CCLASSTRIB_MISSING"}
|
||||
assert {k for k, _ in r.expanded_findings({"CODIGO": 1, "CCLASSTRIB": "123456"}, on, CATALOG)["blockers"]} == {"CCLASSTRIB_NOT_IN_CATALOG"}
|
||||
assert {k for k, _ in r.expanded_findings({"CODIGO": 1, "CCLASSTRIB": "999999"}, on, CATALOG)["blockers"]} == {"RTC_CST_INVALID"}
|
||||
assert {k for k, _ in r.expanded_findings({"CODIGO": 1, "CCLASSTRIB": "12x"}, on, CATALOG)["blockers"]} == {"CCLASSTRIB_INVALID"}
|
||||
assert not [k for k, _ in r.expanded_findings({"CODIGO": 1, "CCLASSTRIB": "200003"}, on, CATALOG)["blockers"] if k.startswith(("CCLASS", "RTC"))]
|
||||
assert r.rtc_applicability(caps(param=None)) == "UNKNOWN_APPLICABILITY"
|
||||
|
||||
|
||||
def test_legacy_and_expanded_coverage_are_reported_side_by_side_and_close():
|
||||
products = [{"CODIGO": 1}, {"CODIGO": 2}, {"CODIGO": 3},
|
||||
{"CODIGO": 4, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFASICA", "COD_SAIDA_PIS": "01", "COD_SAIDA_COFINS": "01", "COD_ENTRADA_PIS": "73", "COD_ENTRADA_COFINS": "73"},
|
||||
{"CODIGO": 5, "COD_SAIDA_PIS": "2", "PISCOFINS": "S"}]
|
||||
states = {"1": "COMPLETE", "2": "BLOCKED_MISSING_EVIDENCE", "3": "FAILED_TECHNICAL", "4": "COMPLETE", "5": "CORRECTED"}
|
||||
out = r.expanded_scope_summary(products, states, caps(), CATALOG)
|
||||
assert out["legacy_scope"] == {"complete": 3, "safe_blocked": 1, "failed": 1}
|
||||
# 1 unknown (no PIS/COFINS evidence), 1 complete (#4 valid), blocked = legacy-blocked #2 + #5 (invalid CST), failed #3
|
||||
assert out["expanded_scope"] == {"complete": 1, "safe_blocked": 2, "failed": 1, "unknown_applicability": 1}
|
||||
assert out["invariant_pass"] and out["blockers_by_type"].get("PIS_CST_INVALID") == 1 and out["local_catalog_only"]
|
||||
|
||||
|
||||
class FakeCursor:
|
||||
def __init__(self, tables, rows):
|
||||
self.tables, self.rows, self.result = tables, rows, []
|
||||
|
||||
def execute(self, sql):
|
||||
assert sql.lstrip().upper().startswith("SELECT") # read-only by construction
|
||||
if "RDB$RELATION_FIELDS" in sql:
|
||||
self.result = [(t, c) for t, cols in self.tables.items() for c in cols]
|
||||
elif "USAR_REFORMA_TRIBUTARIA" in sql:
|
||||
self.result = [(0,)]
|
||||
elif "RDB$RELATION_CONSTRAINTS" in sql:
|
||||
self.result = [(1,)]
|
||||
elif "FROM RTC_CCLASSTRIB" in sql:
|
||||
self.result = [("000001", "000")]
|
||||
elif "FROM RTC_CST" in sql:
|
||||
self.result = [("000",)]
|
||||
elif "FROM C000025" in sql:
|
||||
self.result = self.rows
|
||||
|
||||
def fetchall(self):
|
||||
return self.result
|
||||
|
||||
def fetchone(self):
|
||||
return self.result[0] if self.result else None
|
||||
|
||||
|
||||
def test_loader_reads_schema_catalog_and_param_with_select_only():
|
||||
cur = FakeCursor({"C000025": ["CODIGO", "CCLASSTRIB"], "RTC_CCLASSTRIB": ["CCLASSTRIB", "CSTIBSCBS"], "RTC_CST": ["CSTIBSCBS"], "PARAMETROS": ["USAR_REFORMA_TRIBUTARIA"]}, [(1, None)])
|
||||
schema, catalog, extras = r.load_rtc_context(cur)
|
||||
assert catalog == {"classes": {"000001": "000"}, "cst": {"000"}, "duplicates": 0} and extras == {"param_value": 0, "physical_fk": True}
|
||||
assert r.read_products_rtc_pis_cofins(cur, schema) == [{"CODIGO": 1, "CCLASSTRIB": None}]
|
||||
@@ -0,0 +1,110 @@
|
||||
import re
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
|
||||
from gti_fiscal_agent import unified_fiscal as uf
|
||||
|
||||
pytestmark = pytest.mark.real_unified_fiscal
|
||||
|
||||
SCHEMA_ROWS = ([("C000025", c) for c in ("CODIGO", "CCLASSTRIB", "COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS", "PISCOFINS", "INCIDENCIA_PISCOFINS")]
|
||||
+ [("RTC_CCLASSTRIB", c) for c in ("CCLASSTRIB", "CSTIBSCBS", "INICIO_VIGENCIA", "FIM_VIGENCIA")] + [("RTC_CST", "CSTIBSCBS")] + [("PARAMETROS", "USAR_REFORMA_TRIBUTARIA")])
|
||||
PRODUCT_COLS = ("CODIGO", "CCLASSTRIB", "COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS", "PISCOFINS", "INCIDENCIA_PISCOFINS")
|
||||
PRODUCTS = [("1", None, None, None, None, None, None, None, "N", "NENHUMA"), ("2", "200003", None, "04", None, None, "04", None, "S", "MONOFÁSICA"), ("3", None, "2", "2", "2", "2", "2", "2", "N", "NENHUMA")]
|
||||
CLASSES = [("000001", "000", date(2025, 5, 5), None), ("200003", "200", date(2025, 5, 5), None)]
|
||||
|
||||
|
||||
class FakeCursor:
|
||||
"""READ ONLY fake: answers only SELECTs and records every statement so tests can prove nothing else ran."""
|
||||
def __init__(self, param=0, schema=SCHEMA_ROWS, products=PRODUCTS, classes=CLASSES, cst=(("000",), ("200",))):
|
||||
self.param, self.schema, self.products, self.classes, self.cst, self.statements, self._rows = param, schema, products, classes, cst, [], []
|
||||
|
||||
def execute(self, sql, *args):
|
||||
self.statements.append(sql)
|
||||
text = " ".join(sql.split()).upper()
|
||||
assert text.startswith("SELECT"), f"non-SELECT statement: {sql}"
|
||||
if "FROM RDB$RELATION_FIELDS" in text:
|
||||
self._rows = list(self.schema)
|
||||
elif "FROM PARAMETROS" in text:
|
||||
self._rows = [(self.param,)]
|
||||
elif "RDB$RELATION_CONSTRAINTS" in text:
|
||||
self._rows = [(1,)]
|
||||
elif "FROM RTC_CCLASSTRIB" in text:
|
||||
self._rows = list(self.classes)
|
||||
elif "FROM RTC_CST" in text:
|
||||
self._rows = list(self.cst)
|
||||
elif "FROM C000025" in text:
|
||||
self._rows = list(self.products)
|
||||
else:
|
||||
raise AssertionError(sql)
|
||||
|
||||
def fetchall(self):
|
||||
return self._rows
|
||||
|
||||
def fetchone(self):
|
||||
return self._rows[0] if self._rows else None
|
||||
|
||||
|
||||
class FakeConn:
|
||||
def __init__(self, **kw):
|
||||
self.cur = FakeCursor(**kw)
|
||||
|
||||
def cursor(self):
|
||||
return self.cur
|
||||
|
||||
|
||||
def kinds(found):
|
||||
return sorted({b["type"] for b in found["blockers"]})
|
||||
|
||||
|
||||
def test_rtc_off_in_the_erp_is_out_of_scope_and_never_blocks():
|
||||
conn = FakeConn(param=0)
|
||||
out = uf.assess_from_connection(conn, {"crt": "1"}, today=date(2026, 9, 24))
|
||||
assert out["context"]["rtc_enabled"] is False and out["context"]["rtc_supported"] is True and out["context"]["rtc_catalog_valid"] is True
|
||||
assert not [b for f in out["findings"].values() for b in f["blockers"] if b["category"] == "RTC"]
|
||||
assert kinds(out["findings"]["1"]) == [] and kinds(out["findings"]["2"]) == [] and kinds(out["findings"]["3"]) == ["COFINS_CST_INVALID", "PIS_CST_INVALID"]
|
||||
assert all(s.strip().upper().startswith("SELECT") for s in conn.cur.statements) # read-only: SELECT statements only
|
||||
|
||||
|
||||
def test_rtc_on_in_the_erp_brings_cclasstrib_into_the_same_findings():
|
||||
out = uf.assess_from_connection(FakeConn(param=1), {"crt": "1"}, today=date(2026, 9, 24))
|
||||
assert out["context"]["rtc_enabled"] is True
|
||||
assert kinds(out["findings"]["1"]) == ["CCLASSTRIB_MISSING"] and kinds(out["findings"]["2"]) == []
|
||||
assert kinds(out["findings"]["3"]) == ["CCLASSTRIB_MISSING", "COFINS_CST_INVALID", "PIS_CST_INVALID"] # one queue, one verdict per product
|
||||
|
||||
|
||||
def test_simulation_flag_never_touches_the_erp_parameter():
|
||||
conn = FakeConn(param=0)
|
||||
on = uf.assess_from_connection(conn, {"crt": "1"}, rtc_override=True)
|
||||
assert on["context"]["rtc_enabled"] is True and kinds(on["findings"]["1"]) == ["CCLASSTRIB_MISSING"]
|
||||
assert on["context"]["rtc_param"] == 0 and not any("UPDATE" in s.upper() for s in conn.cur.statements)
|
||||
|
||||
|
||||
def test_old_schema_without_rtc_columns_reports_unsupported_and_still_evaluates_pis_cofins():
|
||||
old = [("C000025", c) for c in ("CODIGO", "COD_SAIDA_PIS", "PISCOFINS", "INCIDENCIA_PISCOFINS")]
|
||||
rows = [("1", None, "N", "NENHUMA"), ("2", "9", "N", "NENHUMA")]
|
||||
out = uf.assess_from_connection(FakeConn(schema=old, products=rows, classes=[], cst=()), {"crt": "3"})
|
||||
assert out["context"]["rtc_supported"] is False and out["context"]["rtc_enabled"] is False
|
||||
assert kinds(out["findings"]["1"]) == [] and kinds(out["findings"]["2"]) == ["PIS_CST_INVALID"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value,expected", [(None, False), (0, False), ("0", False), (1, True), ("1", True), ("S", True), ("N", False)])
|
||||
def test_parameter_values_are_read_fail_closed(value, expected):
|
||||
caps = uf.detect_rtc_capabilities({"C000025": {"CCLASSTRIB"}, "RTC_CCLASSTRIB": {"CCLASSTRIB", "CSTIBSCBS"}, "RTC_CST": {"CSTIBSCBS"}}, param_value=value)
|
||||
assert caps["RTC_ENABLED_BY_PARAM"] is expected
|
||||
|
||||
|
||||
def test_catalog_with_orphan_or_duplicate_is_invalid_and_flagged_only_when_rtc_is_on():
|
||||
bad = FakeConn(param=1, classes=CLASSES + [("200003", "200", date(2025, 5, 5), None)]) # duplicate class
|
||||
on = uf.assess_from_connection(bad, {"crt": "1"})
|
||||
assert on["context"]["rtc_catalog_valid"] is False and kinds(on["findings"]["2"]) == ["RTC_CATALOG_INVALID"]
|
||||
off = uf.assess_from_connection(FakeConn(param=0, classes=CLASSES + [("200003", "200", date(2025, 5, 5), None)]), {"crt": "1"})
|
||||
assert not [b for f in off["findings"].values() for b in f["blockers"] if b["category"] == "RTC"]
|
||||
|
||||
|
||||
def test_input_fingerprint_changes_with_the_fiscal_values_and_the_rtc_switch():
|
||||
a = uf.assess_from_connection(FakeConn(param=0), {"crt": "1"})["findings"]["1"]["input_fingerprint"]
|
||||
b = uf.assess_from_connection(FakeConn(param=1), {"crt": "1"})["findings"]["1"]["input_fingerprint"]
|
||||
changed = [("1", None, None, "99", None, None, None, None, "N", "NENHUMA")] + PRODUCTS[1:]
|
||||
c = uf.assess_from_connection(FakeConn(param=0, products=changed), {"crt": "1"})["findings"]["1"]["input_fingerprint"]
|
||||
assert len({a, b, c}) == 3 and re.fullmatch(r"[0-9a-f]{64}", a)
|
||||
@@ -146,3 +146,42 @@ def test_heartbeats_keep_flowing_while_a_long_job_runs(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(service, "execute_job", lambda *a, **k: (time.sleep(0.4), ({"ok": 1}, None))[1])
|
||||
service.run(_config(agent_id="2", poll_interval_seconds=0, state_db=str(tmp_path / "a.db")), {"AGENT_ID": "2"}, stopper=stopper)
|
||||
assert beats.count("BUSY") >= 3 # heartbeats during the job, not only before it
|
||||
|
||||
|
||||
def _findings(**by_code):
|
||||
out = {}
|
||||
for code, kinds in by_code.items():
|
||||
out[code] = {"blockers": [{"type": t, "category": c, "reason": "R"} for t, c in kinds], "applicability": {"PIS_COFINS": "REQUIRED", "RTC": "NOT_APPLICABLE"}, "input_fingerprint": "fp" + code, "needs_review": False}
|
||||
return out
|
||||
|
||||
|
||||
def test_unified_fiscal_blockers_change_the_single_coverage_and_the_single_queue(env_setup, monkeypatch):
|
||||
from gti_fiscal_agent import unified_fiscal
|
||||
findings = _findings(**{"2": [("PIS_CST_INVALID", "PIS"), ("COFINS_CST_INVALID", "COFINS")]})
|
||||
monkeypatch.setattr(unified_fiscal, "assess_from_connection", lambda conn, company, **kw: {"context": {"rtc_enabled": False, "rtc_supported": True, "rtc_catalog_valid": True}, "findings": findings})
|
||||
hub, state = env_setup
|
||||
result, error = run_job("INITIAL_SYNC", [product("1"), product("2"), product("3", ncm="")], state, monkeypatch)
|
||||
assert error is None
|
||||
op = result["operational"]
|
||||
assert op["TOTAL_PRODUCTS"] == 3 and op["COMPLETE_PRODUCTS"] == 1 and op["SAFE_BLOCKED"] == 2 and op["BLOCKED_FISCAL_DATA"] == 1 # one number, one definition
|
||||
assert op["COMPLETE_PRODUCTS"] + op["SAFE_BLOCKED"] + op["FAILED_TECHNICAL"] + op["UNCLASSIFIED"] == 3
|
||||
queue = sorted((b["product_code"], b["field"], b["reason_code"]) for b in result["blocked_products"])
|
||||
assert queue == [("2", "COFINS", "COFINS_CST_INVALID"), ("2", "PIS", "PIS_CST_INVALID"), ("3", "NCM", "INVALID_NCM")] # ONE queue: NCM and fiscal blockers together
|
||||
assert result["resolved_products"] == ["1"] # a fiscal blocker keeps the product out of resolved
|
||||
assert result["fiscal_context"]["rtc_enabled"] is False and result["fiscal_context"]["evaluated_all"] is True
|
||||
assert "expanded_scope" not in result
|
||||
|
||||
|
||||
def test_rtc_context_is_reported_but_blockers_only_exist_when_the_evaluator_produced_them(env_setup, monkeypatch):
|
||||
from gti_fiscal_agent import unified_fiscal
|
||||
monkeypatch.setattr(unified_fiscal, "assess_from_connection", lambda conn, company, **kw: {"context": {"rtc_enabled": True, "rtc_supported": True, "rtc_catalog_valid": True}, "findings": _findings(**{"1": [("CCLASSTRIB_MISSING", "RTC")]})})
|
||||
hub, state = env_setup
|
||||
result, _ = run_job("REPROCESS_ALL", [product("1"), product("2")], state, monkeypatch)
|
||||
assert [b["reason_code"] for b in result["blocked_products"]] == ["CCLASSTRIB_MISSING"] and result["blocked_products"][0]["field"] == "RTC"
|
||||
assert result["fiscal_context"]["rtc_enabled"] is True and result["operational"]["BLOCKED_FISCAL_DATA"] == 1
|
||||
|
||||
|
||||
def test_partial_reprocess_does_not_claim_a_full_fiscal_evaluation(env_setup, monkeypatch):
|
||||
hub, state = env_setup
|
||||
result, _ = run_job("REPROCESS_PRODUCTS", [product("1")], state, monkeypatch)
|
||||
assert result["fiscal_context"] is None and result["operational"] is None
|
||||
|
||||
Reference in New Issue
Block a user