2026-01-28 11:19:23 +08:00
|
|
|
|
using Service.Transaction;
|
|
|
|
|
|
|
2026-01-28 10:58:15 +08:00
|
|
|
|
namespace WebApi.Test.Budget;
|
2026-01-20 19:11:05 +08:00
|
|
|
|
|
|
|
|
|
|
public class BudgetSavingsTest : BaseTest
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly IBudgetRepository _budgetRepository = Substitute.For<IBudgetRepository>();
|
|
|
|
|
|
private readonly IBudgetArchiveRepository _budgetArchiveRepository = Substitute.For<IBudgetArchiveRepository>();
|
2026-01-28 10:58:15 +08:00
|
|
|
|
private readonly ITransactionStatisticsService _transactionStatisticsService = Substitute.For<ITransactionStatisticsService>();
|
2026-01-20 19:11:05 +08:00
|
|
|
|
private readonly IConfigService _configService = Substitute.For<IConfigService>();
|
|
|
|
|
|
private readonly IDateTimeProvider _dateTimeProvider = Substitute.For<IDateTimeProvider>();
|
|
|
|
|
|
private readonly BudgetSavingsService _service;
|
|
|
|
|
|
|
|
|
|
|
|
public BudgetSavingsTest()
|
|
|
|
|
|
{
|
|
|
|
|
|
_dateTimeProvider.Now.Returns(DateTime.Now);
|
|
|
|
|
|
_service = new BudgetSavingsService(
|
|
|
|
|
|
_budgetRepository,
|
|
|
|
|
|
_budgetArchiveRepository,
|
2026-01-28 10:58:15 +08:00
|
|
|
|
_transactionStatisticsService,
|
2026-01-20 19:11:05 +08:00
|
|
|
|
_configService,
|
|
|
|
|
|
_dateTimeProvider
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task GetSavings_月度_Test()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var referenceDate = new DateTime(2024, 1, 1);
|
|
|
|
|
|
var budgets = new List<BudgetRecord>
|
|
|
|
|
|
{
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1, Name = "工资", Type = BudgetPeriodType.Month, Limit = 10000, Category = BudgetCategory.Income,
|
|
|
|
|
|
SelectedCategories = "工资"
|
|
|
|
|
|
},
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 2, Name = "餐饮", Type = BudgetPeriodType.Month, Limit = 2000, Category = BudgetCategory.Expense,
|
|
|
|
|
|
SelectedCategories = "餐饮"
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var transactions = new Dictionary<(string, TransactionType), decimal>
|
|
|
|
|
|
{
|
|
|
|
|
|
{ ("工资", TransactionType.Income), 10000m },
|
|
|
|
|
|
{ ("餐饮", TransactionType.Expense), 1500m }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_budgetRepository.GetAllAsync().Returns(budgets);
|
2026-01-28 10:58:15 +08:00
|
|
|
|
_transactionStatisticsService.GetAmountGroupByClassifyAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
2026-01-20 19:11:05 +08:00
|
|
|
|
.Returns(transactions);
|
|
|
|
|
|
_configService.GetConfigByKeyAsync<string>("SavingsCategories").Returns(Task.FromResult<string?>("存款"));
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
var result = await _service.GetSavingsDtoAsync(BudgetPeriodType.Month, referenceDate);
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
result.Should().NotBeNull();
|
|
|
|
|
|
result.Limit.Should().Be(10000 - 2000); // 计划收入 - 计划支出 = 10000 - 2000 = 8000
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task GetSavings_月度_年度收支_Test()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var referenceDate = new DateTime(2024, 1, 1);
|
|
|
|
|
|
var budgets = new List<BudgetRecord>
|
|
|
|
|
|
{
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1, Name = "工资", Type = BudgetPeriodType.Month, Limit = 10000, Category = BudgetCategory.Income,
|
|
|
|
|
|
SelectedCategories = "工资"
|
|
|
|
|
|
},
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 2, Name = "餐饮", Type = BudgetPeriodType.Month, Limit = 2000, Category = BudgetCategory.Expense,
|
|
|
|
|
|
SelectedCategories = "餐饮"
|
|
|
|
|
|
},
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 3, Name = "年终奖", Type = BudgetPeriodType.Year, Limit = 50000, Category = BudgetCategory.Income,
|
|
|
|
|
|
SelectedCategories = "奖金"
|
|
|
|
|
|
},
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 4, Name = "保险", Type = BudgetPeriodType.Year, Limit = 6000, Category = BudgetCategory.Expense,
|
|
|
|
|
|
SelectedCategories = "保险"
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var transactions = new Dictionary<(string, TransactionType), decimal>
|
|
|
|
|
|
{
|
|
|
|
|
|
{ ("工资", TransactionType.Income), 10000m },
|
|
|
|
|
|
{ ("餐饮", TransactionType.Expense), 1500m },
|
|
|
|
|
|
{ ("奖金", TransactionType.Income), 50000m },
|
|
|
|
|
|
{ ("保险", TransactionType.Expense), 6000m }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_budgetRepository.GetAllAsync().Returns(budgets);
|
2026-01-28 10:58:15 +08:00
|
|
|
|
_transactionStatisticsService.GetAmountGroupByClassifyAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
2026-01-20 19:11:05 +08:00
|
|
|
|
.Returns(transactions);
|
|
|
|
|
|
_configService.GetConfigByKeyAsync<string>("SavingsCategories").Returns(Task.FromResult<string?>("存款"));
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
var result = await _service.GetSavingsDtoAsync(BudgetPeriodType.Month, referenceDate);
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
result.Should().NotBeNull();
|
|
|
|
|
|
// 计划收入 = 月度计划收入(10000) + 本月发生的年度实际收入(50000) = 60000
|
|
|
|
|
|
// 计划支出 = 月度计划支出(2000) + 本月发生的年度实际支出(6000) = 8000
|
|
|
|
|
|
// 计划存款 = 60000 - 8000 = 52000
|
|
|
|
|
|
result.Limit.Should().Be(60000 - 8000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task GetSavings_月度_年度收支_硬性收支_Test()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
// 模拟当前日期为 2026-01-20
|
|
|
|
|
|
var now = new DateTime(2026, 1, 20);
|
|
|
|
|
|
_dateTimeProvider.Now.Returns(now);
|
|
|
|
|
|
|
|
|
|
|
|
var referenceDate = new DateTime(2026, 1, 1);
|
|
|
|
|
|
var budgets = new List<BudgetRecord>
|
|
|
|
|
|
{
|
|
|
|
|
|
// 房租 3100,硬性支出。假设目前还没付(实际为0),系统应按 20/31 天估算为 2000
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1, Name = "房租", Type = BudgetPeriodType.Month, Limit = 3100, Category = BudgetCategory.Expense,
|
|
|
|
|
|
SelectedCategories = "房租", IsMandatoryExpense = true
|
|
|
|
|
|
},
|
|
|
|
|
|
// 理财收益 6200,硬性收入。假设目前还没到账(实际为0),系统应按 20/31 天估算为 4000
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 2, Name = "理财收益", Type = BudgetPeriodType.Month, Limit = 6200, Category = BudgetCategory.Income,
|
|
|
|
|
|
SelectedCategories = "理财", IsMandatoryExpense = true
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 模拟实际交易为 0
|
|
|
|
|
|
var transactions = new Dictionary<(string, TransactionType), decimal>();
|
|
|
|
|
|
|
|
|
|
|
|
_budgetRepository.GetAllAsync().Returns(budgets);
|
2026-01-28 10:58:15 +08:00
|
|
|
|
_transactionStatisticsService.GetAmountGroupByClassifyAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
2026-01-20 19:11:05 +08:00
|
|
|
|
.Returns(transactions);
|
|
|
|
|
|
_configService.GetConfigByKeyAsync<string>("SavingsCategories").Returns(Task.FromResult<string?>("存款"));
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
var result = await _service.GetSavingsDtoAsync(BudgetPeriodType.Month, referenceDate);
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
// 2026年1月有31天,当前是20号
|
|
|
|
|
|
// 预期的估算值:
|
|
|
|
|
|
// 支出 = 3100 / 31 * 20 = 2000
|
|
|
|
|
|
// 收入 = 6200 / 31 * 20 = 4000
|
|
|
|
|
|
|
|
|
|
|
|
result.Should().NotBeNull();
|
|
|
|
|
|
// 计划存款 = 计划收入(6200) - 计划支出(3100) = 3100
|
|
|
|
|
|
result.Limit.Should().Be(6200 - 3100);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task GetSavings_年度_预算_实际_Test()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var year = 2024;
|
|
|
|
|
|
var referenceDate = new DateTime(year, 1, 1);
|
|
|
|
|
|
_dateTimeProvider.Now.Returns(new DateTime(year, 1, 20));
|
|
|
|
|
|
|
|
|
|
|
|
var budgets = new List<BudgetRecord>
|
|
|
|
|
|
{
|
|
|
|
|
|
new() { Id = 1, Name = "工资", Type = BudgetPeriodType.Month, Limit = 10000, Category = BudgetCategory.Income, SelectedCategories = "工资" },
|
|
|
|
|
|
new() { Id = 2, Name = "房租", Type = BudgetPeriodType.Month, Limit = 3000, Category = BudgetCategory.Expense, SelectedCategories = "房租" },
|
|
|
|
|
|
new() { Id = 3, Name = "年终奖", Type = BudgetPeriodType.Year, Limit = 50000, Category = BudgetCategory.Income, SelectedCategories = "奖金" },
|
|
|
|
|
|
new() { Id = 4, Name = "旅游", Type = BudgetPeriodType.Year, Limit = 20000, Category = BudgetCategory.Expense, SelectedCategories = "旅游" }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var transactions = new Dictionary<(string, TransactionType), decimal>
|
|
|
|
|
|
{
|
|
|
|
|
|
{ ("工资", TransactionType.Income), 10000m },
|
|
|
|
|
|
{ ("房租", TransactionType.Expense), 3000m },
|
|
|
|
|
|
{ ("存款", TransactionType.None), 2000m }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_budgetRepository.GetAllAsync().Returns(budgets);
|
2026-01-28 10:58:15 +08:00
|
|
|
|
_transactionStatisticsService.GetAmountGroupByClassifyAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
2026-01-20 19:11:05 +08:00
|
|
|
|
.Returns(transactions);
|
|
|
|
|
|
_budgetArchiveRepository.GetArchivesByYearAsync(year).Returns(new List<BudgetArchive>());
|
|
|
|
|
|
_configService.GetConfigByKeyAsync<string>("SavingsCategories").Returns(Task.FromResult<string?>("存款"));
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
var result = await _service.GetSavingsDtoAsync(BudgetPeriodType.Year, referenceDate);
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
result.Should().NotBeNull();
|
|
|
|
|
|
// MonthlyIncome: 10000 * 12 = 170000
|
|
|
|
|
|
// MonthlyExpense: 3000 * 12 = 56000
|
|
|
|
|
|
// YearlyIncome: 50000 * 1 = 50000
|
|
|
|
|
|
// YearlyExpense: 20000 * 1 = 20000
|
|
|
|
|
|
// Savings: (170000 + 50000) - (56000 + 20000) = 114000
|
|
|
|
|
|
result.Limit.Should().Be(114000);
|
|
|
|
|
|
result.Current.Should().Be(2000);
|
|
|
|
|
|
result.Name.Should().Be("年度存款计划");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task GetSavings_年度_归档盈亏_Test()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var year = 2024;
|
|
|
|
|
|
// 当前是3月15号
|
|
|
|
|
|
_dateTimeProvider.Now.Returns(new DateTime(year, 3, 15));
|
|
|
|
|
|
|
|
|
|
|
|
var budgets = new List<BudgetRecord>
|
|
|
|
|
|
{
|
|
|
|
|
|
// Monthly Budget changed from 10000 (Jan) to 11000 (Current/Feb)
|
|
|
|
|
|
new() { Id = 1, Name = "工资", Type = BudgetPeriodType.Month, Limit = 11000, Category = BudgetCategory.Income, SelectedCategories = "工资" },
|
|
|
|
|
|
new() { Id = 2, Name = "房租", Type = BudgetPeriodType.Month, Limit = 3000, Category = BudgetCategory.Expense, SelectedCategories = "房租" },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var currentTransactions = new Dictionary<(string, TransactionType), decimal>
|
|
|
|
|
|
{
|
|
|
|
|
|
{ ("工资", TransactionType.Income), 11000m }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var archives = new List<BudgetArchive>
|
|
|
|
|
|
{
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Year = year, Month = 1,
|
|
|
|
|
|
Content =
|
|
|
|
|
|
[
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1,
|
|
|
|
|
|
Name = "工资",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 10000,
|
|
|
|
|
|
Actual = 12000,
|
|
|
|
|
|
Category = BudgetCategory.Income
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 2,
|
|
|
|
|
|
Name = "房租",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 3000,
|
|
|
|
|
|
Actual = 3600,
|
|
|
|
|
|
Category = BudgetCategory.Expense
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Year = year, Month = 2,
|
|
|
|
|
|
Content =
|
|
|
|
|
|
[
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1,
|
|
|
|
|
|
Name = "工资",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 11000,
|
|
|
|
|
|
Actual = 3000,
|
|
|
|
|
|
Category = BudgetCategory.Income
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 2,
|
|
|
|
|
|
Name = "房租",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 3000,
|
|
|
|
|
|
Actual = 5000,
|
|
|
|
|
|
Category = BudgetCategory.Expense
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_budgetRepository.GetAllAsync().Returns(budgets);
|
2026-01-28 10:58:15 +08:00
|
|
|
|
_transactionStatisticsService.GetAmountGroupByClassifyAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
2026-01-20 19:11:05 +08:00
|
|
|
|
.Returns(currentTransactions);
|
|
|
|
|
|
_budgetArchiveRepository.GetArchivesByYearAsync(year).Returns(archives);
|
|
|
|
|
|
_configService.GetConfigByKeyAsync<string>("SavingsCategories").Returns(Task.FromResult<string?>("存款"));
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
var result = await _service.GetSavingsDtoAsync(BudgetPeriodType.Year, new DateTime(year, 1, 1));
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
result.Should().NotBeNull();
|
|
|
|
|
|
// 归档实际收入1月 = 12000 - 3600 = 8400
|
|
|
|
|
|
// 归档实际收入2月 = 3000 - 5000 = -2000
|
|
|
|
|
|
// 预计收入 = 8400 + -2000 + 11000 * 10 = 116400
|
|
|
|
|
|
// 预计支出 = 3000 * 10 = 30000
|
|
|
|
|
|
// 预计存款 = 116400 - 30000 = 86400
|
|
|
|
|
|
result.Limit.Should().Be(86400);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task GetSavings_年度_硬性收支_Test()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var year = 2024;
|
|
|
|
|
|
// 当前是3月15号
|
|
|
|
|
|
_dateTimeProvider.Now.Returns(new DateTime(year, 3, 15));
|
|
|
|
|
|
|
|
|
|
|
|
var budgets = new List<BudgetRecord>
|
|
|
|
|
|
{
|
|
|
|
|
|
// Monthly Budget changed from 10000 (Jan) to 11000 (Current/Feb)
|
|
|
|
|
|
new() { Id = 1, Name = "工资", Type = BudgetPeriodType.Month, Limit = 11000, Category = BudgetCategory.Income, SelectedCategories = "工资" },
|
|
|
|
|
|
new() { Id = 2, Name = "房租", Type = BudgetPeriodType.Month, Limit = 3000, Category = BudgetCategory.Expense, SelectedCategories = "房租" },
|
|
|
|
|
|
new() { Id = 3, Name = "硬性支出", Type = BudgetPeriodType.Year, Limit = 10000, Category = BudgetCategory.Expense, SelectedCategories = "房租", IsMandatoryExpense = true },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var currentTransactions = new Dictionary<(string, TransactionType), decimal>
|
|
|
|
|
|
{
|
|
|
|
|
|
{ ("工资", TransactionType.Income), 11000m }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var archives = new List<BudgetArchive>
|
|
|
|
|
|
{
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Year = year, Month = 1,
|
|
|
|
|
|
Content =
|
|
|
|
|
|
[
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1,
|
|
|
|
|
|
Name = "工资",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 10000,
|
|
|
|
|
|
Actual = 12000,
|
|
|
|
|
|
Category = BudgetCategory.Income
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 2,
|
|
|
|
|
|
Name = "房租",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 3000,
|
|
|
|
|
|
Actual = 3600,
|
|
|
|
|
|
Category = BudgetCategory.Expense
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Year = year, Month = 2,
|
|
|
|
|
|
Content =
|
|
|
|
|
|
[
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1,
|
|
|
|
|
|
Name = "工资",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 11000,
|
|
|
|
|
|
Actual = 3000,
|
|
|
|
|
|
Category = BudgetCategory.Income
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 2,
|
|
|
|
|
|
Name = "房租",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 3000,
|
|
|
|
|
|
Actual = 5000,
|
|
|
|
|
|
Category = BudgetCategory.Expense
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_budgetRepository.GetAllAsync().Returns(budgets);
|
2026-01-28 10:58:15 +08:00
|
|
|
|
_transactionStatisticsService.GetAmountGroupByClassifyAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
2026-01-20 19:11:05 +08:00
|
|
|
|
.Returns(currentTransactions);
|
|
|
|
|
|
_budgetArchiveRepository.GetArchivesByYearAsync(year).Returns(archives);
|
|
|
|
|
|
_configService.GetConfigByKeyAsync<string>("SavingsCategories").Returns(Task.FromResult<string?>("存款"));
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
var result = await _service.GetSavingsDtoAsync(BudgetPeriodType.Year, new DateTime(year, 1, 1));
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
result.Should().NotBeNull();
|
|
|
|
|
|
// 归档实际收入1月 = 12000 - 3600 = 8400
|
|
|
|
|
|
// 归档实际收入2月 = 3000 - 5000 = -2000
|
|
|
|
|
|
// 预计收入 = 8400 + -2000 + 11000 * 10 = 116400
|
|
|
|
|
|
// 硬性支出平均到每天 = 10000 / 366 * 75 = 2049.18
|
|
|
|
|
|
// 预计支出 = 3000 * 10 = 30000
|
|
|
|
|
|
// 预计存款 = 116400 - 30000 - 2049.18 = 84350.82
|
|
|
|
|
|
result.Limit.Should().BeApproximately(84350.82m, 0.01m);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task GetSavings_年度_不限额_Test()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var year = 2024;
|
|
|
|
|
|
// 当前是3月15号
|
|
|
|
|
|
_dateTimeProvider.Now.Returns(new DateTime(year, 3, 15));
|
|
|
|
|
|
|
|
|
|
|
|
var budgets = new List<BudgetRecord>
|
|
|
|
|
|
{
|
|
|
|
|
|
// Monthly Budget changed from 10000 (Jan) to 11000 (Current/Feb)
|
|
|
|
|
|
new() { Id = 1, Name = "工资", Type = BudgetPeriodType.Month, Limit = 11000, Category = BudgetCategory.Income, SelectedCategories = "工资" },
|
|
|
|
|
|
new() { Id = 2, Name = "房租", Type = BudgetPeriodType.Month, Limit = 3000, Category = BudgetCategory.Expense, SelectedCategories = "房租" },
|
|
|
|
|
|
new() { Id = 3, Name = "硬性支出", Type = BudgetPeriodType.Year, Limit = 10000, Category = BudgetCategory.Expense, SelectedCategories = "房租", IsMandatoryExpense = true },
|
|
|
|
|
|
new() { Id = 4, Name = "意外支出", Type = BudgetPeriodType.Year, Limit = 0, Category = BudgetCategory.Expense, SelectedCategories = "意外支出", NoLimit = true },
|
|
|
|
|
|
new() { Id = 5, Name = "意外收入", Type = BudgetPeriodType.Month, Limit = 0, Category = BudgetCategory.Income, SelectedCategories = "意外收入", NoLimit = true }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var currentTransactions = new Dictionary<(string, TransactionType), decimal>
|
|
|
|
|
|
{
|
|
|
|
|
|
{ ("工资", TransactionType.Income), 11000m },
|
|
|
|
|
|
{ ("意外支出", TransactionType.Expense), 300m },
|
|
|
|
|
|
{ ("意外收入", TransactionType.Income), 2000m },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var archives = new List<BudgetArchive>
|
|
|
|
|
|
{
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Year = year, Month = 1,
|
|
|
|
|
|
Content =
|
|
|
|
|
|
[
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1,
|
|
|
|
|
|
Name = "工资",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 10000,
|
|
|
|
|
|
Actual = 12000,
|
|
|
|
|
|
Category = BudgetCategory.Income
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 2,
|
|
|
|
|
|
Name = "房租",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 3000,
|
|
|
|
|
|
Actual = 3600,
|
|
|
|
|
|
Category = BudgetCategory.Expense
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Year = year, Month = 2,
|
|
|
|
|
|
Content =
|
|
|
|
|
|
[
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1,
|
|
|
|
|
|
Name = "工资",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 11000,
|
|
|
|
|
|
Actual = 3000,
|
|
|
|
|
|
Category = BudgetCategory.Income
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 2,
|
|
|
|
|
|
Name = "房租",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 3000,
|
|
|
|
|
|
Actual = 5000,
|
|
|
|
|
|
Category = BudgetCategory.Expense
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_budgetRepository.GetAllAsync().Returns(budgets);
|
2026-01-28 10:58:15 +08:00
|
|
|
|
_transactionStatisticsService.GetAmountGroupByClassifyAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
2026-01-20 19:11:05 +08:00
|
|
|
|
.Returns(currentTransactions);
|
|
|
|
|
|
_budgetArchiveRepository.GetArchivesByYearAsync(year).Returns(archives);
|
|
|
|
|
|
_configService.GetConfigByKeyAsync<string>("SavingsCategories").Returns(Task.FromResult<string?>("存款"));
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
var result = await _service.GetSavingsDtoAsync(BudgetPeriodType.Year, new DateTime(year, 1, 1));
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
result.Should().NotBeNull();
|
|
|
|
|
|
// 归档实际收入1月 = 12000 - 3600 = 8400
|
|
|
|
|
|
// 归档实际收入2月 = 3000 - 5000 = -2000
|
|
|
|
|
|
// 预计收入 = 8400 + -2000 + 11000 * 10 = 116400
|
|
|
|
|
|
// 硬性支出平均到每天 = 10000 / 366 * 75 = 2049.18
|
|
|
|
|
|
// 预计支出 = 3000 * 10 = 30000
|
|
|
|
|
|
// 预计意外支出 = 300
|
|
|
|
|
|
// 预计意外收入 = 2000
|
|
|
|
|
|
// 预计存款 = 116400 - 30000 - 2049.18 - 300 + 2000 = 86050.82
|
|
|
|
|
|
result.Limit.Should().BeApproximately(86050.82m, 0.1m);
|
|
|
|
|
|
}
|
2026-02-11 13:00:01 +08:00
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task GetSavings_年度_包含一月份归档存款_Test()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var year = 2026;
|
|
|
|
|
|
// 当前是2月11号
|
|
|
|
|
|
_dateTimeProvider.Now.Returns(new DateTime(year, 2, 11));
|
|
|
|
|
|
|
|
|
|
|
|
var budgets = new List<BudgetRecord>
|
|
|
|
|
|
{
|
|
|
|
|
|
// 月度预算
|
|
|
|
|
|
new() { Id = 1, Name = "工资", Type = BudgetPeriodType.Month, Limit = 11000, Category = BudgetCategory.Income, SelectedCategories = "工资" },
|
|
|
|
|
|
new() { Id = 2, Name = "房租", Type = BudgetPeriodType.Month, Limit = 3000, Category = BudgetCategory.Expense, SelectedCategories = "房租" }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 当前月(2月)的交易数据
|
|
|
|
|
|
var currentTransactions = new Dictionary<(string, TransactionType), decimal>
|
|
|
|
|
|
{
|
|
|
|
|
|
{ ("工资", TransactionType.Income), 11000m },
|
|
|
|
|
|
{ ("房租", TransactionType.Expense), 3000m },
|
|
|
|
|
|
{ ("转账到公共", TransactionType.Expense), 5000m } // 2月份实际存款:5000元
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 一月份的归档数据
|
|
|
|
|
|
var archives = new List<BudgetArchive>
|
|
|
|
|
|
{
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Year = year, Month = 1,
|
|
|
|
|
|
Content =
|
|
|
|
|
|
[
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1,
|
|
|
|
|
|
Name = "工资",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 11000,
|
|
|
|
|
|
Actual = 11000,
|
|
|
|
|
|
Category = BudgetCategory.Income
|
|
|
|
|
|
},
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 2,
|
|
|
|
|
|
Name = "房租",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 3000,
|
|
|
|
|
|
Actual = 3000,
|
|
|
|
|
|
Category = BudgetCategory.Expense
|
|
|
|
|
|
},
|
|
|
|
|
|
new BudgetArchiveContent
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = -2,
|
|
|
|
|
|
Name = "月度存款计划",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = 6287.34m,
|
|
|
|
|
|
Actual = 9600.0m, // ✅ 一月份实际存款:9600元
|
|
|
|
|
|
Category = BudgetCategory.Savings
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_budgetRepository.GetAllAsync().Returns(budgets);
|
|
|
|
|
|
_transactionStatisticsService.GetAmountGroupByClassifyAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
|
|
|
|
|
.Returns(currentTransactions);
|
|
|
|
|
|
_budgetArchiveRepository.GetArchivesByYearAsync(year).Returns(archives);
|
|
|
|
|
|
_configService.GetConfigByKeyAsync<string>("SavingsCategories").Returns(Task.FromResult<string?>("转账到公共"));
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
var result = await _service.GetSavingsDtoAsync(BudgetPeriodType.Year, new DateTime(year, 1, 1));
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
result.Should().NotBeNull();
|
|
|
|
|
|
// 年度已存款 = 一月份归档存款(9600) + 二月份当前存款(5000) = 14600
|
|
|
|
|
|
result.Current.Should().Be(14600m);
|
|
|
|
|
|
}
|
2026-01-20 19:11:05 +08:00
|
|
|
|
}
|