"""v4.9.24: reconciliation process review must be explainable and actionable.""" import sys import types from pathlib import Path if "sqlalchemy" not in sys.modules: sqlalchemy = types.ModuleType("sqlalchemy") sqlalchemy.text = lambda sql: sql sys.modules["sqlalchemy"] = sqlalchemy if "app.db" not in sys.modules: app_db = types.ModuleType("app.db") app_db.engine = object() sys.modules["app.db"] = app_db if "app.action_catalog" not in sys.modules: action_catalog = types.ModuleType("app.action_catalog") action_catalog.get_action_config = lambda code: {"route": "rever", "action": code, "action_required": True, "safe_to_post": False} sys.modules["app.action_catalog"] = action_catalog from app import reconciliation_service as svc ROOT = Path(__file__).resolve().parents[1] def read(path: str) -> str: return (ROOT / path).read_text(encoding="utf-8") def _item(id_, external_type, number, date, amount): return { "id": id_, "source_system": "jasmin" if external_type.startswith("jasmin") else "odoo", "external_type": external_type, "title": f"{external_type} · {number}", "customer_name": "ACZCO BRAGA ENERGY, LDA", "customer_tax_id": "517249200", "document_number": number, "document_date": date, "amount": amount, "currency": "EUR", "payload": {"record": {"name": number, "invoice_status": "to invoice"}} if external_type == "odoo_sale_order" else {}, "operation_suggestions": [], } def test_process_candidates_explain_reasons_and_risks(monkeypatch): monkeypatch.setattr( svc, "list_reconciliation_items", lambda *, status, limit, days: [ _item("quote-154", "jasmin_quotation", "ORC.ORC2026.154", "2026-06-03", "638.60"), _item("sale-279", "odoo_sale_order", "S00279", "2026-06-05", "638.60"), ], ) candidates = svc.list_reconciliation_process_candidates(status="open", days=7, limit=10) assert len(candidates) == 1 candidate = candidates[0] assert candidate["review_status"] == "needs_review" assert "cliente fiscal por NIF exato" in candidate["reasons"] assert "valor igual entre documentos" in candidate["reasons"] assert "datas próximas" in candidate["reasons"] assert "venda Odoo sem fatura Jasmin associada" in candidate["risks"] def test_process_candidates_flag_conflict_when_amounts_differ(monkeypatch): monkeypatch.setattr( svc, "list_reconciliation_items", lambda *, status, limit, days: [ _item("quote-154", "jasmin_quotation", "ORC.ORC2026.154", "2026-06-03", "638.60"), _item("invoice-80", "jasmin_invoice", "FA2026.80", "2026-06-05", "190.00"), ], ) candidates = svc.list_reconciliation_process_candidates(status="open", days=7, limit=10) assert len(candidates) == 2 # different amounts keep separate purchase anchors assert all("reasons" in candidate and "risks" in candidate for candidate in candidates) def test_v4924_schema_and_ui_have_decisions_review_and_history_actions(): service = read("app/reconciliation_service.py") page = read("app/admin_ui/pages/reconciliation.py") styles = read("app/admin_ui/styles.py") assert "CREATE TABLE IF NOT EXISTS reconciliation_decisions" in service assert "def _candidate_reasons_and_risks" in service assert "reasons" in service and "risks" in service and "review_status" in service assert "/reconciliation/processes/needs-review" in page assert "/reconciliation/processes/historical" in page assert "Motivos" in page and "Riscos" in page assert "cf-reconcile-explain" in styles