1→"use client";
     2→
     3→import { useState } from "react";
     4→import { useRouter } from "next/navigation";
     5→import { logger } from "@/lib/logger";
     6→import Link from "next/link";
     7→import toast from "react-hot-toast";
     8→import { createClient } from "@/lib/supabase/client";
     9→import { Button } from "@/components/ui/button";
    10→import { Card, CardContent } from "@/components/ui/card";
    11→import { Input } from "@/components/ui/input";
    12→import { Label } from "@/components/ui/label";
    13→import { Badge } from "@/components/ui/badge";
    14→import {
    15→  ArrowLeft,
    16→  ArrowRight,
    17→  Loader2,
    18→  Sparkles,
    19→  Check,
    20→  MapPin,
    21→  Calendar,
    22→  Users,
    23→  DollarSign,
    24→  HardHat,
    25→  Wrench,
    26→  Hammer,
    27→  Blocks,
    28→  Fence,
    29→  CircleDot,
    30→  Shovel,
    31→} from "lucide-react";
    32→import { jsonToPostgresPoint } from "@/lib/location-utils";
    33→import { csrfFetch } from "@/lib/csrf-client";
    34→
    35→const TRADE_CATEGORIES = [
    36→  { id: "steel-fixer", label: "Steel Fixer", icon: Wrench, color: "bg-blue-500/10 border-blue-500/30 text-blue-700" },
    37→  { id: "formworker", label: "Formworker", icon: Blocks, color: "bg-amber-500/10 border-amber-500/30 text-amber-700" },
    38→  { id: "labourer", label: "Labourer", icon: Shovel, color: "bg-green-500/10 border-green-500/30 text-green-700" },
    39→  { id: "concreter", label: "Concreter", icon: CircleDot, color: "bg-gray-500/10 border-gray-500/30 text-gray-700" },
    40→  { id: "carpenter", label: "Carpenter", icon: Hammer, color: "bg-orange-500/10 border-orange-500/30 text-orange-700" },
    41→  { id: "scaffolder", label: "Scaffolder", icon: Fence, color: "bg-purple-500/10 border-purple-500/30 text-purple-700" },
    42→  { id: "other", label: "Other", icon: HardHat, color: "bg-muted border-border text-foreground" },
    43→];
    44→
    45→const AI_SUGGESTIONS: Record<string, { title: string; description: string; certs: string[]; rateMin: number; rateMax: number; rateType: "hourly" | "daily" }> = {
    46→  "steel-fixer": {
    47→    title: "Steel Fixer Needed",
    48→    description: "Experienced steel fixer required for commercial construction project. Must be able to read drawings, tie rebar, and work with mesh. PPE required on site at all times. Early starts — 6:30am.",
    49→    certs: ["White Card", "Working at Heights"],
    50→    rateMin: 45,
    51→    rateMax: 65,
    52→    rateType: "hourly",
    53→  },
    54→  "formworker": {
    55→    title: "Formworker Required",
    56→    description: "Skilled formworker needed for concrete pours on a residential build. Experience with timber and steel formwork systems. Must be comfortable with early starts and physical work.",
    57→    certs: ["White Card"],
    58→    rateMin: 42,
    59→    rateMax: 58,
    60→    rateType: "hourly",
    61→  },
    62→  "labourer": {
    63→    title: "General Labourer Needed",
    64→    description: "Reliable labourer needed for site clean-up, material handling, and general duties. No specific trade experience needed but must be fit and punctual. PPE and boots required.",
    65→    certs: ["White Card"],
    66→    rateMin: 32,
    67→    rateMax: 42,
    68→    rateType: "hourly",
    69→  },
    70→  "concreter": {
    71→    title: "Concreter Required",
    72→    description: "Experienced concreter needed for slab pours and finishing work. Must know how to screed, float, and finish concrete to a high standard. Early morning pours expected.",
    73→    certs: ["White Card"],
    74→    rateMin: 45,
    75→    rateMax: 65,
    76→    rateType: "hourly",
    77→  },
    78→  "carpenter": {
    79→    title: "Carpenter Needed",
    80→    description: "Qualified carpenter required for framing and fit-out work. Must have own basic tools. Experience with residential or commercial builds preferred.",
    81→    certs: ["White Card", "Trade Certificate"],
    82→    rateMin: 45,
    83→    rateMax: 65,
    84→    rateType: "hourly",
    85→  },
    86→  "scaffolder": {
    87→    title: "Scaffolder Required",
    88→    description: "Licensed scaffolder needed for erection and dismantling of scaffolding systems. Must hold current scaffolding licence. Safety-first attitude essential.",
    89→    certs: ["White Card", "Scaffolding Licence", "Working at Heights"],
    90→    rateMin: 48,
    91→    rateMax: 68,
    92→    rateType: "hourly",
    93→  },
    94→  "other": {
    95→    title: "Construction Worker Needed",
    96→    description: "Worker needed for construction project. Please specify the role and requirements in the description.",
    97→    certs: ["White Card"],
    98→    rateMin: 35,
    99→    rateMax: 55,
   100→    rateType: "hourly",
   101→  },
   102→};
   103→
   104→export default function PostJobPage() {
   105→  const router = useRouter();
   106→  const supabase = createClient();
   107→
   108→  const [step, setStep] = useState(1);
   109→  const [loading, setLoading] = useState(false);
   110→  const [aiLoading, setAiLoading] = useState(false);
   111→
   112→  // Step 1: Trade
   113→  const [selectedTrade, setSelectedTrade] = useState("");
   114→
   115→  // Step 2: AI-filled job details + location/date/workers (COMBINED)
   116→  const [title, setTitle] = useState("");
   117→  const [description, setDescription] = useState("");
   118→  const [certs, setCerts] = useState<string[]>([]);
   119→  const [rateMin, setRateMin] = useState("");
   120→  const [rateMax, setRateMax] = useState("");
   121→  const [rateType, setRateType] = useState<"hourly" | "daily">("hourly");
   122→  const [suburb, setSuburb] = useState("");
   123→  const [siteAddress, setSiteAddress] = useState("");
   124→  const [startDate, setStartDate] = useState("");
   125→  const [duration, setDuration] = useState("");
   126→  const [workersNeeded, setWorkersNeeded] = useState("1");
   127→
   128→  async function handleTradeSelect(tradeId: string) {
   129→    setSelectedTrade(tradeId);
   130→    setAiLoading(true);
   131→    setStep(2);
   132→
   133→    // Try real AI generation, fall back to hardcoded suggestions
   134→    try {
   135→      const res = await csrfFetch("/api/ai/generate-job", {
   136→        method: "POST",
   137→        headers: { "Content-Type": "application/json" },
   138→        body: JSON.stringify({ trade: tradeId }),
   139→      });
   140→
   141→      if (res.ok) {
   142→        const data = await res.json();
   143→        setTitle(data.title);
   144→        setDescription(data.description);
   145→        setCerts(data.certifications);
   146→        setRateMin(String(data.rateMin));
   147→        setRateMax(String(data.rateMax));
   148→        setRateType(data.rateType);
   149→        setAiLoading(false);
   150→        return;
   151→      }
   152→    } catch {
   153→      // Fall through to hardcoded fallback
   154→    }
   155→
   156→    // Fallback: use hardcoded suggestions
   157→    const suggestion = AI_SUGGESTIONS[tradeId];
   158→    if (suggestion) {
   159→      setTitle(suggestion.title);
   160→      setDescription(suggestion.description);
   161→      setCerts(suggestion.certs);
   162→      setRateMin(String(suggestion.rateMin));
   163→      setRateMax(String(suggestion.rateMax));
   164→      setRateType(suggestion.rateType);
   165→    }
   166→    setAiLoading(false);
   167→  }
   168→
   169→  async function handlePost() {
   170→    setLoading(true);
   171→
   172→    try {
   173→      const { data: { user } } = await supabase.auth.getUser();
   174→      if (!user) {
   175→        toast.error("Session expired. Please log in again.");
   176→        router.push("/auth/login");
   177→        return;
   178→      }
   179→
   180→      // Get contractor's company
   181→      const { data: company } = await supabase
   182→        .from("companies")
   183→        .select("id")
   184→        .eq("profile_id", user.id)
   185→        .single();
   186→
   187→      if (!company) {
   188→        toast.error("Please complete your company profile first.");
   189→        setLoading(false);
   190→        return;
   191→      }
   192→
   193→      // Geocode the suburb to get lat/lng coordinates
   194→      let location: string | null = null;
   195→      if (suburb) {
   196→        try {
   197→          const geocodeRes = await csrfFetch("/api/geocode", {
   198→            method: "POST",
   199→            headers: { "Content-Type": "application/json" },
   200→            body: JSON.stringify({ suburb }),
   201→          });
   202→
   203→          if (geocodeRes.ok) {
   204→            const geocodeData = await geocodeRes.json();
   205→            if (geocodeData.location) {
   206→              location = jsonToPostgresPoint(geocodeData.location);
   207→            }
   208→          }
   209→        } catch (geoError) {
   210→          // Continue without location - suburb text will still be saved
   211→        }
   212→      }
   213→
   214→      const { error: insertError } = await supabase.from("jobs").insert({
   215→        company_id: company.id,
   216→        title,
   217→        description,
   218→        trade: selectedTrade,
   219→        suburb,
   220→        location,
   221→        site_address: siteAddress,
   222→        rate_min: parseFloat(rateMin) || null,
   223→        rate_max: parseFloat(rateMax) || null,
   224→        rate_type: rateType,
   225→        start_date: startDate || null,
   226→        duration: duration || null,
   227→        workers_needed: parseInt(workersNeeded) || 1,
   228→        certifications_required: certs,
   229→        status: "active",
   230→        ai_generated: true,
   231→      });
   232→
   233→      if (insertError) {
   234→        toast.error(`Failed to post job: ${insertError.message}`);
   235→        setLoading(false);
   236→        return;
   237→      }
   238→
   239→      setStep(3); // Success (was step 5, now step 3)
   240→    } catch (error) {
   241→      logger.error("Job post error:", error);
   242→      toast.error("Something went wrong. Please try again.");
   243→      setLoading(false);
   244→    }
   245→  }
   246→
   247→  return (
   248→

   249→
   250→        {/* STEP 1: Select Trade */}
   251→        {step === 1 && (
   252→

   253→
What trade do you need?

   254→

   255→              Tap to select. AI will write the job post for you.
   256→

   257→

   258→              {TRADE_CATEGORIES.map((trade) => {
   259→                const Icon = trade.icon;
   260→                return (
   261→                  <button
   262→                    key={trade.id}
   263→                    onClick={() => handleTradeSelect(trade.id)}
   264→                    className={`flex flex-col items-center justify-center gap-3 rounded-2xl border-2 p-6 min-h-[120px] text-center transition-all active:scale-95 ${trade.color}`}
   265→                  >
   266→                    <Icon className="h-10 w-10" />
   267→                    {trade.label}
   268→                  </button>
   269→                );
   270→              })}
   271→

   272→

   273→        )}
   274→
   275→        {/* STEP 2: AI-filled Job Details + Location/Date/Workers (COMBINED) */}
   276→        {step === 2 && (
   277→

   278→            {aiLoading ? (
   279→

   280→

   281→                  <Sparkles className="h-12 w-12 text-primary animate-pulse" />
   282→

   283→
AI writing your job post...

   284→

   285→                  Based on market rates and common requirements
   286→

   287→

   288→            ) : (
   289→

   290→

   291→                  <Sparkles className="h-5 w-5 text-primary" />
   292→
Job Details

   293→

   294→

   295→                  AI pre-filled these. Edit anything you like.
   296→

   297→
   298→

   299→                  {/* Job Details Section */}
   300→

   301→                    Job Title
   302→                    <Input
   303→                      id="title"
   304→                      value={title}
   305→                      onChange={(e) => setTitle(e.target.value)}
   306→                      className="mt-2 h-14 text-base"
   307→                    />
   308→

   309→
   310→

   311→                    Description
   312→                    <textarea
   313→                      id="description"
   314→                      value={description}
   315→                      onChange={(e) => setDescription(e.target.value)}
   316→                      rows={5}
   317→                      className="mt-2 flex w-full rounded-md border border-input bg-background px-4 py-3 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
   318→                    />
   319→

   320→
   321→

   322→                    Pay Rate (AUD)
   323→

   324→

   325→                        <DollarSign className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
   326→                        <Input
   327→                          type="number"
   328→                          value={rateMin}
   329→                          onChange={(e) => setRateMin(e.target.value)}
   330→                          placeholder="Min"
   331→                          className="h-14 pl-10 text-base"
   332→                        />
   333→

   334→                      to
   335→

   336→                        <DollarSign className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
   337→                        <Input
   338→                          type="number"
   339→                          value={rateMax}
   340→                          onChange={(e) => setRateMax(e.target.value)}
   341→                          placeholder="Max"
   342→                          className="h-14 pl-10 text-base"
   343→                        />
   344→

   345→

   346→

   347→                      <Button
   348→                        type="button"
   349→                        variant={rateType === "hourly" ? "default" : "outline"}
   350→                        onClick={() => setRateType("hourly")}
   351→                        className="h-12 text-base font-semibold"
   352→                      >
   353→                        Per Hour
   354→                      </Button>
   355→                      <Button
   356→                        type="button"
   357→                        variant={rateType === "daily" ? "default" : "outline"}
   358→                        onClick={() => setRateType("daily")}
   359→                        className="h-12 text-base font-semibold"
   360→                      >
   361→                        Per Day
   362→                      </Button>
   363→

   364→

   365→
   366→                  {/* Location Section */}
   367→

   368→

   369→                      <MapPin className="h-5 w-5" />
   370→                      Location
   371→

   372→
   373→

   374→

   375→                        Site Address
   376→                        <Input
   377→                          id="siteAddress"
   378→                          value={siteAddress}
   379→                          onChange={(e) => setSiteAddress(e.target.value)}
   380→                          placeholder="e.g. 123 George St, Sydney"
   381→                          className="mt-2 h-14 text-base"
   382→                        />
   383→

   384→
   385→

   386→                        Suburb
   387→                        <Input
   388→                          id="suburb"
   389→                          value={suburb}
   390→                          onChange={(e) => setSuburb(e.target.value)}
   391→                          placeholder="e.g. Parramatta"
   392→                          className="mt-2 h-14 text-base"
   393→                        />
   394→

   395→

   396→

   397→
   398→                  {/* Date & Workers Section */}
   399→

   400→

   401→                      <Calendar className="h-5 w-5" />
   402→                      Schedule
   403→

   404→
   405→

   406→

   407→                        Start Date
   408→                        <Input
   409→                          id="startDate"
   410→                          type="date"
   411→                          value={startDate}
   412→                          onChange={(e) => setStartDate(e.target.value)}
   413→                          className="mt-2 h-14 text-base"
   414→                        />
   415→

   416→
   417→

   418→                        Duration
   419→                        <Input
   420→                          id="duration"
   421→                          value={duration}
   422→                          onChange={(e) => setDuration(e.target.value)}
   423→                          placeholder="e.g. 2 weeks, ongoing"
   424→                          className="mt-2 h-14 text-base"
   425→                        />
   426→

   427→
   428→

   429→
   430→                          <Users className="h-5 w-5" />
   431→                          Workers Needed
   432→
   433→

   434→                          <Button
   435→                            type="button"
   436→                            variant="outline"
   437→                            onClick={() =>
   438→                              setWorkersNeeded(String(Math.max(1, parseInt(workersNeeded) - 1)))
   439→                            }
   440→                            className="h-14 w-14 text-2xl font-bold"
   441→                          >
   442→                            -
   443→                          </Button>
   444→
   445→                            {workersNeeded}
   446→
   447→                          <Button
   448→                            type="button"
   449→                            variant="outline"
   450→                            onClick={() =>
   451→                              setWorkersNeeded(String(parseInt(workersNeeded) + 1))
   452→                            }
   453→                            className="h-14 w-14 text-2xl font-bold"
   454→                          >
   455→                            +
   456→                          </Button>
   457→

   458→

   459→

   460→

   461→
   462→                  {/* Required Certifications */}
   463→                  {certs.length > 0 && (
   464→

   465→                      Required Certifications
   466→

   467→                        {certs.map((cert) => (
   468→                          <Badge key={cert} variant="secondary" className="text-sm py-1.5 px-3">
   469→                            {cert}
   470→                            <button
   471→                              onClick={() => setCerts(certs.filter((c) => c !== cert))}
   472→                              className="ml-2 text-muted-foreground hover:text-foreground"
   473→                            >
   474→                              ×
   475→                            </button>
   476→                          </Badge>
   477→                        ))}
   478→

   479→

   480→                  )}
   481→

   482→

   483→            )}
   484→

   485→        )}
   486→
   487→        {/* STEP 3: Success */}
   488→        {step === 3 && (
   489→          <Card className="mt-8">
   490→            <CardContent className="flex flex-col items-center py-10 text-center">
   491→

   492→                <Check className="h-10 w-10 text-green-600" />
   493→

   494→
Job Posted!

   495→

   496→                We're matching you with the best workers now.
   497→                You'll get notified when we find matches.
   498→

   499→

   500→                <Button
   501→                  onClick={() => router.push("/contractor/matches")}
   502→                  className="w-full h-12 text-base font-semibold"
   503→                >
   504→                  View Matches
   505→                </Button>
   506→                <Button
   507→                  onClick={() => router.push("/dashboard")}
   508→                  variant="outline"
   509→                  className="w-full h-12 text-base"
   510→                >
   511→                  Back to Dashboard
   512→                </Button>
   513→

   514→            </CardContent>
   515→          </Card>
   516→        )}
   517→
   518→
   519→      {/* Sticky Bottom CTA (Step 2 only) */}
   520→      {step === 2 && !aiLoading && (
   521→

   522→

   523→            <Button
   524→              onClick={handlePost}
   525→              className="w-full h-12 text-base font-semibold"
   526→              disabled={loading || !title || !suburb}
   527→            >
   528→              {loading ? (
   529→                <>
   530→                  <Loader2 className="mr-2 h-6 w-6 animate-spin" />
   531→                  Posting...
   532→                </>
   533→              ) : (
   534→                <>
   535→                  Post Job Now
   536→                  <Check className="ml-2 h-6 w-6" />
   537→                </>
   538→              )}
   539→            </Button>
   540→

   541→              Free to post. You only pay $50 when you hire someone.
   542→

   543→

   544→

   545→      )}
   546→

   547→  );
   548→}
   549→

<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 { NextRequest, NextResponse } from "next/server";
     2→import { logger } from "@/lib/logger";
     3→import { createClient, getSupabaseAdmin } from "@/lib/supabase/server";
     4→import { checkRateLimit, getClientIP, createRateLimitResponse } from "@/lib/rate-limit";
     5→import { withCSRFProtection } from "@/lib/csrf";
     6→
     7→// Rate limit: 10 requests per 15 minutes
     8→const RATE_LIMIT = 10;
     9→const WINDOW_MS = 15 * 60 * 1000;
    10→
    11→/**
    12→ * POST /api/match/find-matches
    13→ *
    14→ * Finds and creates suggested matches for a job using DB-side scoring.
    15→ * Body: { job_id: string, limit?: number, min_score?: number }
    16→ *
    17→ * Scoring weights (calculated in database):
    18→ * - Trade match: 40%
    19→ * - Availability: 20%
    20→ * - Experience: 15%
    21→ * - Certifications: 15%
    22→ * - Location proximity: 10%
    23→ * - Ratings: 10%
    24→ *
    25→ * NOTE: This uses the find_matching_workers() RPC function which scales
    26→ * efficiently to 10k+ workers by performing all scoring calculations
    27→ * database-side with proper indexes.
    28→ */
    29→const handler = async (request: NextRequest) => {
    30→  try {
    31→    // Rate limiting
    32→    const clientIP = getClientIP(request);
    33→    const rateLimit = checkRateLimit(`match:${clientIP}`, RATE_LIMIT, WINDOW_MS);
    34→    if (!rateLimit.allowed) {
    35→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    36→      return createRateLimitResponse(clientIP, retryAfter);
    37→    }
    38→
    39→    // Auth check
    40→    const supabase = await createClient();
    41→    const { data: { user }, error: authError } = await supabase.auth.getUser();
    42→    if (authError || !user) {
    43→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    44→    }
    45→
    46→    const { job_id: jobId, limit = 20, min_score: minScore } = await request.json();
    47→    if (!jobId) {
    48→      return NextResponse.json({ error: "job_id is required" }, { status: 400 });
    49→    }
    50→
    51→    // Validate limit (prevent abuse)
    52→    const validLimit = Math.min(Math.max(parseInt(limit as string) || 20, 1), 50);
    53→    const validMinScore = Math.min(Math.max(parseInt(minScore as string) || 20, 0), 100);
    54→
    55→    // Use admin client to bypass RLS for matching operations
    56→    const admin = getSupabaseAdmin();
    57→
    58→    // Verify job exists and user owns it
    59→    const { data: job, error: jobError } = await admin
    60→      .from("jobs")
    61→      .select("id, status, companies!inner(profile_id)")
    62→      .eq("id", jobId)
    63→      .single();
    64→
    65→    if (jobError || !job) {
    66→      return NextResponse.json({ error: "Job not found" }, { status: 404 });
    67→    }
    68→
    69→    // Verify the requesting user owns this job's company
    70→    const companies = job.companies as unknown as { profile_id: string } | null;
    71→    if (!companies || companies.profile_id !== user.id) {
    72→      return NextResponse.json({ error: "Not authorized for this job" }, { status: 403 });
    73→    }
    74→
    75→    if (job.status !== "active") {
    76→      return NextResponse.json({ error: "Job is not active" }, { status: 400 });
    77→    }
    78→
    79→    // Call the DB-side scoring RPC function
    80→    // This is the key optimization - all scoring happens in the database
    81→    const { data: scoredWorkers, error: rpcError } = await admin.rpc(
    82→      "find_matching_workers",
    83→      {
    84→        p_job_id: jobId,
    85→        p_limit: validLimit,
    86→        p_min_score: validMinScore,
    87→      }
    88→    );
    89→
    90→    if (rpcError) {
    91→      logger.error("RPC error finding matches:", rpcError);
    92→      return NextResponse.json(
    93→        { error: "Failed to find matches", details: rpcError.message },
    94→        { status: 500 }
    95→      );
    96→    }
    97→
    98→    if (!scoredWorkers || scoredWorkers.length === 0) {
    99→      return NextResponse.json({
   100→        matches: [],
   101→        message: "No matching workers found",
   102→        matches_created: 0,
   103→      });
   104→    }
   105→
   106→    // Prepare match rows for insertion
   107→    const matchRows = scoredWorkers.map((match: {
   108→      worker_id: string;
   109→      score: number;
   110→      summary: string;
   111→    }) => ({
   112→      job_id: jobId,
   113→      worker_id: match.worker_id,
   114→      status: "suggested" as const,
   115→      match_score: match.score,
   116→      ai_summary: match.summary,
   117→      contractor_action: "pending" as const,
   118→      worker_action: "pending" as const,
   119→    }));
   120→
   121→    // Insert matches into the database
   122→    const { error: insertError } = await admin
   123→      .from("matches")
   124→      .upsert(matchRows, { onConflict: "job_id,worker_id", ignoreDuplicates: true });
   125→
   126→    if (insertError) {
   127→      logger.error("Error inserting matches:", insertError);
   128→      return NextResponse.json({ error: "Failed to save matches" }, { status: 500 });
   129→    }
   130→
   131→    return NextResponse.json({
   132→      matches: scoredWorkers.map((m: {
   133→        worker_id: string;
   134→        score: number;
   135→        summary: string;
   136→        reasons: string[];
   137→        profile: Record<string, unknown>;
   138→      }) => ({
   139→        worker_id: m.worker_id,
   140→        score: m.score,
   141→        summary: m.summary,
   142→        reasons: m.reasons,
   143→        profile: m.profile,
   144→      })),
   145→      matches_created: scoredWorkers.length,
   146→    });
   147→  } catch (error) {
   148→    logger.error("Match finding error:", error);
   149→    return NextResponse.json(
   150→      { error: "Failed to find matches" },
   151→      { status: 500 }
   152→    );
   153→  }
   154→}
   155→
   156→// Apply CSRF protection to the handler
   157→export const POST = withCSRFProtection(handler);
   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→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→  last_seen_at: string | null;
    42→  ai_generated: boolean;
    43→}
    44→
    45→export interface Company {
    46→  id: string;
    47→  profile_id: string;
    48→  name: string;
    49→  abn: string | null;
    50→  address: string | null;
    51→  industry: string | null;
    52→  size: string | null;
    53→  website: string | null;
    54→  logo_url: string | null;
    55→  ai_research: Record<string, unknown> | null;
    56→  created_at: string;
    57→  updated_at: string;
    58→}
    59→
    60→export interface WorkerProfile {
    61→  id: string;
    62→  profile_id: string;
    63→  trades: string[];
    64→  experience_years: number | null;
    65→  certifications: string[];
    66→  white_card_number: string | null;
    67→  white_card_verified: boolean;
    68→  availability: Availability | null;
    69→  languages: string[];
    70→  bio: string | null;
    71→  voice_note_url: string | null;
    72→  ai_extracted: Record<string, unknown> | null;
    73→  created_at: string;
    74→  updated_at: string;
    75→}
    76→
    77→export interface Job {
    78→  id: string;
    79→  company_id: string;
    80→  title: string;
    81→  description: string | null;
    82→  trade: string;
    83→  /**
    84→   * Location as { lat, lng } object
    85→   * Note: Database stores this as PostgreSQL point type
    86→   * Use postgresPointToJson() when reading from DB
    87→   * Use jsonToPostgresPoint() when writing to DB
    88→   */
    89→  location: { lat: number; lng: number } | null;
    90→  suburb: string | null;
    91→  site_address: string | null;
    92→  rate_min: number | null;
    93→  rate_max: number | null;
    94→  rate_type: RateType;
    95→  start_date: string | null;
    96→  duration: string | null;
    97→  workers_needed: number;
    98→  certifications_required: string[];
    99→  status: JobStatus;
   100→  ai_generated: boolean;
   101→  created_at: string;
   102→  updated_at: string;
   103→}
   104→
   105→export interface Match {
   106→  id: string;
   107→  job_id: string;
   108→  worker_id: string;
   109→  status: MatchStatus;
   110→  match_score: number | null;
   111→  ai_summary: string | null;
   112→  contractor_action: ActionStatus;
   113→  worker_action: ActionStatus;
   114→  hired_at: string | null;
   115→  fee_charged: boolean;
   116→  created_at: string;
   117→  updated_at: string;
   118→}
   119→
   120→export interface Rating {
   121→  id: string;
   122→  match_id: string;
   123→  from_id: string;
   124→  to_id: string;
   125→  score: number;
   126→  comment: string | null;
   127→  created_at: string;
   128→}
   129→
   130→export interface Payment {
   131→  id: string;
   132→  match_id: string;
   133→  company_id: string;
   134→  amount: number;
   135→  status: PaymentStatus;
   136→  stripe_payment_id: string | null;
   137→  created_at: string;
   138→}
   139→
   140→export interface Notification {
   141→  id: string;
   142→  profile_id: string;
   143→  type: NotificationType;
   144→  title: string;
   145→  body: string | null;
   146→  data: Record<string, unknown> | null;
   147→  read: boolean;
   148→  created_at: string;
   149→}
   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>


     1→// Payment constants
     2→export const PAYMENT_AMOUNT = 50.0;
     3→
     4→// Rate limiting constants
     5→export const RATE_LIMIT_REQUESTS_PER_IP = 5;
     6→export const RATE_LIMIT_REQUESTS_PER_USER = 10;
     7→export const RATE_LIMIT_WINDOW_MINUTES = 15;
     8→export const RATE_LIMIT_WINDOW_MS = RATE_LIMIT_WINDOW_MINUTES * 60 * 1000;
     9→export const SEND_MESSAGE_RATE_LIMIT = 60; // 60 messages per 15 min window per IP
    10→export const VERIFY_RATE_LIMIT = 5; // 5 email verification attempts per window
    11→export const VERIFY_WINDOW_MS = RATE_LIMIT_WINDOW_MS;
    12→
    13→// AI rate limiting constants
    14→export const AI_RATE_LIMIT = 5;
    15→export const AI_WINDOW_MS = RATE_LIMIT_WINDOW_MS;
    16→
    17→// Input validation constants
    18→export const MAX_LENGTH_TRADE = 100;
    19→export const MAX_LENGTH_LOCATION = 200;
    20→export const MAX_LENGTH_AVAILABILITY = 50;
    21→export const MAX_LENGTH_TRANSCRIPT = 5000;
    22→export const MAX_LENGTH_VOICE_TRANSCRIPT = 2000;
    23→export const MAX_LENGTH_MESSAGE_CONTENT = 5000;
    24→export const MAX_LENGTH_EMAIL = 254;
    25→export const MAX_RATING_TEXT_LENGTH = 500;
    26→
    27→// Timeout constants (in milliseconds)
    28→export const TIMEOUT_ABN_LOOKUP = 10000;
    29→export const DELAY_RESET_PASSWORD_REDIRECT = 2000;
    30→
    31→// HTTP status codes
    32→export const HTTP_STATUS_INTERNAL_SERVER_ERROR = 500;
    33→export const HTTP_STATUS_OK = 200;
    34→
    35→// Australian postcode constants (for testing/validation)
    36→export const POSTCODE_SYDNEY = '2000';
    37→export const POSTCODE_MELBOURNE = '3000';
    38→
    39→// Percentage constants
    40→export const PERCENTAGE_WORKER_KEEP = 100; // Workers keep 100% of wages
    41→
    42→// Worker trades - used in worker profiles and AI validation
    43→export const TRADES = [
    44→  "Labourer",
    45→  "Steel Fixer",
    46→  "Formworker",
    47→  "Carpenter",
    48→  "Electrician",
    49→  "Plumber",
    50→  "Concreter",
    51→  "Bricklayer",
    52→  "Painter",
    53→  "Roofer",
    54→  "Scaffolder",
    55→  "Tiler",
    56→  "Plasterer",
    57→  "Welder",
    58→  "Crane Operator",
    59→  "Excavator Operator",
    60→] as const;
    61→
    62→export type Trade = typeof TRADES[number];
    63→
    64→// Contractor job categories - used in job posting
    65→export const TRADE_CATEGORIES = [
    66→  { id: "steel-fixer", label: "Steel Fixer" },
    67→  { id: "formworker", label: "Formworker" },
    68→  { id: "labourer", label: "Labourer" },
    69→  { id: "concreter", label: "Concreter" },
    70→  { id: "carpenter", label: "Carpenter" },
    71→  { id: "scaffolder", label: "Scaffolder" },
    72→  { id: "other", label: "Other" },
    73→] as const;
    74→
    75→export type TradeCategory = typeof TRADE_CATEGORIES[number];
    76→
    77→/**
    78→ * Convert job category ID to trade name
    79→ *
    80→ * @param categoryId - ID from TRADE_CATEGORIES
    81→ * @returns Trade name label or null if not found
    82→ *
    83→ * @example
    84→ * getTradeFromCategoryId('steel-fixer')
    85→ * // Returns: 'Steel Fixer'
    86→ *
    87→ * @example
    88→ * getTradeFromCategoryId('invalid-id')
    89→ * // Returns: null
    90→ */
    91→export function getTradeFromCategoryId(categoryId: string): string | null {
    92→  const category = TRADE_CATEGORIES.find(cat => cat.id === categoryId);
    93→  return category ? category.label : null;
    94→}

<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>


Let me search for a few more important files related to messaging and other key components:


     1→import { createClient } from "@/lib/supabase/server";
     2→
     3→/**
     4→ * Gets or creates a conversation between two users
     5→ * Uses the database RPC function 'get_or_create_conversation' to handle
     6→ * the logic of finding existing conversations or creating new ones
     7→ *
     8→ * @param user1Id - ID of the first user in the conversation
     9→ * @param user2Id - ID of the second user in the conversation
    10→ * @returns Promise resolving to the conversation ID
    11→ * @throws Error if the database operation fails
    12→ *
    13→ * @example
    14→ * const conversationId = await getOrCreateConversation('user-123', 'user-456');
    15→ */
    16→export async function getOrCreateConversation(user1Id: string, user2Id: string) {
    17→  const supabase = await createClient();
    18→
    19→  // Use the database function to get or create conversation
    20→  const { data: conversationId, error } = await supabase
    21→    .rpc('get_or_create_conversation', {
    22→      user1_id: user1Id,
    23→      user2_id: user2Id
    24→    });
    25→
    26→  if (error) {
    27→    throw new Error(`Failed to get or create conversation: ${error.message}`);
    28→  }
    29→
    30→  return conversationId as string;
    31→}

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

<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 -type f -name "*.ts" -o -name "*.tsx" | grep -E "(message|conversation)" | head -20


Let me read the messaging pages to complete the picture:


1→"use client";
     2→
     3→import { useState, useEffect } from "react";
     4→import Link from "next/link";
     5→import { logger } from "@/lib/logger";
     6→import { useRouter } from "next/navigation";
     7→import { ConversationPreview } from "@/components/ConversationPreview";
     8→import { Button } from "@/components/ui/button";
     9→import { Input } from "@/components/ui/input";
    10→import { BottomNav } from "@/components/bottom-nav";
    11→import { Search, MessageSquare } from "lucide-react";
    12→import { createClient } from "@/lib/supabase/client";
    13→
    14→interface Conversation {
    15→  conversation_id: string;
    16→  other_participant_name: string;
    17→  other_participant_avatar: string | null;
    18→  last_message: string | null;
    19→  last_message_time: string | null;
    20→  unread_count: number;
    21→  other_participant_is_online: boolean;
    22→}
    23→
    24→export default function MessagesPage() {
    25→  const router = useRouter();
    26→  const [searchQuery, setSearchQuery] = useState("");
    27→  const [conversations, setConversations] = useState<Conversation[]>([]);
    28→  const [loading, setLoading] = useState(true);
    29→  const [userType, setUserType] = useState<"contractor" | "worker">("worker");
    30→  const supabase = createClient();
    31→
    32→  useEffect(() => {
    33→    fetchConversations();
    34→    fetchUserType();
    35→
    36→    // Subscribe to new messages for real-time updates
    37→    const channel = supabase
    38→      .channel('messages')
    39→      .on('postgres_changes',
    40→        {
    41→          event: 'INSERT',
    42→          schema: 'public',
    43→          table: 'messages'
    44→        },
    45→        () => {
    46→          fetchConversations();
    47→        }
    48→      )
    49→      .subscribe();
    50→
    51→    return () => {
    52→      supabase.removeChannel(channel);
    53→    };
    54→  }, []);
    55→
    56→  const fetchUserType = async () => {
    57→    const { data: { user } } = await supabase.auth.getUser();
    58→    if (user) {
    59→      const { data: profile } = await supabase
    60→        .from("profiles")
    61→        .select("type")
    62→        .eq("id", user.id)
    63→        .single();
    64→      if (profile?.type) {
    65→        setUserType(profile.type as "contractor" | "worker");
    66→      }
    67→    }
    68→  };
    69→
    70→  const fetchConversations = async () => {
    71→    try {
    72→      const response = await fetch('/api/messages');
    73→      if (response.ok) {
    74→        const data = await response.json();
    75→        setConversations(data.conversations || []);
    76→      }
    77→    } catch (error) {
    78→      logger.error("Error fetching conversations:", error);
    79→    } finally {
    80→      setLoading(false);
    81→    }
    82→  };
    83→
    84→  const filteredConversations = conversations.filter((conversation) =>
    85→    conversation.other_participant_name?.toLowerCase().includes(searchQuery.toLowerCase())
    86→  );
    87→
    88→  if (loading) {
    89→    return (
    90→

    91→

    92→

    93→            <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
    94→            <Input
    95→              type="search"
    96→              placeholder="Search conversations..."
    97→              value={searchQuery}
    98→              onChange={(e) => setSearchQuery(e.target.value)}
    99→              className="h-12 pl-10 text-base"
   100→              disabled
   101→            />
   102→

   103→

   104→

   105→
Loading conversations...

   106→

   107→        <BottomNav userType={userType} />
   108→

   109→    );
   110→  }
   111→
   112→  return (
   113→

   114→      {/* Search Bar */}
   115→

   116→

   117→          <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
   118→          <Input
   119→            type="search"
   120→            placeholder="Search conversations..."
   121→            value={searchQuery}
   122→            onChange={(e) => setSearchQuery(e.target.value)}
   123→            className="h-12 pl-10 text-base"
   124→          />
   125→

   126→

   127→
   128→      {/* Conversations List */}
   129→
   130→        {filteredConversations.length === 0 ? (
   131→

   132→            <MessageSquare className="h-16 w-16 text-muted-foreground mb-4" />
   133→
No conversations yet

   134→

   135→              {searchQuery
   136→                ? "No conversations match your search"
   137→                : "Start a conversation by hiring a worker or applying for a job"}
   138→

   139→            {!searchQuery && (
   140→              <Button asChild>
   141→                <Link href="/dashboard">Go to Dashboard</Link>
   142→              </Button>
   143→            )}
   144→

   145→        ) : (
   146→

   147→            {filteredConversations.map((conversation) => (
   148→              <ConversationPreview
   149→                key={conversation.conversation_id}
   150→                id={conversation.conversation_id}
   151→                name={conversation.other_participant_name}
   152→                avatar={conversation.other_participant_avatar || undefined}
   153→                lastMessage={conversation.last_message || "No messages yet"}
   154→                lastMessageTime={conversation.last_message_time ? new Date(conversation.last_message_time) : new Date()}
   155→                unreadCount={conversation.unread_count}
   156→                isOnline={conversation.other_participant_is_online}
   157→                onClick={() => router.push(`/messages/${conversation.conversation_id}`)}
   158→              />
   159→            ))}
   160→

   161→        )}
   162→
   163→
   164→      <BottomNav userType={userType} />
   165→

   166→  );
   167→}
   168→

<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→"use client";
     2→
     3→import { useState, useRef, useEffect, use } from "react";
     4→import { useRouter } from "next/navigation";
     5→import { logger } from "@/lib/logger";
     6→import toast from "react-hot-toast";
     7→import { MessageBubble } from "@/components/MessageBubble";
     8→import { Button } from "@/components/ui/button";
     9→import { Input } from "@/components/ui/input";
    10→import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
    11→import { ArrowLeft, Send, MoreVertical } from "lucide-react";
    12→import { createClient } from "@/lib/supabase/client";
    13→import { csrfFetch } from "@/lib/csrf-client";
    14→
    15→interface Message {
    16→  id: string;
    17→  content: string;
    18→  isOwn: boolean;
    19→  timestamp: Date;
    20→  status?: "sent" | "delivered" | "read";
    21→}
    22→
    23→interface OtherParticipant {
    24→  id: string;
    25→  name: string;
    26→  avatar: string | null;
    27→  type: string;
    28→  isOnline: boolean;
    29→}
    30→
    31→interface ConversationData {
    32→  id: string;
    33→  otherParticipant: OtherParticipant;
    34→  messages: Message[];
    35→}
    36→
    37→export default function ConversationPage({ params }: { params: Promise<{ conversationId: string }> }) {
    38→  const router = useRouter();
    39→  const resolvedParams = use(params);
    40→  const conversationId = resolvedParams.conversationId;
    41→
    42→  const [message, setMessage] = useState("");
    43→  const [messages, setMessages] = useState<Message[]>([]);
    44→  const [otherParticipant, setOtherParticipant] = useState<OtherParticipant | null>(null);
    45→  const [loading, setLoading] = useState(true);
    46→  const [sending, setSending] = useState(false);
    47→  const messagesEndRef = useRef<HTMLDivElement>(null);
    48→  const supabase = createClient();
    49→
    50→  const scrollToBottom = () => {
    51→    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
    52→  };
    53→
    54→  useEffect(() => {
    55→    fetchConversation();
    56→
    57→    // Subscribe to new messages in this conversation
    58→    const channel = supabase
    59→      .channel(`conversation:${conversationId}`)
    60→      .on('postgres_changes',
    61→        {
    62→          event: 'INSERT',
    63→          schema: 'public',
    64→          table: 'messages',
    65→          filter: `conversation_id=eq.${conversationId}`
    66→        },
    67→        (payload) => {
    68→          const newMessage = payload.new as { id: string; sender_id: string; content: string; created_at: string; status?: string };
    69→          // Only add if it's not from us (we already added it optimistically)
    70→          supabase.auth.getUser().then(({ data: { user } }) => {
    71→            if (newMessage.sender_id !== user?.id) {
    72→              setMessages(prev => [...prev, {
    73→                id: newMessage.id,
    74→                content: newMessage.content,
    75→                isOwn: false,
    76→                timestamp: new Date(newMessage.created_at),
    77→                status: newMessage.status as "sent" | "delivered" | "read"
    78→              }]);
    79→            }
    80→          });
    81→        }
    82→      )
    83→      .subscribe();
    84→
    85→    return () => {
    86→      supabase.removeChannel(channel);
    87→    };
    88→  }, [conversationId]);
    89→
    90→  useEffect(() => {
    91→    scrollToBottom();
    92→  }, [messages]);
    93→
    94→  const fetchConversation = async () => {
    95→    try {
    96→      const response = await fetch(`/api/messages/${conversationId}`);
    97→      if (response.ok) {
    98→        const data = await response.json();
    99→        const conversation: ConversationData = data.conversation;
   100→        setOtherParticipant(conversation.otherParticipant);
   101→        setMessages(conversation.messages.map(msg => ({
   102→          ...msg,
   103→          timestamp: new Date(msg.timestamp)
   104→        })));
   105→      } else if (response.status === 404) {
   106→        toast.error("Conversation not found");
   107→        router.push('/messages');
   108→      } else {
   109→        toast.error("Failed to load conversation");
   110→      }
   111→    } catch (error) {
   112→      logger.error("Error fetching conversation:", error);
   113→      toast.error("Failed to load conversation. Please try again.");
   114→    } finally {
   115→      setLoading(false);
   116→    }
   117→  };
   118→
   119→  const handleSendMessage = async (e: React.FormEvent) => {
   120→    e.preventDefault();
   121→    if (message.trim() && !sending) {
   122→      setSending(true);
   123→      const messageContent = message.trim();
   124→
   125→      // Optimistic update
   126→      const tempId = Date.now().toString();
   127→      const newMessage: Message = {
   128→        id: tempId,
   129→        content: messageContent,
   130→        isOwn: true,
   131→        timestamp: new Date(),
   132→        status: "sent"
   133→      };
   134→      setMessages(prev => [...prev, newMessage]);
   135→      setMessage("");
   136→
   137→      try {
   138→        const response = await csrfFetch(`/api/messages/${conversationId}/send`, {
   139→          method: 'POST',
   140→          headers: { 'Content-Type': 'application/json' },
   141→          body: JSON.stringify({ content: messageContent })
   142→        });
   143→
   144→        if (response.ok) {
   145→          const data = await response.json();
   146→          // Update the temp message with the real one
   147→          setMessages(prev => prev.map(msg =>
   148→            msg.id === tempId ? { ...data.message, timestamp: new Date(data.message.timestamp) } : msg
   149→          ));
   150→        } else {
   151→          // Remove the optimistic message on error
   152→          setMessages(prev => prev.filter(msg => msg.id !== tempId));
   153→          setMessage(messageContent); // Restore the message
   154→          toast.error("Failed to send message. Please try again.");
   155→        }
   156→      } catch (error) {
   157→        logger.error("Error sending message:", error);
   158→        setMessages(prev => prev.filter(msg => msg.id !== tempId));
   159→        setMessage(messageContent);
   160→        toast.error("Failed to send message. Please try again.");
   161→      } finally {
   162→        setSending(false);
   163→      }
   164→    }
   165→  };
   166→
   167→  const initials = otherParticipant?.name
   168→    ? otherParticipant.name
   169→        .split(" ")
   170→        .map((n) => n[0])
   171→        .join("")
   172→        .toUpperCase()
   173→        .slice(0, 2)
   174→    : "??";
   175→
   176→  if (loading) {
   177→    return (
   178→

   179→
   180→

   181→

   182→              <Button
   183→                variant="ghost"
   184→                size="icon"
   185→                onClick={() => router.push("/messages")}
   186→                className="h-10 w-10"
   187→                aria-label="Go back to messages"
   188→              >
   189→                <ArrowLeft className="h-5 w-5" />
   190→              </Button>
   191→

   192→

   193→

   194→

   195→

   196→

   197→

   198→

   199→

   200→
   201→

   202→
Loading messages...

   203→

   204→

   205→    );
   206→  }
   207→
   208→  return (
   209→

   210→      {/* Header */}
   211→
   212→

   213→

   214→            <Button
   215→              variant="ghost"
   216→              size="icon"
   217→              onClick={() => router.push("/messages")}
   218→              className="h-10 w-10"
   219→            >
   220→              <ArrowLeft className="h-5 w-5" />
   221→            </Button>
   222→

   223→              <Avatar className="h-10 w-10">
   224→                <AvatarImage src={otherParticipant?.avatar || ""} alt={otherParticipant?.name || ""} />
   225→                <AvatarFallback className="bg-primary/10 text-primary font-semibold">
   226→                  {initials}
   227→                </AvatarFallback>
   228→              </Avatar>
   229→

   230→
{otherParticipant?.name || "Unknown"}

   231→

   232→                  {otherParticipant?.isOnline ? "Online" : "Offline"}
   233→

   234→

   235→

   236→

   237→

   238→            <Button variant="ghost" size="icon" className="h-10 w-10" aria-label="More options">
   239→              <MoreVertical className="h-5 w-5" />
   240→            </Button>
   241→

   242→

   243→
   244→
   245→      {/* Messages */}
   246→
   247→

   248→          {messages.length === 0 ? (
   249→

   250→
No messages yet. Say hello!

   251→

   252→          ) : (
   253→

   254→              {messages.map((msg) => (
   255→                <MessageBubble
   256→                  key={msg.id}
   257→                  content={msg.content}
   258→                  isOwn={msg.isOwn}
   259→                  timestamp={msg.timestamp}
   260→                  status={msg.status}
   261→                />
   262→              ))}
   263→

   264→

   265→          )}
   266→

   267→
   268→
   269→      {/* Message Input */}
   270→
   271→

   272→          <form onSubmit={handleSendMessage} className="flex gap-3">
   273→            <Input
   274→              type="text"
   275→              placeholder="Type a message..."
   276→              value={message}
   277→              onChange={(e) => setMessage(e.target.value)}
   278→              className="flex-1 h-12 text-base"
   279→              disabled={sending}
   280→            />
   281→            <Button
   282→              type="submit"
   283→              size="icon"
   284→              className="h-12 w-12 shrink-0"
   285→              disabled={!message.trim() || sending}
   286→              aria-label="Send message"
   287→            >
   288→              <Send className="h-5 w-5" />
   289→            </Button>
   290→          </form>
   291→

   292→
   293→

   294→  );
   295→}
   296→

<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 I have comprehensive information about the codebase. Let me compile the findings into a well-organized report.

## COMPREHENSIVE CODEBASE SEARCH RESULTS

I've completed a thorough search of the RateRight v2 construction hiring marketplace codebase. Here are all the files related to your requests:

---

## 1. MESSAGES/MESSAGING SYSTEM & API ROUTES

### Core Messaging API Routes:
- **`/home/ccuser/the-50-dollar-app/src/app/api/messages/start/route.ts`** - Start/create new conversation endpoint
  - Rate limited: 20 starts per 15 min per IP
  - CSRF protected with `withCSRFProtection()`
  - Creates conversation via RPC function `get_or_create_conversation()`
  - Supports optional initial message
  - Returns `conversationId`

- **`/home/ccuser/the-50-dollar-app/src/app/api/messages/route.ts`** - Get conversation list
  - Rate limited: 100 fetches per 15 min per IP
  - Returns conversation previews using `get_conversation_previews()` RPC
  - Updates `last_seen_at` timestamp

- **`/home/ccuser/the-50-dollar-app/src/app/api/messages/[id]/route.ts`** - Get messages in a conversation
  - Rate limited: 200 per 15 min per IP
  - Supports cursor-based pagination (max 100 messages per request)
  - Verifies user is conversation participant
  - Returns otherParticipant profile + online status (5 min check)
  - Updates `last_read_at` timestamp
  - Includes attachment metadata

- **`/home/ccuser/the-50-dollar-app/src/app/api/messages/[id]/send/route.ts`** - Send message in conversation
  - Rate limited: 60 messages per 15 min per IP
  - CSRF & origin validation
  - Input validation with Zod schema
  - **Attachment schema**: max 5 attachments, 10MB max each
  - Sanitizes message content with `sanitizeInput()`
  - Creates notification for recipient
  - Optimistic updates with temporary IDs

### Messaging Library & Utilities:
- **`/home/ccuser/the-50-dollar-app/src/lib/messaging.ts`** - Messaging utilities
  - `getOrCreateConversation(user1Id, user2Id)` - Helper function using RPC

### Frontend Messaging Pages:
- **`/home/ccuser/the-50-dollar-app/src/app/(authenticated)/messages/page.tsx`** - Conversation list view
  - Search conversations by participant name
  - Real-time updates via Supabase Realtime
  - Displays unread count + online status
  - Shows last message preview + timestamp

- **`/home/ccuser/the-50-dollar-app/src/app/(authenticated)/messages/[conversationId]/page.tsx`** - Conversation detail
  - Real-time message subscription per conversation
  - Optimistic message sending
  - Auto-scroll to newest message
  - Shows online/offline status of other participant
  - CSRF-protected message sending via `csrfFetch()`
  - Cursor pagination with scroll support

### Message Components:
- **`/home/ccuser/the-50-dollar-app/src/components/MessageBubble.tsx`** - Message bubble display
- **`/home/ccuser/the-50-dollar-app/src/components/ConversationPreview.tsx`** - Conversation list item

---

## 2. PHOTO/IMAGE UPLOAD SYSTEM

### Avatar Upload Component:
- **`/home/ccuser/the-50-dollar-app/src/components/avatar-upload.tsx`** - Profile photo upload
  - **File size limit: 2MB** (line 35)
  - Validates image file type: `file.type.startsWith("image/")`
  - Uploads to Supabase Storage bucket: `"avatars"`
  - Storage path format: `${user.id}/avatar.${ext}`
  - Upsert enabled (overwrites existing)
  - Cache control: 3600 seconds
  - Updates profile `avatar_url` column
  - Gets public URL from Supabase Storage
  - Cache-busting with timestamp query parameter
  - Error handling for missing bucket with helpful message

---

## 3. JOB POSTING/CREATION SYSTEM

### Job Creation Page:
- **`/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/post-job/page.tsx`** - Complete job posting flow
  - **Step 1**: Select trade category (7 trades: steel-fixer, formworker, labourer, concreter, carpenter, scaffolder, other)
  - **Step 2**: AI-generated job details (title, description, certifications, rates)
  - **Step 3**: Location & schedule details (suburb, address, start date, duration, workers needed)
  - **Step 4**: Success confirmation
  - Uses hardcoded fallback suggestions if AI fails
  - Geocodes suburb to get lat/lng coordinates
  - Validates required fields (title, suburb)
  - Saves to `jobs` table with fields:
    - `company_id`, `title`, `description`, `trade`, `suburb`, `location` (PostGIS point)
    - `site_address`, `rate_min`, `rate_max`, `rate_type`, `start_date`, `duration`
    - `workers_needed`, `certifications_required`, `status: "active"`, `ai_generated: true`

### AI Job Generation API:
- **`/home/ccuser/the-50-dollar-app/src/app/api/ai/generate-job/route.ts`** - AI-powered job post generation
  - Rate limited: 5 per 15 min per IP (not 10 - see code line 14)
  - Uses OpenAI GPT-4o-mini model
  - Input validation & sanitization via `sanitizeInput(trade, { maxLength: MAX_LENGTH_TRADE })`
  - **Rate clamping logic**:
    - Min: $25/hr, Max: $200/hr
    - Default fallback: $35-$55/hr
    - Ensures 5+ hour difference between min/max
  - Returns: title, description, certifications array, rateMin, rateMax, rateType
  - Fallback to hardcoded suggestions if AI fails

### Input Sanitization for Jobs:
- **`/home/ccuser/the-50-dollar-app/src/lib/sanitize.ts`** - Security sanitization
  - `sanitizeInput(input, options)` - Core sanitization function
  - Strips control characters (0x00-0x1F, 0x7F)
  - Escapes template literals ($, backticks, backslashes)
  - Trims whitespace
  - Enforces max length limits
  - `sanitizeInputs()` - Batch sanitization with field-specific options

---

## 4. WORKER PROFILE & SIGNUP FLOW

### Worker Signup Page:
- **`/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/signup/page.tsx`** - Complete onboarding
  - **9-step flow**: auth → method → voice → trade → experience → location → availability → certifications → review
  - Step selection: Manual or voice-based entry
  - Uses custom hooks:
    - `useWorkerAuth()` - Email/password signup
    - `useWorkerProfile()` - Profile state management
    - `useVoiceProcessing()` - Voice transcription/AI extraction
  - Progress bar shows steps (excluding voice option)
  - Review screen before confirmation
  - Saves to `worker_profiles` table

### Worker Profile Display Page:
- **`/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/profile/page.tsx`** - Public profile view
  - Checks `onboarding_completed` flag
  - Displays profile info: name, avatar, trades, suburb, experience, availability
  - Shows certifications and skills (from `ai_extracted`)
  - **Avatar upload component** integrated
  - Displays ratings summary (avg rating + count)
  - Shows 3 recent ratings with option to view all
  - Edit button to return to profile builder

### Contractor Signup Page:
- **`/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/signup/page.tsx`** - Contractor onboarding
  - **4-step flow**: auth → company search → ABR verification → review
  - ABN lookup with validation via `/api/abn-lookup`
  - Company logo fetching via `/api/company-logo` (Clearbit + Google fallback)
  - Domain guessing from company name
  - Editable company fields after lookup
  - Email verification check with redirect to `/auth/verify-email`
  - Saves to `profiles` + `companies` tables with ABR metadata

---

## 5. PAYMENT VERIFICATION LOGIC

### Payment Creation API:
- **`/home/ccuser/the-50-dollar-app/src/app/api/payments/create/route.ts`** - Create Stripe payment
  - Rate limited: 5 per 15 min per IP + per user
  - Validates match ownership (via company relationship)
  - Prevents duplicate charges by checking for existing "charged" payments
  - **Hardcoded amount: $50 AUD** (cannot be overridden by client)
  - Creates Stripe PaymentIntent via `createPaymentIntent()`
  - Atomic upsert to `payments` table (handles race conditions)
  - Returns: payment record + clientSecret + publishableKey
  - Response includes full payment metadata with timestamps

### Payment Verification API:
- **`/home/ccuser/the-50-dollar-app/src/app/api/payments/verify/route.ts`** - Verify payment status
  - Retrieves PaymentIntent from Stripe
  - Verifies user owns the company that made the payment
  - Returns: status, amount, currency
  - Validates match ownership before returning details

### Constants:
- **`/home/ccuser/the-50-dollar-app/src/lib/constants.ts`** - All app constants
  - `PAYMENT_AMOUNT = 50.0` (AUD)
  - Rate limits: SEND_MESSAGE_RATE_LIMIT = 60, AI_RATE_LIMIT = 5, etc.
  - Message content: MAX_LENGTH_MESSAGE_CONTENT = 5000

---

## 6. MATCHES TABLE & MATCHING SYSTEM

### Match Schema (from types):
- **`/home/ccuser/the-50-dollar-app/src/lib/types.ts`** - TypeScript types
  ```typescript
  interface Match {
    id: string;
    job_id: string;
    worker_id: string;
    status: "suggested" | "applied" | "accepted" | "rejected" | "hired";
    match_score: number | null;  // Weighted scoring
    ai_summary: string | null;   // AI explanation of match
    contractor_action: "pending" | "accepted" | "rejected";
    worker_action: "pending" | "accepted" | "rejected";
    hired_at: string | null;
    fee_charged: boolean;
    created_at: string;
    updated_at: string;
  }
  ```

### Smart Matching API:
- **`/home/ccuser/the-50-dollar-app/src/app/api/match/find-matches/route.ts`** - Find matching workers
  - Rate limited: 10 per 15 min per IP
  - CSRF protected
  - Uses RPC function `find_matching_workers()` for database-side scoring
  - **Scoring weights**:
    - Trade match: 40%
    - Availability: 20%
    - Experience: 15%
    - Certifications: 15%
    - Location proximity: 10%
    - Ratings: 10%
  - Supports: `limit` (1-50, default 20), `min_score` (0-100, default 20)
  - Returns: worker profiles with scores + match reasons
  - Auto-creates "suggested" match records
  - Uses upsert with `onConflict: "job_id,worker_id"` to prevent duplicates

---

## 7. KEY SECURITY & VALIDATION

### CSRF Protection:
- API routes use `withCSRFProtection()` wrapper on handlers:
  - `/api/messages/start`
  - `/api/messages/[id]/send`
  - `/api/ai/generate-job`
  - `/api/match/find-matches`
  - `/api/payments/create`
- Client-side uses `csrfFetch()` utility for protected endpoints
- Origin validation: `isSameOrigin(request)` check

### Rate Limiting:
- Memory-based rate limiter in `/lib/rate-limit.ts`
- Rate limit keys: IP-based and user-based
- Tracks `resetTime` for Retry-After header
- Creates standardized error responses

### Input Validation:
- Zod schemas for request validation (messages, ratings, etc.)
- Template literal injection protection via `escapeTemplateLiterals()`
- Control character stripping via `stripControlChars()`
- Max length enforcement per field

---

## 8. DATABASE FUNCTIONS (RPC)

The system relies on several PostgreSQL RPC functions:
1. **`get_or_create_conversation(user1_id, user2_id)`** - Creates/finds conversation
2. **`get_conversation_previews(user_id)`** - Fetches conversation list with metadata
3. **`find_matching_workers(p_job_id, p_limit, p_min_score)`** - Database-side matching algorithm

---

## 9. KEY FILE PATHS SUMMARY

```
Messaging System:
- /src/app/api/messages/start/route.ts
- /src/app/api/messages/route.ts
- /src/app/api/messages/[id]/route.ts
- /src/app/api/messages/[id]/send/route.ts
- /src/app/(authenticated)/messages/page.tsx
- /src/app/(authenticated)/messages/[conversationId]/page.tsx
- /src/lib/messaging.ts

Photo Upload:
- /src/components/avatar-upload.tsx

Job Posting:
- /src/app/(authenticated)/contractor/post-job/page.tsx
- /src/app/api/ai/generate-job/route.ts
- /src/lib/sanitize.ts

Worker Profiles:
- /src/app/(authenticated)/worker/signup/page.tsx
- /src/app/(authenticated)/worker/profile/page.tsx

Contractor Signup:
- /src/app/(authenticated)/contractor/signup/page.tsx

Payments:
- /src/app/api/payments/create/route.ts
- /src/app/api/payments/verify/route.ts

Matching:
- /src/app/api/match/find-matches/route.ts

Types & Constants:
- /src/lib/types.ts
- /src/lib/constants.ts
```