from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession import logging from job.assignment.models import ApplicationAssignments, JobAssignments from job.assignment.serializers import serialize_application_assignment, serialize_job_assignment from job.job_post.models import JobPosts from role.models import EnumRoles, Roles from users.models import Users # job_assignments.assignment_role → the users.role that may hold it. # primary_recruiter is swappable; hiring_manager is the requisition owner. JOB_ASSIGNMENT_ROLES = { "primary_recruiter": EnumRoles.RECRUITER, "hiring_manager": EnumRoles.HIRING_MANAGER, } JOB_OWNER_COLUMN = { "primary_recruiter": "current_recruiter_ids", "hiring_manager": "hiring_manager_id", } logger = logging.getLogger(__name__) class Assignment: def __init__(self,session:AsyncSession): self.session=session async def require_role(self,user_id,role_enum,field_name): role=await Roles.get_role_by_name(self.session,role_enum.value) user=await Users.get_user_by_id(self.session,user_id) if not role or not user or user.role_id!=role.id: raise HTTPException( status_code=422, detail=f"{field_name} must be a {role_enum.value}", ) if not user.is_active or user.is_deleted: raise HTTPException(status_code=422,detail=f"{field_name} is not an active user") return user def _job_role(self,raw): key=(raw or "primary_recruiter").strip() if key=="recruiter": key="primary_recruiter" if key not in JOB_ASSIGNMENT_ROLES: allowed=", ".join(sorted(JOB_ASSIGNMENT_ROLES)) raise HTTPException( status_code=422, detail=f"assignment_role must be one of {allowed}", ) return key async def record_job_owner(self,job_post_id,user_id,assignment_role,assigned_by): """Close the open interval of this role, then open a new one. user_id None = unassign. No-ops when the same person already holds the open interval. Does not touch job_posts columns. """ role=self._job_role(assignment_role) job_uid=JobAssignments._as_uuid(job_post_id) by_uid=JobAssignments._as_uuid(assigned_by) if not job_uid or not by_uid: raise HTTPException(status_code=422,detail="Invalid job_post_id or assigned_by") current=await JobAssignments.fetch_by_job( self.session,job_uid,current_only=True,assignment_role=role, ) if user_id is None: await JobAssignments.close_current(self.session,job_uid,role) return None user_uid=JobAssignments._as_uuid(user_id) if not user_uid: raise HTTPException(status_code=422,detail="Invalid user_id") if current and str(current[0].user_id)==str(user_uid): return current[0] await JobAssignments.close_current(self.session,job_uid,role) return await JobAssignments.insert_assignment(self.session,{ "job_post_id":job_uid, "user_id":user_uid, "assignment_role":role, "assigned_by":by_uid, }) async def record_job_recruiters(self,job_post_id,user_ids,assigned_by): """Keep open primary_recruiter intervals in sync with the JSON list.""" job_uid=JobAssignments._as_uuid(job_post_id) by_uid=JobAssignments._as_uuid(assigned_by) if not job_uid or not by_uid: raise HTTPException(status_code=422,detail="Invalid job_post_id or assigned_by") return await JobAssignments.sync_open( self.session,job_uid,"primary_recruiter",user_ids,by_uid, ) async def list_job_assignments(self,job_post_id,current_only=True,assignment_role=None): if not job_post_id: raise HTTPException(status_code=400,detail="job_post_id is required") role=self._job_role(assignment_role) if assignment_role else None rows=await JobAssignments.fetch_by_job( self.session,job_post_id,current_only=current_only,assignment_role=role, ) names=await Users.names_by_ids( self.session,[r.user_id for r in rows]+[r.assigned_by for r in rows], ) return [serialize_job_assignment(r,names=names) for r in rows] async def create_job_assignment(self,payload,current_user): user_id=payload.get("user_id") job_post_id=payload.get("job_post_id") if not user_id or not job_post_id: raise HTTPException(status_code=422,detail="user_id and job_post_id are required") job=await JobPosts.get_job_post_by_id(self.session,job_post_id) if not job or job.is_deleted: raise HTTPException(status_code=404,detail="Job post not found") role=self._job_role(payload.get("assignment_role")) await self.require_role(user_id,JOB_ASSIGNMENT_ROLES[role],"user_id") assigned_by=current_user.get("id") if isinstance(current_user,dict) else None row=await self.record_job_owner(job_post_id,user_id,role,assigned_by) column=JOB_OWNER_COLUMN[role] patch={column:user_id} if role=="primary_recruiter": patch["current_recruiter_ids"]=[str(user_id)] updated=await JobPosts.update_job_post(self.session,job_post_id,patch) if updated: try: from notifications.views import notify_job_assignment label="hiring manager" if role=="hiring_manager" else "recruiter" previous=( [job.hiring_manager_id] if role=="hiring_manager" else JobPosts.recruiter_ids_of(job) ) await notify_job_assignment( self.session,updated, role_label=label, actor_id=assigned_by, previous_ids=previous, ) except Exception as exc: logger.warning("notification insert skipped: %s", exc) names=await Users.names_by_ids( self.session,[row.user_id,row.assigned_by] if row else [], ) return serialize_job_assignment(row,names=names) if row else None async def list_application_assignments(self,inbox_id): if inbox_id is None: raise HTTPException(status_code=400,detail="inbox_id is required") rows=await ApplicationAssignments.fetch_by_inbox(self.session,int(inbox_id)) names=await Users.names_by_ids( self.session,[r.user_id for r in rows]+[r.assigned_by for r in rows], ) return [serialize_application_assignment(r,names=names) for r in rows] async def create_application_assignment(self,payload,current_user): user_id=payload.get("user_id") inbox_id=payload.get("inbox_id") if not user_id or inbox_id is None: raise HTTPException(status_code=422,detail="user_id and inbox_id are required") await self.require_role(user_id,EnumRoles.RECRUITER,"user_id") fields={ "inbox_id":int(inbox_id), "user_id":ApplicationAssignments._as_uuid(user_id), "assignment_role":payload.get("assignment_role") or "primary_recruiter", "assigned_by":ApplicationAssignments._as_uuid( current_user.get("id") if isinstance(current_user,dict) else None ), } if not fields["user_id"] or not fields["assigned_by"]: raise HTTPException(status_code=422,detail="Invalid user_id or assigned_by") row=await ApplicationAssignments.insert_assignment(self.session,fields) names=await Users.names_by_ids(self.session,[row.user_id,row.assigned_by]) return serialize_application_assignment(row,names=names)