import { FixedDebt, VariableDebt, CreditCard, MonthlyBudget, Alert, } from './types' import { getDaysUntil, getNextDateByDay, formatCurrency, calculateTotalFixedDebts, calculateTotalVariableDebts, } from './utils' export interface GenerateAlertsParams { fixedDebts: FixedDebt[] variableDebts: VariableDebt[] creditCards: CreditCard[] monthlyBudgets: MonthlyBudget[] currentMonth: number currentYear: number } interface AlertDraft { type: Alert['type'] title: string message: string severity: Alert['severity'] relatedId?: string } /** * Obtiene las deudas fijas no pagadas que vencen en los próximos N días */ export function getUpcomingFixedDebts( fixedDebts: FixedDebt[], days: number ): Array<{ debt: FixedDebt; daysUntil: number; dueDate: Date }> { const today = new Date() const currentDay = today.getDate() const currentMonth = today.getMonth() const currentYear = today.getFullYear() return fixedDebts .filter((debt) => !debt.isPaid) .map((debt) => { // Calcular la fecha de vencimiento para este mes let dueDate = new Date(currentYear, currentMonth, debt.dueDay) // Si ya pasó, calcular para el mes siguiente if (currentDay > debt.dueDay) { dueDate = new Date(currentYear, currentMonth + 1, debt.dueDay) } const daysUntil = getDaysUntil(dueDate) return { debt, daysUntil, dueDate } }) .filter(({ daysUntil }) => daysUntil >= 0 && daysUntil <= days) .sort((a, b) => a.daysUntil - b.daysUntil) } /** * Obtiene el presupuesto del mes actual */ export function getCurrentMonthBudget( monthlyBudgets: MonthlyBudget[], month: number, year: number ): MonthlyBudget | null { return ( monthlyBudgets.find( (budget) => budget.month === month && budget.year === year ) || null ) } /** * Calcula el gasto actual del mes (deudas fijas + variables no pagadas) */ export function calculateCurrentSpending( fixedDebts: FixedDebt[], variableDebts: VariableDebt[] ): number { const fixedSpending = calculateTotalFixedDebts(fixedDebts) const variableSpending = calculateTotalVariableDebts(variableDebts) return fixedSpending + variableSpending } interface CardEvent { card: CreditCard type: 'closing' | 'due' daysUntil: number date: Date } /** * Obtiene los eventos próximos de tarjetas (cierre o vencimiento) */ export function getUpcomingCardEvents( creditCards: CreditCard[], days: number ): CardEvent[] { const events: CardEvent[] = [] for (const card of creditCards) { // Calcular próximo cierre const closingDate = getNextDateByDay(card.closingDay) const daysUntilClosing = getDaysUntil(closingDate) if (daysUntilClosing >= 0 && daysUntilClosing <= days) { events.push({ card, type: 'closing', daysUntil: daysUntilClosing, date: closingDate, }) } // Calcular próximo vencimiento const dueDate = getNextDateByDay(card.dueDay) const daysUntilDue = getDaysUntil(dueDate) if (daysUntilDue >= 0 && daysUntilDue <= days) { events.push({ card, type: 'due', daysUntil: daysUntilDue, date: dueDate, }) } } // Ordenar por días hasta el evento return events.sort((a, b) => a.daysUntil - b.daysUntil) } /** * Genera alertas de pagos próximos (deudas fijas) */ function generatePaymentDueAlerts(fixedDebts: FixedDebt[]): AlertDraft[] { const upcomingDebts = getUpcomingFixedDebts(fixedDebts, 3) const alerts: AlertDraft[] = [] for (const { debt, daysUntil } of upcomingDebts) { const severity: Alert['severity'] = daysUntil <= 1 ? 'danger' : 'warning' const daysText = daysUntil === 0 ? 'hoy' : daysUntil === 1 ? 'mañana' : `en ${daysUntil} días` alerts.push({ type: 'PAYMENT_DUE', title: 'Pago próximo', message: `'${debt.name}' vence ${daysText}: ${formatCurrency(debt.amount)}`, severity, relatedId: debt.id, }) } return alerts } /** * Genera alertas de presupuesto */ function generateBudgetAlerts( fixedDebts: FixedDebt[], variableDebts: VariableDebt[], monthlyBudgets: MonthlyBudget[], currentMonth: number, currentYear: number ): AlertDraft[] { const currentBudget = getCurrentMonthBudget( monthlyBudgets, currentMonth, currentYear ) if (!currentBudget) { return [] } const totalBudget = currentBudget.fixedExpenses + currentBudget.variableExpenses if (totalBudget <= 0) { return [] } const currentSpending = calculateCurrentSpending(fixedDebts, variableDebts) const percentageUsed = (currentSpending / totalBudget) * 100 if (percentageUsed < 80) { return [] } const severity: Alert['severity'] = percentageUsed > 95 ? 'danger' : 'warning' return [ { type: 'BUDGET_WARNING', title: 'Presupuesto al límite', message: `Has usado el ${percentageUsed.toFixed(1)}% de tu presupuesto mensual`, severity, }, ] } /** * Genera alertas de eventos de tarjetas (cierre y vencimiento) */ function generateCardAlerts(creditCards: CreditCard[]): AlertDraft[] { const events = getUpcomingCardEvents(creditCards, 3) const closingAlerts: AlertDraft[] = [] const dueAlerts: AlertDraft[] = [] for (const event of events) { if (event.type === 'closing') { const daysText = event.daysUntil === 0 ? 'hoy' : event.daysUntil === 1 ? 'mañana' : `en ${event.daysUntil} días` closingAlerts.push({ type: 'CARD_CLOSING', title: 'Cierre de tarjeta próximo', message: `Tu tarjeta ${event.card.name} cierra ${daysText}. Balance: ${formatCurrency(event.card.currentBalance)}`, severity: 'info', relatedId: event.card.id, }) } else { const severity: Alert['severity'] = event.daysUntil <= 2 ? 'warning' : 'info' const daysText = event.daysUntil === 0 ? 'hoy' : event.daysUntil === 1 ? 'mañana' : `en ${event.daysUntil} días` dueAlerts.push({ type: 'CARD_DUE', title: 'Vencimiento de tarjeta', message: `Vencimiento de ${event.card.name} ${daysText}. Balance: ${formatCurrency(event.card.currentBalance)}`, severity, relatedId: event.card.id, }) } } return [...closingAlerts, ...dueAlerts] } /** * Genera alertas de meta de ahorro */ function generateSavingsAlerts( fixedDebts: FixedDebt[], variableDebts: VariableDebt[], monthlyBudgets: MonthlyBudget[], currentMonth: number, currentYear: number ): AlertDraft[] { const currentBudget = getCurrentMonthBudget( monthlyBudgets, currentMonth, currentYear ) if (!currentBudget || currentBudget.savingsGoal <= 0) { return [] } const currentSpending = calculateCurrentSpending(fixedDebts, variableDebts) const projectedSavings = currentBudget.totalIncome - currentSpending if (projectedSavings >= currentBudget.savingsGoal) { return [] } const percentageBelow = ((currentBudget.savingsGoal - projectedSavings) / currentBudget.savingsGoal) * 100 return [ { type: 'SAVINGS_GOAL', title: 'Meta de ahorro', message: `Vas ${percentageBelow.toFixed(0)}% por debajo de tu meta de ahorro mensual`, severity: 'info', }, ] } /** * Elimina alertas duplicadas basándose en tipo y relatedId */ function deduplicateAlerts(alerts: AlertDraft[]): AlertDraft[] { const seen = new Set() return alerts.filter((alert) => { const key = `${alert.type}-${alert.relatedId || 'global'}` if (seen.has(key)) { return false } seen.add(key) return true }) } /** * Genera todas las alertas inteligentes basadas en el estado actual */ export function generateAlerts(params: GenerateAlertsParams): AlertDraft[] { const { fixedDebts, variableDebts, creditCards, monthlyBudgets, currentMonth, currentYear, } = params const allAlerts: AlertDraft[] = [ ...generatePaymentDueAlerts(fixedDebts), ...generateBudgetAlerts( fixedDebts, variableDebts, monthlyBudgets, currentMonth, currentYear ), ...generateCardAlerts(creditCards), ...generateSavingsAlerts( fixedDebts, variableDebts, monthlyBudgets, currentMonth, currentYear ), ] // Eliminar duplicados y ordenar por severidad (danger > warning > info) const uniqueAlerts = deduplicateAlerts(allAlerts) const severityOrder = { danger: 0, warning: 1, info: 2 } return uniqueAlerts.sort( (a, b) => severityOrder[a.severity] - severityOrder[b.severity] ) }