Fields catalog
Every borrower-provided input in the proposed flow. Grouped by surface. Doc uploads are modeled as logical fields, not one fixed database column per file slot.
Lead qualification
Anonymous calculator plus contact capture. The live form shows mortgage pre-approval immediately after the property-found question.
| # | Field ID | Label | Type | Required | Validation | Source form / step | Supabase column (proposed) | Notes |
|---|
Dashboard profile questions
35 contract rows after OTP. Income is not re-asked — it's prefilled from F1 and shown on a confirmation card. The employment block expands with five fallback questions when the employer is not in the named taxonomy, plus conditional "Other" text follow-ups for employment type, industry, role family, and down-payment source. Financial commitments now use the v5 ask-and-reveal pattern: the borrower enters AED amounts first, then documents and AECB verify them later.
| # | Field ID | Label | Type | Required | Validation | Source form / step | Supabase column (proposed) | Notes |
|---|
Co-applicant branch
Only unlocked when has_co_applicant = yes. Mirrors the primary borrower's employment matrix — same named-taxonomy lookup, same five-field unknown-employer expansion, plus the same conditional "Other" text follow-ups for employment type, industry, and role family. Self-employed co-applicants swap title / seniority / tenure for business_years. Income confirms from F1 partner inputs (no re-ask), then the co-applicant enters the same five financial-commitment questions with inline AED reveals.
| # | Field ID | Label | Type | Required | Validation | Source form / step | Supabase column (proposed) | Notes |
|---|
Property options simulator
New step between Form 2.5 and Form 3. Gates documents — at least one property option must be locked (or specific-property branch fully filled) before F3 unlocks. Browsers get the progressive-compare simulator; buyers with a property in mind get the exact-property card. Off-plan triggers an immediate reroute.
| # | Field ID | Label | Type | Required | Validation | Source form / step | Supabase column (proposed) | Notes |
|---|
Docs + final submission
Three borrower answers plus logical upload fields. Bank statements and identity documents stay grouped here because storage is row-based, not fixed-column.
| # | Field ID | Label | Type | Required | Validation | Source form / step | Supabase column (proposed) | Notes |
|---|
Computed fields
Derived outputs the frontend can show immediately or the backend can persist. This table intentionally mixes display math, policy booleans, and score components because they all shape the API contract.
| Field name | Formula / source | Depends on | Used in | Notes |
|---|
Closing-cost math comes from the canonical Dubai closing-cost formula. The dashboard can keep it client-side now and move to an engine call later without changing field names.
`eligibility-gates.json` also defines G11-G13. This page keeps G1-G10 as requested and flags the extra gates as post-contract extensions.
Category J (Sector Tier, weight 10%) is now modelled. Source: employer_name from F2 → sector lookup against the underwriting-engine employer taxonomy (~25 named employers in v0.1.1, expandable to ~370 via APP-1844). T4/T5 sectors score 0 — feeds the auto-decline pathway in addition to gate G7 employment-tenure logic.
Relationships
How every form's fields connect to each other, to the doc tiles in Form 3, and to the underwriting engine inputs. Seven sections: end-to-end flow, F1→F2/F2.5 prefill, F1→engine, F2→engine, F2.5→engine, F3 conditional tiles, F3 doc extraction.
End-to-end data flow
One borrower row. Progressive enrichment. Documents update the same record and trigger the decision engine.
contact + property + residency + income"] --> H["URL params +
create buyer_lead row"] H --> D["Dashboard reads buyer_lead
and shows cost breakdown"] D --> F2["Form 2 updates same row
15 base inputs (income prefilled from F1)
+5 if unknown employer
+conditional Other text follow-ups"] F2 --> G{"has_co_applicant?"} G -->|yes| F25["Form 2.5 writes co_app_* fields"] G -->|no| F27["Form 2.7 — Property options simulator
specific-property card OR up to 3 compared options"] F25 --> F27 F27 --> OFF{"ready_to_move_in?"} OFF -->|off_plan| WAIT["Reroute: off-plan waitlist
no F3, lead saved"] OFF -->|ready or locked option| F3["Form 3 starts"] F3 --> DOCS["Supabase Storage + buyer_document rows"] DOCS --> OCR["OCR + bank-statement parsing
fills extracted fields"] OCR --> ENG["Underwriting engine call
indicative_scs + fee_tier_id + decision"] ENG --> SAVE["Update buyer_lead
decision fields + gates_failed"]
Form 1 → Form 2 / Form 2.5 prefill
Ask once. Display later. Re-verify only the phone. The "Income confirmation" card in F2 is not a re-ask — it shows the F1 values back and lets the borrower edit before they move on.
| F1 source field | F2 / F2.5 destination | Treatment |
|---|---|---|
fullName | F2 contact_name | Display only |
emailInput | F2 contact_email | Display only |
phoneInput | F2 phone_otp_verified | Re-verify via OTP |
heroValue | F2 property_budget | Display only |
areaMulti | F2 property_area | Display only |
propertyType | F2 property_type | Editable |
bedrooms | F2 bedrooms | Editable |
residency | F2 residency_status | Editable |
incomeInput | F2 fixed_income_aed | Prefilled, editable — confirm card, not re-ask |
commissionInput | F2 variable_income_aed | Prefilled, editable — confirm card, not re-ask |
householdIncomeInput + partnerCommInput > 0 | F2 has_co_applicant = yes (auto) | If F1 shows partner income, F2.5 unlocks automatically; user can still toggle off |
householdIncomeInput | F2.5 co_app_fixed_income_aed | Prefilled, editable |
partnerCommInput | F2.5 co_app_variable_income_aed | Prefilled, editable |
Form 1 fields → engine inputs
Even before F2 runs, F1 alone resolves G1 (residency), G3 (income floor), G9 (LTV ceiling), and seeds the closing-cost math.
| F1 field | Engine input / gate | Effect |
|---|---|---|
residency | G1 residency_status | Hard gate: must be UAE resident or citizen. |
incomeInput | G3 monthly_income, Category D | Min AED 15K floor. F2 displays this on a confirmation card; the user can edit but doesn't re-type from scratch. |
commissionInput | Category D (volatility haircut) | Variable component flagged separately so SCS applies the haircut. |
householdIncomeInput + partnerCommInput | has_co_applicant default + joint income | If either is non-zero, F2 defaults has_co_applicant = yes and F2.5 unlocks with these values prefilled. |
heroValue (property price) | G10 property_price_aed, closing-cost base | Hard cap AED 4,500,000. Drives DLD 4% + agent 2% + Sooner fee math. |
loanPctInput (LTV) | G9 ltv_pct | Hard cap 80% (resident) / 50% (non-resident). |
propertyType + bedrooms | Category H (property tier) | Apartment / townhouse / villa segments scoring. |
areaMulti | Category I (community tier) | Prime / mid / emerging community classification. |
downPaymentSource | Category G (down-payment source) | Savings / sale / gift weighting. |
Form 2 fields → eligibility gates + scoring
F2 is the gate-and-score backbone. Every question maps to a hard gate or a scoring category. Income is not re-asked — it's prefilled from F1 and shown on a confirmation card. Employment expands with five extra questions when the employer is not in the named taxonomy, plus conditional "Other" text follow-ups that Sooner ops reconciles back to the canonical enums during review.
| F2 field | Gate / category | Effect |
|---|---|---|
| Identity + service | ||
phone_otp_verified | (no gate — anti-fraud only) | Verifies the lead can be contacted before SCS publish. |
service_selection[] | fee_tier_id resolution | CFF only = 15% · CFF + 1 = 12.5% · all three = 10%. |
| Income (confirm from F1, edit-only) | ||
fixed_income_aed (prefilled from F1) | G3 income floor, Category D | Hard min AED 15K monthly. Drives Category D household-income band. |
variable_income_aed (prefilled from F1) | Category D (volatility haircut) | Subtracted from stable base before scoring. |
has_co_applicant (default = yes if F1 partner income > 0) | (branch flag) | Unlocks F2.5 + 5 co-applicant doc tiles in F3. |
| Employment — always asked | ||
employment_type | G14 employment_type whitelist | Hard gate: salaried / self_employed only at F2. Commission-only auto-rejects at Form 1 lead-gen with a waitlist capture. Drives doc-tile branching in F3. |
has_additional_income | (supplementary income capture) | Boolean. Expand row reveals source multi-select (rental / side business / second salary / investment / other) + estimated monthly AED. NOT yet weighted in scoring; engine pass needed. |
additional_income_sources | Category D (future) | Multi-select array. Tracked for ops but not yet scored. |
additional_income_monthly_aed | Category D (future, with haircut) | Less verification than primary income. Documents (Form 3 additional-income tile) confirm. |
employer_employee_count (self-employed) | Category J (self-employed alt) | Plain number input replacing band-select for self-employed. Salaried still uses band-select (enterprise / mid_market / sme / micro). |
employer_name | Category J (sector tier) lookup | If matches named taxonomy (~25 employers v0.1.1, ~370 via APP-1844) → T1–T5 auto-resolved. If no match → reveal the "Unknown employer" block below. |
role_name | Category J seniority bump | Free text + light autocomplete. Hidden when employment_type = self_employed. |
role_seniority | Category J seniority bump | Single-select ladder: entry · mid · senior · manager · director · executive. Hidden when employment_type = self_employed. |
employer_tenure | G7 tenure_months, Category C | Hard min 6 months. Bands: <6mo / 6–12mo / 1–3y / 3–5y / 5y+. Hidden when employment_type = self_employed. |
business_years (self-employed only) | G7 alt path, Category C | Min 2 years; gates audited-financials requirement in F3. |
Employment — Unknown employer block (only if employer_name not in named taxonomy) | ||
industry_id | Category J fallback (industry) | Single-select: government · energy · healthcare · financial-services · tech · real-estate · retail · hospitality · construction · logistics · education · professional-services · other. Maps to industry tier when employer name unknown. |
role_family_id | Category J fallback (role) | Single-select: engineering · medical · finance · sales · operations · marketing · legal · hr · executive · other. Hidden when employment_type = self_employed. |
employer_ownership_type | Category J fallback (ownership) | government (federal · emirate · SWF · GRE merged) · multinational · listed_company · private · sme · startup. |
employer_size_band | Category J fallback (size) | enterprise (3,000+ staff) · mid_market (500–3,000) · sme (50–500) · micro (<50). |
regulated_or_licensed | Category J fallback (regulatory) | Yes / No. Licensed entities (banks, healthcare, legal) get a half-tier bump. |
| Conditional "Other" text follow-ups | ||
employment_type_other | taxonomy enrichment only | Captured when employment_type = other. Free text feeds taxonomy enrichment, not Category J directly — Sooner ops reconciles it to the canonical enum during review. |
industry_other | taxonomy enrichment only | Captured when industry_id = other. Free text feeds taxonomy enrichment, not Category J directly — Sooner ops reconciles it to the canonical enum during review. |
role_family_other | taxonomy enrichment only | Captured when role_family_id = other. Free text feeds taxonomy enrichment, not Category J directly — Sooner ops reconciles it to the canonical enum during review. |
down_payment_source_other | source-of-funds enrichment only | Captured when down_payment_source = other. Free text does not score directly — Sooner ops reconciles it to the canonical enum during review. |
| Liabilities — user-supplied first, AECB verifies later | ||
has_existing_mortgage + mortgage_monthly_aed | G5 existing_mortgages (max 3), G8 DBR | User-supplied monthly payment now; documents and AECB verify later. |
has_rent + rent_payment | Category F (housing cost ratio) | User-supplied monthly rent vs income shapes the provisional affordability view. |
has_personal_loan + personal_loan_payment | G8 DBR | User-supplied; AECB verifies. |
has_car_loan + car_loan_payment | G8 DBR | User-supplied; AECB verifies. |
has_credit_card + credit_card_total_limit | G8 DBR (5% of limit) | User-supplied total credit limit now; DBR assumes a 5% monthly minimum until AECB confirms it. |
F2.5 co-applicant liabilities | Joint DBR | Co-applicant rent, loans, and credit cards are gathered on Form 2.5 and roll into the joint affordability view. |
| Buffers + property | ||
savings | Category B (savings buffer) | Band cards: <50K / 50–150K / 150–300K / 300K+. |
property_usage_raw | G9 LTV by usage | Primary 80% · second home 65% · investment 60%. |
down_payment_source | Category I (lifestyle alignment) | Savings · sale · gift · inheritance · investment-liquidation. |
aecb_band | G4 aecb_score (provisional), Category G | Self-reported band; AECB report finalizes the value. |
Form 2.5 fields → joint scoring
F2.5 only runs when has_co_applicant = yes. Each field has a solo-applicant analogue in F2 — same gate, same category, second contributor.
| F2.5 field | Gate / category | Effect |
|---|---|---|
co_app_full_name + co_app_email + co_app_phone | (contact only) | KYC contact record; no gate impact. |
co_app_employment_type | G6 (joint) | Both applicants must clear G6 independently. |
co_app_employer_name + co_app_role_name + co_app_role_seniority | Category J (joint sector tier) | Joint sector tier = average of primary + co-app tiers. Same named-taxonomy lookup; self-employed co-applicants skip title / seniority / tenure / role and fall back to business-years + industry. |
co_app_fixed_income_aed (prefilled from F1 partner) | G3 (joint), Category D | Added to primary fixed income for joint affordability. |
co_app_variable_income_aed (prefilled from F1 partner) | Category D (volatility haircut) | Same haircut treatment as primary variable_income_aed. |
co_app_has_existing_mortgage + co_app_mortgage_monthly_aed | G8 joint DBR | User-supplied now; documents and AECB verify later. |
co_app_has_rent + co_app_rent_payment | Joint housing-cost ratio | Co-applicant rent feeds the joint affordability view. |
co_app_has_personal_loan + co_app_personal_loan_payment | G8 joint DBR | User-supplied; AECB verifies. |
co_app_has_car_loan + co_app_car_loan_payment | G8 joint DBR | User-supplied; AECB verifies. |
co_app_has_credit_card + co_app_credit_card_total_limit | G8 joint DBR (5% of limit) | Joint DBR assumes 5% of the co-applicant's total credit-card limit until AECB confirms it. |
Form 3 doc tiles → conditional visibility
Doc tiles in F3 are NOT all required. F2 + F2.5 answers gate which tiles appear and which carry the "required" badge.
| Doc tile | Visible when | Required? |
|---|---|---|
| Emirates ID (front + back) | Always | Required |
| Passport (main page) | Always | Required |
| Visa page | Always | Required |
| Bank statements (6 months) | Always | Required |
| AECB report | Always | Required |
| Salary certificate | employment_type = salaried | Required |
| Additional income statements | has_additional_income = yes | Recommended · Multi-file · Per-file type tag (rental / side business / second salary / investment / other) |
| Business financials (12 months) | employment_type = self_employed | Required · Audited OR un-audited OR management accounts · Multi-file or 1 combined PDF (toggle) · Per-file type tag |
| Trade license | employment_type = self_employed | Required |
| Audited financials | employment_type = self_employed | Required if business_years ≥ 2, else recommended |
| Co-app Emirates ID | has_co_applicant = yes | Required |
| Co-app Passport | has_co_applicant = yes | Required |
| Co-app Visa page | has_co_applicant = yes | Required |
| Co-app Salary certificate | has_co_applicant = yes AND co_app_employment_type = salaried | Required |
| Co-app Bank statements (3–6 months) | has_co_applicant = yes | Required |
Form 3 docs → extracted fields → engine
File uploads do not just unlock the next step. They verify or override provisional borrower inputs, finalize gates, and supply the values the engine needs that the user cannot self-report reliably enough on their own (AECB score, debt schedules, defaults, extracted income evidence).
| Document upload | Fields it populates | Gates / categories finalized |
|---|---|---|
| Emirates ID | DoB · Nationality · EID number | G2 (age 21–65), KYC |
| Passport | Romanized name · Nationality · Passport number · Expiry | KYC, visa-validity cross-check |
| Visa page | Residency duration · Visa type · Sponsor | G1 (residency_status confirmed) |
| Bank statements | Variable income patterns · Rent payment · Savings buffer · Recurring debits | Category A (income volatility), Category F (rent), Category D (liquidity), G8 (true DBR) |
| AECB report | AECB score · Credit history · Existing debts · Defaults | G4 (AECB score), G5 (existing mortgages), G8 (DBR), Category E |
| Salary certificate | Employer · Salary · Tenure · Allowances | G7 (tenure confirmed), Category A (income confirmed), Category J (employer → sector tier) |
| Business financials (12 months) | 12 months of monthly P&L · Revenue · EBITDA · Cash flow · Owner draws · Audit opinion (if audited) | Required for self-employed. Multi-file (12 monthly statements) or single combined PDF. Per-file type tag captures which doc kind (audited / un-audited / management accounts / business bank statement / other). |
| Trade license | Business name · License number · Years in business | G7 alt (business_years), Category B |
| Audited financials | Business revenue · Business profit | Category A (self-employed income basis) |
Known gaps from contract decisions
- Dropped
jurisdiction_idfrom Form 2 — defaults all unknown employers to UAE in the engine call, potentially overstating scores for foreign-paid borrowers. Tracked as a future question candidate: “Does your salary land in a UAE bank account?” (binary, low friction). - Commission-only auto-rejected at Form 1. Added new gate
G14: Employment type whitelistwithallowedValues = ["salaried", "self_employed"]as defense in depth at the engine layer. F1 shipsemployment_typeas a URL param to the dashboard so F2 prefills. - Self-employed bypass employer ownership taxonomy. F2 hides
employer_ownership_typeand replacesemployer_size_bandwith a plain numericemployer_employee_countfield. Salaried still uses the band-select. Engine policy needs to map employee count to a Category J tier for self-employed (currently no mapping). - Additional income captured but not yet scored.
has_additional_income/additional_income_sources[]/additional_income_monthly_aedare now collected in F2 + F2.5, with a Form 3 multi-upload doc tile (per-file type tag). Engine scoring should apply a haircut to the variable additional-income amount in Category D (currently ignored). - Salary certificate freshness tightened. Now must be issued within the last 30 days. Employment letter tile dropped entirely (was redundant with salary cert per UAE banking practice).
- Visa page tile dropped. EID front+back already covers residency for both primary and co-applicant. Engine still keys on visa duration from EID extraction.
- Business financials replaces audited financials for self-employed. Accepts 12 monthly statements OR a single combined PDF (toggle). Audited / un-audited / management accounts all accepted with a per-file type tag. No longer gated on
business_years >= 2. - Self-employed borrowers skip
role_name/role_seniority/employer_tenure_bucket/role_family_id. The engine must handle missing seniority + role for Category J self-employed cases — either drop those signals or substitute withbusiness_years+ industry. - Employer size cutoffs revised down (enterprise 10K→3K, mid_market upper 10K→3K). Documented for engine alignment when employer-classification.json
employerSizeBandis recomputed. - Ownership taxonomy collapsed to 6 options:
government(federal + emirate + SWF + GRE merged),multinational,listed_company,private,sme,startup. Trade-off: the engine loses sub-tier resolution between federal vs emirate vs SWF vs GRE — all collapse to "government" with one Category J weighting. Tracked as a future granularity question if the named-employer taxonomy can't disambiguate (the assumption is named-taxonomy hits resolve the sub-tier automatically; only the unknown-employer fallback loses signal). New optionsmultinationalandstartupneed engine-side Category J weights — coordinate withscoring-weights.json+ employer-classification policy. - Form 2.7
scoreOption()is a client-side heuristic placeholder. Real underwriting-engine wiring is pending. The current implementation returnsscs: null+ltv: null+ "Profile incomplete — property fit preview only." copy when the borrower profile isn't complete enough to score truthfully, so the UI never shows false precision. Once the underwriting-engine is callable client-side (or via a sync engine.ts import), swap the heuristic for engine.evaluate() per option. - F2.7 deliberately omits fee-tier (%) language. Dashboard fee tier is currently driven by service selection (10 / 12.5 / 15%); the engine has separate property tier + employer tier + LTV. Until that source-of-truth is reconciled, F2.7 shows SCS + indicative LTV only.
- Long-tail community handling uses 4 statuses:
approved(Tier A/B),committee(Tier C),not_approved(auto_decline),needs_confirmation(chip not incommunity-classification.json). Communities inneeds_confirmationsurface a "we'll confirm this community" line — they do NOT default to Tier B and do NOT silently auto-decline.
Doc extraction
OCR and parser requirements by tile. Vendor bake-off is between Extend AI and GPT-5.4-mini via Azure — same primary across every tile, picked per accuracy benchmark since volume is too low for cost to dominate. Keep the manual-review path first-class because AECB PDFs and self-employed financials will not be clean enough every time.
| Doc tile | Required if | Extracted fields | OCR vendor (proposed) | Confidence threshold | Manual review fallback |
|---|
Supabase schema
Three-table proposal. `buyer_lead` remains the orchestration spine. `buyer_document` becomes extraction-aware. `buyer_journey` gives the CTO an explicit audit trail instead of inference from timestamps.
`buyer_lead`
Extended from the current lead row. This is the single API record the dashboard and engine mutate.
| Column | Type | Default | Constraints | Source | Notes |
|---|
`buyer_document`
Existing document row with extraction metadata added. One file upload = one row.
| Column | Type | Default | Constraints | Source | Notes |
|---|
`buyer_journey`
New audit trail table for progress, abandonment, and replay. Useful for CRM routing, analytics, and support.
| Column | Type | Default | Constraints | Source | Notes |
|---|
buyer_lead is mostly ALTER. buyer_document adds extraction columns. buyer_journey is net-new.
buyer_lead(session_id) unique, buyer_lead(status, updated_at desc), buyer_lead(decision, decision_at desc), buyer_document(buyer_id, doc_kind, uploaded_at desc), buyer_journey(buyer_id, step, entered_at desc).
Use jsonb only for extracted document payloads and journey snapshots. Everything queryable in ops or underwriting stays first-class.