549 lines
26 KiB
C#
549 lines
26 KiB
C#
using Service.AI;
|
||
using Service.Message;
|
||
using Service.Transaction;
|
||
|
||
namespace WebApi.Test.Budget;
|
||
|
||
public class BudgetStatsTest : BaseTest
|
||
{
|
||
private readonly IBudgetRepository _budgetRepository = Substitute.For<IBudgetRepository>();
|
||
private readonly IBudgetArchiveRepository _budgetArchiveRepository = Substitute.For<IBudgetArchiveRepository>();
|
||
private readonly ITransactionRecordRepository _transactionsRepository = Substitute.For<ITransactionRecordRepository>();
|
||
private readonly ITransactionStatisticsService _transactionStatisticsService = Substitute.For<ITransactionStatisticsService>();
|
||
private readonly ISmartHandleService _smartHandleService = Substitute.For<ISmartHandleService>();
|
||
private readonly IMessageService _messageService = Substitute.For<IMessageService>();
|
||
private readonly ILogger<BudgetService> _logger = Substitute.For<ILogger<BudgetService>>();
|
||
private readonly IBudgetSavingsService _budgetSavingsService = Substitute.For<IBudgetSavingsService>();
|
||
private readonly IDateTimeProvider _dateTimeProvider = Substitute.For<IDateTimeProvider>();
|
||
private readonly BudgetService _service;
|
||
|
||
public BudgetStatsTest()
|
||
{
|
||
_dateTimeProvider.Now.Returns(new DateTime(2024, 1, 15));
|
||
|
||
IBudgetStatsService budgetStatsService = new BudgetStatsService(
|
||
_budgetRepository,
|
||
_budgetArchiveRepository,
|
||
_transactionStatisticsService,
|
||
_dateTimeProvider,
|
||
Substitute.For<ILogger<BudgetStatsService>>()
|
||
);
|
||
|
||
_service = new BudgetService(
|
||
_budgetRepository,
|
||
_budgetArchiveRepository,
|
||
_transactionsRepository,
|
||
_transactionStatisticsService,
|
||
_smartHandleService,
|
||
_messageService,
|
||
_logger,
|
||
_budgetSavingsService,
|
||
_dateTimeProvider,
|
||
budgetStatsService
|
||
);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task GetCategoryStats_月度_Test()
|
||
{
|
||
// Arrange
|
||
var referenceDate = new DateTime(2024, 1, 15);
|
||
var budgets = new List<BudgetRecord>
|
||
{
|
||
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<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||
.Returns(args =>
|
||
{
|
||
var b = (BudgetRecord)args[0];
|
||
return b.Name == "吃喝" ? 1200m : 300m;
|
||
});
|
||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
|
||
.Returns(new Dictionary<DateTime, decimal>
|
||
{
|
||
{ new DateTime(2024, 1, 15), 1500m } // 1月15日累计1500(吃喝1200+交通300)
|
||
});
|
||
|
||
// 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<BudgetRecord>
|
||
{
|
||
new() { Id = 1, Name = "房租", Limit = 3100, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, IsMandatoryExpense = true }
|
||
};
|
||
|
||
_budgetRepository.GetAllAsync().Returns(budgets);
|
||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||
.Returns(0m); // 实际支出的金额为0
|
||
|
||
_dateTimeProvider.Now.Returns(referenceDate);
|
||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
|
||
.Returns(new Dictionary<DateTime, decimal>());
|
||
|
||
// Act
|
||
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
|
||
|
||
// Assert
|
||
// 硬性预算的限额保持不变,不根据时间计算
|
||
result.Month.Limit.Should().Be(3100);
|
||
// 实际使用值根据时间计算:1月有31天,15号经过了15天
|
||
// 3100 * 15 / 31 ≈ 1500
|
||
result.Month.Current.Should().BeApproximately(1500, 1);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task GetCategoryStats_年度_1月_Test()
|
||
{
|
||
// Arrange
|
||
var referenceDate = new DateTime(2024, 1, 15);
|
||
var budgets = new List<BudgetRecord>
|
||
{
|
||
new() { Id = 2, Name = "月度吃饭", Limit = 3000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "餐饮" },
|
||
new() { Id = 1, Name = "年度旅游", Limit = 12000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year, SelectedCategories = "旅游" }
|
||
};
|
||
|
||
_budgetRepository.GetAllAsync().Returns(budgets);
|
||
|
||
// 月度统计使用趋势统计数据(只包含月度预算的分类)
|
||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(
|
||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1 && d.Day == 1),
|
||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1 && d.Day == 31),
|
||
TransactionType.Expense,
|
||
Arg.Is<List<string>>(list => list.Count == 1 && list.Contains("餐饮")))
|
||
.Returns(new Dictionary<DateTime, decimal>
|
||
{
|
||
{ new DateTime(2024, 1, 15), 800m } // 1月15日月度吃饭累计800
|
||
});
|
||
|
||
// 年度统计使用GetCurrentAmountAsync
|
||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||
.Returns(args =>
|
||
{
|
||
var b = (BudgetRecord)args[0];
|
||
var startDate = (DateTime)args[1];
|
||
var endDate = (DateTime)args[2];
|
||
|
||
// 月度范围查询 - 月度吃饭(1月)
|
||
if (startDate.Month == 1 && startDate.Day == 1 && endDate.Month == 1)
|
||
{
|
||
return b.Name == "月度吃饭" ? 800m : 0m;
|
||
}
|
||
|
||
// 年度范围查询 - 年度旅游
|
||
if (startDate.Month == 1 && startDate.Day == 1 && endDate.Month == 12)
|
||
{
|
||
return b.Name == "年度旅游" ? 2000m : 0m;
|
||
}
|
||
|
||
return 0m;
|
||
});
|
||
|
||
// 年度趋势统计(包含所有分类)
|
||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(
|
||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1 && d.Day == 1),
|
||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 12 && d.Day == 31),
|
||
TransactionType.Expense,
|
||
Arg.Is<List<string>>(list => list.Count == 2), // 包含所有分类:餐饮、旅游
|
||
true)
|
||
.Returns(new Dictionary<DateTime, decimal>
|
||
{
|
||
{ new DateTime(2024, 1, 1), 2800m } // 1月累计:月度吃饭800 + 年度旅游2000 = 2800
|
||
});
|
||
|
||
// Act
|
||
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
|
||
|
||
// Assert
|
||
// 月度统计中:只包含月度预算
|
||
result.Month.Limit.Should().Be(3000); // 月度吃饭3000
|
||
result.Month.Current.Should().Be(800); // 月度吃饭已用800(从GetCurrentAmountAsync获取)
|
||
result.Month.Count.Should().Be(1); // 只包含1个月度预算
|
||
|
||
// 年度统计中:包含所有预算(月度预算按剩余月份折算)
|
||
// 1月时,月度预算分为:当前月(1月) + 剩余月份(2-12月共11个月)
|
||
result.Year.Limit.Should().Be(12000 + (3000 * 12)); // 年度旅游12000 + 月度吃饭折算年度(3000*12=36000) = 48000
|
||
result.Year.Current.Should().Be(2000 + 800); // 年度旅游2000 + 月度吃饭800 = 2800
|
||
result.Year.Count.Should().Be(3); // 包含3个预算项:年度旅游、月度吃饭(当前月)、月度吃饭(剩余11个月)
|
||
}
|
||
|
||
[Fact]
|
||
public async Task GetCategoryStats_年度_1月_硬性收支_Test()
|
||
{
|
||
// Arrange
|
||
var referenceDate = new DateTime(2024, 1, 1); // 元旦
|
||
var budgets = new List<BudgetRecord>
|
||
{
|
||
new() { Id = 1, Name = "年度固定支出", Limit = 3660, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year, IsMandatoryExpense = true }
|
||
};
|
||
|
||
_budgetRepository.GetAllAsync().Returns(budgets);
|
||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>()).Returns(0m);
|
||
_dateTimeProvider.Now.Returns(referenceDate);
|
||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
|
||
.Returns(new Dictionary<DateTime, decimal>());
|
||
|
||
// Act
|
||
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
|
||
|
||
// Assert
|
||
// 硬性预算的限额保持不变
|
||
result.Year.Limit.Should().Be(3660);
|
||
// 实际使用值根据时间计算:2024是闰年,366天。1月1号是第1天。
|
||
// 3660 * 1 / 366 ≈ 10
|
||
result.Year.Current.Should().BeApproximately(10, 0.1m);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task GetCategoryStats_年度_3月_硬性收支_Test()
|
||
{
|
||
// Arrange
|
||
var referenceDate = new DateTime(2024, 3, 31); // 3月最后一天
|
||
var budgets = new List<BudgetRecord>
|
||
{
|
||
new() { Id = 1, Name = "年度固定支出", Limit = 3660, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year, IsMandatoryExpense = true }
|
||
};
|
||
|
||
_budgetRepository.GetAllAsync().Returns(budgets);
|
||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>()).Returns(0m);
|
||
_dateTimeProvider.Now.Returns(referenceDate);
|
||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
|
||
.Returns(new Dictionary<DateTime, decimal>());
|
||
|
||
// Act
|
||
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
|
||
|
||
// Assert
|
||
// 硬性预算的限额保持不变
|
||
result.Year.Limit.Should().Be(3660);
|
||
// 实际使用值根据时间计算:2024是闰年。1月(31) + 2月(29) + 3月(31) = 91天
|
||
// 3660 * 91 / 366 ≈ 910
|
||
result.Year.Current.Should().BeApproximately(910, 1);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task GetCategoryStats_月度_发生年度收支_Test()
|
||
{
|
||
// Arrange
|
||
var referenceDate = new DateTime(2024, 1, 15);
|
||
|
||
// 设置预算:包含月度预算和年度预算
|
||
var budgets = new List<BudgetRecord>
|
||
{
|
||
// 月度预算:吃喝
|
||
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<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||
.Returns(args =>
|
||
{
|
||
var b = (BudgetRecord)args[0];
|
||
return b.Name switch
|
||
{
|
||
"吃喝" => 1200m, // 月度预算:已用1200元
|
||
"交通" => 300m, // 月度预算:已用300元
|
||
"年度旅游" => 2000m, // 年度预算:1月份已用2000元
|
||
"年度奖金" => 10000m, // 年度预算:1月份已收10000元
|
||
_ => 0m
|
||
};
|
||
});
|
||
|
||
// 设置月度趋势统计数据:只包含月度预算相关的分类(餐饮、零食、交通)
|
||
// 注意:不应包含年度预算的分类(旅游、度假、奖金、年终奖)
|
||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(
|
||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1),
|
||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1),
|
||
TransactionType.Expense,
|
||
Arg.Is<List<string>>(list => list.Count == 3 && list.Contains("餐饮") && list.Contains("零食") && list.Contains("交通")))
|
||
.Returns(new Dictionary<DateTime, decimal>
|
||
{
|
||
{ new DateTime(2024, 1, 15), 1500m } // 1月15日累计1500(吃喝1200+交通300,不包含年度旅游2000)
|
||
});
|
||
|
||
// 设置年度趋势统计数据:包含所有预算相关的分类
|
||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(
|
||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1 && d.Day == 1),
|
||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 12 && d.Day == 31),
|
||
TransactionType.Expense,
|
||
Arg.Is<List<string>>(list => list.Count == 5), // 餐饮、零食、交通、旅游、度假
|
||
true)
|
||
.Returns(new Dictionary<DateTime, decimal>
|
||
{
|
||
{ new DateTime(2024, 1, 1), 3500m } // 1月累计3500(吃喝1200+交通300+年度旅游2000)
|
||
});
|
||
|
||
// 设置收入相关的趋势统计数据
|
||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(
|
||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1),
|
||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1),
|
||
TransactionType.Income,
|
||
Arg.Any<List<string>>())
|
||
.Returns(new Dictionary<DateTime, decimal>()); // 月度收入为空
|
||
|
||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(
|
||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1 && d.Day == 1),
|
||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 12 && d.Day == 31),
|
||
TransactionType.Income,
|
||
Arg.Any<List<string>>(),
|
||
true)
|
||
.Returns(new Dictionary<DateTime, decimal>
|
||
{
|
||
{ new DateTime(2024, 1, 1), 10000m } // 年度奖金10000
|
||
});
|
||
|
||
// 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(不包含年度旅游2000)
|
||
expenseResult.Month.Count.Should().Be(2); // 只包含2个月度预算
|
||
expenseResult.Month.Rate.Should().Be(1500m / 2500m * 100); // 60%
|
||
|
||
// Assert - 支出年度统计:包含所有预算(月度+年度)
|
||
// 1月时,月度预算分为:当前月(1月) + 剩余月份(2-12月共11个月)
|
||
expenseResult.Year.Limit.Should().Be(12000 + (2500 * 12)); // 年度旅游12000 + 月度预算折算为年度(2500*12)
|
||
expenseResult.Year.Current.Should().Be(3500); // 吃喝1200 + 交通300 + 年度旅游2000
|
||
expenseResult.Year.Count.Should().Be(5); // 包含5个预算项:年度旅游、吃喝(当前月)、交通(当前月)、吃喝(剩余11个月)、交通(剩余11个月)
|
||
|
||
// Assert - 收入月度统计:只包含月度预算(这里没有月度收入预算,所以应该为0)
|
||
incomeResult.Month.Limit.Should().Be(0); // 没有月度收入预算
|
||
incomeResult.Month.Current.Should().Be(0); // 没有月度收入预算(不包含年度奖金10000)
|
||
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个年度收入预算
|
||
}
|
||
|
||
|
||
[Fact]
|
||
public async Task GetCategoryStats_年度_3月_2月预算变更_Test()
|
||
{
|
||
// Arrange
|
||
// 测试场景:2024年3月查看年度预算统计,其中2月份发生了预算变更(吃喝预算从2000增加到2500)
|
||
var referenceDate = new DateTime(2024, 3, 15);
|
||
|
||
// 设置当前时间,确保3月被认为是当前月份
|
||
_dateTimeProvider.Now.Returns(new DateTime(2024, 3, 15));
|
||
|
||
// 当前3月份有效的预算
|
||
var currentBudgets = new List<BudgetRecord>
|
||
{
|
||
new() { Id = 1, Name = "吃喝", Limit = 2500, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "餐饮,零食" }, // 2月预算变更后
|
||
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 = "旅游,度假" }
|
||
};
|
||
|
||
// 2月份的归档数据(预算变更前)
|
||
var febArchive = new BudgetArchive
|
||
{
|
||
Year = 2024,
|
||
Month = 2,
|
||
Content = new[]
|
||
{
|
||
new BudgetArchiveContent
|
||
{
|
||
Id = 1,
|
||
Name = "吃喝",
|
||
Limit = 2000, // 2月份时预算还是2000
|
||
Actual = 1800, // 2月份实际花费1800
|
||
Category = BudgetCategory.Expense,
|
||
Type = BudgetPeriodType.Month,
|
||
SelectedCategories = new[] { "餐饮", "零食" }
|
||
},
|
||
new BudgetArchiveContent
|
||
{
|
||
Id = 2,
|
||
Name = "交通",
|
||
Limit = 500,
|
||
Actual = 300,
|
||
Category = BudgetCategory.Expense,
|
||
Type = BudgetPeriodType.Month,
|
||
SelectedCategories = new[] { "交通" }
|
||
}
|
||
}
|
||
};
|
||
|
||
// 1月份的归档数据
|
||
var janArchive = new BudgetArchive
|
||
{
|
||
Year = 2024,
|
||
Month = 1,
|
||
Content = new[]
|
||
{
|
||
new BudgetArchiveContent
|
||
{
|
||
Id = 1,
|
||
Name = "吃喝",
|
||
Limit = 2000, // 1月份预算也是2000
|
||
Actual = 1500, // 1月份实际花费1500
|
||
Category = BudgetCategory.Expense,
|
||
Type = BudgetPeriodType.Month,
|
||
SelectedCategories = new[] { "餐饮", "零食" }
|
||
},
|
||
new BudgetArchiveContent
|
||
{
|
||
Id = 2,
|
||
Name = "交通",
|
||
Limit = 500,
|
||
Actual = 250,
|
||
Category = BudgetCategory.Expense,
|
||
Type = BudgetPeriodType.Month,
|
||
SelectedCategories = new[] { "交通" }
|
||
}
|
||
}
|
||
};
|
||
|
||
// 设置仓储响应
|
||
_budgetRepository.GetAllAsync().Returns(currentBudgets);
|
||
|
||
// 设置归档仓储响应
|
||
_budgetArchiveRepository.GetArchiveAsync(2024, 2).Returns(febArchive);
|
||
_budgetArchiveRepository.GetArchiveAsync(2024, 1).Returns(janArchive);
|
||
|
||
// 设置月度预算的当前金额查询(仅用于3月份)
|
||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Is<DateTime>(d => d.Month == 3), Arg.Is<DateTime>(d => d.Month == 3))
|
||
.Returns(args =>
|
||
{
|
||
var b = (BudgetRecord)args[0];
|
||
return b.Name switch
|
||
{
|
||
"吃喝" => 800m, // 3月份已花费800
|
||
"交通" => 200m, // 3月份已花费200
|
||
_ => 0m
|
||
};
|
||
});
|
||
|
||
// 年度旅游的年度金额查询
|
||
_budgetRepository.GetCurrentAmountAsync(
|
||
Arg.Is<BudgetRecord>(b => b.Id == 3),
|
||
Arg.Is<DateTime>(d => d.Month == 1),
|
||
Arg.Is<DateTime>(d => d.Month == 12))
|
||
.Returns(2500m); // 年度旅游1-3月已花费2500
|
||
|
||
// 设置趋势统计数据查询(用于月度统计)
|
||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(
|
||
Arg.Is<DateTime>(d => d.Month == 3),
|
||
Arg.Is<DateTime>(d => d.Month == 3),
|
||
Arg.Any<TransactionType>(),
|
||
Arg.Any<List<string>>(),
|
||
Arg.Any<bool>())
|
||
.Returns(new Dictionary<DateTime, decimal>
|
||
{
|
||
{ new DateTime(2024, 3, 15), 1000m } // 3月15日累计1000(吃喝800+交通200)
|
||
});
|
||
|
||
// 年度趋势统计数据查询
|
||
// 注意:年度统计使用GetFilteredTrendStatisticsAsync获取趋势数据
|
||
// 需要返回所有分类的累计金额,包括年度旅游的2500
|
||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(
|
||
Arg.Is<DateTime>(d => d.Month == 1),
|
||
Arg.Is<DateTime>(d => d.Month == 12),
|
||
Arg.Any<TransactionType>(),
|
||
Arg.Any<List<string>>(),
|
||
Arg.Is<bool>(b => b == true))
|
||
.Returns(new Dictionary<DateTime, decimal>
|
||
{
|
||
// 3月累计:月度预算1000 + 年度旅游2500 = 3500
|
||
{ new DateTime(2024, 3, 1), 3500m }
|
||
});
|
||
|
||
// 补充:年度旅游的GetCurrentAmountAsync调用(用于计算Current)
|
||
_budgetRepository.GetCurrentAmountAsync(
|
||
Arg.Is<BudgetRecord>(b => b.Id == 3),
|
||
Arg.Is<DateTime>(d => d.Month == 1),
|
||
Arg.Is<DateTime>(d => d.Month == 12))
|
||
.Returns(2500m); // 年度旅游1-3月已花费2500
|
||
|
||
// Act
|
||
// 直接测试BudgetStatsService,而不是通过BudgetService
|
||
var budgetStatsService = new BudgetStatsService(
|
||
_budgetRepository,
|
||
_budgetArchiveRepository,
|
||
_transactionStatisticsService,
|
||
_dateTimeProvider,
|
||
Substitute.For<ILogger<BudgetStatsService>>()
|
||
);
|
||
|
||
var result = await budgetStatsService.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
|
||
|
||
// Assert - 月度统计(3月份)
|
||
// 月度统计应该只包含月度预算,使用当前预算限额
|
||
result.Month.Limit.Should().Be(3000); // 吃喝2500 + 交通500(使用变更后的预算)
|
||
result.Month.Current.Should().Be(1000); // 吃喝800 + 交通200
|
||
result.Month.Count.Should().Be(2); // 包含2个月度预算
|
||
|
||
// Assert - 年度统计(需要考虑预算变更和剩余月份)
|
||
// 新逻辑:
|
||
// 1. 对于归档数据,直接使用归档的限额,不折算
|
||
// 2. 对于当前及未来月份,使用当前预算 × 剩余月份
|
||
//
|
||
// 预期年度限额计算:
|
||
// 1月归档:吃喝2000 + 交通500 = 2500
|
||
// 2月归档:吃喝2000 + 交通500 = 2500
|
||
// 3-12月剩余(12 - 3 + 1 = 10个月):吃喝2500×10 + 交通500×10 = 30000
|
||
// 年度旅游:12000
|
||
// 总计:2500 + 2500 + 30000 + 12000 = 47000
|
||
result.Year.Limit.Should().Be(47000);
|
||
|
||
// 预期年度实际金额:
|
||
// 根据趋势统计数据,3月累计: 月度预算1000 + 年度旅游2500 = 3500
|
||
// 但业务代码会累加所有预算项的Current值:
|
||
// - 1月归档吃喝:1500
|
||
// - 1月归档交通:250
|
||
// - 2月归档吃喝:1800
|
||
// - 2月归档交通:300
|
||
// - 3月吃喝:800
|
||
// - 3月交通:200
|
||
// - 年度旅游:2500
|
||
// 总计:1500+250+1800+300+800+200+2500 = 7350
|
||
result.Year.Current.Should().Be(7350);
|
||
|
||
// 应该包含:
|
||
// - 1月归档的月度预算:吃喝、1个
|
||
// - 1月归档的月度预算:交通、1个
|
||
// - 2月归档的月度预算:吃喝、1个
|
||
// - 2月归档的月度预算:交通、1个
|
||
// - 3月当前月的月度预算:吃喝、1个
|
||
// - 3月当前月的月度预算:交通、1个
|
||
// - 4-12月未来月的月度预算:吃喝、1个(RemainingMonths=9)
|
||
// - 4-12月未来月的月度预算:交通、1个(RemainingMonths=9)
|
||
// - 年度旅游:1个
|
||
// 总计:9个
|
||
result.Year.Count.Should().Be(9);
|
||
|
||
// 验证使用率计算正确
|
||
result.Month.Rate.Should().BeApproximately(1000m / 3000m * 100, 0.01m);
|
||
// 年度使用率:7350 / 47000 * 100 = 15.64%
|
||
result.Year.Rate.Should().BeApproximately(7350m / 47000m * 100, 0.01m);
|
||
}
|
||
} |