diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs
index 0ab0a9de..6aed87d1 100644
--- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs
@@ -47,6 +47,68 @@ public class PosDataService
}
}
+ ///
+ /// 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.
+ ///
+ private async Task> GetListFromApiAsync(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>(json, _jsonOptions) ?? new();
+
+ // Case 2: { "items": [...] } (PagedResult)
+ if (root.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Array)
+ return JsonSerializer.Deserialize>(items.GetRawText(), _jsonOptions) ?? new();
+
+ // Case 3: { "data": { "items": [...] } } (ApiResponse)
+ // Case 4: { "data": [...] } (ApiResponse)
+ 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>(dataItems.GetRawText(), _jsonOptions) ?? new();
+ if (data.ValueKind == JsonValueKind.Array)
+ return JsonSerializer.Deserialize>(data.GetRawText(), _jsonOptions) ?? new();
+ }
+
+ return new();
+ }
+
+ ///
+ /// 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.
+ ///
+ private async Task GetObjectFromApiAsync(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(data.GetRawText(), _jsonOptions);
+
+ // Case 2: plain object
+ if (root.ValueKind == JsonValueKind.Object)
+ return JsonSerializer.Deserialize(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> GetShopsAsync()
- { AttachToken(); return await _http.GetFromJsonAsync>("api/bff/shops", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync("api/bff/shops");
public async Task GetShopByIdAsync(Guid shopId)
- { AttachToken(); return await _http.GetFromJsonAsync($"api/bff/shops/{shopId}", _jsonOptions); }
+ => await GetObjectFromApiAsync($"api/bff/shops/{shopId}");
public async Task> GetProductsAsync(Guid shopId)
- { AttachToken(); return await _http.GetFromJsonAsync>($"api/bff/shops/{shopId}/products", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync($"api/bff/shops/{shopId}/products");
public async Task> GetCategoriesAsync(Guid shopId)
- { AttachToken(); return await _http.GetFromJsonAsync>($"api/bff/shops/{shopId}/categories", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync($"api/bff/shops/{shopId}/categories");
public async Task> GetTablesAsync(Guid shopId)
- { AttachToken(); return await _http.GetFromJsonAsync>($"api/bff/shops/{shopId}/tables", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync($"api/bff/shops/{shopId}/tables");
public async Task> GetAppointmentsAsync(Guid shopId)
- { AttachToken(); return await _http.GetFromJsonAsync>($"api/bff/shops/{shopId}/appointments", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync($"api/bff/shops/{shopId}/appointments");
public async Task> GetStaffAsync()
- { AttachToken(); return await _http.GetFromJsonAsync>("api/bff/staff", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync("api/bff/staff");
// ═══ ADMIN-LEVEL PRODUCT/CATEGORY METHODS ═══
@@ -88,16 +150,14 @@ public class PosDataService
public async Task> GetAllProductsAsync(Guid? shopId = null)
{
- AttachToken();
var url = shopId.HasValue ? $"api/bff/products?shopId={shopId}" : "api/bff/products";
- return await _http.GetFromJsonAsync>(url, _jsonOptions) ?? new();
+ return await GetListFromApiAsync(url);
}
public async Task> GetAllCategoriesAsync(Guid? shopId = null)
{
- AttachToken();
var url = shopId.HasValue ? $"api/bff/categories?shopId={shopId}" : "api/bff/categories";
- return await _http.GetFromJsonAsync>(url, _jsonOptions) ?? new();
+ return await GetListFromApiAsync(url);
}
public async Task CreateProductAsync(CreateProductRequest req)
@@ -128,9 +188,8 @@ public class PosDataService
public async Task> GetInventoryAsync(Guid? shopId = null)
{
- AttachToken();
var url = shopId.HasValue ? $"api/bff/inventory?shopId={shopId}" : "api/bff/inventory";
- return await _http.GetFromJsonAsync>(url, _jsonOptions) ?? new();
+ return await GetListFromApiAsync(url);
}
// ═══ MEMBERSHIP/CUSTOMER METHODS ═══
@@ -139,7 +198,7 @@ public class PosDataService
int CurrentLevel, int TotalExpEarned, DateTime CreatedAt, string? LevelName);
public async Task> GetMembersAsync()
- { AttachToken(); return await _http.GetFromJsonAsync>("api/bff/members", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync("api/bff/members");
// ═══ STAFF CREATE ═══
@@ -182,13 +241,12 @@ public class PosDataService
string? EmployeeCode, string? Role, string? Phone);
public async Task> GetStaffRolesAsync()
- { AttachToken(); return await _http.GetFromJsonAsync>("api/bff/staff/roles", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync("api/bff/staff/roles");
public async Task> GetStaffSchedulesAsync(Guid? shopId = null)
{
- AttachToken();
var url = shopId.HasValue ? $"api/bff/staff/schedules?shopId={shopId}" : "api/bff/staff/schedules";
- return await _http.GetFromJsonAsync>(url, _jsonOptions) ?? new();
+ return await GetListFromApiAsync(url);
}
// ═══ ORDERS ═══
@@ -198,11 +256,10 @@ public class PosDataService
public async Task> 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>(url, _jsonOptions) ?? new();
+ return await GetListFromApiAsync(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> GetWalletsAsync()
- { AttachToken(); return await _http.GetFromJsonAsync>("api/bff/wallets", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync("api/bff/wallets");
public async Task> GetWalletTransactionsAsync(int limit = 50)
- { AttachToken(); return await _http.GetFromJsonAsync>($"api/bff/wallet/transactions?limit={limit}", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync($"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> GetDevicesAsync()
- { AttachToken(); return await _http.GetFromJsonAsync>("api/bff/devices", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync("api/bff/devices");
// ═══ PROMOTIONS ═══
@@ -229,7 +286,7 @@ public class PosDataService
bool IsActive, string? DiscountType, decimal? DiscountValue, int VoucherCount, int RedemptionCount);
public async Task> GetPromotionsAsync()
- { AttachToken(); return await _http.GetFromJsonAsync>("api/bff/promotions", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync("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> GetCampaignsAsync()
- { AttachToken(); return await _http.GetFromJsonAsync>("api/bff/campaigns", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync("api/bff/campaigns");
public async Task CreateCampaignAsync(CreateCampaignRequest req)
{
@@ -297,9 +354,8 @@ public class PosDataService
public async Task> GetInventoryTransactionsAsync(Guid? shopId = null)
{
- AttachToken();
var url = shopId.HasValue ? $"api/bff/inventory/transactions?shopId={shopId}" : "api/bff/inventory/transactions";
- return await _http.GetFromJsonAsync>(url, _jsonOptions) ?? new();
+ return await GetListFromApiAsync(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> GetMembershipLevelsAsync()
- { AttachToken(); return await _http.GetFromJsonAsync>("api/bff/membership/levels", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync("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> GetShopStatsAsync()
- { AttachToken(); return await _http.GetFromJsonAsync>("api/bff/shops/stats", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync("api/bff/shops/stats");
// ═══ BOOKING RESOURCES ═══
public record ResourceInfo(Guid Id, string Name, string? ResourceType, int Capacity, bool IsActive);
public async Task> GetResourcesAsync(Guid shopId)
- { AttachToken(); return await _http.GetFromJsonAsync>($"api/bff/shops/{shopId}/resources", _jsonOptions) ?? new(); }
+ => await GetListFromApiAsync($"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 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 CreatePosOrderAsync(CreatePosOrderRequest req)
@@ -434,11 +490,10 @@ public class PosDataService
public async Task> 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>(url, _jsonOptions) ?? new();
+ return await GetListFromApiAsync(url);
}
// ═══ SHOP SETTINGS ═══
@@ -447,10 +502,7 @@ public class PosDataService
public record UpdateShopSettingsRequest(string? FeaturesConfig, string? OpenTime, string? CloseTime, string? OpenDays);
public async Task GetShopSettingsAsync(Guid shopId)
- {
- AttachToken();
- return await _http.GetFromJsonAsync($"api/bff/shops/{shopId}/settings", _jsonOptions);
- }
+ => await GetObjectFromApiAsync($"api/bff/shops/{shopId}/settings");
public async Task UpdateShopSettingsAsync(Guid shopId, UpdateShopSettingsRequest req)
{
@@ -465,11 +517,10 @@ public class PosDataService
public async Task> 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>(url, _jsonOptions) ?? new();
+ return await GetListFromApiAsync(url);
}
// ═══ TABLES CRUD ═══
@@ -531,10 +582,9 @@ public class PosDataService
public async Task> 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>(url, _jsonOptions) ?? new();
+ return await GetListFromApiAsync(url);
}
public async Task UpdateTicketStatusAsync(Guid ticketId, UpdateTicketStatusRequest req)
@@ -549,10 +599,9 @@ public class PosDataService
public async Task> GetRecipesAsync(Guid? shopId = null)
{
- AttachToken();
if (!shopId.HasValue) return new();
var url = $"api/bff/shops/{shopId}/recipes";
- return await _http.GetFromJsonAsync>(url, _jsonOptions) ?? new();
+ return await GetListFromApiAsync(url);
}
public async Task CreateRecipeAsync(CreateRecipeRequest req)
diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BookingController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BookingController.cs
index 5fbb5d36..024a72be 100644
--- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BookingController.cs
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BookingController.cs
@@ -48,8 +48,12 @@ public class BookingController : ControllerBase
/// VI: Hủy lịch hẹn.
///
[HttpDelete("appointments/{apptId:guid}/cancel")]
- public Task CancelAppointment(Guid apptId) =>
- _booking.DeleteAsync($"/api/v1/appointments/{apptId}/cancel").ProxyAsync();
+ public async Task 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();
+ }
///
/// EN: Get resources for a specific shop.
diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs
index 5cf8b1d8..1b1393ff 100644
--- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/OrderController.cs
@@ -30,8 +30,20 @@ public class OrderController : ControllerBase
{
var qs = new List();
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.
///
[HttpPut("orders/{orderId:guid}/cancel")]
- public Task CancelOrder(Guid orderId) =>
- _order.PutAsync($"/api/v1/orders/{orderId}/cancel", null).ProxyAsync();
+ public Task 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();
+ }
///
/// EN: Create a POS order.
diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs
index 99b83519..e60d0b4e 100644
--- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs
@@ -5,18 +5,20 @@ using WebClientTpos.Server.Infrastructure;
namespace WebClientTpos.Server.Controllers;
///
-/// 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.
///
[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");
}
///
@@ -57,40 +59,40 @@ public class StaffController : ControllerBase
///
[HttpGet("staff/roles")]
public Task GetStaffRoles() =>
- _merchant.GetAsync("/api/v1/merchants/me/staff/roles").ProxyAsync();
+ _merchant.GetAsync("/api/v1/staff/roles").ProxyAsync();
///
- /// 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.
///
[HttpGet("staff/schedules")]
public Task 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();
}
///
- /// 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.
///
[HttpPost("staff/schedules")]
public Task CreateSchedule([FromBody] JsonElement body) =>
- _merchant.PostAsJsonAsync("/api/v1/merchants/me/staff/schedules", body).ProxyAsync();
+ _booking.PostAsJsonAsync("/api/v1/schedules", body).ProxyAsync();
///
- /// 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.
///
[HttpPut("staff/schedules/{scheduleId:guid}")]
public Task 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();
///
- /// 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.
///
[HttpDelete("staff/schedules/{scheduleId:guid}")]
public Task DeleteSchedule(Guid scheduleId) =>
- _merchant.DeleteAsync($"/api/v1/merchants/me/staff/schedules/{scheduleId}").ProxyAsync();
+ _booking.DeleteAsync($"/api/v1/schedules/{scheduleId}").ProxyAsync();
}
diff --git a/services/order-service-net/src/OrderService.API/Application/Queries/GetRevenueReportQuery.cs b/services/order-service-net/src/OrderService.API/Application/Queries/GetRevenueReportQuery.cs
index 998be89c..b1b26b3c 100644
--- a/services/order-service-net/src/OrderService.API/Application/Queries/GetRevenueReportQuery.cs
+++ b/services/order-service-net/src/OrderService.API/Application/Queries/GetRevenueReportQuery.cs
@@ -26,20 +26,21 @@ public record RevenueReportDto(
string Period,
Guid ShopId,
decimal TotalRevenue,
- int TotalOrders,
+ long TotalOrders,
List Data
);
///
-/// 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).
///
-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; }
+}
///
/// EN: Handler for GetRevenueReportQuery.
diff --git a/services/order-service-net/src/OrderService.API/Application/Queries/GetTopProductsQuery.cs b/services/order-service-net/src/OrderService.API/Application/Queries/GetTopProductsQuery.cs
index 432186f8..a2a1d955 100644
--- a/services/order-service-net/src/OrderService.API/Application/Queries/GetTopProductsQuery.cs
+++ b/services/order-service-net/src/OrderService.API/Application/Queries/GetTopProductsQuery.cs
@@ -19,16 +19,17 @@ public record GetTopProductsQuery(
) : IRequest>;
///
-/// 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).
///
-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; }
+}
///
/// EN: Handler for GetTopProductsQuery.