2026-01-03 10:29:03 -06:00

110 lines
3.9 KiB
C#

namespace BudgetApp.Domain.Models;
/// <summary>
/// Goal-based budget with a target amount and date.
/// Can calculate if goal is achievable and required contribution rate.
/// </summary>
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)
{
}
/// <summary>
/// Calculates if the goal is achievable with the current periodic contribution.
/// </summary>
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;
}
/// <summary>
/// Calculates the required periodic contribution rate to achieve the goal.
/// </summary>
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);
}
}