Files
shokuninmarche/jobs/reminderCron.js
airewit-developer c878eee62b feat: Guest import, RSVP reminders — 7ead7758 scope additions
CSV/Excel Import:
- POST /api/events/:id/guests/import (multer memory storage, max 5MB)
- Accepts .csv and .xlsx/.xls via xlsx + csv-parse/sync
- Handles Hebrew column names and English column names interchangeably
- Phone normalization (domain expert spec): strips spaces/hyphens/parens,
  handles 05X-XXXXXXX → +972..., +972... passthrough, 972... → +972...
  Invalid phone → guest imported with phone=null, warning recorded
- Unknown dietary_preference → 'none'; unknown relationship_group → 'other'
- Bulk insert in transaction (all-or-nothing), max 500 rows
- Returns: { imported, skipped, warnings, details } with per-row reasons
- UTF-8 BOM handled on CSV parse (Excel exports)

RSVP Reminder Cron:
- jobs/reminderCron.js: node-cron, daily at 09:00 Asia/Jerusalem
- Queries guests with rsvp_status=pending where event is 7 or 2 days away
- Regenerates wa.me reminder deep-link with urgency text (עוד שבוע / עוד 2 ימים)
- Updates invitations.whatsapp_link in-place
- No auto-send (MVP): organizer clicks link manually
- Started automatically in server.js app.listen callback

GET /api/events/:id/guests/reminders:
- Returns pending guests who have whatsapp_link set (reminder generated by cron)
- Organizer uses this to surface the Pending Reminders panel

Frontend additions:
- ImportGuestsForm component: file picker, POST multipart, shows import summary
  with per-row skipped/warning details
- PendingRemindersPanel component: orange card listing pending guests with
  wa.me reminder links; hides itself when no reminders
- GuestListPage: integrated both components, refreshTrigger propagates to
  reminders panel after any add/import/delete/status-change

Build: 0 TS errors, 62 modules transformed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 18:35:54 +00:00

83 lines
2.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* RSVP Reminder Cron Job
*
* Runs daily at 09:00 Asia/Jerusalem.
* Finds events at 7-day and 2-day thresholds with pending guests.
* Updates invitations.whatsapp_link with a fresh wa.me reminder deep-link.
* Organizer clicks the link manually (no auto-send in MVP).
*/
const cron = require('node-cron');
const pool = require('../db/pool');
const BASE_URL = process.env.APP_BASE_URL || 'http://localhost:3000';
function buildReminderWhatsAppLink(phone, eventTitle, rsvpUrl, daysUntilEvent) {
if (!phone) return null;
const phoneDigits = phone.replace('+', '');
const urgency = daysUntilEvent <= 2 ? 'עוד 2 ימים' : 'עוד שבוע';
const message = encodeURIComponent(
`תזכורת: ${urgency} לאירוע "${eventTitle}". אנא אשר/י הגעה: ${rsvpUrl}`
);
return `https://wa.me/${phoneDigits}?text=${message}`;
}
async function generateReminders() {
console.log('[ReminderCron] Running RSVP reminder generation…');
try {
// Find pending guests whose event is 7 or 2 days from now (±12h window for daily run)
const { rows } = await pool.query(`
SELECT
g.id AS guest_id,
g.name_hebrew,
g.phone,
e.id AS event_id,
e.title,
e.event_date,
i.token,
EXTRACT(DAY FROM (e.event_date::date - CURRENT_DATE)) AS days_until
FROM guests g
JOIN events e ON e.id = g.event_id
JOIN invitations i ON i.guest_id = g.id
WHERE g.rsvp_status = 'pending'
AND e.deleted_at IS NULL
AND e.event_date > NOW()
AND EXTRACT(DAY FROM (e.event_date::date - CURRENT_DATE)) IN (7, 2)
`);
if (rows.length === 0) {
console.log('[ReminderCron] No pending reminders due today.');
return;
}
for (const row of rows) {
const rsvpUrl = `${BASE_URL}/rsvp/${row.token}`;
const whatsappLink = buildReminderWhatsAppLink(
row.phone, row.title, rsvpUrl, parseInt(row.days_until)
);
if (whatsappLink) {
await pool.query(
'UPDATE invitations SET whatsapp_link = $1 WHERE token = $2',
[whatsappLink, row.token]
);
}
}
console.log(`[ReminderCron] Updated ${rows.length} pending reminder links.`);
} catch (err) {
console.error('[ReminderCron] Error:', err.message);
}
}
function startReminderCron() {
// 09:00 every day, Asia/Jerusalem timezone
cron.schedule('0 9 * * *', generateReminders, {
timezone: 'Asia/Jerusalem',
});
console.log('[ReminderCron] Scheduled: daily at 09:00 Asia/Jerusalem');
}
module.exports = { startReminderCron, generateReminders };