using System.Text.Json; using Microsoft.AspNetCore.Mvc; using WebClientTpos.Server.Infrastructure; namespace WebClientTpos.Server.Controllers; /// /// EN: Financial controller — proxies to WalletService (wallets/transactions) and PromotionService (campaigns). /// VI: Controller tài chính — proxy đến WalletService (ví/giao dịch) và PromotionService (chiến dịch). /// [ApiController] [Route("api/bff")] public class FinancialController : ControllerBase { private readonly HttpClient _wallet; private readonly HttpClient _promotion; public FinancialController(IHttpClientFactory httpClientFactory) { _wallet = httpClientFactory.CreateClient("WalletService"); _promotion = httpClientFactory.CreateClient("PromotionService"); } /// /// EN: Extract userId from JWT Bearer token in the Authorization header. /// VI: Trích xuất userId từ JWT Bearer token trong header Authorization. /// private Guid? GetUserIdFromToken() { var authHeader = Request.Headers["Authorization"].FirstOrDefault(); if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ")) return null; var token = authHeader["Bearer ".Length..]; var parts = token.Split('.'); if (parts.Length != 3) return null; var payload = parts[1]; // EN: Fix base64url padding / VI: Sửa padding base64url switch (payload.Length % 4) { case 2: payload += "=="; break; case 3: payload += "="; break; } payload = payload.Replace('-', '+').Replace('_', '/'); try { var json = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(payload)); using var doc = JsonDocument.Parse(json); if (doc.RootElement.TryGetProperty("sub", out var sub) && Guid.TryParse(sub.GetString(), out var userId)) return userId; } catch { /* invalid token format */ } return null; } /// /// EN: Get wallet for the current user (extracted from JWT sub claim). /// VI: Lấy ví của user hiện tại (trích từ JWT sub claim). /// [HttpGet("wallets")] public async Task GetWallets() { var userId = GetUserIdFromToken(); if (userId == null) return Unauthorized(new { message = "Cannot extract user ID from token" }); // EN: WalletService returns single wallet; wrap in array for frontend compatibility. // VI: WalletService trả về 1 ví; bọc trong array cho tương thích frontend. var response = await _wallet.GetAsync($"/api/v1/wallets/{userId}"); var content = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { // EN: If 404, return empty array (user has no wallet yet). // VI: Nếu 404, trả array rỗng (user chưa có ví). if (response.StatusCode == System.Net.HttpStatusCode.NotFound) return new ContentResult { StatusCode = 200, Content = "[]", ContentType = "application/json" }; return new ContentResult { StatusCode = (int)response.StatusCode, Content = content, ContentType = "application/json" }; } // EN: Extract wallet data from ApiResponse envelope and wrap in array. // VI: Trích dữ liệu ví từ ApiResponse envelope và bọc trong array. try { using var doc = JsonDocument.Parse(content); if (doc.RootElement.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.Object) return new ContentResult { StatusCode = 200, Content = $"[{data.GetRawText()}]", ContentType = "application/json" }; } catch { /* fallback */ } return new ContentResult { StatusCode = 200, Content = $"[{content}]", ContentType = "application/json" }; } /// /// EN: Get wallet transactions for the current user. /// VI: Lấy giao dịch ví của user hiện tại. /// [HttpGet("wallet/transactions")] public async Task GetWalletTransactions([FromQuery] int limit = 50) { var userId = GetUserIdFromToken(); if (userId == null) return Unauthorized(new { message = "Cannot extract user ID from token" }); var response = await _wallet.GetAsync($"/api/v1/wallets/{userId}/transactions?limit={limit}"); // EN: If wallet not found, return empty array (user has no wallet yet). // VI: Nếu ví không tồn tại, trả array rỗng (user chưa có ví). if (!response.IsSuccessStatusCode) return new ContentResult { StatusCode = 200, Content = "[]", ContentType = "application/json" }; var content = await response.Content.ReadAsStringAsync(); return new ContentResult { StatusCode = 200, Content = content, ContentType = "application/json" }; } /// /// EN: Get promotions for current merchant. /// VI: Lấy danh sách khuyến mãi của merchant hiện tại. /// [HttpGet("promotions")] public Task GetPromotions() => _promotion.GetAsync("/api/v1/campaigns").ProxyAsync(); /// /// EN: Get campaigns for current merchant. /// VI: Lấy danh sách chiến dịch của merchant hiện tại. /// [HttpGet("campaigns")] public Task GetCampaigns([FromQuery] int pageSize = 100) => _promotion.GetAsync($"/api/v1/admin/campaigns?pageSize={pageSize}").ProxyAsync(); /// /// EN: Create a campaign. Enriches with default backing asset and acquisition fields. /// VI: Tạo chiến dịch. Bổ sung thông tin backing asset và acquisition mặc định. /// [HttpPost("campaigns")] public async Task CreateCampaign([FromBody] JsonElement body) { // EN: Enrich with required fields using raw JSON manipulation // VI: Bổ sung các trường bắt buộc bằng thao tác JSON trực tiếp var rawJson = body.GetRawText(); var defaults = new Dictionary { ["backingAssetType"] = "\"currency\"", ["backingAssetCode"] = "\"VND\"", ["acquisitionType"] = "\"free\"", ["acquisitionPrice"] = "0", ["merchantId"] = $"\"{Guid.Empty}\"", ["merchantWalletId"] = $"\"{Guid.Empty}\"" }; foreach (var (key, val) in defaults) { if (!rawJson.Contains($"\"{key}\"")) rawJson = rawJson.TrimEnd('}') + $",\"{key}\":{val}}}"; } var content = new StringContent(rawJson, System.Text.Encoding.UTF8, "application/json"); var resp = await _promotion.PostAsync("/api/v1/campaigns", content); var respContent = await resp.Content.ReadAsStringAsync(); return new ContentResult { StatusCode = (int)resp.StatusCode, Content = respContent, ContentType = resp.Content.Headers.ContentType?.ToString() ?? "application/json" }; } /// /// EN: Update a campaign. /// VI: Cập nhật chiến dịch. /// [HttpPut("campaigns/{campaignId:guid}")] public Task UpdateCampaign(Guid campaignId, [FromBody] JsonElement body) => _promotion.PutAsJsonAsync($"/api/v1/campaigns/{campaignId}", body).ProxyAsync(); /// /// EN: Disable a campaign (soft-delete). /// VI: Vô hiệu hóa chiến dịch (xóa mềm). /// [HttpDelete("campaigns/{campaignId:guid}")] public Task DeleteCampaign(Guid campaignId) => _promotion.DeleteAsync($"/api/v1/campaigns/{campaignId}").ProxyAsync(); // ═══ VOUCHER ENDPOINTS ═══ /// /// EN: Validate a voucher code. /// VI: Kiểm tra mã voucher. /// [HttpGet("vouchers/validate/{code}")] public async Task ValidateVoucher(string code) { var userId = GetUserIdFromToken(); var qs = userId.HasValue ? $"?userId={userId}" : ""; return await _promotion.GetAsync($"/api/v1/vouchers/validate/{code}{qs}").ProxyAsync(); } /// /// EN: Redeem a voucher. /// VI: Sử dụng voucher. /// [HttpPost("vouchers/redeem")] public Task RedeemVoucher([FromBody] JsonElement body) => _promotion.PostAsJsonAsync("/api/v1/vouchers/redeem", body).ProxyAsync(); // ═══ ADMIN VOUCHER MANAGEMENT ═══ /// /// EN: List vouchers with optional filters (campaignId, status, codeSearch). /// VI: Liệt kê voucher với bộ lọc tùy chọn (campaignId, status, codeSearch). /// [HttpGet("vouchers/list")] public Task GetAdminVouchers( [FromQuery] Guid? campaignId = null, [FromQuery] string? status = null, [FromQuery] string? codeSearch = null, [FromQuery] int pageSize = 50, [FromQuery] int page = 1) { var qs = new List { $"pageSize={pageSize}", $"pageNumber={page}" }; if (campaignId.HasValue) qs.Add($"campaignId={campaignId}"); if (!string.IsNullOrEmpty(status)) qs.Add($"status={status}"); if (!string.IsNullOrEmpty(codeSearch)) qs.Add($"codeSearch={Uri.EscapeDataString(codeSearch)}"); return _promotion.GetAsync($"/api/v1/admin/vouchers?{string.Join("&", qs)}").ProxyAsync(); } /// /// EN: Revoke a voucher. /// VI: Thu hồi voucher. /// [HttpPost("vouchers/{voucherId:guid}/revoke")] public Task RevokeVoucher(Guid voucherId) => _promotion.PostAsJsonAsync($"/api/v1/admin/vouchers/{voucherId}/revoke", new { reason = "Revoked by admin" }).ProxyAsync(); /// /// EN: Activate a campaign. /// VI: Kích hoạt chiến dịch. /// [HttpPost("campaigns/{campaignId:guid}/activate")] public Task ActivateCampaign(Guid campaignId) => _promotion.PostAsync($"/api/v1/campaigns/{campaignId}/activate", null).ProxyAsync(); /// /// EN: Pause a campaign. /// VI: Tạm dừng chiến dịch. /// [HttpPost("campaigns/{campaignId:guid}/pause")] public Task PauseCampaign(Guid campaignId) => _promotion.PostAsync($"/api/v1/campaigns/{campaignId}/pause", null).ProxyAsync(); }