feat(budget): 实现存款明细计算核心逻辑
- 添加 BudgetItemCalculator 辅助类,实现明细项计算规则 - 收入:实际>0取实际,否则取预算 - 支出:取MAX(预算, 实际) - 硬性支出未发生:按天数折算 - 归档数据:直接使用实际值 - 实现月度和年度存款核心公式 - 月度:收入预算 + 本月年度收入 - 支出预算 - 本月年度支出 - 年度:归档已实收 + 未来收入预算 - 归档已实支 - 未来支出预算 - 定义存款明细数据结构 - SavingsDetail: 包含收入/支出明细列表和汇总 - BudgetDetailItem: 预算明细项(含计算用金额、计算说明等) - SavingsCalculationSummary: 计算汇总信息 - 新增单元测试 - BudgetItemCalculatorTest: 11个测试覆盖所有计算规则 - BudgetSavingsCalculationTest: 6个测试验证核心公式 测试结果:所有测试通过 (366 passed, 0 failed)
This commit is contained in:
260
WebApi.Test/Budget/BudgetItemCalculatorTest.cs
Normal file
260
WebApi.Test/Budget/BudgetItemCalculatorTest.cs
Normal file
@@ -0,0 +1,260 @@
|
||||
using Service.Budget;
|
||||
|
||||
namespace WebApi.Test.Budget;
|
||||
|
||||
/// <summary>
|
||||
/// BudgetItemCalculator 单元测试
|
||||
/// 测试明细项计算用金额的各种规则
|
||||
/// </summary>
|
||||
public class BudgetItemCalculatorTest : BaseTest
|
||||
{
|
||||
[Fact]
|
||||
public void 收入项实际已发生_应返回实际值()
|
||||
{
|
||||
// Arrange
|
||||
var budgetLimit = 10000m;
|
||||
var actualAmount = 9500m;
|
||||
|
||||
// Act
|
||||
var result = BudgetItemCalculator.CalculateEffectiveAmount(
|
||||
BudgetCategory.Income,
|
||||
budgetLimit,
|
||||
actualAmount,
|
||||
isMandatory: false,
|
||||
isArchived: false,
|
||||
new DateTime(2026, 2, 15),
|
||||
BudgetPeriodType.Month
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(9500m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 收入项实际未发生_应返回预算值()
|
||||
{
|
||||
// Arrange
|
||||
var budgetLimit = 5000m;
|
||||
var actualAmount = 0m;
|
||||
|
||||
// Act
|
||||
var result = BudgetItemCalculator.CalculateEffectiveAmount(
|
||||
BudgetCategory.Income,
|
||||
budgetLimit,
|
||||
actualAmount,
|
||||
isMandatory: false,
|
||||
isArchived: false,
|
||||
new DateTime(2026, 2, 15),
|
||||
BudgetPeriodType.Month
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(5000m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 支出项普通情况_应返回MAX预算和实际()
|
||||
{
|
||||
// Arrange
|
||||
var budgetLimit = 2000m;
|
||||
var actualAmount = 2500m;
|
||||
|
||||
// Act
|
||||
var result = BudgetItemCalculator.CalculateEffectiveAmount(
|
||||
BudgetCategory.Expense,
|
||||
budgetLimit,
|
||||
actualAmount,
|
||||
isMandatory: false,
|
||||
isArchived: false,
|
||||
new DateTime(2026, 2, 15),
|
||||
BudgetPeriodType.Month
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(2500m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 支出项未超预算_应返回预算值()
|
||||
{
|
||||
// Arrange
|
||||
var budgetLimit = 2000m;
|
||||
var actualAmount = 1800m;
|
||||
|
||||
// Act
|
||||
var result = BudgetItemCalculator.CalculateEffectiveAmount(
|
||||
BudgetCategory.Expense,
|
||||
budgetLimit,
|
||||
actualAmount,
|
||||
isMandatory: false,
|
||||
isArchived: false,
|
||||
new DateTime(2026, 2, 15),
|
||||
BudgetPeriodType.Month
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(2000m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 支出项超预算_应返回实际值()
|
||||
{
|
||||
// Arrange
|
||||
var budgetLimit = 2000m;
|
||||
var actualAmount = 2500m;
|
||||
|
||||
// Act
|
||||
var result = BudgetItemCalculator.CalculateEffectiveAmount(
|
||||
BudgetCategory.Expense,
|
||||
budgetLimit,
|
||||
actualAmount,
|
||||
isMandatory: false,
|
||||
isArchived: false,
|
||||
new DateTime(2026, 2, 15),
|
||||
BudgetPeriodType.Month
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(2500m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 支出项硬性且实际为0_月度_应按天数折算()
|
||||
{
|
||||
// Arrange
|
||||
var budgetLimit = 3000m;
|
||||
var actualAmount = 0m;
|
||||
var date = new DateTime(2026, 2, 15); // 2月共28天,当前15号
|
||||
|
||||
// Act
|
||||
var result = BudgetItemCalculator.CalculateEffectiveAmount(
|
||||
BudgetCategory.Expense,
|
||||
budgetLimit,
|
||||
actualAmount,
|
||||
isMandatory: true,
|
||||
isArchived: false,
|
||||
date,
|
||||
BudgetPeriodType.Month
|
||||
);
|
||||
|
||||
// Assert
|
||||
var expected = 3000m / 28 * 15; // ≈ 1607.14
|
||||
result.Should().BeApproximately(expected, 0.01m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 支出项硬性且实际为0_年度_应按天数折算()
|
||||
{
|
||||
// Arrange
|
||||
var budgetLimit = 12000m;
|
||||
var actualAmount = 0m;
|
||||
var date = new DateTime(2026, 2, 15); // 2026年第46天(31+15)
|
||||
|
||||
// Act
|
||||
var result = BudgetItemCalculator.CalculateEffectiveAmount(
|
||||
BudgetCategory.Expense,
|
||||
budgetLimit,
|
||||
actualAmount,
|
||||
isMandatory: true,
|
||||
isArchived: false,
|
||||
date,
|
||||
BudgetPeriodType.Year
|
||||
);
|
||||
|
||||
// Assert
|
||||
var expected = 12000m / 365 * 46; // ≈ 1512.33
|
||||
result.Should().BeApproximately(expected, 0.01m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 支出项硬性且实际大于0_应返回MAX值()
|
||||
{
|
||||
// Arrange
|
||||
var budgetLimit = 3000m;
|
||||
var actualAmount = 3200m;
|
||||
|
||||
// Act
|
||||
var result = BudgetItemCalculator.CalculateEffectiveAmount(
|
||||
BudgetCategory.Expense,
|
||||
budgetLimit,
|
||||
actualAmount,
|
||||
isMandatory: true,
|
||||
isArchived: false,
|
||||
new DateTime(2026, 2, 15),
|
||||
BudgetPeriodType.Month
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(3200m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 归档数据_应直接返回实际值()
|
||||
{
|
||||
// Arrange
|
||||
var budgetLimit = 2000m;
|
||||
var actualAmount = 1800m;
|
||||
|
||||
// Act
|
||||
var result = BudgetItemCalculator.CalculateEffectiveAmount(
|
||||
BudgetCategory.Expense,
|
||||
budgetLimit,
|
||||
actualAmount,
|
||||
isMandatory: false,
|
||||
isArchived: true, // 归档数据
|
||||
new DateTime(2026, 2, 15),
|
||||
BudgetPeriodType.Month
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(1800m); // 归档数据直接返回实际值,不走MAX逻辑
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 闰年2月按天折算边界情况()
|
||||
{
|
||||
// Arrange
|
||||
var budgetLimit = 3000m;
|
||||
var actualAmount = 0m;
|
||||
var date = new DateTime(2024, 2, 29); // 闰年2月29日
|
||||
|
||||
// Act
|
||||
var result = BudgetItemCalculator.CalculateEffectiveAmount(
|
||||
BudgetCategory.Expense,
|
||||
budgetLimit,
|
||||
actualAmount,
|
||||
isMandatory: true,
|
||||
isArchived: false,
|
||||
date,
|
||||
BudgetPeriodType.Month
|
||||
);
|
||||
|
||||
// Assert
|
||||
var expected = 3000m / 29 * 29; // = 3000
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 平年2月按天折算边界情况()
|
||||
{
|
||||
// Arrange
|
||||
var budgetLimit = 3000m;
|
||||
var actualAmount = 0m;
|
||||
var date = new DateTime(2026, 2, 28); // 平年2月28日
|
||||
|
||||
// Act
|
||||
var result = BudgetItemCalculator.CalculateEffectiveAmount(
|
||||
BudgetCategory.Expense,
|
||||
budgetLimit,
|
||||
actualAmount,
|
||||
isMandatory: true,
|
||||
isArchived: false,
|
||||
date,
|
||||
BudgetPeriodType.Month
|
||||
);
|
||||
|
||||
// Assert
|
||||
var expected = 3000m / 28 * 28; // = 3000
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user