import { requireAuth } from "@/lib/jwt"; import { sql, transaction } from "@/lib/db"; import { getOrder, consumeOrderForRide } from "@/lib/payment-orders"; import { matchNextDriver } from "@/lib/dispatch"; import { isServiceId } from "@/lib/driver"; 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 — request a ride. The rider no longer picks a driver; the ride is // created with status='requested' and driver_id=NULL, then auto-match offers // it to the nearest eligible driver of the requested service. `driver_id` in // the body is accepted for backward compatibility but ignored. 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, payment_method, payment_order_id, 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 }, ); } if (payment_method !== "card" && payment_method !== "cash") return Response.json( { error: "Invalid payment method." }, { status: 400 }, ); const rideService = isServiceId(service) ? service : DEFAULT_SERVICE; const fareCents = Math.round(Number(fare_price)); if (payment_method === "card") { // Card: the ride is only recorded once a paid, server-authoritative // payment order is consumed. The client can no longer self-declare // payment_status='paid'. if (isMissing(payment_order_id)) return Response.json( { error: "Missing payment order id." }, { status: 400 }, ); const order = await getOrder(payment_order_id); if (!order) return Response.json( { error: "Payment order not found." }, { status: 404 }, ); if (order.user_id !== auth.userId) return Response.json({ error: "Unauthorized." }, { status: 403 }); if (order.status !== "paid") return Response.json( { error: "Payment not verified." }, { status: 400 }, ); if (order.amount_cents !== fareCents) return Response.json( { error: "Payment amount mismatch." }, { status: 400 }, ); // Reconcile route intent (driver isn't known yet, so driver_id is no // longer part of the intent check). Null intent fields are skipped. const intentsMatch = (order.origin_address === null || order.origin_address === origin_address) && (order.destination_address === null || order.destination_address === destination_address) && (order.ride_time === null || order.ride_time === Number(ride_time)); if (!intentsMatch) return Response.json( { error: "Payment does not match this ride." }, { status: 400 }, ); // Consume the order and insert the ride on one connection, so a failure // rolls back both and no paid order is wasted without a ride. const inserted = await transaction(async (tx) => { const consumed = await consumeOrderForRide( payment_order_id, auth.userId, tx, ); if (!consumed) throw new Error("PAYMENT_ORDER_NOT_CONSUMABLE"); const rows = await tx` 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, payment_order_id, status, service ) VALUES ( ${origin_address}, ${destination_address}, ${origin_latitude}, ${origin_longitude}, ${destination_latitude}, ${destination_longitude}, ${ride_time}, ${fareCents}, 'paid', NULL, ${auth.userId}, ${payment_order_id}, 'requested', ${rideService} ) RETURNING * `; return rows[0]; }); // Kick off auto-match asynchronously — don't block the response on it. void matchNextDriver(inserted.ride_id); return Response.json({ data: inserted }, { status: 201 }); } // Cash: settled directly with the driver at drop-off. No order involved. 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}, 'cash', NULL, ${auth.userId}, 'requested', ${rideService} ) RETURNING * `; void matchNextDriver(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 }); } }