import { requireAuth } from "@/lib/jwt"; import { sql } from "@/lib/db"; import { broadcastRequest } from "@/lib/dispatch"; import { isServiceId } from "@/lib/driver"; import { ACTIVE_STATUS_ARRAY } from "@/lib/ride-lifecycle"; import { DEFAULT_SERVICE } from "@/constants/services"; // Explicit missing check — a truthy check would reject legitimate 0 values // like latitude 0.0 (the equator) or a zero fare. const isMissing = (v: unknown): boolean => v === undefined || v === null; // POST — open a ride request. // // This fires the moment the rider taps "Find now", before any payment // decision: the ride is created with status='requested', driver_id=NULL and // payment_status='pending', then broadcast to every eligible driver near the // pickup. Drivers volunteer, the rider picks one, and /ride/:id/select is // where the driver, the payment method and (for card) the paid order all land // together. // // Nothing is charged here, so there is nothing to refund if no driver takes // it — which is the point of moving payment behind the pick. export async function POST(request: Request) { const auth = requireAuth(request); if ("error" in auth) return auth.error; try { const body = await request.json(); const { origin_address, destination_address, origin_latitude, origin_longitude, destination_latitude, destination_longitude, ride_time, fare_price, service, } = body; if ( isMissing(origin_address) || isMissing(destination_address) || isMissing(origin_latitude) || isMissing(origin_longitude) || isMissing(destination_latitude) || isMissing(destination_longitude) || isMissing(ride_time) || isMissing(fare_price) ) { return Response.json( { error: "Missing required fields" }, { status: 400 }, ); } const rideService = isServiceId(service) ? service : DEFAULT_SERVICE; const fareCents = Math.round(Number(fare_price)); if (!Number.isFinite(fareCents) || fareCents <= 0) { return Response.json({ error: "Invalid fare." }, { status: 400 }); } // One ride in flight per rider. Without this a rider who backs out of the // tracking screen and re-books ends up with two live requests broadcast to // the same drivers, who then see the same job twice from one person. const inFlight = await sql<{ ride_id: number; status: string }>` SELECT ride_id, status FROM rides WHERE user_id = ${auth.userId} AND status = ANY(${ACTIVE_STATUS_ARRAY}::text[]) ORDER BY created_at DESC LIMIT 1 `; if (inFlight[0]) { return Response.json( { error: "You already have a ride in progress.", code: "RIDE_IN_PROGRESS", ride_id: inFlight[0].ride_id, }, { status: 409 }, ); } const response = await sql` INSERT INTO rides ( origin_address, destination_address, origin_latitude, origin_longitude, destination_latitude, destination_longitude, ride_time, fare_price, payment_status, driver_id, user_id, status, service ) VALUES ( ${origin_address}, ${destination_address}, ${origin_latitude}, ${origin_longitude}, ${destination_latitude}, ${destination_longitude}, ${ride_time}, ${fareCents}, 'pending', NULL, ${auth.userId}, 'requested', ${rideService} ) RETURNING * `; // Announce it to nearby drivers. Not awaited: the rider's screen should // open on "looking for drivers" immediately, and the rider's own status // poll re-drives the broadcast if this one loses its race with the push // service. void broadcastRequest(response[0].ride_id); return Response.json({ data: response[0] }, { status: 201 }); } catch (error) { console.error("[CREATE_RIDES]: ", error); return Response.json({ error: "Internal Server Error" }, { status: 500 }); } }