import { sql } from "@/lib/db"; import { requireRideParticipant } from "@/lib/ride-participants"; import { refreshDriverRating, refreshRiderRating } from "@/lib/ride-lifecycle"; // Two-way rating on a finished ride: the rider rates the driver, the driver // rates the rider. Either party may only rate once (the UNIQUE (ride_id, // rater_type) constraint makes the write an idempotent upsert, so a re-submit // corrects a mis-tap instead of double-counting), and only after the ride is // completed — a cancelled ride has nothing to rate. // GET — both sides' ratings for this ride, so a client can show "you rated // this ride 5" and (once the other party has rated) what they said. export async function GET(req: Request, { id }: { id: string }) { const rideId = Number(id); if (!Number.isInteger(rideId)) { return Response.json({ error: "Invalid ride id." }, { status: 400 }); } const participant = await requireRideParticipant(req, rideId); if ("error" in participant) return participant.error; try { const rows = await sql<{ rater_type: "rider" | "driver"; rating: number; comment: string | null; created_at: string; }>` SELECT rater_type, rating, comment, created_at FROM ride_ratings WHERE ride_id = ${rideId} `; const mine = rows.find((r) => r.rater_type === participant.role) ?? null; const theirs = rows.find((r) => r.rater_type !== participant.role) ?? null; return Response.json({ data: { mine, theirs } }); } catch (error) { console.error("[GET_RIDE_RATING]: ", error); return Response.json({ error: "Internal Server Error" }, { status: 500 }); } } // POST — submit (or correct) this party's rating. Body: { rating: 1..5, // comment?: string }. export async function POST(req: Request, { id }: { id: string }) { const rideId = Number(id); if (!Number.isInteger(rideId)) { return Response.json({ error: "Invalid ride id." }, { status: 400 }); } const participant = await requireRideParticipant(req, rideId); if ("error" in participant) return participant.error; let body: { rating?: unknown; comment?: unknown }; try { body = await req.json(); } catch { return Response.json({ error: "Invalid JSON body." }, { status: 400 }); } const rating = Number(body.rating); if (!Number.isInteger(rating) || rating < 1 || rating > 5) { return Response.json( { error: "rating must be a whole number from 1 to 5." }, { status: 400 }, ); } // Comments are optional and capped — they're shown verbatim in the admin // portal's ride detail, so an unbounded field is a liability. const rawComment = typeof body.comment === "string" ? body.comment.trim() : ""; const comment = rawComment ? rawComment.slice(0, 500) : null; try { const rides = await sql<{ status: string; driver_id: number | null; user_id: string; }>` SELECT status, driver_id, user_id FROM rides WHERE ride_id = ${rideId} `; const ride = rides[0]; if (!ride) { return Response.json({ error: "Ride not found." }, { status: 404 }); } if (ride.status !== "completed") { return Response.json( { error: "Only a completed ride can be rated." }, { status: 409 }, ); } const rows = await sql<{ rating: number; comment: string | null }>` INSERT INTO ride_ratings (ride_id, rater_type, rating, comment) VALUES (${rideId}, ${participant.role}, ${rating}, ${comment}) ON CONFLICT (ride_id, rater_type) DO UPDATE SET rating = EXCLUDED.rating, comment = EXCLUDED.comment, updated_at = CURRENT_TIMESTAMP RETURNING rating, comment `; // Fold the new score into the rated party's headline average. Awaited // rather than fire-and-forget so the client's next read sees it. if (participant.role === "rider" && ride.driver_id !== null) { await refreshDriverRating(ride.driver_id); } else if (participant.role === "driver") { await refreshRiderRating(ride.user_id); } return Response.json({ data: rows[0] }, { status: 201 }); } catch (error) { console.error("[RATE_RIDE]: ", error); return Response.json({ error: "Internal Server Error" }, { status: 500 }); } }