"""Outbound notification for the one thing an employee can do here. The registry is IT-maintained: an employee cannot edit their own ownership row, and they should not be able to — ownership is an accountability record, not a preference. What they CAN do is say "this is not mine", which is a claim IT has to adjudicate. Recipient discipline matches the bounty domain: active ``global_admin`` holders only, because they are the people who can actually change the row. No manager, no walk-up, no cron — one human action produces one push. """ from __future__ import annotations import logging from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.auth.models import AuthRole, AuthUserRole from app.core.rbac import GLOBAL_ADMIN_ROLE from app.domains.system_registry.models import RegisteredSystem from app.identity.person_models import IdentityPerson from app.notifications.service import ( CardAction, NotificationEnvelope, NotificationService, ) logger = logging.getLogger(__name__) #: Where the push lands. The registry admin page, filtered to the #: disputed row — the reach has to land somewhere the recipient can #: actually act, not on a list they then have to search. ADMIN_PAGE = "/admin/systems?q={slug}" async def _registry_maintainers(db: AsyncSession) -> list[IdentityPerson]: """Active global_admin holders — the only role that can re-point an owner (``can_manage_system_registry``). Notifying anyone else would be handing the item to somebody who cannot resolve it.""" rows = ( await db.execute( select(IdentityPerson) .join(AuthUserRole, AuthUserRole.user_id == IdentityPerson.id) .join(AuthRole, AuthRole.id == AuthUserRole.role_id) .where( AuthRole.name == GLOBAL_ADMIN_ROLE, IdentityPerson.employment_status == "active", IdentityPerson.deleted_at.is_(None), ), ) ).scalars().all() seen: set = set() unique: list[IdentityPerson] = [] for person in rows: if person.id in seen: continue seen.add(person.id) unique.append(person) return unique async def notify_ownership_disputed( db: AsyncSession, system: RegisteredSystem, *, disputer: IdentityPerson, role: str, note: str | None, ) -> int: """Tell IT that a named owner says the system is not theirs. Best-effort: a Feishu hiccup must not roll back the event that was just written to the system's trail. Returns how many operators were reached so the caller can report it honestly rather than claiming 「已通知」. """ url = ADMIN_PAGE.format(slug=system.slug) who = disputer.display_name or disputer.work_email role_label = "负责人" if role == "owner" else "备份负责人" context = { "system_name": system.name, "system_slug": system.slug, "disputer_name": who, "disputer_email": disputer.work_email, "role_label": role_label, "note_block": (note or "").strip() or "(未填写说明)", } service = NotificationService(db) sent = 0 for operator in await _registry_maintainers(db): env = NotificationEnvelope( recipient=operator, title=f"{who} 表示不该由自己负责「{system.name}」", body_text="", channels=["feishu"], template_key="system_registry.ownership_disputed", context=context, url=url, related_resource_type="registry_system", related_resource_id=system.id, card_actions=[ CardAction(label="打开系统台账", url=url, style="primary"), ], ) try: await service.push_inbox_and_feishu(env) sent += 1 except Exception: logger.warning( "system_registry: dispute notification failed for %s → %s", system.slug, operator.work_email, exc_info=True, ) return sent