diff --git a/email-worker/src/lib/emailSenders.ts b/email-worker/src/lib/emailSenders.ts index cf75242..22eb70d 100644 --- a/email-worker/src/lib/emailSenders.ts +++ b/email-worker/src/lib/emailSenders.ts @@ -2,6 +2,7 @@ import { Db, ObjectId } from 'mongodb'; import { BillingLocation, EmailStatus, UserSettings, generateShareId } from '@evidencija-rezija/shared-code'; import { sendEmail } from './mailgunService'; import { createLogger } from './logger'; +import { loadAndRender } from './emailTemplates'; const log = createLogger("email:senders"); @@ -53,23 +54,12 @@ export async function sendVerificationRequests(db: Db, budget: number): Promise< const ownerName = userSettings?.ownerName || ''; const shareId = generateShareId(location._id.toString()); - const html = ` -

Hello ${location.tenantName || 'there'}!

- -

You have received this e-mail because your landlord ${ownerName} wants to send you rent and utility bills invoices for ${location.name} via rezije.app

- -

rezije.app is an online app which helps property owners to manage expenses related to properties they lease.

- -

Before the app can start sending you rent due and utility bills emails we need your verification.

- -

To verify that you want to receive these notifications please click on the following link: Verify

- -

You can ignore this email if you don't want to receive notifications. You can also unsubscribe at any time using the link included in every notification email you receive.

- -

Thank you!

- -rezije.app - `; + const html = loadAndRender('email-validation', { + 'location.tenantName': location.tenantName || 'there', + 'ownerName': ownerName, + 'location.name': location.name, + 'shareId': shareId + }); const success = await sendEmail({ to: location.tenantEmail, @@ -80,7 +70,7 @@ export async function sendVerificationRequests(db: Db, budget: number): Promise< // Update location status const newStatus = success ? EmailStatus.VerificationPending : EmailStatus.VerificationFailed; await db.collection('lokacije').updateOne( - { _id: location._id }, + { _id: new ObjectId(location._id as any) }, { $set: { tenantEmailStatus: newStatus } } ); @@ -149,21 +139,29 @@ export async function sendRentDueNotifications(db: Db, budget: number): Promise< continue; } + // Fetch user settings + const userSettings = await db.collection('userSettings') + .findOne({ userId: location.userId }); + + const ownerName = userSettings?.ownerName || ''; const shareId = generateShareId(location._id.toString()); - const html = ` -

Hello ${location.tenantName || 'there'}!

+ // Format rent due date + const rentDueDate = `${location.yearMonth.month}/${location.rentDueDay}/${location.yearMonth.year}`; -

Your rent for the apartment ${location.name} is due today.

+ // Format rent amount (convert from cents to display format) + const rentAmount = location.rentAmount ? (location.rentAmount / 100).toFixed(2) : '0.00'; + const currency = userSettings?.currency || 'EUR'; -

For details and payment options please click the following link: Rent details

- -

Thank you!

- -

rezije.app

- -

If you do no longer want to receive these notifications please click the following link: Unsubscribe

- `; + const html = loadAndRender('rent-due', { + 'location.tenantName': location.tenantName || 'there', + 'location.name': location.name, + 'rentDueDate': rentDueDate, + 'rentAmount': rentAmount, + 'currency': currency, + 'ownerName': ownerName, + 'shareId': shareId + }); const success = await sendEmail({ to: location.tenantEmail, @@ -174,7 +172,7 @@ export async function sendRentDueNotifications(db: Db, budget: number): Promise< // Update location status const newStatus = success ? 'sent' : 'failed'; await db.collection('lokacije').updateOne( - { _id: location._id }, + { _id: new ObjectId(location._id as any) }, { $set: { rentDueNotificationStatus: newStatus } } ); @@ -235,21 +233,28 @@ export async function sendUtilityBillsNotifications(db: Db, budget: number): Pro continue; } + // Fetch user settings + const userSettings = await db.collection('userSettings') + .findOne({ userId: location.userId }); + + const ownerName = userSettings?.ownerName || ''; const shareId = generateShareId(location._id.toString()); - const html = ` -

Hello ${location.tenantName || 'there'}!

+ // Calculate total amount from all bills + const totalAmountCents = (location.bills || []).reduce((sum, bill) => { + return sum + (bill.payedAmount || 0); + }, 0); + const totalAmount = (totalAmountCents / 100).toFixed(2); + const currency = userSettings?.currency || 'EUR'; -

Your utility bills for the apartment ${location.name} are due today.

- -

For details and payment options please click the following link: Utility Bills

- -

Thank you!

- -

rezije.app

- -

If you do no longer want to receive these notifications please click the following link: Unsubscribe

- `; + const html = loadAndRender('util-bills-due', { + 'location.tenantName': location.tenantName || 'there', + 'location.name': location.name, + 'totalAmount': totalAmount, + 'currency': currency, + 'ownerName': ownerName, + 'shareId': shareId + }); const success = await sendEmail({ to: location.tenantEmail, @@ -260,7 +265,7 @@ export async function sendUtilityBillsNotifications(db: Db, budget: number): Pro // Update location status const newStatus = success ? 'sent' : 'failed'; await db.collection('lokacije').updateOne( - { _id: location._id }, + { _id: new ObjectId(location._id as any) }, { $set: { billFwdStatus: newStatus } } ); diff --git a/email-worker/src/lib/emailTemplates.ts b/email-worker/src/lib/emailTemplates.ts new file mode 100644 index 0000000..a37ca4b --- /dev/null +++ b/email-worker/src/lib/emailTemplates.ts @@ -0,0 +1,87 @@ +import fs from 'fs'; +import path from 'path'; +import { createLogger } from './logger'; + +const log = createLogger('email:templates'); + +// Cache for loaded templates +const templateCache = new Map(); + +/** + * Template variable type for type-safe template rendering + */ +export type TemplateVariables = { + [key: string]: string | number | undefined; +}; + +/** + * Load an email template from the templates directory + * @param templateName Name of the template file (without extension) + * @param language Language code (default: 'en') + * @returns Template content as string + */ +export function loadTemplate(templateName: string, language: string = 'en'): string { + const cacheKey = `${templateName}--${language}`; + + // Check cache first + if (templateCache.has(cacheKey)) { + log(`Using cached template: ${cacheKey}`); + return templateCache.get(cacheKey)!; + } + + // Construct template file path + const templateFileName = `email-template--${templateName}--${language}.html`; + const templatePath = path.join(__dirname, '../../email-templates', templateFileName); + + try { + const content = fs.readFileSync(templatePath, 'utf-8'); + templateCache.set(cacheKey, content); + log(`Loaded template: ${templateFileName}`); + return content; + } catch (error) { + log(`Failed to load template ${templateFileName}: ${error}`); + throw new Error(`Template not found: ${templateFileName}`); + } +} + +/** + * Render a template by replacing variables + * @param template Template content + * @param variables Object with variable values + * @returns Rendered HTML string + */ +export function renderTemplate(template: string, variables: TemplateVariables): string { + let rendered = template; + + // Replace all ${variable} occurrences + for (const [key, value] of Object.entries(variables)) { + if (value !== undefined) { + const regex = new RegExp(`\\$\\{${key}\\}`, 'g'); + rendered = rendered.replace(regex, String(value)); + } + } + + // Log warning if there are unreplaced variables + const unreplacedMatches = rendered.match(/\$\{[^}]+\}/g); + if (unreplacedMatches) { + log(`Warning: Unreplaced variables in template: ${unreplacedMatches.join(', ')}`); + } + + return rendered; +} + +/** + * Load and render an email template in one step + * @param templateName Name of the template file (without extension) + * @param variables Object with variable values + * @param language Language code (default: 'en') + * @returns Rendered HTML string + */ +export function loadAndRender( + templateName: string, + variables: TemplateVariables, + language: string = 'en' +): string { + const template = loadTemplate(templateName, language); + return renderTemplate(template, variables); +}