"""Dynamic email-group management REST API (HR / IT Admin). All routes gated by :func:`can_manage_dynamic_email_groups` in the service layer; the whole router also carries the runtime leaver gate for mutations (symmetric with the main email-group router). GET /email-groups/dynamic — list dynamic groups GET /email-groups/dynamic/{id}/members/export.xlsx — export members GET /email-groups/dynamic/{id} — detail + field catalog GET /email-groups/dynamic/{id}/reconcile — expected vs actual POST /email-groups/dynamic/preview — preview a rule (no group) POST /email-groups/dynamic/{id}/preview — preview against a group POST /email-groups/dynamic — create PUT /email-groups/dynamic/{id}/rule — edit membership rule PUT /email-groups/dynamic/{id}/display — edit name/description POST /email-groups/dynamic/{id}/aliases — add alias DELETE /email-groups/dynamic/{id}/aliases — remove alias PUT /email-groups/dynamic/{id}/missing-alert — toggle opt-in alert DELETE /email-groups/dynamic/{id} — delete """ from __future__ import annotations from uuid import UUID from fastapi import APIRouter, Depends, Request from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from app.auth.models import AuthUser from app.core.database import get_db from app.core.dependencies import get_current_user from app.core.dependencies import ( require_active_employee_for_mutations as _leaver_runtime_gate, ) from app.core.exporting import xlsx_export_response from app.core.response import success_response from app.domains.email_group.dynamic_service import DynamicEmailGroupService from app.domains.email_group.schemas import ( DynamicGroupAliasRequest, DynamicGroupCreateRequest, DynamicGroupDisplayUpdateRequest, DynamicGroupMissingAlertRequest, DynamicRulePreviewRequest, DynamicRuleUpdateRequest, ) router = APIRouter( prefix="/api/v1/email-groups/dynamic", tags=["email_groups_dynamic"], dependencies=[Depends(_leaver_runtime_gate)], ) class _DeleteBody(BaseModel): reason: str = Field(..., min_length=4, max_length=500) def _roles(user: AuthUser) -> list[str]: return user.role_names or [] @router.get("", summary="List dynamic email groups (HR / IT Admin)") async def list_dynamic_groups( request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = DynamicEmailGroupService(db) data = await service.list_groups(actor_roles=_roles(current_user)) return success_response(data=data, request=request) # NOTE: registered before ``/{group_id}`` so the literal ``members`` # segment is never shadowed by the UUID path param. @router.get( "/{group_id}/members/export.xlsx", summary="Export a dynamic group's live members to Excel (HR / IT Admin)", ) async def export_dynamic_group_members( group_id: UUID, request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = DynamicEmailGroupService(db) data = await service.list_members( group_id=group_id, actor_roles=_roles(current_user), ) # Two-row header: zh label + stable machine key (see exporting.py). columns = [ ("email", "邮箱"), ("display_name", "姓名"), ("employee_no", "工号"), ("job_title", "职位"), ("job_function", "职能"), ("work_location_name", "办公地点"), ("country", "国家/地区"), ("employment_status", "在职状态"), ("role", "组内角色"), ("member_type", "成员类型"), ("google_status", "Google 状态"), ] return await xlsx_export_response( db, request, current_user, columns=columns, rows=data["members"], total_matched=data["total"], filename_stem=f"dynamic-group-members-{data['group_email']}", sheet_title="成员", audit_action="email_group.dynamic_members.export", audit_resource_type="email_group", filters={ "group_id": str(group_id), "group_email": data["group_email"], }, ) @router.get("/{group_id}", summary="Dynamic group detail + field catalog") async def get_dynamic_group( group_id: UUID, request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = DynamicEmailGroupService(db) data = await service.get_detail( group_id=group_id, actor_roles=_roles(current_user), ) return success_response(data=data, request=request) @router.get( "/{group_id}/reconcile", summary="Expected (rule) vs actual (Google) membership", ) async def reconcile_dynamic_group( group_id: UUID, request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = DynamicEmailGroupService(db) data = await service.reconcile( group_id=group_id, actor_roles=_roles(current_user), ) return success_response(data=data, request=request) @router.post("/preview", summary="Preview a rule's membership (new group)") async def preview_rule_new( body: DynamicRulePreviewRequest, request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = DynamicEmailGroupService(db) data = await service.preview_rule( group_id=None, rule_in=body.rule, actor_roles=_roles(current_user), ) return success_response(data=data, request=request) @router.post("/{group_id}/preview", summary="Preview a rule against a group") async def preview_rule_existing( group_id: UUID, body: DynamicRulePreviewRequest, request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = DynamicEmailGroupService(db) data = await service.preview_rule( group_id=group_id, rule_in=body.rule, actor_roles=_roles(current_user), ) return success_response(data=data, request=request) @router.post("", summary="Create a dynamic email group") async def create_dynamic_group( body: DynamicGroupCreateRequest, request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = DynamicEmailGroupService(db) data = await service.create_group( email=body.email, name=body.name, description=body.description, rule_in=body.rule, actor=current_user, actor_roles=_roles(current_user), trace_id=request.state.trace_id, ) return success_response(data=data, request=request) @router.put("/{group_id}/rule", summary="Edit a dynamic group's membership rule") async def update_dynamic_group_rule( group_id: UUID, body: DynamicRuleUpdateRequest, request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = DynamicEmailGroupService(db) data = await service.update_rule( group_id=group_id, rule_in=body.rule, confirm_large_drop=body.confirm_large_drop, actor=current_user, actor_roles=_roles(current_user), trace_id=request.state.trace_id, ) return success_response(data=data, request=request) @router.put("/{group_id}/display", summary="Edit a dynamic group's name/description") async def update_dynamic_group_display( group_id: UUID, body: DynamicGroupDisplayUpdateRequest, request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = DynamicEmailGroupService(db) data = await service.update_display( group_id=group_id, name=body.name, description=body.description, actor=current_user, actor_roles=_roles(current_user), trace_id=request.state.trace_id, ) return success_response(data=data, request=request) @router.post("/{group_id}/aliases", summary="Add an alias to a dynamic group") async def add_dynamic_group_alias( group_id: UUID, body: DynamicGroupAliasRequest, request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = DynamicEmailGroupService(db) data = await service.add_alias( group_id=group_id, alias_email=body.alias_email, actor=current_user, actor_roles=_roles(current_user), trace_id=request.state.trace_id, ) return success_response(data=data, request=request) @router.delete("/{group_id}/aliases", summary="Remove an alias from a dynamic group") async def remove_dynamic_group_alias( group_id: UUID, body: DynamicGroupAliasRequest, request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = DynamicEmailGroupService(db) data = await service.remove_alias( group_id=group_id, alias_email=body.alias_email, actor=current_user, actor_roles=_roles(current_user), trace_id=request.state.trace_id, ) return success_response(data=data, request=request) @router.put("/{group_id}/missing-alert", summary="Toggle missing-value drift alert") async def set_missing_alert( group_id: UUID, body: DynamicGroupMissingAlertRequest, request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = DynamicEmailGroupService(db) data = await service.set_missing_alert( group_id=group_id, enabled=body.enabled, actor=current_user, actor_roles=_roles(current_user), trace_id=request.state.trace_id, ) return success_response(data=data, request=request) @router.delete("/{group_id}", summary="Delete a dynamic email group") async def delete_dynamic_group( group_id: UUID, body: _DeleteBody, request: Request, current_user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = DynamicEmailGroupService(db) data = await service.delete_group( group_id=group_id, reason=body.reason, actor=current_user, actor_roles=_roles(current_user), trace_id=request.state.trace_id, ) return success_response(data=data, request=request)