import { Db, ObjectId } from 'mongodb'; import { BillingLocation, BillsNotificationStatus, EmailStatus, RentNotificationStatus, 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 { 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('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') .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('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 { 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('lokacije') .find({ 'yearMonth.year': currentYear, 'yearMonth.month': currentMonth, 'tenantEmailStatus': EmailStatus.Verified, 'rentNotificationEnabled': true, 'rentDueDay': currentDay, $or: [ { 'rentNotificationStatus': { $exists: false } }, { 'rentNotificationStatus': 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') .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 ? RentNotificationStatus.Sent : RentNotificationStatus.Failed; await db.collection('lokacije').updateOne( { _id: location._id }, { $set: { rentNotificationStatus: 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 { 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('lokacije') .find({ 'yearMonth.year': currentYear, 'yearMonth.month': currentMonth, 'tenantEmailStatus': EmailStatus.Verified, 'billsNotificationEnabled': true, 'billsNotificationStatus': BillsNotificationStatus.Scheduled }) .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') .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 ? BillsNotificationStatus.Sent : BillsNotificationStatus.Failed; await db.collection('lokacije').updateOne( { _id: location._id }, { $set: { billsNotificationStatus: 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; }