1→"use client";
     2→
     3→import { useState, useEffect } from "react";
     4→import { useRouter, useParams } from "next/navigation";
     5→import { createClient } from "@/lib/supabase/client";
     6→import { Button } from "@/components/ui/button";
     7→import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
     8→import { Star, Loader2, CheckCircle2 } from "lucide-react";
     9→import { cn } from "@/lib/utils";
    10→import { MAX_RATING_TEXT_LENGTH } from "@/lib/constants";
    11→
    12→export default function RatePage() {
    13→  const [score, setScore] = useState(0);
    14→  const [hoverScore, setHoverScore] = useState(0);
    15→  const [comment, setComment] = useState("");
    16→  const [loading, setLoading] = useState(true);
    17→  const [submitting, setSubmitting] = useState(false);
    18→  const [success, setSuccess] = useState(false);
    19→  const [error, setError] = useState<string | null>(null);
    20→  const [otherName, setOtherName] = useState<string>("");
    21→  const [alreadyRated, setAlreadyRated] = useState(false);
    22→
    23→  const router = useRouter();
    24→  const params = useParams();
    25→  const matchId = params.matchId as string;
    26→  const supabase = createClient();
    27→
    28→  useEffect(() => {
    29→    async function loadMatch() {
    30→      const { data: { user } } = await supabase.auth.getUser();
    31→      if (!user) { router.push("/auth/login"); return; }
    32→
    33→      // Get match details
    34→      const { data: match } = await supabase
    35→        .from("matches")
    36→        .select("id, worker_id, status, jobs(company_id, title)")
    37→        .eq("id", matchId)
    38→        .single();
    39→
    40→      if (!match || match.status !== "hired") {
    41→        setError("This match is not available for rating.");
    42→        setLoading(false);
    43→        return;
    44→      }
    45→
    46→      // Check if already rated
    47→      const { data: existingRating } = await supabase
    48→        .from("ratings")
    49→        .select("id")
    50→        .eq("match_id", matchId)
    51→        .eq("from_id", user.id)
    52→        .single();
    53→
    54→      if (existingRating) {
    55→        setAlreadyRated(true);
    56→        setLoading(false);
    57→        return;
    58→      }
    59→
    60→      // Get the other person's name
    61→      const isWorker = match.worker_id === user.id;
    62→      const jobs = match.jobs as unknown as { company_id: string; title: string } | null;
    63→
    64→      if (isWorker && jobs?.company_id) {
    65→        const { data: company } = await supabase
    66→          .from("companies")
    67→          .select("name, profile_id")
    68→          .eq("id", jobs.company_id)
    69→          .single();
    70→        setOtherName(company?.name || "the contractor");
    71→      } else if (!isWorker) {
    72→        const { data: profile } = await supabase
    73→          .from("profiles")
    74→          .select("name")
    75→          .eq("id", match.worker_id)
    76→          .single();
    77→        setOtherName(profile?.name || "the worker");
    78→      } else {
    79→        setOtherName("the other party");
    80→      }
    81→
    82→      setLoading(false);
    83→    }
    84→
    85→    loadMatch();
    86→  }, [matchId, supabase, router]);
    87→
    88→  async function handleSubmit(e: React.FormEvent) {
    89→    e.preventDefault();
    90→    if (score === 0) { setError("Please select a rating."); return; }
    91→
    92→    setSubmitting(true);
    93→    setError(null);
    94→
    95→    const { data: { user } } = await supabase.auth.getUser();
    96→    if (!user) return;
    97→
    98→    // Get the other person's ID
    99→    const { data: match } = await supabase
   100→      .from("matches")
   101→      .select("worker_id, jobs(company_id)")
   102→      .eq("id", matchId)
   103→      .single();
   104→
   105→    if (!match) { setError("Match not found."); setSubmitting(false); return; }
   106→
   107→    const isWorker = match.worker_id === user.id;
   108→    let toId: string;
   109→
   110→    if (isWorker) {
   111→      // Worker rating the contractor — need to find the contractor's profile_id
   112→      const jobs = match.jobs as unknown as { company_id: string } | null;
   113→      if (!jobs?.company_id) { setError("Could not find contractor."); setSubmitting(false); return; }
   114→      const { data: company } = await supabase.from("companies").select("profile_id").eq("id", jobs.company_id).single();
   115→      if (!company) { setError("Could not find contractor."); setSubmitting(false); return; }
   116→      toId = company.profile_id;
   117→    } else {
   118→      toId = match.worker_id;
   119→    }
   120→
   121→    const { error: insertError } = await supabase.from("ratings").insert({
   122→      match_id: matchId,
   123→      from_id: user.id,
   124→      to_id: toId,
   125→      score,
   126→      comment: comment.trim() || null,
   127→    });
   128→
   129→    if (insertError) {
   130→      setError(insertError.message);
   131→      setSubmitting(false);
   132→      return;
   133→    }
   134→
   135→    setSuccess(true);
   136→    setSubmitting(false);
   137→
   138→    setTimeout(() => {
   139→      router.push("/dashboard");
   140→      router.refresh();
   141→    }, 2000);
   142→  }
   143→
   144→  if (loading) {
   145→    return (
   146→

   147→        <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
   148→

   149→    );
   150→  }
   151→
   152→  if (alreadyRated) {
   153→    return (
   154→

   155→        <Card className="w-full max-w-md">
   156→          <CardHeader className="text-center">
   157→

   158→              <CheckCircle2 className="h-6 w-6 text-green-600" />
   159→

   160→            <CardTitle className="text-2xl font-bold">Already rated</CardTitle>
   161→            <CardDescription className="mt-2">
   162→              You've already submitted a rating for this job.
   163→            </CardDescription>
   164→          </CardHeader>
   165→          <CardContent>
   166→            <Button asChild className="w-full h-12 text-base" variant="outline">
   167→              Back to dashboard
   168→            </Button>
   169→          </CardContent>
   170→        </Card>
   171→

   172→    );
   173→  }
   174→
   175→  if (success) {
   176→    return (
   177→

   178→        <Card className="w-full max-w-md">
   179→          <CardHeader className="text-center">
   180→

   181→              <CheckCircle2 className="h-6 w-6 text-green-600" />
   182→

   183→            <CardTitle className="text-2xl font-bold">Thanks for rating!</CardTitle>
   184→            <CardDescription className="mt-2">
   185→              Your feedback helps build trust on RateRight. Redirecting...
   186→            </CardDescription>
   187→          </CardHeader>
   188→        </Card>
   189→

   190→    );
   191→  }
   192→
   193→  return (
   194→

   195→      <Card className="w-full max-w-md">
   196→        <CardHeader className="text-center">
   197→          <CardTitle className="text-2xl font-bold">Rate your experience</CardTitle>
   198→          <CardDescription className="mt-2">
   199→            How was working with {otherName}?
   200→          </CardDescription>
   201→        </CardHeader>
   202→        <CardContent>
   203→          <form onSubmit={handleSubmit} className="space-y-6">
   204→            {/* Star Rating */}
   205→

   206→              {[1, 2, 3, 4, 5].map((value) => (
   207→                <button
   208→                  key={value}
   209→                  type="button"
   210→                  onClick={() => setScore(value)}
   211→                  onMouseEnter={() => setHoverScore(value)}
   212→                  onMouseLeave={() => setHoverScore(0)}
   213→                  className="p-1 transition-transform active:scale-90"
   214→                  aria-label={`${value} star${value !== 1 ? "s" : ""}`}
   215→                >
   216→                  <Star
   217→                    className={cn(
   218→                      "h-10 w-10 transition-colors",
   219→                      (hoverScore || score) >= value
   220→                        ? "fill-yellow-400 text-yellow-400"
   221→                        : "text-muted-foreground/30"
   222→                    )}
   223→                  />
   224→                </button>
   225→              ))}
   226→

   227→

   228→              {score === 0 && "Tap a star to rate"}
   229→              {score === 1 && "Poor"}
   230→              {score === 2 && "Below average"}
   231→              {score === 3 && "Average"}
   232→              {score === 4 && "Good"}
   233→              {score === 5 && "Excellent"}
   234→

   235→
   236→            {/* Comment */}
   237→

   238→
   239→                Comment (optional)
   240→
   241→              <textarea
   242→                id="comment"
   243→                value={comment}
   244→                onChange={(e) => setComment(e.target.value)}
   245→                placeholder="How was the experience?"
   246→                rows={3}
   247→                maxLength={MAX_RATING_TEXT_LENGTH}
   248→                className="w-full rounded-lg border bg-background px-3 py-2 text-base resize-none focus:outline-none focus:ring-2 focus:ring-primary/50"
   249→              />
   250→

   251→
   252→            {error && (
   253→

   254→
{error}

   255→

   256→            )}
   257→
   258→            <Button
   259→              type="submit"
   260→              className="w-full h-12 text-base font-semibold"
   261→              disabled={submitting || score === 0}
   262→            >
   263→              {submitting ? "Submitting..." : "Submit Rating"}
   264→            </Button>
   265→          </form>
   266→
   267→

   268→
   269→              Skip for now
   270→
   271→

   272→        </CardContent>
   273→      </Card>
   274→

   275→  );
   276→}
   277→

<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 the payment and API components:


1→'use client';
     2→
     3→import { useState, useCallback } from 'react';
     4→import { logger } from '@/lib/logger';
     5→import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
     6→import { StripeProvider } from './StripeProvider';
     7→import { PaymentForm } from './PaymentForm';
     8→import { CheckCircle2, Loader2, AlertCircle } from 'lucide-react';
     9→import { createClient } from '@/lib/supabase/client';
    10→import { PAYMENT_AMOUNT } from '@/lib/constants';
    11→
    12→interface HirePaymentModalProps {
    13→  isOpen: boolean;
    14→  onClose: () => void;
    15→  onSuccess: () => void;
    16→  matchId: string;
    17→  companyId: string;
    18→  amount?: number;
    19→  workerName?: string;
    20→}
    21→
    22→type PaymentState =
    23→  | { type: 'initial' }
    24→  | { type: 'loading' }
    25→  | { type: 'ready'; clientSecret: string; publishableKey: string }
    26→  | { type: 'processing' }
    27→  | { type: 'success' }
    28→  | { type: 'error'; message: string };
    29→
    30→export function HirePaymentModal({
    31→  isOpen,
    32→  onClose,
    33→  onSuccess,
    34→  matchId,
    35→  companyId,
    36→  amount = PAYMENT_AMOUNT,
    37→  workerName,
    38→}: HirePaymentModalProps) {
    39→  const [paymentState, setPaymentState] = useState<PaymentState>({ type: 'initial' });
    40→  const supabase = createClient();
    41→
    42→  const handleOpen = useCallback(async () => {
    43→    if (paymentState.type !== 'initial') return;
    44→
    45→    setPaymentState({ type: 'loading' });
    46→
    47→    try {
    48→      const response = await fetch('/api/payments/create', {
    49→        method: 'POST',
    50→        headers: { 'Content-Type': 'application/json' },
    51→        body: JSON.stringify({
    52→          match_id: matchId,
    53→          company_id: companyId,
    54→          amount,
    55→        }),
    56→      });
    57→
    58→      const data = await response.json();
    59→
    60→      if (!response.ok) {
    61→        throw new Error(data.error || 'Failed to initialize payment');
    62→      }
    63→
    64→      if (!data.clientSecret || !data.publishableKey) {
    65→        throw new Error('Invalid payment configuration');
    66→      }
    67→
    68→      setPaymentState({
    69→        type: 'ready',
    70→        clientSecret: data.clientSecret,
    71→        publishableKey: data.publishableKey,
    72→      });
    73→    } catch (error) {
    74→      logger.error('Payment initialization error:', error);
    75→      setPaymentState({
    76→        type: 'error',
    77→        message: error instanceof Error ? error.message : 'Failed to initialize payment',
    78→      });
    79→    }
    80→  }, [matchId, companyId, amount, paymentState.type]);
    81→
    82→  // Initialize payment when modal opens
    83→  if (isOpen && paymentState.type === 'initial') {
    84→    handleOpen();
    85→  }
    86→
    87→  const handlePaymentSuccess = () => {
    88→    setPaymentState({ type: 'success' });
    89→    // Call the onSuccess callback after a brief delay to show success state
    90→    setTimeout(() => {
    91→      onSuccess();
    92→      // Reset state for next time
    93→      setPaymentState({ type: 'initial' });
    94→    }, 2000);
    95→  };
    96→
    97→  const handleCancel = () => {
    98→    onClose();
    99→    // Reset state after a delay
   100→    setTimeout(() => setPaymentState({ type: 'initial' }), 300);
   101→  };
   102→
   103→  const renderContent = () => {
   104→    switch (paymentState.type) {
   105→      case 'initial':
   106→      case 'loading':
   107→        return (
   108→

   109→            <Loader2 className="h-8 w-8 animate-spin text-primary" />
   110→
Initializing payment...

   111→

   112→        );
   113→
   114→      case 'ready':
   115→        return (
   116→          <StripeProvider
   117→            publishableKey={paymentState.publishableKey}
   118→            clientSecret={paymentState.clientSecret}
   119→          >
   120→            <PaymentForm
   121→              onSuccess={handlePaymentSuccess}
   122→              onCancel={handleCancel}
   123→              amount={amount}
   124→            />
   125→          </StripeProvider>
   126→        );
   127→
   128→      case 'processing':
   129→        return (
   130→

   131→            <Loader2 className="h-8 w-8 animate-spin text-primary" />
   132→
Processing payment...

   133→

   134→        );
   135→
   136→      case 'success':
   137→        return (
   138→

   139→

   140→              <CheckCircle2 className="h-12 w-12 text-green-600" />
   141→

   142→
Payment Successful!

   143→

   144→              The worker has been notified of your hire.
   145→

   146→

   147→        );
   148→
   149→      case 'error':
   150→        return (
   151→

   152→

   153→              <AlertCircle className="h-12 w-12 text-destructive" />
   154→

   155→
Payment Failed

   156→

   157→              {paymentState.message}
   158→

   159→            <button
   160→              onClick={() => setPaymentState({ type: 'initial' })}
   161→              className="mt-4 text-primary hover:underline"
   162→            >
   163→              Try again
   164→            </button>
   165→

   166→        );
   167→
   168→      default:
   169→        return null;
   170→    }
   171→  };
   172→
   173→  return (
   174→    <Dialog open={isOpen} onOpenChange={(open) => !open && handleCancel()}>
   175→      <DialogContent className="sm:max-w-md">
   176→        <DialogHeader>
   177→          <DialogTitle>
   178→            {paymentState.type === 'success'
   179→              ? 'Hire Confirmed'
   180→              : `Hire${workerName ? ` ${workerName}` : ''}`}
   181→          </DialogTitle>
   182→          {paymentState.type !== 'success' && paymentState.type !== 'error' && (
   183→            <DialogDescription>
   184→              Complete payment to confirm your hire.
   185→            </DialogDescription>
   186→          )}
   187→        </DialogHeader>
   188→        {renderContent()}
   189→      </DialogContent>
   190→    </Dialog>
   191→  );
   192→}
   193→

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


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

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


     1→import { NextRequest, NextResponse } from 'next/server';
     2→import { logger } from '@/lib/logger';
     3→import { retrievePaymentIntent } from '@/lib/stripe/server';
     4→import { createClient } from '@/lib/supabase/server';
     5→import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '@/lib/constants';
     6→
     7→export async function GET(request: NextRequest) {
     8→  try {
     9→    const searchParams = request.nextUrl.searchParams;
    10→    const paymentIntentId = searchParams.get('payment_intent');
    11→
    12→    if (!paymentIntentId) {
    13→      return NextResponse.json({ error: 'Missing payment_intent parameter' }, { status: 400 });
    14→    }
    15→
    16→    // Authenticate user
    17→    const supabase = await createClient();
    18→    const {
    19→      data: { user },
    20→    } = await supabase.auth.getUser();
    21→
    22→    if (!user) {
    23→      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    24→    }
    25→
    26→    // Retrieve payment intent from Stripe
    27→    const paymentIntent = await retrievePaymentIntent(paymentIntentId);
    28→
    29→    // Check if user owns the payment by verifying the match belongs to their company
    30→    const { data: payment } = await supabase
    31→      .from('payments')
    32→      .select('match_id, company_id, companies!inner(profile_id)')
    33→      .eq('stripe_payment_id', paymentIntentId)
    34→      .single();
    35→
    36→    if (!payment || (payment.companies as { profileId?: string }).profileId !== user.id) {
    37→      return NextResponse.json({ error: 'Payment not found or unauthorized' }, { status: 403 });
    38→    }
    39→
    40→    // Return payment intent status
    41→    return NextResponse.json({
    42→      status: paymentIntent.status,
    43→      amount: paymentIntent.amount,
    44→      currency: paymentIntent.currency,
    45→    });
    46→  } catch (error) {
    47→    logger.error('Payment verification error:', error);
    48→    if (error instanceof Error && error.message.includes('No such payment_intent')) {
    49→      return NextResponse.json({ error: 'Payment not found' }, { status: 404 });
    50→    }
    51→    return NextResponse.json({ error: 'Failed to verify payment' }, { status: HTTP_STATUS_INTERNAL_SERVER_ERROR });
    52→  }
    53→}
    54→

<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 the API routes and dashboard:


1→import { redirect } from "next/navigation";
     2→import Link from "next/link";
     3→import { createClient } from "@/lib/supabase/server";
     4→import { Button } from "@/components/ui/button";
     5→import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
     6→import { Badge } from "@/components/ui/badge";
     7→import { Building2, HardHat, Plus, Users, Star } from "lucide-react";
     8→import { SignOutButton } from "@/components/sign-out-button";
     9→import { BottomNavWrapper } from "@/components/bottom-nav-wrapper";
    10→
    11→export default async function DashboardPage() {
    12→  const supabase = await createClient();
    13→
    14→  const {
    15→    data: { user },
    16→  } = await supabase.auth.getUser();
    17→
    18→  if (!user) {
    19→    redirect("/auth/login");
    20→  }
    21→
    22→  const { data: profile } = await supabase
    23→    .from("profiles")
    24→    .select("id, type, name, onboarding_completed")
    25→    .eq("id", user.id)
    26→    .single();
    27→
    28→  const isContractor = profile?.type === "contractor";
    29→
    30→  // Fetch stats
    31→  let jobCount = 0;
    32→  let matchCount = 0;
    33→  let pendingRatings: { matchId: string; otherName: string }[] = [];
    34→
    35→  if (isContractor) {
    36→    const { data: company } = await supabase
    37→      .from("companies")
    38→      .select("id")
    39→      .eq("profile_id", user.id)
    40→      .single();
    41→
    42→    if (company) {
    43→      const { count } = await supabase
    44→        .from("jobs")
    45→        .select("*", { count: "exact", head: true })
    46→        .eq("company_id", company.id)
    47→        .eq("status", "active");
    48→      jobCount = count || 0;
    49→
    50→      const { count: mCount } = await supabase
    51→        .from("matches")
    52→        .select("*, jobs!inner(company_id)", { count: "exact", head: true })
    53→        .eq("jobs.company_id", company.id)
    54→        .in("status", ["applied", "suggested"]);
    55→      matchCount = mCount || 0;
    56→    }
    57→  }
    58→
    59→  // Fetch hired matches that haven't been rated yet
    60→  const { data: hiredMatches } = await supabase
    61→    .from("matches")
    62→    .select("id, worker_id, jobs(title)")
    63→    .eq("status", "hired");
    64→
    65→  if (hiredMatches && hiredMatches.length > 0) {
    66→    const matchIds = hiredMatches.map((m) => m.id);
    67→    const workerIds = hiredMatches.map((m) => m.worker_id).filter(Boolean);
    68→
    69→    // Batch fetch all ratings and profiles in parallel
    70→    const [{ data: existingRatings }, { data: profiles }] = await Promise.all([
    71→      supabase
    72→        .from("ratings")
    73→        .select("id, match_id")
    74→        .in("match_id", matchIds)
    75→        .eq("from_id", user.id),
    76→      workerIds.length > 0 && isContractor
    77→        ? supabase.from("profiles").select("id, name").in("id", workerIds)
    78→        : Promise.resolve({ data: [] }),
    79→    ]);
    80→
    81→    // Build lookup maps
    82→    const ratedMatchIds = new Set(existingRatings?.map((r) => r.match_id) || []);
    83→    const profileMap = new Map(profiles?.map((p) => [p.id, p.name]) || []);
    84→
    85→    // Build pending ratings list
    86→    for (const match of hiredMatches) {
    87→      if (!ratedMatchIds.has(match.id)) {
    88→        const otherId = isContractor ? match.worker_id : null;
    89→        const name = otherId && profileMap.has(otherId) ? profileMap.get(otherId)! : "your hire";
    90→        pendingRatings.push({ matchId: match.id, otherName: name });
    91→      }
    92→    }
    93→  }
    94→
    95→  return (
    96→

    97→
    98→

    99→          G'day{profile?.name ? `, ${profile.name}` : ""}!
   100→

   101→

   102→          {isContractor
   103→            ? "Ready to find your next worker?"
   104→            : "Let's get you on a job."}
   105→

   106→
   107→

   108→          {/* Pending ratings prompt */}
   109→          {pendingRatings.length > 0 && (
   110→            <Card className="border-yellow-500/30 bg-yellow-500/5">
   111→              <CardHeader className="pb-3">
   112→                <CardTitle className="flex items-center gap-2 text-lg">
   113→                  <Star className="h-6 w-6 text-yellow-500" />
   114→                  Rate your experience
   115→                </CardTitle>
   116→                <CardDescription className="text-base">
   117→                  Your feedback helps build trust on RateRight
   118→                </CardDescription>
   119→              </CardHeader>
   120→              <CardContent className="space-y-3">
   121→                {pendingRatings.slice(0, 3).map((pr) => (
   122→

   123→                    <Button asChild variant="outline" className="flex-1 h-12 text-base justify-start">
   124→                      <Link href={`/rate/${pr.matchId}`}>
   125→                        <Star className="h-4 w-4 mr-2 text-yellow-500" />
   126→                        Rate {pr.otherName}
   127→                      </Link>
   128→                    </Button>
   129→

   130→                ))}
   131→                {pendingRatings.length > 3 && (
   132→

   133→                    +{pendingRatings.length - 3} more to rate
   134→

   135→                )}
   136→              </CardContent>
   137→            </Card>
   138→          )}
   139→
   140→          {isContractor ? (
   141→            <>
   142→              {/* Quick stats */}
   143→

   144→                <Card>
   145→                  <CardContent className="flex flex-col items-center p-4">
   146→                    {jobCount}
   147→                    Active Jobs
   148→                  </CardContent>
   149→                </Card>
   150→                <Card>
   151→                  <CardContent className="flex flex-col items-center p-4">
   152→                    {matchCount}
   153→                    New Matches
   154→                  </CardContent>
   155→                </Card>
   156→

   157→
   158→              {/* Post a Job CTA */}
   159→              <Card className="border-primary/30 bg-primary/5">
   160→                <CardHeader className="pb-3">
   161→                  <CardTitle className="flex items-center gap-2 text-lg">
   162→                    <Plus className="h-6 w-6" />
   163→                    Post a Job
   164→                  </CardTitle>
   165→                  <CardDescription className="text-base">
   166→                    AI writes the post. You just tap.
   167→                  </CardDescription>
   168→                </CardHeader>
   169→                <CardContent>
   170→                  <Button asChild className="w-full h-16 text-lg font-semibold">
   171→                    <Link href="/contractor/post-job">Post a Job</Link>
   172→                  </Button>
   173→                </CardContent>
   174→              </Card>
   175→
   176→              {/* View Matches */}
   177→              {matchCount > 0 && (
   178→                <Card>
   179→                  <CardHeader className="pb-3">
   180→                    <CardTitle className="flex items-center gap-2 text-lg">
   181→                      <Users className="h-6 w-6" />
   182→                      {matchCount} Worker{matchCount > 1 ? "s" : ""} Matched
   183→                    </CardTitle>
   184→                    <CardDescription className="text-base">
   185→                      Workers have applied to your jobs
   186→                    </CardDescription>
   187→                  </CardHeader>
   188→                  <CardContent>
   189→                    <Button asChild variant="outline" className="w-full h-12 text-base font-semibold">
   190→                      <Link href="/contractor/matches">View Matches</Link>
   191→                    </Button>
   192→                  </CardContent>
   193→                </Card>
   194→              )}
   195→
   196→              {/* Company profile */}
   197→              <Card>
   198→                <CardHeader className="pb-3">
   199→                  <CardTitle className="flex items-center gap-2 text-lg">
   200→                    <Building2 className="h-6 w-6" />
   201→                    Company Profile
   202→                  </CardTitle>
   203→                  <CardDescription className="text-base">
   204→                    Manage your company details
   205→                  </CardDescription>
   206→                </CardHeader>
   207→                <CardContent>
   208→                  <Button asChild variant="outline" className="w-full h-12 text-base">
   209→                    <Link href="/contractor/signup">Edit Profile</Link>
   210→                  </Button>
   211→                </CardContent>
   212→              </Card>
   213→            </>
   214→          ) : (
   215→            <>
   216→              {/* Browse Jobs CTA */}
   217→              <Card className="border-primary/30 bg-primary/5">
   218→                <CardHeader className="pb-3">
   219→                  <CardTitle className="flex items-center gap-2 text-lg">
   220→                    <HardHat className="h-6 w-6" />
   221→                    Find Work
   222→                  </CardTitle>
   223→                  <CardDescription className="text-base">
   224→                    Browse available construction jobs near you
   225→                  </CardDescription>
   226→                </CardHeader>
   227→                <CardContent>
   228→                  <Button asChild className="w-full h-16 text-lg font-semibold">
   229→                    <Link href="/worker/jobs">Browse Jobs</Link>
   230→                  </Button>
   231→                </CardContent>
   232→              </Card>
   233→
   234→              {/* Profile */}
   235→              <Card>
   236→                <CardHeader className="pb-3">
   237→                  <CardTitle className="flex items-center gap-2 text-lg">
   238→                    <HardHat className="h-6 w-6" />
   239→                    Your Profile
   240→                  </CardTitle>
   241→                  <CardDescription className="text-base">
   242→                    {profile?.onboarding_completed
   243→                      ? "View and manage your profile"
   244→                      : "A strong profile gets you more matches"}
   245→                  </CardDescription>
   246→                </CardHeader>
   247→                <CardContent>
   248→                  <Button asChild variant="outline" className="w-full h-12 text-base">
   249→                    <Link href={profile?.onboarding_completed ? "/worker/profile" : "/worker/profile/build"}>
   250→                      {profile?.onboarding_completed ? "View Profile" : "Build Profile"}
   251→                    </Link>
   252→                  </Button>
   253→                </CardContent>
   254→              </Card>
   255→            </>
   256→          )}
   257→

   258→
   259→
   260→      <BottomNavWrapper userType={profile?.type || "worker"} />
   261→

   262→  );
   263→}
   264→

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


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

    91→

    92→

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

   103→

   104→

   105→
Loading conversations...

   106→

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

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

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

   116→

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

   126→

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

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

   134→

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

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

   145→        ) : (
   146→

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

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

   166→  );
   167→}
   168→

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


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

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


Let me read a few more important files for the payment form and other key components:


1→'use client';
     2→
     3→import { useState } from 'react';
     4→import {
     5→  PaymentElement,
     6→  useStripe,
     7→  useElements,
     8→} from '@stripe/react-stripe-js';
     9→import { Button } from '@/components/ui/button';
    10→import { Loader2, CreditCard } from 'lucide-react';
    11→import { logger } from '@/lib/logger';
    12→
    13→interface PaymentFormProps {
    14→  onSuccess: () => void;
    15→  onCancel: () => void;
    16→  amount: number;
    17→}
    18→
    19→export function PaymentForm({ onSuccess, onCancel, amount }: PaymentFormProps) {
    20→  const stripe = useStripe();
    21→  const elements = useElements();
    22→  const [isLoading, setIsLoading] = useState(false);
    23→  const [errorMessage, setErrorMessage] = useState<string | null>(null);
    24→
    25→  const handleSubmit = async (e: React.FormEvent) => {
    26→    e.preventDefault();
    27→
    28→    if (!stripe || !elements) {
    29→      return;
    30→    }
    31→
    32→    setIsLoading(true);
    33→    setErrorMessage(null);
    34→
    35→    try {
    36→      const { error, paymentIntent } = await stripe.confirmPayment({
    37→        elements,
    38→        confirmParams: {
    39→          return_url: `${window.location.origin}/contractor/payment-success`,
    40→        },
    41→        redirect: 'if_required',
    42→      });
    43→
    44→      if (error) {
    45→        setErrorMessage(error.message || 'Payment failed. Please try again.');
    46→      } else if (paymentIntent && paymentIntent.status === 'succeeded') {
    47→        onSuccess();
    48→      } else if (paymentIntent && paymentIntent.status === 'requires_action') {
    49→        // 3D Secure or other authentication required - Stripe handles this automatically
    50→        // The redirect above will handle it
    51→      } else {
    52→        setErrorMessage('Payment processing. Please wait...');
    53→      }
    54→    } catch (error) {
    55→      logger.error('Payment error:', error);
    56→      setErrorMessage('An unexpected error occurred. Please try again.');
    57→    } finally {
    58→      setIsLoading(false);
    59→    }
    60→  };
    61→
    62→  return (
    63→    <form onSubmit={handleSubmit} className="space-y-4">
    64→

    65→

    66→

    67→
Hire Fee

    68→
${amount.toFixed(2)} AUD

    69→

    70→          <CreditCard className="h-8 w-8 text-muted-foreground" />
    71→

    72→

    73→          One-time fee to hire this worker. No additional charges.
    74→

    75→

    76→
    77→

    78→        <PaymentElement
    79→          options={{
    80→            layout: {
    81→              type: 'tabs',
    82→              defaultCollapsed: false,
    83→            },
    84→          }}
    85→        />
    86→

    87→
    88→      {errorMessage && (
    89→

    90→          {errorMessage}
    91→

    92→      )}
    93→
    94→

    95→        <Button
    96→          type="button"
    97→          variant="outline"
    98→          onClick={onCancel}
    99→          disabled={isLoading}
   100→          className="flex-1"
   101→        >
   102→          Cancel
   103→        </Button>
   104→        <Button
   105→          type="submit"
   106→          disabled={!stripe || isLoading}
   107→          className="flex-1"
   108→        >
   109→          {isLoading ? (
   110→            <>
   111→              <Loader2 className="mr-2 h-4 w-4 animate-spin" />
   112→              Processing...
   113→            </>
   114→          ) : (
   115→            `Pay $${amount.toFixed(2)}`
   116→          )}
   117→        </Button>
   118→

   119→
   120→

   121→        Secured by Stripe. We never store your card details.
   122→

   123→    </form>
   124→  );
   125→}
   126→

<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 { 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→import { z } from 'zod';
     7→import {
     8→  MAX_LENGTH_EMAIL,
     9→  TIMEOUT_ABN_LOOKUP,
    10→  POSTCODE_SYDNEY,
    11→  POSTCODE_MELBOURNE
    12→} from '@/lib/constants';
    13→import { getCachedABN, cacheABNData } from '@/lib/abn-cache';
    14→import { logger } from '@/lib/logger';
    15→
    16→/**
    17→ * ABN Lookup API Route — GET /api/abn-lookup?abn=XXXXXXXXXXX
    18→ *
    19→ * Validates the ABN locally (checksum), then calls the ABR JSON endpoint
    20→ * to fetch entity details.
    21→ *
    22→ * ABR JSON docs: https://abr.business.gov.au/json/
    23→ * Registration:  https://abr.business.gov.au/Tools/WebServices
    24→ *
    25→ * Required env var:
    26→ *   ABR_GUID — the authentication GUID issued after registration
    27→ *
    28→ * Optional env var:
    29→ *   ABR_DEV_MODE — set to 'true' to enable development mode with mock data
    30→ */
    31→
    32→// ── ABR configuration ──────────────────────────────────────────────
    33→const ABR_JSON_URL = 'https://abr.business.gov.au/json/AbnDetails.aspx';
    34→const ABR_GUID = process.env.ABR_GUID ?? '';
    35→const ABR_DEV_MODE = process.env.ABR_DEV_MODE === 'true';
    36→
    37→// ── Types ──────────────────────────────────────────────────────────
    38→
    39→/** Shape returned by the ABR JSON endpoint (JSONP-unwrapped). */
    40→interface ABRJsonResponse {
    41→  Abn: string;
    42→  AbnStatus: string;
    43→  AbnStatusEffectiveFrom: string;
    44→  Acn: string;
    45→  AddressDate: string | null;
    46→  AddressPostcode: string;
    47→  AddressState: string;
    48→  BusinessName: Array<string | { Value: string; EffectiveFrom: string }>;
    49→  EntityName: string;
    50→  EntityTypeCode: string;
    51→  EntityTypeName: string;
    52→  Gst: string | null;
    53→  Message: string;
    54→}
    55→
    56→/** Director verification result */
    57→interface DirectorVerification {
    58→  status: 'pending' | 'verified' | 'mismatch' | 'not_available' | 'auto_verified' | 'manual_review';
    59→  message: string;
    60→  requiresManualReview?: boolean;
    61→  verificationMethod?: string;
    62→  riskScore?: number;
    63→  riskFactors?: string[];
    64→}
    65→
    66→/** Company data from ABR */
    67→interface CompanyData {
    68→  name: string;
    69→  abn: string;
    70→  status: string;
    71→  statusDate: string;
    72→  state: string;
    73→  postcode: string;
    74→  type: string;
    75→  typeCode: string;
    76→  gstRegistered: boolean;
    77→  acn?: string;
    78→  directorVerification?: DirectorVerification;
    79→}
    80→
    81→/** Our normalised response. */
    82→interface ABNLookupResult {
    83→  success: boolean;
    84→  data?: CompanyData;
    85→  error?: string;
    86→  devMode?: boolean;
    87→}
    88→
    89→// ── Development Mode Mock Data ─────────────────────────────────────
    90→
    91→/**
    92→ * Mock company data for development when ABR_GUID is not configured.
    93→ * Uses obviously fake ABNs to prevent confusion with real data.
    94→ */
    95→const MOCK_COMPANIES: Record<string, CompanyData> = {
    96→  // Test ABN for development - passes checksum but is obviously fake
    97→  '51824753556': {
    98→    name: 'TEST COMPANY PTY LTD (Dev Mode)',
    99→    abn: '51 824 753 556',
   100→    status: 'Active',
   101→    statusDate: '2020-01-01',
   102→    state: 'NSW',
   103→    postcode: POSTCODE_SYDNEY,
   104→    type: 'Australian Private Company',
   105→    typeCode: 'PRV',
   106→    gstRegistered: true,
   107→    directorVerification: {
   108→      status: 'auto_verified',
   109→      message: 'Development mode - auto-verified',
   110→      verificationMethod: 'dev_mode',
   111→      riskScore: 0,
   112→    },
   113→  },
   114→  // Another test ABN
   115→  '34241177887': {
   116→    name: 'EXAMPLE BUILDERS PTY LTD (Dev Mode)',
   117→    abn: '34 241 177 887',
   118→    status: 'Active',
   119→    statusDate: '2019-06-15',
   120→    state: 'VIC',
   121→    postcode: POSTCODE_MELBOURNE,
   122→    type: 'Australian Private Company',
   123→    typeCode: 'PRV',
   124→    gstRegistered: true,
   125→    directorVerification: {
   126→      status: 'auto_verified',
   127→      message: 'Development mode - auto-verified',
   128→      verificationMethod: 'dev_mode',
   129→      riskScore: 0,
   130→    },
   131→  },
   132→};
   133→
   134→/**
   135→ * Generate mock data for any valid ABN in development mode.
   136→ */
   137→function generateMockData(abn: string): CompanyData {
   138→  const formatted = formatABN(abn);
   139→  return {
   140→    name: `MOCK COMPANY ${abn.slice(-4)} PTY LTD (Dev Mode)`,
   141→    abn: formatted,
   142→    status: 'Active',
   143→    statusDate: new Date().toISOString().split('T')[0],
   144→    state: 'NSW',
   145→    postcode: POSTCODE_SYDNEY,
   146→    type: 'Australian Private Company',
   147→    typeCode: 'PRV',
   148→    gstRegistered: Math.random() > 0.3,
   149→    directorVerification: {
   150→      status: 'auto_verified',
   151→      message: 'Development mode - auto-verified',
   152→      verificationMethod: 'dev_mode',
   153→      riskScore: 0,
   154→    },
   155→  };
   156→}
   157→
   158→// ── Helpers ────────────────────────────────────────────────────────
   159→
   160→function buildABRUrl(abn: string): string {
   161→  const params = new URLSearchParams({
   162→    abn,
   163→    guid: ABR_GUID,
   164→    callback: 'cbr',
   165→  });
   166→  return `${ABR_JSON_URL}?${params.toString()}`;
   167→}
   168→
   169→function stripJSONP(jsonp: string): ABRJsonResponse {
   170→  const match = jsonp.match(/^cbr\((.*)\)$/);
   171→  if (!match) throw new Error('Unexpected ABR response format');
   172→  return JSON.parse(match[1]);
   173→}
   174→
   175→function normaliseABRResponse(abr: ABRJsonResponse): CompanyData {
   176→  const name = abr.EntityName ||
   177→    (Array.isArray(abr.BusinessName) && abr.BusinessName[0]) ||
   178→    'Unknown';
   179→
   180→  return {
   181→    name: typeof name === 'string' ? name : (name as any).Value || 'Unknown',
   182→    abn: formatABN(abr.Abn),
   183→    status: abr.AbnStatus,
   184→    statusDate: abr.AbnStatusEffectiveFrom,
   185→    state: abr.AddressState,
   186→    postcode: abr.AddressPostcode,
   187→    type: abr.EntityTypeName,
   188→    typeCode: abr.EntityTypeCode,
   189→    gstRegistered: abr.Gst !== null,
   190→    acn: abr.Acn || undefined,
   191→    // Director verification will be done by verifyDirector() function
   192→  };
   193→}
   194→
   195→/**
   196→ * Multi-Layer Director Verification
   197→ *
   198→ * Implements a practical 3-tier verification system:
   199→ * 1. Automated checks (email domain, GST status, entity type)
   200→ * 2. Risk scoring to flag suspicious registrations
   201→ * 3. Manual review fallback for high-risk cases
   202→ *
   203→ * Future: ASIC Connect API integration for real director lookup
   204→ * @see https://connectonline.asic.gov.au/
   205→ */
   206→async function verifyDirector(
   207→  abn: string,
   208→  companyData: CompanyData,
   209→  userEmail?: string
   210→): Promise<DirectorVerification> {
   211→  const riskFactors: string[] = [];
   212→  let riskScore = 0;
   213→
   214→  // --- Layer 1: Automated Verification Checks ---
   215→
   216→  // Check 1: GST Registration (legitimate businesses usually have GST)
   217→  if (!companyData.gstRegistered) {
   218→    riskScore += 15;
   219→    riskFactors.push('Not GST registered');
   220→  }
   221→
   222→  // Check 2: Entity Type (only allow private companies and sole traders)
   223→  const allowedEntityTypes = ['PRV', 'IND']; // Private Company, Individual/Sole Trader
   224→  if (!allowedEntityTypes.includes(companyData.typeCode)) {
   225→    riskScore += 30;
   226→    riskFactors.push(`Unusual entity type: ${companyData.type}`);
   227→  }
   228→
   229→  // Check 3: ABN Status (must be active)
   230→  if (companyData.status.toLowerCase() !== 'active') {
   231→    riskScore += 50;
   232→    riskFactors.push(`ABN status: ${companyData.status}`);
   233→  }
   234→
   235→  // Check 4: Email Domain Validation (if provided)
   236→  if (userEmail) {
   237→    const emailDomain = userEmail.split('@')[1]?.toLowerCase();
   238→    const companyName = companyData.name.toLowerCase();
   239→
   240→    // Check if email is free provider (gmail, outlook, etc.)
   241→    const freeEmailProviders = [
   242→      'gmail.com', 'outlook.com', 'hotmail.com', 'yahoo.com',
   243→      'icloud.com', 'live.com', 'msn.com', 'protonmail.com'
   244→    ];
   245→
   246→    if (freeEmailProviders.includes(emailDomain)) {
   247→      riskScore += 20;
   248→      riskFactors.push('Using free email provider (no company domain)');
   249→    } else {
   250→      // Check if email domain roughly matches company name
   251→      const domainParts = emailDomain.replace(/\.com\.au|\.com|\.net\.au|\.net|\.org\.au|\.org/g, '').split('.');
   252→      const companyParts = companyName.replace(/pty ltd|pty|ltd|limited|proprietary/gi, '').trim().split(/\s+/);
   253→
   254→      // Simple fuzzy match: check if any significant word from company name is in domain
   255→      const hasMatch = companyParts.some(part =>
   256→        part.length > 3 && domainParts.some(dp =>
   257→          dp.includes(part.toLowerCase()) || part.toLowerCase().includes(dp)
   258→        )
   259→      );
   260→
   261→      if (!hasMatch) {
   262→        riskScore += 25;
   263→        riskFactors.push('Email domain does not match company name');
   264→      }
   265→    }
   266→  } else {
   267→    // No email provided for verification
   268→    riskScore += 10;
   269→    riskFactors.push('No email provided for verification');
   270→  }
   271→
   272→  // Check 5: Company Age (ABN status date - newly registered companies are riskier)
   273→  const statusDate = new Date(companyData.statusDate);
   274→  const monthsOld = (Date.now() - statusDate.getTime()) / (1000 * 60 * 60 * 24 * 30);
   275→
   276→  if (monthsOld < 3) {
   277→    riskScore += 20;
   278→    riskFactors.push('Company registered less than 3 months ago');
   279→  } else if (monthsOld < 12) {
   280→    riskScore += 10;
   281→    riskFactors.push('Company registered less than 1 year ago');
   282→  }
   283→
   284→  // Check 6: Missing ACN (private companies should have ACN)
   285→  if (companyData.typeCode === 'PRV' && !companyData.acn) {
   286→    riskScore += 15;
   287→    riskFactors.push('Private company missing ACN');
   288→  }
   289→
   290→  // --- Layer 2: Risk Score Evaluation ---
   291→
   292→  // Low Risk (0-25): Auto-verify
   293→  if (riskScore <= 25 && companyData.status.toLowerCase() === 'active') {
   294→    return {
   295→      status: 'auto_verified',
   296→      message: 'Company automatically verified based on ABR data and basic checks',
   297→      verificationMethod: 'automated',
   298→      riskScore,
   299→      riskFactors: riskFactors.length > 0 ? riskFactors : undefined,
   300→      requiresManualReview: false,
   301→    };
   302→  }
   303→
   304→  // Medium Risk (26-50): Flag for manual review but allow signup
   305→  if (riskScore <= 50) {
   306→    return {
   307→      status: 'manual_review',
   308→      message: 'Company registration flagged for manual review. You can proceed with signup.',
   309→      verificationMethod: 'manual_review_required',
   310→      riskScore,
   311→      riskFactors,
   312→      requiresManualReview: true,
   313→    };
   314→  }
   315→
   316→  // High Risk (51+): Require manual approval before activation
   317→  return {
   318→    status: 'pending',
   319→    message: 'Company registration requires manual verification before activation. Our team will review within 24 hours.',
   320→    verificationMethod: 'manual_approval_required',
   321→    riskScore,
   322→    riskFactors,
   323→    requiresManualReview: true,
   324→  };
   325→}
   326→
   327→// ── Route Handler ──────────────────────────────────────────────────
   328→
   329→// Rate limit: 10 requests per 15 minutes per IP (external API call)
   330→const ABN_LOOKUP_RATE_LIMIT = 10;
   331→const ABN_LOOKUP_WINDOW_MS = 15 * 60 * 1000;
   332→
   333→// Email validation schema
   334→const emailSchema = z.string().email().max(254);
   335→
   336→export async function GET(request: NextRequest) {
   337→  try {
   338→    // --- 0. Rate limiting ---------------------------------------------------
   339→    const clientIP = getClientIP(request);
   340→    const rateLimit = checkRateLimit(`abn-lookup:${clientIP}`, ABN_LOOKUP_RATE_LIMIT, ABN_LOOKUP_WINDOW_MS);
   341→
   342→    if (!rateLimit.allowed) {
   343→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
   344→      return createRateLimitResponse(clientIP, retryAfter);
   345→    }
   346→
   347→    const searchParams = request.nextUrl.searchParams;
   348→    const rawABN = searchParams.get('abn');
   349→    const rawEmail = searchParams.get('email');
   350→
   351→    // --- 1. Validate input --------------------------------------------------
   352→    if (!rawABN) {
   353→      return NextResponse.json({
   354→        success: false,
   355→        error: 'Missing query parameter: abn'
   356→      }, { status: 400 });
   357→    }
   358→
   359→    const abn = cleanABN(rawABN);
   360→    if (!isValidABN(abn)) {
   361→      return NextResponse.json({
   362→        success: false,
   363→        error: 'Invalid ABN format. Please check the number and try again.'
   364→      }, { status: 400 });
   365→    }
   366→
   367→    // Validate email format if provided
   368→    let userEmail: string | null = null;
   369→    if (rawEmail) {
   370→      const sanitizedEmail = sanitizeInput(rawEmail, { maxLength: MAX_LENGTH_EMAIL });
   371→      const emailValidation = emailSchema.safeParse(sanitizedEmail);
   372→
   373→      if (!emailValidation.success) {
   374→        return NextResponse.json({
   375→          success: false,
   376→          error: 'Invalid email format. Please provide a valid email address.'
   377→        }, { status: 400 });
   378→      }
   379→
   380→      userEmail = emailValidation.data;
   381→    }
   382→
   383→    // --- 2. Check cache first ----------------------------------------------
   384→    const cachedData = await getCachedABN(abn);
   385→    if (cachedData) {
   386→      logger.info(`[ABN Lookup] Cache hit for ABN: ${abn}`);
   387→
   388→      // Convert cached data to the expected format
   389→      const result: CompanyData & { directorVerification?: DirectorVerification } = {
   390→        name: cachedData.companyName,
   391→        abn: cachedData.abn,
   392→        status: cachedData.status,
   393→        statusDate: cachedData.statusDate || '',
   394→        state: cachedData.state || '',
   395→        postcode: cachedData.postcode || '',
   396→        type: cachedData.entityType || '',
   397→        typeCode: cachedData.entityTypeCode || '',
   398→        gstRegistered: cachedData.gstRegistered || false,
   399→        acn: cachedData.acn,
   400→      };
   401→
   402→      // Director verification (same as before)
   403→      result.directorVerification = await verifyDirector(
   404→        abn,
   405→        result,
   406→        userEmail || undefined
   407→      );
   408→
   409→      return NextResponse.json({
   410→        success: true,
   411→        data: result,
   412→        cached: true,
   413→        cachedAt: cachedData.cachedAt,
   414→      });
   415→    }
   416→
   417→    logger.info(`[ABN Lookup] Cache miss for ABN: ${abn}, calling ABR API`);
   418→
   419→    // --- 3. Check if ABR_GUID is configured --------------------------------
   420→    const isGuidConfigured = ABR_GUID &&
   421→      ABR_GUID !== 'REGISTER-TOMORROW-AT-ABR-BUSINESS-GOV-AU' &&
   422→      ABR_GUID !== 'temporary-abr-guid-for-build';
   423→
   424→    if (!isGuidConfigured) {
   425→      // Development mode: return mock data
   426→      if (ABR_DEV_MODE) {
   427→        logger.info(`[ABN Lookup] Dev mode - returning mock data for ${abn}`);
   428→        const mockData = MOCK_COMPANIES[abn] || generateMockData(abn);
   429→        return NextResponse.json({
   430→          success: true,
   431→          data: mockData,
   432→          devMode: true,
   433→        });
   434→      }
   435→
   436→      // Production without GUID: return error with validation info
   437→      return NextResponse.json({
   438→        success: false,
   439→        error: 'ABR service not configured',
   440→        devMode: false,
   441→      }, { status: 503 });
   442→    }
   443→
   444→    // --- 4. Call ABR web service -------------------------------------------
   445→    const url = buildABRUrl(abn);
   446→    const abrResponse = await fetch(url, {
   447→      method: 'GET',
   448→      headers: { 'User-Agent': 'RateRight/1.0' },
   449→      signal: AbortSignal.timeout(TIMEOUT_ABN_LOOKUP),
   450→    });
   451→
   452→    if (!abrResponse.ok) {
   453→      const text = await abrResponse.text();
   454→      logger.error(`[ABN Lookup] ABR error ${abrResponse.status}:`, text);
   455→      return NextResponse.json({
   456→        success: false,
   457→        error: `ABR service error: ${abrResponse.status}`,
   458→      }, { status: 502 });
   459→    }
   460→
   461→    const jsonpText = await abrResponse.text();
   462→    const abrData = stripJSONP(jsonpText);
   463→
   464→    // --- 5. Handle ABR business logic ---------------------------------------
   465→    if (abrData.Message) {
   466→      // Cache the "not found" result with shorter expiry
   467→      await cacheABNData({
   468→        abn: abn,
   469→        companyName: 'Not Found',
   470→        status: 'Not Found',
   471→        abrResponse: abrData,
   472→      });
   473→
   474→      return NextResponse.json({
   475→        success: false,
   476→        error: abrData.Message
   477→      }, { status: 404 });
   478→    }
   479→
   480→    const result: CompanyData & { directorVerification?: DirectorVerification } = normaliseABRResponse(abrData);
   481→
   482→    // Cache the successful result
   483→    await cacheABNData({
   484→      abn: result.abn,
   485→      companyName: result.name,
   486→      status: result.status,
   487→      statusDate: result.statusDate,
   488→      state: result.state,
   489→      postcode: result.postcode,
   490→      entityType: result.type,
   491→      entityTypeCode: result.typeCode,
   492→      gstRegistered: result.gstRegistered,
   493→      acn: result.acn,
   494→      abrResponse: abrData,
   495→    });
   496→
   497→    // --- 5. Director verification (multi-layer automated + risk scoring) ----
   498→    result.directorVerification = await verifyDirector(
   499→      abn,
   500→      result,
   501→      userEmail || undefined
   502→    );
   503→
   504→    return NextResponse.json({
   505→      success: true,
   506→      data: result,
   507→    });
   508→
   509→  } catch (err: unknown) {
   510→    logger.error('[ABN Lookup] Uncaught error:', err);
   511→    return NextResponse.json({
   512→      success: false,
   513→      error: 'Internal server error',
   514→    }, { status: 500 });
   515→  }
   516→}
   517→

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


     1→import { NextRequest, NextResponse } from "next/server";
     2→import { logger } from "@/lib/logger";
     3→import OpenAI from "openai";
     4→import { createClient } from "@/lib/supabase/server";
     5→import { checkRateLimit, getClientIP, createRateLimitResponse } from "@/lib/rate-limit";
     6→import { sanitizeInput } from "@/lib/sanitize";
     7→import { withCSRFProtection } from "@/lib/csrf";
     8→import {
     9→  AI_RATE_LIMIT,
    10→  RATE_LIMIT_WINDOW_MS as AI_WINDOW_MS,
    11→  MAX_LENGTH_TRADE
    12→} from "@/lib/constants";
    13→
    14→if (!process.env.OPENAI_API_KEY) {
    15→  throw new Error("OPENAI_API_KEY environment variable is not set");
    16→}
    17→
    18→const openai = new OpenAI({
    19→  apiKey: process.env.OPENAI_API_KEY,
    20→});
    21→
    22→// Rate limit: 10 requests per 15 minutes per user
    23→
    24→const handler = async (request: NextRequest) => {
    25→  try {
    26→    const clientIP = getClientIP(request);
    27→    const rateLimit = checkRateLimit(`ai-job:${clientIP}`, AI_RATE_LIMIT, AI_WINDOW_MS);
    28→
    29→    if (!rateLimit.allowed) {
    30→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    31→      return createRateLimitResponse(clientIP, retryAfter);
    32→    }
    33→
    34→    const supabase = await createClient();
    35→    const { data: { user }, error: authError } = await supabase.auth.getUser();
    36→    if (authError || !user) {
    37→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    38→    }
    39→
    40→    const { trade } = await request.json();
    41→
    42→    if (!trade) {
    43→      return NextResponse.json({ error: "Missing trade" }, { status: 400 });
    44→    }
    45→
    46→    const sanitizedTrade = sanitizeInput(trade, { maxLength: MAX_LENGTH_TRADE });
    47→
    48→    const completion = await openai.chat.completions.create({
    49→      model: "gpt-4o-mini",
    50→      messages: [
    51→        {
    52→          role: "system",
    53→          content: "You are an Australian construction industry expert. Always respond with valid JSON only.",
    54→        },
    55→        {
    56→          role: "user",
    57→          content: `Generate a job posting for a ${sanitizedTrade} in the Australian construction industry.
    58→
    59→Return JSON with:
    60→1. title: A concise job title (e.g. "Experienced Steel Fixer Needed")
    61→2. description: A 3-4 sentence job description mentioning key duties, site expectations, and any specific requirements. Use Australian English. Mention PPE, early starts if relevant.
    62→3. certifications: Array of required certifications (e.g. "White Card", "Working at Heights", "Trade Certificate", "Scaffolding Licence"). Always include "White Card".
    63→4. rateMin: Minimum hourly rate in AUD (realistic 2026 Australian market rate)
    64→5. rateMax: Maximum hourly rate in AUD
    65→6. rateType: "hourly" or "daily"
    66→
    67→Return ONLY valid JSON:
    68→{
    69→  "title": "string",
    70→  "description": "string",
    71→  "certifications": ["string"],
    72→  "rateMin": number,
    73→  "rateMax": number,
    74→  "rateType": "hourly"
    75→}`,
    76→        },
    77→      ],
    78→      temperature: 0.7,
    79→      response_format: { type: "json_object" },
    80→    });
    81→
    82→    const content = completion.choices[0]?.message?.content;
    83→    if (!content) {
    84→      throw new Error("No response from AI");
    85→    }
    86→
    87→    const generated = JSON.parse(content);
    88→
    89→    // Validate and sanitize rate values
    90→    const MIN_HOURLY_RATE = 25; // Australian minimum for construction
    91→    const MAX_HOURLY_RATE = 200; // Upper bound for specialist trades
    92→    const DEFAULT_MIN_RATE = 35;
    93→    const DEFAULT_MAX_RATE = 55;
    94→
    95→    let rateMin = typeof generated.rateMin === 'number' ? generated.rateMin : DEFAULT_MIN_RATE;
    96→    let rateMax = typeof generated.rateMax === 'number' ? generated.rateMax : DEFAULT_MAX_RATE;
    97→
    98→    // Clamp rates to reasonable bounds
    99→    rateMin = Math.max(MIN_HOURLY_RATE, Math.min(rateMin, MAX_HOURLY_RATE));
   100→    rateMax = Math.max(MIN_HOURLY_RATE, Math.min(rateMax, MAX_HOURLY_RATE));
   101→
   102→    // Ensure rateMax >= rateMin
   103→    if (rateMax < rateMin) {
   104→      [rateMin, rateMax] = [rateMax, rateMin]; // Swap if reversed
   105→    }
   106→
   107→    // Ensure meaningful range (at least $5/hr difference)
   108→    if (rateMax - rateMin < 5) {
   109→      rateMax = rateMin + 10;
   110→      // Re-clamp if we exceeded max
   111→      if (rateMax > MAX_HOURLY_RATE) {
   112→        rateMax = MAX_HOURLY_RATE;
   113→        rateMin = Math.max(MIN_HOURLY_RATE, rateMax - 10);
   114→      }
   115→    }
   116→
   117→    return NextResponse.json({
   118→      title: generated.title || "",
   119→      description: generated.description || "",
   120→      certifications: generated.certifications || ["White Card"],
   121→      rateMin,
   122→      rateMax,
   123→      rateType: generated.rateType || "hourly",
   124→    });
   125→  } catch (error) {
   126→    logger.error("AI job generation error:", error);
   127→
   128→    if (error instanceof Error) {
   129→      if (error.message.includes("API key")) {
   130→        return NextResponse.json({ error: "AI service configuration error" }, { status: 503 });
   131→      }
   132→      if (error.message.includes("rate limit") || error.message.includes("429")) {
   133→        return NextResponse.json({ error: "AI service is busy. Please try again." }, { status: 429 });
   134→      }
   135→    }
   136→
   137→    return NextResponse.json({ error: "Failed to generate job details" }, { status: 500 });
   138→  }
   139→};
   140→
   141→export const POST = withCSRFProtection(handler);
   142→

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


Excellent! Let me check a few more supporting files to complete the picture:


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

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


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

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


Perfect! Let me get the layout information to understand the navigation and authenticated states:


1→import { redirect } from 'next/navigation'
     2→import { createClient } from '@/lib/supabase/server'
     3→import Link from 'next/link'
     4→import Image from 'next/image'
     5→import { SignOutButton } from '@/components/sign-out-button'
     6→import { NotificationBell } from '@/components/notification-bell'
     7→
     8→export default async function AuthenticatedLayout({
     9→  children,
    10→}: {
    11→  children: React.ReactNode
    12→}) {
    13→  const supabase = await createClient()
    14→  const { data: { user } } = await supabase.auth.getUser()
    15→
    16→  if (!user) {
    17→    redirect('/auth/login')
    18→  }
    19→
    20→  return (
    21→

    22→      {/* Header */}
    23→
    24→

    25→          <Link href="/dashboard">
    26→            <Image src="/logo_black.svg" alt="RateRight" width={120} height={34} className="h-7 w-auto" />
    27→          </Link>
    28→

    29→            {user.email}
    30→            <NotificationBell />
    31→            <SignOutButton />
    32→

    33→

    34→
    35→
    36→      {/* Main Content */}
    37→
    38→        {children}
    39→
    40→

    41→  )
    42→}
    43→

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


Bash

grep -r "contact" /home/ccuser/the-50-dollar-app/src --include="*.tsx" --include="*.ts" | grep -i "reveal\|show\|display" | head -20


Now let me check if there's any explicit contact reveal mechanism:


grep -r "phone\|email\|contact" /home/ccuser/the-50-dollar-app/src/app --include="*.tsx" | grep -i "hired\|payment\|confirm" | head -20


/home/ccuser/the-50-dollar-app/src/app/auth/login/page.tsx:    if (data.user && !data.user.email_confirmed_at) {
/home/ccuser/the-50-dollar-app/src/app/auth/verify-email/page.tsx:    if (user?.email_confirmed_at) {
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/payment-success/page.tsx:          setMessage(`Payment status: ${stripeStatus}. Please contact support if you were charged.`);
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/payment-success/page.tsx:        setMessage('Unable to verify payment status. Please contact support if you were charged.');
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/signup/page.tsx:        if (user && !user.email_confirmed_at) {
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/signup/page.tsx:    if (user && !user.email_confirmed_at) {