// Server-authoritative payment order records. The client may never set // payment_status or the Areeba successIndicator; both are stored here and // verified against the gateway before an order can pay for a ride. // // A paid order can only be consumed once: consumeOrderForRide atomically // flips status 'paid' -> 'consumed', so a single card payment can never buy // two rides. import type { QueryResultRow } from "pg"; import { sql, type SqlValue } from "@/lib/db"; // A tagged-template runner — either the pool-level `sql` helper or the `tx` // passed inside a transaction() callback. consumeOrderForRide accepts one so // the consume + ride insert can run on a single connection. type Runner = ( strings: TemplateStringsArray, ...values: SqlValue[] ) => Promise; export type PaymentOrder = { order_id: string; user_id: string; amount_cents: number; currency: string; driver_id: number | null; origin_address: string | null; destination_address: string | null; origin_latitude: number | null; origin_longitude: number | null; destination_latitude: number | null; destination_longitude: number | null; ride_time: number | null; success_indicator: string | null; status: string; created_at: Date; paid_at: Date | null; }; export type NewOrder = { order_id: string; user_id: string; amount_cents: number; currency: string; driver_id?: number | null; origin_address?: string | null; destination_address?: string | null; origin_latitude?: number | null; origin_longitude?: number | null; destination_latitude?: number | null; destination_longitude?: number | null; ride_time?: number | null; success_indicator: string | null; status?: string; }; export const createOrder = async (order: NewOrder): Promise => { const rows = await sql` INSERT INTO payment_orders ( order_id, user_id, amount_cents, currency, driver_id, origin_address, destination_address, origin_latitude, origin_longitude, destination_latitude, destination_longitude, ride_time, success_indicator, status ) VALUES ( ${order.order_id}, ${order.user_id}, ${order.amount_cents}, ${order.currency}, ${order.driver_id ?? null}, ${order.origin_address ?? null}, ${order.destination_address ?? null}, ${order.origin_latitude ?? null}, ${order.origin_longitude ?? null}, ${order.destination_latitude ?? null}, ${order.destination_longitude ?? null}, ${order.ride_time ?? null}, ${order.success_indicator}, ${order.status ?? "pending"} ) RETURNING * `; return rows[0]; }; export const getOrder = async (orderId: string): Promise => { const rows = await sql` SELECT * FROM payment_orders WHERE order_id = ${orderId} `; return rows[0] ?? null; }; // Mark an order paid after the gateway confirms capture. The status='pending' // guard means an already-paid or consumed order can never be flipped back to // 'paid' — this is what prevents a single payment from being resurrected to // buy multiple rides (double-spend). verify+api.ts also rejects non-pending // orders, so this is defense-in-depth. export const markPaid = async (orderId: string): Promise => { const rows = await sql` UPDATE payment_orders SET status = 'paid', paid_at = CURRENT_TIMESTAMP WHERE order_id = ${orderId} AND status = 'pending' RETURNING * `; return rows[0] ?? null; }; // Atomically consume a paid order for a ride. The WHERE status='paid' guard // means a paid order can only be used once; a second attempt gets no row. // Pass the transaction `tx` runner so this can run inside ride/create's // transaction together with the ride insert. export const consumeOrderForRide = async ( orderId: string, userId: string, runner: Runner = sql, ): Promise => { const rows = await runner` UPDATE payment_orders SET status = 'consumed' WHERE order_id = ${orderId} AND user_id = ${userId} AND status = 'paid' RETURNING * `; return rows[0] ?? null; };