Files
EmailBill/WebApi.Test/Budget/BudgetSavingsTest.cs
SunCheng b71eadd4f9
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 26s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
重构账单查询sql
2026-01-28 10:58:15 +08:00

500 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.
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);
}
}