# Recruitment — the HR module's first piece

Status: **phases 1–3 built 2026-10-28** (backend, admin pages, public careers pages);
phase 4 (import from Recruit) next, after it's tested live. Replaces the separate Recruit app
(`D:\DevOps\iShapps-Recruit`, live at recruit.ishapps.com.au), which ran its own
database, admin users and Sanctum auth, and created HCPs by calling
`POST /api/v2/hcps` over HTTP with a token pinned to one agency.

Owner module: **Human Resource Management** (`hrm`). Depends on core (it creates
HCPs through `HcpOnboardingService`); core never depends on it — the HCP profile
asks `/recruitment/applications?hcp_id=` rather than `HcpResource` knowing about
applications.

## Flow

1. HR writes a **job posting** (draft → published → closed), optionally naming the
   profession/level it hires for, and its **questions** (plus global ones shared by
   every posting).
2. Applicants find it on the agency's public **careers page**,
   `/careers/{org_code}`, and apply: name, email, phone, CV, answers.
   → `recruitment.application-received` to the applicant,
   `admin.recruitment-application` to the agency (activity emails + Teams).
3. HR reviews: `pending` → `reviewing` (opened) → `shortlisted` → `qualified` or
   `rejected`. Rejecting is possible at any stage before the decision; qualifying only
   from `shortlisted`.
4. **Shortlist** (added 2026-10-29) → `recruitment.application-shortlisted`: invited to
   an interview, with HR's message (time, place, what to bring) or a generic "we'll be in
   touch to arrange it". `shortlisted_by/at` and `shortlist_message` stay after the
   decision, so the Shortlisted Candidates page shows how each one ended.
5. **Reject** → `recruitment.application-rejected` (final), carrying HR's message, or a
   generic one when none was given — worded for after the interview when they were
   shortlisted.
6. **Qualify** (after the interview) → `recruitment.application-qualified` ("Congratulations"), then the
   HCP account is created by `HcpOnboardingService::createHcp()` — whose
   `hcp.welcome` email (sign-in details) follows. One transaction; notifications are
   dispatched after commit, in that order. The CV is copied to the HCP's CV, the
   application keeps `hcp_id`.
   - Email already an HCP → the application is linked to that HCP; no new account.
   - Email belongs to a staff account → refused.
   - Qualify / reject happen once: a decided application can't be decided again.

## Data (tenant DB)

| Table | Notes |
|---|---|
| `job_postings` | title, unique slug, department, employment_type, location, profession/category/level (optional), description, requirements, status, closes_at, published_at; soft-deleted |
| `job_posting_questions` | `job_posting_id` null = asked on every posting; type text/textarea/select/yesno; options; required; sort_order |
| `job_applications` | applicant details, CV (`recruitment_cvs` folder), `answers` as a snapshot `[{question_id, question, answer}]` so editing a question never rewrites history, status, rejection_reason, shortlisted_by/at, shortlist_message, reviewed_by/at (the final decision), hcp_id, cv_purged_at |

## Public careers API (central routes, tenant from the path)

`/api/v2/careers/{orgCode}/…` — `InitializeTenancyByOrgCode` resolves the agency from
its `org_code` (the human-facing code, never the routing UUID elsewhere). The SPA
serves the pages at `/careers/{orgCode}`. Rate limited; Cloudflare Turnstile and a
honeypot field guard the apply form (Turnstile is enforced once its secret is set).

## SPA

- Admin: sidebar section **Human Resource Management** (`module: "hrm"`, after Workforce;
  recruit first, then they are HCPs) → **Recruitment** dropdown, one independent page per
  item: Job Postings `/admin/recruitment/postings`, Job Applications
  `/admin/recruitment/applications` (status tabs, job filter), Shortlisted Candidates
  `/admin/recruitment/shortlisted` (everyone who reached interview: awaiting decision /
  qualified / rejected), Configuration
  `/admin/recruitment/configuration` (global application questions; geolocation and other
  posting defaults later, as their own sections) — plus `/admin/recruitment/applications/:id`
  (CV preview, answers, Shortlist / Reject, then Qualify / Reject) and `/admin/recruitment/postings/:id|new` (details,
  hiring-for profession, the posting's own questions, publish/close). Opening a new
  application marks it Reviewing.
  The HCP profile shows a "Recruited via" card (asked by `hcp_id`).
- Public: `/careers/:orgCode` and `/careers/:orgCode/jobs/:slug`, using their own axios
  client (no admin token or `X-Tenant-Code`, no login redirect). Turnstile shows when the
  agency endpoint says it's enforced **and** `VITE_TURNSTILE_SITE_KEY` is set — set both
  keys together, or the form can't pass the server check.

## Permissions

`view-recruitment`, `manage-job-postings`, `review-applications` (module
`recruitment`). Added to existing tenants by a migration (additive — the
roles seeder's `syncPermissions` would reset customised roles), and to the seeder
for new tenants.

## Privacy

Applicants' CVs are personal information (Australian Privacy Principles). The
monthly `recruitment:purge-cvs` deletes the CVs of rejected applications decided
more than `RETENTION_MONTHS` (12) ago; the application row stays for the record.

## Phases

| Phase | Scope |
|---|---|
| 1 | Backend: tables, services, permissions, emails, admin + public APIs, qualify → HCP |
| 2 | SPA admin pages in the HR section: Job Postings (with questions), Applications |
| 3 | Public careers pages `/careers/{orgCode}` + Turnstile |
| 4 | Import from Recruit's production DB + CVs into Smart Healthcare's tenant; redirect recruit.ishapps.com.au |
| 5 | Interviews, reference checks, policy sign-off |

## Phase 4 — importing Recruit's data (Smart Healthcare)

Recruit's data is small (10 postings, 14 questions, 552 applications, 2026-05 → 2026-10), so it
goes in as one generated SQL file plus a CV upload — no live link between the databases.

1. The SQL (generated from Recruit's dump, kept outside the repo — it holds applicants' personal
   data) inserts postings, questions and applications with Recruit's ids, `source =
   'recruit-import'`, answers converted to the snapshot shape, and `cv` = Recruit's filename. It
   runs in one transaction and refuses unless the recruitment tables are empty. Reviewers map to
   the iShapps user with the Recruit admin's email; qualified applications link to the HCP with
   the applicant's email (else the HCP id Recruit recorded). Onboarding invitations and
   Recruit's admin accounts aren't imported.
2. `recruitment:import-cvs --dir=<copy of Recruit's private cvs/ folder>` (via `tenants:run`)
   uploads each CV into `recruitment_cvs/{id}/` through tenant storage. Re-runnable.
