Implement dues reminder system with monthly payment cycle
Build And Push Image / docker (push) Failing after 1m10s
Details
Build And Push Image / docker (push) Failing after 1m10s
Details
- Add API endpoint and email templates for dues reminders - Change due date calculation from yearly to monthly billing - Add visual status indicators for overdue and due-soon members - Enhance member cards with status stripes and styling
This commit is contained in:
parent
7784fab23f
commit
888059a612
|
|
@ -254,10 +254,10 @@ watch(duesPaid, (newValue) => {
|
||||||
} else {
|
} else {
|
||||||
form.value['Membership Date Paid'] = '';
|
form.value['Membership Date Paid'] = '';
|
||||||
if (!form.value['Payment Due Date']) {
|
if (!form.value['Payment Due Date']) {
|
||||||
// Set due date to one year from member since date or today
|
// Set due date to one month from member since date or today
|
||||||
const memberSince = form.value['Member Since'] || new Date().toISOString().split('T')[0];
|
const memberSince = form.value['Member Since'] || new Date().toISOString().split('T')[0];
|
||||||
const dueDate = new Date(memberSince);
|
const dueDate = new Date(memberSince);
|
||||||
dueDate.setFullYear(dueDate.getFullYear() + 1);
|
dueDate.setMonth(dueDate.getMonth() + 1);
|
||||||
form.value['Payment Due Date'] = dueDate.toISOString().split('T')[0];
|
form.value['Payment Due Date'] = dueDate.toISOString().split('T')[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -415,10 +415,10 @@ watch(duesPaid, (newValue) => {
|
||||||
} else {
|
} else {
|
||||||
form.value.membership_date_paid = '';
|
form.value.membership_date_paid = '';
|
||||||
if (!form.value.payment_due_date) {
|
if (!form.value.payment_due_date) {
|
||||||
// Set due date to one year from member since date or today
|
// Set due date to one month from member since date or today
|
||||||
const memberSince = form.value.member_since || new Date().toISOString().split('T')[0];
|
const memberSince = form.value.member_since || new Date().toISOString().split('T')[0];
|
||||||
const dueDate = new Date(memberSince);
|
const dueDate = new Date(memberSince);
|
||||||
dueDate.setFullYear(dueDate.getFullYear() + 1);
|
dueDate.setMonth(dueDate.getMonth() + 1);
|
||||||
form.value.payment_due_date = dueDate.toISOString().split('T')[0];
|
form.value.payment_due_date = dueDate.toISOString().split('T')[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<v-card
|
<v-card
|
||||||
class="member-card"
|
class="member-card"
|
||||||
:class="{ 'member-card--inactive': !isActive }"
|
:class="{
|
||||||
|
'member-card--inactive': !isActive,
|
||||||
|
'member-card--overdue': isOverdue,
|
||||||
|
'member-card--due-soon': isDuesComingDue
|
||||||
|
}"
|
||||||
elevation="2"
|
elevation="2"
|
||||||
@click="$emit('view', member)"
|
@click="$emit('view', member)"
|
||||||
>
|
>
|
||||||
|
<!-- Status Stripe -->
|
||||||
|
<div
|
||||||
|
v-if="isOverdue || isDuesComingDue"
|
||||||
|
class="status-stripe"
|
||||||
|
:class="{
|
||||||
|
'status-stripe--overdue': isOverdue,
|
||||||
|
'status-stripe--due-soon': isDuesComingDue
|
||||||
|
}"
|
||||||
|
/>
|
||||||
<!-- Member Status Badge -->
|
<!-- Member Status Badge -->
|
||||||
<div class="member-status-badge">
|
<div class="member-status-badge">
|
||||||
<v-chip
|
<v-chip
|
||||||
|
|
@ -547,4 +560,33 @@ const formatDate = (dateString: string): string => {
|
||||||
.text-error {
|
.text-error {
|
||||||
color: rgb(var(--v-theme-error)) !important;
|
color: rgb(var(--v-theme-error)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Status Stripe Styles */
|
||||||
|
.status-stripe {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 4px;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-stripe--overdue {
|
||||||
|
background: linear-gradient(180deg, #f44336 0%, #d32f2f 100%);
|
||||||
|
box-shadow: 2px 0 8px rgba(244, 67, 54, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-stripe--due-soon {
|
||||||
|
background: linear-gradient(180deg, #ff9800 0%, #f57c00 100%);
|
||||||
|
box-shadow: 2px 0 8px rgba(255, 152, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card--overdue {
|
||||||
|
border-left: 4px solid #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card--due-soon {
|
||||||
|
border-left: 4px solid #ff9800;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
import { sendEmail } from '~/server/utils/email';
|
||||||
|
import { createNocoDBClient } from '~/server/utils/nocodb';
|
||||||
|
import { calculateDaysUntilDue, calculateOverdueDays, getNextDuesDate, isPaymentOverOneYear, isDuesActuallyCurrent } from '~/utils/dues-calculations';
|
||||||
|
import { getRegistrationConfig } from '~/server/utils/admin-config';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const memberId = getRouterParam(event, 'id');
|
||||||
|
const { reminderType } = await readBody(event);
|
||||||
|
|
||||||
|
if (!memberId) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Member ID is required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['due-soon', 'overdue'].includes(reminderType)) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Invalid reminder type. Must be "due-soon" or "overdue"'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get member data
|
||||||
|
const nocodb = createNocoDBClient();
|
||||||
|
const member = await nocodb.findOne('nc_members', memberId);
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: 'Member not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!member.email) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Member does not have an email address'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current admin configuration for payment details
|
||||||
|
const registrationConfig = getRegistrationConfig();
|
||||||
|
|
||||||
|
// Calculate dues status
|
||||||
|
const memberName = member.FullName || `${member.first_name} ${member.last_name}`.trim() || 'Member';
|
||||||
|
const nextDueDate = getNextDuesDate(member);
|
||||||
|
const membershipFee = `€${registrationConfig.membershipFee}`;
|
||||||
|
const paymentIban = registrationConfig.iban;
|
||||||
|
const accountHolder = registrationConfig.accountHolder;
|
||||||
|
const portalUrl = `${process.env.NUXT_PUBLIC_DOMAIN || 'https://monacousa.org'}/dashboard`;
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
|
// Format dates
|
||||||
|
const formatDate = (dateString: string | Date | null | undefined): string => {
|
||||||
|
if (!dateString) return '';
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return String(dateString);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let emailData;
|
||||||
|
let subject;
|
||||||
|
let template;
|
||||||
|
|
||||||
|
if (reminderType === 'due-soon') {
|
||||||
|
// Check if this member actually has dues coming due
|
||||||
|
const daysUntilDue = calculateDaysUntilDue(member);
|
||||||
|
|
||||||
|
if (!daysUntilDue || daysUntilDue.daysUntilDue <= 0) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Member does not have dues coming due soon'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
subject = `🏎️ MonacoUSA - Annual Dues Due in ${daysUntilDue.daysUntilDue} Days`;
|
||||||
|
template = 'dues-due-soon';
|
||||||
|
|
||||||
|
emailData = {
|
||||||
|
memberName,
|
||||||
|
memberId: member.member_id || member.Id,
|
||||||
|
memberEmail: member.email,
|
||||||
|
memberSince: formatDate(member.member_since),
|
||||||
|
amount: membershipFee,
|
||||||
|
dueDate: formatDate(daysUntilDue.nextDueDate),
|
||||||
|
daysUntilDue: daysUntilDue.daysUntilDue,
|
||||||
|
paymentIban,
|
||||||
|
accountHolder,
|
||||||
|
portalUrl,
|
||||||
|
currentYear
|
||||||
|
};
|
||||||
|
|
||||||
|
} else if (reminderType === 'overdue') {
|
||||||
|
// Check if this member is actually overdue
|
||||||
|
const isDuesCurrent = isDuesActuallyCurrent(member);
|
||||||
|
|
||||||
|
if (isDuesCurrent) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Member does not have overdue dues'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const overdueDays = calculateOverdueDays(member);
|
||||||
|
const originalDueDate = member.payment_due_date || nextDueDate;
|
||||||
|
|
||||||
|
subject = `🚨 MonacoUSA - URGENT: Overdue Dues Notice (${overdueDays} days overdue)`;
|
||||||
|
template = 'dues-overdue';
|
||||||
|
|
||||||
|
emailData = {
|
||||||
|
memberName,
|
||||||
|
memberId: member.member_id || member.Id,
|
||||||
|
memberEmail: member.email,
|
||||||
|
memberSince: formatDate(member.member_since),
|
||||||
|
amount: membershipFee,
|
||||||
|
originalDueDate: formatDate(originalDueDate),
|
||||||
|
daysOverdue: overdueDays,
|
||||||
|
paymentIban,
|
||||||
|
accountHolder,
|
||||||
|
portalUrl,
|
||||||
|
currentYear
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the email
|
||||||
|
console.log(`[Dues Reminder] Sending ${reminderType} reminder to ${member.email}`, {
|
||||||
|
memberId,
|
||||||
|
memberName,
|
||||||
|
reminderType,
|
||||||
|
emailData
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendEmail({
|
||||||
|
to: member.email,
|
||||||
|
subject,
|
||||||
|
template,
|
||||||
|
data: emailData
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log the reminder sent (could store in database for tracking)
|
||||||
|
console.log(`[Dues Reminder] Successfully sent ${reminderType} reminder to ${memberName} (${member.email})`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `${reminderType === 'due-soon' ? 'Due soon' : 'Overdue'} reminder sent successfully`,
|
||||||
|
data: {
|
||||||
|
memberId,
|
||||||
|
memberName,
|
||||||
|
memberEmail: member.email,
|
||||||
|
reminderType,
|
||||||
|
sentAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Dues Reminder] Error sending reminder:', error);
|
||||||
|
|
||||||
|
// Handle specific error cases
|
||||||
|
if (error.statusCode) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: error.message || 'Failed to send dues reminder'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dues Payment Reminder - MonacoUSA</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #a31515 0%, #d32f2f 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 30px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
max-width: 200px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.alert-warning {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
color: #856404;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.member-info {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.amount-due {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #a31515;
|
||||||
|
text-align: center;
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
background: linear-gradient(135deg, #a31515 0%, #d32f2f 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 30px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background: linear-gradient(135deg, #8a1212 0%, #b71c1c 100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>🏎️ MonacoUSA</h1>
|
||||||
|
<h2>Annual Dues Payment Reminder</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="alert-warning">
|
||||||
|
<strong>⏰ Payment Due Soon!</strong> Your annual membership dues will be due in {{daysUntilDue}} days.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Dear {{memberName}},</p>
|
||||||
|
|
||||||
|
<p>This is a friendly reminder that your annual MonacoUSA membership dues will be due soon.</p>
|
||||||
|
|
||||||
|
<div class="member-info">
|
||||||
|
<h3>Member Information</h3>
|
||||||
|
<p><strong>Name:</strong> {{memberName}}</p>
|
||||||
|
<p><strong>Member ID:</strong> {{memberId}}</p>
|
||||||
|
<p><strong>Email:</strong> {{memberEmail}}</p>
|
||||||
|
<p><strong>Member Since:</strong> {{memberSince}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="amount-due">
|
||||||
|
Annual Dues: {{amount}}
|
||||||
|
<br>
|
||||||
|
<small style="font-size: 16px; color: #666;">Due Date: {{dueDate}}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>To ensure uninterrupted access to MonacoUSA benefits and avoid any service disruption, please submit your payment before the due date.</p>
|
||||||
|
|
||||||
|
<div class="member-info">
|
||||||
|
<h3>Payment Information</h3>
|
||||||
|
<p>Membership fee is {{amount}}/yr per person, paid using the IBAN below:</p>
|
||||||
|
<p><strong>IBAN Euro:</strong> {{paymentIban}}</p>
|
||||||
|
<p><strong>Account Holder:</strong> {{accountHolder}}</p>
|
||||||
|
<p><strong>Payment Reference:</strong> {{memberId}} - Annual Dues</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><strong>Important:</strong> Please include your member ID ({{memberId}}) in the payment reference to ensure proper credit to your account.</p>
|
||||||
|
|
||||||
|
<p>If you have already made your payment, please disregard this reminder. If you have any questions about your membership or payment, please contact us:</p>
|
||||||
|
|
||||||
|
<p><strong>Contact Information:</strong></p>
|
||||||
|
<p>📧 Email: info@monacousa.org</p>
|
||||||
|
|
||||||
|
<p>Thank you for your continued membership with MonacoUSA!</p>
|
||||||
|
|
||||||
|
<p>Best regards,<br>
|
||||||
|
The MonacoUSA Team</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>This is an automated message from MonacoUSA. Please do not reply to this email.</p>
|
||||||
|
<p>© {{currentYear}} MonacoUSA. All rights reserved.</p>
|
||||||
|
<p>If you no longer wish to receive these reminders, please contact us at info@monacousa.org</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Overdue Dues Notice - MonacoUSA</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #d32f2f 0%, #f44336 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 30px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
max-width: 200px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.alert-danger {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.member-info {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.amount-due {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #d32f2f;
|
||||||
|
text-align: center;
|
||||||
|
background: #ffebee;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border: 2px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
.urgency-notice {
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
color: #856404;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
background: linear-gradient(135deg, #d32f2f 0%, #f44336 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 15px 35px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background: linear-gradient(135deg, #b71c1c 0%, #d32f2f 100%);
|
||||||
|
}
|
||||||
|
.consequences {
|
||||||
|
background: #ffebee;
|
||||||
|
border-left: 4px solid #f44336;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>🏎️ MonacoUSA</h1>
|
||||||
|
<h2>⚠️ Overdue Dues Notice</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="alert-danger">
|
||||||
|
<strong>🚨 URGENT: Payment Overdue!</strong> Your annual membership dues are now {{daysOverdue}} days overdue.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Dear {{memberName}},</p>
|
||||||
|
|
||||||
|
<p>This is an urgent notice that your annual MonacoUSA membership dues are now overdue and require immediate payment to avoid service interruption.</p>
|
||||||
|
|
||||||
|
<div class="member-info">
|
||||||
|
<h3>Member Information</h3>
|
||||||
|
<p><strong>Name:</strong> {{memberName}}</p>
|
||||||
|
<p><strong>Member ID:</strong> {{memberId}}</p>
|
||||||
|
<p><strong>Email:</strong> {{memberEmail}}</p>
|
||||||
|
<p><strong>Member Since:</strong> {{memberSince}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="amount-due">
|
||||||
|
Overdue Amount: {{amount}}
|
||||||
|
<br>
|
||||||
|
<small style="font-size: 16px; color: #666;">Original Due Date: {{originalDueDate}}</small>
|
||||||
|
<br>
|
||||||
|
<small style="font-size: 14px; color: #d32f2f;">{{daysOverdue}} days overdue</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="member-info">
|
||||||
|
<h3>Payment Information</h3>
|
||||||
|
<p>Membership fee is {{amount}}/yr per person, paid using the IBAN below:</p>
|
||||||
|
<p><strong>IBAN Euro:</strong> {{paymentIban}}</p>
|
||||||
|
<p><strong>Account Holder:</strong> {{accountHolder}}</p>
|
||||||
|
<p><strong>Payment Reference:</strong> {{memberId}} - Annual Dues</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><strong>Important:</strong> Please include your member ID ({{memberId}}) in the payment reference to ensure proper credit to your account.</p>
|
||||||
|
|
||||||
|
<div class="consequences">
|
||||||
|
<p><strong>Important:</strong> Failure of payment will lead to a revoking of MonacoUSA Member Privileges.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>If you have already submitted payment, please forward your payment confirmation to info@monacousa.org to update your account status.</p>
|
||||||
|
|
||||||
|
<p><strong>Questions or Concerns?</strong> Contact us:</p>
|
||||||
|
<p>📧 Email: info@monacousa.org</p>
|
||||||
|
|
||||||
|
<p>We value your membership and hope to resolve this matter promptly.</p>
|
||||||
|
|
||||||
|
<p>Best regards,<br>
|
||||||
|
The MonacoUSA Membership Team</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>This is an automated notice from MonacoUSA. Please do not reply to this email.</p>
|
||||||
|
<p>© {{currentYear}} MonacoUSA. All rights reserved.</p>
|
||||||
|
<p>Please submit your payment to avoid membership suspension.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -3,7 +3,7 @@ import type { Member, DuesCalculationUtils } from '~/utils/types';
|
||||||
// Constants for date calculations
|
// Constants for date calculations
|
||||||
export const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
|
export const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
|
||||||
export const DAYS_IN_YEAR = 365;
|
export const DAYS_IN_YEAR = 365;
|
||||||
export const UPCOMING_THRESHOLD_DAYS = 30;
|
export const UPCOMING_THRESHOLD_DAYS = 30; // Trigger reminders 1 month before yearly due date
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate days between two dates
|
* Calculate days between two dates
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue