From 0bec3c574aa42389311d830c4357b66c258f1fb1 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Thu, 14 May 2026 07:16:43 -0400 Subject: [PATCH] FRE-5335 Hook waitlist signup to send confirmation email via Resend - Added @shieldai/shared-notifications, bullmq, ioredis deps to API - POST /api/waitlist/signup now sends waitlist_confirmation email via EmailService - Schedules welcome sequence (day1 intro, day3 features, day7 launch teaser) via BullMQ delayed jobs - Added waitlist email worker in @shieldai/jobs to process delayed welcome sequence emails - Templates already in place: waitlist_confirmation, waitlist_intro, waitlist_features, waitlist_launch_teaser with dark-themed HTML layouts Co-Authored-By: Paperclip --- packages/api/package.json | 5 +- packages/api/src/routes/waitlist.routes.ts | 55 ++++++++++++++++++++++ packages/jobs/src/index.ts | 41 ++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 7e583d8..d81c6f3 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -20,9 +20,12 @@ "@shieldai/db": "workspace:*", "@shieldai/monitoring": "workspace:*", "@shieldai/report": "workspace:*", + "@shieldai/shared-notifications": "workspace:*", "@shieldai/types": "workspace:*", "@shieldai/voiceprint": "workspace:*", - "fastify": "^5.2.0" + "bullmq": "^5.24.0", + "fastify": "^5.2.0", + "ioredis": "^5.4.0" }, "devDependencies": { "@vitest/coverage-v8": "^4.1.5", diff --git a/packages/api/src/routes/waitlist.routes.ts b/packages/api/src/routes/waitlist.routes.ts index e5b257c..6a5d9bc 100644 --- a/packages/api/src/routes/waitlist.routes.ts +++ b/packages/api/src/routes/waitlist.routes.ts @@ -1,5 +1,12 @@ import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import { prisma } from '@shieldai/db'; +import { EmailService } from '@shieldai/shared-notifications'; +import { Queue } from 'bullmq'; +import { Redis } from 'ioredis'; + +const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'; +const connection = new Redis(redisUrl); +const waitlistEmailQueue = new Queue('waitlist-emails', { connection }); interface WaitlistSignupBody { email: string; @@ -10,6 +17,13 @@ interface WaitlistSignupBody { utmCampaign?: string; } +function getPosition(entryId: string): string { + const hash = entryId.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0); + return String(10000 + (hash % 90000)); +} + +const DAY_MS = 24 * 60 * 60 * 1000; + export async function waitlistRoutes(fastify: FastifyInstance) { fastify.post('/waitlist/signup', async (request: FastifyRequest, reply: FastifyReply) => { const body = request.body as WaitlistSignupBody; @@ -48,6 +62,47 @@ export async function waitlistRoutes(fastify: FastifyInstance) { }, }); + const name = body.name?.trim() || 'there'; + const position = getPosition(entry.id); + + try { + const emailService = EmailService.getInstance(); + const result = await emailService.sendWithTemplate(email, { + templateId: 'waitlist_confirmation', + variables: { name, position }, + }); + if (result.status === 'failed') { + request.log.warn({ error: result.error }, 'Failed to send waitlist confirmation email'); + } else { + request.log.info({ email }, 'Waitlist confirmation email sent'); + } + } catch (err) { + request.log.error({ err }, 'Error sending waitlist confirmation email'); + } + + try { + await Promise.all([ + waitlistEmailQueue.add( + 'send-waitlist-intro', + { email, name, entryId: entry.id, tier }, + { delay: 1 * DAY_MS, attempts: 3, backoff: { type: 'exponential', delay: 5000 } } + ), + waitlistEmailQueue.add( + 'send-waitlist-features', + { email, name, entryId: entry.id, tier }, + { delay: 3 * DAY_MS, attempts: 3, backoff: { type: 'exponential', delay: 5000 } } + ), + waitlistEmailQueue.add( + 'send-waitlist-launch-teaser', + { email, name, entryId: entry.id, tier }, + { delay: 7 * DAY_MS, attempts: 3, backoff: { type: 'exponential', delay: 5000 } } + ), + ]); + request.log.info({ email }, 'Welcome sequence scheduled'); + } catch (err) { + request.log.error({ err }, 'Failed to schedule welcome sequence emails'); + } + return reply.code(201).send({ message: 'Welcome to the ShieldAI waitlist', id: entry.id, diff --git a/packages/jobs/src/index.ts b/packages/jobs/src/index.ts index d7edae1..8221da5 100644 --- a/packages/jobs/src/index.ts +++ b/packages/jobs/src/index.ts @@ -134,6 +134,47 @@ export async function scheduleWebhookProcessor() { }); } +// Waitlist email worker +import { EmailService } from '@shieldai/shared-notifications'; + +const waitlistEmailWorker = new Worker( + "waitlist-emails", + async (job) => { + const { email, name, entryId } = job.data; + const templateIdMap: Record = { + 'send-waitlist-intro': 'waitlist_intro', + 'send-waitlist-features': 'waitlist_features', + 'send-waitlist-launch-teaser': 'waitlist_launch_teaser', + }; + + const templateId = templateIdMap[job.name]; + if (!templateId) { + throw new Error(`Unknown waitlist email job: ${job.name}`); + } + + const emailService = EmailService.getInstance(); + const result = await emailService.sendWithTemplate(email, { + templateId, + variables: { name, entryId }, + }); + + if (result.status === 'failed') { + throw new Error(`Failed to send ${templateId} to ${email}: ${result.error}`); + } + + return { templateId, email, deliveredAt: result.deliveredAt }; + }, + { connection, concurrency: 5 } +); + +waitlistEmailWorker.on("completed", (job) => { + console.log(`[WaitlistEmail] Job ${job?.id} (${job?.name}) completed for ${job?.data?.email}`); +}); + +waitlistEmailWorker.on("failed", (job, err) => { + console.error(`[WaitlistEmail] Job ${job?.id} (${job?.name}) failed: ${err.message}`); +}); + console.log("Job workers started"); // Report generation workers