109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
|
import { pool } from '../db';
|
|
import { requireCustomer } from '../middleware/customerAuth';
|
|
|
|
const router = Router();
|
|
|
|
async function getCartExpiryHours(): Promise<number> {
|
|
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, 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, 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, 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;
|