from fastapi import APIRouter,Depends,Query,Response from fastapi.responses import FileResponse,JSONResponse from fastapi import HTTPException from db_setup import get_session from job.candidate.views import CandidateScoring,FileRead,CandidateView from job.interviews.views import Interview from job.notes.views import Note from job.activity.views import ActivityLog from job.feedback.views import FeedbackView from job.pipeline.views import Pipeline from job.history.views import HistoryRecorder from job.assignment.views import Assignment from job.cost.views import HiringCost from sqlalchemy.ext.asyncio import AsyncSession from users.permissions import PermissionTag, require_permission from job.job_post.views import JobPost,JobPostCreate from job_assist.execute_agent import run_field_assist from job.job_post.export import build_jobs_workbook import logging from users.views import User from job.job_post.models import SocialPlatform from fastapi import UploadFile, File, Form from dotenv import load_dotenv from datetime import datetime, time, timezone from pydantic import BaseModel from uuid import UUID from typing import Literal, Optional import uuid load_dotenv() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) router = APIRouter() class CandidateUpdate(BaseModel): favorite: bool | None = None rating: float | None = None class InterviewCreate(BaseModel): inbox_id: int interview_date: datetime | None = None interview_time: datetime | None = None interview_type: str | None = None interview_status: str | None = None class InterviewUpdate(BaseModel): interview_date: datetime | None = None interview_time: datetime | None = None interview_type: str | None = None interview_status: str | None = None inbox_id: int | None = None class NoteCreate(BaseModel): user_id: UUID note: str class NoteUpdate(BaseModel): note: str | None = None class ActivityCreate(BaseModel): message_id: UUID | None = None user_id: UUID | None = None inbox_id: int | None = None activity_type: str | None = None activity_status: str | None = None description: str | None = None activity_date: datetime | None = None activity_time: datetime | None = None class FeedbackCreate(BaseModel): inbox_id: int | None = None review: str | None = None financial_status: str | None = None score: float | None = None note: str | None = None reviewed_by: UUID | None = None class FeedbackUpdate(BaseModel): review: str | None = None financial_status: str | None = None score: float | None = None note: str | None = None inbox_id: int | None = None reviewed_by: UUID | None = None class StageChange(BaseModel): inbox_id: int | None = None manual_upload_id: UUID | None = None to_stage: str change_reason: str | None = None class JobAssignmentCreate(BaseModel): job_post_id: UUID user_id: UUID assignment_role: str | None = None class ApplicationAssignmentCreate(BaseModel): inbox_id: int user_id: UUID assignment_role: str | None = None class HiringCostCreate(BaseModel): cost_type: str amount: float job_post_id: UUID | None = None source_channel_id: int | None = None currency: str | None = None description: str | None = None incurred_at: datetime | None = None class JobUpdate(BaseModel): title: str | None = None department: str | None = None location: str | None = None employment_type: str | None = None vacancies: int | None = None salary: str | None = None salary_min: float | None = None salary_max: float | None = None experience_min: int | None = None experience_max: int | None = None description: str | None = None class JobStatusUpdate(BaseModel): requisition_status: str class FeedbackTemplateCreate(BaseModel): name: str department: str | None = None criteria: list[str] | None = None is_active: bool | None = None class FeedbackTemplateUpdate(BaseModel): name: str | None = None department: str | None = None criteria: list[str] | None = None is_active: bool | None = None @router.get("/jobs/alias") async def get_job_alias(session: AsyncSession = Depends(get_session)): try: alias_lst=await SocialPlatform.list_aliases(session) return JSONResponse(content={"data":alias_lst,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/candidate/create/candidate") async def create_manual_candidate( file: UploadFile = File(...), candidate_email: str | None = Form(None), candidate_name: str | None = Form(None), candidate_phone: str | None = Form(None), job_post_id: str | None = Form(None), current_company: str | None = Form(None), current_position: str | None = Form(None), platform: str | None = Form(None), experience: str | None = Form(None), status: str | None = Form(None), referral_by: str | None = Form(None), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)), session: AsyncSession = Depends(get_session), ): saved_path=None try: file_content = await file.read() logger.info(f"Received file: {file.filename} ({len(file_content)} bytes)") reader=FileRead(session=session,filename=file.filename,file=file_content) # Parse first: an unreadable PDF is a 400, and doing it before the write # keeps a file that can never back a row off the disk entirely. parsed=await reader.injest_manual_upload() saved=await reader.save_manual_upload() saved_path=saved.get("file_path") service=CandidateView(session=session) data=await service.create_candidate( candidate_email=candidate_email, candidate_name=candidate_name, candidate_phone=candidate_phone, job_post_id=job_post_id, current_company=current_company, current_position=current_position, platform=platform, experience=experience, status=status, referral_by=referral_by, file_name=saved.get("file_name"), file_path=saved_path, full_text=parsed.get("text") or "", current_user=current_user.get("id"), ) return JSONResponse(content={"data":data,"status_code":200}) except HTTPException: # create_candidate rejects a blank email with a 422 AFTER the file has # landed, so without this every such attempt would leave an orphan PDF. FileRead.discard_upload(saved_path) raise except Exception as e: FileRead.discard_upload(saved_path) raise HTTPException(status_code=500,detail=str(e)) @router.get("/candidate/fetch/users") async def fetch_users( role_id:int=Query(8), top:int=Query(10), skip:int=Query(0), search:str=Query(None), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=User(session=session) data=await service.get_users(role_id=role_id,top=top,skip=skip) return JSONResponse(content={"data":data,"status_code":200}) except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/candidate/cv_upload") async def cv_upload( file: UploadFile = File(...), candidate_email: str | None = Form(None), candidate_name: str | None = Form(None), candidate_phone: str | None = Form(None), job_post_id: str | None = Form(None), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)), session: AsyncSession = Depends(get_session), ): try: file_content = await file.read() logger.info(f"Received file: {file.filename} ({len(file_content)} bytes)") service=FileRead(session=session,filename=file.filename,file=file_content) data=await service.ingest_upload( candidate_email=candidate_email,candidate_name=candidate_name, current_user=current_user, ) return JSONResponse(content={"data":data,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/candidate/inbox-match") async def candidate_inbox_match( inbox_message_id: str = Query(...), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=FileRead(session=session) data=await service.match_inbox_cv(inbox_message_id,current_user=current_user) return JSONResponse(content={"data":data,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/job/post-job") async def post_job( payload: JobPostCreate, current_user: dict = Depends(require_permission(PermissionTag.JOB_BOARD_CREATE)), session: AsyncSession = Depends(get_session), ): try: service=JobPost(session=session) data=payload.model_dump() if data['mode']=="customScheduled" and data.get('scheduler_date'): data['due_at']=datetime.combine( data['scheduler_date'], data['scheduler_time'] or time(0, 0, 0), tzinfo=timezone.utc, ).strftime("%Y-%m-%dT%H:%M:%S.000Z") result=await service.post_job(data,current_user) return JSONResponse(content={"data":result,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) class JobAssistRequest(BaseModel): field: Literal[ "title", "department", "location", "salary", "requirements", "optional_skills", "description", ] action: Literal["fix", "suggest"] text: str = "" context: dict = {} @router.post("/job/assist-field") async def assist_job_field( payload: JobAssistRequest, current_user: dict = Depends(require_permission( PermissionTag.JOB_BOARD_CREATE, PermissionTag.JOBS_EDIT, require_all=False, )), ): """AI assist for one job-form field: fix the recruiter's text or suggest content. No DB session — the form state travels in the payload. RuntimeError from the agent is a provider problem and must surface as a generic 503; its cause can carry prompt content that never belongs in a response body. """ try: suggestion = await run_field_assist( field=payload.field, action=payload.action, text=payload.text, context=payload.context, ) return JSONResponse(content={"data": {"suggestion": suggestion}, "status_code": 200}) except HTTPException: raise except ValueError as e: raise HTTPException(status_code=422, detail=str(e)) except Exception: raise HTTPException(status_code=503, detail="AI assist is unavailable right now. Try again shortly.") @router.get("/job/buffer/channels") async def buffer_channels( current_user: dict = Depends(require_permission(PermissionTag.JOB_BOARD_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=JobPost(session=session) data=await service.list_channels() return JSONResponse(content={"data":data,"total":len(data),"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) class InboxScoreRequest(BaseModel): job_id: str message_ids: list[str] # inbox_messages PK uuids, not Graph message ids @router.post("/candidate/score") async def score_candidates( job_id: str = Form(...), files: list[UploadFile] = File(...), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)), session: AsyncSession = Depends(get_session), ): """Score uploaded CV PDFs against a job post; persists and returns the leaderboard.""" try: pairs=[(f.filename,await f.read()) for f in files] service=CandidateScoring(session=session) data=await service.score_uploads(job_id,pairs,current_user) return JSONResponse(content={"data":data,"total":len(data),"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/candidate/score_inbox") async def score_inbox_candidates( payload: InboxScoreRequest, current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)), session: AsyncSession = Depends(get_session), ): """Score the decoded attachments of inbox messages against a job post.""" try: service=CandidateScoring(session=session) data=await service.score_inbox(payload.job_id,payload.message_ids,current_user) return JSONResponse(content={"data":data,"total":len(data),"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/candidate/scored/fetch") async def fetch_scored_candidates( job_id: str = Query(None), limit: int = Query(10, ge=1, le=100), offset: int = Query(0, ge=0), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), session: AsyncSession = Depends(get_session), ): """Persisted scored candidates, newest first. Without job_id returns the whole pool across jobs. `total` is the full result-set size, not the page length.""" try: service=CandidateScoring(session=session) data,total=await service.fetch_candidates(job_id,limit=limit,offset=offset) return JSONResponse(content={"data":data,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/job/fetch") async def fetch_job_posts( search: str | None = Query(None), top: int | None = Query(10, ge=1, le=100), skip: int = Query(0, ge=0), ids: str | None = Query(None), active_only: bool = Query(True), # Job-board, candidate, or talent viewers may list jobs — recruiters scoring # CVs need a job to score against (CV Import picker), and talent sourcing # needs the same picker to choose which job to source for. current_user: dict = Depends( require_permission( PermissionTag.JOB_BOARD_VIEW, PermissionTag.CANDIDATES_VIEW, PermissionTag.TALENT_VIEW, require_all=False, ) ), session: AsyncSession = Depends(get_session), ): try: service=JobPost(session=session) id_list=[x.strip() for x in (ids or "").split(",") if x.strip()] or None data,total=await service.fetch_job_posts( search=search, top=top, skip=skip, ids=id_list, active_only=active_only, ) return JSONResponse(content={"data":data,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/jobs/fetch") async def fetch_jobs( search: str | None = Query(None), department: str | None = Query(None), requisition_status: str | None = Query(None), employment_type: str | None = Query(None), # le=500 (not 100): the Jobs board loads a full client-side page for facets; # a 200 ceiling used to 422 the SPA and render an empty requisition list. top: int | None = Query(10, ge=1, le=500), skip: int = Query(0, ge=0), # Defaults False, unlike /job/fetch: a requisition list must show CLOSED # requisitions, and those carry is_active = false. Soft-deleted rows are still # excluded by the include_deleted branch in fetch_job_posts. active_only: bool = Query(False), current_user: dict = Depends(require_permission(PermissionTag.JOBS_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=JobPost(session=session) data,total=await service.fetch_jobs( search=search,department=department,requisition_status=requisition_status, employment_type=employment_type,top=top,skip=skip,active_only=active_only, ) return JSONResponse(content={"data":data,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/jobs/export") async def export_jobs( search: str | None = Query(None), department: str | None = Query(None), requisition_status: str | None = Query(None), employment_type: str | None = Query(None), active_only: bool = Query(False), current_user: dict = Depends(require_permission(PermissionTag.JOBS_EXPORT)), session: AsyncSession = Depends(get_session), ): """Styled .xlsx of the requisition list — same filters as /jobs/fetch, no paging.""" try: service=JobPost(session=session) data,_=await service.fetch_jobs( search=search,department=department,requisition_status=requisition_status, employment_type=employment_type,top=None,skip=0,active_only=active_only, ) filename=f"jobs-export-{datetime.now(timezone.utc).strftime('%Y-%m-%d')}.xlsx" return Response( content=build_jobs_workbook(data), media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={"Content-Disposition":f'attachment; filename="{filename}"'}, ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/candidate/fetch_by_id") async def fetch_candidate_by_id( candidate_id: str = Query(...), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=CandidateScoring(session=session) data=await service.fetch_candidate_by_id(candidate_id) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/candidate/fetch") async def fetch_candidate( user_id:str=Query(None), limit:int=Query(10,ge=1,le=100), offset:int=Query(0,ge=0), search:str=Query(None), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=CandidateView(session=session) data=await service.get_candidate(user_id=user_id,limit=limit,offset=offset,search=search) total=await service.count_candidates(user_id=user_id,search=search) if isinstance(data,list) else 1 return JSONResponse(content={"data":data,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/candidate/update") async def update_candidate( user_id:str=Query(...), payload:CandidateUpdate=..., current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=CandidateView(session=session) data=await service.update_candidate(user_id,payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/candidate/history/fetch") async def fetch_candidate_history( user_id:str=Query(...), limit:int=Query(200,ge=1,le=500), offset:int=Query(0,ge=0), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=HistoryRecorder(session=session) data,total=await service.list_for_user(user_id,limit=limit,offset=offset) return JSONResponse(content={"data":data,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/interview/fetch") async def fetch_interview( interview_id:str=Query(None), inbox_id:int=Query(None), from_date:datetime=Query(None), to_date:datetime=Query(None), status:str=Query(None), top:int=Query(None), skip:int=Query(0,ge=0), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=Interview(session=session) if not interview_id and inbox_id is None and (from_date is not None or to_date is not None or status is not None or top is not None): data,total=await service.get_interviews_range( from_date=from_date,to_date=to_date,status=status,top=top,skip=skip, ) return JSONResponse(content={"data":data,"total":total,"status_code":200}) data=await service.get_interview( interview_id=interview_id,inbox_id=inbox_id, from_date=from_date,to_date=to_date,status=status,top=top,skip=skip, ) if isinstance(data,tuple): data,total=data else: total=1 if isinstance(data,dict) else len(data) return JSONResponse(content={"data":data,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/interview/create") async def create_interview( payload:InterviewCreate, current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)), session: AsyncSession = Depends(get_session), ): try: service=Interview(session=session) data=await service.create_interview(payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/interview/update") async def update_interview( interview_id:str=Query(...), payload:InterviewUpdate=..., current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=Interview(session=session) data=await service.update_interview(interview_id,payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/notes/fetch") async def fetch_notes( note_id:str=Query(None), user_id:str=Query(None), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=Note(session=session) data=await service.get_note(note_id=note_id,user_id=user_id) total=1 if isinstance(data,dict) else len(data) return JSONResponse(content={"data":data,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/notes/create") async def create_note( payload:NoteCreate, current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)), session: AsyncSession = Depends(get_session), ): try: service=Note(session=session) data=await service.create_note(payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/notes/update") async def update_note( note_id:str=Query(...), payload:NoteUpdate=..., current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=Note(session=session) data=await service.update_note(note_id,payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/activity/fetch") async def fetch_activity( activity_id:str=Query(None), inbox_id:int=Query(None), top:int=Query(None), skip:int=Query(0,ge=0), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=ActivityLog(session=session) if not activity_id and inbox_id is None and top is not None: data,total=await service.get_activity_feed(top=top,skip=skip) return JSONResponse(content={"data":data,"total":total,"status_code":200}) data=await service.get_activity(activity_id=activity_id,inbox_id=inbox_id,top=top,skip=skip) if isinstance(data,tuple): data,total=data else: total=1 if isinstance(data,dict) else len(data) return JSONResponse(content={"data":data,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/activity/create") async def create_activity( payload:ActivityCreate, current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)), session: AsyncSession = Depends(get_session), ): try: service=ActivityLog(session=session) data=await service.create_activity(payload.model_dump(exclude_unset=True)) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/feedback/fetch") async def fetch_feedback( feedback_id:str=Query(None), inbox_id:int=Query(None), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=FeedbackView(session=session) data=await service.get_feedback(feedback_id=feedback_id,inbox_id=inbox_id) total=1 if isinstance(data,dict) else len(data) return JSONResponse(content={"data":data,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/feedback/create") async def create_feedback( payload:FeedbackCreate, current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)), session: AsyncSession = Depends(get_session), ): try: service=FeedbackView(session=session) data=await service.create_feedback(payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/feedback/update") async def update_feedback( feedback_id:str=Query(...), payload:FeedbackUpdate=..., current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=FeedbackView(session=session) data=await service.update_feedback(feedback_id,payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/candidate/stage") async def change_candidate_stage( payload:StageChange, current_user: dict = Depends(require_permission(PermissionTag.PIPELINE_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=Pipeline(session=session) data=await service.change_stage( payload.to_stage,current_user,inbox_id=payload.inbox_id, manual_upload_id=payload.manual_upload_id,change_reason=payload.change_reason, ) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/pipeline/candidates/fetch") async def fetch_pipeline_candidates( job_post_id:Optional[uuid.UUID]=Query(None), limit:int=Query(10,ge=1,le=1000), offset:int=Query(0,ge=0), current_user: dict = Depends(require_permission(PermissionTag.PIPELINE_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=Pipeline(session=session) result=await service.get_all(job_post_id=job_post_id,limit=limit,offset=offset) return JSONResponse(content={**result,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/pipeline/candidate/score/fetch") async def fetch_pipeline_candidate_score( user_id:uuid.UUID=Query(...), job_post_id:uuid.UUID=Query(None), current_user: dict = Depends(require_permission(PermissionTag.PIPELINE_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=Pipeline(session=session) data=await service.get_pipeline_candidates(user_id=user_id,job_post_id=job_post_id) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/pipeline/transitions/fetch") async def fetch_pipeline_transitions( transition_id:str=Query(None), inbox_id:int=Query(None), manual_upload_id:UUID=Query(None), current_user: dict = Depends(require_permission(PermissionTag.PIPELINE_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=Pipeline(session=session) data=await service.get_transitions(inbox_id=inbox_id,transition_id=transition_id,manual_upload_id=manual_upload_id) total=1 if isinstance(data,dict) else len(data) return JSONResponse(content={"data":data,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/job/assignments/fetch") async def fetch_job_assignments( job_post_id:str=Query(...), current_user: dict = Depends(require_permission(PermissionTag.JOBS_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=Assignment(session=session) data=await service.list_job_assignments(job_post_id) return JSONResponse(content={"data":data,"total":len(data),"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/job/assignments/create") async def create_job_assignment( payload:JobAssignmentCreate, current_user: dict = Depends(require_permission(PermissionTag.JOBS_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=Assignment(session=session) data=await service.create_job_assignment(payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/candidate/assignments/fetch") async def fetch_application_assignments( inbox_id:int=Query(...), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=Assignment(session=session) data=await service.list_application_assignments(inbox_id) return JSONResponse(content={"data":data,"total":len(data),"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/candidate/assignments/create") async def create_application_assignment( payload:ApplicationAssignmentCreate, current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=Assignment(session=session) data=await service.create_application_assignment(payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/job/costs/fetch") async def fetch_hiring_costs( job_post_id:str=Query(None), from_date:datetime=Query(None), to_date:datetime=Query(None), top:int=Query(None), skip:int=Query(0,ge=0), current_user: dict = Depends(require_permission(PermissionTag.JOBS_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=HiringCost(session=session) data,total=await service.list_costs( job_post_id=job_post_id,from_date=from_date,to_date=to_date,top=top,skip=skip, ) return JSONResponse(content={"data":data,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/job/costs/source-channels/fetch") async def fetch_cost_source_channels( current_user: dict = Depends(require_permission(PermissionTag.JOBS_VIEW)), session: AsyncSession = Depends(get_session), ): """Active source channels for tagging spend (REQ-ANL-09 attribution).""" try: from inbox.models import SourceChannels rows=await SourceChannels.list_active(session) data=[{"id":r.id,"key":r.key,"label":r.label} for r in rows] return JSONResponse(content={"data":data,"total":len(data),"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/job/costs/create") async def create_hiring_cost( payload:HiringCostCreate, current_user: dict = Depends(require_permission(PermissionTag.JOBS_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=HiringCost(session=session) data=await service.create_cost(payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/jobs/update") async def update_job( payload:JobUpdate, job_post_id:str=Query(...), current_user: dict = Depends(require_permission(PermissionTag.JOBS_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=JobPost(session=session) data=await service.update_job(job_post_id,payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.delete("/jobs/delete") async def delete_job( job_post_id:str=Query(...), current_user: dict = Depends(require_permission(PermissionTag.JOBS_DELETE)), session: AsyncSession = Depends(get_session), ): try: service=JobPost(session=session) data=await service.delete_job(job_post_id,current_user) return JSONResponse(content={"data":data,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/jobs/status") async def set_job_status( payload:JobStatusUpdate, job_post_id:str=Query(...), current_user: dict = Depends(require_permission(PermissionTag.JOBS_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=JobPost(session=session) data=await service.set_job_status(job_post_id,payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/feedback/templates/fetch") async def fetch_feedback_templates( current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=FeedbackView(session=session) data,total=await service.get_templates() return JSONResponse(content={"data":data,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/feedback/templates/create") async def create_feedback_template( payload:FeedbackTemplateCreate, current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)), session: AsyncSession = Depends(get_session), ): try: service=FeedbackView(session=session) data=await service.create_template(payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/feedback/templates/update") async def update_feedback_template( payload:FeedbackTemplateUpdate, template_id:str=Query(...), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=FeedbackView(session=session) data=await service.update_template(template_id,payload.model_dump(exclude_unset=True)) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.delete("/feedback/templates/delete") async def delete_feedback_template( template_id:str=Query(...), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_DELETE)), session: AsyncSession = Depends(get_session), ): try: service=FeedbackView(session=session) data=await service.delete_template(template_id) return JSONResponse(content={"data":data,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/documents/download") async def download_document( inbox_id:int|None=Query(None), manual_upload_candidate_id:str|None=Query(None), index:int=Query(0,ge=0), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=CandidateView(session=session) path,filename=await service.download_document(inbox_id,manual_upload_candidate_id,index) return FileResponse( path=str(path), filename=filename, media_type="application/octet-stream", content_disposition_type="attachment", ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e))