using Microsoft.Extensions.Logging; using Common; namespace WebApi.Test.Budget; public class BudgetTest : BaseTest { private readonly IBudgetRepository _budgetRepository = Substitute.For(); private readonly IBudgetArchiveRepository _budgetArchiveRepository = Substitute.For(); private readonly ITransactionRecordRepository _transactionsRepository = Substitute.For(); private readonly IOpenAiService _openAiService = Substitute.For(); private readonly IMessageService _messageService = Substitute.For(); private readonly ILogger _logger = Substitute.For>(); private readonly IBudgetSavingsService _budgetSavingsService = Substitute.For(); private readonly IDateTimeProvider _dateTimeProvider = Substitute.For(); private readonly BudgetService _service; public BudgetTest() { _dateTimeProvider.Now.Returns(new DateTime(2024, 1, 15)); _service = new BudgetService( _budgetRepository, _budgetArchiveRepository, _transactionsRepository, _openAiService, _messageService, _logger, _budgetSavingsService, _dateTimeProvider ); } [Fact] public async Task GetCategoryStats_月度_Test() { // Arrange var referenceDate = new DateTime(2024, 1, 15); var budgets = new List { new() { Id = 1, Name = "吃喝", Limit = 2000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "餐饮,零食" }, new() { Id = 2, Name = "交通", Limit = 500, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "交通" } }; _budgetRepository.GetAllAsync().Returns(budgets); _budgetRepository.GetCurrentAmountAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(args => { var b = (BudgetRecord)args[0]; return b.Name == "吃喝" ? 1200m : 300m; }); _transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new Dictionary()); // Act var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate); // Assert result.Month.Limit.Should().Be(2500); // 吃喝2000 + 交通500 result.Month.Current.Should().Be(1500); // 吃喝1200 + 交通300 } [Fact] public async Task GetCategoryStats_月度_硬性收支_Test() { // Arrange var referenceDate = new DateTime(2024, 1, 15); var budgets = new List { new() { Id = 1, Name = "房租", Limit = 3100, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, IsMandatoryExpense = true } }; _budgetRepository.GetAllAsync().Returns(budgets); _budgetRepository.GetCurrentAmountAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(0m); // 实际支出的金额为0 _dateTimeProvider.Now.Returns(referenceDate); _transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new Dictionary()); // Act var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate); // Assert // 1月有31天,15号经过了15天 // 3100 * 15 / 31 = 1500 result.Month.Limit.Should().Be(3100); result.Month.Current.Should().Be(1500); } [Fact] public async Task GetCategoryStats_年度_1月_Test() { // Arrange var referenceDate = new DateTime(2024, 1, 15); var budgets = new List { new() { Id = 1, Name = "年度旅游", Limit = 12000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year } }; _budgetRepository.GetAllAsync().Returns(budgets); _budgetRepository.GetCurrentAmountAsync(Arg.Any(), Arg.Is(d => d.Month == 1 && d.Day == 1), Arg.Is(d => d.Month == 12 && d.Day == 31)) .Returns(2000m); _transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new Dictionary()); // Act var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate); // Assert // 月度统计中,年度预算被忽略 (Limit=0) result.Month.Limit.Should().Be(0); result.Month.Current.Should().Be(0); // 年度统计中 result.Year.Limit.Should().Be(12000); result.Year.Current.Should().Be(2000); } [Fact] public async Task GetCategoryStats_年度_1月_硬性收支_Test() { // Arrange var referenceDate = new DateTime(2024, 1, 1); // 元旦 var budgets = new List { new() { Id = 1, Name = "年度固定支出", Limit = 3660, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year, IsMandatoryExpense = true } }; _budgetRepository.GetAllAsync().Returns(budgets); _budgetRepository.GetCurrentAmountAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(0m); _dateTimeProvider.Now.Returns(referenceDate); _transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new Dictionary()); // Act var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate); // Assert // 2024是闰年,366天。1月1号是第1天。 // 3660 * 1 / 366 = 10 result.Year.Limit.Should().Be(3660); result.Year.Current.Should().Be(10); } [Fact] public async Task GetCategoryStats_年度_3月_硬性收支_Test() { // Arrange var referenceDate = new DateTime(2024, 3, 31); // 3月最后一天 var budgets = new List { new() { Id = 1, Name = "年度固定支出", Limit = 3660, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year, IsMandatoryExpense = true } }; _budgetRepository.GetAllAsync().Returns(budgets); _budgetRepository.GetCurrentAmountAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(0m); _dateTimeProvider.Now.Returns(referenceDate); _transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new Dictionary()); // Act var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate); // Assert // 2024是闰年。1月(31) + 2月(29) + 3月(31) = 91天 // 3660 * 91 / 366 = 910 result.Year.Limit.Should().Be(3660); result.Year.Current.Should().Be(910); } [Fact] public async Task GetCategoryStats_月度_发生年度收支_Test() { // Arrange var referenceDate = new DateTime(2024, 1, 15); // 设置预算:包含月度预算和年度预算 var budgets = new List { // 月度预算:吃喝 new() { Id = 1, Name = "吃喝", Limit = 2000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "餐饮,零食" }, // 月度预算:交通 new() { Id = 2, Name = "交通", Limit = 500, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "交通" }, // 年度预算:年度旅游(当前月度发生了相关支出) new() { Id = 3, Name = "年度旅游", Limit = 12000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year, SelectedCategories = "旅游,度假" }, // 年度预算:年度奖金(当前月度发生了相关收入) new() { Id = 4, Name = "年度奖金", Limit = 50000, Category = BudgetCategory.Income, Type = BudgetPeriodType.Year, SelectedCategories = "奖金,年终奖" } }; _budgetRepository.GetAllAsync().Returns(budgets); // 设置月度预算的当前金额 _budgetRepository.GetCurrentAmountAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(args => { var b = (BudgetRecord)args[0]; return b.Name switch { "吃喝" => 1200m, // 月度预算:已用1200元 "交通" => 300m, // 月度预算:已用300元 "年度旅游" => 2000m, // 年度预算:1月份已用2000元 "年度奖金" => 10000m, // 年度预算:1月份已收10000元 _ => 0m }; }); // 设置趋势统计数据为空(简化测试) _transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new Dictionary()); // Act - 测试支出统计 var expenseResult = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate); // Act - 测试收入统计 var incomeResult = await _service.GetCategoryStatsAsync(BudgetCategory.Income, referenceDate); // Assert - 支出月度统计:只包含月度预算 expenseResult.Month.Limit.Should().Be(2500); // 吃喝2000 + 交通500 expenseResult.Month.Current.Should().Be(1500); // 吃喝1200 + 交通300 expenseResult.Month.Count.Should().Be(2); // 只包含2个月度预算 expenseResult.Month.Rate.Should().Be(1500m / 2500m * 100); // 60% // Assert - 支出年度统计:包含所有预算(月度+年度) expenseResult.Year.Limit.Should().Be(12000 + (2500 * 12)); // 年度旅游12000 + 月度预算折算为年度(2500*12) expenseResult.Year.Current.Should().Be(2000 + 1500); // 年度旅游2000 + 月度预算1500 expenseResult.Year.Count.Should().Be(3); // 包含3个预算(2个月度+1个年度) // Assert - 收入月度统计:只包含月度预算(这里没有月度收入预算,所以应该为0) incomeResult.Month.Limit.Should().Be(0); // 没有月度收入预算 incomeResult.Month.Current.Should().Be(0); // 没有月度收入预算 incomeResult.Month.Count.Should().Be(0); // 没有月度收入预算 // Assert - 收入年度统计:包含所有预算(只有年度收入预算) incomeResult.Year.Limit.Should().Be(50000); // 年度奖金50000 incomeResult.Year.Current.Should().Be(10000); // 年度奖金已收10000 incomeResult.Year.Count.Should().Be(1); // 包含1个年度收入预算 } }