feat: Implement Escrow module with new APIs and update documentation.
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
The Wallet Service provides comprehensive wallet and loyalty points management with:
|
||||
|
||||
- **Wallet Management** - Create, deposit, withdraw, transfer funds
|
||||
- **Escrow Module** - Hold, commit, and release funds (for Promotion Service)
|
||||
- **Point Account** - Earn, spend, and track loyalty points
|
||||
- **Transaction History** - Full audit trail of all transactions
|
||||
- **Multi-Currency Support** - Default VND with currency support
|
||||
@@ -72,6 +73,16 @@ dotnet run --project src/WalletService.API
|
||||
| `POST` | `/api/v1/wallets/{id}/unfreeze` | Unfreeze wallet |
|
||||
| `GET` | `/api/v1/wallets/transactions` | Get transaction history |
|
||||
|
||||
### Escrow APIs (New)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/api/v1/wallets/{walletId}/holds` | Create a hold |
|
||||
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/execute` | Execute/Commit hold (deduct funds) |
|
||||
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/release` | Release hold (return funds) |
|
||||
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/cancel` | Cancel hold |
|
||||
| `GET` | `/api/v1/wallets/{walletId}/holds/{holdId}` | Get hold details |
|
||||
|
||||
### Points APIs
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
@@ -140,6 +151,13 @@ wallet.Withdraw(new Money(500000m, "VND"), "Shopping", "REF002");
|
||||
// Freeze/Unfreeze
|
||||
wallet.Freeze();
|
||||
wallet.Unfreeze();
|
||||
|
||||
// Escrow
|
||||
var hold = wallet.Hold(100000m, CurrencyType.VND, "CAMPAIGN", campaignId, "Hold for campaign");
|
||||
wallet.ExecuteHold(hold.Id, 50000m, "ORDER123"); // Commit 50k
|
||||
wallet.ReleaseHold(hold.Id, 50000m); // Return 50k
|
||||
wallet.CancelHold(hold.Id); // Cancel remaining
|
||||
|
||||
```
|
||||
|
||||
### Point Account Aggregate
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
Wallet Service cung cấp quản lý ví và điểm thưởng toàn diện:
|
||||
|
||||
- **Quản Lý Ví** - Tạo, nạp tiền, rút tiền, chuyển khoản
|
||||
- **Escrow Module** - Ký quỹ, cam kết và giải phóng tiền (cho Promotion Service)
|
||||
- **Tài Khoản Điểm** - Tích, tiêu và theo dõi điểm thưởng
|
||||
- **Lịch Sử Giao Dịch** - Audit trail đầy đủ các giao dịch
|
||||
- **Hỗ Trợ Đa Tiền Tệ** - Mặc định VND với hỗ trợ đa tiền tệ
|
||||
@@ -72,6 +73,16 @@ dotnet run --project src/WalletService.API
|
||||
| `POST` | `/api/v1/wallets/{id}/unfreeze` | Mở đóng băng ví |
|
||||
| `GET` | `/api/v1/wallets/transactions` | Lấy lịch sử giao dịch |
|
||||
|
||||
### Escrow APIs (New)
|
||||
|
||||
| Method | Endpoint | Mô Tả |
|
||||
|--------|----------|-------|
|
||||
| `POST` | `/api/v1/wallets/{walletId}/holds` | Tạo lệnh giữ tiền (Hold) |
|
||||
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/execute` | Thực thi lệnh giữ tiền (trừ tiền thực) |
|
||||
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/release` | Giải phóng tiền giữ (trả lại ví) |
|
||||
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/cancel` | Hủy lệnh giữ tiền |
|
||||
| `GET` | `/api/v1/wallets/{walletId}/holds/{holdId}` | Lấy thông tin lệnh giữ tiền |
|
||||
|
||||
### Points APIs
|
||||
|
||||
| Method | Endpoint | Mô Tả |
|
||||
@@ -140,6 +151,13 @@ wallet.Withdraw(new Money(500000m, "VND"), "Mua sắm", "REF002");
|
||||
// Đóng băng/Mở đóng băng
|
||||
wallet.Freeze();
|
||||
wallet.Unfreeze();
|
||||
|
||||
// Escrow / Ký Quỹ
|
||||
var hold = wallet.Hold(100000m, CurrencyType.VND, "CAMPAIGN", campaignId, "Hold for campaign");
|
||||
wallet.ExecuteHold(hold.Id, 50000m, "ORDER123"); // Thực thi 50k
|
||||
wallet.ReleaseHold(hold.Id, 50000m); // Trả lại 50k
|
||||
wallet.CancelHold(hold.Id); // Hủy phần còn lại
|
||||
|
||||
```
|
||||
|
||||
### Point Account Aggregate
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
<!-- EN: FluentValidation for request validation / VI: FluentValidation cho validation request -->
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
|
||||
|
||||
<!-- EN: Swagger/OpenAPI / VI: Swagger/OpenAPI -->
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
namespace WalletService.UnitTests.Domain;
|
||||
|
||||
using FluentAssertions;
|
||||
using WalletService.Domain.AggregatesModel.WalletAggregate;
|
||||
using WalletService.Domain.Exceptions;
|
||||
using Xunit;
|
||||
|
||||
public class HoldItemTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_ValidArguments_ShouldCreateHoldItem()
|
||||
{
|
||||
// Arrange
|
||||
var walletId = Guid.NewGuid();
|
||||
var amount = 100m;
|
||||
var currency = CurrencyType.VND;
|
||||
var refType = "CAMPAIGN";
|
||||
var refId = Guid.NewGuid();
|
||||
var desc = "Test Hold";
|
||||
|
||||
// Act
|
||||
var hold = new HoldItem(walletId, amount, currency, refType, refId, desc);
|
||||
|
||||
// Assert
|
||||
hold.WalletId.Should().Be(walletId);
|
||||
hold.OriginalAmount.Should().Be(amount);
|
||||
hold.RemainingAmount.Should().Be(amount);
|
||||
hold.ExecutedAmount.Should().Be(0);
|
||||
hold.ReleasedAmount.Should().Be(0);
|
||||
hold.Status.Should().Be(HoldStatus.Active);
|
||||
hold.ReferenceType.Should().Be(refType);
|
||||
hold.ReferenceId.Should().Be(refId);
|
||||
hold.Description.Should().Be(desc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_ValidAmount_ShouldReduceRemainingAndIncreaseExecuted()
|
||||
{
|
||||
// Arrange
|
||||
var hold = CreateHold(100m);
|
||||
var execAmount = 40m;
|
||||
|
||||
// Act
|
||||
hold.Execute(execAmount, "EXEC01");
|
||||
|
||||
// Assert
|
||||
hold.RemainingAmount.Should().Be(60m);
|
||||
hold.ExecutedAmount.Should().Be(40m);
|
||||
hold.Status.Should().Be(HoldStatus.Active); // Still active as remaining > 0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_FullAmount_ShouldMarkAsExecuted()
|
||||
{
|
||||
// Arrange
|
||||
var hold = CreateHold(100m);
|
||||
|
||||
// Act
|
||||
hold.Execute(100m, "EXEC_FULL");
|
||||
|
||||
// Assert
|
||||
hold.RemainingAmount.Should().Be(0);
|
||||
hold.ExecutedAmount.Should().Be(100m);
|
||||
hold.Status.Should().Be(HoldStatus.Executed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_ExcessAmount_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var hold = CreateHold(100m);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => hold.Execute(101m);
|
||||
act.Should().Throw<WalletDomainException>()
|
||||
.WithMessage("*Cannot execute*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Release_ValidAmount_ShouldReduceRemainingAndIncreaseReleased()
|
||||
{
|
||||
// Arrange
|
||||
var hold = CreateHold(100m);
|
||||
var releaseAmount = 30m;
|
||||
|
||||
// Act
|
||||
hold.Release(releaseAmount);
|
||||
|
||||
// Assert
|
||||
hold.RemainingAmount.Should().Be(70m);
|
||||
hold.ReleasedAmount.Should().Be(30m);
|
||||
hold.Status.Should().Be(HoldStatus.PartiallyReleased);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Release_RestAmount_ShouldMarkAsReleased()
|
||||
{
|
||||
// Arrange
|
||||
var hold = CreateHold(100m);
|
||||
|
||||
// Act
|
||||
hold.Release(100m);
|
||||
|
||||
// Assert
|
||||
hold.RemainingAmount.Should().Be(0);
|
||||
hold.ReleasedAmount.Should().Be(100m);
|
||||
hold.Status.Should().Be(HoldStatus.Released);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cancel_ShouldReleaseAllRemainingAndMarkCancelled()
|
||||
{
|
||||
// Arrange
|
||||
var hold = CreateHold(100m);
|
||||
hold.Execute(20m); // Remaining 80
|
||||
|
||||
// Act
|
||||
hold.Cancel();
|
||||
|
||||
// Assert
|
||||
hold.RemainingAmount.Should().Be(0);
|
||||
hold.ExecutedAmount.Should().Be(20m);
|
||||
hold.ReleasedAmount.Should().Be(80m); // The rest is released
|
||||
hold.Status.Should().Be(HoldStatus.Cancelled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_OnCancelledHold_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var hold = CreateHold(100m);
|
||||
hold.Cancel();
|
||||
|
||||
// Act & Assert
|
||||
var act = () => hold.Execute(10m);
|
||||
act.Should().Throw<WalletDomainException>()
|
||||
.WithMessage("*not active*");
|
||||
}
|
||||
|
||||
private static HoldItem CreateHold(decimal amount)
|
||||
{
|
||||
return new HoldItem(
|
||||
Guid.NewGuid(),
|
||||
amount,
|
||||
CurrencyType.VND,
|
||||
"TEST",
|
||||
Guid.NewGuid(),
|
||||
"Description");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user