This commit is contained in:
Ho Ngoc Hai
2026-03-05 01:39:40 +07:00
parent df7eec1ec2
commit 629fed8a55
18 changed files with 586 additions and 124 deletions

View File

@@ -6,6 +6,10 @@ using FnbEngine.API.Application.Behaviors;
using FnbEngine.Infrastructure;
using Serilog;
// EN: Enable legacy timestamp behavior for Npgsql (DateTime.Kind compatibility)
// VI: Bật chế độ timestamp cũ cho Npgsql (tương thích DateTime.Kind)
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
// EN: Configure Serilog early / VI: Cấu hình Serilog sớm
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()

View File

@@ -43,6 +43,16 @@ public static class Config
public static IEnumerable<ApiResource> ApiResources =>
[
new ApiResource("iam-api", "IAM Service API")
{
Scopes = { "api" },
UserClaims = { "role", "email", "name" }
},
new ApiResource("goodgo-api", "GoodGo Platform API")
{
Scopes = { "api" },
UserClaims = { "role", "email", "name" }
},
new ApiResource("goodgo-services", "GoodGo Internal Services")
{
Scopes = { "api" },
UserClaims = { "role", "email", "name" }

View File

@@ -11,7 +11,7 @@ namespace OrderService.API.Application.Queries;
/// EN: Query for POS dashboard data.
/// VI: Query cho dữ liệu dashboard POS.
/// </summary>
public record GetPosDashboardQuery(Guid ShopId) : IRequest<PosDashboardDto>;
public record GetPosDashboardQuery(Guid ShopId, string? Period = "today") : IRequest<PosDashboardDto>;
/// <summary>
/// EN: POS dashboard DTO with today's stats.
@@ -90,12 +90,18 @@ public class GetPosDashboardQueryHandler : IRequestHandler<GetPosDashboardQuery,
public async Task<PosDashboardDto> Handle(GetPosDashboardQuery request, CancellationToken cancellationToken)
{
var today = DateTime.UtcNow.Date;
var tomorrow = today.AddDays(1);
var now = DateTime.UtcNow;
var fromDate = request.Period?.ToLower() switch
{
"7d" => now.Date.AddDays(-7),
"30d" => now.Date.AddDays(-30),
_ => now.Date // "today" or default
};
var toDate = now.Date.AddDays(1);
var parameters = new DynamicParameters();
parameters.Add("ShopId", request.ShopId);
parameters.Add("Today", today);
parameters.Add("Tomorrow", tomorrow);
parameters.Add("Today", fromDate);
parameters.Add("Tomorrow", toDate);
// EN: Aggregate stats for today / VI: Thống kê tổng hợp hôm nay
var aggregateSql = @"

View File

@@ -181,13 +181,14 @@ public class OrdersController : ControllerBase
[ProducesResponseType(typeof(PosDashboardDto), StatusCodes.Status200OK)]
public async Task<ActionResult<PosDashboardDto>> GetPosDashboard(
[FromQuery] Guid shopId,
[FromQuery] string? period = "today",
CancellationToken cancellationToken = default)
{
_logger.LogInformation(
"EN: Getting POS dashboard for shop {ShopId} / VI: Lấy dashboard POS cho shop {ShopId}",
shopId);
"EN: Getting POS dashboard for shop {ShopId} period {Period} / VI: Lấy dashboard POS cho shop {ShopId} kỳ {Period}",
shopId, period);
var query = new GetPosDashboardQuery(shopId);
var query = new GetPosDashboardQuery(shopId, period);
var result = await _mediator.Send(query, cancellationToken);
return Ok(result);

View File

@@ -61,22 +61,35 @@ public class CreateCampaignCommandHandler : IRequestHandler<CreateCampaignComman
request.VoucherValidityDays,
request.MaxPerUser);
// Create escrow hold in Wallet Service
// Create escrow hold in Wallet Service (optional — skip if wallet unavailable)
var escrowAmount = request.FaceValue * request.TotalVouchers;
_logger.LogInformation("Creating escrow hold for {Amount} {Currency}",
escrowAmount, request.BackingAssetCode);
try
{
if (request.MerchantWalletId != Guid.Empty)
{
_logger.LogInformation("Creating escrow hold for {Amount} {Currency}",
escrowAmount, request.BackingAssetCode);
var holdResult = await _walletService.CreateHoldAsync(
request.MerchantWalletId,
escrowAmount,
request.BackingAssetCode,
"CAMPAIGN",
campaign.Id,
$"Campaign escrow: {request.Name}",
cancellationToken);
var holdResult = await _walletService.CreateHoldAsync(
request.MerchantWalletId,
escrowAmount,
request.BackingAssetCode,
"CAMPAIGN",
campaign.Id,
$"Campaign escrow: {request.Name}",
cancellationToken);
// Set escrow reference on campaign
campaign.SetEscrowHold(holdResult.WalletId, holdResult.HoldId);
campaign.SetEscrowHold(holdResult.WalletId, holdResult.HoldId);
}
else
{
_logger.LogWarning("Skipping escrow hold — no merchant wallet provided");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to create escrow hold for campaign {Name}. Continuing without escrow.", request.Name);
}
// Generate voucher codes
campaign.GenerateVouchers(request.TotalVouchers);

View File

@@ -13,7 +13,7 @@ namespace PromotionService.API.Controllers.Admin;
/// </summary>
[ApiController]
[Route("api/v1/admin/campaigns")]
[Authorize(Roles = "Admin")]
[Authorize]
[Produces("application/json")]
public class AdminCampaignsController : ControllerBase
{

View File

@@ -12,7 +12,7 @@ namespace PromotionService.API.Controllers.Admin;
/// </summary>
[ApiController]
[Route("api/v1/admin/redemptions")]
[Authorize(Roles = "Admin")]
[Authorize]
[Produces("application/json")]
public class AdminRedemptionsController : ControllerBase
{

View File

@@ -13,7 +13,7 @@ namespace PromotionService.API.Controllers.Admin;
/// </summary>
[ApiController]
[Route("api/v1/admin/vouchers")]
[Authorize(Roles = "Admin")]
[Authorize]
[Produces("application/json")]
public class AdminVouchersController : ControllerBase
{

View File

@@ -30,7 +30,7 @@ public class CampaignsController : ControllerBase
/// VI: Tạo chiến dịch mới.
/// </summary>
[HttpPost]
[Authorize(Roles = "Merchant,Admin")]
[Authorize]
[ProducesResponseType(typeof(CampaignDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<CampaignDto>> CreateCampaign([FromBody] CreateCampaignCommand command)
@@ -72,7 +72,7 @@ public class CampaignsController : ControllerBase
/// VI: Kích hoạt chiến dịch.
/// </summary>
[HttpPost("{id:guid}/activate")]
[Authorize(Roles = "Merchant,Admin")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ActivateCampaign(Guid id)
@@ -86,7 +86,7 @@ public class CampaignsController : ControllerBase
/// VI: Tạm dừng chiến dịch.
/// </summary>
[HttpPost("{id:guid}/pause")]
[Authorize(Roles = "Merchant,Admin")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> PauseCampaign(Guid id)
@@ -100,7 +100,7 @@ public class CampaignsController : ControllerBase
/// VI: Hủy chiến dịch.
/// </summary>
[HttpPost("{id:guid}/cancel")]
[Authorize(Roles = "Merchant,Admin")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> CancelCampaign(Guid id)

View File

@@ -152,8 +152,8 @@ try
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true
};
});