Files
EmailBill/WebApi.Test/Budget/BudgetSavingsTest.cs
SunCheng 3ed9cf5ebd
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 22s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
重构: 将 LogCleanupService 转为 Quartz Job 服务
- 创建 LogCleanupJob 替代 LogCleanupService (BackgroundService)
- 在 Expand.cs 中注册 LogCleanupJob (每天凌晨2点执行, 保留30天日志)
- 从 Program.cs 移除 LogCleanupService 的 HostedService 注册
- 删除 Service/LogCleanupService.cs
- 删除 Service/PeriodicBillBackgroundService.cs (已无用的重复服务)

所有后台任务现在统一通过 Quartz.NET 管理, 支持运行时控制
2026-01-28 11:19:23 +08:00

502 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Service.Transaction;
namespace WebApi.Test.Budget;
public class BudgetSavingsTest : BaseTest
{
private readonly IBudgetRepository _budgetRepository = Substitute.For<IBudgetRepository>();
private readonly IBudgetArchiveRepository _budgetArchiveRepository = Substitute.For<IBudgetArchiveRepository>();
private readonly ITransactionStatisticsService _transactionStatisticsService = Substitute.For<ITransactionStatisticsService>();
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,
_transactionStatisticsService,
_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);
_transactionStatisticsService.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);
_transactionStatisticsService.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);
_transactionStatisticsService.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);
_transactionStatisticsService.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);
_transactionStatisticsService.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);
_transactionStatisticsService.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);
_transactionStatisticsService.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);
}
}