refactor(web-client-tpos, order-service): improve API deserialization, update DTO types for Dapper compatibility, and refine API proxying for staff schedules and order cancellations.

This commit is contained in:
Ho Ngoc Hai
2026-03-04 12:53:43 +07:00
parent 64e7b4e00d
commit 7baba14fad
6 changed files with 155 additions and 81 deletions

View File

@@ -47,6 +47,68 @@ public class PosDataService
}
}
/// <summary>
/// EN: Robust list deserialization — handles plain arrays, PagedResult wrappers, and ApiResponse envelopes.
/// VI: Deserialize list linh hoạt — xử lý array thuần, PagedResult wrapper, và ApiResponse envelope.
/// </summary>
private async Task<List<T>> GetListFromApiAsync<T>(string url)
{
AttachToken();
var resp = await _http.GetAsync(url);
if (!resp.IsSuccessStatusCode) return new();
var json = await resp.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(json)) return new();
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Case 1: plain array [...]
if (root.ValueKind == JsonValueKind.Array)
return JsonSerializer.Deserialize<List<T>>(json, _jsonOptions) ?? new();
// Case 2: { "items": [...] } (PagedResult)
if (root.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Array)
return JsonSerializer.Deserialize<List<T>>(items.GetRawText(), _jsonOptions) ?? new();
// Case 3: { "data": { "items": [...] } } (ApiResponse<PagedResult>)
// Case 4: { "data": [...] } (ApiResponse<List>)
if (root.TryGetProperty("data", out var data))
{
if (data.ValueKind == JsonValueKind.Object && data.TryGetProperty("items", out var dataItems) && dataItems.ValueKind == JsonValueKind.Array)
return JsonSerializer.Deserialize<List<T>>(dataItems.GetRawText(), _jsonOptions) ?? new();
if (data.ValueKind == JsonValueKind.Array)
return JsonSerializer.Deserialize<List<T>>(data.GetRawText(), _jsonOptions) ?? new();
}
return new();
}
/// <summary>
/// EN: Robust single-object deserialization — handles plain objects and ApiResponse envelopes.
/// VI: Deserialize đối tượng đơn linh hoạt — xử lý object thuần và ApiResponse envelope.
/// </summary>
private async Task<T?> GetObjectFromApiAsync<T>(string url) where T : class
{
AttachToken();
var resp = await _http.GetAsync(url);
if (!resp.IsSuccessStatusCode) return null;
var json = await resp.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(json)) return null;
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Case 1: { "data": {...} } (ApiResponse envelope)
if (root.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.Object)
return JsonSerializer.Deserialize<T>(data.GetRawText(), _jsonOptions);
// Case 2: plain object
if (root.ValueKind == JsonValueKind.Object)
return JsonSerializer.Deserialize<T>(json, _jsonOptions);
return null;
}
public record ShopInfo(Guid Id, string Name, string Slug, string? Description, string? Phone, string? Email, string? Category, string? Status, Guid? MerchantId = null);
public record ProductInfo(Guid Id, string Name, decimal Price, string? Sku, string? Description, string? Category, int? DurationMinutes);
public record CategoryInfo(Guid Id, string Name, string? Description, int DisplayOrder);
@@ -55,25 +117,25 @@ public class PosDataService
public record StaffInfo(Guid Id, Guid? UserId, string? EmployeeCode, string? Phone, string? Email, DateTime? JoinedAt, DateTime? TerminatedAt, string? Role, string? Status, string? ShopName);
public async Task<List<ShopInfo>> GetShopsAsync()
{ AttachToken(); return await _http.GetFromJsonAsync<List<ShopInfo>>("api/bff/shops", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<ShopInfo>("api/bff/shops");
public async Task<ShopInfo?> GetShopByIdAsync(Guid shopId)
{ AttachToken(); return await _http.GetFromJsonAsync<ShopInfo>($"api/bff/shops/{shopId}", _jsonOptions); }
=> await GetObjectFromApiAsync<ShopInfo>($"api/bff/shops/{shopId}");
public async Task<List<ProductInfo>> GetProductsAsync(Guid shopId)
{ AttachToken(); return await _http.GetFromJsonAsync<List<ProductInfo>>($"api/bff/shops/{shopId}/products", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<ProductInfo>($"api/bff/shops/{shopId}/products");
public async Task<List<CategoryInfo>> GetCategoriesAsync(Guid shopId)
{ AttachToken(); return await _http.GetFromJsonAsync<List<CategoryInfo>>($"api/bff/shops/{shopId}/categories", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<CategoryInfo>($"api/bff/shops/{shopId}/categories");
public async Task<List<TableInfo>> GetTablesAsync(Guid shopId)
{ AttachToken(); return await _http.GetFromJsonAsync<List<TableInfo>>($"api/bff/shops/{shopId}/tables", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<TableInfo>($"api/bff/shops/{shopId}/tables");
public async Task<List<AppointmentInfo>> GetAppointmentsAsync(Guid shopId)
{ AttachToken(); return await _http.GetFromJsonAsync<List<AppointmentInfo>>($"api/bff/shops/{shopId}/appointments", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<AppointmentInfo>($"api/bff/shops/{shopId}/appointments");
public async Task<List<StaffInfo>> GetStaffAsync()
{ AttachToken(); return await _http.GetFromJsonAsync<List<StaffInfo>>("api/bff/staff", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<StaffInfo>("api/bff/staff");
// ═══ ADMIN-LEVEL PRODUCT/CATEGORY METHODS ═══
@@ -88,16 +150,14 @@ public class PosDataService
public async Task<List<AdminProductInfo>> GetAllProductsAsync(Guid? shopId = null)
{
AttachToken();
var url = shopId.HasValue ? $"api/bff/products?shopId={shopId}" : "api/bff/products";
return await _http.GetFromJsonAsync<List<AdminProductInfo>>(url, _jsonOptions) ?? new();
return await GetListFromApiAsync<AdminProductInfo>(url);
}
public async Task<List<AdminCategoryInfo>> GetAllCategoriesAsync(Guid? shopId = null)
{
AttachToken();
var url = shopId.HasValue ? $"api/bff/categories?shopId={shopId}" : "api/bff/categories";
return await _http.GetFromJsonAsync<List<AdminCategoryInfo>>(url, _jsonOptions) ?? new();
return await GetListFromApiAsync<AdminCategoryInfo>(url);
}
public async Task<bool> CreateProductAsync(CreateProductRequest req)
@@ -128,9 +188,8 @@ public class PosDataService
public async Task<List<InventoryItemInfo>> GetInventoryAsync(Guid? shopId = null)
{
AttachToken();
var url = shopId.HasValue ? $"api/bff/inventory?shopId={shopId}" : "api/bff/inventory";
return await _http.GetFromJsonAsync<List<InventoryItemInfo>>(url, _jsonOptions) ?? new();
return await GetListFromApiAsync<InventoryItemInfo>(url);
}
// ═══ MEMBERSHIP/CUSTOMER METHODS ═══
@@ -139,7 +198,7 @@ public class PosDataService
int CurrentLevel, int TotalExpEarned, DateTime CreatedAt, string? LevelName);
public async Task<List<MemberInfo>> GetMembersAsync()
{ AttachToken(); return await _http.GetFromJsonAsync<List<MemberInfo>>("api/bff/members", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<MemberInfo>("api/bff/members");
// ═══ STAFF CREATE ═══
@@ -182,13 +241,12 @@ public class PosDataService
string? EmployeeCode, string? Role, string? Phone);
public async Task<List<StaffRoleInfo>> GetStaffRolesAsync()
{ AttachToken(); return await _http.GetFromJsonAsync<List<StaffRoleInfo>>("api/bff/staff/roles", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<StaffRoleInfo>("api/bff/staff/roles");
public async Task<List<ScheduleInfo>> GetStaffSchedulesAsync(Guid? shopId = null)
{
AttachToken();
var url = shopId.HasValue ? $"api/bff/staff/schedules?shopId={shopId}" : "api/bff/staff/schedules";
return await _http.GetFromJsonAsync<List<ScheduleInfo>>(url, _jsonOptions) ?? new();
return await GetListFromApiAsync<ScheduleInfo>(url);
}
// ═══ ORDERS ═══
@@ -198,11 +256,10 @@ public class PosDataService
public async Task<List<OrderInfo>> GetOrdersAsync(Guid? shopId = null, string filter = "today")
{
AttachToken();
var url = shopId.HasValue
? $"api/bff/orders?shopId={shopId}&filter={filter}"
: $"api/bff/orders?filter={filter}";
return await _http.GetFromJsonAsync<List<OrderInfo>>(url, _jsonOptions) ?? new();
return await GetListFromApiAsync<OrderInfo>(url);
}
// ═══ WALLETS / FINANCE ═══
@@ -211,17 +268,17 @@ public class PosDataService
public record WalletTxnInfo(Guid Id, Guid WalletId, decimal Amount, string? Description, DateTime CreatedAt, string? ItemName);
public async Task<List<WalletInfo>> GetWalletsAsync()
{ AttachToken(); return await _http.GetFromJsonAsync<List<WalletInfo>>("api/bff/wallets", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<WalletInfo>("api/bff/wallets");
public async Task<List<WalletTxnInfo>> GetWalletTransactionsAsync(int limit = 50)
{ AttachToken(); return await _http.GetFromJsonAsync<List<WalletTxnInfo>>($"api/bff/wallet/transactions?limit={limit}", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<WalletTxnInfo>($"api/bff/wallet/transactions?limit={limit}");
// ═══ DEVICES ═══
public record DeviceInfo(Guid Id, string? DeviceToken, string? Platform, bool IsActive, DateTime CreatedAt, string? StaffCode);
public async Task<List<DeviceInfo>> GetDevicesAsync()
{ AttachToken(); return await _http.GetFromJsonAsync<List<DeviceInfo>>("api/bff/devices", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<DeviceInfo>("api/bff/devices");
// ═══ PROMOTIONS ═══
@@ -229,7 +286,7 @@ public class PosDataService
bool IsActive, string? DiscountType, decimal? DiscountValue, int VoucherCount, int RedemptionCount);
public async Task<List<PromotionInfo>> GetPromotionsAsync()
{ AttachToken(); return await _http.GetFromJsonAsync<List<PromotionInfo>>("api/bff/promotions", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<PromotionInfo>("api/bff/promotions");
// ═══ CAMPAIGNS CRUD ═══
@@ -240,7 +297,7 @@ public class PosDataService
public record CreateCampaignRequest(string Name, string? Description, decimal FaceValue, int TotalVouchers, DateTime StartDate, DateTime EndDate);
public async Task<List<CampaignInfo>> GetCampaignsAsync()
{ AttachToken(); return await _http.GetFromJsonAsync<List<CampaignInfo>>("api/bff/campaigns", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<CampaignInfo>("api/bff/campaigns");
public async Task<bool> CreateCampaignAsync(CreateCampaignRequest req)
{
@@ -297,9 +354,8 @@ public class PosDataService
public async Task<List<InventoryTxnInfo>> GetInventoryTransactionsAsync(Guid? shopId = null)
{
AttachToken();
var url = shopId.HasValue ? $"api/bff/inventory/transactions?shopId={shopId}" : "api/bff/inventory/transactions";
return await _http.GetFromJsonAsync<List<InventoryTxnInfo>>(url, _jsonOptions) ?? new();
return await GetListFromApiAsync<InventoryTxnInfo>(url);
}
// ═══ MEMBERSHIP LEVELS ═══
@@ -307,21 +363,21 @@ public class PosDataService
public record LevelDefinitionInfo(Guid Id, int Level, string Name, int MinExp, int MaxExp, int MemberCount);
public async Task<List<LevelDefinitionInfo>> GetMembershipLevelsAsync()
{ AttachToken(); return await _http.GetFromJsonAsync<List<LevelDefinitionInfo>>("api/bff/membership/levels", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<LevelDefinitionInfo>("api/bff/membership/levels");
// ═══ SHOP STATS (aggregated per-shop) ═══
public record ShopStatsInfo(Guid ShopId, int ProductCount, int OrderCount, int StaffCount, decimal Revenue);
public async Task<List<ShopStatsInfo>> GetShopStatsAsync()
{ AttachToken(); return await _http.GetFromJsonAsync<List<ShopStatsInfo>>("api/bff/shops/stats", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<ShopStatsInfo>("api/bff/shops/stats");
// ═══ BOOKING RESOURCES ═══
public record ResourceInfo(Guid Id, string Name, string? ResourceType, int Capacity, bool IsActive);
public async Task<List<ResourceInfo>> GetResourcesAsync(Guid shopId)
{ AttachToken(); return await _http.GetFromJsonAsync<List<ResourceInfo>>($"api/bff/shops/{shopId}/resources", _jsonOptions) ?? new(); }
=> await GetListFromApiAsync<ResourceInfo>($"api/bff/shops/{shopId}/resources");
// ═══ POS DASHBOARD (real-time daily stats) ═══
@@ -351,7 +407,7 @@ public class PosDataService
// EN: POS order creation DTOs
// VI: DTOs cho tạo đơn POS
public record CreatePosOrderRequest(Guid ShopId, string? PaymentMethod, List<PosOrderItemRequest> Items);
public record PosOrderItemRequest(Guid ProductId, string ProductName, int Quantity, decimal UnitPrice);
public record PosOrderItemRequest(Guid ProductId, string ProductName, int Quantity, decimal UnitPrice, string? ProductType = "Physical");
public record CreatePosOrderResponse(Guid OrderId, string TransactionId, decimal TotalAmount, string Status);
public async Task<CreatePosOrderResponse?> CreatePosOrderAsync(CreatePosOrderRequest req)
@@ -434,11 +490,10 @@ public class PosDataService
public async Task<List<RevenueReportItem>> GetRevenueReportAsync(string period = "daily", Guid? shopId = null)
{
AttachToken();
var url = shopId.HasValue
? $"api/bff/reports/revenue?period={period}&shopId={shopId}"
: $"api/bff/reports/revenue?period={period}";
return await _http.GetFromJsonAsync<List<RevenueReportItem>>(url, _jsonOptions) ?? new();
return await GetListFromApiAsync<RevenueReportItem>(url);
}
// ═══ SHOP SETTINGS ═══
@@ -447,10 +502,7 @@ public class PosDataService
public record UpdateShopSettingsRequest(string? FeaturesConfig, string? OpenTime, string? CloseTime, string? OpenDays);
public async Task<ShopSettingsInfo?> GetShopSettingsAsync(Guid shopId)
{
AttachToken();
return await _http.GetFromJsonAsync<ShopSettingsInfo>($"api/bff/shops/{shopId}/settings", _jsonOptions);
}
=> await GetObjectFromApiAsync<ShopSettingsInfo>($"api/bff/shops/{shopId}/settings");
public async Task<bool> UpdateShopSettingsAsync(Guid shopId, UpdateShopSettingsRequest req)
{
@@ -465,11 +517,10 @@ public class PosDataService
public async Task<List<TopProductInfo>> GetTopProductsAsync(Guid? shopId = null, int limit = 10)
{
AttachToken();
var url = shopId.HasValue
? $"api/bff/reports/top-products?shopId={shopId}&limit={limit}"
: $"api/bff/reports/top-products?limit={limit}";
return await _http.GetFromJsonAsync<List<TopProductInfo>>(url, _jsonOptions) ?? new();
return await GetListFromApiAsync<TopProductInfo>(url);
}
// ═══ TABLES CRUD ═══
@@ -531,10 +582,9 @@ public class PosDataService
public async Task<List<KitchenTicketInfo>> GetKitchenTicketsAsync(Guid? shopId = null, string status = "pending")
{
AttachToken();
if (!shopId.HasValue) return new();
var url = $"api/bff/shops/{shopId}/kitchen-tickets?status={status}";
return await _http.GetFromJsonAsync<List<KitchenTicketInfo>>(url, _jsonOptions) ?? new();
return await GetListFromApiAsync<KitchenTicketInfo>(url);
}
public async Task<bool> UpdateTicketStatusAsync(Guid ticketId, UpdateTicketStatusRequest req)
@@ -549,10 +599,9 @@ public class PosDataService
public async Task<List<RecipeInfo>> GetRecipesAsync(Guid? shopId = null)
{
AttachToken();
if (!shopId.HasValue) return new();
var url = $"api/bff/shops/{shopId}/recipes";
return await _http.GetFromJsonAsync<List<RecipeInfo>>(url, _jsonOptions) ?? new();
return await GetListFromApiAsync<RecipeInfo>(url);
}
public async Task<bool> CreateRecipeAsync(CreateRecipeRequest req)

View File

@@ -48,8 +48,12 @@ public class BookingController : ControllerBase
/// VI: Hủy lịch hẹn.
/// </summary>
[HttpDelete("appointments/{apptId:guid}/cancel")]
public Task<IActionResult> CancelAppointment(Guid apptId) =>
_booking.DeleteAsync($"/api/v1/appointments/{apptId}/cancel").ProxyAsync();
public async Task<IActionResult> CancelAppointment(Guid apptId)
{
using var request = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/appointments/{apptId}");
request.Content = System.Net.Http.Json.JsonContent.Create(new { reason = "Cancelled from POS" });
return await _booking.SendAsync(request).ProxyAsync();
}
/// <summary>
/// EN: Get resources for a specific shop.

View File

@@ -30,8 +30,20 @@ public class OrderController : ControllerBase
{
var qs = new List<string>();
if (shopId.HasValue) qs.Add($"shopId={shopId}");
if (!string.IsNullOrEmpty(filter)) qs.Add($"filter={Uri.EscapeDataString(filter)}");
var query = qs.Count > 0 ? "?" + string.Join("&", qs) : "";
// EN: Convert filter shorthand to fromDate/toDate for OrderService
// VI: Chuyển đổi filter shorthand thành fromDate/toDate cho OrderService
var now = DateTime.UtcNow;
var (fromDate, toDate) = filter?.ToLower() switch
{
"week" => (now.Date.AddDays(-(int)now.DayOfWeek), now),
"month" => (new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc), now),
_ => (now.Date, now) // "today" or default
};
qs.Add($"fromDate={fromDate:O}");
qs.Add($"toDate={toDate:O}");
var query = "?" + string.Join("&", qs);
return _order.GetAsync($"/api/v1/orders{query}").ProxyAsync();
}
@@ -48,8 +60,13 @@ public class OrderController : ControllerBase
/// VI: Hủy đơn hàng.
/// </summary>
[HttpPut("orders/{orderId:guid}/cancel")]
public Task<IActionResult> CancelOrder(Guid orderId) =>
_order.PutAsync($"/api/v1/orders/{orderId}/cancel", null).ProxyAsync();
public Task<IActionResult> CancelOrder(Guid orderId, [FromQuery] Guid? shopId = null)
{
var qs = shopId.HasValue ? $"?shopId={shopId}" : "";
return _order.PostAsJsonAsync(
$"/api/v1/orders/{orderId}/cancel{qs}",
new { reason = "Cancelled from POS" }).ProxyAsync();
}
/// <summary>
/// EN: Create a POS order.

View File

@@ -5,18 +5,20 @@ using WebClientTpos.Server.Infrastructure;
namespace WebClientTpos.Server.Controllers;
/// <summary>
/// EN: Staff controller — proxies to MerchantService for staff/roles/schedules CRUD.
/// VI: Controller nhân viên — proxy đến MerchantService cho CRUD nhân viên/vai trò/lịch.
/// EN: Staff controller — proxies to MerchantService for staff/roles CRUD and BookingService for schedules.
/// VI: Controller nhân viên — proxy đến MerchantService cho CRUD nhân viên/vai trò và BookingService cho lịch.
/// </summary>
[ApiController]
[Route("api/bff")]
public class StaffController : ControllerBase
{
private readonly HttpClient _merchant;
private readonly HttpClient _booking;
public StaffController(IHttpClientFactory httpClientFactory)
{
_merchant = httpClientFactory.CreateClient("MerchantService");
_booking = httpClientFactory.CreateClient("BookingService");
}
/// <summary>
@@ -57,40 +59,40 @@ public class StaffController : ControllerBase
/// </summary>
[HttpGet("staff/roles")]
public Task<IActionResult> GetStaffRoles() =>
_merchant.GetAsync("/api/v1/merchants/me/staff/roles").ProxyAsync();
_merchant.GetAsync("/api/v1/staff/roles").ProxyAsync();
/// <summary>
/// EN: Get staff schedules — optionally filtered by shopId.
/// VI: Lấy lịch làm việc nhân viên — tùy chọn lọc theo shopId.
/// EN: Get staff schedules — proxies to BookingService.
/// VI: Lấy lịch làm việc nhân viên — proxy đến BookingService.
/// </summary>
[HttpGet("staff/schedules")]
public Task<IActionResult> GetStaffSchedules([FromQuery] Guid? shopId = null)
{
var qs = shopId.HasValue ? $"?shopId={shopId}" : "";
return _merchant.GetAsync($"/api/v1/merchants/me/staff/schedules{qs}").ProxyAsync();
return _booking.GetAsync($"/api/v1/schedules{qs}").ProxyAsync();
}
/// <summary>
/// EN: Create a staff schedule.
/// VI: Tạo lịch làm việc nhân viên.
/// EN: Create a staff schedule — proxies to BookingService.
/// VI: Tạo lịch làm việc nhân viên — proxy đến BookingService.
/// </summary>
[HttpPost("staff/schedules")]
public Task<IActionResult> CreateSchedule([FromBody] JsonElement body) =>
_merchant.PostAsJsonAsync("/api/v1/merchants/me/staff/schedules", body).ProxyAsync();
_booking.PostAsJsonAsync("/api/v1/schedules", body).ProxyAsync();
/// <summary>
/// EN: Update a staff schedule.
/// VI: Cập nhật lịch làm việc nhân viên.
/// EN: Update a staff schedule — proxies to BookingService.
/// VI: Cập nhật lịch làm việc nhân viên — proxy đến BookingService.
/// </summary>
[HttpPut("staff/schedules/{scheduleId:guid}")]
public Task<IActionResult> UpdateSchedule(Guid scheduleId, [FromBody] JsonElement body) =>
_merchant.PutAsJsonAsync($"/api/v1/merchants/me/staff/schedules/{scheduleId}", body).ProxyAsync();
_booking.PutAsJsonAsync($"/api/v1/schedules/{scheduleId}", body).ProxyAsync();
/// <summary>
/// EN: Delete a staff schedule.
/// VI: Xóa lịch làm việc nhân viên.
/// EN: Delete a staff schedule — proxies to BookingService.
/// VI: Xóa lịch làm việc nhân viên — proxy đến BookingService.
/// </summary>
[HttpDelete("staff/schedules/{scheduleId:guid}")]
public Task<IActionResult> DeleteSchedule(Guid scheduleId) =>
_merchant.DeleteAsync($"/api/v1/merchants/me/staff/schedules/{scheduleId}").ProxyAsync();
_booking.DeleteAsync($"/api/v1/schedules/{scheduleId}").ProxyAsync();
}

View File

@@ -26,20 +26,21 @@ public record RevenueReportDto(
string Period,
Guid ShopId,
decimal TotalRevenue,
int TotalOrders,
long TotalOrders,
List<RevenuePeriodDto> Data
);
/// <summary>
/// EN: Revenue data for a single period.
/// VI: Dữ liệu doanh thu cho một kỳ.
/// EN: Revenue data for a single period (class for Dapper compatibility).
/// VI: Dữ liệu doanh thu cho một kỳ (class cho tương thích Dapper).
/// </summary>
public record RevenuePeriodDto(
DateTime PeriodStart,
decimal Revenue,
int OrderCount,
decimal AvgOrderValue
);
public class RevenuePeriodDto
{
public DateTime PeriodStart { get; set; }
public decimal Revenue { get; set; }
public long OrderCount { get; set; }
public decimal AvgOrderValue { get; set; }
}
/// <summary>
/// EN: Handler for GetRevenueReportQuery.

View File

@@ -19,16 +19,17 @@ public record GetTopProductsQuery(
) : IRequest<List<TopProductDto>>;
/// <summary>
/// EN: Top product DTO.
/// VI: DTO sản phẩm bán chạy.
/// EN: Top product DTO (class for Dapper compatibility).
/// VI: DTO sản phẩm bán chạy (class cho tương thích Dapper).
/// </summary>
public record TopProductDto(
Guid ProductId,
string ProductName,
int TotalQuantity,
decimal TotalRevenue,
int OrderCount
);
public class TopProductDto
{
public Guid ProductId { get; set; }
public string ProductName { get; set; } = string.Empty;
public long TotalQuantity { get; set; }
public decimal TotalRevenue { get; set; }
public long OrderCount { get; set; }
}
/// <summary>
/// EN: Handler for GetTopProductsQuery.