"""Serialise close-domain ORM rows + computed windows to plain JSON dicts. Shared by the HTTP router and (later) the MCP tools so the wire shape stays consistent across surfaces. """ from __future__ import annotations from app.domains.finance import close_calendar as engine from app.domains.finance import close_service from app.domains.finance.close_models import ( FinanceCloseCalendarConfig, FinanceCloseEntity, FinanceCloseHoliday, FinanceCloseMakeupDay, FinanceCloseRun, FinanceCloseTask, FinanceCloseTaskTemplate, ) def _iso(d) -> str | None: return d.isoformat() if d is not None else None def window_to_dict(window: engine.CloseWindow, effective_close) -> dict: return { "close_date": _iso(effective_close), "computed_close_date": _iso(window.close_date), "target_workday": window.target_workday, "work_weekend_day": window.work_weekend_day, "anchor_minus_1": _iso(window.anchor_minus_1), "anchor_plus_1": _iso(window.anchor_plus_1), "weekend_work_dates": [_iso(d) for d in window.weekend_work_dates], "days": [ { "day": _iso(wd.day), "kind": wd.kind, "workday_number": wd.workday_number, "is_close": wd.is_close, } for wd in window.days ], # Day-token → resolved deadline map (Day-5..Day+target+margin), handy # for the deadline-anchor panel and for instance deadline computation. "anchors": _anchor_preview(window), } def _anchor_preview(window: engine.CloseWindow) -> dict[str, str | None]: out: dict[str, str | None] = {} for n in range(5, 0, -1): out[f"Day-{n}"] = _iso(window.deadline_for(f"Day-{n}")) max_plus = window.target_workday + 5 for n in range(1, max_plus + 1): out[f"Day+{n}"] = _iso(window.deadline_for(f"Day+{n}")) return out def run_to_dict(run: FinanceCloseRun, window: engine.CloseWindow) -> dict: effective_close = close_service.effective_close_date(run, window) return { "period": run.period, "label": run.label, "status": run.status, "anchor_minus_1": _iso(run.anchor_minus_1), "anchor_plus_1": _iso(run.anchor_plus_1), "override_close_date": _iso(run.override_close_date), "override_reason": run.override_reason, "override_by_id": run.override_by_id, "override_at": run.override_at.isoformat() if run.override_at else None, "frozen_close_date": _iso(run.frozen_close_date), "soft_closed_at": run.soft_closed_at.isoformat() if run.soft_closed_at else None, "closed_at": run.closed_at.isoformat() if run.closed_at else None, "reopened_at": run.reopened_at.isoformat() if run.reopened_at else None, "note": run.note, "window": window_to_dict(window, effective_close), } def config_to_dict(config: FinanceCloseCalendarConfig) -> dict: return { "target_workday": config.target_workday, "work_weekend_day": config.work_weekend_day, "updated_at": config.updated_at.isoformat() if config.updated_at else None, } def holiday_to_dict(h: FinanceCloseHoliday) -> dict: return { "id": h.id, "day": _iso(h.day), "country": h.country, "name": h.name, "source": h.source, } def makeup_to_dict(m: FinanceCloseMakeupDay) -> dict: return {"id": m.id, "day": _iso(m.day), "note": m.note, "source": m.source} def _num(v): return float(v) if v is not None else None def task_to_dict(t: FinanceCloseTask) -> dict: return { "id": t.id, "period": t.period, "instance_key": t.instance_key, "template_source_key": t.template_source_key, "process": t.process, "sub_process": t.sub_process, "description": t.description, "owner_person_id": t.owner_person_id, "owner_label": t.owner_label, "frequency": t.frequency, "calendar_day": t.calendar_day, "estimated_hours": _num(t.estimated_hours), "submit_to_fpa": t.submit_to_fpa, "ai_work": t.ai_work, "link": t.link, "entity_codes": t.entity_codes or [], "sort_order": t.sort_order, "signal_key": t.signal_key, "status": t.status, "deadline": _iso(t.deadline), "comment": t.comment, "commenter_person_id": t.commenter_person_id, "commenter_label": t.commenter_label, "completed_at": t.completed_at.isoformat() if t.completed_at else None, } def template_to_dict(t: FinanceCloseTaskTemplate) -> dict: return { "id": t.id, "source_key": t.source_key, "process": t.process, "sub_process": t.sub_process, "description": t.description, "owner_person_id": t.owner_person_id, "owner_label": t.owner_label, "frequency": t.frequency, "calendar_day": t.calendar_day, "estimated_hours": _num(t.estimated_hours), "submit_to_fpa": t.submit_to_fpa, "ai_work": t.ai_work, "link": t.link, "entity_codes": t.entity_codes or [], "sort_order": t.sort_order, "signal_key": t.signal_key, "is_active": t.is_active, } def entity_to_dict(e: FinanceCloseEntity) -> dict: return { "id": e.id, "code": e.code, "label": e.label, "legal_entity_id": e.legal_entity_id, "legal_entity_name": e.legal_entity_name, "sort_order": e.sort_order, "active": e.active, }