feat(budget): 实现存款明细计算核心逻辑

- 添加 BudgetItemCalculator 辅助类,实现明细项计算规则
  - 收入:实际>0取实际,否则取预算
  - 支出:取MAX(预算, 实际)
  - 硬性支出未发生:按天数折算
  - 归档数据:直接使用实际值

- 实现月度和年度存款核心公式
  - 月度:收入预算 + 本月年度收入 - 支出预算 - 本月年度支出
  - 年度:归档已实收 + 未来收入预算 - 归档已实支 - 未来支出预算

- 定义存款明细数据结构
  - SavingsDetail: 包含收入/支出明细列表和汇总
  - BudgetDetailItem: 预算明细项(含计算用金额、计算说明等)
  - SavingsCalculationSummary: 计算汇总信息

- 新增单元测试
  - BudgetItemCalculatorTest: 11个测试覆盖所有计算规则
  - BudgetSavingsCalculationTest: 6个测试验证核心公式

测试结果:所有测试通过 (366 passed, 0 failed)
This commit is contained in:
SunCheng
2026-02-20 16:26:04 +08:00
parent 32d5ed62d0
commit 4cc205fc25
21 changed files with 1730 additions and 1 deletions

View 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);
}
}

View File

@@ -0,0 +1,135 @@
using Service.Budget;
namespace WebApi.Test.Budget;
/// <summary>
/// 存款计划核心公式单元测试
/// </summary>
public class BudgetSavingsCalculationTest : BaseTest
{
[Fact]
public void _纯月度预算场景()
{
// Arrange
var monthlyIncomeBudget = 15000m; // 工资10000 + 奖金5000
var yearlyIncomeInThisMonth = 0m;
var monthlyExpenseBudget = 5000m; // 房租3000 + 餐饮2000
var yearlyExpenseInThisMonth = 0m;
// Act
var result = BudgetSavingsService.CalculateMonthlyPlannedSavings(
monthlyIncomeBudget,
yearlyIncomeInThisMonth,
monthlyExpenseBudget,
yearlyExpenseInThisMonth
);
// Assert
result.Should().Be(10000m); // 15000 - 5000
}
[Fact]
public void _月度预算加本月发生的年度预算()
{
// Arrange
var monthlyIncomeBudget = 10000m; // 工资
var yearlyIncomeInThisMonth = 0m;
var monthlyExpenseBudget = 3000m; // 房租
var yearlyExpenseInThisMonth = 3000m; // 旅游实际发生
// Act
var result = BudgetSavingsService.CalculateMonthlyPlannedSavings(
monthlyIncomeBudget,
yearlyIncomeInThisMonth,
monthlyExpenseBudget,
yearlyExpenseInThisMonth
);
// Assert
result.Should().Be(4000m); // 10000 - 3000 - 3000
}
[Fact]
public void _年度预算未在本月发生应不计入()
{
// Arrange
var monthlyIncomeBudget = 10000m;
var yearlyIncomeInThisMonth = 0m; // 年终奖未发生
var monthlyExpenseBudget = 3000m;
var yearlyExpenseInThisMonth = 0m; // 旅游未发生
// Act
var result = BudgetSavingsService.CalculateMonthlyPlannedSavings(
monthlyIncomeBudget,
yearlyIncomeInThisMonth,
monthlyExpenseBudget,
yearlyExpenseInThisMonth
);
// Assert
result.Should().Be(7000m); // 10000 - 3000
}
[Fact]
public void _年初无归档数据场景()
{
// Arrange
var archivedIncome = 0m;
var futureIncomeBudget = 120000m; // 10000×12
var archivedExpense = 0m;
var futureExpenseBudget = 36000m; // 3000×12
// Act
var result = BudgetSavingsService.CalculateYearlyPlannedSavings(
archivedIncome,
futureIncomeBudget,
archivedExpense,
futureExpenseBudget
);
// Assert
result.Should().Be(84000m); // 120000 - 36000
}
[Fact]
public void _年中有归档数据场景()
{
// Arrange
var archivedIncome = 29000m; // 1月15000 + 2月14000
var futureIncomeBudget = 100000m; // 10000×10月
var archivedExpense = 10000m; // 1月4800 + 2月5200
var futureExpenseBudget = 30000m; // 3000×10月
// Act
var result = BudgetSavingsService.CalculateYearlyPlannedSavings(
archivedIncome,
futureIncomeBudget,
archivedExpense,
futureExpenseBudget
);
// Assert
result.Should().Be(89000m); // 29000 + 100000 - 10000 - 30000
}
[Fact]
public void _归档数据包含年度预算()
{
// Arrange
var archivedIncome = 15000m;
var futureIncomeBudget = 110000m;
var archivedExpense = 7800m; // 包含1月旅游3000的年度支出
var futureExpenseBudget = 30000m;
// Act
var result = BudgetSavingsService.CalculateYearlyPlannedSavings(
archivedIncome,
futureIncomeBudget,
archivedExpense,
futureExpenseBudget
);
// Assert
result.Should().Be(87200m); // 15000 + 110000 - 7800 - 30000
}
}