monacousa-portal/utils/dues-calculations.ts

244 lines
6.5 KiB
TypeScript

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;
/**
* 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
};