From 055c6c40752f5c9de0a86dfd7e5e0921cc864afc Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Thu, 15 Jan 2026 19:27:35 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20th=C3=AAm=20c=C3=A1c=20API,=20command?= =?UTF-8?q?=20v=C3=A0=20query=20qu=E1=BA=A3n=20tr=E1=BB=8B=20cho=20vi?= =?UTF-8?q?=E1=BB=87c=20qu=E1=BA=A3n=20l=C3=BD=20v=C3=AD=20v=C3=A0=20t?= =?UTF-8?q?=C3=A0i=20kho=E1=BA=A3n=20=C4=91i=E1=BB=83m.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Application/Commands/AdminCommands.cs | 99 +++++++++ .../Application/Queries/AdminQueries.cs | 158 +++++++++++++++ .../Admin/AdminPointsController.cs | 168 ++++++++++++++++ .../Admin/AdminWalletsController.cs | 190 ++++++++++++++++++ 4 files changed, 615 insertions(+) create mode 100644 services/wallet-service-net/src/WalletService.API/Application/Commands/AdminCommands.cs create mode 100644 services/wallet-service-net/src/WalletService.API/Application/Queries/AdminQueries.cs create mode 100644 services/wallet-service-net/src/WalletService.API/Controllers/Admin/AdminPointsController.cs create mode 100644 services/wallet-service-net/src/WalletService.API/Controllers/Admin/AdminWalletsController.cs diff --git a/services/wallet-service-net/src/WalletService.API/Application/Commands/AdminCommands.cs b/services/wallet-service-net/src/WalletService.API/Application/Commands/AdminCommands.cs new file mode 100644 index 00000000..f1c84673 --- /dev/null +++ b/services/wallet-service-net/src/WalletService.API/Application/Commands/AdminCommands.cs @@ -0,0 +1,99 @@ +namespace WalletService.API.Application.Commands; + +using MediatR; + +#region Admin Wallet Commands + +/// +/// EN: Command to freeze a wallet (Admin only). +/// VI: Command để đóng băng ví (Chỉ Admin). +/// +public record AdminFreezeWalletCommand( + Guid WalletId, + string Reason, + Guid AdminId) : IRequest; + +/// +/// EN: Command to unfreeze a wallet (Admin only). +/// VI: Command để mở băng ví (Chỉ Admin). +/// +public record AdminUnfreezeWalletCommand( + Guid WalletId, + string Reason, + Guid AdminId) : IRequest; + +/// +/// EN: Command to adjust wallet balance (Admin only). +/// VI: Command để điều chỉnh số dư ví (Chỉ Admin). +/// +public record AdminAdjustBalanceCommand( + Guid WalletId, + decimal Amount, + int CurrencyTypeId, + string Reason, + Guid AdminId) : IRequest; + +#endregion + +#region Admin Wallet Command Results + +public record AdminWalletActionResult( + Guid WalletId, + string Status, + string ActionBy, + DateTime ActionAt); + +public record AdminAdjustBalanceResult( + Guid WalletId, + decimal PreviousBalance, + decimal AdjustmentAmount, + decimal NewBalance, + string Currency, + string Reason, + DateTime AdjustedAt); + +#endregion + +#region Admin Points Commands + +/// +/// EN: Command to adjust points (Admin only). +/// VI: Command để điều chỉnh điểm (Chỉ Admin). +/// +public record AdminAdjustPointsCommand( + Guid AccountId, + long Points, + string Reason, + Guid AdminId) : IRequest; + +/// +/// EN: Command to grant bonus points (Admin only). +/// VI: Command để tặng điểm thưởng (Chỉ Admin). +/// +public record AdminGrantBonusCommand( + Guid AccountId, + long Points, + string Reason, + int? ExpiryMonths, + Guid AdminId) : IRequest; + +#endregion + +#region Admin Points Command Results + +public record AdminAdjustPointsResult( + Guid AccountId, + long PreviousPoints, + long AdjustmentPoints, + long NewPoints, + string Reason, + DateTime AdjustedAt); + +public record AdminGrantBonusResult( + Guid AccountId, + long BonusPoints, + long NewTotalPoints, + DateTime? ExpiresAt, + DateTime GrantedAt); + +#endregion diff --git a/services/wallet-service-net/src/WalletService.API/Application/Queries/AdminQueries.cs b/services/wallet-service-net/src/WalletService.API/Application/Queries/AdminQueries.cs new file mode 100644 index 00000000..0e18586e --- /dev/null +++ b/services/wallet-service-net/src/WalletService.API/Application/Queries/AdminQueries.cs @@ -0,0 +1,158 @@ +namespace WalletService.API.Application.Queries; + +using MediatR; + +#region Admin Wallet Queries + +/// +/// EN: Query to get all wallets with pagination (Admin only). +/// VI: Query lấy tất cả ví với phân trang (Chỉ Admin). +/// +public record GetAllWalletsQuery( + int Page, + int PageSize, + string? Status = null, + string? Currency = null) : IRequest; + +/// +/// EN: Query to get wallet by ID (Admin only). +/// VI: Query lấy ví theo ID (Chỉ Admin). +/// +public record GetWalletByIdQuery(Guid WalletId) : IRequest; + +/// +/// EN: Query to search wallets (Admin only). +/// VI: Query tìm kiếm ví (Chỉ Admin). +/// +public record SearchWalletsQuery( + Guid? UserId = null, + Guid? WalletId = null, + string? Status = null) : IRequest>; + +/// +/// EN: Query to get wallet statistics (Admin only). +/// VI: Query lấy thống kê ví (Chỉ Admin). +/// +public record GetWalletStatisticsQuery() : IRequest; + +#endregion + +#region Admin Points Queries + +/// +/// EN: Query to get all point accounts with pagination (Admin only). +/// VI: Query lấy tất cả tài khoản điểm với phân trang (Chỉ Admin). +/// +public record GetAllPointAccountsQuery( + int Page, + int PageSize, + long? MinPoints = null, + long? MaxPoints = null) : IRequest; + +/// +/// EN: Query to get point account by ID (Admin only). +/// VI: Query lấy tài khoản điểm theo ID (Chỉ Admin). +/// +public record GetPointAccountByIdQuery(Guid AccountId) : IRequest; + +/// +/// EN: Query to search point accounts (Admin only). +/// VI: Query tìm kiếm tài khoản điểm (Chỉ Admin). +/// +public record SearchPointAccountsQuery(Guid? UserId = null) : IRequest>; + +/// +/// EN: Query to get points statistics (Admin only). +/// VI: Query lấy thống kê điểm (Chỉ Admin). +/// +public record GetPointsStatisticsQuery() : IRequest; + +#endregion + +#region Admin DTOs + +/// +/// EN: DTO for admin wallets list with pagination. +/// VI: DTO cho danh sách ví Admin với phân trang. +/// +public record AdminWalletsListDto( + List Wallets, + int TotalCount, + int Page, + int PageSize); + +public record AdminWalletSummaryDto( + Guid Id, + Guid UserId, + string Status, + List Balances, + DateTime CreatedAt, + DateTime UpdatedAt); + +public record BalanceItemDto( + string Currency, + decimal Balance); + +public record AdminWalletDetailDto( + Guid Id, + Guid UserId, + string Status, + List Balances, + int TransactionCount, + DateTime CreatedAt, + DateTime UpdatedAt); + +/// +/// EN: DTO for wallet statistics. +/// VI: DTO cho thống kê ví. +/// +public record WalletStatisticsDto( + int TotalWallets, + int ActiveWallets, + int FrozenWallets, + int ClosedWallets, + Dictionary TotalBalanceByCurrency, + decimal TotalTransactionsToday, + decimal TotalDepositsToday, + decimal TotalWithdrawalsToday); + +/// +/// EN: DTO for admin point accounts list with pagination. +/// VI: DTO cho danh sách tài khoản điểm Admin với phân trang. +/// +public record AdminPointAccountsListDto( + List Accounts, + int TotalCount, + int Page, + int PageSize); + +public record AdminPointAccountSummaryDto( + Guid Id, + Guid UserId, + long TotalPoints, + long AvailablePoints, + DateTime CreatedAt); + +public record AdminPointAccountDetailDto( + Guid Id, + Guid UserId, + long TotalPoints, + long AvailablePoints, + int TransactionCount, + DateTime CreatedAt, + DateTime UpdatedAt); + +/// +/// EN: DTO for points statistics. +/// VI: DTO cho thống kê điểm. +/// +public record PointsStatisticsDto( + int TotalAccounts, + long TotalPointsIssued, + long TotalPointsAvailable, + long TotalPointsSpent, + long TotalPointsExpired, + long PointsEarnedToday, + long PointsSpentToday); + +#endregion diff --git a/services/wallet-service-net/src/WalletService.API/Controllers/Admin/AdminPointsController.cs b/services/wallet-service-net/src/WalletService.API/Controllers/Admin/AdminPointsController.cs new file mode 100644 index 00000000..e3ef193a --- /dev/null +++ b/services/wallet-service-net/src/WalletService.API/Controllers/Admin/AdminPointsController.cs @@ -0,0 +1,168 @@ +namespace WalletService.API.Controllers.Admin; + +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using WalletService.API.Application.Commands; +using WalletService.API.Application.Queries; +using WalletService.API.Controllers; +using WalletService.Domain.AggregatesModel.PointAccountAggregate; + +/// +/// EN: Admin controller for points management operations (Backoffice). +/// VI: Controller Admin cho các thao tác quản lý điểm (Backoffice). +/// +[ApiController] +[Route("api/v1/admin/points")] +[Authorize(Roles = "Admin,SuperAdmin")] +[SwaggerTag("Admin Points Management / Quản lý điểm Admin")] +public class AdminPointsController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly IPointAccountRepository _pointAccountRepository; + private readonly ILogger _logger; + + public AdminPointsController( + IMediator mediator, + IPointAccountRepository pointAccountRepository, + ILogger logger) + { + _mediator = mediator; + _pointAccountRepository = pointAccountRepository; + _logger = logger; + } + + /// + /// EN: Get all point accounts with pagination. + /// VI: Lấy tất cả tài khoản điểm với phân trang. + /// + [HttpGet] + [SwaggerOperation(Summary = "Get all point accounts", Description = "Get all point accounts with pagination (Admin only)")] + [SwaggerResponse(200, "Success", typeof(ApiResponse))] + public async Task GetAllPointAccounts( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] long? minPoints = null, + [FromQuery] long? maxPoints = null) + { + var query = new GetAllPointAccountsQuery(page, pageSize, minPoints, maxPoints); + var result = await _mediator.Send(query); + + return Ok(new ApiResponse(true, "Success", result)); + } + + /// + /// EN: Get point account details by ID. + /// VI: Lấy chi tiết tài khoản điểm theo ID. + /// + [HttpGet("{accountId:guid}")] + [SwaggerOperation(Summary = "Get point account by ID", Description = "Get point account details by ID (Admin only)")] + [SwaggerResponse(200, "Success", typeof(ApiResponse))] + [SwaggerResponse(404, "Point account not found")] + public async Task GetPointAccountById(Guid accountId) + { + var query = new GetPointAccountByIdQuery(accountId); + var result = await _mediator.Send(query); + + if (result == null) + return NotFound(new ApiResponse(false, "Point account not found")); + + return Ok(new ApiResponse(true, "Success", result)); + } + + /// + /// EN: Adjust points (add or subtract). + /// VI: Điều chỉnh điểm (cộng hoặc trừ). + /// + [HttpPost("{accountId:guid}/adjust")] + [SwaggerOperation(Summary = "Adjust points", Description = "Adjust points for a user account (Admin only)")] + [SwaggerResponse(200, "Points adjusted")] + [SwaggerResponse(400, "Invalid adjustment")] + [SwaggerResponse(404, "Point account not found")] + public async Task AdjustPoints(Guid accountId, [FromBody] AdminAdjustPointsRequest request) + { + var command = new AdminAdjustPointsCommand( + accountId, + request.Points, + request.Reason, + GetAdminId()); + + var result = await _mediator.Send(command); + + _logger.LogInformation( + "Admin {AdminId} adjusted points for account {AccountId} by {Points}. Reason: {Reason}", + GetAdminId(), accountId, request.Points, request.Reason); + + return Ok(new ApiResponse(true, "Points adjusted successfully", result)); + } + + /// + /// EN: Grant bonus points to user. + /// VI: Tặng điểm thưởng cho người dùng. + /// + [HttpPost("{accountId:guid}/bonus")] + [SwaggerOperation(Summary = "Grant bonus points", Description = "Grant bonus points to user (Admin only)")] + [SwaggerResponse(200, "Bonus points granted")] + [SwaggerResponse(404, "Point account not found")] + public async Task GrantBonusPoints(Guid accountId, [FromBody] AdminGrantBonusRequest request) + { + var command = new AdminGrantBonusCommand( + accountId, + request.Points, + request.Reason, + request.ExpiryMonths, + GetAdminId()); + + var result = await _mediator.Send(command); + + _logger.LogInformation( + "Admin {AdminId} granted {Points} bonus points to account {AccountId}. Reason: {Reason}", + GetAdminId(), request.Points, accountId, request.Reason); + + return Ok(new ApiResponse(true, "Bonus points granted successfully", result)); + } + + /// + /// EN: Get points statistics. + /// VI: Lấy thống kê điểm. + /// + [HttpGet("statistics")] + [SwaggerOperation(Summary = "Get statistics", Description = "Get points statistics (Admin only)")] + [SwaggerResponse(200, "Success", typeof(ApiResponse))] + public async Task GetStatistics() + { + var query = new GetPointsStatisticsQuery(); + var result = await _mediator.Send(query); + + return Ok(new ApiResponse(true, "Success", result)); + } + + /// + /// EN: Search point accounts by user ID. + /// VI: Tìm kiếm tài khoản điểm theo user ID. + /// + [HttpGet("search")] + [SwaggerOperation(Summary = "Search point accounts", Description = "Search point accounts by user ID (Admin only)")] + [SwaggerResponse(200, "Success")] + public async Task SearchPointAccounts([FromQuery] Guid? userId = null) + { + var query = new SearchPointAccountsQuery(userId); + var result = await _mediator.Send(query); + + return Ok(new ApiResponse>(true, "Success", result)); + } + + private Guid GetAdminId() + { + var userIdClaim = User.FindFirst("sub")?.Value ?? User.FindFirst("id")?.Value; + return Guid.TryParse(userIdClaim, out var userId) ? userId : Guid.Empty; + } +} + +#region Admin Request/Response DTOs + +public record AdminAdjustPointsRequest(long Points, string Reason); +public record AdminGrantBonusRequest(long Points, string Reason, int? ExpiryMonths = 12); + +#endregion diff --git a/services/wallet-service-net/src/WalletService.API/Controllers/Admin/AdminWalletsController.cs b/services/wallet-service-net/src/WalletService.API/Controllers/Admin/AdminWalletsController.cs new file mode 100644 index 00000000..10c548d2 --- /dev/null +++ b/services/wallet-service-net/src/WalletService.API/Controllers/Admin/AdminWalletsController.cs @@ -0,0 +1,190 @@ +namespace WalletService.API.Controllers.Admin; + +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using WalletService.API.Application.Commands; +using WalletService.API.Application.Queries; +using WalletService.API.Controllers; +using WalletService.Domain.AggregatesModel.WalletAggregate; + +/// +/// EN: Admin controller for wallet management operations (Backoffice). +/// VI: Controller Admin cho các thao tác quản lý ví (Backoffice). +/// +[ApiController] +[Route("api/v1/admin/wallets")] +[Authorize(Roles = "Admin,SuperAdmin")] +[SwaggerTag("Admin Wallet Management / Quản lý ví Admin")] +public class AdminWalletsController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly IWalletRepository _walletRepository; + private readonly ILogger _logger; + + public AdminWalletsController( + IMediator mediator, + IWalletRepository walletRepository, + ILogger logger) + { + _mediator = mediator; + _walletRepository = walletRepository; + _logger = logger; + } + + /// + /// EN: Get all wallets with pagination. + /// VI: Lấy tất cả ví với phân trang. + /// + [HttpGet] + [SwaggerOperation(Summary = "Get all wallets", Description = "Get all wallets with pagination (Admin only)")] + [SwaggerResponse(200, "Success", typeof(ApiResponse))] + public async Task GetAllWallets( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? status = null, + [FromQuery] string? currency = null) + { + var query = new GetAllWalletsQuery(page, pageSize, status, currency); + var result = await _mediator.Send(query); + + return Ok(new ApiResponse(true, "Success", result)); + } + + /// + /// EN: Get wallet details by ID. + /// VI: Lấy chi tiết ví theo ID. + /// + [HttpGet("{walletId:guid}")] + [SwaggerOperation(Summary = "Get wallet by ID", Description = "Get wallet details by wallet ID (Admin only)")] + [SwaggerResponse(200, "Success", typeof(ApiResponse))] + [SwaggerResponse(404, "Wallet not found")] + public async Task GetWalletById(Guid walletId) + { + var query = new GetWalletByIdQuery(walletId); + var result = await _mediator.Send(query); + + if (result == null) + return NotFound(new ApiResponse(false, "Wallet not found")); + + return Ok(new ApiResponse(true, "Success", result)); + } + + /// + /// EN: Freeze wallet. + /// VI: Đóng băng ví. + /// + [HttpPost("{walletId:guid}/freeze")] + [SwaggerOperation(Summary = "Freeze wallet", Description = "Freeze wallet to prevent transactions (Admin only)")] + [SwaggerResponse(200, "Wallet frozen")] + [SwaggerResponse(404, "Wallet not found")] + public async Task FreezeWallet(Guid walletId, [FromBody] AdminActionRequest request) + { + var command = new AdminFreezeWalletCommand(walletId, request.Reason, GetAdminId()); + var result = await _mediator.Send(command); + + _logger.LogInformation( + "Admin {AdminId} froze wallet {WalletId}. Reason: {Reason}", + GetAdminId(), walletId, request.Reason); + + return Ok(new ApiResponse(true, "Wallet frozen successfully", result)); + } + + /// + /// EN: Unfreeze wallet. + /// VI: Mở băng ví. + /// + [HttpPost("{walletId:guid}/unfreeze")] + [SwaggerOperation(Summary = "Unfreeze wallet", Description = "Unfreeze wallet to allow transactions (Admin only)")] + [SwaggerResponse(200, "Wallet unfrozen")] + [SwaggerResponse(404, "Wallet not found")] + public async Task UnfreezeWallet(Guid walletId, [FromBody] AdminActionRequest request) + { + var command = new AdminUnfreezeWalletCommand(walletId, request.Reason, GetAdminId()); + var result = await _mediator.Send(command); + + _logger.LogInformation( + "Admin {AdminId} unfroze wallet {WalletId}. Reason: {Reason}", + GetAdminId(), walletId, request.Reason); + + return Ok(new ApiResponse(true, "Wallet unfrozen successfully", result)); + } + + /// + /// EN: Adjust wallet balance (credit/debit). + /// VI: Điều chỉnh số dư ví (cộng/trừ). + /// + [HttpPost("{walletId:guid}/adjust")] + [SwaggerOperation(Summary = "Adjust balance", Description = "Adjust wallet balance (Admin only)")] + [SwaggerResponse(200, "Balance adjusted")] + [SwaggerResponse(400, "Invalid adjustment")] + [SwaggerResponse(404, "Wallet not found")] + public async Task AdjustBalance(Guid walletId, [FromBody] AdminAdjustBalanceRequest request) + { + var command = new AdminAdjustBalanceCommand( + walletId, + request.Amount, + request.CurrencyTypeId, + request.Reason, + GetAdminId()); + + var result = await _mediator.Send(command); + + _logger.LogInformation( + "Admin {AdminId} adjusted wallet {WalletId} by {Amount}. Reason: {Reason}", + GetAdminId(), walletId, request.Amount, request.Reason); + + return Ok(new ApiResponse(true, "Balance adjusted successfully", result)); + } + + /// + /// EN: Get wallet statistics. + /// VI: Lấy thống kê ví. + /// + [HttpGet("statistics")] + [SwaggerOperation(Summary = "Get statistics", Description = "Get wallet statistics (Admin only)")] + [SwaggerResponse(200, "Success", typeof(ApiResponse))] + public async Task GetStatistics() + { + var query = new GetWalletStatisticsQuery(); + var result = await _mediator.Send(query); + + return Ok(new ApiResponse(true, "Success", result)); + } + + /// + /// EN: Search wallets by user ID or wallet ID. + /// VI: Tìm kiếm ví theo user ID hoặc wallet ID. + /// + [HttpGet("search")] + [SwaggerOperation(Summary = "Search wallets", Description = "Search wallets by user ID or wallet ID (Admin only)")] + [SwaggerResponse(200, "Success")] + public async Task SearchWallets( + [FromQuery] Guid? userId = null, + [FromQuery] Guid? walletId = null, + [FromQuery] string? status = null) + { + var query = new SearchWalletsQuery(userId, walletId, status); + var result = await _mediator.Send(query); + + return Ok(new ApiResponse>(true, "Success", result)); + } + + private Guid GetAdminId() + { + var userIdClaim = User.FindFirst("sub")?.Value ?? User.FindFirst("id")?.Value; + return Guid.TryParse(userIdClaim, out var userId) ? userId : Guid.Empty; + } +} + +#region Admin Request/Response DTOs + +public record AdminActionRequest(string Reason); + +public record AdminAdjustBalanceRequest( + decimal Amount, + int CurrencyTypeId, + string Reason); + +#endregion