317 lines
9.0 KiB
TypeScript
317 lines
9.0 KiB
TypeScript
import { Elysia, t } from 'elysia';
|
|
import { db } from '../db';
|
|
import { clients, communications, userProfiles } 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');
|
|
}
|
|
|
|
// 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,
|
|
});
|
|
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<string, unknown> = {};
|
|
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' }),
|
|
}),
|
|
})
|
|
|
|
// 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' }),
|
|
}),
|
|
});
|