openstock/lib/inngest/functions.ts

120 lines
5.0 KiB
TypeScript

import {inngest} from "@/lib/inngest/client";
import {NEWS_SUMMARY_EMAIL_PROMPT, PERSONALIZED_WELCOME_EMAIL_PROMPT} from "@/lib/inngest/prompts";
import {sendNewsSummaryEmail, sendWelcomeEmail} from "@/lib/nodemailer";
import {getAllUsersForNewsEmail} from "@/lib/actions/user.actions";
import { getWatchlistSymbolsByEmail } from "@/lib/actions/watchlist.actions";
import { getNews } from "@/lib/actions/finnhub.actions";
import { getFormattedTodayDate } from "@/lib/utils";
export const sendSignUpEmail = inngest.createFunction(
{ id: 'sign-up-email' },
{ event: 'app/user.created'},
async ({ event, step }) => {
const userProfile = `
- Country: ${event.data.country}
- Investment goals: ${event.data.investmentGoals}
- Risk tolerance: ${event.data.riskTolerance}
- Preferred industry: ${event.data.preferredIndustry}
`
const prompt = PERSONALIZED_WELCOME_EMAIL_PROMPT.replace('{{userProfile}}', userProfile)
const response = await step.ai.infer('generate-welcome-intro', {
model: step.ai.models.gemini({ model: 'gemini-2.5-flash-lite' }),
body: {
contents: [
{
role: 'user',
parts: [
{ text: prompt }
]
}]
}
})
await step.run('send-welcome-email', async () => {
const part = response.candidates?.[0]?.content?.parts?.[0];
const introText = (part && 'text' in part ? part.text : null) ||'Thanks for joining Openstock. You now have the tools to track markets and make smarter moves.'
const { data: { email, name } } = event;
return await sendWelcomeEmail({ email, name, intro: introText });
})
return {
success: true,
message: 'Welcome email sent successfully'
}
}
)
export const sendDailyNewsSummary = inngest.createFunction(
{ id: 'daily-news-summary' },
[ { event: 'app/send.daily.news' }, { cron: '0 12 * * *' } ],
async ({ step }) => {
// Step #1: Get all users for news delivery
const users = await step.run('get-all-users', getAllUsersForNewsEmail)
if(!users || users.length === 0) return { success: false, message: 'No users found for news email' };
// Step #2: For each user, get watchlist symbols -> fetch news (fallback to general)
const results = await step.run('fetch-user-news', async () => {
const perUser: Array<{ user: User; articles: MarketNewsArticle[] }> = [];
for (const user of users as User[]) {
try {
const symbols = await getWatchlistSymbolsByEmail(user.email);
let articles = await getNews(symbols);
// Enforce max 6 articles per user
articles = (articles || []).slice(0, 6);
// If still empty, fallback to general
if (!articles || articles.length === 0) {
articles = await getNews();
articles = (articles || []).slice(0, 6);
}
perUser.push({ user, articles });
} catch (e) {
console.error('daily-news: error preparing user news', user.email, e);
perUser.push({ user, articles: [] });
}
}
return perUser;
});
// Step #3: (placeholder) Summarize news via AI
const userNewsSummaries: { user: User; newsContent: string | null }[] = [];
for (const { user, articles } of results) {
try {
const prompt = NEWS_SUMMARY_EMAIL_PROMPT.replace('{{newsData}}', JSON.stringify(articles, null, 2));
const response = await step.ai.infer(`summarize-news-${user.email}`, {
model: step.ai.models.gemini({ model: 'gemini-2.5-flash-lite' }),
body: {
contents: [{ role: 'user', parts: [{ text:prompt }]}]
}
});
const part = response.candidates?.[0]?.content?.parts?.[0];
const newsContent = (part && 'text' in part ? part.text : null) || 'No market news.'
userNewsSummaries.push({ user, newsContent });
} catch (e) {
console.error('Failed to summarize news for : ', user.email);
userNewsSummaries.push({ user, newsContent: null });
}
}
// Step #4: (placeholder) Send the emails
await step.run('send-news-emails', async () => {
await Promise.all(
userNewsSummaries.map(async ({ user, newsContent}) => {
if(!newsContent) return false;
return await sendNewsSummaryEmail({ email: user.email, date: getFormattedTodayDate(), newsContent })
})
)
})
return { success: true, message: 'Daily news summary emails sent successfully' }
}
)