import { Elysia, t } from 'elysia'; import { db } from '../db'; import { clients, communications } from '../db/schema'; import { eq, and } from 'drizzle-orm'; import { generateEmail, generateSubject, generateBirthdayMessage, type AIProvider } from '../services/ai'; import { sendEmail } from '../services/email'; import type { User } from '../lib/auth'; export const emailRoutes = new Elysia({ prefix: '/emails' }) // 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'); } // 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: user.name, clientName: client.firstName, interests: client.interests || [], notes: client.notes || '', purpose: body.purpose, provider: body.provider, }); 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 }) => { // 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) { throw new Error('Email not found or already sent'); } if (!email.client.email) { throw new Error('Client has no email address'); } // Send via Resend await sendEmail({ to: email.client.email, subject: email.email.subject || 'Message from your advisor', content: email.email.content, }); // 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' }), }), }) // 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' }), }), });