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

398 lines
14 KiB
Python

from fastapi import APIRouter,Depends, Query
from fastapi.responses import JSONResponse
from fastapi import HTTPException
from pydantic import BaseModel
from db_setup import get_session
from inbox.enums import Candidate_application_Status
from sqlalchemy.ext.asyncio import AsyncSession
from inbox.views import Email
from users.permissions import PermissionTag, require_permission
from dotenv import load_dotenv
load_dotenv()
router = APIRouter()
class AssignJobPostBody(BaseModel):
job_post_id: str | None = None
class ProcessingStateBody(BaseModel):
processing_state: str
class DuplicateBody(BaseModel):
is_duplicate: bool
class ReadBody(BaseModel):
read: bool = True
class BulkReadBody(BaseModel):
record_ids: list[str]
read: bool = True
class ReadAllBody(BaseModel):
"""The caller's CURRENT list filter, echoed back so the update narrows the same way.
Every field defaults to the same "no filter" value the list endpoint uses, so an
empty body means "the All Applications tab" — exactly what GET
/inbox/all-applications returns with no query params.
"""
read: bool = True
search: str | None = None
isread: bool = True
application_status: Candidate_application_Status = Candidate_application_Status.CLOSED
assigned: bool | None = None
class TriageOverrideBody(BaseModel):
is_application: bool
class EmailSendBody(BaseModel):
to: str
subject: str
body: str
content_type: str | None = "html"
inbox_id: int | None = None
class EmailReplyBody(BaseModel):
record_id: str
body: str
@router.get("/email/fetch")
async def fetch_email(
top:int=Query(100),
skip:int=Query(0,ge=0),
test_on: bool = Query(True),
token: str | None = Query(None),
session: AsyncSession = Depends(get_session),
):
try:
service=Email(session=session,token=token)
if not service.token:
raise HTTPException(status_code=401,detail="Unauthorized")
data=await service.service_email(top,skip)
value=data.get("value")
items_lst=[]
# Classify the whole page first, bounded-parallel, then replay it in upstream
# order: the inserts stay serial on the one request session and pending_match_ids
# keeps the sequence it has today.
decisions=await service.triage_round([item.get("id") for item in value])
for item in value:
message_id=item.get("id")
service_per_email=await service.get_email_by_id(message_id,test_on,decision=decisions.get(str(message_id)))
items_lst.append({"message_id":message_id,"email_contents":service_per_email})
if service.pending_match_ids:
await service.enqueue_matching(list(service.pending_match_ids),force=False)
skipped=len(service.skipped_message_ids)
triage={"ingested":len(items_lst)-skipped,"skipped":skipped,"errors":len(service.triage_errors)}
account_setup=[]
if test_on:
return JSONResponse(content={"data":items_lst,"triage":triage,"status_code":200})
if service.pending_confirmation_emails:
account_setup=await service.send_account_setup(list(service.pending_confirmation_emails))
return JSONResponse(content={"data":items_lst,"account_setup":account_setup,"triage":triage,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/inbox/fetch")
async def fetch_inbox(
record_id: str | None = Query(None),
search: str | None = Query(None),
top: int | None = Query(None),
skip: int = Query(0, ge=0),
session: AsyncSession = Depends(get_session),
):
try:
service=Email(session=session)
if record_id:
item=await service.get_inbox_message_by_id(record_id)
return JSONResponse(content={"data":item,"total":1,"status_code":200})
items=await service.get_inbox_messages(top,skip,search)
total=await service.count_inbox_messages(search)
return JSONResponse(content={"data":items,"total":total,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.post("/inbox/{record_id}/match")
async def rematch_inbox(
record_id: str,
current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=Email(session=session)
queued_id=await service.queue_rematch(record_id)
task_ids=await service.enqueue_matching([queued_id], force=True)
return JSONResponse(content={"data":{"queued":True,"task_ids":task_ids},"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.patch("/inbox/{record_id}/assign-job-post")
async def assign_job_post(
record_id: str,
payload: AssignJobPostBody,
current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=Email(session=session)
data=await service.assign_job_post(record_id,payload.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.post("/inbox/{record_id}/read")
async def mark_inbox_read(
record_id: str,
payload: ReadBody | None = None,
current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)),
session: AsyncSession = Depends(get_session),
):
"""Flip one row. The body is OPTIONAL and defaults to read=true, so the original
bodyless POST this route shipped with keeps working unchanged."""
try:
service=Email(session=session)
data=await service.mark_read(record_id,payload.read if payload else 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("/inbox/read")
async def bulk_mark_inbox_read(
payload: BulkReadBody,
current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)),
session: AsyncSession = Depends(get_session),
):
"""Selected rows -> read/unread. Single segment after /inbox, so it never collides
with the two-segment /inbox/{record_id}/read above."""
try:
service=Email(session=session)
data=await service.set_read_bulk(payload.record_ids,payload.read)
return JSONResponse(content={"data":data,"total":data["updated"],"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.patch("/inbox/read-all")
async def mark_all_inbox_read(
payload: ReadAllBody,
current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)),
session: AsyncSession = Depends(get_session),
):
"""Every row matching the caller's current list filter -> read/unread."""
try:
service=Email(session=session)
data=await service.set_read_all(
payload.read,
search=payload.search,
isread=payload.isread,
application_status=payload.application_status,
assigned=payload.assigned,
)
return JSONResponse(content={"data":data,"total":data["updated"],"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/inbox/{record_id}/read-status")
async def get_inbox_read_status(
record_id: str,
token: str | None = Query(None),
current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=Email(session=session,token=token)
data=await service.refresh_read_status(record_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("/inbox/all-applications")
async def get_all_applications(
record_id: str | None = Query(None),
application_status: Candidate_application_Status = Query(default=Candidate_application_Status.CLOSED),
isread: bool = Query(default=True),
assigned: bool | None = Query(default=None),
search: str | None = Query(None),
top: int | None = Query(None),
skip: int = Query(0, ge=0),
current_user: dict = Depends(require_permission(PermissionTag.INBOX_VIEW)),
session: AsyncSession = Depends(get_session),
):
try:
service=Email(session=session)
if application_status in (Candidate_application_Status.PROCESS, Candidate_application_Status.REJECTED, Candidate_application_Status.SCREENING, Candidate_application_Status.ASSESSMENT, Candidate_application_Status.INTERVIEW, Candidate_application_Status.OFFER, Candidate_application_Status.HIRED):
items=await service.get_all_applications(top, skip, search, application_status=application_status, assigned=assigned)
total=await service.count_inbox_messages(search, application_status=application_status, assigned=assigned)
return JSONResponse(content={"data":items,"total":total,"status_code":200})
if isread==False:
items=await service.get_all_applications(top, skip, search, isread=False, assigned=assigned)
total=await service.count_inbox_messages(search, isread=False, assigned=assigned)
return JSONResponse(content={"data":items,"total":total,"status_code":200})
if record_id:
item=await service.get_application_by_id(record_id)
return JSONResponse(content={"data":item,"total":1,"status_code":200})
items=await service.get_all_applications(top,skip,search,assigned=assigned)
total=await service.count_inbox_messages(search,assigned=assigned)
return JSONResponse(content={"data":items,"total":total,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/inbox/counts")
async def get_inbox_counts(
current_user: dict = Depends(require_permission(PermissionTag.INBOX_VIEW)),
session: AsyncSession = Depends(get_session),
):
try:
service=Email(session=session)
data=await service.get_counts()
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("/inbox/triage")
async def fetch_triage(
search: str | None = Query(None),
is_application: bool | None = Query(None),
status: str | None = Query(None),
top: int = Query(100),
skip: int = Query(0, ge=0),
current_user: dict = Depends(require_permission(PermissionTag.INBOX_VIEW)),
session: AsyncSession = Depends(get_session),
):
try:
service=Email(session=session)
data=await service.get_triage_messages(top,skip,search,is_application,status)
total=await service.count_triage(search,is_application,status)
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("/inbox/triage/{record_id}/override")
async def override_triage(
record_id: str,
payload: TriageOverrideBody,
current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=Email(session=session)
data=await service.override_triage(record_id,payload.is_application,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("/inbox/{record_id}/processing-state")
async def set_processing_state(
record_id: str,
payload: ProcessingStateBody,
current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=Email(session=session)
data=await service.set_processing_state(record_id,payload.processing_state)
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("/inbox/{record_id}/duplicate")
async def set_duplicate(
record_id: str,
payload: DuplicateBody,
current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=Email(session=session)
data=await service.set_duplicate(record_id,payload.is_duplicate)
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.post("/email/send")
async def send_email(
payload: EmailSendBody,
current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=Email(session=session)
data=await service.send_email(payload.model_dump(exclude_unset=True),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("/email/reply")
async def reply_email(
payload: EmailReplyBody,
current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=Email(session=session)
data=await service.reply_email(payload.model_dump(exclude_unset=True),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))