// EN: BFF Super Admin Controller — aggregates data from microservices for platform management. // VI: BFF Super Admin Controller — tổng hợp dữ liệu từ microservices cho quản lý nền tảng. using Microsoft.AspNetCore.Mvc; using System.Net.Http.Headers; using System.Text.Json; namespace WebClientTpos.Server.Controllers; /// /// EN: BFF endpoints for Super Admin panel — proxies to IAM + Merchant services. /// VI: BFF endpoints cho trang Super Admin — proxy đến IAM + Merchant services. /// [ApiController] [Route("api/bff/superadmin")] public class SuperAdminController : ControllerBase { private readonly IHttpClientFactory _httpFactory; private readonly ILogger _logger; private static readonly JsonSerializerOptions _json = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; public SuperAdminController(IHttpClientFactory httpFactory, ILogger logger) { _httpFactory = httpFactory; _logger = logger; } // ═══════════════════════════════════════════════ // ─── PLATFORM STATS (Dashboard) ─── // ═══════════════════════════════════════════════ /// /// EN: Get aggregated platform statistics from multiple services. /// VI: Lấy thống kê nền tảng tổng hợp từ nhiều services. /// [HttpGet("stats")] public async Task GetPlatformStats() { try { var merchantClient = CreateAuthClient("MerchantService"); var iamClient = CreateAuthClient("IamService"); // EN: Fetch data from services in parallel // VI: Lấy dữ liệu từ services song song var merchantStatsTask = SafeGetJson(merchantClient, "/api/v1/admin/merchants/statistics"); var usersTask = SafeGetJson(iamClient, "/api/v1/users?pageNumber=1&pageSize=1"); await Task.WhenAll(merchantStatsTask, usersTask); var merchantStats = merchantStatsTask.Result; var usersData = usersTask.Result; // EN: Extract values from merchant statistics // VI: Trích xuất giá trị từ thống kê merchants int totalMerchants = 0, activeMerchants = 0, pendingMerchants = 0, suspendedMerchants = 0; int totalShops = 0, activeShops = 0; if (merchantStats != null) { var data = GetDataProperty(merchantStats.Value); totalMerchants = GetInt(data, "totalMerchants"); activeMerchants = GetInt(data, "activeMerchants", GetInt(data, "active")); pendingMerchants = GetInt(data, "pendingMerchants", GetInt(data, "pending", GetInt(data, "pendingApproval"))); suspendedMerchants = GetInt(data, "suspendedMerchants", GetInt(data, "suspended")); totalShops = GetInt(data, "totalShops"); activeShops = GetInt(data, "activeShops"); } // EN: Extract total users from pagination // VI: Trích xuất tổng users từ pagination int totalUsers = 0; if (usersData != null) { if (usersData.Value.TryGetProperty("pagination", out var pg) && pg.TryGetProperty("totalCount", out var tc)) totalUsers = tc.GetInt32(); } return Ok(new { success = true, data = new { totalMerchants, activeMerchants, pendingMerchants, suspendedMerchants, totalShops, activeShops, totalUsers, newUsersToday = 0, totalOrders = 0, ordersToday = 0, gmvTotal = 0m, gmvToday = 0m } }); } catch (Exception ex) { _logger.LogError(ex, "Failed to fetch platform stats"); return Ok(new { success = true, data = new { totalMerchants = 0, activeMerchants = 0, pendingMerchants = 0, suspendedMerchants = 0, totalShops = 0, activeShops = 0, totalUsers = 0, newUsersToday = 0, totalOrders = 0, ordersToday = 0, gmvTotal = 0m, gmvToday = 0m } }); } } // ═══════════════════════════════════════════════ // ─── MERCHANTS ─── // ═══════════════════════════════════════════════ [HttpGet("merchants")] public async Task GetMerchants( [FromQuery] int page = 1, [FromQuery] int pageSize = 20, [FromQuery] string? status = null, [FromQuery] string? search = null) { var client = CreateAuthClient("MerchantService"); var url = $"/api/v1/admin/merchants?pageNumber={page}&pageSize={pageSize}"; if (!string.IsNullOrEmpty(status)) url += $"&status={status}"; if (!string.IsNullOrEmpty(search)) url += $"&search={Uri.EscapeDataString(search)}"; return await ProxyGet(client, url); } [HttpGet("merchants/{id}")] public async Task GetMerchantDetail(Guid id) { var client = CreateAuthClient("MerchantService"); return await ProxyGet(client, $"/api/v1/admin/merchants/{id}"); } [HttpPost("merchants/{id}/approve")] public async Task ApproveMerchant(Guid id) { var client = CreateAuthClient("MerchantService"); return await ProxyPost(client, $"/api/v1/admin/merchants/{id}/approve"); } [HttpPost("merchants/{id}/suspend")] public async Task SuspendMerchant(Guid id, [FromBody] JsonElement body) { var client = CreateAuthClient("MerchantService"); return await ProxyPostWithBody(client, $"/api/v1/admin/merchants/{id}/suspend", body); } [HttpPost("merchants/{id}/reactivate")] public async Task ReactivateMerchant(Guid id) { var client = CreateAuthClient("MerchantService"); return await ProxyPost(client, $"/api/v1/admin/merchants/{id}/reactivate"); } // ═══════════════════════════════════════════════ // ─── SUBSCRIPTION PLANS (in-memory for MVP) ─── // ═══════════════════════════════════════════════ private static readonly List _plans = new() { new { id = Guid.Parse("00000000-0000-0000-0000-000000000001"), name = "Starter", slug = "starter", description = "Gói miễn phí cho doanh nghiệp mới bắt đầu", priceMonthly = 0m, priceYearly = 0m, maxShops = 1, maxStaff = 5, maxProducts = 100, isActive = true, merchantCount = 0, sortOrder = 0 }, new { id = Guid.Parse("00000000-0000-0000-0000-000000000002"), name = "Growth", slug = "growth", description = "Gói tăng trưởng cho doanh nghiệp đang mở rộng", priceMonthly = 299000m, priceYearly = 2990000m, maxShops = 3, maxStaff = 15, maxProducts = 500, isActive = true, merchantCount = 0, sortOrder = 1 }, new { id = Guid.Parse("00000000-0000-0000-0000-000000000003"), name = "Pro", slug = "pro", description = "Gói chuyên nghiệp với đầy đủ tính năng", priceMonthly = 799000m, priceYearly = 7990000m, maxShops = 10, maxStaff = 50, maxProducts = 2000, isActive = true, merchantCount = 0, sortOrder = 2 }, new { id = Guid.Parse("00000000-0000-0000-0000-000000000004"), name = "Enterprise", slug = "enterprise", description = "Gói doanh nghiệp lớn — tùy chỉnh theo nhu cầu", priceMonthly = 0m, priceYearly = 0m, maxShops = 999, maxStaff = 999, maxProducts = 99999, isActive = true, merchantCount = 0, sortOrder = 3 }, }; [HttpGet("plans")] public IActionResult GetPlans() => Ok(new { success = true, data = _plans }); [HttpPost("plans")] public IActionResult CreatePlan([FromBody] JsonElement body) => Ok(new { success = true, data = body }); [HttpPut("plans/{id}")] public IActionResult UpdatePlan(Guid id, [FromBody] JsonElement body) => Ok(new { success = true, data = body }); // ═══════════════════════════════════════════════ // ─── SYSTEM HEALTH ─── // ═══════════════════════════════════════════════ [HttpGet("system/health")] public async Task GetSystemHealth() { var services = new[] { ("IamService", "IAM Service"), ("MerchantService", "Merchant Service"), ("CatalogService", "Catalog Service"), ("OrderService", "Order Service"), ("InventoryService", "Inventory Service"), ("WalletService", "Wallet Service"), ("MembershipService", "Membership Service"), ("PromotionService", "Promotion Service"), ("BookingService", "Booking Service"), ("FnbEngine", "F&B Engine"), ("StorageService", "Storage Service"), }; var healthTasks = services.Select(async s => { var (clientName, displayName) = s; try { var client = _httpFactory.CreateClient(clientName); var sw = System.Diagnostics.Stopwatch.StartNew(); var response = await client.GetAsync("/health", new System.Threading.CancellationTokenSource(3000).Token); sw.Stop(); return new { name = displayName, status = response.IsSuccessStatusCode ? "Healthy" : "Degraded", responseTimeMs = (int)sw.ElapsedMilliseconds, lastChecked = DateTime.UtcNow }; } catch { return new { name = displayName, status = "Unhealthy", responseTimeMs = -1, lastChecked = DateTime.UtcNow }; } }); var results = await Task.WhenAll(healthTasks); return Ok(new { success = true, data = results }); } // ═══════════════════════════════════════════════ // ─── FEATURE FLAGS (in-memory for MVP) ─── // ═══════════════════════════════════════════════ private static readonly List> _featureFlags = new() { new() { ["key"] = "ai_assistant", ["description"] = "AI Chat Assistant cho quản lý cửa hàng", ["isEnabled"] = true, ["rolloutPercentage"] = 100, ["updatedAt"] = DateTime.UtcNow, ["updatedBy"] = "system" }, new() { ["key"] = "multi_language", ["description"] = "Hỗ trợ đa ngôn ngữ (EN/VI)", ["isEnabled"] = true, ["rolloutPercentage"] = 100, ["updatedAt"] = DateTime.UtcNow, ["updatedBy"] = "system" }, new() { ["key"] = "loyalty_program", ["description"] = "Chương trình tích điểm thành viên", ["isEnabled"] = true, ["rolloutPercentage"] = 100, ["updatedAt"] = DateTime.UtcNow, ["updatedBy"] = "system" }, new() { ["key"] = "advanced_analytics", ["description"] = "Phân tích nâng cao với AI insights", ["isEnabled"] = false, ["rolloutPercentage"] = 0, ["updatedAt"] = DateTime.UtcNow, ["updatedBy"] = "system" }, new() { ["key"] = "booking_system", ["description"] = "Hệ thống đặt lịch cho Spa/Beauty", ["isEnabled"] = true, ["rolloutPercentage"] = 80, ["updatedAt"] = DateTime.UtcNow, ["updatedBy"] = "system" }, new() { ["key"] = "kitchen_display", ["description"] = "Kitchen Display System (KDS)", ["isEnabled"] = false, ["rolloutPercentage"] = 0, ["updatedAt"] = DateTime.UtcNow, ["updatedBy"] = "system" }, }; [HttpGet("feature-flags")] public IActionResult GetFeatureFlags() => Ok(new { success = true, data = _featureFlags }); [HttpPut("feature-flags/{key}")] public IActionResult UpdateFeatureFlag(string key, [FromBody] JsonElement body) { var flag = _featureFlags.FirstOrDefault(f => f["key"].ToString() == key); if (flag == null) return NotFound(new { success = false, error = new { message = "Feature flag not found" } }); if (body.TryGetProperty("isEnabled", out var enabled)) { flag["isEnabled"] = enabled.GetBoolean(); flag["updatedAt"] = DateTime.UtcNow; } return Ok(new { success = true, data = flag }); } // ═══════════════════════════════════════════════ // ─── PROXY HELPERS ─── // ═══════════════════════════════════════════════ private HttpClient CreateAuthClient(string name) { var client = _httpFactory.CreateClient(name); if (Request.Cookies.TryGetValue("bff_session", out var token) && !string.IsNullOrEmpty(token)) client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return client; } private static async Task SafeGetJson(HttpClient client, string url) { try { var response = await client.GetAsync(url); if (!response.IsSuccessStatusCode) return null; return await response.Content.ReadFromJsonAsync(_json); } catch { return null; } } private static JsonElement GetDataProperty(JsonElement json) { return json.TryGetProperty("data", out var data) ? data : json; } private static int GetInt(JsonElement el, string prop, int fallback = 0) { if (el.ValueKind != JsonValueKind.Object) return fallback; if (el.TryGetProperty(prop, out var val) && val.ValueKind == JsonValueKind.Number) return val.GetInt32(); return fallback; } private async Task ProxyGet(HttpClient client, string url) { try { var response = await client.GetAsync(url); var content = await response.Content.ReadAsStringAsync(); return Content(content, "application/json"); } catch (Exception ex) { _logger.LogError(ex, "Proxy GET failed: {Url}", url); return StatusCode(502, new { success = false, error = new { message = "Service unavailable" } }); } } private async Task ProxyPost(HttpClient client, string url) { try { var response = await client.PostAsync(url, new StringContent("{}", System.Text.Encoding.UTF8, "application/json")); var content = await response.Content.ReadAsStringAsync(); return Content(content, "application/json"); } catch (Exception ex) { _logger.LogError(ex, "Proxy POST failed: {Url}", url); return StatusCode(502, new { success = false, error = new { message = "Service unavailable" } }); } } private async Task ProxyPostWithBody(HttpClient client, string url, JsonElement body) { try { var json = JsonSerializer.Serialize(body, _json); var response = await client.PostAsync(url, new StringContent(json, System.Text.Encoding.UTF8, "application/json")); var content = await response.Content.ReadAsStringAsync(); return Content(content, "application/json"); } catch (Exception ex) { _logger.LogError(ex, "Proxy POST with body failed: {Url}", url); return StatusCode(502, new { success = false, error = new { message = "Service unavailable" } }); } } }