feat: add customer cart with expiry, shipping addresses with USPS validation, and multi-item PayPal checkout
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user