Files
evidencija-rezija/email-worker/src/lib/emailSenders.ts
Knee Cola d7abd99448 feat: implement language-based email template selection
Update email worker to select templates based on BillingLocation.tenantEmailLanguage:
- Change default language from 'en' to 'hr' in emailTemplates.ts
- Pass language parameter (location.tenantEmailLanguage || 'hr') to all loadAndRender calls
- Applies to email validation, rent due, and utility bills notifications

Email templates are now automatically selected based on tenant's language preference,
defaulting to Croatian when not specified.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-31 11:07:39 +01:00

285 lines
9.3 KiB
TypeScript

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");
/**
* Send email verification requests
* @param db Database instance
* @param budget Remaining email budget
* @returns Number of emails sent
*/
export async function sendVerificationRequests(db: Db, budget: number): Promise<number> {
if (budget <= 0) {
log('Budget exhausted, skipping verification requests');
return 0;
}
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1; // JavaScript months are 0-indexed
log(`Fetching locations for verification: year=${currentYear}, month=${currentMonth}`);
const locations = await db.collection<BillingLocation>('lokacije')
.find({
'yearMonth.year': currentYear,
'yearMonth.month': currentMonth,
'tenantEmailStatus': EmailStatus.Unverified
})
.toArray();
log(`Found ${locations.length} locations needing verification`);
let sentCount = 0;
for (const location of locations) {
if (budget <= 0) {
log('Budget exhausted during verification sending');
break;
}
if (!location.tenantEmail) {
log(`Skipping location ${location._id}: no tenant email`);
continue;
}
// Fetch user settings
const userSettings = await db.collection<UserSettings>('userSettings')
.findOne({ userId: location.userId });
const ownerName = userSettings?.ownerName || '';
const shareId = generateShareId(location._id.toString());
const html = loadAndRender('email-validation', {
'location.tenantName': location.tenantName || 'there',
'ownerName': ownerName,
'location.name': location.name,
'shareId': shareId
}, location.tenantEmailLanguage || 'hr');
const success = await sendEmail({
to: location.tenantEmail,
subject: `${ownerName} has invited you to rezije.app`,
html
});
// Update location status
const newStatus = success ? EmailStatus.VerificationPending : EmailStatus.VerificationFailed;
await db.collection<BillingLocation>('lokacije').updateOne(
{ _id: location._id },
{ $set: { tenantEmailStatus: newStatus } }
);
if (success) {
sentCount++;
log(`Verification email sent to ${location.tenantEmail}`);
} else {
log(`Failed to send verification email to ${location.tenantEmail}`);
}
budget--;
}
log(`Sent ${sentCount} verification emails`);
return sentCount;
}
/**
* Send rent due notifications
* @param db Database instance
* @param budget Remaining email budget
* @returns Number of emails sent
*/
export async function sendRentDueNotifications(db: Db, budget: number): Promise<number> {
if (budget <= 0) {
log('Budget exhausted, skipping rent due notifications');
return 0;
}
const now = new Date();
// Use CET timezone
const cetDate = new Date(now.toLocaleString('en-US', { timeZone: 'Europe/Belgrade' }));
const currentYear = cetDate.getFullYear();
const currentMonth = cetDate.getMonth() + 1;
const currentDay = cetDate.getDate();
log(`Fetching locations for rent due: year=${currentYear}, month=${currentMonth}, day=${currentDay}`);
const locations = await db.collection<BillingLocation>('lokacije')
.find({
'yearMonth.year': currentYear,
'yearMonth.month': currentMonth,
'tenantEmailStatus': EmailStatus.Verified,
'rentDueNotificationEnabled': true,
'rentDueDay': currentDay,
$or: [
{ 'rentDueNotificationStatus': { $exists: false } },
{ 'rentDueNotificationStatus': null }
]
})
.toArray();
log(`Found ${locations.length} locations needing rent due notifications`);
let sentCount = 0;
for (const location of locations) {
if (budget <= 0) {
log('Budget exhausted during rent due sending');
break;
}
if (!location.tenantEmail) {
log(`Skipping location ${location._id}: no tenant email`);
continue;
}
// Fetch user settings
const userSettings = await db.collection<UserSettings>('userSettings')
.findOne({ userId: location.userId });
const ownerName = userSettings?.ownerName || '';
const shareId = generateShareId(location._id.toString());
// Format rent due date
const rentDueDate = `${location.yearMonth.month}/${location.rentDueDay}/${location.yearMonth.year}`;
// Format rent amount (convert from cents to display format)
const rentAmount = location.rentAmount ? (location.rentAmount).toFixed(2) : '0.00';
const currency = userSettings?.currency || 'EUR';
const html = loadAndRender('rent-due', {
'location.tenantName': location.tenantName || 'there',
'location.name': location.name,
'rentDueDate': rentDueDate,
'rentAmount': rentAmount,
'currency': currency,
'ownerName': ownerName,
'shareId': shareId
}, location.tenantEmailLanguage || 'hr');
const success = await sendEmail({
to: location.tenantEmail,
subject: `Rent due for ${location.tenantName || 'your apartment'}`,
html
});
// Update location status
const newStatus = success ? 'sent' : 'failed';
await db.collection<BillingLocation>('lokacije').updateOne(
{ _id: location._id },
{ $set: { rentDueNotificationStatus: newStatus } }
);
if (success) {
sentCount++;
log(`Rent due notification sent to ${location.tenantEmail}`);
} else {
log(`Failed to send rent due notification to ${location.tenantEmail}`);
}
budget--;
}
log(`Sent ${sentCount} rent due notifications`);
return sentCount;
}
/**
* Send utility bills due notifications
* @param db Database instance
* @param budget Remaining email budget
* @returns Number of emails sent
*/
export async function sendUtilityBillsNotifications(db: Db, budget: number): Promise<number> {
if (budget <= 0) {
log('Budget exhausted, skipping utility bills notifications');
return 0;
}
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1;
log(`Fetching locations for utility bills: year=${currentYear}, month=${currentMonth}`);
const locations = await db.collection<BillingLocation>('lokacije')
.find({
'yearMonth.year': currentYear,
'yearMonth.month': currentMonth,
'tenantEmailStatus': EmailStatus.Verified,
'billFwdEnabled': true,
'billFwdStatus': 'pending'
})
.toArray();
log(`Found ${locations.length} locations needing utility bills notifications`);
let sentCount = 0;
for (const location of locations) {
if (budget <= 0) {
log('Budget exhausted during utility bills sending');
break;
}
if (!location.tenantEmail) {
log(`Skipping location ${location._id}: no tenant email`);
continue;
}
// Fetch user settings
const userSettings = await db.collection<UserSettings>('userSettings')
.findOne({ userId: location.userId });
const ownerName = userSettings?.ownerName || '';
const shareId = generateShareId(location._id.toString());
// 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';
const html = loadAndRender('util-bills-due', {
'location.tenantName': location.tenantName || 'there',
'location.name': location.name,
'totalAmount': totalAmount,
'currency': currency,
'ownerName': ownerName,
'shareId': shareId
}, location.tenantEmailLanguage || 'hr');
const success = await sendEmail({
to: location.tenantEmail,
subject: `Utility bills due for ${location.tenantName || 'your apartment'}`,
html
});
// Update location status
const newStatus = success ? 'sent' : 'failed';
await db.collection<BillingLocation>('lokacije').updateOne(
{ _id: location._id },
{ $set: { billFwdStatus: newStatus } }
);
if (success) {
sentCount++;
log(`Utility bills notification sent to ${location.tenantEmail}`);
} else {
log(`Failed to send utility bills notification to ${location.tenantEmail}`);
}
budget--;
}
log(`Sent ${sentCount} utility bills notifications`);
return sentCount;
}