diff --git a/backend/main.py b/backend/main.py index 2e33469..e3eb2c3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -25,6 +25,7 @@ from candidate_forms.app import router as candidate_forms_router from g_sheet.app import router as g_sheet_router from s3.app import router as s3_router from department.app import router as department_router +from verification_check.app import router as verification_check_router logging.basicConfig(level=logging.INFO,format="%(levelname)-8s %(name)s: %(message)s") logger=logging.getLogger("main") @@ -138,3 +139,4 @@ app.include_router(candidate_forms_router) app.include_router(g_sheet_router) app.include_router(s3_router) app.include_router(department_router) +app.include_router(verification_check_router) diff --git a/backend/migrations/manual/042_verification_check.sql b/backend/migrations/manual/042_verification_check.sql new file mode 100644 index 0000000..89b2d1e --- /dev/null +++ b/backend/migrations/manual/042_verification_check.sql @@ -0,0 +1,73 @@ +-- 042_verification_check.sql +-- Annexure E - Employment Verification Check Form as its own table +-- (backend/verification_check/models.py, VerificationCheck). One row per referee +-- call, so a candidate can have several. recruiter_id and candidate_id both +-- point at users.id (recruiters and candidates are users in this system); +-- created_by is the logged-in user who filled the form. +-- +-- The table name is mixed case at the user's request, so it must stay quoted +-- ("Verification_Check") in any raw SQL. Index / FK names follow the db_setup +-- NAMING_CONVENTION so a dev DB that autogenerated first is a no-op. +-- +-- Idempotent, applied at startup by alembic_setup.run_manual_sql(). Needed +-- because prod boots with DB_AUTOGENERATE=false and never autogenerates tables. + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_type t + JOIN pg_namespace n ON n.oid = t.typnamespace + WHERE t.typname = 'rehireeligibility' AND n.nspname = 'app' + ) THEN + CREATE TYPE app.rehireeligibility AS ENUM ('yes', 'no', 'conditional'); + END IF; +END $$; + +CREATE TABLE IF NOT EXISTS app."Verification_Check" ( + id uuid PRIMARY KEY, + recruiter_id uuid, + candidate_id uuid, + position_applied varchar, + department_id uuid, + department varchar, + date_time timestamptz, + confirmed_job_title varchar, + confirmed_from date, + confirmed_to date, + reason_for_leaving varchar, + technical_knowledge_rating integer, + technical_knowledge_notes varchar, + reliability_rating integer, + reliability_notes varchar, + communication_rating integer, + communication_notes varchar, + problem_solving_rating integer, + problem_solving_notes varchar, + strengths varchar, + areas_for_growth varchar, + rehire_eligibility app.rehireeligibility, + rehire_comments varchar, + referee_full_name varchar, + referee_current_title varchar, + referee_company varchar, + referee_phone varchar, + referee_email varchar, + created_by uuid, + created_at timestamptz NOT NULL DEFAULT NOW(), + updated_at timestamptz NOT NULL DEFAULT NOW(), + is_deleted boolean NOT NULL DEFAULT false, + CONSTRAINT "fk_Verification_Check_recruiter_id_users" + FOREIGN KEY (recruiter_id) REFERENCES app.users (id), + CONSTRAINT "fk_Verification_Check_candidate_id_users" + FOREIGN KEY (candidate_id) REFERENCES app.users (id), + CONSTRAINT "fk_Verification_Check_department_id_departments" + FOREIGN KEY (department_id) REFERENCES app.departments (id), + CONSTRAINT "fk_Verification_Check_created_by_users" + FOREIGN KEY (created_by) REFERENCES app.users (id) +); + +CREATE INDEX IF NOT EXISTS "ix_Verification_Check_recruiter_id" + ON app."Verification_Check" (recruiter_id); + +CREATE INDEX IF NOT EXISTS "ix_Verification_Check_candidate_id" + ON app."Verification_Check" (candidate_id); diff --git a/backend/reference_check/app.py b/backend/reference_check/app.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/verification_check/app.py b/backend/verification_check/app.py new file mode 100644 index 0000000..b5856d2 --- /dev/null +++ b/backend/verification_check/app.py @@ -0,0 +1,108 @@ +import uuid +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi.responses import JSONResponse +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession + +from db_setup import get_session +from users.permissions import PermissionTag, require_permission +from verification_check.enums import ( + CompetencyEvaluation, + EmploymentVerification, + QualitativeEvaluation, + RecordOverview, + RefereeDetails, +) +from verification_check.views import VerificationCheckForm + +router = APIRouter() + + +class VerificationCheckCreate(BaseModel): + recruiter_id:uuid.UUID + candidate_id:uuid.UUID + record_overview:Optional[RecordOverview] + employment_verification:Optional[EmploymentVerification] + competency_evaluation:Optional[CompetencyEvaluation] + qualitative_evaluation:Optional[QualitativeEvaluation] + referee:Optional[RefereeDetails] + + +class VerificationCheckUpdate(BaseModel): + recruiter_id:Optional[uuid.UUID]=None + candidate_id:Optional[uuid.UUID]=None + record_overview:Optional[RecordOverview]=None + employment_verification:Optional[EmploymentVerification]=None + competency_evaluation:Optional[CompetencyEvaluation]=None + qualitative_evaluation:Optional[QualitativeEvaluation]=None + referee:Optional[RefereeDetails]=None + + +@router.get("/verification-check/fetch") +async def fetch_verification_check( + current_user:dict=Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), + form_id:str=Query(None), + candidate_id:str=Query(None), + session:AsyncSession=Depends(get_session), +): + """One row by form_id, else the list (optionally one candidate's referees). + Admins see every row; other roles stay scoped to created_by.""" + try: + service=VerificationCheckForm(session=session) + data=await service.get_form_by_id(form_id,candidate_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.post("/verification-check/create") +async def create_verification_check( + payload: VerificationCheckCreate, + current_user:dict=Depends(require_permission(PermissionTag.CANDIDATES_CREATE)), + session:AsyncSession=Depends(get_session), +): + try: + service = VerificationCheckForm(session=session) + data = await service.create_form(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.patch("/verification-check/update") +async def update_verification_check( + payload: VerificationCheckUpdate, + current_user:dict=Depends(require_permission(PermissionTag.CANDIDATES_EDIT)), + form_id:str=Query(...), + session:AsyncSession=Depends(get_session), +): + try: + service=VerificationCheckForm(session=session) + data=await service.update_form(form_id,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.delete("/verification-check/delete") +async def delete_verification_check( + current_user:dict=Depends(require_permission(PermissionTag.CANDIDATES_DELETE)), + form_id:str=Query(...), + session:AsyncSession=Depends(get_session), +): + try: + service=VerificationCheckForm(session=session) + data=await service.delete_form(form_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)) diff --git a/backend/verification_check/enums.py b/backend/verification_check/enums.py new file mode 100644 index 0000000..c521917 --- /dev/null +++ b/backend/verification_check/enums.py @@ -0,0 +1,64 @@ +from enum import Enum +from pydantic import BaseModel +from typing import Optional +from datetime import date, datetime +import uuid + + +class CompetencyRating(int, Enum): + NEEDS_IMPROVEMENT = 1 + SATISFACTORY = 2 + GOOD = 3 + EXCELLENT = 4 + + +class ReHireEligibility(str, Enum): + YES = "yes" + NO = "no" + CONDITIONAL = "conditional" + + +# SECTION 1: INFORMATION & RECORD OVERVIEW +class RecordOverview(BaseModel): + position_applied:Optional[str]=None + department_id:Optional[uuid.UUID]=None + department:Optional[str]=None + date_time:Optional[datetime]=None + + +# SECTION 2: EMPLOYMENT VERIFICATION & DUTIES +class EmploymentVerification(BaseModel): + confirmed_job_title:Optional[str]=None + confirmed_from:Optional[date]=None + confirmed_to:Optional[date]=None + reason_for_leaving:Optional[str]=None + + +# SECTION 3: one row of the competency grid (1-4 tick + Notes column) +class CompetencyScore(BaseModel): + rating:Optional[CompetencyRating]=None + notes:Optional[str]=None + + +class CompetencyEvaluation(BaseModel): + technical_knowledge:Optional[CompetencyScore]=None + reliability:Optional[CompetencyScore]=None + communication:Optional[CompetencyScore]=None + problem_solving:Optional[CompetencyScore]=None + + +# SECTION 4: QUALITATIVE EVALUATION & KEY INSIGHTS +class QualitativeEvaluation(BaseModel): + strengths:Optional[str]=None + areas_for_growth:Optional[str]=None + rehire_eligibility:Optional[ReHireEligibility]=None + rehire_comments:Optional[str]=None + + +# SECTION 5: REFEREE CONTACT & RELATIONSHIP VERIFICATION +class RefereeDetails(BaseModel): + full_name:Optional[str]=None + current_title:Optional[str]=None + company:Optional[str]=None + phone:Optional[str]=None + email:Optional[str]=None diff --git a/backend/verification_check/models.py b/backend/verification_check/models.py new file mode 100644 index 0000000..e2ab26b --- /dev/null +++ b/backend/verification_check/models.py @@ -0,0 +1,229 @@ +import uuid +from datetime import datetime, date as Date, timezone +from typing import Optional + +from sqlalchemy import DateTime, Enum as SAEnum +from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import Field, SQLModel, select + +from verification_check.enums import ReHireEligibility + + +def _now() -> datetime: + return datetime.now(timezone.utc) + + +class VerificationCheck(SQLModel, table=True): + """Annexure E - Employment Verification Check Form. One row per referee call, + so a candidate can hold several. recruiter_id / candidate_id are both users.id.""" + + __tablename__ = "Verification_Check" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + + recruiter_id: Optional[uuid.UUID] = Field(default=None, foreign_key="users.id", index=True) + candidate_id: Optional[uuid.UUID] = Field(default=None, foreign_key="users.id", index=True) + + position_applied: Optional[str] = None + department_id: Optional[uuid.UUID] = Field(default=None, foreign_key="departments.id") + department: Optional[str] = None + date_time: Optional[datetime] = Field(default=None, sa_type=DateTime(timezone=True)) + + confirmed_job_title: Optional[str] = None + confirmed_from: Optional[Date] = None + confirmed_to: Optional[Date] = None + reason_for_leaving: Optional[str] = None + + technical_knowledge_rating: Optional[int] = None + technical_knowledge_notes: Optional[str] = None + reliability_rating: Optional[int] = None + reliability_notes: Optional[str] = None + communication_rating: Optional[int] = None + communication_notes: Optional[str] = None + problem_solving_rating: Optional[int] = None + problem_solving_notes: Optional[str] = None + + strengths: Optional[str] = None + areas_for_growth: Optional[str] = None + rehire_eligibility: Optional[ReHireEligibility] = Field( + default=None, + sa_type=SAEnum( + ReHireEligibility, + name="rehireeligibility", + schema="app", + native_enum=True, + values_callable=lambda enum: [member.value for member in enum], + ), + ) + rehire_comments: Optional[str] = None + + referee_full_name: Optional[str] = None + referee_current_title: Optional[str] = None + referee_company: Optional[str] = None + referee_phone: Optional[str] = None + referee_email: Optional[str] = None + + created_by: Optional[uuid.UUID] = Field(foreign_key="users.id") + created_at: datetime = Field(default_factory=_now, sa_type=DateTime(timezone=True)) + updated_at: datetime = Field(default_factory=_now, sa_type=DateTime(timezone=True)) + is_deleted: bool = Field(default=False) + + @classmethod + async def get_form_by_id(cls, session: AsyncSession, record_id=None, created_by=None, candidate_id=None): + qry = select(cls).where(cls.is_deleted == False) # noqa: E712 + if created_by is not None: + qry = qry.where(cls.created_by == created_by) + if candidate_id is not None: + qry = qry.where(cls.candidate_id == candidate_id) + if record_id not in (None, ""): + try: + uid = uuid.UUID(str(record_id)) + except (TypeError, ValueError): + return None + qry = qry.where(cls.id == uid) + result = await session.execute(qry) + return result.scalars().first() + result = await session.execute(qry.order_by(cls.created_at.desc(), cls.id.desc())) + return list(result.scalars().all()) + + @classmethod + async def insert_form(cls, session: AsyncSession, fields: dict): + overview = fields.get("record_overview") if fields.get("record_overview") else {} + employment = fields.get("employment_verification") if fields.get("employment_verification") else {} + competency = fields.get("competency_evaluation") if fields.get("competency_evaluation") else {} + technical = competency.get("technical_knowledge") if competency.get("technical_knowledge") else {} + reliability = competency.get("reliability") if competency.get("reliability") else {} + communication = competency.get("communication") if competency.get("communication") else {} + problem_solving = competency.get("problem_solving") if competency.get("problem_solving") else {} + qualitative = fields.get("qualitative_evaluation") if fields.get("qualitative_evaluation") else {} + referee = fields.get("referee") if fields.get("referee") else {} + row = cls( + recruiter_id=fields.get("recruiter_id") if fields.get("recruiter_id") else None, + candidate_id=fields.get("candidate_id") if fields.get("candidate_id") else None, + position_applied=overview.get("position_applied") if overview.get("position_applied") else None, + department_id=overview.get("department_id") if overview.get("department_id") else None, + department=overview.get("department") if overview.get("department") else None, + date_time=overview.get("date_time") if overview.get("date_time") else None, + confirmed_job_title=employment.get("confirmed_job_title") if employment.get("confirmed_job_title") else None, + confirmed_from=employment.get("confirmed_from") if employment.get("confirmed_from") else None, + confirmed_to=employment.get("confirmed_to") if employment.get("confirmed_to") else None, + reason_for_leaving=employment.get("reason_for_leaving") if employment.get("reason_for_leaving") else None, + technical_knowledge_rating=int(technical.get("rating")) if technical.get("rating") else None, + technical_knowledge_notes=technical.get("notes") if technical.get("notes") else None, + reliability_rating=int(reliability.get("rating")) if reliability.get("rating") else None, + reliability_notes=reliability.get("notes") if reliability.get("notes") else None, + communication_rating=int(communication.get("rating")) if communication.get("rating") else None, + communication_notes=communication.get("notes") if communication.get("notes") else None, + problem_solving_rating=int(problem_solving.get("rating")) if problem_solving.get("rating") else None, + problem_solving_notes=problem_solving.get("notes") if problem_solving.get("notes") else None, + strengths=qualitative.get("strengths") if qualitative.get("strengths") else None, + areas_for_growth=qualitative.get("areas_for_growth") if qualitative.get("areas_for_growth") else None, + rehire_eligibility=ReHireEligibility(qualitative.get("rehire_eligibility")) if qualitative.get("rehire_eligibility") else None, + rehire_comments=qualitative.get("rehire_comments") if qualitative.get("rehire_comments") else None, + referee_full_name=referee.get("full_name") if referee.get("full_name") else None, + referee_current_title=referee.get("current_title") if referee.get("current_title") else None, + referee_company=referee.get("company") if referee.get("company") else None, + referee_phone=referee.get("phone") if referee.get("phone") else None, + referee_email=referee.get("email") if referee.get("email") else None, + created_by=fields.get("created_by") if fields.get("created_by") else None, + ) + session.add(row) + await session.commit() + return await cls.get_form_by_id(session, row.id) + + @classmethod + async def update_form(cls, session: AsyncSession, record_id, fields: dict): + row = await cls.get_form_by_id(session, record_id) + if not row: + return None + if "recruiter_id" in fields: + row.recruiter_id = fields.get("recruiter_id") if fields.get("recruiter_id") else None + if "candidate_id" in fields: + row.candidate_id = fields.get("candidate_id") if fields.get("candidate_id") else None + if "record_overview" in fields: + overview = fields.get("record_overview") if fields.get("record_overview") else {} + if "position_applied" in overview: + row.position_applied = overview.get("position_applied") if overview.get("position_applied") else None + if "department_id" in overview: + row.department_id = overview.get("department_id") if overview.get("department_id") else None + if "department" in overview: + row.department = overview.get("department") if overview.get("department") else None + if "date_time" in overview: + row.date_time = overview.get("date_time") if overview.get("date_time") else None + if "employment_verification" in fields: + employment = fields.get("employment_verification") if fields.get("employment_verification") else {} + if "confirmed_job_title" in employment: + row.confirmed_job_title = employment.get("confirmed_job_title") if employment.get("confirmed_job_title") else None + if "confirmed_from" in employment: + row.confirmed_from = employment.get("confirmed_from") if employment.get("confirmed_from") else None + if "confirmed_to" in employment: + row.confirmed_to = employment.get("confirmed_to") if employment.get("confirmed_to") else None + if "reason_for_leaving" in employment: + row.reason_for_leaving = employment.get("reason_for_leaving") if employment.get("reason_for_leaving") else None + if "competency_evaluation" in fields: + competency = fields.get("competency_evaluation") if fields.get("competency_evaluation") else {} + if "technical_knowledge" in competency: + technical = competency.get("technical_knowledge") if competency.get("technical_knowledge") else {} + if "rating" in technical: + row.technical_knowledge_rating = int(technical.get("rating")) if technical.get("rating") else None + if "notes" in technical: + row.technical_knowledge_notes = technical.get("notes") if technical.get("notes") else None + if "reliability" in competency: + reliability = competency.get("reliability") if competency.get("reliability") else {} + if "rating" in reliability: + row.reliability_rating = int(reliability.get("rating")) if reliability.get("rating") else None + if "notes" in reliability: + row.reliability_notes = reliability.get("notes") if reliability.get("notes") else None + if "communication" in competency: + communication = competency.get("communication") if competency.get("communication") else {} + if "rating" in communication: + row.communication_rating = int(communication.get("rating")) if communication.get("rating") else None + if "notes" in communication: + row.communication_notes = communication.get("notes") if communication.get("notes") else None + if "problem_solving" in competency: + problem_solving = competency.get("problem_solving") if competency.get("problem_solving") else {} + if "rating" in problem_solving: + row.problem_solving_rating = int(problem_solving.get("rating")) if problem_solving.get("rating") else None + if "notes" in problem_solving: + row.problem_solving_notes = problem_solving.get("notes") if problem_solving.get("notes") else None + if "qualitative_evaluation" in fields: + qualitative = fields.get("qualitative_evaluation") if fields.get("qualitative_evaluation") else {} + if "strengths" in qualitative: + row.strengths = qualitative.get("strengths") if qualitative.get("strengths") else None + if "areas_for_growth" in qualitative: + row.areas_for_growth = qualitative.get("areas_for_growth") if qualitative.get("areas_for_growth") else None + if "rehire_eligibility" in qualitative: + row.rehire_eligibility = ReHireEligibility(qualitative.get("rehire_eligibility")) if qualitative.get("rehire_eligibility") else None + if "rehire_comments" in qualitative: + row.rehire_comments = qualitative.get("rehire_comments") if qualitative.get("rehire_comments") else None + if "referee" in fields: + referee = fields.get("referee") if fields.get("referee") else {} + if "full_name" in referee: + row.referee_full_name = referee.get("full_name") if referee.get("full_name") else None + if "current_title" in referee: + row.referee_current_title = referee.get("current_title") if referee.get("current_title") else None + if "company" in referee: + row.referee_company = referee.get("company") if referee.get("company") else None + if "phone" in referee: + row.referee_phone = referee.get("phone") if referee.get("phone") else None + if "email" in referee: + row.referee_email = referee.get("email") if referee.get("email") else None + row.updated_at = _now() + session.add(row) + await session.commit() + await session.refresh(row) + return row + + @classmethod + async def soft_delete_form(cls, session: AsyncSession, record_id): + row = await cls.get_form_by_id(session, record_id) + if not row: + return None + row.is_deleted = True + row.updated_at = _now() + session.add(row) + await session.commit() + return row + + +import users.models as _users_models # noqa: E402, F401 +import department.models as _department_models # noqa: E402, F401 diff --git a/backend/verification_check/serializers.py b/backend/verification_check/serializers.py new file mode 100644 index 0000000..4028e6d --- /dev/null +++ b/backend/verification_check/serializers.py @@ -0,0 +1,68 @@ +def _date(value): + return value.isoformat() if value else None + + +def _enum(value): + if value is None: + return None + return getattr(value, "value", value) + + +def serialize_verification_check(row, names=None) -> dict: + """`names` is {user_id: name} from one batched Users.names_by_ids lookup in + views, so the recruiter / candidate search dropdowns can preselect on edit.""" + names = names or {} + return { + "id": str(row.id) if row.id else None, + "recruiter_id": str(row.recruiter_id) if row.recruiter_id else None, + "recruiter_name": names.get(str(row.recruiter_id)) if row.recruiter_id else None, + "candidate_id": str(row.candidate_id) if row.candidate_id else None, + "candidate_name": names.get(str(row.candidate_id)) if row.candidate_id else None, + "record_overview": { + "position_applied": row.position_applied, + "department_id": str(row.department_id) if row.department_id else None, + "department": row.department, + "date_time": _date(row.date_time), + }, + "employment_verification": { + "confirmed_job_title": row.confirmed_job_title, + "confirmed_from": _date(row.confirmed_from), + "confirmed_to": _date(row.confirmed_to), + "reason_for_leaving": row.reason_for_leaving, + }, + "competency_evaluation": { + "technical_knowledge": { + "rating": row.technical_knowledge_rating, + "notes": row.technical_knowledge_notes, + }, + "reliability": { + "rating": row.reliability_rating, + "notes": row.reliability_notes, + }, + "communication": { + "rating": row.communication_rating, + "notes": row.communication_notes, + }, + "problem_solving": { + "rating": row.problem_solving_rating, + "notes": row.problem_solving_notes, + }, + }, + "qualitative_evaluation": { + "strengths": row.strengths, + "areas_for_growth": row.areas_for_growth, + "rehire_eligibility": _enum(row.rehire_eligibility), + "rehire_comments": row.rehire_comments, + }, + "referee": { + "full_name": row.referee_full_name, + "current_title": row.referee_current_title, + "company": row.referee_company, + "phone": row.referee_phone, + "email": row.referee_email, + }, + "created_by": str(row.created_by) if row.created_by else None, + "created_by_name": names.get(str(row.created_by)) if row.created_by else None, + "created_at": row.created_at.isoformat() if row.created_at else None, + "updated_at": row.updated_at.isoformat() if row.updated_at else None, + } diff --git a/backend/verification_check/views.py b/backend/verification_check/views.py new file mode 100644 index 0000000..b35e40c --- /dev/null +++ b/backend/verification_check/views.py @@ -0,0 +1,60 @@ +from fastapi import HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from candidate_forms.plugins import _as_uuid, _aware, _user_id +from users.models import Users +from users.permissions import is_admin +from verification_check.models import VerificationCheck +from verification_check.serializers import serialize_verification_check + + +class VerificationCheckForm: + def __init__(self, session: AsyncSession): + self.session = session + + async def _serialize_rows(self, rows): + user_ids = set() + for row in rows: + user_ids |= {row.recruiter_id, row.candidate_id, row.created_by} + names = await Users.names_by_ids(self.session, user_ids) + return [serialize_verification_check(row, names) for row in rows] + + async def create_form(self, payload, current_user): + if payload.get("record_overview") and "date_time" in payload["record_overview"]: + payload["record_overview"]["date_time"] = _aware(payload["record_overview"]["date_time"]) + payload["created_by"] = _user_id(current_user) + row = await VerificationCheck.insert_form(self.session, payload) + return (await self._serialize_rows([row]))[0] + + async def update_form(self, form_id, payload, current_user): + _user_id(current_user) + row = await VerificationCheck.get_form_by_id(self.session, form_id) + if not row: + raise HTTPException(status_code=404, detail="Verification check not found") + if not payload: + raise HTTPException(status_code=400, detail="No fields to update") + if payload.get("record_overview") and "date_time" in payload["record_overview"]: + payload["record_overview"]["date_time"] = _aware(payload["record_overview"]["date_time"]) + updated = await VerificationCheck.update_form(self.session, form_id, payload) + if not updated: + raise HTTPException(status_code=404, detail="Verification check not found") + return (await self._serialize_rows([updated]))[0] + + async def get_form_by_id(self, form_id, candidate_id, current_user): + created_by = None if is_admin(current_user) else _user_id(current_user) + if form_id: + row = await VerificationCheck.get_form_by_id(self.session, record_id=form_id, created_by=created_by) + if not row: + raise HTTPException(status_code=404, detail="Verification check not found") + return (await self._serialize_rows([row]))[0] + rows = await VerificationCheck.get_form_by_id( + self.session, created_by=created_by, candidate_id=_as_uuid(candidate_id) + ) + return await self._serialize_rows(rows) + + async def delete_form(self, form_id, current_user): + _user_id(current_user) + row = await VerificationCheck.soft_delete_form(self.session, form_id) + if not row: + raise HTTPException(status_code=404, detail="Verification check not found") + return {"id": str(row.id), "deleted": True}