110 lines
3.9 KiB
C#
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);
|
|
}
|
|
}
|
|
|