"""Helix-native close **signals** — live decision support for close tasks. Generalises the controller close-cockpit's go/no-go checks (data complete / GL synced / NetSuite period closed / reconciliation clean / no open restatements) into a named registry, and adds a few more drawn straight from the finance-owned data (FX frozen, cloud cost materialised, 汇联易 synced). Each signal evaluates to ``ok`` / ``warn`` / ``fail`` / ``na`` for a period, with a human detail line and a deep-link to the Helix page that is the *evidence* for it. A close task may bind to one signal via ``signal_key`` — the console then shows that signal's live state beside the task as decision support. **Signals never auto-complete a task** — marking a task done stays a human judgement (the data being green is necessary, not sufficient). All reads are finance-domain-owned (read_service + finance models), so no cross-domain import is needed. """ from __future__ import annotations from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession # status values from app.core.safe_logging import get_safe_logger from app.domains.finance import read_service from app.domains.finance.close_signal_keys import ( # noqa: F401 (re-exported) ALL_SIGNAL_KEYS, DEFAULT_SIGNAL_BINDINGS, SIG_CLOUD_COST, SIG_DATA_COMPLETE, SIG_FX_FROZEN, SIG_GL_SYNCED, SIG_HUILIANYI, SIG_NO_OPEN_RESTATEMENTS, SIG_NS_PERIOD_CLOSED, SIG_RECON_CLEAN, ) from app.domains.finance.models import ( COST_TYPE_CLOUD, FinanceExternalStatement, FinanceFxMonthly, FinanceSpendFact, ) logger = get_safe_logger(__name__) OK = "ok" WARN = "warn" FAIL = "fail" NA = "na" # Evidence deep-links (existing finance pages). _LINK_COCKPIT = "/admin/finance/close-cockpit" _LINK_GL = "/admin/finance/netsuite-gl" _LINK_EXTERNAL = "/admin/finance/external-recon" _LINK_EXPLORER = "/admin/finance/explorer" def _sig(key: str, status: str, detail: str, evidence_link: str) -> dict: return {"key": key, "status": status, "detail": detail, "evidence_link": evidence_link} async def evaluate_signals(db: AsyncSession, period: str) -> dict[str, dict]: """Evaluate every close signal for ``period`` → ``{key: signal}``.""" out: dict[str, dict] = {} # ── the five cockpit checks (reuse the single source of truth) ── cockpit_links = { SIG_DATA_COMPLETE: _LINK_COCKPIT, SIG_GL_SYNCED: _LINK_GL, SIG_NS_PERIOD_CLOSED: _LINK_GL, SIG_RECON_CLEAN: _LINK_COCKPIT, SIG_NO_OPEN_RESTATEMENTS: _LINK_COCKPIT, } # Signals are advisory decision-support; a failure in the (heavier) # cockpit query must NOT take down the task list it rides along with. # Degrade those five to ``na`` instead of propagating the error. try: cockpit = await read_service.close_cockpit(db, period) for item in cockpit["checklist"]: key = item["key"] out[key] = _sig( key, OK if item["ok"] else FAIL, item["detail"], cockpit_links.get(key, _LINK_COCKPIT), ) except Exception: # noqa: BLE001 - advisory; degrade rather than 500 the list logger.warning("close signals: cockpit evaluation failed for %s", period, exc_info=True) for key, link in cockpit_links.items(): out[key] = _sig(key, NA, "signal unavailable", link) # ── FX frozen for the period ── fx_total, fx_frozen = ( await db.execute( select( func.count(), func.coalesce(func.sum(FinanceFxMonthly.frozen), 0), ).where(FinanceFxMonthly.period == period) ) ).one() if not fx_total: out[SIG_FX_FROZEN] = _sig(SIG_FX_FROZEN, WARN, "no FX rates for the period yet", _LINK_EXPLORER) elif int(fx_frozen) == int(fx_total): out[SIG_FX_FROZEN] = _sig(SIG_FX_FROZEN, OK, f"{fx_total} currencies frozen", _LINK_EXPLORER) else: out[SIG_FX_FROZEN] = _sig( SIG_FX_FROZEN, WARN, f"{int(fx_frozen)}/{int(fx_total)} currencies frozen", _LINK_EXPLORER, ) # ── cloud cost materialised ── cloud_rows = ( await db.execute( select(func.count()).where( FinanceSpendFact.period == period, FinanceSpendFact.cost_type == COST_TYPE_CLOUD, ) ) ).scalar() or 0 out[SIG_CLOUD_COST] = _sig( SIG_CLOUD_COST, OK if cloud_rows else WARN, f"{cloud_rows} cloud cost rows" if cloud_rows else "no cloud cost materialised yet", _LINK_EXPLORER, ) # ── 汇联易 external statements synced ── hly_rows = ( await db.execute( select(func.count()).where(FinanceExternalStatement.period == period) ) ).scalar() or 0 out[SIG_HUILIANYI] = _sig( SIG_HUILIANYI, OK if hly_rows else WARN, f"{hly_rows} expense statements" if hly_rows else "no 汇联易 statements for the period", _LINK_EXTERNAL, ) return out