feat: add customer cart with expiry, shipping addresses with USPS validation, and multi-item PayPal checkout
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

This commit is contained in:
2026-08-14 14:02:25 -05:00
parent 433d7e1db5
commit 9ab689e624
18 changed files with 1242 additions and 189 deletions
+48 -5
View File
@@ -1,16 +1,59 @@
import cron from 'node-cron';
import app from './app';
import { pool } from './db';
import { sendMail } from './mailer';
// Release cart holds whose expiry has passed, every 5 minutes.
setInterval(async () => {
try {
await pool.query(
`UPDATE items SET status = 'available', reserved_until = NULL, paypal_order_id = NULL
WHERE status = 'reserved' AND reserved_until < now()`
const { rows } = await pool.query(
`DELETE FROM cart_items WHERE expires_at < now() RETURNING item_id`
);
for (const row of rows) {
await pool.query(`UPDATE items SET status = 'available' WHERE id = $1 AND status = 'reserved'`, [row.item_id]);
}
} catch (err) {
console.error('reservation sweep failed:', (err as Error).message);
console.error('cart expiry sweep failed:', (err as Error).message);
}
}, 60 * 1000);
}, 5 * 60 * 1000);
// Daily cart reminder emails at 9am server time, for customers who opted into marketing email.
cron.schedule('0 9 * * *', async () => {
try {
const { rows } = await pool.query(`
SELECT c.email, c.name, i.name AS item_name, ci.expires_at, ci.id AS cart_item_id
FROM cart_items ci
JOIN carts ca ON ca.id = ci.cart_id
JOIN customers c ON c.id = ca.customer_id
JOIN items i ON i.id = ci.item_id
WHERE c.marketing_consent = true
AND (ci.last_reminder_sent_at IS NULL OR ci.last_reminder_sent_at < now() - interval '20 hours')
AND ci.expires_at > now()
`);
const byEmail = new Map<string, { name: string | null; items: { name: string; expiresAt: Date; cartItemId: number }[] }>();
for (const row of rows) {
if (!byEmail.has(row.email)) byEmail.set(row.email, { name: row.name, items: [] });
byEmail.get(row.email)!.items.push({ name: row.item_name, expiresAt: row.expires_at, cartItemId: row.cart_item_id });
}
for (const [email, data] of byEmail) {
const itemList = data.items.map(i => `<li>${i.name} — reserved until ${i.expiresAt.toLocaleString()}</li>`).join('');
await sendMail(
email,
'Items waiting in your cart',
`<p>Hi${data.name ? ' ' + data.name : ''},</p>
<p>You still have items in your cart at Redefined Designs:</p>
<ul>${itemList}</ul>
<p><a href="${process.env.PUBLIC_URL}/cart">View your cart</a> before your reservation expires.</p>`
);
const ids = data.items.map(i => i.cartItemId);
await pool.query(`UPDATE cart_items SET last_reminder_sent_at = now() WHERE id = ANY($1::int[])`, [ids]);
}
} catch (err) {
console.error('daily cart reminder job failed:', (err as Error).message);
}
});
const PORT = parseInt(process.env.PORT || '3000', 10);
app.listen(PORT, () => console.log(`redefined-designs listening on ${PORT}`));