feat: Add escrow command handlers and unit tests for wallet hold functionality, including updates to hold item status logic.

This commit is contained in:
Ho Ngoc Hai
2026-01-17 21:20:58 +07:00
parent ce15956aba
commit 2fa92bb52c
5 changed files with 151 additions and 3 deletions

View File

@@ -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

View File

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

View File

@@ -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)

View File

@@ -197,4 +197,37 @@ public class WalletTests
}
#endregion
#region Escrow Tests / Tests 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
}

View File

@@ -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>