HR-ATS-Portal/backend/job/app.py

669 lines
23 KiB
Python

from fastapi import APIRouter,Depends,Query
from fastapi.responses import JSONResponse
from fastapi import HTTPException
from db_setup import get_session
from job.candidate.views import 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.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
import logging
from job.job_post.plugins import PlatformAlias
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
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
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
currency: str | None = None
description: str | None = None
incurred_at: datetime | None = None
@router.get("/jobs/alias")
async def get_job_alias():
try:
alias_lst=[k.name for k in PlatformAlias]
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),
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,
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.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,
)
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)
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":
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))
@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))
@router.get("/job/fetch")
async def fetch_job_posts(
search: str | None = Query(None),
top: int | None = Query(None),
skip: int = Query(0, ge=0),
ids: str | None = Query(None),
active_only: bool = Query(True),
current_user: dict = Depends(require_permission(PermissionTag.JOB_BOARD_VIEW)),
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("/candidate/fetch")
async def fetch_candidate(
user_id:str=Query(None),
limit:int=Query(10),
offset:int=Query(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))
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("/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))
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))
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))
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))
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.inbox_id,payload.to_stage,current_user,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/transitions/fetch")
async def fetch_pipeline_transitions(
transition_id:str=Query(None),
inbox_id:int=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)
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.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))