import type { Member, DuesCalculationUtils } from '~/utils/types'; // Constants for date calculations export const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24; export const DAYS_IN_YEAR = 365; export const UPCOMING_THRESHOLD_DAYS = 30; // Trigger reminders 1 month before yearly due date /** * Calculate days between two dates */ export 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 * Used consistently across all dues components */ export function isInGracePeriod(member: Member): boolean { if (!member.payment_due_date) return false; try { const dueDate = new Date(member.payment_due_date); const today = new Date(); return dueDate > today; } catch { return false; } } /** * Check if a member's last payment is over 1 year old * Used consistently across all dues components */ export function isPaymentOverOneYear(member: Member): 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); const today = new Date(); return today > oneYearFromPayment; } catch { return false; } } /** * Check if dues are actually current * Used consistently across all dues components * This is the main logic used throughout the system */ export function isDuesActuallyCurrent(member: Member): boolean { const paymentTooOld = isPaymentOverOneYear(member); const duesCurrentlyPaid = member.current_year_dues_paid === 'true'; const gracePeriod = isInGracePeriod(member); // Member is NOT overdue if they're in grace period OR (dues paid AND payment not too old) const isOverdue = paymentTooOld || (!duesCurrentlyPaid && !gracePeriod); return !isOverdue; } /** * Calculate overdue days for a member */ export function calculateOverdueDays(member: Member): number { const today = new Date(); const paymentTooOld = isPaymentOverOneYear(member); 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 */ export function calculateDaysUntilDue(member: Member): { daysUntilDue: number; nextDueDate: string } | null { const today = new Date(); // 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; } /** * Get the next dues date for a member * Used for display purposes */ export function getNextDuesDate(member: Member): string { // If dues are paid, calculate 1 year from payment date if (member.current_year_dues_paid === 'true' && member.membership_date_paid) { try { const lastPaidDate = new Date(member.membership_date_paid); const nextDue = new Date(lastPaidDate); nextDue.setFullYear(nextDue.getFullYear() + 1); return nextDue.toISOString().split('T')[0]; } catch { // Fall through } } // If not paid but has a due date, use that if (member.payment_due_date) { return member.payment_due_date; } // Fallback: 1 year from member since date if (member.member_since) { try { const memberSince = new Date(member.member_since); const nextDue = new Date(memberSince); nextDue.setFullYear(nextDue.getFullYear() + 1); return nextDue.toISOString().split('T')[0]; } catch { // Ignore invalid dates } } return ''; } /** * Get days remaining until due date */ export function getDaysRemaining(member: Member): number { if (!member.payment_due_date) return 0; try { const dueDate = new Date(member.payment_due_date); const today = new Date(); const diffTime = dueDate.getTime() - today.getTime(); const diffDays = Math.ceil(diffTime / MILLISECONDS_PER_DAY); return Math.max(0, diffDays); } catch { return 0; } } /** * Get a human-readable payment message for a member */ export function getPaymentMessage(member: Member, membershipFee: number = 50): string { const daysRemaining = getDaysRemaining(member); if (daysRemaining > 30) { return `Your annual membership dues of €${membershipFee} are due in ${daysRemaining} days.`; } else if (daysRemaining > 0) { return `Your annual membership dues of €${membershipFee} are due in ${daysRemaining} days. Please pay soon to avoid account suspension.`; } else { return `Your annual membership dues of €${membershipFee} are overdue. Your account may be suspended soon.`; } } /** * Export all utilities as a single object * This matches the DuesCalculationUtils interface */ export const duesCalculationUtils: DuesCalculationUtils = { isInGracePeriod, isPaymentOverOneYear, isDuesActuallyCurrent, calculateOverdueDays, calculateDaysUntilDue }; /** * Default export for easy importing */ export default { // Core logic functions isInGracePeriod, isPaymentOverOneYear, isDuesActuallyCurrent, calculateOverdueDays, calculateDaysUntilDue, // Utility functions calculateDaysDifference, getNextDuesDate, getDaysRemaining, getPaymentMessage, // Constants MILLISECONDS_PER_DAY, DAYS_IN_YEAR, UPCOMING_THRESHOLD_DAYS };