# Pre-Qualifier — Technical Specification (handoff to engineering)

> **Status:** Prototype-backed spec for team review. NOT a build order yet.
> **Source:** `/pm-run-workflow` OV-951 · Epic `[EP-VC] Pre-Qualifier` · OV Student Portal
> **Companion artifact:** the clickable prototype in this folder (`index.html`)
> **Mode:** EXPLORATION — decisions below are the product intent; items marked
> **⟨OPEN⟩** are explicitly deferred to engineering judgement + a follow-up DEFINE.
> **Destination market for v1:** Australia (Student visa subclass 500, GTE/Genuine Student framing).

---

## 1. What this is (one paragraph)

The Pre-Qualifier is a **light version of the Student Portal** that lets a
**school** assess the **visa-profile strength** of a prospective student
**before issuing the COE**. It is also OneVisa's **lead magnet**: schools
require their partner agencies to pre-qualify students through OneVisa,
which puts agencies inside a OneVisa surface — one step away from becoming
visa clients. The student fills a light profile and **never sees a verdict**;
the **strength report** is shown only to the **agency** and the **school**,
who decides to approve (upload the COE) or decline (with a note).

---

## 2. Strategic frame (why it's built this way — keep in mind for trade-offs)

- **Acquisition is always sales-led.** OneVisa closes the school; ops/superadmin
  provisions it. No school self-serve in v1.
- **Schools become a referral channel.** They bring their agencies in for free.
- **The locked "Visas" tab is the conversion engine.** Agencies experience the
  pre-qualifier free; the visa product is visible-but-locked → "Book a demo" →
  sales → pilot → unlock.
- **Pre-qualifications are free/unlimited.** Credits exist **only** on the visa
  side. Do not gate pre-qualification behind credits.
- **Sophistication of the report is a sales asset.** It must read like an expert
  assessment, not a black-box score — that is what earns school trust and
  makes agencies want the full product.

---

## 3. Entities & data model

### 3.1 Organization (reuse existing)
`organizations.client_type` already supports `'agency' | 'school'` (DB check
constraint exists). `url_slug` (unique) and the `themes` / `theme_properties`
tables already exist. **Schools are not a new entity** — they are an
organization with `client_type='school'`.

### 3.2 Agency is a GLOBAL entity
An agency exists **once** across the whole platform. It may be:
- created by a school's import,
- created by a OneVisa self-signup (future),
- or already an existing OneVisa **client** (onboarded by us).

### 3.3 School ↔ Agency is a MANY-TO-MANY ASSOCIATION
A new association object links a school to an agency. The **same agency** can be
associated with **many schools**, and a school has **many agencies**. The
association — not the agency — is what an import creates.

```
school (org, client_type=school)
   └──< school_agency_association >──┐
                                     ├── agency (org, client_type=agency)  [GLOBAL]
student_prequalification ────────────┘
   ├── student (the person filling the form)
   ├── agency_id        (which agency this routes results to)
   ├── school_id        (which school decides)
   ├── status           (see §6)
   ├── report           (see §7)
   ├── coe_document      (on approve)
   └── decision_note / decision_reason (on decide)
```

**✅ DECIDED (Aug-2026) — the assessment is a SEPARATE OBJECT, not an application type.**

It is NOT `application.type='assessment'`. It is its own entity (`assessment`),
with its own table, its own status enum, its own domain events and its own
analytics (PostHog) object/event namespace. Rationale:

1. **It isn't an application.** Different lifecycle, different visibility rules,
   no credits, no government submission.
2. **No application workflow may fire for it.** Overloading `application.type`
   means every existing subscriber keyed on application events (document
   pipeline, expert-review orchestration, submission automation, application
   lifecycle notifications, billing) would have to be audited and guarded.
   A separate object gives isolation *by construction*.
3. **Clean data & analytics.** Assessment funnels never blend with visa funnels.
4. **One student, two objects, one login.** The join point is the `student`
   (single Clerk identity), NOT the application — a student can hold N
   assessments and N applications simultaneously.

**The only coupling is a one-way conversion service**: it reads an approved
assessment and **creates** a new, pre-filled visa application for the same
student. The assessment row is never mutated into an application — it survives
as the source record for audit, analytics and the school's history.

Consequences engineering should note:
- `application` needs **no new column**; the FK (`converted_application_id`)
  lives on the assessment side, so the visa domain need not know assessments exist.
- The application enters the standard state machine at its **normal starting
  state** — no special-casing downstream.
- The portal must resolve "what am I working on?" across object types (a student
  home listing both) — a real new surface, not a detail.
- Invitations must resolve to an **existing student** when the email is already
  known, to avoid duplicate identities across object types.

---

## 4. The agency identity & import logic (the part to get exactly right)

This is the most intricate piece. When a school imports its agency list (CSV
now, CRM integrations later — **⟨OPEN⟩ which CRMs**), the system must
**match-and-link, never blindly create or re-invite**.

### 4.1 Matching rule
For each row in the import, match against existing global agencies by
**contact email first, then email domain** (normalised, case-insensitive).
**⟨OPEN⟩** confirm the match key and fuzzy-match tolerance with engineering;
domain-only matching risks false positives for large shared domains
(gmail.com etc.) — domain matching should apply to **corporate domains only**,
fall back to exact email otherwise.

### 4.2 The three outcomes per imported row

| Case | Condition | System action | Agency experience |
|------|-----------|---------------|-------------------|
| **A — Already on OneVisa (link)** | Match found, agency exists | Create the school↔agency association only. **No new invite. No onboarding.** Notify the agency they were added by this school. | Sees a new pre-qualification relationship in the admin they already use. If they're a **client (Tier 2)**, pre-quals appear alongside their visa work. |
| **B — New agency (invite)** | No match | Create the global agency (Tier 0/1) + the association + send the **pre-qualification-specific welcome email** (a NEW template, not the standard OneVisa welcome). | Receives the welcome email → magic-link results or account creation. |
| **C — Duplicate within the same import** | Two rows resolve to the same agency | Collapse to one association. | n/a |

The import **preview screen is mandatory** before commit, and must show the
summary counts and per-row treatment (see prototype `school-agencies.html`):
> "12 imported · 3 already on OneVisa → linked · 9 new → welcome email"

### 4.3 Overlap across schools (the expected steady state)
School 1 and School 2 will both import overlapping agencies. The system must
**reuse the same global agency** and add a second association — it must
**never** create a duplicate agency or re-onboard. This is by design: overlap
is the growth mechanism (see §5).

### 4.4 Unrecognised agency from the student side
On the public-URL path (**optional**, see §9), the student picks their agency
from the school's list. Fallback option "My agency isn't listed" captures
**agency name + contact email** as a free-text, **unrecognised** agency. The
school admin can then one-click add it to their list (which runs it through
§4.2 again). This quietly grows the funnel with agencies the school didn't
even import. **⟨OPEN⟩** abuse/spam controls on free-text agency capture.

---

## 5. The three-tier agency ladder (drives UI state + conversion)

A given agency is always in exactly one tier **per its global account state**
(not per school):

| Tier | Who | Access | Pre-qual results delivery | Visas tab |
|------|-----|--------|---------------------------|-----------|
| **Tier 0 — Magic-link participant** | School imported them, no account | None (no login) | **Magic link** email per result (**optional feature**, see §9) | n/a |
| **Tier 1 — Pre-qualifier account** | Signed up (often after 2+ schools added them) | Free account | In-admin "Pre-qualifications" tab, grouped by school | **Locked** — pitch + Book a demo |
| **Tier 2 — Full OneVisa client** | Already onboarded by us / converted | Paid client | Pre-quals appear **inside their existing admin** | **Unlocked** |

**Graduation triggers (lead-magnet funnel):**
- Tier 0 → Tier 1: agency added by **2+ schools** (or self-signs-up) → "manage
  them in one place" account-creation prompt.
- Tier 1 → Tier 2: opens locked Visas tab → **Book a demo** → sales → **pilot
  activated** (manual, sales-led) → Visas unlocks + pilot credits loaded.

---

## 6. Status model (state machine)

One status field per pre-qualification, four states; **Approved/Rejected are a
single `Decided` state with an `outcome` attribute**, not two states.

```
Invited ──> In Progress ──> Pending School Decision ──> Decided
                                                          ├─ outcome = Approved (+ COE doc, optional note)
                                                          └─ outcome = Rejected (+ reason, brief note)
```

- **Invited** — link created / student invited, not yet registered. (Maps to
  today's "Student Invited".)
- **In Progress** — student registered, filling the light form. (Maps to
  "Student in progress".)
- **Pending School Decision** — info complete, **analysis generated instantly**
  (no human-in-the-loop, no "Analyzing" state), sitting in the school's queue.
- **Decided** — `Approved` (COE uploaded) or `Rejected` (reason + note).

**Role-relative labels (same state, different reader):**
- School sees state 3 as **"Awaiting your decision"**.
- Agency sees state 3 as **"Pending school decision"**.
- One status value, reader-relative copy at the presentation layer.

**The student is NEVER notified of the outcome by OneVisa.** Consistent with
the success-page promise ("your agency and the school will contact you").

**⟨OPEN⟩ — re-submission after rejection:** can an agency submit a corrected
pre-qualification for the same student after a Decline? Product instinct: yes
(sales-positive), as a new pre-qualification linked to the prior one. Decide
in DEFINE.

---

## 7. The strength report (the showpiece)

Generated **instantly and automatically** on form completion. Shown to **agency
and school only — never the student**.

### 7.1 Structure
- **Overall band:** `Strong | Moderate | Weak` — always rendered as
  **colour + icon + label** (never colour alone — accessibility + print).
- **5 dimensions**, each with its own band + a **one-line evidence subtext**
  citing the actual data point:
  1. **Financial capacity** — declared funds vs required (tuition + living +
     travel), seasoning, source.
  2. **Academic & employment history** — progression, gaps, relevance to course.
  3. **Ties to home country / Genuine Student** — family, employment, property,
     return incentive.
  4. **English level** — test scores/evidence vs course requirement.
  5. **Immigration history** — prior visas, refusals, overstays.
- **2–4 risk flags** — specific, actionable, each tied to a data point
  (e.g. "Funds gap: AUD ~5,600 short of guideline").
- **Methodology footnote** — states it's advisory, assessed against GS criteria
  + Dept. of Home Affairs subclass-500 financial guidance. **Not a visa decision.**
- **Tone:** analyst-neutral. Sophistication = trust = sales.

### 7.2 What the SCHOOL sees vs the AGENCY sees (privacy boundary)
- **School:** the **report only** (band, dimensions, evidence, flags) +
  status tracking. **Never the student's raw form answers or documents.**
- **Agency:** the full report (they own the student relationship).
- **⟨OPEN⟩** will schools eventually need evidence/document access for their
  own GTE files? Out of scope for v1, flag for later.

### 7.3 Engine
**⟨OPEN⟩** reuse the existing application-analysis / expert-review machinery
with a pre-qualifier rule set, OR a dedicated lightweight scorer. The light
template collects **declared data only** (no document uploads required in v1),
so the engine must score from declared inputs. Per-destination rule sets needed
if we expand beyond Australia.

---

## 8. The two entry paths + the monetization loop

### 8.1 Entry paths
- **Path 2 — Agency admin (PRIMARY for v1):** inside the agency's admin,
  "Invite student" with **invitation type = Pre-qualification** (vs New Visa).
  Identical mechanics to today's Student Invite; different template + a
  mandatory **school** selector (every pre-qualification routes to one school).
- **Path 1 — Public school URL (OPTIONAL, see §9):** `onevisa.ai/prequalifier/{slug}`
  — student self-enters, picks agency, results routed to that agency (magic
  link if Tier 0).

### 8.2 The agency admin structure
- **"Pre-qualifications" tab** — FREE/unlimited. Pipeline + list views, stats
  strip (strength mix), grouped by school when multiple. **Approved rows show a
  prominent "Start visa" button** — this is where conversion is triggered.
- **"Visas" tab** (today's "Students") — the standard visa product.
  - Tier 0/1: **locked** — blurred real pipeline behind a gate (3 value bullets
    + "Book a demo"). The locked tab **is** the lead-magnet CTA.
  - Tier 2: **unlocked** — full product.

### 8.3 The conversion flow ("Start visa" — pre-qualification → visa)
Triggered from the **Pre-qualifications list** on an **Approved** student (NOT
from the Visas tab — Visas is the destination, not the trigger). The flow:
1. Pre-fills everything carried from the pre-qualification (identity, financials,
   education, English, immigration history, COE).
2. Asks **only the visa-specific gaps**:
   - **Onshore / Offshore** selector → **this decides which visa template applies**.
   - Course **start date / end date**.
   - **Total tuition** + **paid to date** (enrolment/deposit).
   - Nationality (carried over, confirm).
   - OSHC health cover.
3. Sends the full visa invite to the student (1 credit) → from here it's the
   **standard OneVisa visa flow** (out of this project's scope).

---

## 9. White-label (OPTIONAL in v1)

Because the **agency-invite path is primary**, school branding is a
**nice-to-have, not core**. The prototype demonstrates it (logo + primary/
secondary colours + URL) via the existing `themes`/`url_slug` machinery, but:
- **Default:** OneVisa-branded portal that names the school.
- **Optional add-on:** full white-label (logo/colours/URL).
- **⟨OPEN⟩** depth of white-label for v1; whether Path 1 (public URL) and the
  **magic-link** delivery ship at all, or whether the agency-invite path alone
  is enough for v1. Magic-link expiry/security model is **⟨OPEN⟩** and only
  relevant if Path 1 ships.

---

## 10. Dashboards

- **School dashboard:** incoming quality mix (strong/moderate/weak counts) +
  **quality-by-agency** table ("GlobalReach sends 82% strong; Mumbai EduWorld
  0%"). Lets a school rank its agencies *before* the COE, not after a refusal.
- **Agency stats strip:** strength mix of its pre-qualifying pipeline as a
  visa-stage predictor.
- **Superadmin dashboard:** schools overview + the **lead-magnet funnel**
  (agencies linked → active → opened locked tab → demos booked → pilots
  activated).
- All are rollups over pre-qualification outcomes — same pattern as existing
  org dashboards.

---

## 11. Credits & billing

- **Pre-qualifications are unlimited and free.** No credit consumption, no
  credit UI on the pre-qualification side anywhere.
- **Credits exist only on the Visas (visa application) side.** The "Start visa"
  conversion consumes 1 credit like any visa invite.
- Pilot activation loads pilot credits onto the agency org.

---

## 12. Notifications (touchpoints — surfaces deferred)

The prototype mocks these inline; the actual notification system is out of
prototype scope but the **touchpoints** are part of the spec:
- Agency: "School X added you as a partner agency" (on association).
- New agency: pre-qualification welcome email (NEW template).
- Agency + school: "Student completed — analysis ready".
- Agency: "School approved — COE attached" / "School declined — reason".
- Student: invitation email + completion confirmation. **No outcome email.**

---

## 13. Reuse map (what already exists — minimise net-new build)

| Need | Existing machinery |
|------|--------------------|
| School as tenant | `organizations.client_type='school'` (+ DB constraint) |
| White-label branding | `themes` + `theme_properties` (per-org overrides) |
| White-label URL | `organizations.url_slug` (unique) |
| School provisioning | superadmin `organizations/new` |
| User invitations | existing admin invitation flows |
| Student invite + light form | Student Portal step-1 + CMS template mechanism |
| Visa application (post-conversion) | the standard product |
| Analysis | **⟨OPEN⟩** — reuse expert-review engine or new scorer |

Approx. "~70% of the ecosystem already exists" — net-new is mostly: the
pre-qualification entity + status machine, the association object + import/
match logic, the report rendering, the role-scoped admin tabs, and the
locked-tab/conversion UX.

---

## 14. Consolidated list of OPEN questions for the team

1. **Status model:** reuse application status enum (with a type) or a dedicated
   `pre_qualification` entity?
2. **Re-submission after rejection:** allowed? linked to the prior attempt?
3. **School evidence access:** will schools later need document access for GTE?
4. **Path 1 + magic-link:** ship in v1, or is the agency-invite path enough?
5. **White-label depth:** required for v1 or optional add-on?
6. **Magic-link security:** expiry, single-use, scope (only if Path 1 ships).
7. **CRM integrations** for agency import beyond CSV — which ones?
8. **Analysis engine:** reuse expert-review machinery or a dedicated scorer?
9. **Agency match key:** exact email vs corporate-domain matching tolerance.
10. **Sequencing:** ship Path 2 (agency invite) first, both paths together, or
    Path 1 first?

---

## 15. Prototype screen ↔ spec section map

| Prototype screen | Spec section |
|------------------|--------------|
| `index.html` (demo map + flow) | §1, §2, §13 |
| `sa-dashboard.html` / `sa-organizations.html` / `sa-create-school.html` | §3.1, §9, §10 |
| `school-welcome.html` | §2, §12 |
| `school-agencies.html` (CSV import preview) | **§4** (the import logic) |
| `agency-invite.html` (invite modal) | §8.1, §8.2, §11 |
| `student-landing.html` (public URL) | §9 (optional path) |
| `student-email/signup/wizard/success.html` | §6, §7.2 |
| `agency-report.html` / `school-report.html` | **§7** (the report) |
| `school-report.html` (approve/reject modals) | §6 |
| `agency-approved.html` / `agency-prequal-approved.html` | §6, §8.2 |
| `agency-visas-locked.html` / `-unlocked.html` | §5, §8.2 |
| `agency-convert.html` (Start visa) | **§8.3** (conversion) |
| `agency-magiclink.html` | §5 (Tier 0), §9 |
| `demo-end.html` | §14 (open questions) |
