import { Resend } from 'resend'; import { Prisma } from '@prisma/client'; import prisma from '@prisma/prisma'; import { ReportExpenseData, ReportDayLogsData } from '@utils/types'; export class ExpenseReporter { private resend: Resend; private senderEmail: string; private recipientEmail: string; constructor() { if (!process.env.RESEND_API_KEY) { throw new Error('RESEND_API_KEY environment variable is not set'); } if (!process.env.SENDER_EMAIL) { throw new Error('SENDER_EMAIL environment variable is not set'); } if (!process.env.RECIPIENT_EMAIL) { throw new Error('RECIPIENT_EMAIL environment variable is not set'); } this.resend = new Resend(process.env.RESEND_API_KEY); this.senderEmail = process.env.SENDER_EMAIL; this.recipientEmail = process.env.RECIPIENT_EMAIL; } private async generateExpenses( from: Date, to: Date ): Promise { const startDate = new Date(from); startDate.setHours(0, 0, 0, 0); const endDate = new Date(to); endDate.setHours(23, 59, 59, 999); const expenses = await prisma.expense.findMany({ where: { deleted: false, createdAt: { gte: startDate, lte: endDate } }, include: { category: true }, orderBy: { createdAt: 'desc' } }); const totalExpenses = expenses.reduce((sum, exp) => sum + exp.cost, 0); const categoryMap = new Map(); expenses.forEach(exp => { const current = categoryMap.get(exp.category.name) || { total: 0, count: 0 }; categoryMap.set(exp.category.name, { total: current.total + exp.cost, count: current.count + 1 }); }); const byCategory = Array.from(categoryMap.entries()) .map(([category, stats]) => ({ category, total: stats.total, count: stats.count })) .sort((a, b) => b.total - a.total); return { expenses, summary: { totalExpenses, byCategory }, dateRange: { from, to } }; } private async generateDayLogs( from: Date, to: Date ): Promise { const startDate = new Date(from); startDate.setHours(0, 0, 0, 0); const endDate = new Date(to); endDate.setHours(23, 59, 59, 999); const dayLogs = await prisma.dayLog.findMany({ where: { createdAt: { gte: startDate, lte: endDate } }, orderBy: { createdAt: 'desc' } }); return { dayLogs, dateRange: { from, to } }; } private generateHtmlReport( expenses: ReportExpenseData, dayLogs: ReportDayLogsData ): string { const formatDate = (date: Date) => date.toLocaleDateString('en-GB', { year: 'numeric', month: 'long', day: 'numeric' }); const formatCurrency = (amount: number) => amount.toLocaleString('en-GB', { style: 'currency', currency: 'EUR' }); return `

Diary Report

From ${formatDate(expenses.dateRange.from)} to ${formatDate(expenses.dateRange.to)}

Summary

Total Expenses: ${formatCurrency(expenses.summary.totalExpenses)}

Expenses by Category

${expenses.summary.byCategory .map( cat => ` ` ) .join('')}
Category Total Count
${cat.category} ${formatCurrency(cat.total)} ${cat.count}

Detailed Expenses

${expenses.expenses .map( exp => ` ` ) .join('')}
ID Date Description Category Amount
${exp.id} ${formatDate(exp.createdAt)} ${exp.description} ${exp.category.name} ${formatCurrency(exp.cost)}

Day Logs Report

From ${formatDate(dayLogs.dateRange.from)} to ${formatDate(dayLogs.dateRange.to)}

${dayLogs.dayLogs .filter( (dl): dl is typeof dl & { comments: any[] } => dl.comments !== null && Array.isArray(dl.comments) ) .map( dl => ` ` ) .join('')}
ID Date Stars Log
${dl.id} ${formatDate(dl.createdAt)}
${dl.comments.map(c => `- ${c.text}`).join('
')}
`; } async sendReport( from: Date, to: Date, includeJson: boolean = false ): Promise { const reportExpenseData = await this.generateExpenses(from, to); const reportDayLogData = await this.generateDayLogs(from, to); const htmlContent = this.generateHtmlReport( reportExpenseData, reportDayLogData ); const attachments = []; if (includeJson) { const jsonExpenseData = JSON.stringify(reportExpenseData, null, 2); attachments.push({ filename: 'expenses.json', content: Buffer.from(jsonExpenseData).toString('base64'), contentType: 'application/json' }); const jsonDayLogData = JSON.stringify(reportDayLogData, null, 2); attachments.push({ filename: 'day-logs.json', content: Buffer.from(jsonDayLogData).toString('base64'), contentType: 'application/json' }); } try { const response = await this.resend.emails.send({ from: this.senderEmail, to: this.recipientEmail, subject: `Diary Report: ${from.toLocaleDateString('en-GB')} - ${to.toLocaleDateString('en-GB')}`, html: htmlContent, attachments }); if (response.error) { throw new Error('Failed to send email: No id returned from Resend'); } } catch (error) { console.error('Failed to send email:', error); throw new Error(`Email sending failed: ${error}`); } } }