From 5dcc824d70d4b9cef5a0aea38cf0dffc43c212c2 Mon Sep 17 00:00:00 2001 From: "ahmed.mujtaba" Date: Mon, 24 Aug 2026 16:23:55 +0000 Subject: [PATCH 1/4] . --- backend/.env | 105 --------------------------------------------------- 1 file changed, 105 deletions(-) delete mode 100644 backend/.env diff --git a/backend/.env b/backend/.env deleted file mode 100644 index 2cca649..0000000 --- a/backend/.env +++ /dev/null @@ -1,105 +0,0 @@ - -# true → RDS over SSL (asyncpg). false → local Postgres over asyncpg (no SSH). -PROD_ENV=false - -DB_USERNAME=postgres -DB_PASSWORD=ambaig123 -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=dev_hrms -# Blank: require when PROD_ENV=true, off when local. Override only if needed. -DB_SSLMODE= -# DB_USERNAME=utopiaaiadmin -# DB_PASSWORD=cOIYBbjeRpb5rZigZBYX -# DB_HOST=utopia-ai-hr-ats-portal-db.co658idowpql.us-east-2.rds.amazonaws.com -# DB_PORT=5432 -# DB_NAME=dev_hrms -# DB_SSLMODE=require - -EMAIL_URL=http://172.16.204.191:5000 -EMAIL_API_TOKEN=vn1a9sx6tI3L5LVXgblOiVpxn3gAiJXTVYvO5GICUNo -EMAIL_SYNC_FOLDER=inbox -EMAIL_SYNC_SINCE= -EMAIL_SYNC_CRON=* * * * * -JWT_SECRET_KEY=8B6lep0TuSrGp__2SFDCqKGyEluuUWV7SxvobhG5ECQYvN4o4P0OakRW0VWmiflZ6hgNK7RGdOVmi4H8LnSLwA -JWT_ALGORITHM=HS256 -JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30 -JWT_REFRESH_TOKEN_EXPIRE_DAYS=7 - -TEAMS_MAIL_API_URL=http://ec2-13-222-174-68.compute-1.amazonaws.com:8000/mail/send -TEAMS_API_TOKEN=wt139Dg1bm0lXoYszzwCcMmkjQrkuP -RESET_CODE_TTL_SECONDS=60 -RESET_CODE_RESEND_SECONDS=30 -RESET_CODE_MAX_ATTEMPTS=5 -JWT_RESET_TOKEN_EXPIRE_MINUTES=10 - -# Vite / email links. Docker SPA publishes on FRONTEND_PORT (5173). -FRONTEND_URL=http://127.0.0.1:5173 -CONFIRM_EMAIL_PATH=/auth/confirm-email -CONFIRM_TOKEN_TTL_SECONDS=86400 -CONFIRM_TOKEN_RESEND_SECONDS=60 - -BUFFER_API=1aF1d6NQ3qTsjtfA_mdNwqfhK6myTqXK0vwSySiPRrK -BUFFER_API_URL=https://api.buffer.com -BUFFER_CHANNEL_ID=6a73328399afb4434907444f -CLIENT_ID=6a7331dc2d90e07071891bfc - -# OpenAI API Key -OPENAI_API_KEY=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 -OPENAI_BASE_URL=https://api.openai.com/v1 -OPENAI_MAX_OUTPUT_TOKENS=32768 -OPENAI_MODEL=gpt-5.4-mini -OPENAI_TIMEOUT=60 -OPENAI_MAX_RETRIES=3 -OPENAI_CONNECT_RETRIES=3 -OPENAI_TEMPERATURE=0.3 - -REDIS_URL=redis://redis:6379/0 -TASKIQ_QUEUE_NAME=inbox -TASKIQ_MAX_RETRIES=3 -TASKIQ_RETRY_DELAY=5 -TASKIQ_MAX_DELAY=120 -TASKIQ_DLQ_STREAM=taskiq:dlq -TASKIQ_IDLE_TIMEOUT_MS=600000 -APP_VERSION=dev - - -# Google Sheet -SPREADSHEET_NAME=Product Hiring -SPREADSHEET_ID=1yjRueJjA1NbW8rS-KsSCJxxnkqh6cPiJjwcmXbTLusk -SPREADSHEET_URL=https://docs.google.com/spreadsheets/d/1yjRueJjA1NbW8rS-KsSCJxxnkqh6cPiJjwcmXbTLusk/edit - -# Google Cloud project -GOOGLE_CLOUD_PROJECT=hrms-ats-portal -GOOGLE_ACCOUNT=ahmed.mujtaba@utopiabrands.com - -# OAuth client (Desktop) -GOOGLE_CLIENT_ID=679334897177-ufal3rogbg8cgm3qcren6pv20phqd7nl.apps.googleusercontent.com -GOOGLE_CLIENT_SECRET=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 -GOOGLE_OAUTH_CLIENT_ID_FILE=credentials/client_secret.json - -# Application Default Credentials (used by google-auth) -GOOGLE_APPLICATION_CREDENTIALS=credentials/application_default_credentials.json -GOOGLE_REFRESH_TOKEN=1//038IfgCu3D42fCgYIARAAGAMSNwF-L9IrYAZ_DJUqwC9ETwLtH23D46j61gWMwFQjRWPklFZIiLmv7Q3-TOgcNxTrjO30jgkeYOo -GOOGLE_CREDENTIALS_TYPE=authorized_user - - - -BACKEND_URL=http://backend-api:8000 -DB_AUTOGENERATE=false - -# --------------------------------------------------------------------------- - -DB_AUTO_MIGRATE=false - -# Compose host ports (docker compose --env-file ./backend/.env …). -FRONTEND_PORT=5173 -BACKEND_PORT=8000 -ATS_PORT=8100 -REDIS_PORT=6379 -POSTGRES_PORT=5433 - -UVICORN_WORKERS=2 - -# Empty = same-origin; frontend nginx proxies API paths to backend-api. -VITE_API_BASE= From 09965f420f616237d4c985115ad0a72fdf36c39c Mon Sep 17 00:00:00 2001 From: "ahmed.mujtaba" Date: Mon, 24 Aug 2026 16:24:49 +0000 Subject: [PATCH 2/4] . --- .env | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 94335dc..0000000 --- a/.env +++ /dev/null @@ -1,5 +0,0 @@ -# Compose pointer only — NO secrets here. -# Makes `docker compose up -d --build` read ${FRONTEND_PORT}, ${BACKEND_PORT}, … -# from backend/.env without typing --env-file every time. -# All credentials and app config live in backend/.env. -COMPOSE_ENV_FILES=./backend/.env From 8bfd71194e41aa239b36a9022a846255f6224e82 Mon Sep 17 00:00:00 2001 From: Talha Ahmed Date: Mon, 24 Aug 2026 22:21:36 +0500 Subject: [PATCH 3/4] Forms also unlock when the candidate has an interview scheduled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scheduling an interview record did not move the pipeline stage, so recruiters who booked an interview from the profile still saw the Forms tab locked (reported on a Shortlist-stage candidate with a completed Technical interview). The gate now passes when the stage is INTERVIEW/OFFER/HIRED OR the application has any interview on record — enforced server-side in candidate_forms create (manual-upload candidates still unlock by stage alone, since interviews link only to inbox rows) and mirrored in the Forms tab hint. Co-Authored-By: Claude Fable 5 --- backend/candidate_forms/views.py | 26 +++++++++++++++++-------- frontend/dist/index.html | 2 +- frontend/src/screens/CandidateForms.jsx | 10 +++++++--- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/backend/candidate_forms/views.py b/backend/candidate_forms/views.py index 447a797..fffdd92 100644 --- a/backend/candidate_forms/views.py +++ b/backend/candidate_forms/views.py @@ -18,7 +18,7 @@ from candidate_forms.plugins import ( ) from candidate_forms.serializers import serialize_form from inbox.models import Inbox -from job.candidate.models import Manual_UPLOAD_CANDIDATE +from job.candidate.models import Interviews, Manual_UPLOAD_CANDIDATE from job.history.enums import HistoryEvent from job.history.views import HistoryRecorder from job.job_post.models import JobPosts @@ -263,13 +263,23 @@ class CandidateForm: ) inbox_id, manual_id, job_post_id, stage = await self._validate_link(payload) if stage not in FORM_READY_STATUSES: - raise HTTPException( - status_code=409, - detail=( - "Forms unlock at the Interview stage — this candidate is at " - f"{stage or 'Shortlist'}" - ), - ) + # A scheduled interview also unlocks the forms: the paperwork belongs + # to the interview, not to which kanban column the card sits in. + # (Interview records link only to inbox rows, so manual-upload + # candidates unlock by stage alone.) + has_interview = False + if inbox_id is not None: + rows = await Interviews.get_interviews_by_inbox(self.session, inbox_id) + has_interview = bool(rows) + if not has_interview: + raise HTTPException( + status_code=409, + detail=( + "Forms unlock once the candidate reaches the Interview stage " + f"or has an interview scheduled — this candidate is at " + f"{stage or 'Shortlist'} with no interview on record" + ), + ) fields = { "inbox_id": inbox_id, diff --git a/frontend/dist/index.html b/frontend/dist/index.html index df37875..7498ad5 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -23,7 +23,7 @@ - + diff --git a/frontend/src/screens/CandidateForms.jsx b/frontend/src/screens/CandidateForms.jsx index 391cd71..4547a28 100644 --- a/frontend/src/screens/CandidateForms.jsx +++ b/frontend/src/screens/CandidateForms.jsx @@ -72,7 +72,10 @@ export default function CandidateFormsTab({ userId, live }) { const listParams = inboxId ? { inboxId } : { manualUploadCandidateId: manualId } const hasApplication = Boolean(inboxId || manualId) const stage = String(live?.application_status || '').toUpperCase() - const unlocked = formsApi.FORM_READY_STATUSES.includes(stage) + // Stage-ready OR an interview on the books — scheduling an interview is what + // makes the paperwork relevant, wherever the kanban card sits. + const hasInterview = (live?.interviews?.length ?? 0) > 0 + const unlocked = formsApi.FORM_READY_STATUSES.includes(stage) || hasInterview const defsQuery = useQuery({ queryKey: qk.forms.definitions(), @@ -102,8 +105,9 @@ export default function CandidateFormsTab({ userId, live }) { if (!unlocked) { return ( - This candidate is at {STAGE_FROM_STATUS[stage] ?? titleCase(stage)}. Move them along - the pipeline to fill the requisition, evaluation and offer forms. + This candidate is at {STAGE_FROM_STATUS[stage] ?? titleCase(stage)} with no interview + on record. Schedule an interview on the Interview tab, or move them along the + pipeline, to fill the requisition, evaluation and offer forms. ) } From 2aa151f3e47c826dc422398839a3d2abe8b5fc8f Mon Sep 17 00:00:00 2001 From: Talha Ahmed Date: Mon, 24 Aug 2026 22:23:26 +0500 Subject: [PATCH 4/4] Restore /candidate_forms/ gitignore entry lost in merge Both branches appended at EOF; the merge kept only main's hunk and its missing trailing newline. Root-anchored so backend/candidate_forms/ (the forms domain package) stays tracked. Co-Authored-By: Claude Fable 5 --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9acc410..3b123d1 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,8 @@ Utopia-ai-hr-ats-portal 1.pem # Local-only Compose overrides (never deployed) docker.local.env tests/** -**/.env** \ No newline at end of file +**/.env** + +# Paper form source documents (Annexure A/E/J) — reference material, not code. +# Root-anchored: backend/candidate_forms/ is the forms domain package and IS tracked. +/candidate_forms/