     1→import { NextRequest, NextResponse } from 'next/server';
     2→import { checkRateLimit, getClientIP, createRateLimitResponse } from '@/lib/rate-limit';
     3→
     4→// Cache to store logo URLs (in production, use Redis or database)
     5→const logoCache = new Map<string, { logoUrl: string | null; source: string; timestamp: number }>();
     6→const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
     7→
     8→interface LogoResponse {
     9→  logoUrl: string | null;
    10→  source: 'clearbit' | 'google' | 'none';
    11→}
    12→
    13→// Rate limit: 30 requests per 15 minutes per IP
    14→const LOGO_RATE_LIMIT = 30;
    15→const LOGO_WINDOW_MS = 15 * 60 * 1000;
    16→
    17→async function checkLogoExists(url: string): Promise<boolean> {
    18→  try {
    19→    const response = await fetch(url, {
    20→      method: 'HEAD',
    21→      headers: {
    22→        'User-Agent': 'RateRight/1.0',
    23→      },
    24→    });
    25→    return response.ok;
    26→  } catch {
    27→    return false;
    28→  }
    29→}
    30→
    31→export async function GET(request: NextRequest) {
    32→  try {
    33→    // --- Rate limiting ---
    34→    const clientIP = getClientIP(request);
    35→    const rateLimit = checkRateLimit(`company-logo:${clientIP}`, LOGO_RATE_LIMIT, LOGO_WINDOW_MS);
    36→
    37→    if (!rateLimit.allowed) {
    38→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    39→      return createRateLimitResponse(clientIP, retryAfter);
    40→    }
    41→
    42→    const searchParams = request.nextUrl.searchParams;
    43→    const domain = searchParams.get('domain');
    44→
    45→    if (!domain) {
    46→      return NextResponse.json<LogoResponse>(
    47→        { logoUrl: null, source: 'none' },
    48→        { status: 400, headers: getCORSHeaders(request) }
    49→      );
    50→    }
    51→
    52→    // Clean the domain (remove protocol if present)
    53→    const cleanDomain = domain.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
    54→
    55→    // Check cache first
    56→    const cacheKey = cleanDomain;
    57→    const cached = logoCache.get(cacheKey);
    58→    if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
    59→      return NextResponse.json<LogoResponse>({
    60→        logoUrl: cached.logoUrl,
    61→        source: cached.source as LogoResponse['source'],
    62→      }, {
    63→        headers: getCORSHeaders(request),
    64→      });
    65→    }
    66→
    67→    // Try Clearbit Logo API first
    68→    const clearbitUrl = `https://logo.clearbit.com/${cleanDomain}`;
    69→    const clearbitExists = await checkLogoExists(clearbitUrl);
    70→
    71→    if (clearbitExists) {
    72→      logoCache.set(cacheKey, {
    73→        logoUrl: clearbitUrl,
    74→        source: 'clearbit',
    75→        timestamp: Date.now(),
    76→      });
    77→
    78→      return NextResponse.json<LogoResponse>({
    79→        logoUrl: clearbitUrl,
    80→        source: 'clearbit',
    81→      }, {
    82→        headers: getCORSHeaders(request),
    83→      });
    84→    }
    85→
    86→    // Try Google Favicon as fallback
    87→    const googleUrl = `https://www.google.com/s2/favicons?domain=${cleanDomain}&sz=128`;
    88→    const googleExists = await checkLogoExists(googleUrl);
    89→
    90→    if (googleExists) {
    91→      logoCache.set(cacheKey, {
    92→        logoUrl: googleUrl,
    93→        source: 'google',
    94→        timestamp: Date.now(),
    95→      });
    96→
    97→      return NextResponse.json<LogoResponse>({
    98→        logoUrl: googleUrl,
    99→        source: 'google',
   100→      }, {
   101→        headers: getCORSHeaders(request),
   102→      });
   103→    }
   104→
   105→    // No logo found
   106→    logoCache.set(cacheKey, {
   107→      logoUrl: null,
   108→      source: 'none',
   109→      timestamp: Date.now(),
   110→    });
   111→
   112→    return NextResponse.json<LogoResponse>({
   113→      logoUrl: null,
   114→      source: 'none',
   115→    }, {
   116→      headers: getCORSHeaders(request),
   117→    });
   118→
   119→  } catch (error) {
   120→    console.error('Logo fetch error:', error);
   121→
   122→    return NextResponse.json<LogoResponse>(
   123→      { logoUrl: null, source: 'none' },
   124→      { status: 500, headers: getCORSHeaders(request) }
   125→    );
   126→  }
   127→}
   128→
   129→// CORS configuration - restrict to known origins
   130→const ALLOWED_ORIGINS = [
   131→  process.env.NEXT_PUBLIC_APP_URL, // Production URL from env
   132→  'http://localhost:3000',         // Local development
   133→  'https://localhost:3000',        // Local development with HTTPS
   134→].filter(Boolean) as string[];
   135→
   136→function getCORSHeaders(request: NextRequest): Record<string, string> {
   137→  const origin = request.headers.get('origin');
   138→
   139→  // Only allow specific origins, no wildcards (H5 fix)
   140→  const allowedOrigin = origin && ALLOWED_ORIGINS.includes(origin)
   141→    ? origin
   142→    : ALLOWED_ORIGINS[0] || 'null';
   143→
   144→  return {
   145→    'Access-Control-Allow-Origin': allowedOrigin,
   146→    'Access-Control-Allow-Methods': 'GET, OPTIONS',
   147→    'Access-Control-Allow-Headers': 'Content-Type',
   148→    'Vary': 'Origin',
   149→  };
   150→}
   151→
   152→// Add OPTIONS handler for CORS
   153→export async function OPTIONS(request: NextRequest) {
   154→  return new NextResponse(null, {
   155→    status: 200,
   156→    headers: getCORSHeaders(request),
   157→  });
   158→}

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→import { getSupabaseAdmin, createClient } from '@/lib/supabase/server';
     2→import { NextRequest, NextResponse } from 'next/server';
     3→import { checkRateLimit, getClientIP, createRateLimitResponse } from '@/lib/rate-limit';
     4→import { createPaymentIntent } from '@/lib/stripe/server';
     5→import { withCSRFProtection } from '@/lib/csrf';
     6→
     7→// Rate limit: 10 requests per 15 minutes per user (sensitive financial operation)
     8→const PAYMENT_RATE_LIMIT = 10;
     9→const PAYMENT_WINDOW_MS = 15 * 60 * 1000;
    10→
    11→const handler = async (request: NextRequest) => {
    12→  try {
    13→    // --- Rate limiting (by IP as initial check) ---
    14→    const clientIP = getClientIP(request);
    15→    const ipRateLimit = checkRateLimit(`payment-ip:${clientIP}`, PAYMENT_RATE_LIMIT, PAYMENT_WINDOW_MS);
    16→
    17→    if (!ipRateLimit.allowed) {
    18→      const retryAfter = Math.ceil((ipRateLimit.resetTime - Date.now()) / 1000);
    19→      return createRateLimitResponse(clientIP, retryAfter);
    20→    }
    21→
    22→    const supabase = await createClient();
    23→    const {
    24→      data: { user },
    25→    } = await supabase.auth.getUser();
    26→
    27→    if (!user) {
    28→      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    29→    }
    30→
    31→    // --- Stricter rate limiting by user ID ---
    32→    const userRateLimit = checkRateLimit(`payment-user:${user.id}`, PAYMENT_RATE_LIMIT, PAYMENT_WINDOW_MS);
    33→
    34→    if (!userRateLimit.allowed) {
    35→      const retryAfter = Math.ceil((userRateLimit.resetTime - Date.now()) / 1000);
    36→      return createRateLimitResponse(user.id, retryAfter);
    37→    }
    38→
    39→    const body = await request.json();
    40→    const { match_id, company_id } = body;
    41→    const amount = 50.0; // Hardcoded amount to prevent client override
    42→
    43→    if (!match_id || !company_id) {
    44→      return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
    45→    }
    46→
    47→    // Verify user owns the company
    48→    const { data: company } = await supabase
    49→      .from('companies')
    50→      .select('id, name')
    51→      .eq('id', company_id)
    52→      .eq('profile_id', user.id)
    53→      .single();
    54→
    55→    if (!company) {
    56→      return NextResponse.json({ error: 'Company not found or unauthorized' }, { status: 403 });
    57→    }
    58→
    59→    // Verify match exists and belongs to company's job (can be in any status except rejected)
    60→    const { data: match } = await supabase
    61→      .from('matches')
    62→      .select('status, jobs!inner(company_id, title)')
    63→      .eq('id', match_id)
    64→      .neq('status', 'rejected')
    65→      .eq('jobs.company_id', company_id)
    66→      .single();
    67→
    68→    if (!match) {
    69→      return NextResponse.json({ error: 'Match not found, already rejected, or unauthorized' }, { status: 403 });
    70→    }
    71→
    72→    // Check if payment already exists for this match
    73→    const { data: existingPayment } = await supabase
    74→      .from('payments')
    75→      .select('id, status, stripe_payment_id')
    76→      .eq('match_id', match_id)
    77→      .single();
    78→
    79→    if (existingPayment?.status === 'charged') {
    80→      return NextResponse.json({ error: 'Payment already completed for this hire' }, { status: 400 });
    81→    }
    82→
    83→    // Create Stripe PaymentIntent
    84→    const jobTitle = (match.jobs as { title?: string }).title || 'Job';
    85→    const { clientSecret, paymentIntentId } = await createPaymentIntent({
    86→      amount,
    87→      matchId: match_id,
    88→      companyId: company_id,
    89→      companyName: company.name,
    90→      description: `Hire for ${jobTitle}`,
    91→    });
    92→
    93→    // Create or update payment record using service role
    94→    const supabaseService = getSupabaseAdmin();
    95→
    96→    let payment;
    97→    if (existingPayment) {
    98→      // Update existing payment record with new PaymentIntent
    99→      const { data: updatedPayment, error } = await supabaseService
   100→        .from('payments')
   101→        .update({
   102→          stripe_payment_id: paymentIntentId,
   103→          amount,
   104→          status: 'pending',
   105→        })
   106→        .eq('id', existingPayment.id)
   107→        .select()
   108→        .single();
   109→
   110→      if (error) {
   111→        return NextResponse.json({ error: error.message }, { status: 500 });
   112→      }
   113→      payment = updatedPayment;
   114→    } else {
   115→      // Create new payment record
   116→      const { data: newPayment, error } = await supabaseService
   117→        .from('payments')
   118→        .insert({
   119→          match_id,
   120→          company_id,
   121→          amount,
   122→          status: 'pending',
   123→          stripe_payment_id: paymentIntentId,
   124→        })
   125→        .select()
   126→        .single();
   127→
   128→      if (error) {
   129→        return NextResponse.json({ error: error.message }, { status: 500 });
   130→      }
   131→      payment = newPayment;
   132→    }
   133→
   134→    return NextResponse.json({
   135→      payment,
   136→      clientSecret,
   137→      publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
   138→    });
   139→  } catch (error) {
   140→    console.error('Payment creation error:', error);
   141→    if (error instanceof Error && error.message.includes('Stripe')) {
   142→      return NextResponse.json({ error: 'Payment service unavailable' }, { status: 503 });
   143→    }
   144→    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
   145→  }
   146→}
   147→
   148→// Apply CSRF protection to the handler
   149→export const POST = withCSRFProtection(handler);
   150→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Perfect! Now let me read a few more key files to get the complete picture:


100→  const swipeDirection = swipeX > 0 ? "right" : "left";
   101→
   102→  return (
   103→

   104→      {/* Swipe indicator backgrounds */}
   105→      {!isHired && swipeX !== 0 && (
   106→        <>
   107→          {/* Right swipe (hire) */}
   108→          {swipeX > 0 && (
   109→

   116→              <UserCheck className="h-8 w-8 text-green-600" />
   117→
   118→                {swipeProgress >= 1 ? "Release to hire" : "Swipe to hire"}
   119→
   120→

   121→          )}
   122→          {/* Left swipe (reject) */}
   123→          {swipeX < 0 && (
   124→

   131→
   132→                {swipeProgress >= 1 ? "Release to pass" : "Swipe to pass"}
   133→
   134→              <AlertCircle className="h-8 w-8 text-red-600" />
   135→

   136→          )}
   137→        </>
   138→      )}
   139→
   140→      <Card
   141→        className={`transition-all touch-pan-y ${justHired ? "ring-2 ring-green-500" : ""}`}
   142→        style={{
   143→          transform: `translateX(${swipeX}px)`,
   144→          transition: swiping ? 'none' : 'transform 0.2s ease-out',
   145→        }}
   146→        onTouchStart={handleTouchStart}
   147→        onTouchMove={handleTouchMove}
   148→        onTouchEnd={handleTouchEnd}
   149→      >
   150→        <CardContent className="p-5">
   151→

   152→            {/* Avatar */}
   153→

   154→              {match.worker?.name?.[0]?.toUpperCase() || "?"}
   155→

   156→
   157→

   158→

   159→

   160→                  {match.worker?.name || "Worker"}
   161→

   162→                {isHired && (
   163→                  <Badge className="bg-green-600 text-xs shrink-0">Hired</Badge>
   164→                )}
   165→

   166→
   167→              {/* AI Summary */}
   168→

   169→                {summary}
   170→

   171→
   172→              {/* Details */}
   173→

   174→                {match.worker_profile?.experience_years && (
   175→
   176→                    <Clock className="h-4 w-4" />
   177→                    {match.worker_profile.experience_years}yr exp
   178→
   179→                )}
   180→                {match.worker?.suburb && (
   181→
   182→                    <MapPin className="h-4 w-4" />
   183→                    {match.worker.suburb}
   184→
   185→                )}
   186→                {match.match_score && (
   187→
   188→                    <Star className="h-4 w-4 fill-amber-400 text-amber-400" />
   189→                    {Math.round(match.match_score)}% match
   190→
   191→                )}
   192→

   193→
   194→              {/* Certs */}
   195→              {match.worker_profile?.certifications && match.worker_profile.certifications.length > 0 && (
   196→

   197→                  {match.worker_profile.certifications.slice(0, 3).map((cert) => (
   198→                    <Badge key={cert} variant="outline" className="text-xs px-2 py-1">
   199→                      {cert}
   200→                    </Badge>
   201→                  ))}
   202→

   203→              )}
   204→

   205→

   206→
   207→          {/* Action */}
   208→          {!isHired && (
   209→            <Button
   210→              onClick={() => onHire(match)}
   211→              className="mt-4 w-full h-14 text-base font-semibold"
   212→              disabled={hiring}
   213→            >
   214→              {hiring ? (
   215→                <>
   216→                  <Loader2 className="mr-2 h-5 w-5 animate-spin" />
   217→                  Processing...
   218→                </>
   219→              ) : (
   220→                <>
   221→                  <UserCheck className="mr-2 h-5 w-5" />
   222→                  Hire — $50 Fee
   223→                </>
   224→              )}
   225→            </Button>
   226→          )}
   227→
   228→          {justHired && (
   229→

   230→              <CheckCircle2 className="h-5 w-5" />
   231→              Hired! Worker has been notified.
   232→

   233→          )}
   234→
   235→          {isHired && !justHired && (
   236→

   237→              <CheckCircle2 className="h-5 w-5 text-green-600" />
   238→              Hired on{" "}
   239→              {match.hired_at
   240→                ? new Date(match.hired_at).toLocaleDateString("en-AU", {
   241→                    day: "numeric",
   242→                    month: "short",
   243→                  })
   244→                : "recently"}
   245→

   246→          )}
   247→        </CardContent>
   248→      </Card>
   249→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Bash

find /home/ccuser/the-50-dollar-app/src/lib -type f -name "*.ts" | head -20


/home/ccuser/the-50-dollar-app/src/lib/stripe/server.ts
/home/ccuser/the-50-dollar-app/src/lib/csrf-client.ts
/home/ccuser/the-50-dollar-app/src/lib/abn-utils.ts
/home/ccuser/the-50-dollar-app/src/lib/supabase/client.ts
/home/ccuser/the-50-dollar-app/src/lib/supabase/middleware.ts
/home/ccuser/the-50-dollar-app/src/lib/supabase/server.ts
/home/ccuser/the-50-dollar-app/src/lib/messaging.ts
/home/ccuser/the-50-dollar-app/src/lib/sanitize.ts
/home/ccuser/the-50-dollar-app/src/lib/location-utils.ts
/home/ccuser/the-50-dollar-app/src/lib/types.ts
/home/ccuser/the-50-dollar-app/src/lib/utils.ts
/home/ccuser/the-50-dollar-app/src/lib/rate-limit.ts
/home/ccuser/the-50-dollar-app/src/lib/company-utils.ts
/home/ccuser/the-50-dollar-app/src/lib/api-errors.ts
/home/ccuser/the-50-dollar-app/src/lib/csrf.ts


total 72
drwxrwxr-x 4 ccuser ccuser 4096 Feb  8 11:18 .
drwxrwxr-x 5 ccuser ccuser 4096 Feb  8 11:18 ..
-rw-rw-r-- 1 ccuser ccuser 2606 Feb  8 11:18 abn-utils.ts
-rw-rw-r-- 1 ccuser ccuser 3487 Feb  8 11:18 api-errors.ts
-rw-rw-r-- 1 ccuser ccuser 2807 Feb  8 11:18 company-utils.ts
-rw-rw-r-- 1 ccuser ccuser 2210 Feb  8 11:18 csrf-client.ts
-rw-rw-r-- 1 ccuser ccuser 6160 Feb  8 11:18 csrf.ts
-rw-rw-r-- 1 ccuser ccuser 3484 Feb  8 11:18 error-handling.tsx
-rw-rw-r-- 1 ccuser ccuser 1668 Feb  8 11:18 location-utils.ts
-rw-rw-r-- 1 ccuser ccuser  531 Feb  8 11:18 messaging.ts
-rw-rw-r-- 1 ccuser ccuser 5040 Feb  8 11:18 rate-limit.ts
-rw-rw-r-- 1 ccuser ccuser 1822 Feb  8 11:18 sanitize.ts
drwxrwxr-x 2 ccuser ccuser 4096 Feb  8 11:18 stripe
drwxrwxr-x 2 ccuser ccuser 4096 Feb  8 11:46 supabase
-rw-rw-r-- 1 ccuser ccuser 3520 Feb  8 11:18 types.ts
-rw-rw-r-- 1 ccuser ccuser  166 Feb  8 11:18 utils.ts


Excellent. Let me read the types file to understand the data structures:


     1→export type UserType = "worker" | "contractor";
     2→
     3→export type Availability = "full-time" | "casual" | "weekends" | "flexible";
     4→
     5→export type RateType = "hourly" | "daily";
     6→
     7→export type JobStatus = "draft" | "active" | "filled" | "closed";
     8→
     9→export type MatchStatus =
    10→  | "suggested"
    11→  | "applied"
    12→  | "accepted"
    13→  | "rejected"
    14→  | "hired";
    15→
    16→export type ActionStatus = "pending" | "accepted" | "rejected";
    17→
    18→export type PaymentStatus = "pending" | "charged" | "refunded";
    19→
    20→export type NotificationType = "match" | "hire_request" | "hire_confirmed" | "rating" | "job_posted" | "system";
    21→
    22→export interface Profile {
    23→  id: string;
    24→  type: UserType;
    25→  name: string | null;
    26→  phone: string | null;
    27→  email: string | null;
    28→  avatar_url: string | null;
    29→  /**
    30→   * Location as { lat, lng } object
    31→   * Note: Database stores this as PostgreSQL point type
    32→   * Use postgresPointToJson() when reading from DB
    33→   * Use jsonToPostgresPoint() when writing to DB
    34→   */
    35→  location: { lat: number; lng: number } | null;
    36→  suburb: string | null;
    37→  verified: boolean;
    38→  onboarding_completed: boolean;
    39→  created_at: string;
    40→  updated_at: string;
    41→  ai_generated: boolean;
    42→}
    43→
    44→export interface Company {
    45→  id: string;
    46→  profile_id: string;
    47→  name: string;
    48→  abn: string | null;
    49→  address: string | null;
    50→  industry: string | null;
    51→  size: string | null;
    52→  website: string | null;
    53→  logo_url: string | null;
    54→  ai_research: Record<string, unknown> | null;
    55→  created_at: string;
    56→  updated_at: string;
    57→}
    58→
    59→export interface WorkerProfile {
    60→  id: string;
    61→  profile_id: string;
    62→  trades: string[];
    63→  experience_years: number | null;
    64→  certifications: string[];
    65→  white_card_number: string | null;
    66→  white_card_verified: boolean;
    67→  availability: Availability | null;
    68→  languages: string[];
    69→  bio: string | null;
    70→  voice_note_url: string | null;
    71→  ai_extracted: Record<string, unknown> | null;
    72→  created_at: string;
    73→  updated_at: string;
    74→}
    75→
    76→export interface Job {
    77→  id: string;
    78→  company_id: string;
    79→  title: string;
    80→  description: string | null;
    81→  trade: string;
    82→  /**
    83→   * Location as { lat, lng } object
    84→   * Note: Database stores this as PostgreSQL point type
    85→   * Use postgresPointToJson() when reading from DB
    86→   * Use jsonToPostgresPoint() when writing to DB
    87→   */
    88→  location: { lat: number; lng: number } | null;
    89→  suburb: string | null;
    90→  site_address: string | null;
    91→  rate_min: number | null;
    92→  rate_max: number | null;
    93→  rate_type: RateType;
    94→  start_date: string | null;
    95→  duration: string | null;
    96→  workers_needed: number;
    97→  certifications_required: string[];
    98→  status: JobStatus;
    99→  ai_generated: boolean;
   100→  created_at: string;
   101→  updated_at: string;
   102→}
   103→
   104→export interface Match {
   105→  id: string;
   106→  job_id: string;
   107→  worker_id: string;
   108→  status: MatchStatus;
   109→  match_score: number | null;
   110→  ai_summary: string | null;
   111→  contractor_action: ActionStatus;
   112→  worker_action: ActionStatus;
   113→  hired_at: string | null;
   114→  fee_charged: boolean;
   115→  created_at: string;
   116→  updated_at: string;
   117→}
   118→
   119→export interface Rating {
   120→  id: string;
   121→  match_id: string;
   122→  from_id: string;
   123→  to_id: string;
   124→  score: number;
   125→  comment: string | null;
   126→  created_at: string;
   127→}
   128→
   129→export interface Payment {
   130→  id: string;
   131→  match_id: string;
   132→  company_id: string;
   133→  amount: number;
   134→  status: PaymentStatus;
   135→  stripe_payment_id: string | null;
   136→  created_at: string;
   137→}
   138→
   139→export interface Notification {
   140→  id: string;
   141→  profile_id: string;
   142→  type: NotificationType;
   143→  title: string;
   144→  body: string | null;
   145→  data: Record<string, unknown> | null;
   146→  read: boolean;
   147→  created_at: string;
   148→}
   149→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


cat /home/ccuser/the-50-dollar-app/supabase/schema.sql | grep -A 50 "-- Matches"


Error: Exit code 2
grep: unrecognized option '-- Matches'
Usage: grep [OPTION]... PATTERNS [FILE]...
Try 'grep --help' for more information.


Let me check the schema for the matches table directly:


   120→  BEFORE UPDATE ON jobs
   121→  FOR EACH ROW EXECUTE FUNCTION update_updated_at();
   122→
   123→-- Matches
   124→CREATE TABLE matches (
   125→  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
   126→  job_id uuid NOT NULL REFERENCES jobs ON DELETE CASCADE,
   127→  worker_id uuid NOT NULL REFERENCES profiles ON DELETE CASCADE,
   128→  status match_status DEFAULT 'suggested',
   129→  match_score decimal,
   130→  ai_summary text,
   131→  contractor_action action_status DEFAULT 'pending',
   132→  worker_action action_status DEFAULT 'pending',
   133→  hired_at timestamptz,
   134→  fee_charged boolean DEFAULT false,
   135→  created_at timestamptz DEFAULT now(),
   136→  updated_at timestamptz DEFAULT now(),
   137→  UNIQUE (job_id, worker_id)
   138→);
   139→
   140→CREATE TRIGGER matches_updated_at
   141→  BEFORE UPDATE ON matches
   142→  FOR EACH ROW EXECUTE FUNCTION update_updated_at();
   143→
   144→-- Ratings
   145→CREATE TABLE ratings (
   146→  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
   147→  match_id uuid NOT NULL REFERENCES matches ON DELETE CASCADE,
   148→  from_id uuid NOT NULL REFERENCES profiles ON DELETE CASCADE,
   149→  to_id uuid NOT NULL REFERENCES profiles ON DELETE CASCADE,
   150→  score integer NOT NULL CHECK (score BETWEEN 1 AND 5),
   151→  comment text,
   152→  created_at timestamptz DEFAULT now(),
   153→  UNIQUE (match_id, from_id)
   154→);
   155→
   156→-- Payments
   157→CREATE TABLE payments (
   158→  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
   159→  match_id uuid NOT NULL REFERENCES matches ON DELETE CASCADE,
   160→  company_id uuid NOT NULL REFERENCES companies ON DELETE CASCADE,
   161→  amount decimal DEFAULT 50.00,
   162→  status payment_status DEFAULT 'pending',
   163→  stripe_payment_id text,
   164→  created_at timestamptz DEFAULT now()
   165→);
   166→
   167→-- Notifications
   168→CREATE TABLE notifications (
   169→  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


grep -r "privacy\|terms\|tos" /home/ccuser/the-50-dollar-app/src --include="*.tsx" --include="*.ts" 2>/dev/null | head -5


/home/ccuser/the-50-dollar-app/src/app/faq/page.tsx:    a: "Payment terms are agreed directly with the contractor before starting work. Most contractors pay weekly or fortnightly. RateRight does not handle payments.",


Perfect! Now let me compile my findings. Let me create one final summary by checking the main directory structure:


find /home/ccuser/the-50-dollar-app/src -type f \( -name "*.tsx" -o -name "*.ts" \) | wc -l && find /home/ccuser/the-50-dollar-app/src -type f -name "*.tsx" -o -name "*.ts" | grep -E "(page|route)" | sort


Perfect! Now I have all the information I need. Let me compile the comprehensive report.

## Codebase Exploration Report: RateRight v2

I've thoroughly explored the RateRight v2 codebase. Here's a comprehensive summary of my findings:

---

### 1. **MATCHING LOGIC & API ENDPOINTS**

#### Matching Pages (No dedicated match API yet - all logic is frontend)
- **Contractor Match Browsing:** `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/matches/page.tsx` (19,738 bytes)
  - Displays swipeable match cards for contractors to hire workers
  - Implements touch-based swipe UI (left=reject, right=hire)
  - Shows worker details: trades, experience, certifications, location, availability
  - AI-generated summary for each match
  - Direct hire button with $50 fee modal

- **Worker Job Browsing:** `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/jobs/page.tsx` (15,327 bytes)
  - Swipeable job cards for workers to apply/reject
  - Filter by trade with TRADE_FILTERS array
  - Shows job details: location, pay rate, duration, contractor name

#### Matches Table Schema
**File:** `/home/ccuser/the-50-dollar-app/supabase/schema.sql` (lines 123-142)
```sql
CREATE TABLE matches (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  job_id uuid NOT NULL REFERENCES jobs ON DELETE CASCADE,
  worker_id uuid NOT NULL REFERENCES profiles ON DELETE CASCADE,
  status match_status DEFAULT 'suggested',  -- suggested|applied|accepted|rejected|hired
  match_score decimal,
  ai_summary text,
  contractor_action action_status DEFAULT 'pending',  -- pending|accepted|rejected
  worker_action action_status DEFAULT 'pending',
  hired_at timestamptz,
  fee_charged boolean DEFAULT false,
  created_at timestamptz DEFAULT now(),
  updated_at timestamptz DEFAULT now(),
  UNIQUE (job_id, worker_id)
);
```

#### Match TypeScript Interface
**File:** `/home/ccuser/the-50-dollar-app/src/lib/types.ts` (lines 104-117)
```typescript
export interface Match {
  id: string;
  job_id: string;
  worker_id: string;
  status: MatchStatus;  // "suggested" | "applied" | "accepted" | "rejected" | "hired"
  match_score: number | null;
  ai_summary: string | null;
  contractor_action: ActionStatus;
  worker_action: ActionStatus;
  hired_at: string | null;
  fee_charged: boolean;
  created_at: string;
  updated_at: string;
}
```

#### Match-Related Supabase RLS Policies
**File:** `/home/ccuser/the-50-dollar-app/supabase/schema.sql` (lines 286-300)
- Users can only view matches they're involved in (as worker or contractor)
- System can insert new matches (for future smart matching)

---

### 2. **SIGNUP PAGES & ONBOARDING FLOWS**

#### Initial Signup (Unauthenticated)
**File:** `/home/ccuser/the-50-dollar-app/src/app/auth/signup/page.tsx` (139 lines)
- Email/password signup with user type selection (contractor/worker)
- Redirects to `/auth/verify-email` after signup
- Uses Supabase Auth with custom `type` data field

#### Worker Signup (Multi-step)
**File:** `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/signup/page.tsx` (820 lines)
**Steps:**
1. **Auth** - Email/password creation
2. **Method** - Choose input method (type answers or voice note - voice coming soon)
3. **Trade** - Select one or more trades from predefined list (Labourer, Steel Fixer, Formworker, Carpenter, Electrician, Plumber, Concreter, Bricklayer, Painter, Roofer, Scaffolder, Tiler, Plasterer, Welder, Crane Operator, Excavator Operator)
4. **Experience** - Years of construction experience (0, 2, 4, 7, 12+ years)
5. **Location** - Suburb/area
6. **Availability** - Full-time, Casual, Weekends, or Flexible
7. **Certifications** - Optional (White Card, Forklift, EWP, Working at Heights, First Aid, Traffic Control, Rigging, Dogging, Confined Spaces)
8. **Review** - AI-generated bio and confirmation

**Creates in DB:**
- Updates `profiles` table (sets name, suburb, onboarding_completed)
- Inserts into `worker_profiles` table with all answers + AI-extracted metadata

#### Contractor Signup (Multi-step)
**File:** `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/signup/page.tsx` (705 lines)
**Steps:**
1. **Auth** - Email/password creation
2. **Company** - ABN lookup and verification
3. **Researching** - Loading state while calling ABR
4. **Review** - Editable company details (name, ABN, address, industry, size, website)

**Creates in DB:**
- Updates `profiles` table (sets name, onboarding_completed)
- Inserts into `companies` table with ABR-verified data + logo lookup

**Special Features:**
- ABN validation via `/api/abn-lookup` endpoint
- Company logo lookup via Clearbit or Google Favicon
- Email verification required before contractor signup completion

---

### 3. **API ENDPOINTS**

#### Authentication Endpoints
- **POST** `/api/auth/csrf` - CSRF token generation
- **POST** `/api/auth/verify-email` - Email verification link handler
- **GET** `/auth/callback` - OAuth callback route

#### Matching/Hiring Endpoints
- **POST** `/api/payments/create` - Create Stripe payment for $50 hire fee
  - Validates: user ownership of company, match exists, payment not already charged
  - Uses CSRF protection and rate limiting (10 req/15 min per user)
  - Returns Stripe `clientSecret` and `publishableKey`

#### Company/ABN Endpoints
- **GET** `/api/abn-lookup?abn=XXXXXXXXXXX` - ABN lookup via Australian Business Register
  - Development mode with mock data for testing
  - Rate limited: 10 requests per 15 minutes per IP
  - Returns: company name, ABN, status, state, postcode, type, GST status
  - Director verification stub (pending ASIC Connect integration)

- **GET** `/api/company-logo?domain=example.com` - Company logo lookup
  - Tries Clearbit first, fallback to Google Favicon
  - 24-hour cache per domain
  - Rate limited: 30 requests per 15 minutes per IP

#### Messaging Endpoints
- **GET** `/api/messages` - List conversations
- **POST** `/api/messages/start` - Start new conversation
- **GET** `/api/messages/[id]` - Get conversation details
- **POST** `/api/messages/[id]/send` - Send message

#### AI Endpoints
- **POST** `/api/ai/transcribe-voice` - Transcribe voice to text (Whisper)
- **POST** `/api/ai/parse-profile-voice` - Parse worker profile from voice
- **POST** `/api/ai/generate-profile` - Generate profile bio from text

#### Payment Webhooks
- **POST** `/api/webhooks/stripe` - Stripe webhook handler for payment status

---

### 4. **SIGNUP FORM/PAGE COMPONENTS**

**Initial Signup Page**
- Path: `/home/ccuser/the-50-dollar-app/src/app/auth/signup/page.tsx`
- User type selection with icons (Building2 for Contractor, HardHat for Worker)
- Email input field
- Password input field (minLength 6)
- Error display
- Links to login page

**Worker Signup Components**
- Path: `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/signup/page.tsx`
- Progress bar (8 steps)
- Step-based form with forward/back navigation
- StepHeader component (icon + title + subtitle)
- ReviewField component (editable fields)
- Swipeable card layout
- AI-generated bio display with Sparkles icon

**Contractor Signup Components**
- Path: `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/signup/page.tsx`
- Progress bar (3-4 steps)
- EditableField component (tap to edit)
- Company logo display in name field
- ABN input with validation
- Verification loading spinner
- Green checkmark for verified businesses

---

### 5. **TERMS OF SERVICE & PRIVACY POLICY PAGES**

**Status: NOT IMPLEMENTED**

Searched entire codebase for:
- `/privacy`, `/privacy-policy` - No files found
- `/terms`, `/terms-of-service`, `/tos` - No files found
- No legal pages in page.tsx files
- No legal content in FAQ

**Existing Legal-Related Content:**
- **FAQ Page:** `/home/ccuser/the-50-dollar-app/src/app/faq/page.tsx`
  - Worker FAQs (free platform, White Card required, payment handled directly)
  - Contractor FAQs ($50 flat fee per placement, worker verification, direct payment)
  - General FAQs (not a labour hire company, marketplace model)
  - Covers areas of operation (Sydney, Melbourne, Brisbane, Perth, Adelaide)

**Recommendation:** Need to create:
- `/src/app/legal/privacy-policy/page.tsx`
- `/src/app/legal/terms-of-service/page.tsx`
- `/src/app/legal/about/page.tsx`

---

### 6. **DATABASE SCHEMA OVERVIEW**

**File:** `/home/ccuser/the-50-dollar-app/supabase/schema.sql`

**Tables (11 total):**
1. `profiles` - User account base (id, type, name, email, avatar_url, location, suburb, verified, onboarding_completed)
2. `companies` - Contractor company info (profile_id FK, name, abn, address, industry, size, website, ai_research)
3. `worker_profiles` - Worker-specific data (profile_id FK, trades[], experience_years, certifications[], availability, bio, ai_extracted)
4. `jobs` - Job postings (company_id FK, title, description, trade, location, suburb, rate_min/max, start_date, duration, workers_needed, certifications_required[], status)
5. `matches` - Worker-job connections (job_id FK, worker_id FK, status, match_score, ai_summary, contractor_action, worker_action, hired_at, fee_charged)
6. `ratings` - Hire ratings (match_id FK, from_id, to_id, score 1-5, comment)
7. `payments` - Stripe payments (match_id FK, company_id FK, amount 50.00, status, stripe_payment_id)
8. `notifications` - User notifications (profile_id FK, type, title, body, data, read)
9. `conversations` - Messaging threads
10. `conversation_participants` - Participants in conversations
11. `messages` - Chat messages

**Enums:**
- `user_type`: 'worker' | 'contractor'
- `availability_type`: 'full-time' | 'casual' | 'weekends' | 'flexible'
- `rate_type`: 'hourly' | 'daily'
- `job_status`: 'draft' | 'active' | 'filled' | 'closed'
- `match_status`: 'suggested' | 'applied' | 'accepted' | 'rejected' | 'hired'
- `action_status`: 'pending' | 'accepted' | 'rejected'
- `payment_status`: 'pending' | 'charged' | 'refunded'
- `notification_type`: 'match' | 'hire_request' | 'hire_confirmed' | 'rating' | 'job_posted' | 'system'

---

### 7. **KEY LIBRARY FILES**

- `/home/ccuser/the-50-dollar-app/src/lib/types.ts` - TypeScript interfaces for all entities
- `/home/ccuser/the-50-dollar-app/src/lib/abn-utils.ts` - ABN validation and formatting
- `/home/ccuser/the-50-dollar-app/src/lib/company-utils.ts` - Company domain guessing
- `/home/ccuser/the-50-dollar-app/src/lib/rate-limit.ts` - In-memory rate limiting
- `/home/ccuser/the-50-dollar-app/src/lib/csrf.ts` - CSRF protection
- `/home/ccuser/the-50-dollar-app/src/lib/supabase/client.ts` - Client-side Supabase
- `/home/ccuser/the-50-dollar-app/src/lib/supabase/server.ts` - Server-side Supabase with service role
- `/home/ccuser/the-50-dollar-app/src/lib/stripe/server.ts` - Stripe integration

---

### 8. **IMPORTANT OBSERVATIONS**

1. **No dedicated matching/recommendation API exists yet** - All matching happens on the frontend by fetching matches from Supabase
2. **No smart matching algorithm implemented** - `match_score` and `ai_summary` fields exist but aren't populated
3. **Match status never transitions to 'applied'** - Only 'suggested' and 'hired' are used
4. **RLS policies restrict match visibility** - Workers and contractors can only see their own matches
5. **Payment workflow exists** - Creates $50 Stripe payments tied to matches
6. **Email verification required** - Before contractor signup completes
7. **ABN lookup has dev mode** - Uses mock data when ABR_GUID not configured
8. **Messaging system exists** - Separate conversation/message tables with API endpoints
9. **No Terms/Privacy pages** - Only FAQ exists for legal content
10. **Swipe-based UX** - Both matching surfaces use touch swipe gestures (mobile-first design)

---

### **File Paths Summary**

#### Matching:
- `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/matches/page.tsx`
- `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/jobs/page.tsx`

#### Signup:
- `/home/ccuser/the-50-dollar-app/src/app/auth/signup/page.tsx`
- `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/signup/page.tsx`
- `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/signup/page.tsx`

#### API:
- `/home/ccuser/the-50-dollar-app/src/app/api/abn-lookup/route.ts`
- `/home/ccuser/the-50-dollar-app/src/app/api/company-logo/route.ts`
- `/home/ccuser/the-50-dollar-app/src/app/api/payments/create/route.ts`

#### Database:
- `/home/ccuser/the-50-dollar-app/supabase/schema.sql`
- `/home/ccuser/the-50-dollar-app/src/lib/types.ts`

#### Legal:
- `/home/ccuser/the-50-dollar-app/src/app/faq/page.tsx` (only legal-related content)
- **MISSING:** Privacy policy, Terms of Service pages