258 lines
10 KiB
C#
258 lines
10 KiB
C#
using System.Text.Json;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using WebClientTpos.Server.Infrastructure;
|
|
|
|
namespace WebClientTpos.Server.Controllers;
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Extract userId from JWT Bearer token in the Authorization header.
|
|
/// VI: Trích xuất userId từ JWT Bearer token trong header Authorization.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
[HttpGet("wallets")]
|
|
public async Task<IActionResult> 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" };
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Get wallet transactions for the current user.
|
|
/// VI: Lấy giao dịch ví của user hiện tại.
|
|
/// </summary>
|
|
[HttpGet("wallet/transactions")]
|
|
public async Task<IActionResult> 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" };
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Get promotions for current merchant.
|
|
/// VI: Lấy danh sách khuyến mãi của merchant hiện tại.
|
|
/// </summary>
|
|
[HttpGet("promotions")]
|
|
public Task<IActionResult> GetPromotions() =>
|
|
_promotion.GetAsync("/api/v1/campaigns").ProxyAsync();
|
|
|
|
/// <summary>
|
|
/// EN: Get campaigns for current merchant.
|
|
/// VI: Lấy danh sách chiến dịch của merchant hiện tại.
|
|
/// </summary>
|
|
[HttpGet("campaigns")]
|
|
public Task<IActionResult> GetCampaigns([FromQuery] int pageSize = 100) =>
|
|
_promotion.GetAsync($"/api/v1/admin/campaigns?pageSize={pageSize}").ProxyAsync();
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[HttpPost("campaigns")]
|
|
public async Task<IActionResult> 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<string, string>
|
|
{
|
|
["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"
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Update a campaign.
|
|
/// VI: Cập nhật chiến dịch.
|
|
/// </summary>
|
|
[HttpPut("campaigns/{campaignId:guid}")]
|
|
public Task<IActionResult> UpdateCampaign(Guid campaignId, [FromBody] JsonElement body) =>
|
|
_promotion.PutAsJsonAsync($"/api/v1/campaigns/{campaignId}", body).ProxyAsync();
|
|
|
|
/// <summary>
|
|
/// EN: Disable a campaign (soft-delete).
|
|
/// VI: Vô hiệu hóa chiến dịch (xóa mềm).
|
|
/// </summary>
|
|
[HttpDelete("campaigns/{campaignId:guid}")]
|
|
public Task<IActionResult> DeleteCampaign(Guid campaignId) =>
|
|
_promotion.DeleteAsync($"/api/v1/campaigns/{campaignId}").ProxyAsync();
|
|
|
|
// ═══ VOUCHER ENDPOINTS ═══
|
|
|
|
/// <summary>
|
|
/// EN: Validate a voucher code.
|
|
/// VI: Kiểm tra mã voucher.
|
|
/// </summary>
|
|
[HttpGet("vouchers/validate/{code}")]
|
|
public async Task<IActionResult> ValidateVoucher(string code)
|
|
{
|
|
var userId = GetUserIdFromToken();
|
|
var qs = userId.HasValue ? $"?userId={userId}" : "";
|
|
return await _promotion.GetAsync($"/api/v1/vouchers/validate/{code}{qs}").ProxyAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Redeem a voucher.
|
|
/// VI: Sử dụng voucher.
|
|
/// </summary>
|
|
[HttpPost("vouchers/redeem")]
|
|
public Task<IActionResult> RedeemVoucher([FromBody] JsonElement body) =>
|
|
_promotion.PostAsJsonAsync("/api/v1/vouchers/redeem", body).ProxyAsync();
|
|
|
|
// ═══ ADMIN VOUCHER MANAGEMENT ═══
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
[HttpGet("vouchers/list")]
|
|
public Task<IActionResult> 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<string> { $"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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Revoke a voucher.
|
|
/// VI: Thu hồi voucher.
|
|
/// </summary>
|
|
[HttpPost("vouchers/{voucherId:guid}/revoke")]
|
|
public Task<IActionResult> RevokeVoucher(Guid voucherId) =>
|
|
_promotion.PostAsJsonAsync($"/api/v1/admin/vouchers/{voucherId}/revoke", new { reason = "Revoked by admin" }).ProxyAsync();
|
|
|
|
/// <summary>
|
|
/// EN: Activate a campaign.
|
|
/// VI: Kích hoạt chiến dịch.
|
|
/// </summary>
|
|
[HttpPost("campaigns/{campaignId:guid}/activate")]
|
|
public Task<IActionResult> ActivateCampaign(Guid campaignId) =>
|
|
_promotion.PostAsync($"/api/v1/campaigns/{campaignId}/activate", null).ProxyAsync();
|
|
|
|
/// <summary>
|
|
/// EN: Pause a campaign.
|
|
/// VI: Tạm dừng chiến dịch.
|
|
/// </summary>
|
|
[HttpPost("campaigns/{campaignId:guid}/pause")]
|
|
public Task<IActionResult> PauseCampaign(Guid campaignId) =>
|
|
_promotion.PostAsync($"/api/v1/campaigns/{campaignId}/pause", null).ProxyAsync();
|
|
}
|