verification_check #96

Merged
ahmed.mujtaba merged 2 commits from Verification_check_Form into dev_main 2026-09-16 11:06:16 +00:00
8 changed files with 604 additions and 0 deletions

View File

@ -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 g_sheet.app import router as g_sheet_router
from s3.app import router as s3_router from s3.app import router as s3_router
from department.app import router as department_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") logging.basicConfig(level=logging.INFO,format="%(levelname)-8s %(name)s: %(message)s")
logger=logging.getLogger("main") logger=logging.getLogger("main")
@ -138,3 +139,4 @@ app.include_router(candidate_forms_router)
app.include_router(g_sheet_router) app.include_router(g_sheet_router)
app.include_router(s3_router) app.include_router(s3_router)
app.include_router(department_router) app.include_router(department_router)
app.include_router(verification_check_router)

View File

@ -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);

View File

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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,
}

View File

@ -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}