diff --git a/src/routes/api/public/process-task-reminders.ts b/src/routes/api/public/process-task-reminders.ts new file mode 100644 index 0000000..58d4c20 --- /dev/null +++ b/src/routes/api/public/process-task-reminders.ts @@ -0,0 +1,105 @@ +import { createClient } from '@supabase/supabase-js' +import { createFileRoute } from '@tanstack/react-router' + +// Cron-callable endpoint that finds due task reminders and triggers the +// transactional email send route for each. Authenticated by service-role key +// passed as Bearer token (configured in pg_cron job). + +export const Route = createFileRoute('/api/public/process-task-reminders')({ + server: { + handlers: { + POST: async ({ request }) => { + const supabaseUrl = import.meta.env.VITE_SUPABASE_URL + const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY + if (!supabaseUrl || !serviceKey) { + return Response.json({ error: 'config' }, { status: 500 }) + } + + const auth = request.headers.get('Authorization') ?? '' + const token = auth.startsWith('Bearer ') ? auth.slice(7).trim() : '' + if (token !== serviceKey) { + return Response.json({ error: 'unauthorized' }, { status: 401 }) + } + + const supabase = createClient(supabaseUrl, serviceKey) + + // Fetch due reminders (limit batch to avoid timeout). + const { data: reminders, error } = await supabase + .from('task_reminders') + .select( + 'id, task_id, recipient_email, remind_at, note, ' + + 'task:tasks(id, title, due_date, case_id, case:cases(case_number, title))' + ) + .is('sent_at', null) + .lte('remind_at', new Date().toISOString()) + .order('remind_at', { ascending: true }) + .limit(50) + + if (error) { + console.error('Failed to load task reminders', error) + return Response.json({ error: 'load_failed' }, { status: 500 }) + } + + if (!reminders?.length) { + return Response.json({ processed: 0 }) + } + + const url = new URL(request.url) + const sendUrl = `${url.origin}/lovable/email/transactional/send` + + let processed = 0 + for (const r of reminders as any[]) { + const task = r.task + const caseInfo = task?.case + const caseTitle = caseInfo + ? `${caseInfo.case_number} — ${caseInfo.title}` + : null + const dueDate = task?.due_date + ? new Date(task.due_date).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }) + : null + + try { + const res = await fetch(sendUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${serviceKey}`, + }, + body: JSON.stringify({ + templateName: 'task-reminder', + recipientEmail: r.recipient_email, + idempotencyKey: `task-reminder-${r.id}`, + templateData: { + taskTitle: task?.title ?? 'Task', + caseTitle, + dueDate, + note: r.note, + }, + }), + }) + + if (!res.ok) { + const body = await res.text() + console.error('Reminder send failed', { id: r.id, status: res.status, body }) + continue + } + + await supabase + .from('task_reminders') + .update({ sent_at: new Date().toISOString() }) + .eq('id', r.id) + processed++ + } catch (e) { + console.error('Reminder dispatch error', { id: r.id, error: e }) + } + } + + return Response.json({ processed }) + }, + }, + }, +})