fix
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 22s
Docker Build & Deploy / Deploy to Production (push) Successful in 5s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s

This commit is contained in:
SunCheng
2026-01-21 18:52:31 +08:00
parent b2e903e968
commit 64aa24c07b
6 changed files with 326 additions and 127 deletions

View File

@@ -0,0 +1,500 @@
namespace WebApi.Test.Budget;
public class BudgetSavingsTest : BaseTest
{
private readonly IBudgetRepository _budgetRepository = Substitute.For<IBudgetRepository>();
private readonly IBudgetArchiveRepository _budgetArchiveRepository = Substitute.For<IBudgetArchiveRepository>();
private readonly ITransactionRecordRepository _transactionsRepository = Substitute.For<ITransactionRecordRepository>();
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,
_transactionsRepository,
_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);
_transactionsRepository.GetAmountGroupByClassifyAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>())
.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);
_transactionsRepository.GetAmountGroupByClassifyAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>())
.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);
_transactionsRepository.GetAmountGroupByClassifyAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>())
.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);
_transactionsRepository.GetAmountGroupByClassifyAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>())
.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);
_transactionsRepository.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();
// 归档实际收入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);
_transactionsRepository.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();
// 归档实际收入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);
_transactionsRepository.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();
// 归档实际收入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);
}
}

View File

@@ -0,0 +1,172 @@
using Microsoft.Extensions.Logging;
using Common;
namespace WebApi.Test.Budget;
public class BudgetTest : BaseTest
{
private readonly IBudgetRepository _budgetRepository = Substitute.For<IBudgetRepository>();
private readonly IBudgetArchiveRepository _budgetArchiveRepository = Substitute.For<IBudgetArchiveRepository>();
private readonly ITransactionRecordRepository _transactionsRepository = Substitute.For<ITransactionRecordRepository>();
private readonly IOpenAiService _openAiService = Substitute.For<IOpenAiService>();
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 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<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;
});
_transactionsRepository.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(2500);
result.Month.Current.Should().Be(1500);
result.Month.Count.Should().Be(2);
result.Month.Rate.Should().Be(1500m / 2500m * 100);
}
[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);
_transactionsRepository.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
// 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<BudgetRecord>
{
new() { Id = 1, Name = "年度旅游", Limit = 12000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year }
};
_budgetRepository.GetAllAsync().Returns(budgets);
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Is<DateTime>(d => d.Month == 1 && d.Day == 1), Arg.Is<DateTime>(d => d.Month == 12 && d.Day == 31))
.Returns(2000m);
_transactionsRepository.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
// 月度统计中,年度预算被忽略 (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<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);
_transactionsRepository.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
// 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<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);
_transactionsRepository.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
// 2024是闰年。1月(31) + 2月(29) + 3月(31) = 91天
// 3660 * 91 / 366 = 910
result.Year.Limit.Should().Be(3660);
result.Year.Current.Should().Be(910);
}
}