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