"""``offboarding.account_assurance_rosters`` — the 账号终态 page learns every identity-sync system (PR3, migration 609). Reads the vendor roster snapshots the identity-sync domain already refreshes nightly (``identity_sync.vendor_snapshot_refresh``, 03:00 UTC) and writes one ``offboarding_account_assurance`` row per (leaver × target) plus a freshness heartbeat per target. DB-only — no vendor calls; the evidence is the snapshot, and the snapshot's age is reported next to every verdict. """ from __future__ import annotations import logging from typing import Any from sqlalchemy.ext.asyncio import AsyncSession from app.scheduled_jobs.registry import register_scheduled_job logger = logging.getLogger(__name__) ACCOUNT_ASSURANCE_ROSTERS_HANDLER = "offboarding.account_assurance_rosters" @register_scheduled_job( key=ACCOUNT_ASSURANCE_ROSTERS_HANDLER, display_name="离职账号终态 · 身份同步名册对账", description=( "每日 04:10 UTC(排在 identity_sync.leaver_assurance 03:47 之后)。对每个启用且 " "on_offboard∈(deactivate,delete) 的身份同步目标,读厂商名册快照,给每个离职者写一行 " "offboarding_account_assurance(system=isync:):快照里没有 → ok(absent);" "is_active=false / 厂商状态为停用 → ok(disabled);is_active=true 且快照拍摄日晚于离职日 → " "at_risk(登记保留分类则 exempt);快照拍在离职之前或没有快照 → unknown。" "同时按目标记快照新鲜度心跳。纯 DB 读,不调厂商。" ), ) async def run_account_assurance_rosters( db: AsyncSession, config: dict[str, Any], ) -> dict[str, Any]: from app.domains.offboarding import assurance_service # noqa: PLC0415 try: max_leavers = int(config.get("max_leavers") or 5000) except (TypeError, ValueError): max_leavers = 5000 summary = await assurance_service.upsert_from_identity_sync_targets( db, max_leavers=max_leavers, ) await db.commit() if summary.get("errors"): raise RuntimeError( "offboarding.account_assurance_rosters: " f"{len(summary['errors'])} target(s) failed: " + " | ".join(str(e) for e in summary["errors"][:5]) ) return summary