feat: implement email notification worker with Mailgun integration
- Add MongoDB connection module for database access - Implement Mailgun email service for sending notifications - Add shareChecksum utility for generating secure share links - Implement three email sender functions: - Email verification requests (highest priority) - Rent due notifications (CET timezone) - Utility bills due notifications - Create main email worker with budget-based email sending - Add environment variables for configuration - Install dependencies: mongodb, mailgun.js, form-data - Update package.json description to reflect email worker purpose - Add .env.example with all required configuration The worker processes emails in priority order and respects a configurable budget to prevent overwhelming the mail server. All database operations are atomic and updates are performed immediately after each email send. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
26
email-worker/.env.example
Normal file
26
email-worker/.env.example
Normal file
@@ -0,0 +1,26 @@
|
||||
# Worker Configuration
|
||||
PULL_INTERVAL=60000
|
||||
EMAIL_BUDGET=10
|
||||
|
||||
# MongoDB Configuration
|
||||
MONGODB_URI=mongodb://localhost:27017/utility-bills
|
||||
|
||||
# Mailgun Configuration
|
||||
MAILGUN_API_KEY=your-mailgun-api-key-here
|
||||
MAILGUN_DOMAIN=rezije.app
|
||||
|
||||
# Security
|
||||
SHARE_LINK_SECRET=your-secret-key-here
|
||||
|
||||
# Server Configuration
|
||||
PORT=3000
|
||||
|
||||
# Logging
|
||||
DEBUG=worker:*,email:*,db:*
|
||||
|
||||
# Prometheus Metrics (optional)
|
||||
PROMETHEUS_APP_LABEL=email-worker
|
||||
PROMETHEUS_HISTOGRAM_BUCKETS=0.1, 0.5, 1, 5, 10
|
||||
|
||||
# Environment
|
||||
ENV=dev
|
||||
9788
email-worker/package-lock.json
generated
Normal file
9788
email-worker/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "email-worker",
|
||||
"version": "0.1.0",
|
||||
"description": "Background worker service with HTTP health monitoring and metrics collection",
|
||||
"description": "Email notification worker service for sending verification requests, rent due notices, and utility bills notifications",
|
||||
"main": "entry.ts",
|
||||
"scripts": {
|
||||
"start": "nodemon ./src/entry.ts",
|
||||
@@ -14,7 +14,10 @@
|
||||
"dependencies": {
|
||||
"debug": "^2.6.9",
|
||||
"express": "^4.18.2",
|
||||
"form-data": "^4.0.5",
|
||||
"http-errors": "^1.7.2",
|
||||
"mailgun.js": "^12.4.1",
|
||||
"mongodb": "^7.0.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"prom-client": "^14.0.1",
|
||||
"stoppable": "^1.1.0"
|
||||
@@ -25,6 +28,7 @@
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/http-errors": "^1.8.1",
|
||||
"@types/jest": "^29.2.5",
|
||||
"@types/mongodb": "^4.0.6",
|
||||
"@types/node": "^16.10.2",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/stoppable": "^1.1.1",
|
||||
@@ -46,4 +50,4 @@
|
||||
"@babel/preset-typescript"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
70
email-worker/src/emailWorker.ts
Normal file
70
email-worker/src/emailWorker.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { connectToDatabase, disconnectFromDatabase } from './lib/dbClient';
|
||||
import { sendVerificationRequests, sendRentDueNotifications, sendUtilityBillsNotifications } from './lib/emailSenders';
|
||||
import { createLogger } from './lib/logger';
|
||||
|
||||
const log = createLogger("worker:email");
|
||||
|
||||
/**
|
||||
* Email worker implementation
|
||||
*
|
||||
* Sends three types of emails in priority order:
|
||||
* 1. Email verification requests (highest priority)
|
||||
* 2. Rent due notifications
|
||||
* 3. Utility bills due notifications
|
||||
*
|
||||
* Uses a budget system to limit total emails sent per run.
|
||||
*/
|
||||
export const doWork = async () => {
|
||||
const startTime = Date.now();
|
||||
const emailBudget = parseInt(process.env.EMAIL_BUDGET || '10', 10);
|
||||
|
||||
log(`Starting email worker run with budget: ${emailBudget}`);
|
||||
|
||||
let remainingBudget = emailBudget;
|
||||
let totalSent = 0;
|
||||
|
||||
try {
|
||||
// Connect to database
|
||||
const db = await connectToDatabase();
|
||||
|
||||
// 1. Send verification requests (highest priority)
|
||||
const verificationsSent = await sendVerificationRequests(db, remainingBudget);
|
||||
totalSent += verificationsSent;
|
||||
remainingBudget -= verificationsSent;
|
||||
log(`Verification emails sent: ${verificationsSent}, remaining budget: ${remainingBudget}`);
|
||||
|
||||
// 2. Send rent due notifications
|
||||
if (remainingBudget > 0) {
|
||||
const rentSent = await sendRentDueNotifications(db, remainingBudget);
|
||||
totalSent += rentSent;
|
||||
remainingBudget -= rentSent;
|
||||
log(`Rent due emails sent: ${rentSent}, remaining budget: ${remainingBudget}`);
|
||||
}
|
||||
|
||||
// 3. Send utility bills notifications
|
||||
if (remainingBudget > 0) {
|
||||
const billsSent = await sendUtilityBillsNotifications(db, remainingBudget);
|
||||
totalSent += billsSent;
|
||||
remainingBudget -= billsSent;
|
||||
log(`Utility bills emails sent: ${billsSent}, remaining budget: ${remainingBudget}`);
|
||||
}
|
||||
|
||||
// Disconnect from database
|
||||
await disconnectFromDatabase();
|
||||
|
||||
const workDuration = Date.now() - startTime;
|
||||
log(`Email worker completed in ${workDuration}ms. Total emails sent: ${totalSent}`);
|
||||
|
||||
} catch (error) {
|
||||
log(`Email worker failed: ${error}`);
|
||||
|
||||
// Try to disconnect even on error
|
||||
try {
|
||||
await disconnectFromDatabase();
|
||||
} catch (disconnectError) {
|
||||
log(`Failed to disconnect from database: ${disconnectError}`);
|
||||
}
|
||||
|
||||
throw error; // Re-throw to mark work as failed
|
||||
}
|
||||
};
|
||||
53
email-worker/src/lib/dbClient.ts
Normal file
53
email-worker/src/lib/dbClient.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { MongoClient, Db } from 'mongodb';
|
||||
import { createLogger } from './logger';
|
||||
|
||||
const log = createLogger("db:client");
|
||||
|
||||
let client: MongoClient | null = null;
|
||||
let db: Db | null = null;
|
||||
|
||||
/**
|
||||
* Connect to MongoDB
|
||||
* @returns Database instance
|
||||
*/
|
||||
export async function connectToDatabase(): Promise<Db> {
|
||||
if (!process.env.MONGODB_URI) {
|
||||
throw new Error('MONGODB_URI environment variable is not set');
|
||||
}
|
||||
|
||||
if (db) {
|
||||
log('Reusing existing database connection');
|
||||
return db;
|
||||
}
|
||||
|
||||
log('Creating new database connection');
|
||||
client = new MongoClient(process.env.MONGODB_URI);
|
||||
await client.connect();
|
||||
db = client.db("utility-bills");
|
||||
|
||||
log('Connected to database');
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from MongoDB
|
||||
*/
|
||||
export async function disconnectFromDatabase(): Promise<void> {
|
||||
if (client) {
|
||||
log('Disconnecting from database');
|
||||
await client.close();
|
||||
client = null;
|
||||
db = null;
|
||||
log('Disconnected from database');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current database instance (must call connectToDatabase first)
|
||||
*/
|
||||
export function getDatabase(): Db {
|
||||
if (!db) {
|
||||
throw new Error('Database not connected. Call connectToDatabase() first.');
|
||||
}
|
||||
return db;
|
||||
}
|
||||
280
email-worker/src/lib/emailSenders.ts
Normal file
280
email-worker/src/lib/emailSenders.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { Db, ObjectId } from 'mongodb';
|
||||
import { BillingLocation, EmailStatus, UserSettings } from '../types/db-types';
|
||||
import { generateShareId } from './shareChecksum';
|
||||
import { sendEmail } from './mailgunService';
|
||||
import { createLogger } from './logger';
|
||||
|
||||
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 = `
|
||||
<p>Hello ${location.tenantName || 'there'}!</p>
|
||||
|
||||
<p>You have received this e-mail because your landlord <strong>${ownerName}</strong> wants to send you rent and utility bills invoices via rezije.app</p>
|
||||
|
||||
<p><strong>rezije.app</strong> is an online app which helps property owners to manage expenses related to properties they lease.</p>
|
||||
|
||||
<p>Before the app can start sending you rent due and utility bills emails we need your verification.</p>
|
||||
|
||||
<p>To verify that you want to receive these notifications please click on the following link: <a href="https://rezije.app/email/verify/${shareId}">Verify</a></p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>Thank you!</p>
|
||||
|
||||
<a href="https://rezije.app" target="_blank">rezije.app</a>
|
||||
`;
|
||||
|
||||
const success = await sendEmail({
|
||||
to: location.tenantEmail,
|
||||
subject: 'Please verify your e-mail address',
|
||||
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<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;
|
||||
}
|
||||
|
||||
const shareId = generateShareId(location._id.toString());
|
||||
|
||||
const html = `
|
||||
<p>Hello ${location.tenantName || 'there'}!</p>
|
||||
|
||||
<p>Your rent for the apartment ${location.name} is due today.</p>
|
||||
|
||||
<p>For details and payment options please click the following link: <a href="https://rezije.app/share/location/rent/${shareId}">Rent details</a></p>
|
||||
|
||||
<p>Thank you!</p>
|
||||
|
||||
<p><a href="https://rezije.app" target="_blank">rezije.app</a></p>
|
||||
|
||||
<p style="font-size:.7em">If you do no longer want to receive these notifications please click the following link: <a href="https://rezije.app/email/unsubscribe/${shareId}">Unsubscribe</a></p>
|
||||
`;
|
||||
|
||||
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('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;
|
||||
}
|
||||
|
||||
const shareId = generateShareId(location._id.toString());
|
||||
|
||||
const html = `
|
||||
<p>Hello ${location.tenantName || 'there'}!</p>
|
||||
|
||||
<p>Your utility bills for the apartment ${location.name} are due today.</p>
|
||||
|
||||
<p>For details and payment options please click the following link: <a href="https://rezije.app/share/location/bills/${shareId}">Utility Bills</a></p>
|
||||
|
||||
<p>Thank you!</p>
|
||||
|
||||
<p><a href="https://rezije.app" target="_blank">rezije.app</a></p>
|
||||
|
||||
<p style="font-size:.7em">If you do no longer want to receive these notifications please click the following link: <a href="https://rezije.app/email/unsubscribe/${shareId}">Unsubscribe</a></p>
|
||||
`;
|
||||
|
||||
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('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;
|
||||
}
|
||||
63
email-worker/src/lib/mailgunService.ts
Normal file
63
email-worker/src/lib/mailgunService.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import formData from 'form-data';
|
||||
import Mailgun from 'mailgun.js';
|
||||
import { createLogger } from './logger';
|
||||
|
||||
const log = createLogger("email:mailgun");
|
||||
|
||||
export interface EmailMessage {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
}
|
||||
|
||||
let mailgunClient: any = null;
|
||||
|
||||
/**
|
||||
* Initialize Mailgun client
|
||||
*/
|
||||
function getMailgunClient() {
|
||||
if (mailgunClient) {
|
||||
return mailgunClient;
|
||||
}
|
||||
|
||||
const apiKey = process.env.MAILGUN_API_KEY;
|
||||
const domain = process.env.MAILGUN_DOMAIN || 'rezije.app';
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('MAILGUN_API_KEY environment variable is not set');
|
||||
}
|
||||
|
||||
const mailgun = new Mailgun(formData);
|
||||
mailgunClient = mailgun.client({ username: 'api', key: apiKey });
|
||||
|
||||
return mailgunClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email using Mailgun
|
||||
* @param message Email message to send
|
||||
* @returns True if successful, false otherwise
|
||||
*/
|
||||
export async function sendEmail(message: EmailMessage): Promise<boolean> {
|
||||
try {
|
||||
const client = getMailgunClient();
|
||||
const domain = process.env.MAILGUN_DOMAIN || 'rezije.app';
|
||||
|
||||
const messageData = {
|
||||
from: 'rezije.app <noreply@rezije.app>',
|
||||
to: message.to,
|
||||
subject: message.subject,
|
||||
html: message.html
|
||||
};
|
||||
|
||||
log(`Sending email to ${message.to}: ${message.subject}`);
|
||||
|
||||
const response = await client.messages.create(domain, messageData);
|
||||
|
||||
log(`Email sent successfully to ${message.to}, ID: ${response.id}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log(`Failed to send email to ${message.to}: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
36
email-worker/src/lib/shareChecksum.ts
Normal file
36
email-worker/src/lib/shareChecksum.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* Checksum length in hex characters (16 chars = 64 bits of entropy)
|
||||
*/
|
||||
export const CHECKSUM_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* Generate share link checksum for location
|
||||
* Uses HMAC-SHA256 for cryptographic integrity
|
||||
*
|
||||
* SECURITY: Prevents location ID enumeration while allowing stateless validation
|
||||
*/
|
||||
export function generateShareChecksum(locationId: string): string {
|
||||
const secret = process.env.SHARE_LINK_SECRET;
|
||||
|
||||
if (!secret) {
|
||||
throw new Error('SHARE_LINK_SECRET environment variable not configured');
|
||||
}
|
||||
|
||||
return crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(locationId)
|
||||
.digest('hex')
|
||||
.substring(0, CHECKSUM_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate combined location ID with checksum appended
|
||||
* @param locationId - The MongoDB location ID (24 chars)
|
||||
* @returns Combined ID: locationId + checksum (40 chars total)
|
||||
*/
|
||||
export function generateShareId(locationId: string): string {
|
||||
const checksum = generateShareChecksum(locationId);
|
||||
return locationId + checksum;
|
||||
}
|
||||
54
email-worker/src/types/db-types.ts
Normal file
54
email-worker/src/types/db-types.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
export interface YearMonth {
|
||||
year: number;
|
||||
month: number;
|
||||
}
|
||||
|
||||
export enum EmailStatus {
|
||||
/** Email is not yet verified - recipient has not yet confirmed their email address */
|
||||
Unverified = "unverified",
|
||||
/** Email is not yet verified - a verification request has been sent */
|
||||
VerificationPending = "verification-pending",
|
||||
/** sending of verification email failed */
|
||||
VerificationFailed = "verification-failed",
|
||||
/** Email is verified and is in good standing: emails are being successfully delivered */
|
||||
Verified = "verified",
|
||||
/** Recepient has unsubscribed from receiving emails via link - no further emails will be sent */
|
||||
Unsubscribed = "unsubscribed"
|
||||
}
|
||||
|
||||
/** User settings data */
|
||||
export interface UserSettings {
|
||||
/** user's ID */
|
||||
userId: string;
|
||||
/** owner name */
|
||||
ownerName?: string | null;
|
||||
}
|
||||
|
||||
/** Billing location document from MongoDB */
|
||||
export interface BillingLocation {
|
||||
_id: ObjectId;
|
||||
/** user's ID */
|
||||
userId: string;
|
||||
/** name of the location */
|
||||
name: string;
|
||||
/** billing period year and month */
|
||||
yearMonth: YearMonth;
|
||||
/** (optional) tenant name */
|
||||
tenantName?: string | null;
|
||||
/** (optional) tenant email */
|
||||
tenantEmail?: string | null;
|
||||
/** (optional) tenant email status */
|
||||
tenantEmailStatus?: EmailStatus | null;
|
||||
/** (optional) whether to automatically notify tenant */
|
||||
billFwdEnabled?: boolean | null;
|
||||
/** (optional) bill forwarding status */
|
||||
billFwdStatus?: "pending" | "sent" | "failed" | null;
|
||||
/** (optional) whether to automatically send rent notification */
|
||||
rentDueNotificationEnabled?: boolean | null;
|
||||
/** (optional) day of month when rent is due (1-31) */
|
||||
rentDueDay?: number | null;
|
||||
/** (optional) when was the rent due notification sent */
|
||||
rentDueNotificationStatus?: "sent" | "failed" | null;
|
||||
}
|
||||
31
email-worker/src/types/environment.d.ts
vendored
31
email-worker/src/types/environment.d.ts
vendored
@@ -22,6 +22,37 @@ declare global {
|
||||
* @default "10000"
|
||||
* */
|
||||
PULL_INTERVAL:string
|
||||
/**
|
||||
* (required) MongoDB connection URI
|
||||
* */
|
||||
MONGODB_URI: string
|
||||
/**
|
||||
* (required) Mailgun API key for sending emails
|
||||
* */
|
||||
MAILGUN_API_KEY: string
|
||||
/**
|
||||
* (optional) Mailgun domain
|
||||
* @default "rezije.app"
|
||||
* */
|
||||
MAILGUN_DOMAIN?: string
|
||||
/**
|
||||
* (required) Secret key for generating share link checksums
|
||||
* */
|
||||
SHARE_LINK_SECRET: string
|
||||
/**
|
||||
* (optional) Maximum number of emails to send per worker run
|
||||
* @default "10"
|
||||
* */
|
||||
EMAIL_BUDGET?: string
|
||||
/**
|
||||
* (optional) HTTP server port
|
||||
* @default "3000"
|
||||
* */
|
||||
PORT?: string
|
||||
/**
|
||||
* (optional) Debug namespaces for console logging
|
||||
* */
|
||||
DEBUG?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { failedRequestCounter, requestDurationHistogram, successfulRequestCounte
|
||||
import { coalesce } from "./lib/initTools";
|
||||
import { createLogger } from "./lib/logger";
|
||||
import { serializeError } from "./lib/serializeError";
|
||||
import { doWork } from "./exampleWorker";
|
||||
import { doWork } from "./emailWorker";
|
||||
|
||||
/** time between two pull operations */
|
||||
const PULL_INTERVAL = parseInt(coalesce(process.env.PULL_INTERVAL, "10000"));
|
||||
|
||||
Reference in New Issue
Block a user