1→"use client";
     2→
     3→import { useState, useEffect, useCallback } from "react";
     4→import { useRouter } from "next/navigation";
     5→import Link from "next/link";
     6→import toast from "react-hot-toast";
     7→import { createClient } from "@/lib/supabase/client";
     8→import { Button } from "@/components/ui/button";
     9→import { Input } from "@/components/ui/input";
    10→import { Label } from "@/components/ui/label";
    11→import { Card, CardContent } from "@/components/ui/card";
    12→import VoiceRecorder from "@/components/VoiceRecorder";
    13→import { csrfFetch, addCSRFTokenToFormData, initializeCSRF } from "@/lib/csrf-client";
    14→import {
    15→  HardHat,
    16→  ArrowLeft,
    17→  ArrowRight,
    18→  Loader2,
    19→  CheckCircle2,
    20→  Sparkles,
    21→  MapPin,
    22→  Clock,
    23→  Shield,
    24→  Edit3,
    25→  Wrench,
    26→  Calendar,
    27→  Zap,
    28→  Mic,
    29→} from "lucide-react";
    30→
    31→type Step = "trade" | "experience" | "location" | "availability" | "generating" | "review";
    32→
    33→const TRADES = [
    34→  "Labourer",
    35→  "Steel Fixer",
    36→  "Formworker",
    37→  "Carpenter",
    38→  "Electrician",
    39→  "Plumber",
    40→  "Concreter",
    41→  "Bricklayer",
    42→  "Painter",
    43→  "Roofer",
    44→  "Scaffolder",
    45→  "Tiler",
    46→  "Plasterer",
    47→  "Welder",
    48→  "Crane Operator",
    49→  "Excavator Operator",
    50→];
    51→
    52→const EXPERIENCE_OPTIONS = [
    53→  { label: "Less than 1 year", value: 0 },
    54→  { label: "1-3 years", value: 2 },
    55→  { label: "3-5 years", value: 4 },
    56→  { label: "5-10 years", value: 7 },
    57→  { label: "10+ years", value: 12 },
    58→];
    59→
    60→const AVAILABILITY_OPTIONS = [
    61→  { label: "Full-time", value: "full-time" as const, desc: "Mon-Fri, regular hours" },
    62→  { label: "Casual", value: "casual" as const, desc: "As needed, flexible" },
    63→  { label: "Weekends", value: "weekends" as const, desc: "Saturday & Sunday" },
    64→  { label: "Flexible", value: "flexible" as const, desc: "Any time, any days" },
    65→];
    66→
    67→interface ProfileData {
    68→  trade: string;
    69→  experience: number;
    70→  location: string;
    71→  availability: "full-time" | "casual" | "weekends" | "flexible";
    72→}
    73→
    74→interface GeneratedProfile {
    75→  bio: string;
    76→  skills: string[];
    77→  certifications: string[];
    78→}
    79→
    80→function closestExperience(years: number): number {
    81→  const values = EXPERIENCE_OPTIONS.map((o) => o.value);
    82→  return values.reduce((prev, curr) =>
    83→    Math.abs(curr - years) < Math.abs(prev - years) ? curr : prev
    84→  );
    85→}
    86→
    87→export default function ProfileBuildPage() {
    88→  const [step, setStep] = useState<Step>("trade");
    89→  const [loading, setLoading] = useState(true);
    90→  const [generating, setGenerating] = useState(false);
    91→  const [saving, setSaving] = useState(false);
    92→
    93→  // Form data
    94→  const [trade, setTrade] = useState<string>("");
    95→  const [experience, setExperience] = useState<number | null>(null);
    96→  const [location, setLocation] = useState("");
    97→  const [availability, setAvailability] = useState<ProfileData["availability"] | null>(null);
    98→
    99→  // Generated data
   100→  const [generatedBio, setGeneratedBio] = useState("");
   101→  const [generatedSkills, setGeneratedSkills] = useState<string[]>([]);
   102→  const [generatedCerts, setGeneratedCerts] = useState<string[]>([]);
   103→
   104→  // Editable states
   105→  const [editedBio, setEditedBio] = useState("");
   106→  const [editedSkills, setEditedSkills] = useState<string[]>([]);
   107→  const [editedCerts, setEditedCerts] = useState<string[]>([]);
   108→
   109→  // Voice flow
   110→  const [showVoice, setShowVoice] = useState(false);
   111→  const [voiceProcessing, setVoiceProcessing] = useState(false);
   112→  const [voiceStatus, setVoiceStatus] = useState("");
   113→  const [voiceError, setVoiceError] = useState<string | null>(null);
   114→
   115→  const router = useRouter();
   116→  const supabase = createClient();
   117→
   118→  useEffect(() => {
   119→    async function checkAuth() {
   120→      const {
   121→        data: { user },
   122→      } = await supabase.auth.getUser();
   123→      if (!user) {
   124→        router.push("/auth/login");
   125→        return;
   126→      }
   127→      // Initialize CSRF token for API calls
   128→      await initializeCSRF();
   129→      setLoading(false);
   130→    }
   131→    checkAuth();
   132→  }, [supabase.auth, router]);
   133→
   134→  async function generateProfile() {
   135→    if (!trade || experience === null || !location || !availability) return;
   136→
   137→    setGenerating(true);
   138→    setStep("generating");
   139→
   140→    try {
   141→      const response = await csrfFetch("/api/ai/generate-profile", {
   142→        method: "POST",
   143→        headers: { "Content-Type": "application/json" },
   144→        body: JSON.stringify({
   145→          trade,
   146→          experience,
   147→          location,
   148→          availability,
   149→        }),
   150→      });
   151→
   152→      if (!response.ok) {
   153→        const errorData = await response.json().catch(() => ({}));
   154→        if (response.status === 401) {
   155→          toast.error("Session expired. Please log in again.");
   156→          router.push("/auth/login");
   157→          return;
   158→        }
   159→        if (response.status === 429) {
   160→          toast.error("Too many requests. Please wait a few minutes and try again.");
   161→          setStep("availability");
   162→          return;
   163→        }
   164→        throw new Error(errorData.error || "Failed to generate profile");
   165→      }
   166→
   167→      const data: GeneratedProfile = await response.json();
   168→
   169→      setGeneratedBio(data.bio);
   170→      setGeneratedSkills(data.skills);
   171→      setGeneratedCerts(data.certifications);
   172→
   173→      // Initialize editable versions
   174→      setEditedBio(data.bio);
   175→      setEditedSkills(data.skills);
   176→      setEditedCerts(data.certifications);
   177→
   178→      setStep("review");
   179→    } catch (error) {
   180→      console.error("Generation error:", error);
   181→      const message = error instanceof Error ? error.message : "Failed to generate profile";
   182→      toast.error(`${message}. Please try again.`);
   183→      setStep("availability");
   184→    } finally {
   185→      setGenerating(false);
   186→    }
   187→  }
   188→
   189→  const handleVoiceComplete = useCallback(async (blob: Blob) => {
   190→    setVoiceProcessing(true);
   191→    setVoiceError(null);
   192→    setVoiceStatus("Transcribing your voice...");
   193→
   194→    try {
   195→      // Step 1: Transcribe (FormData with CSRF token)
   196→      const formData = new FormData();
   197→      formData.append("audio", blob, "recording.webm");
   198→      addCSRFTokenToFormData(formData);
   199→
   200→      const transcribeRes = await fetch("/api/ai/transcribe-voice", {
   201→        method: "POST",
   202→        body: formData,
   203→      });
   204→
   205→      if (!transcribeRes.ok) {
   206→        if (transcribeRes.status === 429) {
   207→          throw new Error("Too many requests. Please wait a few minutes.");
   208→        }
   209→        const errData = await transcribeRes.json().catch(() => ({}));
   210→        throw new Error(errData.error || "Failed to transcribe audio");
   211→      }
   212→
   213→      const { text } = await transcribeRes.json();
   214→      if (!text) throw new Error("No speech detected. Please try again.");
   215→
   216→      setVoiceStatus("Understanding your details...");
   217→
   218→      // Step 2: Parse profile fields (JSON with CSRF header)
   219→      const parseRes = await csrfFetch("/api/ai/parse-profile-voice", {
   220→        method: "POST",
   221→        headers: { "Content-Type": "application/json" },
   222→        body: JSON.stringify({ transcript: text }),
   223→      });
   224→
   225→      if (!parseRes.ok) {
   226→        const errData = await parseRes.json().catch(() => ({}));
   227→        throw new Error(errData.error || "Failed to parse your details");
   228→      }
   229→
   230→      const parsed = await parseRes.json();
   231→
   232→      // Pre-fill fields
   233→      if (parsed.trade) setTrade(parsed.trade);
   234→      if (parsed.experience_years !== null && parsed.experience_years !== undefined) {
   235→        setExperience(closestExperience(parsed.experience_years));
   236→      }
   237→      if (parsed.location) setLocation(parsed.location);
   238→      if (parsed.availability) setAvailability(parsed.availability);
   239→
   240→      // Check what we got and move to the right step
   241→      setShowVoice(false);
   242→      setVoiceStatus("");
   243→
   244→      const hasTrade = !!parsed.trade;
   245→      const hasExp = parsed.experience_years !== null && parsed.experience_years !== undefined;
   246→      const hasLoc = !!parsed.location;
   247→      const hasAvail = !!parsed.availability;
   248→
   249→      if (hasTrade && hasExp && hasLoc && hasAvail) {
   250→        // All fields filled — go straight to generating
   251→        // Need to call generateProfile after state updates, so use a timeout
   252→        setTimeout(() => {
   253→          setStep("generating");
   254→        }, 0);
   255→      } else if (!hasTrade) {
   256→        setStep("trade");
   257→      } else if (!hasExp) {
   258→        setStep("experience");
   259→      } else if (!hasLoc) {
   260→        setStep("location");
   261→      } else {
   262→        setStep("availability");
   263→      }
   264→    } catch (error) {
   265→      console.error("Voice processing error:", error);
   266→      setVoiceError(error instanceof Error ? error.message : "Something went wrong. Please try again.");
   267→      setVoiceStatus("");
   268→    } finally {
   269→      setVoiceProcessing(false);
   270→    }
   271→  }, []);
   272→
   273→  // When step changes to "generating" from voice flow, trigger profile generation
   274→  useEffect(() => {
   275→    if (step === "generating" && !generating && trade && experience !== null && location && availability) {
   276→      generateProfile();
   277→    }
   278→    // eslint-disable-next-line react-hooks/exhaustive-deps
   279→  }, [step]);
   280→
   281→  async function handleSave() {
   282→    setSaving(true);
   283→
   284→    try {
   285→      const {
   286→        data: { user },
   287→      } = await supabase.auth.getUser();
   288→      if (!user) throw new Error("Not authenticated");
   289→
   290→      // Update or create worker profile
   291→      const { data: existingProfile } = await supabase
   292→        .from("worker_profiles")
   293→        .select("id")
   294→        .eq("profile_id", user.id)
   295→        .single();
   296→
   297→      const profileData = {
   298→        profile_id: user.id,
   299→        trades: [trade],
   300→        experience_years: experience,
   301→        availability,
   302→        certifications: editedCerts,
   303→        bio: editedBio,
   304→        ai_extracted: {
   305→          skills: editedSkills,
   306→          generated_at: new Date().toISOString(),
   307→          original_bio: generatedBio,
   308→          original_skills: generatedSkills,
   309→          original_certs: generatedCerts,
   310→        },
   311→      };
   312→
   313→      if (existingProfile) {
   314→        await supabase
   315→          .from("worker_profiles")
   316→          .update(profileData)
   317→          .eq("profile_id", user.id);
   318→      } else {
   319→        await supabase.from("worker_profiles").insert(profileData);
   320→      }
   321→
   322→      // Update main profile
   323→      await supabase
   324→        .from("profiles")
   325→        .update({
   326→          suburb: location,
   327→          onboarding_completed: true,
   328→        })
   329→        .eq("id", user.id);
   330→
   331→      router.push("/dashboard");
   332→      router.refresh();
   333→    } catch (error) {
   334→      console.error("Save error:", error);
   335→      toast.error("Failed to save profile. Please try again.");
   336→    } finally {
   337→      setSaving(false);
   338→    }
   339→  }
   340→
   341→  const stepOrder: Step[] = ["trade", "experience", "location", "availability", "generating", "review"];
   342→  const currentIndex = stepOrder.indexOf(step);
   343→  const totalSteps = 4; // Only count user-facing steps
   344→
   345→  if (loading) {
   346→    return (
   347→

   348→        <Loader2 className="h-8 w-8 animate-spin text-primary" />
   349→

   350→    );
   351→  }
   352→
   353→  return (
   354→

   355→
   356→        {/* Progress bar */}
   357→        {step !== "generating" && step !== "review" && (
   358→

   359→            {Array.from({ length: totalSteps }, (_, i) => (
   360→              <div
   361→                key={i}
   362→                className={`h-1.5 flex-1 rounded-full transition-colors ${
   363→                  i <= currentIndex ? "bg-primary" : "bg-muted"
   364→                }`}
   365→              />
   366→            ))}
   367→

   368→        )}
   369→
   370→        {/* Step: Trade */}
   371→        {step === "trade" && (
   372→

   373→            <StepHeader
   374→              icon={<Wrench className="h-5 w-5" />}
   375→              title="What's your trade?"
   376→              subtitle="Pick your primary trade"
   377→            />
   378→
   379→            {/* Voice option */}
   380→            {!showVoice && (
   381→              <button
   382→                onClick={() => setShowVoice(true)}
   383→                className="mt-6 flex w-full items-center justify-center gap-3 rounded-xl border-2 border-dashed border-primary/40 bg-primary/5 px-5 py-4 text-base font-medium text-primary transition-colors hover:border-primary hover:bg-primary/10 active:scale-95"
   384→                style={{ minHeight: "64px" }}
   385→              >
   386→                <Mic className="h-6 w-6" />
   387→                Or tell us with your voice
   388→              </button>
   389→            )}
   390→
   391→            {showVoice && (
   392→              <Card className="mt-6">
   393→                <CardContent className="flex flex-col items-center gap-4 pt-6">
   394→

   395→                    Say something like: “I'm a chippie with 8 years experience in Parramatta, looking for full-time work”
   396→

   397→
   398→                  {voiceProcessing ? (
   399→

   400→                      <Loader2 className="h-10 w-10 animate-spin text-primary" />
   401→
{voiceStatus}

   402→

   403→                  ) : (
   404→                    <VoiceRecorder onRecordingComplete={handleVoiceComplete} />
   405→                  )}
   406→
   407→                  {voiceError && (
   408→
{voiceError}

   409→                  )}
   410→
   411→                  <button
   412→                    onClick={() => {
   413→                      setShowVoice(false);
   414→                      setVoiceError(null);
   415→                    }}
   416→                    className="text-sm text-muted-foreground hover:text-foreground"
   417→                  >
   418→                    Cancel — I'll tap instead
   419→                  </button>
   420→                </CardContent>
   421→              </Card>
   422→            )}
   423→
   424→

   425→              {TRADES.map((t) => (
   426→                <button
   427→                  key={t}
   428→                  onClick={() => {
   429→                    setTrade(t);
   430→                    setStep("experience");
   431→                  }}
   432→                  className={`rounded-xl border-2 px-4 py-4 text-left text-base font-medium transition-colors active:scale-95 ${
   433→                    trade === t
   434→                      ? "border-primary bg-primary/10 text-primary"
   435→                      : "border-border hover:border-primary/50"
   436→                  }`}
   437→                  style={{ minHeight: "64px" }}
   438→                >
   439→                  {t}
   440→                </button>
   441→              ))}
   442→

   443→

   444→        )}
   445→
   446→        {/* Step: Experience */}
   447→        {step === "experience" && (
   448→

   449→            <StepHeader
   450→              icon={<Clock className="h-5 w-5" />}
   451→              title="Your experience?"
   452→              subtitle="Years in construction"
   453→            />
   454→
   455→

   456→              {EXPERIENCE_OPTIONS.map((opt) => (
   457→                <button
   458→                  key={opt.value}
   459→                  onClick={() => {
   460→                    setExperience(opt.value);
   461→                    setStep("location");
   462→                  }}
   463→                  className={`flex w-full items-center justify-between rounded-xl border-2 px-5 text-left text-base font-medium transition-colors active:scale-95 ${
   464→                    experience === opt.value
   465→                      ? "border-primary bg-primary/10 text-primary"
   466→                      : "border-border hover:border-primary/50"
   467→                  }`}
   468→                  style={{ minHeight: "64px" }}
   469→                >
   470→                  {opt.label}
   471→                  <ArrowRight className="h-5 w-5 text-muted-foreground" />
   472→                </button>
   473→              ))}
   474→

   475→
   476→            <button
   477→              onClick={() => setStep("trade")}
   478→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   479→            >
   480→              <ArrowLeft className="h-4 w-4" />
   481→              Back
   482→            </button>
   483→

   484→        )}
   485→
   486→        {/* Step: Location */}
   487→        {step === "location" && (
   488→

   489→            <StepHeader
   490→              icon={<MapPin className="h-5 w-5" />}
   491→              title="Where are you based?"
   492→              subtitle="Your suburb or area"
   493→            />
   494→
   495→            <form
   496→              onSubmit={(e) => {
   497→                e.preventDefault();
   498→                if (location.trim()) setStep("availability");
   499→              }}
   500→              className="mt-6 space-y-4"
   501→            >
   502→

   503→                Suburb or Area
   504→                <Input
   505→                  id="location"
   506→                  placeholder="e.g. Parramatta, Blacktown"
   507→                  value={location}
   508→                  onChange={(e) => setLocation(e.target.value)}
   509→                  required
   510→                  autoFocus
   511→                  className="h-14 text-base"
   512→                />
   513→

   514→

   515→                We'll match you to nearby jobs
   516→

   517→
   518→

   519→                <Button
   520→                  type="button"
   521→                  variant="outline"
   522→                  onClick={() => setStep("experience")}
   523→                  className="h-14 w-14 p-0"
   524→                >
   525→                  <ArrowLeft className="h-5 w-5" />
   526→                </Button>
   527→                <Button
   528→                  type="submit"
   529→                  disabled={!location.trim()}
   530→                  className="h-14 flex-1 gap-2 text-base font-semibold"
   531→                >
   532→                  Continue
   533→                  <ArrowRight className="h-5 w-5" />
   534→                </Button>
   535→

   536→            </form>
   537→

   538→        )}
   539→
   540→        {/* Step: Availability */}
   541→        {step === "availability" && (
   542→

   543→            <StepHeader
   544→              icon={<Calendar className="h-5 w-5" />}
   545→              title="Your availability?"
   546→              subtitle="When can you work?"
   547→            />
   548→
   549→

   550→              {AVAILABILITY_OPTIONS.map((opt) => (
   551→                <button
   552→                  key={opt.value}
   553→                  onClick={() => {
   554→                    setAvailability(opt.value);
   555→                    generateProfile();
   556→                  }}
   557→                  className={`flex w-full items-start justify-between rounded-xl border-2 px-5 py-4 text-left transition-colors active:scale-95 ${
   558→                    availability === opt.value
   559→                      ? "border-primary bg-primary/10"
   560→                      : "border-border hover:border-primary/50"
   561→                  }`}
   562→                  style={{ minHeight: "64px" }}
   563→                >
   564→

   565→                    <p className={`text-base font-semibold ${availability === opt.value ? "text-primary" : ""}`}>
   566→                      {opt.label}
   567→

   568→
{opt.desc}

   569→

   570→                  <Zap className="h-5 w-5 shrink-0 text-primary" />
   571→                </button>
   572→              ))}
   573→

   574→
   575→            <button
   576→              onClick={() => setStep("location")}
   577→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   578→            >
   579→              <ArrowLeft className="h-4 w-4" />
   580→              Back
   581→            </button>
   582→

   583→        )}
   584→
   585→        {/* Step: Generating */}
   586→        {step === "generating" && (
   587→

   588→

   589→              <Sparkles className="h-10 w-10 animate-pulse text-primary" />
   590→

   591→
Building your profile...

   592→

   593→              AI is generating your bio, skills, and certifications
   594→

   595→

   596→

   597→

   598→

   599→

   600→

   601→        )}
   602→
   603→        {/* Step: Review */}
   604→        {step === "review" && (
   605→

   606→

   607→

   608→                <Sparkles className="h-5 w-5" />
   609→

   610→

   611→
Review & Edit

   612→

   613→                  AI generated this — customise as needed
   614→

   615→

   616→

   617→
   618→            {/* Your Answers Summary */}
   619→            <Card className="mt-6">
   620→              <CardContent className="space-y-3 pt-6">
   621→

   622→

   623→                    <Wrench className="h-4 w-4 text-muted-foreground" />
   624→                    {trade}
   625→

   626→                  <button
   627→                    onClick={() => setStep("trade")}
   628→                    className="text-xs text-primary hover:underline"
   629→                  >
   630→                    Change
   631→                  </button>
   632→

   633→

   634→

   635→                    <Clock className="h-4 w-4 text-muted-foreground" />
   636→
   637→                      {EXPERIENCE_OPTIONS.find((o) => o.value === experience)?.label}
   638→
   639→

   640→                  <button
   641→                    onClick={() => setStep("experience")}
   642→                    className="text-xs text-primary hover:underline"
   643→                  >
   644→                    Change
   645→                  </button>
   646→

   647→

   648→

   649→                    <MapPin className="h-4 w-4 text-muted-foreground" />
   650→                    {location}
   651→

   652→                  <button
   653→                    onClick={() => setStep("location")}
   654→                    className="text-xs text-primary hover:underline"
   655→                  >
   656→                    Change
   657→                  </button>
   658→

   659→              </CardContent>
   660→            </Card>
   661→
   662→            {/* Bio */}
   663→

   664→

   665→
   666→                  <Sparkles className="h-4 w-4 text-primary" />
   667→                  Your Bio
   668→
   669→                AI-generated
   670→

   671→              <textarea
   672→                value={editedBio}
   673→                onChange={(e) => setEditedBio(e.target.value)}
   674→                className="min-h-[120px] w-full rounded-xl border-2 border-border bg-background px-4 py-3 text-base focus:border-primary focus:outline-none"
   675→                placeholder="Your professional bio..."
   676→              />
   677→

   678→
   679→            {/* Skills */}
   680→

   681→

   682→
   683→                  <Zap className="h-4 w-4 text-primary" />
   684→                  Skills
   685→
   686→                Tap to remove
   687→

   688→

   689→                {editedSkills.map((skill, idx) => (
   690→                  <button
   691→                    key={idx}
   692→                    onClick={() => setEditedSkills(editedSkills.filter((_, i) => i !== idx))}
   693→                    className="rounded-full border-2 border-primary bg-primary/10 px-4 py-2 text-sm font-medium text-primary transition-colors hover:bg-primary/20 active:scale-95"
   694→                  >
   695→                    {skill} ×
   696→                  </button>
   697→                ))}
   698→

   699→

   700→
   701→            {/* Certifications */}
   702→

   703→

   704→
   705→                  <Shield className="h-4 w-4 text-primary" />
   706→                  Certifications
   707→
   708→                Tap to remove
   709→

   710→

   711→                {editedCerts.map((cert, idx) => (
   712→                  <button
   713→                    key={idx}
   714→                    onClick={() => setEditedCerts(editedCerts.filter((_, i) => i !== idx))}
   715→                    className="flex w-full items-center justify-between rounded-xl border-2 border-border bg-background px-4 py-3 text-left text-sm font-medium transition-colors hover:border-destructive hover:bg-destructive/5 active:scale-95"
   716→                  >
   717→                    {cert}
   718→                    Remove
   719→                  </button>
   720→                ))}
   721→

   722→

   723→

   724→        )}
   725→
   726→
   727→      {/* Bottom sticky CTA - only show on review step */}
   728→      {step === "review" && (
   729→

   730→

   731→            <Button
   732→              onClick={handleSave}
   733→              className="h-16 w-full gap-2 text-lg font-semibold"
   734→              disabled={saving}
   735→            >
   736→              {saving ? (
   737→                <>
   738→                  <Loader2 className="h-6 w-6 animate-spin" />
   739→                  Saving profile...
   740→                </>
   741→              ) : (
   742→                <>
   743→                  <CheckCircle2 className="h-6 w-6" />
   744→                  Save My Profile
   745→                </>
   746→              )}
   747→            </Button>
   748→

   749→

   750→      )}
   751→

   752→  );
   753→}
   754→
   755→function StepHeader({
   756→  icon,
   757→  title,
   758→  subtitle,
   759→}: {
   760→  icon: React.ReactNode;
   761→  title: string;
   762→  subtitle: string;
   763→}) {
   764→  return (
   765→

   766→

   767→        {icon}
   768→

   769→

   770→
{title}

   771→
{subtitle}

   772→

   773→

   774→  );
   775→}
   776→

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


<tool_use_error>File does not exist.</tool_use_error>


Let me read the messages conversation page with proper escaping:


Bash

find /home/ccuser/the-50-dollar-app/src/app/\(authenticated\)/messages -name "page.tsx" -o -name "layout.tsx"


/home/ccuser/the-50-dollar-app/src/app/(authenticated)/messages/page.tsx
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/messages/[conversationId]/page.tsx


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

   170→
   171→

   172→

   173→              <Button
   174→                variant="ghost"
   175→                size="icon"
   176→                onClick={() => router.push("/messages")}
   177→                className="h-10 w-10"
   178→              >
   179→                <ArrowLeft className="h-5 w-5" />
   180→              </Button>
   181→

   182→

   183→

   184→

   185→

   186→

   187→

   188→

   189→

   190→
   191→

   192→
Loading messages...

   193→

   194→

   195→    );
   196→  }
   197→
   198→  return (
   199→

   200→      {/* Header */}
   201→
   202→

   203→

   204→            <Button
   205→              variant="ghost"
   206→              size="icon"
   207→              onClick={() => router.push("/messages")}
   208→              className="h-10 w-10"
   209→            >
   210→              <ArrowLeft className="h-5 w-5" />
   211→            </Button>
   212→

   213→              <Avatar className="h-10 w-10">
   214→                <AvatarImage src={otherParticipant?.avatar || ""} alt={otherParticipant?.name || ""} />
   215→                <AvatarFallback className="bg-primary/10 text-primary font-semibold">
   216→                  {initials}
   217→                </AvatarFallback>
   218→              </Avatar>
   219→

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

   221→

   222→                  {otherParticipant?.isOnline ? "Online" : "Offline"}
   223→

   224→

   225→

   226→

   227→

   228→            <Button variant="ghost" size="icon" className="h-10 w-10">
   229→              <Phone className="h-5 w-5" />
   230→            </Button>
   231→            <Button variant="ghost" size="icon" className="h-10 w-10">
   232→              <MoreVertical className="h-5 w-5" />
   233→            </Button>
   234→

   235→

   236→
   237→
   238→      {/* Messages */}
   239→
   240→

   241→          {messages.length === 0 ? (
   242→

   243→
No messages yet. Say hello!

   244→

   245→          ) : (
   246→

   247→              {messages.map((msg) => (
   248→                <MessageBubble
   249→                  key={msg.id}
   250→                  content={msg.content}
   251→                  isOwn={msg.isOwn}
   252→                  timestamp={msg.timestamp}
   253→                  status={msg.status}
   254→                />
   255→              ))}
   256→

   257→

   258→          )}
   259→

   260→
   261→
   262→      {/* Message Input */}
   263→
   264→

   265→          <form onSubmit={handleSendMessage} className="flex gap-3">
   266→            <Input
   267→              type="text"
   268→              placeholder="Type a message..."
   269→              value={message}
   270→              onChange={(e) => setMessage(e.target.value)}
   271→              className="flex-1 h-12 text-base"
   272→              disabled={sending}
   273→            />
   274→            <Button
   275→              type="submit"
   276→              size="icon"
   277→              className="h-12 w-12 shrink-0"
   278→              disabled={!message.trim() || sending}
   279→            >
   280→              <Send className="h-5 w-5" />
   281→            </Button>
   282→          </form>
   283→

   284→
   285→

   286→  );
   287→}
   288→

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


Now let me read all the API routes systematically:


     1→import { formatABN } from "@/lib/abn-utils";
     2→import { NextRequest, NextResponse } from 'next/server';
     3→import { isValidABN, cleanABN } from '@/lib/abn-utils';
     4→import { checkRateLimit, getClientIP, createRateLimitResponse } from '@/lib/rate-limit';
     5→import { sanitizeInput } from '@/lib/sanitize';
     6→
     7→/**
     8→ * ABN Lookup API Route — GET /api/abn-lookup?abn=XXXXXXXXXXX
     9→ *
    10→ * Validates the ABN locally (checksum), then calls the ABR JSON endpoint
    11→ * to fetch entity details.
    12→ *
    13→ * ABR JSON docs: https://abr.business.gov.au/json/
    14→ * Registration:  https://abr.business.gov.au/Tools/WebServices
    15→ *
    16→ * Required env var:
    17→ *   ABR_GUID — the authentication GUID issued after registration
    18→ *
    19→ * Optional env var:
    20→ *   ABR_DEV_MODE — set to 'true' to enable development mode with mock data
    21→ */
    22→
    23→// ── ABR configuration ──────────────────────────────────────────────
    24→const ABR_JSON_URL = 'https://abr.business.gov.au/json/AbnDetails.aspx';
    25→const ABR_GUID = process.env.ABR_GUID ?? '';
    26→const ABR_DEV_MODE = process.env.ABR_DEV_MODE === 'true' || process.env.NODE_ENV === 'development';
    27→
    28→// ── Types ──────────────────────────────────────────────────────────
    29→
    30→/** Shape returned by the ABR JSON endpoint (JSONP-unwrapped). */
    31→interface ABRJsonResponse {
    32→  Abn: string;
    33→  AbnStatus: string;
    34→  AbnStatusEffectiveFrom: string;
    35→  Acn: string;
    36→  AddressDate: string | null;
    37→  AddressPostcode: string;
    38→  AddressState: string;
    39→  BusinessName: Array<string | { Value: string; EffectiveFrom: string }>;
    40→  EntityName: string;
    41→  EntityTypeCode: string;
    42→  EntityTypeName: string;
    43→  Gst: string | null;
    44→  Message: string;
    45→}
    46→
    47→/** Director verification result */
    48→interface DirectorVerification {
    49→  status: 'pending' | 'verified' | 'mismatch' | 'not_available';
    50→  message: string;
    51→  requiresManualReview?: boolean;
    52→}
    53→
    54→/** Company data from ABR */
    55→interface CompanyData {
    56→  name: string;
    57→  abn: string;
    58→  status: string;
    59→  statusDate: string;
    60→  state: string;
    61→  postcode: string;
    62→  type: string;
    63→  typeCode: string;
    64→  gstRegistered: boolean;
    65→  acn?: string;
    66→  directorVerification?: DirectorVerification;
    67→}
    68→
    69→/** Our normalised response. */
    70→interface ABNLookupResult {
    71→  success: boolean;
    72→  data?: CompanyData;
    73→  error?: string;
    74→  devMode?: boolean;
    75→}
    76→
    77→// ── Development Mode Mock Data ─────────────────────────────────────
    78→
    79→/**
    80→ * Mock company data for development when ABR_GUID is not configured.
    81→ * Uses obviously fake ABNs to prevent confusion with real data.
    82→ */
    83→const MOCK_COMPANIES: Record<string, CompanyData> = {
    84→  // Test ABN for development - passes checksum but is obviously fake
    85→  '51824753556': {
    86→    name: 'TEST COMPANY PTY LTD (Dev Mode)',
    87→    abn: '51 824 753 556',
    88→    status: 'Active',
    89→    statusDate: '2020-01-01',
    90→    state: 'NSW',
    91→    postcode: '2000',
    92→    type: 'Australian Private Company',
    93→    typeCode: 'PRV',
    94→    gstRegistered: true,
    95→    directorVerification: {
    96→      status: 'not_available',
    97→      message: 'Director verification unavailable in development mode',
    98→    },
    99→  },
   100→  // Another test ABN
   101→  '34241177887': {
   102→    name: 'EXAMPLE BUILDERS PTY LTD (Dev Mode)',
   103→    abn: '34 241 177 887',
   104→    status: 'Active',
   105→    statusDate: '2019-06-15',
   106→    state: 'VIC',
   107→    postcode: '3000',
   108→    type: 'Australian Private Company',
   109→    typeCode: 'PRV',
   110→    gstRegistered: true,
   111→    directorVerification: {
   112→      status: 'not_available',
   113→      message: 'Director verification unavailable in development mode',
   114→    },
   115→  },
   116→};
   117→
   118→/**
   119→ * Generate mock data for any valid ABN in development mode.
   120→ */
   121→function generateMockData(abn: string): CompanyData {
   122→  const formatted = formatABN(abn);
   123→  return {
   124→    name: `MOCK COMPANY ${abn.slice(-4)} PTY LTD (Dev Mode)`,
   125→    abn: formatted,
   126→    status: 'Active',
   127→    statusDate: new Date().toISOString().split('T')[0],
   128→    state: 'NSW',
   129→    postcode: '2000',
   130→    type: 'Australian Private Company',
   131→    typeCode: 'PRV',
   132→    gstRegistered: Math.random() > 0.3,
   133→    directorVerification: {
   134→      status: 'not_available',
   135→      message: 'Director verification unavailable in development mode. Register at https://abr.business.gov.au/Tools/WebServices',
   136→    },
   137→  };
   138→}
   139→
   140→// ── Helpers ────────────────────────────────────────────────────────
   141→
   142→function buildABRUrl(abn: string): string {
   143→  const params = new URLSearchParams({
   144→    abn,
   145→    guid: ABR_GUID,
   146→    callback: 'cbr',
   147→  });
   148→  return `${ABR_JSON_URL}?${params.toString()}`;
   149→}
   150→
   151→function stripJSONP(jsonp: string): ABRJsonResponse {
   152→  const match = jsonp.match(/^cbr\((.*)\)$/);
   153→  if (!match) throw new Error('Unexpected ABR response format');
   154→  return JSON.parse(match[1]);
   155→}
   156→
   157→function normaliseABRResponse(abr: ABRJsonResponse): CompanyData {
   158→  const name = abr.EntityName ||
   159→    (Array.isArray(abr.BusinessName) && abr.BusinessName[0]) ||
   160→    'Unknown';
   161→
   162→  return {
   163→    name: typeof name === 'string' ? name : (name as any).Value || 'Unknown',
   164→    abn: formatABN(abr.Abn),
   165→    status: abr.AbnStatus,
   166→    statusDate: abr.AbnStatusEffectiveFrom,
   167→    state: abr.AddressState,
   168→    postcode: abr.AddressPostcode,
   169→    type: abr.EntityTypeName,
   170→    typeCode: abr.EntityTypeCode,
   171→    gstRegistered: abr.Gst !== null,
   172→    acn: abr.Acn || undefined,
   173→    // Director verification will be done separately via ASIC
   174→    directorVerification: {
   175→      status: 'pending',
   176→      message: 'Director verification pending. ASIC lookup required.',
   177→      requiresManualReview: true,
   178→    },
   179→  };
   180→}
   181→
   182→/**
   183→ * ASIC Director Verification (Stub)
   184→ *
   185→ * ASIC Connect API requires paid subscription.
   186→ * For MVP, we flag all signups for manual review.
   187→ *
   188→ * Future implementation:
   189→ * - Call ASIC Connect API to get director list
   190→ * - Cross-reference user email/name with director records
   191→ * - Auto-verify if match found
   192→ * - Flag for manual review if no match
   193→ *
   194→ * @see https://connectonline.asic.gov.au/
   195→ */
   196→async function verifyDirector(
   197→  abn: string,
   198→  acn?: string,
   199→  userEmail?: string
   200→): Promise<DirectorVerification> {
   201→  // TODO: Implement ASIC Connect API integration
   202→  // For now, flag all for manual review
   203→
   204→  console.log(`[ASIC] Director verification requested for ABN ${abn}, ACN ${acn || 'N/A'}`);
   205→
   206→  return {
   207→    status: 'pending',
   208→    message: 'Director verification requires manual review. ASIC integration pending.',
   209→    requiresManualReview: true,
   210→  };
   211→}
   212→
   213→// ── Route Handler ──────────────────────────────────────────────────
   214→
   215→// Rate limit: 10 requests per 15 minutes per IP (external API call)
   216→const ABN_LOOKUP_RATE_LIMIT = 10;
   217→const ABN_LOOKUP_WINDOW_MS = 15 * 60 * 1000;
   218→
   219→export async function GET(request: NextRequest) {
   220→  try {
   221→    // --- 0. Rate limiting ---------------------------------------------------
   222→    const clientIP = getClientIP(request);
   223→    const rateLimit = checkRateLimit(`abn-lookup:${clientIP}`, ABN_LOOKUP_RATE_LIMIT, ABN_LOOKUP_WINDOW_MS);
   224→
   225→    if (!rateLimit.allowed) {
   226→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
   227→      return createRateLimitResponse(clientIP, retryAfter);
   228→    }
   229→
   230→    const searchParams = request.nextUrl.searchParams;
   231→    const rawABN = searchParams.get('abn');
   232→    const rawEmail = searchParams.get('email');
   233→    const userEmail = rawEmail ? sanitizeInput(rawEmail, { maxLength: 254 }) : null;
   234→
   235→    // --- 1. Validate input --------------------------------------------------
   236→    if (!rawABN) {
   237→      return NextResponse.json({
   238→        success: false,
   239→        error: 'Missing query parameter: abn'
   240→      }, { status: 400 });
   241→    }
   242→
   243→    const abn = cleanABN(rawABN);
   244→    if (!isValidABN(abn)) {
   245→      return NextResponse.json({
   246→        success: false,
   247→        error: 'Invalid ABN format. Please check the number and try again.'
   248→      }, { status: 400 });
   249→    }
   250→
   251→    // --- 2. Check if ABR_GUID is configured --------------------------------
   252→    const isGuidConfigured = ABR_GUID &&
   253→      ABR_GUID !== 'REGISTER-TOMORROW-AT-ABR-BUSINESS-GOV-AU' &&
   254→      ABR_GUID !== 'temporary-abr-guid-for-build';
   255→
   256→    if (!isGuidConfigured) {
   257→      // Development mode: return mock data
   258→      if (ABR_DEV_MODE) {
   259→        console.log(`[ABN Lookup] Dev mode - returning mock data for ${abn}`);
   260→        const mockData = MOCK_COMPANIES[abn] || generateMockData(abn);
   261→        return NextResponse.json({
   262→          success: true,
   263→          data: mockData,
   264→          devMode: true,
   265→        });
   266→      }
   267→
   268→      // Production without GUID: return error with validation info
   269→      return NextResponse.json({
   270→        success: false,
   271→        error: 'ABR service not configured',
   272→        devMode: false,
   273→      }, { status: 503 });
   274→    }
   275→
   276→    // --- 3. Call ABR web service -------------------------------------------
   277→    const url = buildABRUrl(abn);
   278→    const abrResponse = await fetch(url, {
   279→      method: 'GET',
   280→      headers: { 'User-Agent': 'RateRight/1.0' },
   281→      signal: AbortSignal.timeout(10000),
   282→    });
   283→
   284→    if (!abrResponse.ok) {
   285→      const text = await abrResponse.text();
   286→      console.error(`[ABN Lookup] ABR error ${abrResponse.status}:`, text);
   287→      return NextResponse.json({
   288→        success: false,
   289→        error: `ABR service error: ${abrResponse.status}`,
   290→      }, { status: 502 });
   291→    }
   292→
   293→    const jsonpText = await abrResponse.text();
   294→    const abrData = stripJSONP(jsonpText);
   295→
   296→    // --- 4. Handle ABR business logic ---------------------------------------
   297→    if (abrData.Message) {
   298→      return NextResponse.json({
   299→        success: false,
   300→        error: abrData.Message
   301→      }, { status: 404 });
   302→    }
   303→
   304→    const result = normaliseABRResponse(abrData);
   305→
   306→    // --- 5. Director verification (async, non-blocking) --------------------
   307→    if (result.acn || userEmail) {
   308→      result.directorVerification = await verifyDirector(
   309→        abn,
   310→        result.acn,
   311→        userEmail || undefined
   312→      );
   313→    }
   314→
   315→    return NextResponse.json({
   316→      success: true,
   317→      data: result,
   318→    });
   319→
   320→  } catch (err: any) {
   321→    console.error('[ABN Lookup] Uncaught error:', err);
   322→    return NextResponse.json({
   323→      success: false,
   324→      error: 'Internal server error',
   325→    }, { status: 500 });
   326→  }
   327→}
   328→

<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 { createCSRTEndpoint } from '@/lib/csrf';
     3→import { createClient } from '@/lib/supabase/server';
     4→
     5→/**
     6→ * CSRF Token Endpoint
     7→ * Generates and returns a CSRF token for authenticated users
     8→ * This should be called after successful authentication
     9→ */
    10→export async function GET(request: NextRequest) {
    11→  try {
    12→    // Check if user is authenticated
    13→    const supabase = await createClient();
    14→    const { data: { user } } = await supabase.auth.getUser();
    15→
    16→    if (!user) {
    17→      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    18→    }
    19→
    20→    // Create and return CSRF token
    21→    return createCSRTEndpoint(request);
    22→  } catch (error) {
    23→    console.error('CSRF token generation error:', error);
    24→    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
    25→  }
    26→}

<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 { 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 { createClient } from "@/lib/supabase/server";
     2→import { NextResponse } from "next/server";
     3→
     4→export async function GET() {
     5→  try {
     6→    const supabase = await createClient();
     7→
     8→    // Check if user is authenticated
     9→    const { data: { user }, error: authError } = await supabase.auth.getUser();
    10→
    11→    if (authError || !user) {
    12→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    13→    }
    14→
    15→    // Get conversation previews using the database function
    16→    const { data: conversations, error } = await supabase
    17→      .rpc('get_conversation_previews', { user_id: user.id });
    18→
    19→    if (error) {
    20→      console.error("Error fetching conversations:", error);
    21→      return NextResponse.json(
    22→        { error: "Failed to fetch conversations" },
    23→        { status: 500 }
    24→      );
    25→    }
    26→
    27→    return NextResponse.json({ conversations: conversations || [] });
    28→  } catch (error) {
    29→    console.error("Error in messages API:", error);
    30→    return NextResponse.json(
    31→      { error: "Internal server error" },
    32→      { status: 500 }
    33→    );
    34→  }
    35→}

<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 continue with more API routes:


<tool_use_error>Sibling tool call errored</tool_use_error>


total 20
drwxrwxr-x  4 ccuser ccuser 4096 Feb  8 11:18 .
drwxrwxr-x 10 ccuser ccuser 4096 Feb  8 19:21 ..
drwxrwxr-x  3 ccuser ccuser 4096 Feb  8 11:18 [id]
-rw-rw-r--  1 ccuser ccuser 1050 Feb  8 11:18 route.ts
drwxrwxr-x  2 ccuser ccuser 4096 Feb  8 11:18 start


total 12
drwxrwxr-x  3 ccuser ccuser 4096 Feb  8 11:18 .
drwxrwxr-x 10 ccuser ccuser 4096 Feb  8 19:21 ..
drwxrwxr-x  2 ccuser ccuser 4096 Feb  8 11:18 create


find /home/ccuser/the-50-dollar-app/src/app/api -name "route.ts" | sort


     1→import { NextRequest, NextResponse } from "next/server";
     2→import OpenAI from "openai";
     3→import { createClient } from "@/lib/supabase/server";
     4→import { checkRateLimit, getClientIP, createRateLimitResponse } from "@/lib/rate-limit";
     5→import { sanitizeInput } from "@/lib/sanitize";
     6→import { withCSRFProtection } from "@/lib/csrf";
     7→
     8→const openai = new OpenAI({
     9→  apiKey: process.env.OPENAI_API_KEY || "",
    10→});
    11→
    12→// Rate limit: 5 requests per 15 minutes per IP (expensive AI call)
    13→const AI_RATE_LIMIT = 5;
    14→const AI_WINDOW_MS = 15 * 60 * 1000;
    15→
    16→const handler = async (request: NextRequest) => {
    17→  try {
    18→    // --- Rate limiting ---
    19→    const clientIP = getClientIP(request);
    20→    const rateLimit = checkRateLimit(`ai-profile:${clientIP}`, AI_RATE_LIMIT, AI_WINDOW_MS);
    21→
    22→    if (!rateLimit.allowed) {
    23→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    24→      return createRateLimitResponse(clientIP, retryAfter);
    25→    }
    26→
    27→    // --- Auth check ---
    28→    const supabase = await createClient();
    29→    const { data: { user }, error: authError } = await supabase.auth.getUser();
    30→    if (authError || !user) {
    31→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    32→    }
    33→
    34→    const { trade, experience, location, availability } = await request.json();
    35→
    36→    if (!trade || experience === undefined || !location || !availability) {
    37→      return NextResponse.json(
    38→        { error: "Missing required fields" },
    39→        { status: 400 }
    40→      );
    41→    }
    42→
    43→    // Sanitize inputs to prevent prompt injection
    44→    const sanitizedTrade = sanitizeInput(trade, { maxLength: 100 });
    45→    const sanitizedLocation = sanitizeInput(location, { maxLength: 200 });
    46→    const sanitizedAvailability = sanitizeInput(availability, { maxLength: 50 });
    47→
    48→    // Validate experience is a number
    49→    const experienceYears = parseInt(experience, 10);
    50→    if (isNaN(experienceYears) || experienceYears < 0 || experienceYears > 100) {
    51→      return NextResponse.json(
    52→        { error: "Invalid experience value" },
    53→        { status: 400 }
    54→      );
    55→    }
    56→
    57→    const prompt = `You are an expert construction industry recruiter. Based on the following information about a construction worker, generate a professional profile.
    58→
    59→Worker Information:
    60→- Trade: ${sanitizedTrade}
    61→- Years of Experience: ${experienceYears} years
    62→- Location: ${sanitizedLocation}
    63→- Availability: ${sanitizedAvailability}
    64→
    65→Generate a JSON response with:
    66→1. bio: A professional 2-3 sentence bio (50-80 words) highlighting their expertise, experience, and work style
    67→2. skills: An array of 6-10 relevant technical skills for their trade (specific tools, techniques, materials)
    68→3. certifications: An array of 3-5 common/recommended certifications for their trade (actual Australian construction certifications)
    69→
    70→Make it sound professional but approachable. Focus on practical skills and experience.
    71→
    72→Return ONLY valid JSON in this exact format:
    73→{
    74→  "bio": "string",
    75→  "skills": ["skill1", "skill2", ...],
    76→  "certifications": ["cert1", "cert2", ...]
    77→}`;
    78→
    79→    const completion = await openai.chat.completions.create({
    80→      model: "gpt-4o-mini",
    81→      messages: [
    82→        {
    83→          role: "system",
    84→          content: "You are a construction industry expert. Always respond with valid JSON only.",
    85→        },
    86→        {
    87→          role: "user",
    88→          content: prompt,
    89→        },
    90→      ],
    91→      temperature: 0.7,
    92→      response_format: { type: "json_object" },
    93→    });
    94→
    95→    const content = completion.choices[0]?.message?.content;
    96→    if (!content) {
    97→      throw new Error("No response from AI");
    98→    }
    99→
   100→    const generated = JSON.parse(content);
   101→
   102→    return NextResponse.json({
   103→      bio: generated.bio || "",
   104→      skills: generated.skills || [],
   105→      certifications: generated.certifications || [],
   106→    });
   107→  } catch (error) {
   108→    console.error("AI generation error:", error);
   109→
   110→    // Return more specific error messages
   111→    if (error instanceof Error) {
   112→      if (error.message.includes("API key")) {
   113→        return NextResponse.json(
   114→          { error: "AI service configuration error" },
   115→          { status: 503 }
   116→        );
   117→      }
   118→      if (error.message.includes("rate limit") || error.message.includes("429")) {
   119→        return NextResponse.json(
   120→          { error: "AI service is busy. Please try again in a moment." },
   121→          { status: 429 }
   122→        );
   123→      }
   124→    }
   125→
   126→    return NextResponse.json(
   127→      { error: "Failed to generate profile. Please try again." },
   128→      { status: 500 }
   129→    );
   130→  }
   131→}
   132→
   133→// Apply CSRF protection to the handler
   134→export const POST = withCSRFProtection(handler);
   135→

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


     1→import { NextRequest, NextResponse } from 'next/server';
     2→import { constructWebhookEvent, stripe } from '@/lib/stripe/server';
     3→import { getSupabaseAdmin } from '@/lib/supabase/server';
     4→import type Stripe from 'stripe';
     5→
     6→// Disable body parsing for webhook verification
     7→export const runtime = 'nodejs';
     8→export const dynamic = 'force-dynamic';
     9→
    10→export async function POST(request: NextRequest) {
    11→  try {
    12→    // Get the raw body as text for signature verification
    13→    const payload = await request.text();
    14→    const signature = request.headers.get('stripe-signature');
    15→
    16→    if (!signature) {
    17→      return NextResponse.json({ error: 'Missing stripe-signature header' }, { status: 400 });
    18→    }
    19→
    20→    // Verify webhook signature and construct event
    21→    let event: Stripe.Event;
    22→    try {
    23→      event = constructWebhookEvent(payload, signature);
    24→    } catch (error) {
    25→      console.error('Webhook signature verification failed:', error);
    26→      return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
    27→    }
    28→
    29→    const supabaseService = getSupabaseAdmin();
    30→
    31→    // Handle specific event types
    32→    switch (event.type) {
    33→      case 'payment_intent.succeeded': {
    34→        const paymentIntent = event.data.object as Stripe.PaymentIntent;
    35→        await handlePaymentSuccess(supabaseService, paymentIntent);
    36→        break;
    37→      }
    38→
    39→      case 'payment_intent.payment_failed': {
    40→        const paymentIntent = event.data.object as Stripe.PaymentIntent;
    41→        await handlePaymentFailure(supabaseService, paymentIntent);
    42→        break;
    43→      }
    44→
    45→      case 'payment_intent.canceled': {
    46→        const paymentIntent = event.data.object as Stripe.PaymentIntent;
    47→        await handlePaymentCancellation(supabaseService, paymentIntent);
    48→        break;
    49→      }
    50→
    51→      default:
    52→        console.log(`Unhandled event type: ${event.type}`);
    53→    }
    54→
    55→    return NextResponse.json({ received: true });
    56→  } catch (error) {
    57→    console.error('Webhook error:', error);
    58→    return NextResponse.json({ error: 'Webhook processing failed' }, { status: 500 });
    59→  }
    60→}
    61→
    62→async function handlePaymentSuccess(
    63→  supabase: ReturnType<typeof getSupabaseAdmin>,
    64→  paymentIntent: Stripe.PaymentIntent
    65→) {
    66→  const matchId = paymentIntent.metadata?.match_id;
    67→
    68→  if (!matchId) {
    69→    console.error('No match_id in payment intent metadata');
    70→    throw new Error('No match_id in payment intent metadata');
    71→  }
    72→
    73→  // Update payment status to charged
    74→  const { data: payment, error: paymentError } = await supabase
    75→    .from('payments')
    76→    .update({
    77→      status: 'charged',
    78→    })
    79→    .eq('stripe_payment_id', paymentIntent.id)
    80→    .select('id, match_id, company_id')
    81→    .single();
    82→
    83→  if (paymentError) {
    84→    console.error('Error updating payment status:', paymentError);
    85→    throw new Error(`Failed to update payment status: ${paymentError.message}`);
    86→  }
    87→
    88→  if (!payment) {
    89→    console.error('Payment not found for intent:', paymentIntent.id);
    90→    throw new Error(`Payment not found for intent: ${paymentIntent.id}`);
    91→  }
    92→
    93→  // Update match status to hired
    94→  const { error: matchError } = await supabase
    95→    .from('matches')
    96→    .update({
    97→      status: 'hired',
    98→      contractor_action: 'accepted',
    99→      hired_at: new Date().toISOString(),
   100→      fee_charged: true,
   101→    })
   102→    .eq('id', matchId);
   103→
   104→  if (matchError) {
   105→    console.error('Error updating match status:', matchError);
   106→    throw new Error(`Failed to update match status: ${matchError.message}`);
   107→  }
   108→
   109→  // Create notification for worker (non-critical, don't throw)
   110→  const { data: match } = await supabase
   111→    .from('matches')
   112→    .select('worker_id, jobs(title)')
   113→    .eq('id', matchId)
   114→    .single();
   115→
   116→  if (match) {
   117→    const jobs = match.jobs as { title?: string } | null;
   118→    const jobTitle = jobs?.title || 'a job';
   119→    const { error: notifError } = await supabase.from('notifications').insert({
   120→      profile_id: match.worker_id,
   121→      type: 'hire_confirmed',
   122→      title: 'You\'ve been hired!',
   123→      body: `Congratulations! You've been hired for ${jobTitle}.`,
   124→      data: { match_id: matchId, payment_id: payment.id },
   125→    });
   126→    if (notifError) {
   127→      console.error('Failed to create hire notification:', notifError);
   128→    }
   129→  }
   130→
   131→  console.log('Payment success processed for match:', matchId);
   132→}
   133→
   134→async function handlePaymentFailure(
   135→  supabase: ReturnType<typeof getSupabaseAdmin>,
   136→  paymentIntent: Stripe.PaymentIntent
   137→) {
   138→  try {
   139→    const { error } = await supabase
   140→      .from('payments')
   141→      .update({
   142→        status: 'pending', // Keep as pending to allow retry
   143→      })
   144→      .eq('stripe_payment_id', paymentIntent.id);
   145→
   146→    if (error) {
   147→      console.error('Error updating payment failure:', error);
   148→    }
   149→
   150→    console.log('Payment failed for intent:', paymentIntent.id);
   151→  } catch (error) {
   152→    console.error('Error handling payment failure:', error);
   153→  }
   154→}
   155→
   156→async function handlePaymentCancellation(
   157→  supabase: ReturnType<typeof getSupabaseAdmin>,
   158→  paymentIntent: Stripe.PaymentIntent
   159→) {
   160→  try {
   161→    const { error } = await supabase
   162→      .from('payments')
   163→      .delete()
   164→      .eq('stripe_payment_id', paymentIntent.id)
   165→      .eq('status', 'pending');
   166→
   167→    if (error) {
   168→      console.error('Error deleting cancelled payment:', error);
   169→    }
   170→
   171→    console.log('Payment cancelled for intent:', paymentIntent.id);
   172→  } catch (error) {
   173→    console.error('Error handling payment cancellation:', error);
   174→  }
   175→}
   176→

<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 create a comprehensive report. Let me quickly check some key files like legal pages:


1→import Link from "next/link";
     2→import Image from "next/image";
     3→import type { Metadata } from "next";
     4→import { HelpCircle, Building2, Wrench, Users } from "lucide-react";
     5→import { Button } from "@/components/ui/button";
     6→import {
     7→  Accordion,
     8→  AccordionContent,
     9→  AccordionItem,
    10→  AccordionTrigger,
    11→} from "@/components/ui/accordion";
    12→
    13→export const metadata: Metadata = {
    14→  title: "FAQ — RateRight",
    15→  description:
    16→    "Frequently asked questions about RateRight, the simplest way to hire construction workers in Australia.",
    17→};
    18→
    19→export const dynamic = "force-static";
    20→
    21→const workerFaqs = [
    22→  {
    23→    q: "How does RateRight work for workers?",
    24→    a: "RateRight connects qualified construction workers with contractors who need skilled labour. Simply create your profile, verify your qualifications, and get matched with jobs that fit your skills and schedule.",
    25→  },
    26→  {
    27→    q: "Is RateRight free for workers?",
    28→    a: "Yes! It is completely free for workers to join, create a profile, and get matched with jobs. We do not charge any subscription fees or take a percentage of your earnings.",
    29→  },
    30→  {
    31→    q: "How do I get hired through RateRight?",
    32→    a: "Complete your profile with accurate information about your skills, experience, and certifications. Upload your White Card and any trade-specific licences. Contractors search our database and contact workers directly.",
    33→  },
    34→  {
    35→    q: "What trades does RateRight cover?",
    36→    a: "We cover all major construction trades including carpentry, plumbing, electrical, bricklaying, concreting, tiling, plastering, painting, landscaping, and general labour.",
    37→  },
    38→  {
    39→    q: "Do I need a White Card to join?",
    40→    a: "Yes, all workers must have a valid White Card (General Construction Induction Card) to work on Australian construction sites.",
    41→  },
    42→  {
    43→    q: "How quickly can I find work?",
    44→    a: "Many workers receive their first job invitation within 24\u201348 hours of profile approval.",
    45→  },
    46→  {
    47→    q: "How and when do I get paid?",
    48→    a: "Payment terms are agreed directly with the contractor before starting work. Most contractors pay weekly or fortnightly. RateRight does not handle payments.",
    49→  },
    50→  {
    51→    q: "Can I choose when and where I work?",
    52→    a: "Absolutely! You set your availability and preferred work locations in your profile.",
    53→  },
    54→];
    55→
    56→const contractorFaqs = [
    57→  {
    58→    q: "How does RateRight work for contractors?",
    59→    a: "RateRight gives you instant access to pre-verified construction workers. Search our database by trade, location, availability, and ratings.",
    60→  },
    61→  {
    62→    q: "What is the $50 fee about?",
    63→    a: "We charge a flat $50 fee per successful job placement. No subscriptions, no hidden costs, no percentage cuts.",
    64→  },
    65→  {
    66→    q: "How fast can I find workers?",
    67→    a: "Most contractors find suitable workers within hours. Many jobs are filled the same day they are posted.",
    68→  },
    69→  {
    70→    q: "What if the worker does not show up?",
    71→    a: "We take reliability seriously. Workers with no-shows are removed from the platform. Contact us immediately and we will help you find a replacement quickly.",
    72→  },
    73→  {
    74→    q: "Are workers verified?",
    75→    a: "Yes, all workers go through our verification process. We check White Cards, trade licences, and conduct identity verification.",
    76→  },
    77→  {
    78→    q: "How many workers can I contact?",
    79→    a: "As many as you need! There is no limit to how many workers you can view or contact.",
    80→  },
    81→  {
    82→    q: "Do I have to use RateRight for payment?",
    83→    a: "No, you handle payment directly with the worker according to your normal payroll processes.",
    84→  },
    85→  {
    86→    q: "What areas does RateRight cover?",
    87→    a: "We currently operate in Sydney, Melbourne, Brisbane, Perth, and Adelaide metropolitan areas.",
    88→  },
    89→];
    90→
    91→const generalFaqs = [
    92→  {
    93→    q: "Is RateRight a labour hire company?",
    94→    a: "No, RateRight is a marketplace platform that connects contractors directly with workers. We are not a labour hire company.",
    95→  },
    96→  {
    97→    q: "How is payment handled?",
    98→    a: "Payment is arranged directly between the contractor and worker. RateRight does not process payments or take a percentage of wages.",
    99→  },
   100→  {
   101→    q: "What areas do you cover?",
   102→    a: "RateRight currently operates in major Australian cities: Sydney, Melbourne, Brisbane, Perth, and Adelaide.",
   103→  },
   104→  {
   105→    q: "Is there a rating system?",
   106→    a: "Yes, both workers and contractors can rate each other after completing a job.",
   107→  },
   108→  {
   109→    q: "How do I get started?",
   110→    a: "Simply visit our website and click Sign Up. Workers create a profile with their skills and qualifications. Contractors can start searching immediately.",
   111→  },
   112→  {
   113→    q: "What makes RateRight different?",
   114→    a: "Unlike traditional labour hire companies, we charge contractors a simple flat fee with no ongoing subscriptions. Workers keep 100% of their wages.",
   115→  },
   116→];
   117→
   118→function FAQSection({
   119→  title,
   120→  icon,
   121→  faqs,
   122→  idPrefix,
   123→}: {
   124→  title: string;
   125→  icon: React.ReactNode;
   126→  faqs: { q: string; a: string }[];
   127→  idPrefix: string;
   128→}) {
   129→  return (
   130→

   131→

   132→

   133→          {icon}
   134→

   135→
{title}

   136→

   137→      <Accordion type="single" collapsible className="w-full">
   138→        {faqs.map((faq, i) => (
   139→          <AccordionItem key={i} value={`${idPrefix}-${i}`}>
   140→            <AccordionTrigger>{faq.q}</AccordionTrigger>
   141→            <AccordionContent>
   142→
{faq.a}

   143→            </AccordionContent>
   144→          </AccordionItem>
   145→        ))}
   146→      </Accordion>
   147→

   148→  );
   149→}
   150→
   151→export default function FAQPage() {
   152→  return (
   153→

   154→      {/* Orange accent bar */}
   155→

   156→
   157→      {/* Header */}
   158→
   159→

   160→          <Link href="/" className="flex items-center gap-2">
   161→            <Image src="/logo_black.svg" alt="RateRight" width={140} height={40} className="h-8 w-auto" priority />
   162→          </Link>
   163→          <Link href="/auth/login">
   164→            <Button variant="ghost" size="sm">
   165→              Sign in
   166→            </Button>
   167→          </Link>
   168→

   169→
   170→
   171→      {/* Hero */}
   172→

   173→

   174→

   175→            <HelpCircle className="h-6 w-6 text-primary" />
   176→

   177→

   178→            Frequently Asked Questions
   179→

   180→

   181→            Everything you need to know about hiring and finding work on
   182→            RateRight.
   183→

   184→

   185→

   186→
   187→      {/* FAQ Sections */}
   188→

   189→

   190→          <FAQSection
   191→            title="For Workers"
   192→            icon={<Wrench className="h-4 w-4 text-primary-foreground" />}
   193→            faqs={workerFaqs}
   194→            idPrefix="worker"
   195→          />
   196→
   197→          <FAQSection
   198→            title="For Contractors"
   199→            icon={<Building2 className="h-4 w-4 text-primary-foreground" />}
   200→            faqs={contractorFaqs}
   201→            idPrefix="contractor"
   202→          />
   203→
   204→          <FAQSection
   205→            title="General Questions"
   206→            icon={<Users className="h-4 w-4 text-primary-foreground" />}
   207→            faqs={generalFaqs}
   208→            idPrefix="general"
   209→          />
   210→

   211→

   212→
   213→      {/* CTA */}
   214→

   215→

   216→

   217→            Still have questions?
   218→

   219→

   220→            Get in touch and we'll get back to you as soon as we can.
   221→

   222→

   223→            <Link href="/auth/signup">
   224→              <Button className="w-full sm:w-auto h-12 gap-2 text-base font-semibold">
   225→                Get started
   226→              </Button>
   227→            </Link>
   228→

   229→

   230→

   231→
   232→      {/* Footer */}
   233→
   234→

   235→

   236→            <Image src="/logo_black.svg" alt="RateRight" width={110} height={32} className="h-6 w-auto" />
   237→

   238→

   239→            The simplest way to hire construction workers in Australia.
   240→

   241→

   242→            © {new Date().getFullYear()} RateRight Pty Ltd. All rights
   243→            reserved.
   244→

   245→

   246→
   247→

   248→  );
   249→}
   250→

<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 Link from "next/link";
     2→import { ArrowLeft } from "lucide-react";
     3→
     4→export default function PrivacyPolicyPage() {
     5→  return (
     6→

     7→

     8→        <Link
     9→          href="/auth/signup"
    10→          className="mb-6 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
    11→        >
    12→          <ArrowLeft className="h-4 w-4" />
    13→          Back
    14→        </Link>
    15→
    16→
Privacy Policy

    17→

    18→          Last updated: February 2026
    19→

    20→
    21→

    22→

    23→
1. Information We Collect

    24→
We collect information you provide when creating an account and using the Platform:

    25→

    26→
Name, email address, and phone number

    27→
ABN and company details (contractors)

    28→
Trade, experience, certifications, and availability (workers)

    29→
Location (suburb/area) for job matching

    30→
Voice recordings (if you use voice input features)

    31→
Payment information (processed by Stripe)

    32→

    33→

    34→
    35→

    36→
2. How We Use Your Information

    37→

    38→
To create and manage your account

    39→
To match workers with relevant job opportunities

    40→
To process payments for hiring fees

    41→
To generate AI-powered profiles and suggestions

    42→
To send notifications about matches and hires

    43→
To improve our services

    44→

    45→

    46→
    47→

    48→
3. AI and Voice Data

    49→

    50→              We use AI services (OpenAI) to generate profile content and transcribe voice
    51→              recordings. Voice recordings are processed in real-time and are not permanently
    52→              stored. AI-generated content is stored as part of your profile.
    53→

    54→

    55→
    56→

    57→
4. Data Sharing

    58→
We share your information with:

    59→

    60→
Other users (your profile is visible to contractors/workers on the Platform)

    61→
Stripe (for payment processing)

    62→
OpenAI (for AI features — voice transcription and profile generation)

    63→
Supabase (our database and authentication provider)

    64→

    65→
We do not sell your personal information to third parties.

    66→

    67→
    68→

    69→
5. Data Security

    70→

    71→              We use industry-standard security measures including encryption, secure
    72→              authentication, and access controls to protect your data. All data is stored
    73→              in secure cloud infrastructure.
    74→

    75→

    76→
    77→

    78→
6. Your Rights

    79→
Under the Australian Privacy Act 1988, you have the right to:

    80→

    81→
Access your personal information

    82→
Request correction of inaccurate information

    83→
Request deletion of your account and data

    84→
Opt out of marketing communications

    85→

    86→

    87→
    88→

    89→
7. Cookies

    90→

    91→              We use essential cookies for authentication and security (CSRF protection).
    92→              We do not use tracking or advertising cookies.
    93→

    94→

    95→
    96→

    97→
8. Contact

    98→

    99→              For privacy enquiries, contact us at privacy@rateright.com.au.
   100→

   101→

   102→

   103→

   104→

   105→  );
   106→}
   107→

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