feat: refactor email-worker to use HTML email templates

Replace inline HTML in email notifications with professional HTML templates
for better email client compatibility and consistent branding.

Changes:
- Created emailTemplates.ts utility for loading and rendering templates
  - Template caching for performance
  - Variable substitution with ${variable} syntax
  - Warning logging for unreplaced variables

- Updated sendVerificationRequests to use email-validation template
  - Variables: location.tenantName, ownerName, location.name, shareId

- Updated sendRentDueNotifications to use rent-due template
  - Fetches user settings for owner name and currency
  - Calculates rent due date from yearMonth and rentDueDay
  - Formats rent amount (converts cents to display format)
  - Variables: location.tenantName, location.name, rentDueDate,
    rentAmount, currency, ownerName, shareId

- Updated sendUtilityBillsNotifications to use util-bills-due template
  - Calculates total amount from all bills
  - Fetches user settings for owner name and currency
  - Variables: location.tenantName, location.name, totalAmount,
    currency, ownerName, shareId

- Fixed ObjectId type mismatches in MongoDB operations

All emails now feature:
- Responsive 3-column layout
- rezije.app branding with logo
- Professional typography and spacing
- Unsubscribe links
- Email client compatible table-based layouts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Knee Cola
2025-12-30 18:37:34 +01:00
parent 3c34627e7e
commit 767dda6355
2 changed files with 135 additions and 43 deletions

View File

@@ -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<string, string>();
/**
* 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);
}