namespace BudgetApp.Domain.Models; /// /// Goal-based budget with a target amount and date. /// Can calculate if goal is achievable and required contribution rate. /// public class GoalBudget : Budget { public Money GoalAmount { get; protected set; } public DateTime TargetDate { get; protected set; } public Money PeriodicContribution { get; protected set; } public TimeSpan ContributionPeriod { get; protected set; } // e.g., monthly, bi-weekly public GoalBudget( Guid id, string name, Money initialBalance, Money goalAmount, DateTime targetDate, Money periodicContribution, TimeSpan contributionPeriod) : base(id, name, initialBalance) { if (goalAmount == null) throw new ArgumentNullException(nameof(goalAmount)); if (goalAmount.Amount <= 0) throw new ArgumentException("Goal amount must be greater than zero.", nameof(goalAmount)); if (targetDate <= DateTime.Now) throw new ArgumentException("Target date must be in the future.", nameof(targetDate)); if (periodicContribution == null) throw new ArgumentNullException(nameof(periodicContribution)); if (periodicContribution.Amount < 0) throw new ArgumentException("Periodic contribution cannot be negative.", nameof(periodicContribution)); if (initialBalance.Currency != goalAmount.Currency || initialBalance.Currency != periodicContribution.Currency) throw new InvalidOperationException("All amounts must use the same currency."); GoalAmount = goalAmount; TargetDate = targetDate; PeriodicContribution = periodicContribution; ContributionPeriod = contributionPeriod; } public GoalBudget( string name, Money initialBalance, Money goalAmount, DateTime targetDate, Money periodicContribution, TimeSpan contributionPeriod) : this(Guid.NewGuid(), name, initialBalance, goalAmount, targetDate, periodicContribution, contributionPeriod) { } /// /// Calculates if the goal is achievable with the current periodic contribution. /// public bool IsGoalAchievable(DateTime currentDate) { if (currentDate >= TargetDate) return Balance.Amount >= GoalAmount.Amount; var remainingAmount = GoalAmount.Amount - Balance.Amount; if (remainingAmount <= 0) return true; var timeRemaining = TargetDate - currentDate; var numberOfPeriods = (int)Math.Ceiling(timeRemaining.TotalDays / ContributionPeriod.TotalDays); if (numberOfPeriods <= 0) return false; var totalContributions = PeriodicContribution.Amount * numberOfPeriods; return totalContributions >= remainingAmount; } /// /// Calculates the required periodic contribution rate to achieve the goal. /// public Money CalculateRequiredContributionRate(DateTime currentDate) { if (currentDate >= TargetDate) { var remaining = GoalAmount.Amount - Balance.Amount; return remaining <= 0 ? new Money(0, Balance.Currency) : new Money(remaining, Balance.Currency); } var remainingAmount = GoalAmount.Amount - Balance.Amount; if (remainingAmount <= 0) return new Money(0, Balance.Currency); var timeRemaining = TargetDate - currentDate; var numberOfPeriods = (int)Math.Ceiling(timeRemaining.TotalDays / ContributionPeriod.TotalDays); if (numberOfPeriods <= 0) throw new InvalidOperationException("Target date has already passed or is too close."); var requiredContribution = remainingAmount / numberOfPeriods; return new Money(requiredContribution, Balance.Currency); } }