import { Router, Request, Response } from 'express'; import { pool } from '../db'; import { asyncRoute } from '../asyncRoute'; import { requireCustomer } from '../middleware/customerAuth'; const router = Router(); async function getCartExpiryHours(): Promise { const { rows } = await pool.query(`SELECT value FROM admin_settings WHERE key = 'cart_expiry_hours'`); return rows.length ? parseFloat(rows[0].value) : 24; } const CART_ITEM_SELECT = ` SELECT ci.item_id, ci.added_at, ci.expires_at, i.name, i.price_cents, i.status, COALESCE( json_agg(json_build_object('id', img.id, 'image_path', img.image_path) ORDER BY img.sort_order) FILTER (WHERE img.id IS NOT NULL), '[]' ) AS images FROM cart_items ci JOIN items i ON i.id = ci.item_id LEFT JOIN item_images img ON img.item_id = i.id WHERE ci.cart_id = $1 GROUP BY ci.item_id, ci.added_at, ci.expires_at, i.name, i.price_cents, i.status ORDER BY ci.added_at DESC `; router.get('/', requireCustomer, asyncRoute(async (req: Request, res: Response) => { const { rows: cartRows } = await pool.query(`SELECT id FROM carts WHERE customer_id = $1`, [req.customerId]); if (!cartRows.length) return res.json({ items: [] }); const { rows: items } = await pool.query(CART_ITEM_SELECT, [cartRows[0].id]); res.json({ items }); })); router.post('/items/:itemId', requireCustomer, asyncRoute(async (req: Request, res: Response) => { const itemId = req.params.itemId; const client = await pool.connect(); try { await client.query('BEGIN'); const { rows: itemRows } = await client.query(`SELECT * FROM items WHERE id = $1 FOR UPDATE`, [itemId]); const item = itemRows[0]; if (!item) { await client.query('ROLLBACK'); return res.status(404).json({ error: 'not found' }); } if (item.status !== 'available') { await client.query('ROLLBACK'); return res.status(409).json({ error: 'item is no longer available' }); } let { rows: cartRows } = await client.query(`SELECT id FROM carts WHERE customer_id = $1`, [req.customerId]); let cartId: number; if (cartRows.length) { cartId = cartRows[0].id; await client.query(`UPDATE carts SET updated_at = now() WHERE id = $1`, [cartId]); } else { const { rows: newCart } = await client.query( `INSERT INTO carts (customer_id) VALUES ($1) RETURNING id`, [req.customerId] ); cartId = newCart[0].id; } const hours = await getCartExpiryHours(); const expiresAt = new Date(Date.now() + hours * 60 * 60 * 1000); await client.query( `INSERT INTO cart_items (cart_id, item_id, expires_at) VALUES ($1, $2, $3)`, [cartId, itemId, expiresAt] ); await client.query(`UPDATE items SET status = 'reserved' WHERE id = $1`, [itemId]); await client.query('COMMIT'); res.status(201).json({ itemId: parseInt(itemId, 10), expiresAt }); } catch (err) { await client.query('ROLLBACK'); console.error(err); res.status(500).json({ error: 'internal error' }); } finally { client.release(); } })); router.delete('/items/:itemId', requireCustomer, asyncRoute(async (req: Request, res: Response) => { const itemId = req.params.itemId; const client = await pool.connect(); try { await client.query('BEGIN'); const { rows } = await client.query( `DELETE FROM cart_items ci USING carts c WHERE ci.cart_id = c.id AND c.customer_id = $1 AND ci.item_id = $2 RETURNING ci.item_id`, [req.customerId, itemId] ); if (!rows.length) { await client.query('ROLLBACK'); return res.status(404).json({ error: 'not in your cart' }); } await client.query( `UPDATE items SET status = 'available' WHERE id = $1 AND status = 'reserved'`, [itemId] ); await client.query('COMMIT'); res.status(204).end(); } catch (err) { await client.query('ROLLBACK'); console.error(err); res.status(500).json({ error: 'internal error' }); } finally { client.release(); } })); export default router;