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; try { 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 => { try { const { getMembers, updateMember } = await import('~/server/utils/nocodb'); // Get all members const allMembers = await getMembers(); if (!allMembers?.list?.length) { return { success: true, data: { overdue: [], upcoming: [], autoUpdated: 0 } }; } const today = new Date(); const overdueMembers: DuesMemberWithStatus[] = []; const upcomingMembers: DuesMemberWithStatus[] = []; const membersToUpdate: Member[] = []; console.log(`[dues-status] Processing ${allMembers.list.length} members for dues status...`); for (const member of allMembers.list) { const memberName = `${member.first_name || ''} ${member.last_name || ''}`.trim() || `Member ${member.Id}`; // 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); } 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'; overdueMembers.push({ ...member, overdueDays, overdueReason }); } else if (duesCurrentlyPaid || gracePeriod) { // Check if member has upcoming dues const upcomingInfo = calculateDaysUntilDue(member, today); if (upcomingInfo) { upcomingMembers.push({ ...member, ...upcomingInfo }); } } } // 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); } } } // Sort results overdueMembers.sort((a, b) => (b.overdueDays || 0) - (a.overdueDays || 0)); upcomingMembers.sort((a, b) => (a.daysUntilDue || 999) - (b.daysUntilDue || 999)); console.log(`[dues-status] ✓ Results: ${overdueMembers.length} overdue, ${upcomingMembers.length} upcoming, ${autoUpdatedCount} auto-updated`); return { success: true, data: { overdue: overdueMembers, upcoming: upcomingMembers, autoUpdated: autoUpdatedCount } }; } catch (error: any) { console.error('[dues-status] ✗ Error processing dues status:', error); throw createError({ statusCode: error.statusCode || 500, statusMessage: error.message || 'Failed to process dues status' }); } });