feat: Add escrow command handlers and unit tests for wallet hold functionality, including updates to hold item status logic.
This commit is contained in:
@@ -237,9 +237,9 @@ public class HoldItem : Entity
|
||||
// All amount has been used (executed or released)
|
||||
Status = ExecutedAmount > 0 ? HoldStatus.Executed : HoldStatus.Released;
|
||||
}
|
||||
else if (ReleasedAmount > 0 || ExecutedAmount > 0)
|
||||
else if (ReleasedAmount > 0)
|
||||
{
|
||||
// Some amount was released or executed but not all
|
||||
// Some amount was released but not all
|
||||
Status = HoldStatus.PartiallyReleased;
|
||||
}
|
||||
// else status remains Active
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
namespace WalletService.UnitTests.Application;
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using WalletService.API.Application.Commands;
|
||||
using WalletService.Domain.AggregatesModel.WalletAggregate;
|
||||
using WalletService.Domain.Exceptions;
|
||||
using WalletService.Domain.SeedWork;
|
||||
using Xunit;
|
||||
|
||||
public class EscrowCommandHandlersTests
|
||||
{
|
||||
private readonly IWalletRepository _walletRepository;
|
||||
private readonly ILogger<CreateHoldCommandHandler> _createLogger;
|
||||
private readonly ILogger<ExecuteHoldCommandHandler> _executeLogger;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public EscrowCommandHandlersTests()
|
||||
{
|
||||
_walletRepository = Substitute.For<IWalletRepository>();
|
||||
_unitOfWork = Substitute.For<IUnitOfWork>();
|
||||
_walletRepository.UnitOfWork.Returns(_unitOfWork);
|
||||
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);
|
||||
|
||||
_createLogger = Substitute.For<ILogger<CreateHoldCommandHandler>>();
|
||||
_executeLogger = Substitute.For<ILogger<ExecuteHoldCommandHandler>>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateHold_ValidCommand_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var walletId = Guid.NewGuid();
|
||||
var userId = Guid.NewGuid();
|
||||
var wallet = new Wallet(userId, CurrencyType.VND);
|
||||
wallet.Deposit(1000m, CurrencyType.VND, "Init");
|
||||
|
||||
// Mock repository to return our wallet when searching by UserId (CreateHoldCommand uses UserId)
|
||||
_walletRepository.GetByUserIdAsync(userId).Returns(wallet);
|
||||
|
||||
var handler = new CreateHoldCommandHandler(_walletRepository, _createLogger);
|
||||
var command = new CreateHoldCommand(
|
||||
userId,
|
||||
100m,
|
||||
"VND",
|
||||
"CAMPAIGN",
|
||||
Guid.NewGuid(),
|
||||
"Test Hold");
|
||||
|
||||
// Act
|
||||
var result = await handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.RemainingAmount.Should().Be(100m);
|
||||
wallet.GetBalance(CurrencyType.VND).Should().Be(900m);
|
||||
|
||||
_walletRepository.Received(1).Update(wallet);
|
||||
await _unitOfWork.Received(1).SaveEntitiesAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteHold_ValidCommand_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var wallet = new Wallet(userId, CurrencyType.VND);
|
||||
wallet.Deposit(1000m, CurrencyType.VND, "Init");
|
||||
var hold = wallet.Hold(100m, CurrencyType.VND, "REF", Guid.NewGuid(), "Desc");
|
||||
|
||||
// Mock repository to return wallet by ID (ExecuteHoldCommand uses WalletId)
|
||||
_walletRepository.GetByIdAsync(wallet.Id).Returns(wallet);
|
||||
|
||||
var handler = new ExecuteHoldCommandHandler(_walletRepository, _executeLogger);
|
||||
var command = new ExecuteHoldCommand(
|
||||
wallet.Id,
|
||||
hold.Id,
|
||||
40m,
|
||||
"EXEC_REF");
|
||||
|
||||
// Act
|
||||
var result = await handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.ExecutedAmount.Should().Be(40m);
|
||||
result.RemainingAmount.Should().Be(60m);
|
||||
|
||||
await _unitOfWork.Received(1).SaveEntitiesAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteHold_InsufficientHoldAmount_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var wallet = new Wallet(userId, CurrencyType.VND);
|
||||
wallet.Deposit(1000m, CurrencyType.VND, "Init");
|
||||
var hold = wallet.Hold(100m, CurrencyType.VND, "REF", Guid.NewGuid(), "Desc");
|
||||
|
||||
_walletRepository.GetByIdAsync(wallet.Id).Returns(wallet);
|
||||
|
||||
var handler = new ExecuteHoldCommandHandler(_walletRepository, _executeLogger);
|
||||
var command = new ExecuteHoldCommand(
|
||||
wallet.Id,
|
||||
hold.Id,
|
||||
150m, // Try to execute more than held
|
||||
"EXEC_REF");
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<WalletDomainException>(() =>
|
||||
handler.Handle(command, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,7 @@ public class HoldItemTests
|
||||
// Act & Assert
|
||||
var act = () => hold.Execute(10m);
|
||||
act.Should().Throw<WalletDomainException>()
|
||||
.WithMessage("*not active*");
|
||||
.WithMessage("*Cannot modify hold in Cancelled status*");
|
||||
}
|
||||
|
||||
private static HoldItem CreateHold(decimal amount)
|
||||
|
||||
@@ -197,4 +197,37 @@ public class WalletTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Escrow Tests / Tests Ký Quỹ
|
||||
|
||||
[Fact]
|
||||
public void Hold_ShouldReduceAvailableBalanceAndCreateHoldItem()
|
||||
{
|
||||
// Arrange
|
||||
var wallet = new Wallet(TestUserId, CurrencyType.VND);
|
||||
wallet.Deposit(1000m, CurrencyType.VND, "Init", "REF001");
|
||||
|
||||
// Act
|
||||
var hold = wallet.Hold(100m, CurrencyType.VND, "REF", Guid.NewGuid(), "Desc");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(900m, wallet.GetBalance(CurrencyType.VND));
|
||||
Assert.Single(wallet.Holds);
|
||||
Assert.Equal(100m, wallet.GetTotalHeldAmount(CurrencyType.VND));
|
||||
Assert.Equal(HoldStatus.Active, hold.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hold_ShouldThrow_WhenInsufficientBalance()
|
||||
{
|
||||
// Arrange
|
||||
var wallet = new Wallet(TestUserId, CurrencyType.VND);
|
||||
wallet.Deposit(50m, CurrencyType.VND, "Init", "REF001");
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InsufficientBalanceException>(() =>
|
||||
wallet.Hold(100m, CurrencyType.VND, "REF", Guid.NewGuid(), "Desc"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<ItemGroup>
|
||||
<!-- EN: Test framework / VI: Test framework -->
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
Reference in New Issue
Block a user