Skip to content

Mr. Hire — Jobs, Applications & Candidate Quizzes

This document describes the recruitment / ATS core of Mr. Hire as implemented inside the mr-mentor-backend Node service: how HR users author job posts (often from a job template), publish them to external platforms, how candidates apply, how an application travels through the pipeline stages, how a candidate takes a screening quiz that produces a ScreeningResult, how the screening config gates auto-advance / auto-shortlist, and how placement scores are uploaded and computed. The AI-heavy stages (resume parsing/scoring, AI voice calls) live in the resumeAnalysisQueue worker and the Python mr-hire-backend; those are documented in sibling docs and only referenced here.

Status: documented from source on this branch.


Overview

Mr. Hire is the recruitment product hosted inside the shared mr-mentor-backend. The jobs & applications domain covers the structured parts of the ATS — everything that is plain CRUD, pipeline movement, configuration, and quiz delivery. The intelligent parts (resume parsing, resume scoring, voice interviews) are queued out to the AI subsystem.

Who uses it (personas / roles):

Persona Role value What they do
External HR / recruiter EXTERNAL_HR Create/manage their own job posts, templates, screening config, send quizzes, trigger AI calls, manage their candidate pipeline.
Admin / Superadmin ADMIN, SUPERADMIN Manage all job posts/templates, global templates, the master platform list, AI-screening config per user, view all applications.
Candidate (public) (no auth) Apply via the public hiring form, take a screening quiz via a signed link.
Student (placement) USER student Subject of placement-readiness scoring (a separate concept — see Placement score).

Roles are checked by auth.middleware.ts (authMiddleware → populates req.user), admin.middleware.ts (adminMiddlewareADMIN | SUPERADMIN), and hrRole.middleware.ts (hrRoleMiddlewareEXTERNAL_HR | ADMIN, see src/middleware/hrRole.middleware.ts).

Where it sits in the suite: the mr-hire-frontend (React/Vite) and the public Mr. Hire hiring page call these endpoints. HR-only AI features (JD generation, quiz curation, AI call triggering, resume parsing) are proxied through this backend to the Python mr-hire-backend at MR_HIRE_BACKEND_URL (default http://localhost:8001) so the frontend never calls it directly.


Key concepts & entities

Glossary

  • Job post — a published opening. status drives whether it is visible/active.
  • Job template — a reusable blueprint for a job post; global (admin-authored, visible to all HR) or private (HR-owned). Templates can also carry quiz/AI-call defaults.
  • Job platform posting — a record that a job was published to one external platform (Naukri, LinkedIn, Indeed, Google for Jobs, etc.), with method (automatic/api/manual) and a posting status.
  • Application (candidate) — a JobApplication; the candidate record that moves through the pipeline.
  • Screening configJobScreeningConfig: per-job → per-HR-global → system-global → hardcoded defaults cascade controlling weights, thresholds, quiz settings, AI-call gating.
  • AI screening configAiScreeningConfig: a per-user on/off switch for auto-screening on apply.
  • Candidate quiz responseCandidateQuizResponse: a snapshot of quiz questions plus a signed JWT token, the candidate's answers, and the computed score.
  • Screening resultScreeningResult: the aggregate scorecard (resume + quiz + call scores, parsed data, final score, recommendation, cost tracking, config snapshot).

Main TypeORM entities (file paths)

Entity File Table Notes
JobPost src/entities/JobPost.ts job_posts status: draft\|active\|paused\|closed; postedBy (email), belongsTo (owner userId).
JobTemplate src/entities/JobTemplate.ts job_templates scope: global\|private; createdBy FK → User.
JobPlatformPosting src/entities/JobPlatformPosting.ts job_platform_postings Unique (jobPostId, platformId); status: pending\|posted\|failed\|manual_required.
JobScreeningConfig src/entities/JobScreeningConfig.entity.ts job_screening_configs Per-job (jobPostId) or global (isGlobal, belongsTo). Weights must sum to 100.
AiScreeningConfig src/entities/AiScreeningConfig.entity.ts ai_screening_configs Unique userId; autoScreenEnabled, autoScreenOnApply.
JobApplication src/entities/JobApplication.ts job_applications status: CandidateStatus; priority 1-5; resumeKey/resumeUrl.
CandidateQuizResponse src/entities/CandidateQuizResponse.ts candidate_quiz_responses Unique token; questions snapshot, answers, percentage, expiresAt.
ScreeningResult src/entities/ScreeningResult.entity.ts screening_results One-to-one-ish with application; screeningStatus, finalScore, screeningConfig snapshot.
Application src/entities/Application.ts Distinct legacy/placement application entity (used by placement scoring), only mentioned here.

Related sibling entities referenced but documented elsewhere: ResumeAnalysis (resume scoring), Platform / HrPlatformAccount (platform master + HR connections), QuizTemplate, EmailTemplate.


Architecture

flowchart TD
  subgraph FE["Clients"]
    PUB["Public hiring page"]
    HRUI["mr-hire-frontend (HR)"]
    ADMINUI["Admin UI"]
  end

  subgraph RT["Routes (mounted under /api)"]
    R1["jobPost.routes"]
    R2["jobTemplate.routes"]
    R3["jobApplication.routes (/api/admin)"]
    R4["mrHire.routes"]
    R5["candidateQuiz.routes (/api/candidate-quiz)"]
    R6["aiScreeningConfig.routes (/api/admin)"]
    R7["platform.routes"]
  end

  subgraph CTRL["Controllers"]
    C1["JobPostController"]
    C1b["JobScreeningConfigController"]
    C2["JobTemplateController"]
    C3["JobApplicationController"]
    C4["MrHireController"]
    C5["CandidateQuizController"]
    C6["AiScreeningConfigController"]
    C7["PlatformController"]
  end

  subgraph SVC["Services"]
    S1["JobPostService"]
    S2["JobTemplateService"]
    S3["JobApplicationService"]
    S4["PlatformService"]
    S5["S3Service"]
    S6["QueueService"]
  end

  subgraph DB["PostgreSQL (TypeORM)"]
    D1["job_posts"]
    D2["job_templates"]
    D3["job_applications"]
    D4["job_platform_postings"]
    D5["job_screening_configs"]
    D6["ai_screening_configs"]
    D7["candidate_quiz_responses"]
    D8["screening_results"]
  end

  subgraph EXT["External / async"]
    Q["resumeAnalysisQueue + emailQueue (BullMQ)"]
    PY["mr-hire-backend (Python, port 8001)"]
    S3B["AWS S3 (resumes)"]
  end

  PUB --> R4
  PUB --> R5
  HRUI --> R1 & R2 & R3 & R4 & R7
  ADMINUI --> R1 & R2 & R3 & R6 & R7

  R1 --> C1
  R1 --> C1b
  R2 --> C2
  R3 --> C3
  R4 --> C4
  R5 --> C5
  R6 --> C6
  R7 --> C7

  C1 --> S1 --> D1
  C1b --> D5
  C2 --> S2 --> D2
  C3 --> S3 --> D3
  C3 --> D8
  C4 --> S3
  C4 --> S6
  C4 --> PY
  C5 --> D7
  C5 --> D8
  C6 --> D6
  C7 --> S4 --> D4

  S3 --> S5 --> S3B
  S6 --> Q
  Q --> PY
  Q --> D8

Data model

erDiagram
  JOB_POST ||--o{ JOB_APPLICATION : "receives"
  JOB_POST ||--o{ JOB_PLATFORM_POSTING : "published to"
  JOB_POST ||--o| JOB_SCREENING_CONFIG : "configured by"
  JOB_TEMPLATE ||--o{ JOB_POST : "blueprint for"
  JOB_APPLICATION ||--o| SCREENING_RESULT : "scored by"
  JOB_APPLICATION ||--o{ CANDIDATE_QUIZ_RESPONSE : "quiz attempts"
  USER ||--o{ JOB_TEMPLATE : "creates"
  USER ||--o| AI_SCREENING_CONFIG : "owns"
  PLATFORM ||--o{ JOB_PLATFORM_POSTING : "target of"
  HR_PLATFORM_ACCOUNT ||--o{ JOB_PLATFORM_POSTING : "credential for"

  JOB_POST {
    uuid id PK
    string name
    string hiringLabel
    json roles
    json tools
    json domains
    boolean isActive
    string status "draft|active|paused|closed"
    string postedBy "HR email"
    uuid belongsTo "owner userId"
    string salaryBasis "fixed|ctc_based|negotiable"
    int headcount
  }

  JOB_TEMPLATE {
    uuid id PK
    string name
    string scope "global|private"
    uuid createdBy FK
    string quizAutoSet
    int quizQuestionCount
    string aiCallScript
  }

  JOB_PLATFORM_POSTING {
    uuid id PK
    uuid jobPostId FK
    string platformId FK
    uuid hrPlatformAccountId FK
    string method "automatic|api|manual"
    string status "pending|posted|failed|manual_required"
    int applicationsReceived
    string externalUrl
  }

  JOB_SCREENING_CONFIG {
    uuid id PK
    uuid jobPostId FK
    boolean isGlobal
    uuid belongsTo
    int weightResume
    int weightQuiz
    int weightVoiceInterview
    int autoShortlistThreshold
    int autoRejectThreshold
    boolean aiCallEnabled
    int aiCallMinimumScore
  }

  AI_SCREENING_CONFIG {
    uuid id PK
    uuid userId FK
    boolean autoScreenEnabled
    boolean autoScreenOnApply
  }

  JOB_APPLICATION {
    uuid id PK
    string fullName
    string email
    string phone
    string jobPostId
    string role
    string resumeKey
    string status "CandidateStatus"
    int priority "1-5"
    string source
  }

  CANDIDATE_QUIZ_RESPONSE {
    uuid id PK
    string token UK
    uuid applicationId
    string jobPostId
    uuid belongsToId
    json questions
    json answers
    int score
    decimal percentage
    boolean submitted
    timestamp expiresAt
  }

  SCREENING_RESULT {
    uuid id PK
    uuid applicationId FK
    float overallResumeScore
    float quizScore
    float overallCallScore
    float finalScore
    string finalRecommendation
    string screeningStatus
    json screeningConfig
  }

Notable enums / status fields

  • JobPost.status: draft | active | paused | closed (PortalType, JobType, SalaryBasis also defined in JobPost.ts).
  • JobApplication.status (CandidateStatus): resume_received | resume_parsed | outreach_sent | quiz_invited | quiz_completed | voice_interview_scheduled | voice_interview_completed | scoring_complete | shortlisted | talent_pool | rejected.
  • ScreeningResult.screeningStatus (ScreeningStatus): pending | resume_screening | resume_screened | quiz_sent | quiz_completed | call_scheduled | call_completed | completed | failed.
  • JobPlatformPosting.status (PostingStatus): pending | posted | failed | manual_required.

API surface

Paths below are the full paths including the mount prefix from src/routes/index.ts.

Job posts — jobPost.routes (mounted at /api)

Method Path Auth/role Purpose
GET /api/mr-hire/job-openings public List active job posts for the hiring page.
GET /api/mr-hire/job-openings/:id public Get a single job post.
GET /api/hr/job-posts auth (HR) HR's own posts (by belongsTo + legacy postedBy).
POST /api/hr/job-posts auth (HR) Create a post (drafts allow missing fields).
PUT /api/hr/job-posts/:id auth (HR) Update own post.
DELETE /api/hr/job-posts/:id auth (HR) Delete own post.
PATCH /api/hr/job-posts/:id/toggle auth (HR) Toggle isActive (syncs status active↔paused).
GET /api/hr/job-posts-counts auth (HR) Applicant + shortlisted counts per job.
GET /api/hr/screening-config/global auth (HR) Get HR's global screening config (cascade).
PUT /api/hr/screening-config/global auth (HR) Upsert HR's global screening config.
GET /api/hr/job-posts/:jobPostId/screening-config auth (HR) Get per-job config (job→global→default cascade).
PUT /api/hr/job-posts/:jobPostId/screening-config auth (HR) Upsert per-job config.
GET /api/admin/job-posts admin All posts.
POST /api/admin/job-posts admin Create post (admin).
POST /api/admin/job-posts/import admin Bulk import posts.
PUT /api/admin/job-posts/:id admin Update any post.
DELETE /api/admin/job-posts/:id admin Delete any post.
PATCH /api/admin/job-posts/:id/toggle admin Toggle any post.

Job templates — jobTemplate.routes (mounted at /api)

Method Path Auth/role Purpose
GET /api/hr/job-templates auth (HR) Global + own private templates.
POST /api/hr/job-templates auth (HR) Create a private template.
PUT /api/hr/job-templates/:id auth (HR) Update own (cannot promote to global).
DELETE /api/hr/job-templates/:id auth (HR) Delete own private template.
GET /api/admin/job-templates admin All templates.
POST /api/admin/job-templates admin Create a global template.
PUT /api/admin/job-templates/:id admin Update any template.
DELETE /api/admin/job-templates/:id admin Delete any template.

Applications / pipeline — jobApplication.routes (mounted at /api/admin)

Candidate/Kanban endpoints require authMiddleware only (HR uses them); the /job-applications/* group additionally requires adminMiddleware.

Method Path Auth/role Purpose
GET /api/admin/candidates auth Paginated candidates (search/status/job/belongsTo filters) with resume analysis + screening result.
GET /api/admin/candidates/recent auth Recent candidates with final score.
GET /api/admin/candidates/top auth Top candidates by finalScore.
GET /api/admin/candidates/counts-by-status auth Pipeline counts grouped by status (scoped by belongsTo).
PATCH /api/admin/candidates/bulk-status auth Bulk status update.
GET /api/admin/candidates/:id auth Single candidate (UUID validated) with analysis + screening.
PATCH /api/admin/candidates/:id/status auth Move candidate stage + send status-change email.
PATCH /api/admin/candidates/:id/manual-scores auth Set manual resume/quiz/call scores, recompute final score.
PATCH /api/admin/candidates/:id/clear-stage auth Clear stage data on backward Kanban move.
PATCH /api/admin/candidates/:id/priority auth Set priority 1-5.
GET /api/admin/candidates/:id/resume auth Fresh pre-signed resume URL.
GET /api/admin/candidates/:id/analysis auth Resume analysis for candidate.
GET /api/admin/job-applications admin All applications (paginated).
GET /api/admin/job-applications/export admin Export (CSV-shaped JSON).
GET /api/admin/job-applications/:id admin Single application.
GET /api/admin/job-applications/by-job/:jobPostId admin Applications for a job.
GET /api/admin/job-applications/stats/summary admin Stats by job/source.
DELETE /api/admin/job-applications/:id admin Delete application.
GET /api/admin/job-applications/:id/resume admin Pre-signed resume URL.

Mr. Hire apply + HR tools — mrHire.routes (mounted at /api)

Method Path Auth/role Purpose
GET /api/mr-hire/check-applied/:jobPostId auth Has the current user applied (and to which roles).
POST /api/mr-hire/apply public, multipart (resume) Direct candidate apply (creates application + uploads resume).
POST /api/mr-hire/send-mas101-email public Send MAS101 info email.
POST /api/mr-hire/import-from-drive HR Import resumes from Google Drive → create applications.
POST /api/mr-hire/parse-resumes HR Parse Drive resumes (preview only, proxied to Python).
POST /api/mr-hire/bulk-apply HR Bulk-create applications from parsed data.
GET /api/mr-hire/calls public proxy List AI calls (proxied to Python).
GET /api/mr-hire/call/:callId public proxy Call details (proxied).
POST /api/mr-hire/call HR Trigger AI call (proxied).
POST /api/mr-hire/generate-jd HR AI job-description generation (proxied to /api/v1/generate-jd).
GET/PUT/POST /api/mr-hire/interview-prompt[...] HR Get/update/reset interview system prompt.
POST /api/mr-hire/quiz/curate HR Curate quiz from JD + resume (proxied to /api/v1/quiz/curate), saves to ScreeningResult.
POST /api/mr-hire/quiz/curate-for-template HR Curate a quiz for a template (only needs JD + count).
POST /api/mr-hire/quiz/send-email HR Generate signed quiz token + queue invite email.
GET/POST/PUT/DELETE /api/mr-hire/quiz/templates[...] HR Quiz template CRUD + roles list.
GET/POST/PUT/DELETE /api/mr-hire/call-prompts[...] HR AI call prompt CRUD + active prompt.

Candidate quiz — candidateQuiz.routes (mounted at /api/candidate-quiz)

Method Path Auth/role Purpose
GET /api/candidate-quiz/take/:token public (token IS auth) Get quiz questions (answers stripped).
POST /api/candidate-quiz/take/:token/submit public (token IS auth) Submit answers, auto-score, advance pipeline.
GET /api/candidate-quiz/admin/quiz-results/:applicationId auth HR fetch submitted quiz + score.

AI screening config — aiScreeningConfig.routes (mounted at /api/admin)

Method Path Auth/role Purpose
GET /api/admin/mr-hire/ai-screening-configs admin List per-user auto-screen configs.
POST /api/admin/mr-hire/ai-screening-config admin Upsert per-user config.

Multi-platform posting — platform.routes (mounted at /api)

Method Path Auth/role Purpose
GET /api/careers/google-jobs/:id public Google for Jobs JSON-LD for a post.
GET /api/careers/google-jobs public JSON-LD for all active posts.
GET /api/careers/indeed-feed.xml public Indeed XML feed.
GET /api/hr/platforms auth Active platforms.
GET/POST/PUT/DELETE /api/hr/platform-accounts[...] auth HR's connected platform accounts.
POST /api/hr/job-posts/:id/publish auth Publish a job to selected platforms (body platformIds).
GET /api/hr/job-posts/:id/platforms auth Postings for a job.
PATCH /api/hr/job-posts/:id/platforms/:platformId auth Mark a manual posting as posted.
GET/POST/PUT/PATCH /api/admin/platforms[...] admin Master platform list management.

Placement / score upload (adjacent)

Method Path Auth/role Purpose
GET /api/placement-score/student/:studentId auth Student placement-readiness score.
GET /api/placement-score/batch/:batchId auth Batch placement scores.
POST /api/placement-score/students/batch auth Scores for many students.
GET /api/placement-score/weights auth Placement weight reference.
POST /api/batchlead/upload-score (see batchLead.routes) UploadScoreController.uploadScores — bulk score upload.

User journeys

1. HR creates a job post from a template and publishes to platforms

HR picks a template (global or their own), the frontend pre-fills the create form, the post is saved (owned by belongsTo = req.user.id), and then HR publishes to one or more platforms. Note the frontend copies template fields into the create payload — the backend JobPostService.create does not read the template itself.

sequenceDiagram
  participant HR as HR Frontend
  participant API as Backend API
  participant TS as JobTemplateService
  participant PS as JobPostService
  participant PLS as PlatformService
  participant DB as PostgreSQL

  HR->>API: GET /api/hr/job-templates
  API->>TS: getForUser userId
  TS->>DB: select global plus own private
  DB-->>TS: templates
  TS-->>API: templates
  API-->>HR: template list

  Note over HR: HR selects a template and edits the form
  HR->>API: POST /api/hr/job-posts with filled fields
  API->>PS: create dto with belongsTo and postedBy
  PS->>DB: insert job_posts
  DB-->>PS: saved post
  Note over API: if about text is long it records a JD generation cost
  PS-->>API: saved post
  API-->>HR: 201 created post

  HR->>API: POST /api/hr/job-posts/:id/publish with platformIds
  API->>PLS: publishToPlaftorms jobPostId userId platformIds
  loop each platform
    PLS->>DB: upsert job_platform_postings
    Note over PLS: automatic platform set status posted, api or oauth set manual_required
  end
  PLS-->>API: postings
  API-->>HR: 200 postings with statuses

2. Candidate applies via the public hiring form

The public apply endpoint validates fields (relaxed for HR uploads), uploads the resume to S3, creates the JobApplication, and conditionally queues resume analysis based on the global auto-screen flag or a special source.

sequenceDiagram
  participant C as Candidate Browser
  participant API as Backend API
  participant JAS as JobApplicationService
  participant S3 as S3Service
  participant SC as SystemConfigService
  participant Q as resumeAnalysisQueue

  C->>API: POST /api/mr-hire/apply multipart with resume
  Note over API: multer accepts pdf doc docx up to 5MB
  API->>API: validate required fields and email format
  API->>JAS: createApplication dto
  JAS->>JAS: hasAppliedForRole check
  alt already applied for this role
    JAS-->>API: throw already applied
    API-->>C: 400 already applied for this role
  else new application
    JAS->>S3: uploadJobApplicationResume buffer
    S3-->>JAS: s3Key and url
    JAS-->>API: saved application
    API->>SC: getValue global_auto_screen_on_apply
    SC-->>API: flag value
    alt auto screen enabled or source is external hr form
      API->>Q: addResumeAnalysisJob resume-analysis with priority
      Note over Q: AI parsing and scoring handled by resume screening worker
    else auto screen off
      Note over API: no job queued
    end
    API-->>C: 201 application submitted
  end

3. HR sends a screening quiz, candidate takes it, ScreeningResult updates

HR curates a quiz (questions are saved onto the ScreeningResult), then sends it. Sending invalidates old quiz links, generates a fresh signed JWT and a CandidateQuizResponse record, and queues the invite email. The candidate opens the link, answers, and submission auto-scores and advances the pipeline.

sequenceDiagram
  participant HR as HR Frontend
  participant API as Backend API
  participant PY as Python mr-hire-backend
  participant SR as screening_results
  participant CQ as candidate_quiz_responses
  participant Q as emailQueue
  participant C as Candidate

  HR->>API: POST /api/mr-hire/quiz/curate with jd and resume
  API->>PY: POST /api/v1/quiz/curate
  PY-->>API: quiz questions
  API->>SR: save quizQuestions onto screening result
  API-->>HR: curated quiz

  HR->>API: POST /api/mr-hire/quiz/send-email
  API->>SR: load screening result and quizQuestions
  API->>CQ: expire existing responses for application
  API->>CQ: create response then sign JWT with responseId
  API->>SR: set quizLink and screeningStatus quiz_sent
  API->>API: set application status quiz_invited
  API->>Q: addEmailJob quiz-invitation with quizLink
  API-->>HR: 200 quizUrl and expiresAt

  C->>API: GET /api/candidate-quiz/take/:token
  API->>API: verify JWT and check expiry and submitted
  API->>CQ: load questions
  Note over API: correct answers stripped before sending
  API-->>C: questions without answers

  C->>API: POST /api/candidate-quiz/take/:token/submit with answers
  API->>API: score answers and compute percentage
  API->>CQ: save answers score and submitted true
  API->>SR: set quizScore and quizCompletedAt and quiz_completed
  API->>API: advance application to quiz_completed unless later stage
  alt aiCallEnabled and percentage above call minimum and phone present
    API->>Q: addResumeAnalysisJob ai-call delayed
  else no call
    API->>API: compute final score from resume plus quiz
    API->>API: auto shortlist or reject or scoring_complete
  end
  API-->>C: 200 score and percentage

4. Application moves through the pipeline (Kanban)

HR drags a candidate between stages. Forward moves can set status and trigger templated emails; backward moves clear downstream stage data and reset the screening result.

sequenceDiagram
  participant HR as HR Frontend
  participant API as Backend API
  participant AR as job_applications
  participant SR as screening_results
  participant ET as email_templates
  participant Q as emailQueue

  HR->>API: PATCH /api/admin/candidates/:id/status with status
  API->>AR: update status
  API->>ET: find template by trigger and owner or global
  alt template found or default exists for shortlisted or rejected
    API->>Q: addEmailJob workflow-email with rendered subject and body
    Note over API: email failures are non blocking
  else no email mapping
    Note over API: no email sent
  end
  API-->>HR: 200 updated candidate

  Note over HR: backward move triggers clear-stage
  HR->>API: PATCH /api/admin/candidates/:id/clear-stage with targetStageId
  API->>SR: clear call quiz or resume fields by stage index
  API->>SR: reset finalScore and screeningStatus pending
  API-->>HR: 200 stage data cleared

  Note over HR: manual scores for skipped stages
  HR->>API: PATCH /api/admin/candidates/:id/manual-scores
  API->>SR: apply provided scores and load weights from config
  API->>SR: recompute final score with weight normalization
  API-->>HR: 200 finalScore and recommendation

5. HR bulk-imports candidates from Google Drive

sequenceDiagram
  participant HR as HR Frontend
  participant API as Backend API
  participant GD as Google Drive
  participant JAS as JobApplicationService
  participant S3 as S3Service
  participant Q as resumeAnalysisQueue

  HR->>API: POST /api/mr-hire/import-from-drive
  Note over API: requires HR or admin role
  API->>GD: download resume files
  GD-->>API: file buffers
  loop each resume
    API->>JAS: createApplication with resume buffer
    JAS->>S3: upload resume
    JAS-->>API: saved application
    API->>Q: addResumeAnalysisJob resume-analysis
  end
  API-->>HR: 200 imported summary

6. Admin configures AI auto-screening and screening thresholds

sequenceDiagram
  participant ADM as Admin UI
  participant API as Backend API
  participant ASC as ai_screening_configs
  participant JSC as job_screening_configs

  ADM->>API: POST /api/admin/mr-hire/ai-screening-config with userId and flags
  API->>ASC: upsert by userId
  API-->>ADM: saved config

  ADM->>API: PUT /api/hr/screening-config/global with weights and thresholds
  API->>API: validate weights sum to 100
  alt weights invalid
    API-->>ADM: 400 weights must sum to 100
  else valid
    API->>JSC: upsert global config for owner
    API-->>ADM: saved config
  end

Placement score (a separate concept)

The PlacementScoreController / PlacementScoreService compute a student placement-readiness score (weighted blend of evaluation tests 15%, learn progress 15%, super-mentor 30%, communication 15%, attendance 15%, mock interviews 10% — see PLACEMENT_WEIGHTS in src/services/PlacementScoreService.ts). This is a placement/LMS concept and is not the same as the Mr. Hire candidate finalScore in ScreeningResult. UploadScoreController.uploadScores (POST /api/batchlead/upload-score) ingests external test scores used by this computation. These are included here only because they were in scope; see the placement/LMS docs for the full picture.


Background jobs & async

  • resumeAnalysisQueue (BullMQ, worker src/workers/resumeAnalysis.worker.ts): processes resume-analysis / full-screening jobs (resume parse + score, quiz generation) and ai-call jobs (voice interview). Enqueued by applyDirect, importFromDrive, bulkApply, and the quiz submit flow (delayed AI call). priority maps from JobApplication.priority (lower = higher priority). The quiz submit handler also imports calculateFinalScore and addToTalentPoolIfEligible from this worker module to compute final scores inline when no AI call is scheduled.
  • emailQueue (via QueueService.addEmailJob): quiz invitations (quiz-invitation), pipeline status-change emails (workflow-email), MAS101 info email.
  • Quiz token expiry / re-send: CandidateQuizResponse.expiresAt plus the signed JWT expiresIn (default 7 days). Re-sending a quiz immediately expires all prior responses for the application and resets ScreeningResult.quizScore/quizCompletedAt, so old links return HTTP 410.
  • AI call gap: MR_HIRE_CALL_GAP (minutes, default 1) delays the queued ai-call job after a passing quiz.
  • No Socket.IO events or inbound webhooks are owned by this domain (call/result webhooks land in the voice-interview / Python subsystem).

External integrations

Integration Where Env / config Failure behavior
Python mr-hire-backend MrHireController proxy methods MR_HIRE_BACKEND_URL (default http://localhost:8001) Proxied responses pass upstream status through; quiz curation persists questions only when applicationId given.
AWS S3 S3Service.uploadJobApplicationResume, getJobApplicationResumeSignedUrl AWS_* / bucket env Resume upload failure is caught — application is still saved without a resume. Pre-signed URLs valid ~1 hour.
Quiz token signing candidateQuiz.controller.ts QUIZ_TOKEN_SECRET or JWT_SECRET (HS256). Startup throws if neither set. Invalid token → 400; expired → 410.
Quiz frontend URL generateQuizToken MR_HIRE_FRONTEND_URL (default http://localhost:5173) Builds /quiz/take/:token.
Google Drive importFromDrive, parseResumes Drive connector / Google OAuth Per-file errors are collected; import continues.
Auto-screen flag applyDirect SystemConfig key global_auto_screen_on_apply and AiScreeningConfig When off and source is not a special HR form, no screening job is queued.
OpenAI (cost ledger) JobPostController.create records JD-generation cost PipelineCostService / PipelineCostLedger Cost recording failures are warnings only.

Status lifecycles

JobApplication.status (candidate pipeline)

stateDiagram-v2
  [*] --> resume_received
  resume_received --> resume_parsed
  resume_parsed --> outreach_sent
  outreach_sent --> quiz_invited
  resume_parsed --> quiz_invited
  quiz_invited --> quiz_completed
  quiz_completed --> voice_interview_scheduled
  voice_interview_scheduled --> voice_interview_completed
  voice_interview_completed --> scoring_complete
  quiz_completed --> scoring_complete
  scoring_complete --> shortlisted
  scoring_complete --> rejected
  scoring_complete --> talent_pool
  shortlisted --> [*]
  rejected --> [*]
  talent_pool --> [*]

Note: the quiz-submit handler only advances to quiz_completed if the application is not already in a later stage, then auto-routes to shortlisted / rejected / scoring_complete based on thresholds, or to voice_interview_scheduled indirectly by queueing the AI call.

ScreeningResult.screeningStatus

stateDiagram-v2
  [*] --> pending
  pending --> resume_screening
  resume_screening --> resume_screened
  resume_screened --> quiz_sent
  pending --> quiz_sent
  quiz_sent --> quiz_completed
  quiz_completed --> call_scheduled
  call_scheduled --> call_completed
  call_completed --> completed
  quiz_completed --> completed
  resume_screening --> failed
  call_scheduled --> failed
  failed --> [*]
  completed --> [*]

JobPost.status

stateDiagram-v2
  [*] --> draft
  draft --> active
  active --> paused
  paused --> active
  active --> closed
  paused --> closed
  closed --> [*]

toggleActive flips isActive and keeps status in sync (activepaused); closed is set via explicit update.

JobPlatformPosting.status

stateDiagram-v2
  [*] --> pending
  pending --> posted
  pending --> manual_required
  pending --> failed
  manual_required --> posted
  failed --> posted
  posted --> [*]

Edge cases, limits & gotchas

  • Auth split: candidate/Kanban endpoints under /api/admin/candidates/* require only authMiddleware (so HR can use them) while /api/admin/job-applications/* additionally require adminMiddleware. Despite the /api/admin mount, the candidate group is not admin-gated.
  • HR role: Mr. Hire HR tools require hrRoleMiddleware (EXTERNAL_HR | ADMIN). SUPERADMIN is not in the HR-allowed set, only ADMIN.
  • Ownership scoping: candidate lists scope by belongsTo via an inner join jp.id = app."jobPostId"::uuid — applications with no jobPostId are excluded from belongsTo filters and from counts-by-status.
  • Templates are not server-applied: creating a post from a template is a frontend copy; JobPostService.create never reads JobTemplate. HR cannot promote a private template to global (scope/createdBy are stripped on HR update).
  • Screening config cascade: per-job → user-global (isGlobal && belongsTo) → system-global (isGlobal && belongsTo IS NULL) → HARDCODED_DEFAULTS. Weights must sum to 100 or the upsert returns 400. A snapshot of the resolved config is stored on each ScreeningResult.screeningConfig.
  • Quiz idempotency / re-send: re-sending a quiz expires every prior CandidateQuizResponse for the application; opening a stale link returns 410 with a "link has been replaced" message if a newer non-expired record exists. Submission rejects if submitted already true or expired.
  • Quiz scoring: supports MCQ (letter↔option-text resolution), multi-select (sorted, normalized), and coding questions (scored by stored {passed, totalTestCases} ratio). Correct answers are stripped before questions are sent to candidates.
  • Resume upload resilience: an S3 upload failure does not fail the application — it is saved without a resume, which also means no screening job will be queued (application.resumeKey falsy).
  • Legacy source typo: the special auto-screen source string is exterenal-hr-hiring-form (intentional typo kept for compatibility); default form source is mr-hire-hiring-form.
  • Duplicate apply: enforced per (email, jobPostId, role); a duplicate returns 400 "already applied for this role".
  • TypeORM auto-sync is on in dev — these entities create/update tables automatically; the job_applications.jobPostId is a plain varchar (not a FK), hence the ::uuid casts in joins.
  • Placement score ≠ candidate final score: see the Placement score note — different entity (Application), different math (PLACEMENT_WEIGHTS).