Files
redefined-designs/backend/src/routes/cart.ts
T
bermudalamb 9ab689e624
SonarQube Analysis / sonarqube (pull_request) Failing after 59s
Tests / backend-unit (pull_request) Successful in 34s
Tests / backend-integration (pull_request) Failing after 1m33s
Tests / frontend-e2e (pull_request) Failing after 1m5s
feat: add customer cart with expiry, shipping addresses with USPS validation, and multi-item PayPal checkout
2026-08-14 14:02:25 -05:00

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;