import { authMiddleware } from '../middleware/auth'; import { Elysia, t } from 'elysia'; import { db } from '../db'; import { clients, communications, userProfiles } from '../db/schema'; import { eq, and, inArray } from 'drizzle-orm'; import { generateEmail, generateSubject, generateBirthdayMessage, type AIProvider } from '../services/ai'; import { sendEmail } from '../services/email'; import type { User } from '../lib/auth'; import { randomUUID } from 'crypto'; export const emailRoutes = new Elysia({ prefix: '/emails' }) .use(authMiddleware) // Generate email for a client .post('/generate', async ({ body, user }: { body: { clientId: string; purpose: string; provider?: AIProvider; }; user: User; }) => { // Get client const [client] = await db.select() .from(clients) .where(and(eq(clients.id, body.clientId), eq(clients.userId, user.id))) .limit(1); if (!client) { throw new Error('Client not found'); } // Get user profile for signature const [profile] = await db.select() .from(userProfiles) .where(eq(userProfiles.userId, user.id)) .limit(1); // Build advisor info const advisorInfo = { name: user.name, title: profile?.title || '', company: profile?.company || '', phone: profile?.phone || '', signature: profile?.emailSignature || '', }; // Generate email content console.log(`[${new Date().toISOString()}] Generating email for client ${client.firstName}, purpose: ${body.purpose}`); let content: string; let subject: string; try { content = await generateEmail({ advisorName: advisorInfo.name, advisorTitle: advisorInfo.title, advisorCompany: advisorInfo.company, advisorPhone: advisorInfo.phone, advisorSignature: advisorInfo.signature, clientName: client.firstName, interests: client.interests || [], notes: client.notes || '', purpose: body.purpose, provider: body.provider, communicationStyle: profile?.communicationStyle as any, }); console.log(`[${new Date().toISOString()}] Email content generated successfully`); } catch (e) { console.error(`[${new Date().toISOString()}] Failed to generate email content:`, e); throw e; } // Generate subject try { subject = await generateSubject(body.purpose, client.firstName, body.provider); console.log(`[${new Date().toISOString()}] Email subject generated successfully`); } catch (e) { console.error(`[${new Date().toISOString()}] Failed to generate subject:`, e); throw e; } // Save as draft const [communication] = await db.insert(communications) .values({ userId: user.id, clientId: client.id, type: 'email', subject, content, aiGenerated: true, aiModel: body.provider || 'anthropic', status: 'draft', }) .returning(); return communication; }, { body: t.Object({ clientId: t.String({ format: 'uuid' }), purpose: t.String({ minLength: 1 }), provider: t.Optional(t.Union([t.Literal('anthropic'), t.Literal('openai')])), }), }) // Generate birthday message .post('/generate-birthday', async ({ body, user }: { body: { clientId: string; provider?: AIProvider; }; user: User; }) => { // Get client const [client] = await db.select() .from(clients) .where(and(eq(clients.id, body.clientId), eq(clients.userId, user.id))) .limit(1); if (!client) { throw new Error('Client not found'); } // Calculate years as client const yearsAsClient = Math.floor( (Date.now() - new Date(client.createdAt).getTime()) / (365.25 * 24 * 60 * 60 * 1000) ); // Generate message const content = await generateBirthdayMessage({ clientName: client.firstName, yearsAsClient, interests: client.interests || [], provider: body.provider, }); // Save as draft const [communication] = await db.insert(communications) .values({ userId: user.id, clientId: client.id, type: 'birthday', subject: `Happy Birthday, ${client.firstName}!`, content, aiGenerated: true, aiModel: body.provider || 'anthropic', status: 'draft', }) .returning(); return communication; }, { body: t.Object({ clientId: t.String({ format: 'uuid' }), provider: t.Optional(t.Union([t.Literal('anthropic'), t.Literal('openai')])), }), }) // List emails (drafts and sent) .get('/', async ({ query, user }: { query: { status?: string; clientId?: string }; user: User; }) => { let conditions = [eq(communications.userId, user.id)]; if (query.status) { conditions.push(eq(communications.status, query.status)); } if (query.clientId) { conditions.push(eq(communications.clientId, query.clientId)); } const results = await db.select() .from(communications) .where(and(...conditions)) .orderBy(communications.createdAt); return results; }, { query: t.Object({ status: t.Optional(t.String()), clientId: t.Optional(t.String({ format: 'uuid' })), }), }) // Get single email .get('/:id', async ({ params, user }: { params: { id: string }; user: User }) => { const [email] = await db.select() .from(communications) .where(and(eq(communications.id, params.id), eq(communications.userId, user.id))) .limit(1); if (!email) { throw new Error('Email not found'); } return email; }, { params: t.Object({ id: t.String({ format: 'uuid' }), }), }) // Update email (edit draft) .put('/:id', async ({ params, body, user }: { params: { id: string }; body: { subject?: string; content?: string }; user: User; }) => { const updateData: Record = {}; if (body.subject !== undefined) updateData.subject = body.subject; if (body.content !== undefined) updateData.content = body.content; const [email] = await db.update(communications) .set(updateData) .where(and( eq(communications.id, params.id), eq(communications.userId, user.id), eq(communications.status, 'draft') // Can only edit drafts )) .returning(); if (!email) { throw new Error('Email not found or already sent'); } return email; }, { params: t.Object({ id: t.String({ format: 'uuid' }), }), body: t.Object({ subject: t.Optional(t.String()), content: t.Optional(t.String()), }), }) // Send email .post('/:id/send', async ({ params, user }: { params: { id: string }; user: User }) => { console.log(`[${new Date().toISOString()}] Send email request for id: ${params.id}`); // Get email const [email] = await db.select({ email: communications, client: clients, }) .from(communications) .innerJoin(clients, eq(communications.clientId, clients.id)) .where(and( eq(communications.id, params.id), eq(communications.userId, user.id), eq(communications.status, 'draft') )) .limit(1); if (!email) { console.log(`[${new Date().toISOString()}] Email not found or already sent`); throw new Error('Email not found or already sent'); } if (!email.client.email) { console.log(`[${new Date().toISOString()}] Client has no email address`); throw new Error('Client has no email address'); } console.log(`[${new Date().toISOString()}] Sending email to: ${email.client.email}`); // Send via Resend try { await sendEmail({ to: email.client.email, subject: email.email.subject || 'Message from your advisor', content: email.email.content, }); console.log(`[${new Date().toISOString()}] Email sent successfully`); } catch (e) { console.error(`[${new Date().toISOString()}] Failed to send email:`, e); throw e; } // Update status const [updated] = await db.update(communications) .set({ status: 'sent', sentAt: new Date(), }) .where(eq(communications.id, params.id)) .returning(); // Update client's last contacted await db.update(clients) .set({ lastContactedAt: new Date() }) .where(eq(clients.id, email.client.id)); return updated; }, { params: t.Object({ id: t.String({ format: 'uuid' }), }), }) // Bulk generate emails .post('/bulk-generate', async ({ body, user }: { body: { clientIds: string[]; purpose: string; provider?: AIProvider }; user: User; }) => { const batchId = randomUUID(); // Get user profile const [profile] = await db.select() .from(userProfiles) .where(eq(userProfiles.userId, user.id)) .limit(1); const advisorInfo = { name: user.name, title: profile?.title || '', company: profile?.company || '', phone: profile?.phone || '', signature: profile?.emailSignature || '', }; // Get all selected clients const selectedClients = await db.select() .from(clients) .where(and( inArray(clients.id, body.clientIds), eq(clients.userId, user.id), )); if (selectedClients.length === 0) { throw new Error('No valid clients found'); } const results = []; for (const client of selectedClients) { try { const content = await generateEmail({ advisorName: advisorInfo.name, advisorTitle: advisorInfo.title, advisorCompany: advisorInfo.company, advisorPhone: advisorInfo.phone, advisorSignature: advisorInfo.signature, clientName: client.firstName, interests: client.interests || [], notes: client.notes || '', purpose: body.purpose, provider: body.provider, }); const subject = await generateSubject(body.purpose, client.firstName, body.provider); const [comm] = await db.insert(communications) .values({ userId: user.id, clientId: client.id, type: 'email', subject, content, aiGenerated: true, aiModel: body.provider || 'anthropic', status: 'draft', batchId, }) .returning(); results.push({ clientId: client.id, email: comm, success: true }); } catch (error: any) { results.push({ clientId: client.id, error: error.message, success: false }); } } return { batchId, results, total: selectedClients.length, generated: results.filter(r => r.success).length }; }, { body: t.Object({ clientIds: t.Array(t.String({ format: 'uuid' }), { minItems: 1 }), purpose: t.String({ minLength: 1 }), provider: t.Optional(t.Union([t.Literal('anthropic'), t.Literal('openai')])), }), }) // Bulk send all drafts in a batch .post('/bulk-send', async ({ body, user }: { body: { batchId: string }; user: User; }) => { // Get all drafts in this batch const drafts = await db.select({ email: communications, client: clients, }) .from(communications) .innerJoin(clients, eq(communications.clientId, clients.id)) .where(and( eq(communications.batchId, body.batchId), eq(communications.userId, user.id), eq(communications.status, 'draft'), )); const results = []; for (const { email, client } of drafts) { if (!client.email) { results.push({ id: email.id, success: false, error: 'Client has no email' }); continue; } try { await sendEmail({ to: client.email, subject: email.subject || 'Message from your advisor', content: email.content, }); await db.update(communications) .set({ status: 'sent', sentAt: new Date() }) .where(eq(communications.id, email.id)); await db.update(clients) .set({ lastContactedAt: new Date() }) .where(eq(clients.id, client.id)); results.push({ id: email.id, success: true }); } catch (error: any) { results.push({ id: email.id, success: false, error: error.message }); } } return { batchId: body.batchId, total: drafts.length, sent: results.filter(r => r.success).length, failed: results.filter(r => !r.success).length, results, }; }, { body: t.Object({ batchId: t.String({ minLength: 1 }), }), }) // Delete draft .delete('/:id', async ({ params, user }: { params: { id: string }; user: User }) => { const [deleted] = await db.delete(communications) .where(and( eq(communications.id, params.id), eq(communications.userId, user.id), eq(communications.status, 'draft') // Can only delete drafts )) .returning({ id: communications.id }); if (!deleted) { throw new Error('Email not found or already sent'); } return { success: true, id: deleted.id }; }, { params: t.Object({ id: t.String({ format: 'uuid' }), }), });