# RateRight User Flow Audit

Complete documentation of worker and contractor flows, payment gates, contact reveals, and platform bypass risks.

---

## 1. WORKER FLOW

### Step 1: Sign Up

| Item | Detail |
|------|--------|
| **Screen** | `/auth/signup` |
| **File** | `src/app/auth/signup/page.tsx` |
| **Data collected** | Email, password (min 6 chars), user type = "worker", ToS agreement |
| **API calls** | `supabase.auth.signUp()` with `metadata: { type: "worker" }` |
| **DB trigger** | `handle_new_user` auto-creates row in `profiles` table with `type="worker"`, `onboarding_completed=false` |
| **Next** | Redirect to `/auth/verify-email` → email link → `/auth/callback` |

### Step 2: Profile Creation (Onboarding)

| Item | Detail |
|------|--------|
| **Screen** | `/worker/signup` (9-step wizard) |
| **File** | `src/app/(authenticated)/worker/signup/page.tsx` |
| **Auth guard** | `(authenticated)` layout — must be logged in |

**Steps in the wizard:**

| # | Step | Component | Data Collected | API Calls |
|---|------|-----------|----------------|-----------|
| 1 | Auth | `AuthStep.tsx` | Email/password (if not already logged in) | `supabase.auth.signUp()` or `signInWithPassword()` |
| 2 | Method | `MethodStep.tsx` | Choice: "Type answers" or "Voice" | None |
| 3a | Voice (optional) | `VoiceStep.tsx` | Audio recording (WebM) | `POST /api/ai/transcribe-voice` (Whisper), `POST /api/ai/parse-profile-voice` (GPT-4o-mini) |
| 3b | Trade | `TradeStep.tsx` | `selectedTrades: string[]` (from 16 options) | None |
| 4 | Experience | `ExperienceStep.tsx` | `experience: number` (0, 2, 4, 7, or 12 years) | None |
| 5 | Location | `LocationStep.tsx` | `suburb: string` | Geocoded later via `POST /api/geocode` |
| 6 | Availability | `AvailabilityStep.tsx` | `availability: "full-time" | "casual" | "weekends" | "flexible"` | None |
| 7 | Certifications | `CertificationsStep.tsx` | `certifications: string[]` (9 options) | None |
| 8 | Review | `ReviewStep.tsx` | Confirms all data, AI-generated bio | `POST /api/geocode`, then Supabase inserts |

**On save (via `useWorkerProfile.saveWorkerProfile()`):**
1. Geocodes suburb → lat/lng
2. Updates `profiles`: `name`, `suburb`, `location`, `onboarding_completed = true`
3. Inserts into `worker_profiles`: `trades`, `experience_years`, `availability`, `certifications`, `bio`, `ai_extracted`
4. Redirects to `/dashboard`

### Step 3: Dashboard

| Item | Detail |
|------|--------|
| **Screen** | `/dashboard` |
| **File** | `src/app/(authenticated)/dashboard/page.tsx` (server component) |
| **Shows** | "Let's get you on a job" greeting, pending ratings (if any), "Find Work" CTA → `/worker/jobs`, "Your Profile" CTA → `/worker/profile` |
| **Data fetched** | User profile (type, name, onboarding status), pending hired matches not yet rated |

### Step 4: Browse Jobs

| Item | Detail |
|------|--------|
| **Screen** | `/worker/jobs` |
| **File** | `src/app/(authenticated)/worker/jobs/page.tsx` (client component) |
| **Features** | Trade filter pills (horizontal scroll), swipeable job cards, tap-to-view detail sheet, pagination (10/page) |

**What workers SEE on each job card:**
- Trade badge
- Job title
- Suburb (location)
- Pay range ($min–$max /hr or /day)
- Start date
- Workers needed count

**What workers DO NOT see:**
- Company name
- Contractor name
- ABN
- Phone/email/contact details
- Site address (only shown in detail sheet as `site_address || suburb`)

**Data fetched:**
- `jobs` table: `id, title, trade, suburb, rate_min, rate_max, rate_type, start_date, workers_needed, status, created_at, location`
- `matches` table: worker's existing applications (to show "Applied" badge)

### Step 5: Apply for Job

| Item | Detail |
|------|--------|
| **Trigger** | Swipe right > 100px on card, OR tap "Apply with One Tap" in detail sheet |
| **Data written** | `matches.insert({ job_id, worker_id, status: "applied", contractor_action: "pending", worker_action: "accepted" })` |
| **Cost** | Free — "Always free for workers. No fees ever." |
| **Feedback** | Haptic vibration, success toast, "Applied" badge |

### Step 6: Get Hired

| Item | Detail |
|------|--------|
| **Trigger** | Contractor pays $50 and hires worker (see Contractor Flow Step 6) |
| **Worker sees** | Notification that they were hired |
| **Match status change** | `status: "hired"`, `contractor_action: "accepted"`, `hired_at` timestamp, `fee_charged: true` |
| **Contact reveal** | Messaging unlocked (see Messaging section) |

### Step 7: Messaging (Post-Hire)

| Item | Detail |
|------|--------|
| **Screen** | `/messages` (conversation list), `/messages/[conversationId]` (chat) |
| **File** | `src/app/(authenticated)/messages/page.tsx` |
| **API calls** | `GET /api/messages` (conversation list), `POST /api/messages/start` (create conversation), `POST /api/messages/[id]/send` (send message) |
| **Real-time** | Supabase Realtime subscription on `messages` table |
| **Rate limits** | 100 list fetches/15min, 20 conversation starts/15min, 60 messages/15min |

### Step 8: Job Completion & Rating

| Item | Detail |
|------|--------|
| **Screen** | `/rate/[matchId]` |
| **File** | `src/app/(authenticated)/rate/[matchId]/page.tsx` |
| **Guard** | Match must be `status: "hired"` and not already rated by this user |
| **Data collected** | Star rating (1–5), optional comment (max 500 chars) |
| **Data written** | `ratings.insert({ match_id, from_id: worker, to_id: contractor, score, comment })` |
| **Post-submit** | Success screen → auto-redirect to `/dashboard` after 2s |

---

## 2. CONTRACTOR FLOW

### Step 1: Sign Up

| Item | Detail |
|------|--------|
| **Screen** | `/auth/signup` |
| **File** | `src/app/auth/signup/page.tsx` |
| **Data collected** | Email, password, user type = "contractor", ToS agreement |
| **API calls** | `supabase.auth.signUp()` with `metadata: { type: "contractor" }` |
| **DB trigger** | `handle_new_user` creates `profiles` row with `type="contractor"` |

### Step 2: Company Onboarding (ABN Verification)

| Item | Detail |
|------|--------|
| **Screen** | `/contractor/signup` (4-step wizard) |
| **File** | `src/app/(authenticated)/contractor/signup/page.tsx` |

| # | Step | Data Collected | API Calls |
|---|------|----------------|-----------|
| 1 | Auth | Email/password (if not logged in) | `supabase.auth.signUp()` |
| 2 | ABN Search | ABN (11 digits, checksum validated) | `GET /api/abn-lookup?abn={abn}` → ABR JSON API; `GET /api/company-logo` → Clearbit/Google |
| 3 | Verification | Loading screen (auto-transitions) | None |
| 4 | Review | Editable: company name, ABN, address, industry, size, website | `profiles.update()`, `companies.insert()` |

**ABN Lookup response includes:**
- Company name, ABN, address (postcode + state), industry, size, website
- Logo URL (from Clearbit/Google)
- Director verification: risk scoring (0–25 auto-verify, 26–50 manual review, 51+ manual approval)

**On save:**
1. Updates `profiles`: `name = companyName`, `onboarding_completed = true`
2. Inserts into `companies`: `profile_id, name, abn, address, industry, size, website, ai_research`
3. Redirects to `/dashboard`

### Step 3: Dashboard

| Item | Detail |
|------|--------|
| **Screen** | `/dashboard` |
| **Shows** | "G'day, {Company Name}!", active jobs count, new matches count, pending ratings, CTAs for "Post a Job" and "View Matches" |
| **Data fetched** | Company ID, active job count, match count (applied/suggested), hired matches needing rating |

### Step 4: Post a Job

| Item | Detail |
|------|--------|
| **Screen** | `/contractor/post-job` |
| **File** | `src/app/(authenticated)/contractor/post-job/page.tsx` |

| Stage | What Happens | Data/APIs |
|-------|-------------|-----------|
| Trade selection | Pick from 7 trade categories | None |
| AI generation | Auto-fills job details | `POST /api/ai/generate-job` → GPT-4o-mini generates title, description, certs, rate range |
| Edit details | Title, description, pay range, rate type, certifications | Editable form fields |
| Location | Site address + suburb | `POST /api/geocode` → lat/lng |
| Schedule | Start date, duration, workers needed | Date picker + number inputs |
| Post | Save to database | `jobs.insert()` with all fields + `status: "active"`, `ai_generated: true` |
| Success | Confirmation screen | Links to "View Matches" or "Back to Dashboard" |

### Step 5: Browse/Match Workers

| Item | Detail |
|------|--------|
| **Screen** | `/contractor/matches` |
| **File** | `src/app/(authenticated)/contractor/matches/page.tsx` |
| **Layout** | Job selector pills (horizontal scroll) → swipeable match cards |

**What contractors SEE on match cards (BEFORE payment):**

| Data Point | Shown? | Source |
|------------|--------|--------|
| Worker first initial (avatar) | Yes | `worker.name[0]` |
| Worker name | Yes | `profiles.name` |
| AI summary (trades, experience, location, availability, certs) | Yes | `matches.ai_summary` or generated from profile |
| Match score (%) | Yes | `matches.match_score` |
| Experience years | Yes | `worker_profiles.experience_years` |
| Suburb/location | Yes | `profiles.suburb` |
| Certifications (up to 3) | Yes | `worker_profiles.certifications` |
| Availability | Yes (in AI summary) | `worker_profiles.availability` |
| **Phone number** | **No** | Not queried |
| **Email** | **No** | Not queried |
| **Full profile link** | **No** | No clickable link to worker profile |

**Match data query selects only:** `id, name, suburb, avatar_url` from profiles and `trades, experience_years, certifications, availability` from worker_profiles.

### Step 6: Hire Worker (Payment — $50 Fee)

| Item | Detail |
|------|--------|
| **Trigger** | Swipe right > 100px on match card, OR tap "Hire — $50 Fee" button |
| **Modal** | `HirePaymentModal` → `PaymentForm` with Stripe Elements |
| **File** | `src/components/payment/HirePaymentModal.tsx`, `src/components/payment/PaymentForm.tsx` |

**Payment flow:**

```
1. Contractor triggers hire
2. POST /api/payments/create
   → Validates: user owns company, match exists & not rejected, no duplicate payment
   → Creates Stripe PaymentIntent ($50 AUD, hardcoded server-side)
   → Upserts payment record (status: "pending")
   → Returns clientSecret + publishableKey
3. Stripe PaymentElement renders (card, Apple Pay, Google Pay)
4. User confirms → stripe.confirmPayment()
5. On success:
   → matches.update({ status: "hired", contractor_action: "accepted", hired_at, fee_charged: true })
   → Success screen shown for 2s
   → Modal closes
```

**Payment security:**
- Amount hardcoded to `PAYMENT_AMOUNT` constant ($50) — client cannot override
- Rate limited: 10 requests/15min per user AND per IP
- CSRF protected
- Duplicate payment prevention (checks for existing `status: "charged"`)
- Atomic upsert prevents race conditions
- Stripe handles PCI compliance

### Step 7: Contact Reveal (Post-Payment)

| Item | Detail |
|------|--------|
| **When** | After successful $50 payment and match `status = "hired"` |
| **How** | Messaging unlocked — contractor can start a conversation via `/api/messages/start` |
| **What's revealed** | Names visible in conversation; direct communication channel open |

### Step 8: Messaging

Same as Worker Step 7. Both parties can message freely after hire.

### Step 9: Job Completion & Rating

Same flow as Worker Step 8, but contractor rates the worker:
- `ratings.insert({ from_id: contractor, to_id: worker, score, comment })`

---

## 3. THE $50 FEE — WHERE AND HOW

| Aspect | Detail |
|--------|--------|
| **Who pays** | Contractor only. Workers are always free. |
| **When charged** | At the moment of hire (swipe right / tap "Hire" on match card) |
| **Amount** | $50 AUD flat fee, hardcoded in `PAYMENT_AMOUNT` constant |
| **Payment method** | Stripe PaymentElement (card, Apple Pay, Google Pay) |
| **What it unlocks** | Match status → "hired", messaging access, mutual contact |
| **Refund policy** | Not implemented in current codebase |
| **API endpoint** | `POST /api/payments/create` |
| **Verification** | `GET /api/payments/verify?payment_intent={id}` |
| **Redirect fallback** | `/contractor/payment-success` (for 3D Secure redirects) |

---

## 4. CONTACT REVEAL TIMELINE

| Stage | Worker Sees | Contractor Sees |
|-------|-------------|-----------------|
| **Job browsing** | Job title, trade, suburb, pay, start date, duration, certs, description. **NO company name, no contractor contact.** | N/A |
| **Application** | Same as above + "Applied" badge | N/A |
| **Match card (pre-payment)** | N/A | Worker name, initial, suburb, trades, experience, certs, availability, match score. **NO phone, email, or profile link.** |
| **After $50 payment** | Notification of hire | "Hired" badge on match card |
| **Messaging** | Can message contractor by name | Can message worker by name |
| **Profile views** | Contractor profile visible at `/contractor/profile` (company name, logo, ABN, industry, ratings) | Worker profile visible at `/worker/profile` (trades, experience, suburb, certs, bio, ratings) |

---

## 5. PLATFORM BYPASS / LEAKAGE ANALYSIS

### CRITICAL RISKS

#### 5.1 Messaging API Has No Hire Gate
- **Severity:** CRITICAL
- **Location:** `src/app/api/messages/start/route.ts`
- **Issue:** The `/api/messages/start` endpoint only checks authentication and that the recipient exists. It does **NOT** verify that a hired match exists between the two users.
- **Impact:** Any authenticated user can start a conversation with any other user by knowing their profile ID, completely bypassing the $50 payment gate.
- **Exploit:** A contractor could discover worker profile IDs (from match data) and call `/api/messages/start` directly with the worker's ID, without ever paying.

#### 5.2 Worker Name Visible Before Payment
- **Severity:** HIGH
- **Location:** `src/app/(authenticated)/contractor/matches/page.tsx:163`
- **Issue:** The match card shows the worker's full `name` from `profiles.name`. Since workers set their name to `email.split("@")[0]` during signup, this often reveals part of their email address (e.g., "john.smith" → john.smith@something.com).
- **Impact:** Contractors can potentially reconstruct worker emails or search for workers on social media using their name + suburb + trade combination.

#### 5.3 Worker Suburb Visible Before Payment
- **Severity:** MEDIUM
- **Location:** `src/app/(authenticated)/contractor/matches/page.tsx:188`
- **Issue:** Worker suburb is shown on match cards pre-payment. Combined with name + trade, this narrows identification significantly in smaller suburbs.
- **Impact:** In regional areas, "John the electrician in Mudgee" is essentially a unique identifier.

### HIGH RISKS

#### 5.4 No RLS Verification on Profile Queries
- **Severity:** HIGH
- **Location:** `src/app/(authenticated)/contractor/matches/page.tsx:391-393`
- **Issue:** The match page queries `profiles` and `worker_profiles` tables directly using `supabase.from("profiles").select("id, name, suburb, avatar_url").in("id", workerIds)`. If RLS policies on these tables are permissive (any authenticated user can SELECT), then any user could query any other user's profile data directly via the Supabase client.
- **Impact:** Depends entirely on RLS policy configuration. If `profiles` table allows `SELECT` for authenticated users, all profile data (including phone and email columns) could be queried directly.
- **Note:** The client-side code only selects `id, name, suburb, avatar_url`, but a malicious user could modify the query to include `phone` and `email` columns.

#### 5.5 Contractor Profile Publicly Visible to Workers
- **Severity:** MEDIUM
- **Location:** Worker can view contractor company profile after being matched
- **Issue:** After a worker applies to a job, the worker knows the `company_id` from the job listing. The company profile at `/contractor/profile` shows company name, ABN, website, and location.
- **Impact:** Workers who apply can look up the contractor's company name and contact them directly through public business directories, ABR lookup, or the company website listed on their profile.

#### 5.6 Job Description May Contain Contact Info
- **Severity:** MEDIUM
- **Location:** `src/app/(authenticated)/worker/jobs/page.tsx:460`
- **Issue:** Job descriptions are free-text and displayed in full in the detail sheet. There is no server-side sanitisation to strip phone numbers, emails, or URLs from descriptions.
- **Impact:** A contractor could embed their phone number or email in the job description (intentionally to avoid the fee, or accidentally). Workers would see it before any payment occurs.

### MEDIUM RISKS

#### 5.7 AI Summary May Leak Data
- **Severity:** MEDIUM
- **Location:** `src/app/(authenticated)/contractor/matches/page.tsx:70`
- **Issue:** The `ai_summary` field on matches is generated by AI and stored in the database. If the AI extracts or includes identifying information from worker profiles (like specific employer names, project details, or contact info from bios), this appears on pre-payment match cards.
- **Impact:** The AI summary could inadvertently include enough identifying detail to find a worker outside the platform.

#### 5.8 Rating Reviews Show Reviewer Identity
- **Severity:** LOW
- **Location:** `/worker/profile/page.tsx`, `/contractor/profile/page.tsx`
- **Issue:** Ratings display `from_profile.name` and `from_profile.avatar_url`. This reveals who rated whom.
- **Impact:** Minimal direct bypass risk, but creates a social graph that could facilitate off-platform contact in future engagements.

#### 5.9 Post-First-Hire Bypass
- **Severity:** HIGH (business model risk)
- **Location:** N/A (behavioural, not technical)
- **Issue:** After a contractor pays $50 to hire a worker once, they exchange contact details via messaging. For all future hires of the same worker, they can contact them directly and bypass the platform entirely.
- **Impact:** The platform only captures revenue on the first hire per contractor-worker pair. Repeat business is unmonetised. This is the **primary long-term revenue leakage risk**.

### LOW RISKS

#### 5.10 Avatar URLs Are Public Supabase Storage
- **Severity:** LOW
- **Location:** `src/components/avatar-upload.tsx`
- **Issue:** Avatars are uploaded to Supabase Storage (bucket: `avatars`) and the URL is stored in `profiles.avatar_url`. If the bucket is publicly readable, avatar images could be reverse-image-searched.
- **Impact:** Theoretical but unlikely to be exploited at scale.

---

## 6. COMPLETE API ENDPOINT MAP

### Worker-Used Endpoints

| Endpoint | Method | Purpose | Auth | Rate Limit | CSRF |
|----------|--------|---------|------|------------|------|
| `/api/auth/verify-email` | POST | Verify email | Yes | 5/15min | No |
| `/api/ai/transcribe-voice` | POST | Speech→text (Whisper) | Yes | 10/15min | Yes |
| `/api/ai/parse-profile-voice` | POST | Extract profile from transcript | Yes | 10/15min | Yes |
| `/api/ai/voice-complete` | POST | Combined voice→profile | Yes | 5/15min | Yes |
| `/api/ai/generate-profile` | POST | AI profile generation | Yes | Standard | Yes |
| `/api/geocode` | POST | Suburb→lat/lng | Yes | Standard | No |
| `/api/messages` | GET | List conversations | Yes | 100/15min | No |
| `/api/messages/start` | POST | Create conversation | Yes | 20/15min | Yes |
| `/api/messages/[id]/send` | POST | Send message | Yes | 60/15min | Yes |

### Contractor-Used Endpoints

| Endpoint | Method | Purpose | Auth | Rate Limit | CSRF |
|----------|--------|---------|------|------------|------|
| `/api/abn-lookup` | GET | ABR verification | Yes | 10/15min | No |
| `/api/company-logo` | GET | Fetch company logo | Yes | Standard | No |
| `/api/ai/generate-job` | POST | AI job details | Yes | 5/15min | Yes |
| `/api/geocode` | POST | Suburb→lat/lng | Yes | Standard | No |
| `/api/match/find-matches` | POST | Auto-match workers | Yes | 10/15min | Yes |
| `/api/payments/create` | POST | Create Stripe PaymentIntent | Yes | 10/15min | Yes |
| `/api/payments/verify` | GET | Check payment status | Yes | Standard | No |
| `/api/messages` | GET | List conversations | Yes | 100/15min | No |
| `/api/messages/start` | POST | Create conversation | Yes | 20/15min | Yes |
| `/api/messages/[id]/send` | POST | Send message | Yes | 60/15min | Yes |

---

## 7. DATABASE TABLES INVOLVED

| Table | Worker Flow | Contractor Flow | Key Fields |
|-------|-------------|-----------------|------------|
| `profiles` | Read/Write | Read/Write | id, type, name, email, phone, avatar_url, suburb, location, onboarding_completed |
| `worker_profiles` | Write (signup), Read (profile) | Read (match cards) | profile_id, trades, experience_years, certifications, availability, bio |
| `companies` | Read (via jobs) | Write (signup), Read | profile_id, name, abn, address, industry, website |
| `jobs` | Read (browse) | Write (post), Read | company_id, title, trade, suburb, rate_min, rate_max, status |
| `matches` | Write (apply), Read | Read (browse), Write (hire/reject) | job_id, worker_id, status, match_score, hired_at, fee_charged |
| `payments` | — | Write (Stripe) | match_id, company_id, amount, status, stripe_payment_id |
| `ratings` | Write/Read | Write/Read | match_id, from_id, to_id, score, comment |
| `conversations` | Read/Write | Read/Write | id |
| `conversation_participants` | Read/Write | Read/Write | conversation_id, profile_id |
| `messages` | Read/Write | Read/Write | conversation_id, sender_id, content |
| `notifications` | Read | Read | (schema not fully explored) |

---

## 8. RECOMMENDATIONS SUMMARY

| Priority | Issue | Fix |
|----------|-------|-----|
| **P0** | Messaging API has no hire/payment gate | Add check to `/api/messages/start` verifying a `hired` match exists between sender and recipient |
| **P0** | RLS policies may allow direct profile queries with phone/email | Audit and restrict RLS policies; create a view that excludes sensitive columns |
| **P1** | Job descriptions can contain contact info | Add server-side sanitisation to strip phone numbers, emails, and URLs from job descriptions |
| **P1** | Worker name derived from email prefix | Let workers set a display name during onboarding instead of using `email.split("@")[0]` |
| **P2** | Post-first-hire repeat bypass | Consider subscription model, per-job fees, or "re-hire" discounts; add "hire again" flow through platform |
| **P2** | AI summaries may leak identifying data | Review AI prompt to ensure it doesn't include overly specific identifying information |
| **P3** | Contractor company profile visible after apply | Consider limiting company info visibility until hire, or accept as acceptable business info |
