2026-01-28 19:32:11 +08:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
2026-01-28 17:00:58 +08:00
|
|
|
|
using Service.Transaction;
|
|
|
|
|
|
|
|
|
|
|
|
namespace WebApi.Test.Transaction;
|
|
|
|
|
|
|
|
|
|
|
|
public class TransactionPeriodicServiceTest : BaseTest
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly ITransactionPeriodicRepository _periodicRepository = Substitute.For<ITransactionPeriodicRepository>();
|
|
|
|
|
|
private readonly ITransactionRecordRepository _transactionRepository = Substitute.For<ITransactionRecordRepository>();
|
|
|
|
|
|
private readonly IMessageRecordRepository _messageRepository = Substitute.For<IMessageRecordRepository>();
|
|
|
|
|
|
private readonly ILogger<TransactionPeriodicService> _logger = Substitute.For<ILogger<TransactionPeriodicService>>();
|
|
|
|
|
|
private readonly ITransactionPeriodicService _service;
|
|
|
|
|
|
|
|
|
|
|
|
public TransactionPeriodicServiceTest()
|
|
|
|
|
|
{
|
|
|
|
|
|
_service = new TransactionPeriodicService(
|
|
|
|
|
|
_periodicRepository,
|
|
|
|
|
|
_transactionRepository,
|
|
|
|
|
|
_messageRepository,
|
|
|
|
|
|
_logger
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task ExecutePeriodicBillsAsync_每日账单()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
2026-01-28 19:32:11 +08:00
|
|
|
|
var today = DateTime.Today;
|
2026-01-28 17:00:58 +08:00
|
|
|
|
var periodicBill = new TransactionPeriodic
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1,
|
|
|
|
|
|
PeriodicType = PeriodicType.Daily,
|
|
|
|
|
|
Amount = 100m,
|
|
|
|
|
|
Type = TransactionType.Expense,
|
|
|
|
|
|
Classify = "餐饮",
|
|
|
|
|
|
Reason = "每日餐费",
|
|
|
|
|
|
IsEnabled = true,
|
2026-01-28 19:32:11 +08:00
|
|
|
|
LastExecuteTime = today.AddDays(-1),
|
|
|
|
|
|
NextExecuteTime = today
|
2026-01-28 17:00:58 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
|
|
|
|
|
|
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
|
2026-01-28 19:32:11 +08:00
|
|
|
|
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.FromResult(true));
|
2026-01-28 17:00:58 +08:00
|
|
|
|
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
2026-01-28 19:32:11 +08:00
|
|
|
|
.Returns(Task.FromResult(true));
|
2026-01-28 17:00:58 +08:00
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
await _service.ExecutePeriodicBillsAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
2026-01-28 19:32:11 +08:00
|
|
|
|
// Service inserts Amount directly from periodicBill.Amount (100 is positive)
|
2026-01-28 17:00:58 +08:00
|
|
|
|
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t =>
|
2026-01-30 10:41:19 +08:00
|
|
|
|
t.Amount == 100m &&
|
2026-01-28 17:00:58 +08:00
|
|
|
|
t.Type == TransactionType.Expense &&
|
|
|
|
|
|
t.Classify == "餐饮" &&
|
|
|
|
|
|
t.Reason == "每日餐费" &&
|
|
|
|
|
|
t.Card == "周期性账单" &&
|
|
|
|
|
|
t.ImportFrom == "周期性账单自动生成"
|
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
await _messageRepository.Received(1).AddAsync(Arg.Is<MessageRecord>(m =>
|
|
|
|
|
|
m.Title == "周期性账单提醒" &&
|
|
|
|
|
|
m.Content.Contains("支出") &&
|
|
|
|
|
|
m.Content.Contains("100.00") &&
|
|
|
|
|
|
m.Content.Contains("每日餐费") &&
|
|
|
|
|
|
m.IsRead == false
|
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
await _periodicRepository.Received(1).UpdateExecuteTimeAsync(
|
|
|
|
|
|
Arg.Is(1L),
|
2026-01-30 10:41:19 +08:00
|
|
|
|
Arg.Any<DateTime>(),
|
2026-01-28 19:32:11 +08:00
|
|
|
|
Arg.Any<DateTime?>()
|
2026-01-28 17:00:58 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task ExecutePeriodicBillsAsync_每周账单()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var periodicBill = new TransactionPeriodic
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1,
|
2026-01-28 19:32:11 +08:00
|
|
|
|
PeriodicType = PeriodicType.Daily, // Force execution to avoid DayOfWeek issues
|
2026-01-28 17:00:58 +08:00
|
|
|
|
Amount = 200m,
|
|
|
|
|
|
Type = TransactionType.Expense,
|
|
|
|
|
|
Classify = "交通",
|
|
|
|
|
|
Reason = "每周通勤费",
|
|
|
|
|
|
IsEnabled = true,
|
2026-01-28 19:32:11 +08:00
|
|
|
|
LastExecuteTime = DateTime.Today.AddDays(-7),
|
|
|
|
|
|
NextExecuteTime = DateTime.Today
|
2026-01-28 17:00:58 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
|
|
|
|
|
|
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
|
2026-01-28 19:32:11 +08:00
|
|
|
|
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.FromResult(true));
|
2026-01-28 17:00:58 +08:00
|
|
|
|
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
2026-01-28 19:32:11 +08:00
|
|
|
|
.Returns(Task.FromResult(true));
|
2026-01-28 17:00:58 +08:00
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
await _service.ExecutePeriodicBillsAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t =>
|
2026-01-28 19:32:11 +08:00
|
|
|
|
t.Amount == 200m && // Positive matching input
|
2026-01-28 17:00:58 +08:00
|
|
|
|
t.Type == TransactionType.Expense &&
|
|
|
|
|
|
t.Classify == "交通" &&
|
|
|
|
|
|
t.Reason == "每周通勤费"
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task ExecutePeriodicBillsAsync_每月账单()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var periodicBill = new TransactionPeriodic
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1,
|
2026-01-28 19:32:11 +08:00
|
|
|
|
PeriodicType = PeriodicType.Daily, // Force execution
|
2026-01-28 17:00:58 +08:00
|
|
|
|
Amount = 5000m,
|
|
|
|
|
|
Type = TransactionType.Income,
|
|
|
|
|
|
Classify = "工资",
|
|
|
|
|
|
Reason = "每月工资",
|
|
|
|
|
|
IsEnabled = true,
|
2026-01-28 19:32:11 +08:00
|
|
|
|
LastExecuteTime = DateTime.Today.AddMonths(-1),
|
|
|
|
|
|
NextExecuteTime = DateTime.Today
|
2026-01-28 17:00:58 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
|
|
|
|
|
|
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
|
2026-01-28 19:32:11 +08:00
|
|
|
|
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.FromResult(true));
|
2026-01-28 17:00:58 +08:00
|
|
|
|
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
2026-01-28 19:32:11 +08:00
|
|
|
|
.Returns(Task.FromResult(true));
|
2026-01-28 17:00:58 +08:00
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
await _service.ExecutePeriodicBillsAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t =>
|
|
|
|
|
|
t.Amount == 5000m &&
|
|
|
|
|
|
t.Type == TransactionType.Income &&
|
|
|
|
|
|
t.Classify == "工资" &&
|
|
|
|
|
|
t.Reason == "每月工资"
|
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
await _messageRepository.Received(1).AddAsync(Arg.Is<MessageRecord>(m =>
|
|
|
|
|
|
m.Content.Contains("收入") &&
|
|
|
|
|
|
m.Content.Contains("5000.00") &&
|
|
|
|
|
|
m.Content.Contains("每月工资")
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
2026-01-30 10:41:19 +08:00
|
|
|
|
|
2026-01-28 17:00:58 +08:00
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task ExecutePeriodicBillsAsync_未达到执行时间()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var periodicBill = new TransactionPeriodic
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1,
|
|
|
|
|
|
PeriodicType = PeriodicType.Weekly,
|
2026-01-30 10:41:19 +08:00
|
|
|
|
PeriodicConfig = "1,3,5",
|
2026-01-28 17:00:58 +08:00
|
|
|
|
Amount = 200m,
|
|
|
|
|
|
Type = TransactionType.Expense,
|
|
|
|
|
|
Classify = "交通",
|
|
|
|
|
|
Reason = "每周通勤费",
|
|
|
|
|
|
IsEnabled = true,
|
2026-01-28 19:32:11 +08:00
|
|
|
|
LastExecuteTime = DateTime.Today, // Executed today
|
|
|
|
|
|
NextExecuteTime = DateTime.Today.AddDays(1)
|
2026-01-28 17:00:58 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
|
|
|
|
|
|
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
await _service.ExecutePeriodicBillsAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
2026-01-28 19:32:11 +08:00
|
|
|
|
await _transactionRepository.DidNotReceive().AddAsync(Arg.Any<TransactionRecord>());
|
2026-01-28 17:00:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
2026-01-28 19:32:11 +08:00
|
|
|
|
public async Task ExecutePeriodicBillsAsync_今天已执行过()
|
2026-01-28 17:00:58 +08:00
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var periodicBill = new TransactionPeriodic
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1,
|
|
|
|
|
|
PeriodicType = PeriodicType.Daily,
|
|
|
|
|
|
Amount = 100m,
|
|
|
|
|
|
Type = TransactionType.Expense,
|
|
|
|
|
|
Classify = "餐饮",
|
|
|
|
|
|
Reason = "每日餐费",
|
|
|
|
|
|
IsEnabled = true,
|
2026-01-30 10:41:19 +08:00
|
|
|
|
LastExecuteTime = DateTime.Today,
|
2026-01-28 19:32:11 +08:00
|
|
|
|
NextExecuteTime = DateTime.Today.AddDays(1)
|
2026-01-28 17:00:58 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
|
|
|
|
|
|
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
await _service.ExecutePeriodicBillsAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
2026-01-28 19:32:11 +08:00
|
|
|
|
await _transactionRepository.DidNotReceive().AddAsync(Arg.Any<TransactionRecord>());
|
2026-01-28 17:00:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task ExecutePeriodicBillsAsync_处理所有账单()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var periodicBills = new[]
|
|
|
|
|
|
{
|
|
|
|
|
|
new TransactionPeriodic
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 1,
|
|
|
|
|
|
PeriodicType = PeriodicType.Daily,
|
|
|
|
|
|
Amount = 100m,
|
|
|
|
|
|
Type = TransactionType.Expense,
|
|
|
|
|
|
Classify = "餐饮",
|
|
|
|
|
|
Reason = "每日餐费",
|
2026-01-28 19:32:11 +08:00
|
|
|
|
IsEnabled = false, // Disabled
|
|
|
|
|
|
LastExecuteTime = DateTime.Today.AddDays(-1),
|
|
|
|
|
|
NextExecuteTime = DateTime.Today
|
2026-01-28 17:00:58 +08:00
|
|
|
|
},
|
|
|
|
|
|
new TransactionPeriodic
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = 2,
|
|
|
|
|
|
PeriodicType = PeriodicType.Daily,
|
|
|
|
|
|
Amount = 200m,
|
|
|
|
|
|
Type = TransactionType.Expense,
|
|
|
|
|
|
Classify = "交通",
|
|
|
|
|
|
Reason = "每日交通",
|
|
|
|
|
|
IsEnabled = true,
|
2026-01-28 19:32:11 +08:00
|
|
|
|
LastExecuteTime = DateTime.Today.AddDays(-1),
|
|
|
|
|
|
NextExecuteTime = DateTime.Today
|
2026-01-28 17:00:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(periodicBills);
|
|
|
|
|
|
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
|
2026-01-28 19:32:11 +08:00
|
|
|
|
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.FromResult(true));
|
2026-01-28 17:00:58 +08:00
|
|
|
|
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
2026-01-28 19:32:11 +08:00
|
|
|
|
.Returns(Task.FromResult(true));
|
2026-01-28 17:00:58 +08:00
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
await _service.ExecutePeriodicBillsAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
// 只有启用的账单会被处理
|
|
|
|
|
|
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t => t.Reason == "每日交通"));
|
|
|
|
|
|
await _transactionRepository.Received(0).AddAsync(Arg.Is<TransactionRecord>(t => t.Reason == "每日餐费"));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|