2025-08-11 15:29:42 +02:00
|
|
|
import type { Member } from '~/utils/types';
|
|
|
|
|
|
|
|
|
|
interface DuesMemberWithStatus extends Member {
|
|
|
|
|
overdueDays?: number;
|
|
|
|
|
overdueReason?: string;
|
|
|
|
|
daysUntilDue?: number;
|
|
|
|
|
nextDueDate?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface DuesStatusResponse {
|
|
|
|
|
success: boolean;
|
|
|
|
|
data: {
|
|
|
|
|
overdue: DuesMemberWithStatus[];
|
|
|
|
|
upcoming: DuesMemberWithStatus[];
|
|
|
|
|
autoUpdated: number;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Constants for date calculations
|
|
|
|
|
const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
|
|
|
|
|
const DAYS_IN_YEAR = 365;
|
|
|
|
|
const UPCOMING_THRESHOLD_DAYS = 30;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate days between two dates
|
|
|
|
|
*/
|
|
|
|
|
function calculateDaysDifference(date1: Date, date2: Date): number {
|
|
|
|
|
return Math.floor((date1.getTime() - date2.getTime()) / MILLISECONDS_PER_DAY);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a member is in their grace period
|
|
|
|
|
*/
|
|
|
|
|
function isInGracePeriod(member: Member, today: Date): boolean {
|
|
|
|
|
if (!member.payment_due_date) return false;
|
|
|
|
|
|
2025-08-10 23:19:48 +02:00
|
|
|
try {
|
2025-08-11 15:29:42 +02:00
|
|
|
const dueDate = new Date(member.payment_due_date);
|
|
|
|
|
return dueDate > today;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a member's last payment is over 1 year old
|
|
|
|
|
*/
|
|
|
|
|
function isPaymentOverOneYear(member: Member, today: Date): boolean {
|
|
|
|
|
if (!member.membership_date_paid) return false;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const lastPaidDate = new Date(member.membership_date_paid);
|
|
|
|
|
const oneYearFromPayment = new Date(lastPaidDate);
|
|
|
|
|
oneYearFromPayment.setFullYear(oneYearFromPayment.getFullYear() + 1);
|
|
|
|
|
|
|
|
|
|
return today > oneYearFromPayment;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate overdue days for a member
|
|
|
|
|
*/
|
|
|
|
|
function calculateOverdueDays(member: Member, today: Date, paymentTooOld: boolean): number {
|
|
|
|
|
if (paymentTooOld && member.membership_date_paid) {
|
|
|
|
|
try {
|
|
|
|
|
const lastPaidDate = new Date(member.membership_date_paid);
|
|
|
|
|
return calculateDaysDifference(today, lastPaidDate) - DAYS_IN_YEAR;
|
|
|
|
|
} catch {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (member.payment_due_date) {
|
|
|
|
|
try {
|
|
|
|
|
const dueDate = new Date(member.payment_due_date);
|
|
|
|
|
return calculateDaysDifference(today, dueDate);
|
|
|
|
|
} catch {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate days until due for upcoming members
|
|
|
|
|
*/
|
|
|
|
|
function calculateDaysUntilDue(member: Member, today: Date): { daysUntilDue: number; nextDueDate: string } | null {
|
|
|
|
|
// First try membership_date_paid (existing members)
|
|
|
|
|
if (member.membership_date_paid) {
|
|
|
|
|
try {
|
|
|
|
|
const lastPaidDate = new Date(member.membership_date_paid);
|
|
|
|
|
const nextDueDate = new Date(lastPaidDate);
|
|
|
|
|
nextDueDate.setFullYear(nextDueDate.getFullYear() + 1);
|
|
|
|
|
|
|
|
|
|
const daysUntilDue = calculateDaysDifference(nextDueDate, today);
|
|
|
|
|
|
|
|
|
|
if (daysUntilDue <= UPCOMING_THRESHOLD_DAYS && daysUntilDue > 0) {
|
|
|
|
|
return {
|
|
|
|
|
daysUntilDue,
|
|
|
|
|
nextDueDate: nextDueDate.toISOString().split('T')[0]
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Fall through to payment_due_date check
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try payment_due_date (new members in grace period)
|
|
|
|
|
if (member.payment_due_date) {
|
|
|
|
|
try {
|
|
|
|
|
const dueDate = new Date(member.payment_due_date);
|
|
|
|
|
const daysUntilDue = calculateDaysDifference(dueDate, today);
|
|
|
|
|
|
|
|
|
|
if (daysUntilDue <= UPCOMING_THRESHOLD_DAYS && daysUntilDue > 0) {
|
|
|
|
|
return {
|
|
|
|
|
daysUntilDue,
|
|
|
|
|
nextDueDate: member.payment_due_date
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore invalid dates
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default defineEventHandler(async (event): Promise<DuesStatusResponse> => {
|
|
|
|
|
try {
|
|
|
|
|
const { getMembers, updateMember } = await import('~/server/utils/nocodb');
|
2025-08-10 23:19:48 +02:00
|
|
|
|
|
|
|
|
// Get all members
|
|
|
|
|
const allMembers = await getMembers();
|
|
|
|
|
|
2025-08-11 15:29:42 +02:00
|
|
|
if (!allMembers?.list?.length) {
|
2025-08-10 23:19:48 +02:00
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
data: {
|
|
|
|
|
overdue: [],
|
2025-08-11 15:29:42 +02:00
|
|
|
upcoming: [],
|
|
|
|
|
autoUpdated: 0
|
2025-08-10 23:19:48 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const today = new Date();
|
2025-08-11 15:29:42 +02:00
|
|
|
const overdueMembers: DuesMemberWithStatus[] = [];
|
|
|
|
|
const upcomingMembers: DuesMemberWithStatus[] = [];
|
|
|
|
|
const membersToUpdate: Member[] = [];
|
2025-08-10 23:19:48 +02:00
|
|
|
|
2025-08-11 15:29:42 +02:00
|
|
|
console.log(`[dues-status] Processing ${allMembers.list.length} members for dues status...`);
|
2025-08-10 23:19:48 +02:00
|
|
|
|
|
|
|
|
for (const member of allMembers.list) {
|
2025-08-11 15:29:42 +02:00
|
|
|
const memberName = `${member.first_name || ''} ${member.last_name || ''}`.trim() || `Member ${member.Id}`;
|
2025-08-10 23:29:48 +02:00
|
|
|
|
2025-08-11 15:29:42 +02:00
|
|
|
// Check member status
|
|
|
|
|
const gracePeriod = isInGracePeriod(member, today);
|
|
|
|
|
const paymentTooOld = isPaymentOverOneYear(member, today);
|
|
|
|
|
const duesCurrentlyPaid = member.current_year_dues_paid === 'true';
|
|
|
|
|
|
|
|
|
|
// Determine if member is overdue
|
|
|
|
|
const isOverdue = paymentTooOld || (!duesCurrentlyPaid && !gracePeriod);
|
|
|
|
|
|
|
|
|
|
if (isOverdue) {
|
|
|
|
|
// Handle overdue member
|
|
|
|
|
if (paymentTooOld && member.membership_status !== 'Inactive') {
|
|
|
|
|
console.log(`[dues-status] ${memberName}: Auto-updating to Inactive (payment over 1 year old)`);
|
|
|
|
|
membersToUpdate.push(member);
|
2025-08-10 23:29:48 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-11 15:29:42 +02:00
|
|
|
const overdueDays = calculateOverdueDays(member, today, paymentTooOld);
|
|
|
|
|
const overdueReason = paymentTooOld
|
|
|
|
|
? `Payment from ${member.membership_date_paid} is over 1 year old`
|
|
|
|
|
: 'Current year dues not paid';
|
2025-08-10 23:29:48 +02:00
|
|
|
|
2025-08-11 15:29:42 +02:00
|
|
|
overdueMembers.push({
|
|
|
|
|
...member,
|
|
|
|
|
overdueDays,
|
|
|
|
|
overdueReason
|
|
|
|
|
});
|
2025-08-10 23:29:48 +02:00
|
|
|
|
2025-08-11 15:29:42 +02:00
|
|
|
} else if (duesCurrentlyPaid || gracePeriod) {
|
|
|
|
|
// Check if member has upcoming dues
|
|
|
|
|
const upcomingInfo = calculateDaysUntilDue(member, today);
|
|
|
|
|
|
|
|
|
|
if (upcomingInfo) {
|
|
|
|
|
upcomingMembers.push({
|
|
|
|
|
...member,
|
|
|
|
|
...upcomingInfo
|
|
|
|
|
});
|
2025-08-10 23:29:48 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-11 15:29:42 +02:00
|
|
|
}
|
2025-08-10 23:29:48 +02:00
|
|
|
|
2025-08-11 15:29:42 +02:00
|
|
|
// Auto-update members who need status changes
|
|
|
|
|
let autoUpdatedCount = 0;
|
|
|
|
|
if (membersToUpdate.length > 0) {
|
|
|
|
|
console.log(`[dues-status] Auto-updating ${membersToUpdate.length} members to Inactive status...`);
|
|
|
|
|
|
|
|
|
|
for (const memberUpdate of membersToUpdate) {
|
|
|
|
|
try {
|
|
|
|
|
await updateMember(memberUpdate.Id, {
|
|
|
|
|
membership_status: 'Inactive',
|
|
|
|
|
current_year_dues_paid: 'false'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update the overdue member in our list to reflect the status change
|
|
|
|
|
const overdueIndex = overdueMembers.findIndex(m => m.Id === memberUpdate.Id);
|
|
|
|
|
if (overdueIndex !== -1) {
|
|
|
|
|
overdueMembers[overdueIndex].membership_status = 'Inactive';
|
|
|
|
|
overdueMembers[overdueIndex].current_year_dues_paid = 'false';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
autoUpdatedCount++;
|
|
|
|
|
console.log(`[dues-status] ✓ Updated ${memberUpdate.first_name} ${memberUpdate.last_name} to Inactive`);
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
console.error(`[dues-status] ✗ Failed to update member ${memberUpdate.Id}:`, error.message);
|
2025-08-10 23:19:48 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-11 15:29:42 +02:00
|
|
|
// Sort results
|
|
|
|
|
overdueMembers.sort((a, b) => (b.overdueDays || 0) - (a.overdueDays || 0));
|
|
|
|
|
upcomingMembers.sort((a, b) => (a.daysUntilDue || 999) - (b.daysUntilDue || 999));
|
2025-08-10 23:19:48 +02:00
|
|
|
|
2025-08-11 15:29:42 +02:00
|
|
|
console.log(`[dues-status] ✓ Results: ${overdueMembers.length} overdue, ${upcomingMembers.length} upcoming, ${autoUpdatedCount} auto-updated`);
|
2025-08-10 23:19:48 +02:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
data: {
|
|
|
|
|
overdue: overdueMembers,
|
2025-08-11 15:29:42 +02:00
|
|
|
upcoming: upcomingMembers,
|
|
|
|
|
autoUpdated: autoUpdatedCount
|
2025-08-10 23:19:48 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} catch (error: any) {
|
2025-08-11 15:29:42 +02:00
|
|
|
console.error('[dues-status] ✗ Error processing dues status:', error);
|
2025-08-10 23:19:48 +02:00
|
|
|
|
|
|
|
|
throw createError({
|
|
|
|
|
statusCode: error.statusCode || 500,
|
2025-08-11 15:29:42 +02:00
|
|
|
statusMessage: error.message || 'Failed to process dues status'
|
2025-08-10 23:19:48 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|