feat: thêm các API, command và query quản trị cho việc quản lý ví và tài khoản điểm.

This commit is contained in:
Ho Ngoc Hai
2026-01-15 19:27:35 +07:00
parent 85bd4d6f58
commit 055c6c4075
4 changed files with 615 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
namespace WalletService.API.Application.Commands;
using MediatR;
#region Admin Wallet Commands
/// <summary>
/// EN: Command to freeze a wallet (Admin only).
/// VI: Command để đóng băng ví (Chỉ Admin).
/// </summary>
public record AdminFreezeWalletCommand(
Guid WalletId,
string Reason,
Guid AdminId) : IRequest<AdminWalletActionResult>;
/// <summary>
/// EN: Command to unfreeze a wallet (Admin only).
/// VI: Command để mở băng ví (Chỉ Admin).
/// </summary>
public record AdminUnfreezeWalletCommand(
Guid WalletId,
string Reason,
Guid AdminId) : IRequest<AdminWalletActionResult>;
/// <summary>
/// EN: Command to adjust wallet balance (Admin only).
/// VI: Command để điều chỉnh số dư ví (Chỉ Admin).
/// </summary>
public record AdminAdjustBalanceCommand(
Guid WalletId,
decimal Amount,
int CurrencyTypeId,
string Reason,
Guid AdminId) : IRequest<AdminAdjustBalanceResult>;
#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
/// <summary>
/// EN: Command to adjust points (Admin only).
/// VI: Command để điều chỉnh điểm (Chỉ Admin).
/// </summary>
public record AdminAdjustPointsCommand(
Guid AccountId,
long Points,
string Reason,
Guid AdminId) : IRequest<AdminAdjustPointsResult>;
/// <summary>
/// EN: Command to grant bonus points (Admin only).
/// VI: Command để tặng điểm thưởng (Chỉ Admin).
/// </summary>
public record AdminGrantBonusCommand(
Guid AccountId,
long Points,
string Reason,
int? ExpiryMonths,
Guid AdminId) : IRequest<AdminGrantBonusResult>;
#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

View File

@@ -0,0 +1,158 @@
namespace WalletService.API.Application.Queries;
using MediatR;
#region Admin Wallet Queries
/// <summary>
/// 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).
/// </summary>
public record GetAllWalletsQuery(
int Page,
int PageSize,
string? Status = null,
string? Currency = null) : IRequest<AdminWalletsListDto>;
/// <summary>
/// EN: Query to get wallet by ID (Admin only).
/// VI: Query lấy ví theo ID (Chỉ Admin).
/// </summary>
public record GetWalletByIdQuery(Guid WalletId) : IRequest<AdminWalletDetailDto?>;
/// <summary>
/// EN: Query to search wallets (Admin only).
/// VI: Query tìm kiếm ví (Chỉ Admin).
/// </summary>
public record SearchWalletsQuery(
Guid? UserId = null,
Guid? WalletId = null,
string? Status = null) : IRequest<List<AdminWalletDetailDto>>;
/// <summary>
/// EN: Query to get wallet statistics (Admin only).
/// VI: Query lấy thống kê ví (Chỉ Admin).
/// </summary>
public record GetWalletStatisticsQuery() : IRequest<WalletStatisticsDto>;
#endregion
#region Admin Points Queries
/// <summary>
/// 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).
/// </summary>
public record GetAllPointAccountsQuery(
int Page,
int PageSize,
long? MinPoints = null,
long? MaxPoints = null) : IRequest<AdminPointAccountsListDto>;
/// <summary>
/// EN: Query to get point account by ID (Admin only).
/// VI: Query lấy tài khoản điểm theo ID (Chỉ Admin).
/// </summary>
public record GetPointAccountByIdQuery(Guid AccountId) : IRequest<AdminPointAccountDetailDto?>;
/// <summary>
/// EN: Query to search point accounts (Admin only).
/// VI: Query tìm kiếm tài khoản điểm (Chỉ Admin).
/// </summary>
public record SearchPointAccountsQuery(Guid? UserId = null) : IRequest<List<AdminPointAccountDetailDto>>;
/// <summary>
/// EN: Query to get points statistics (Admin only).
/// VI: Query lấy thống kê điểm (Chỉ Admin).
/// </summary>
public record GetPointsStatisticsQuery() : IRequest<PointsStatisticsDto>;
#endregion
#region Admin DTOs
/// <summary>
/// EN: DTO for admin wallets list with pagination.
/// VI: DTO cho danh sách ví Admin với phân trang.
/// </summary>
public record AdminWalletsListDto(
List<AdminWalletSummaryDto> Wallets,
int TotalCount,
int Page,
int PageSize);
public record AdminWalletSummaryDto(
Guid Id,
Guid UserId,
string Status,
List<BalanceItemDto> Balances,
DateTime CreatedAt,
DateTime UpdatedAt);
public record BalanceItemDto(
string Currency,
decimal Balance);
public record AdminWalletDetailDto(
Guid Id,
Guid UserId,
string Status,
List<BalanceItemDto> Balances,
int TransactionCount,
DateTime CreatedAt,
DateTime UpdatedAt);
/// <summary>
/// EN: DTO for wallet statistics.
/// VI: DTO cho thống kê ví.
/// </summary>
public record WalletStatisticsDto(
int TotalWallets,
int ActiveWallets,
int FrozenWallets,
int ClosedWallets,
Dictionary<string, decimal> TotalBalanceByCurrency,
decimal TotalTransactionsToday,
decimal TotalDepositsToday,
decimal TotalWithdrawalsToday);
/// <summary>
/// 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.
/// </summary>
public record AdminPointAccountsListDto(
List<AdminPointAccountSummaryDto> 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);
/// <summary>
/// EN: DTO for points statistics.
/// VI: DTO cho thống kê điểm.
/// </summary>
public record PointsStatisticsDto(
int TotalAccounts,
long TotalPointsIssued,
long TotalPointsAvailable,
long TotalPointsSpent,
long TotalPointsExpired,
long PointsEarnedToday,
long PointsSpentToday);
#endregion

View File

@@ -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;
/// <summary>
/// EN: Admin controller for points management operations (Backoffice).
/// VI: Controller Admin cho các thao tác quản lý điểm (Backoffice).
/// </summary>
[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<AdminPointsController> _logger;
public AdminPointsController(
IMediator mediator,
IPointAccountRepository pointAccountRepository,
ILogger<AdminPointsController> logger)
{
_mediator = mediator;
_pointAccountRepository = pointAccountRepository;
_logger = logger;
}
/// <summary>
/// EN: Get all point accounts with pagination.
/// VI: Lấy tất cả tài khoản điểm với phân trang.
/// </summary>
[HttpGet]
[SwaggerOperation(Summary = "Get all point accounts", Description = "Get all point accounts with pagination (Admin only)")]
[SwaggerResponse(200, "Success", typeof(ApiResponse<AdminPointAccountsListDto>))]
public async Task<IActionResult> 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<AdminPointAccountsListDto>(true, "Success", result));
}
/// <summary>
/// EN: Get point account details by ID.
/// VI: Lấy chi tiết tài khoản điểm theo ID.
/// </summary>
[HttpGet("{accountId:guid}")]
[SwaggerOperation(Summary = "Get point account by ID", Description = "Get point account details by ID (Admin only)")]
[SwaggerResponse(200, "Success", typeof(ApiResponse<AdminPointAccountDetailDto>))]
[SwaggerResponse(404, "Point account not found")]
public async Task<IActionResult> GetPointAccountById(Guid accountId)
{
var query = new GetPointAccountByIdQuery(accountId);
var result = await _mediator.Send(query);
if (result == null)
return NotFound(new ApiResponse<object>(false, "Point account not found"));
return Ok(new ApiResponse<AdminPointAccountDetailDto>(true, "Success", result));
}
/// <summary>
/// EN: Adjust points (add or subtract).
/// VI: Điều chỉnh điểm (cộng hoặc trừ).
/// </summary>
[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<IActionResult> 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<AdminAdjustPointsResult>(true, "Points adjusted successfully", result));
}
/// <summary>
/// EN: Grant bonus points to user.
/// VI: Tặng điểm thưởng cho người dùng.
/// </summary>
[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<IActionResult> 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<AdminGrantBonusResult>(true, "Bonus points granted successfully", result));
}
/// <summary>
/// EN: Get points statistics.
/// VI: Lấy thống kê điểm.
/// </summary>
[HttpGet("statistics")]
[SwaggerOperation(Summary = "Get statistics", Description = "Get points statistics (Admin only)")]
[SwaggerResponse(200, "Success", typeof(ApiResponse<PointsStatisticsDto>))]
public async Task<IActionResult> GetStatistics()
{
var query = new GetPointsStatisticsQuery();
var result = await _mediator.Send(query);
return Ok(new ApiResponse<PointsStatisticsDto>(true, "Success", result));
}
/// <summary>
/// EN: Search point accounts by user ID.
/// VI: Tìm kiếm tài khoản điểm theo user ID.
/// </summary>
[HttpGet("search")]
[SwaggerOperation(Summary = "Search point accounts", Description = "Search point accounts by user ID (Admin only)")]
[SwaggerResponse(200, "Success")]
public async Task<IActionResult> SearchPointAccounts([FromQuery] Guid? userId = null)
{
var query = new SearchPointAccountsQuery(userId);
var result = await _mediator.Send(query);
return Ok(new ApiResponse<List<AdminPointAccountDetailDto>>(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

View File

@@ -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;
/// <summary>
/// EN: Admin controller for wallet management operations (Backoffice).
/// VI: Controller Admin cho các thao tác quản lý ví (Backoffice).
/// </summary>
[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<AdminWalletsController> _logger;
public AdminWalletsController(
IMediator mediator,
IWalletRepository walletRepository,
ILogger<AdminWalletsController> logger)
{
_mediator = mediator;
_walletRepository = walletRepository;
_logger = logger;
}
/// <summary>
/// EN: Get all wallets with pagination.
/// VI: Lấy tất cả ví với phân trang.
/// </summary>
[HttpGet]
[SwaggerOperation(Summary = "Get all wallets", Description = "Get all wallets with pagination (Admin only)")]
[SwaggerResponse(200, "Success", typeof(ApiResponse<AdminWalletsListDto>))]
public async Task<IActionResult> 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<AdminWalletsListDto>(true, "Success", result));
}
/// <summary>
/// EN: Get wallet details by ID.
/// VI: Lấy chi tiết ví theo ID.
/// </summary>
[HttpGet("{walletId:guid}")]
[SwaggerOperation(Summary = "Get wallet by ID", Description = "Get wallet details by wallet ID (Admin only)")]
[SwaggerResponse(200, "Success", typeof(ApiResponse<AdminWalletDetailDto>))]
[SwaggerResponse(404, "Wallet not found")]
public async Task<IActionResult> GetWalletById(Guid walletId)
{
var query = new GetWalletByIdQuery(walletId);
var result = await _mediator.Send(query);
if (result == null)
return NotFound(new ApiResponse<object>(false, "Wallet not found"));
return Ok(new ApiResponse<AdminWalletDetailDto>(true, "Success", result));
}
/// <summary>
/// EN: Freeze wallet.
/// VI: Đóng băng ví.
/// </summary>
[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<IActionResult> 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<AdminWalletActionResult>(true, "Wallet frozen successfully", result));
}
/// <summary>
/// EN: Unfreeze wallet.
/// VI: Mở băng ví.
/// </summary>
[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<IActionResult> 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<AdminWalletActionResult>(true, "Wallet unfrozen successfully", result));
}
/// <summary>
/// EN: Adjust wallet balance (credit/debit).
/// VI: Điều chỉnh số dư ví (cộng/trừ).
/// </summary>
[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<IActionResult> 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<AdminAdjustBalanceResult>(true, "Balance adjusted successfully", result));
}
/// <summary>
/// EN: Get wallet statistics.
/// VI: Lấy thống kê ví.
/// </summary>
[HttpGet("statistics")]
[SwaggerOperation(Summary = "Get statistics", Description = "Get wallet statistics (Admin only)")]
[SwaggerResponse(200, "Success", typeof(ApiResponse<WalletStatisticsDto>))]
public async Task<IActionResult> GetStatistics()
{
var query = new GetWalletStatisticsQuery();
var result = await _mediator.Send(query);
return Ok(new ApiResponse<WalletStatisticsDto>(true, "Success", result));
}
/// <summary>
/// EN: Search wallets by user ID or wallet ID.
/// VI: Tìm kiếm ví theo user ID hoặc wallet ID.
/// </summary>
[HttpGet("search")]
[SwaggerOperation(Summary = "Search wallets", Description = "Search wallets by user ID or wallet ID (Admin only)")]
[SwaggerResponse(200, "Success")]
public async Task<IActionResult> 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<List<AdminWalletDetailDto>>(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