- Extract shared infrastructure: BffDbConnectionFactory, TenantContext (per-request cache) - Extract 19 DTOs to Models/BffDtos.cs - Create 10 controllers: Shop, Catalog, Staff, Order, Inventory, Financial, Booking, Fnb, Reports, Membership - Register DI services in Program.cs - Delete monolithic BffDataController.cs (1831 lines) - All API routes preserved under api/bff prefix (zero breaking changes)
260 lines
11 KiB
C#
260 lines
11 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using Dapper;
|
|
using WebClientTpos.Server.Infrastructure;
|
|
using WebClientTpos.Server.Models;
|
|
|
|
namespace WebClientTpos.Server.Controllers;
|
|
|
|
/// <summary>
|
|
/// EN: Shop management controller — CRUD for shops, settings, stats, and devices.
|
|
/// All endpoints are scoped to the current merchant's shops.
|
|
/// VI: Controller quản lý cửa hàng — CRUD cho shops, settings, stats và devices.
|
|
/// Tất cả endpoints đều lọc theo shops của merchant hiện tại.
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/bff")]
|
|
public class ShopController : ControllerBase
|
|
{
|
|
private readonly TenantContext _tenant;
|
|
private readonly BffDbConnectionFactory _dbFactory;
|
|
|
|
public ShopController(TenantContext tenant, BffDbConnectionFactory dbFactory)
|
|
{
|
|
_tenant = tenant;
|
|
_dbFactory = dbFactory;
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Get all shops belonging to the current merchant.
|
|
/// VI: Lấy tất cả cửa hàng thuộc merchant hiện tại.
|
|
/// </summary>
|
|
[HttpGet("shops")]
|
|
public async Task<IActionResult> GetShops()
|
|
{
|
|
var merchantId = await _tenant.GetMerchantIdAsync();
|
|
if (merchantId == null)
|
|
return Ok(Array.Empty<object>());
|
|
|
|
await using var conn = _dbFactory.CreateConnection("merchant_service");
|
|
var shops = await conn.QueryAsync<dynamic>(
|
|
@"SELECT s.id, s.name, s.slug, s.description, s.phone, s.email,
|
|
s.open_time, s.close_time, s.features_config,
|
|
bc.name as category, st.name as status
|
|
FROM shops s
|
|
JOIN business_categories bc ON s.category_id = bc.id
|
|
JOIN shop_statuses st ON s.status_id = st.id
|
|
WHERE s.merchant_id = @MerchantId AND s.is_deleted = false
|
|
ORDER BY s.name",
|
|
new { MerchantId = merchantId });
|
|
return Ok(shops);
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Get shop by ID — validates merchant ownership.
|
|
/// VI: Lấy cửa hàng theo ID — kiểm tra quyền sở hữu merchant.
|
|
/// </summary>
|
|
[HttpGet("shops/{shopId:guid}")]
|
|
public async Task<IActionResult> GetShopById(Guid shopId)
|
|
{
|
|
var merchantId = await _tenant.GetMerchantIdAsync();
|
|
if (merchantId == null)
|
|
return NotFound(new { message = "Shop not found" });
|
|
|
|
await using var conn = _dbFactory.CreateConnection("merchant_service");
|
|
var shop = await conn.QueryFirstOrDefaultAsync<dynamic>(
|
|
@"SELECT s.id, s.name, s.slug, s.description, s.phone, s.email,
|
|
s.open_time, s.close_time,
|
|
bc.name as category, st.name as status
|
|
FROM shops s
|
|
JOIN business_categories bc ON s.category_id = bc.id
|
|
JOIN shop_statuses st ON s.status_id = st.id
|
|
WHERE s.id = @ShopId AND s.merchant_id = @MerchantId AND s.is_deleted = false",
|
|
new { ShopId = shopId, MerchantId = merchantId });
|
|
|
|
if (shop == null)
|
|
return NotFound(new { message = "Shop not found" });
|
|
|
|
return Ok(shop);
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Update shop info — validates ownership.
|
|
/// VI: Cập nhật thông tin cửa hàng — kiểm tra quyền sở hữu.
|
|
/// </summary>
|
|
[HttpPut("shops/{shopId:guid}")]
|
|
public async Task<IActionResult> UpdateShop(Guid shopId, [FromBody] UpdateShopRequest req)
|
|
{
|
|
var merchantId = await _tenant.GetMerchantIdAsync();
|
|
if (merchantId == null) return Unauthorized();
|
|
|
|
var myShopIds = await _tenant.GetShopIdsAsync();
|
|
if (!myShopIds.Contains(shopId))
|
|
return Forbid();
|
|
|
|
await using var conn = _dbFactory.CreateConnection("merchant_service");
|
|
var rows = await conn.ExecuteAsync(
|
|
@"UPDATE shops SET name=@Name, phone=@Phone, email=@Email, description=@Description,
|
|
open_time=@OpenTime, close_time=@CloseTime, updated_at=NOW()
|
|
WHERE id=@ShopId AND id = ANY(@ShopIds)",
|
|
new { req.Name, req.Phone, req.Email, req.Description,
|
|
req.OpenTime, req.CloseTime, ShopId = shopId, ShopIds = myShopIds.ToArray() });
|
|
return rows > 0 ? Ok(new { id = shopId }) : NotFound();
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Get shop settings (features_config, open/close time, open days).
|
|
/// VI: Lấy cài đặt cửa hàng (features_config, giờ mở/đóng cửa, ngày mở cửa).
|
|
/// </summary>
|
|
[HttpGet("shops/{shopId:guid}/settings")]
|
|
public async Task<IActionResult> GetShopSettings(Guid shopId)
|
|
{
|
|
var merchantId = await _tenant.GetMerchantIdAsync();
|
|
if (merchantId == null) return NotFound();
|
|
|
|
await using var conn = _dbFactory.CreateConnection("merchant_service");
|
|
var settings = await conn.QueryFirstOrDefaultAsync<dynamic>(
|
|
@"SELECT features_config::text as features_config,
|
|
open_time::text as open_time,
|
|
close_time::text as close_time,
|
|
open_days
|
|
FROM shops
|
|
WHERE id = @ShopId AND merchant_id = @MerchantId AND is_deleted = false",
|
|
new { ShopId = shopId, MerchantId = merchantId });
|
|
|
|
if (settings == null) return NotFound();
|
|
return Ok(settings);
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Update shop settings — validates ownership.
|
|
/// VI: Cập nhật cài đặt cửa hàng — kiểm tra quyền sở hữu.
|
|
/// </summary>
|
|
[HttpPut("shops/{shopId:guid}/settings")]
|
|
public async Task<IActionResult> UpdateShopSettings(Guid shopId, [FromBody] UpdateShopSettingsRequest req)
|
|
{
|
|
var merchantId = await _tenant.GetMerchantIdAsync();
|
|
if (merchantId == null) return Unauthorized();
|
|
|
|
var myShopIds = await _tenant.GetShopIdsAsync();
|
|
if (!myShopIds.Contains(shopId)) return Forbid();
|
|
|
|
await using var conn = _dbFactory.CreateConnection("merchant_service");
|
|
try
|
|
{
|
|
await conn.ExecuteAsync(
|
|
@"UPDATE shops
|
|
SET features_config = @FeaturesConfig::jsonb,
|
|
open_time = @OpenTime::time,
|
|
close_time = @CloseTime::time,
|
|
open_days = @OpenDays,
|
|
updated_at = NOW()
|
|
WHERE id = @ShopId AND id = ANY(@ShopIds)",
|
|
new
|
|
{
|
|
ShopId = shopId,
|
|
ShopIds = myShopIds.ToArray(),
|
|
FeaturesConfig = string.IsNullOrWhiteSpace(req.FeaturesConfig) ? "{}" : req.FeaturesConfig,
|
|
OpenTime = string.IsNullOrWhiteSpace(req.OpenTime) ? (object)DBNull.Value : req.OpenTime,
|
|
CloseTime= string.IsNullOrWhiteSpace(req.CloseTime) ? (object)DBNull.Value : req.CloseTime,
|
|
OpenDays = req.OpenDays
|
|
});
|
|
return Ok(new { success = true });
|
|
}
|
|
catch (Exception ex) { return BadRequest(new { error = ex.Message }); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Get aggregated stats per shop — scoped to current merchant.
|
|
/// VI: Lấy thống kê tổng hợp theo shop — lọc theo merchant hiện tại.
|
|
/// </summary>
|
|
[HttpGet("shops/stats")]
|
|
public async Task<IActionResult> GetShopStats()
|
|
{
|
|
var merchantId = await _tenant.GetMerchantIdAsync();
|
|
if (merchantId == null)
|
|
return Ok(Array.Empty<object>());
|
|
|
|
var myShopIds = await _tenant.GetShopIdsAsync();
|
|
if (!myShopIds.Any())
|
|
return Ok(Array.Empty<object>());
|
|
|
|
var shopIdsArray = myShopIds.ToArray();
|
|
|
|
// EN: Products per shop (scoped) / VI: Số sản phẩm mỗi shop
|
|
Dictionary<Guid, int> productCounts = new();
|
|
try
|
|
{
|
|
await using var catConn = _dbFactory.CreateConnection("catalog_service");
|
|
var prodStats = await catConn.QueryAsync<dynamic>(
|
|
"SELECT shop_id, COUNT(*) as cnt FROM products WHERE is_active = true AND shop_id = ANY(@ShopIds) GROUP BY shop_id",
|
|
new { ShopIds = shopIdsArray });
|
|
foreach (var ps in prodStats)
|
|
productCounts[(Guid)ps.shop_id] = (int)(long)ps.cnt;
|
|
}
|
|
catch { /* catalog_service may not have data yet */ }
|
|
|
|
// EN: Orders per shop + revenue (scoped) / VI: Số đơn + doanh thu mỗi shop
|
|
Dictionary<Guid, int> orderCounts = new();
|
|
Dictionary<Guid, decimal> revenues = new();
|
|
try
|
|
{
|
|
await using var orderConn = _dbFactory.CreateConnection("order_service");
|
|
var orderStats = await orderConn.QueryAsync<dynamic>(
|
|
"SELECT shop_id, COUNT(*) as cnt, COALESCE(SUM(total_amount), 0) as revenue FROM orders WHERE shop_id = ANY(@ShopIds) GROUP BY shop_id",
|
|
new { ShopIds = shopIdsArray });
|
|
foreach (var os in orderStats)
|
|
{
|
|
orderCounts[(Guid)os.shop_id] = (int)(long)os.cnt;
|
|
revenues[(Guid)os.shop_id] = (decimal)os.revenue;
|
|
}
|
|
}
|
|
catch { /* order_service may not have data yet */ }
|
|
|
|
// EN: Staff per shop (scoped) / VI: Số nhân viên mỗi shop
|
|
Dictionary<Guid, int> staffCounts = new();
|
|
try
|
|
{
|
|
await using var mConn = _dbFactory.CreateConnection("merchant_service");
|
|
var staffStats = await mConn.QueryAsync<dynamic>(
|
|
@"SELECT sm.shop_id, COUNT(DISTINCT sm.staff_id) as cnt
|
|
FROM shop_members sm
|
|
JOIN merchant_staff ms ON sm.staff_id = ms.id
|
|
JOIN staff_statuses ss ON ms.status_id = ss.id
|
|
WHERE ss.name = 'Active' AND sm.shop_id = ANY(@ShopIds)
|
|
GROUP BY sm.shop_id",
|
|
new { ShopIds = shopIdsArray });
|
|
foreach (var ss in staffStats)
|
|
staffCounts[(Guid)ss.shop_id] = (int)(long)ss.cnt;
|
|
}
|
|
catch { /* merchant_service may not have data yet */ }
|
|
|
|
var result = myShopIds.Select(shopId => new
|
|
{
|
|
shop_id = shopId,
|
|
product_count = productCounts.GetValueOrDefault(shopId, 0),
|
|
order_count = orderCounts.GetValueOrDefault(shopId, 0),
|
|
staff_count = staffCounts.GetValueOrDefault(shopId, 0),
|
|
revenue = revenues.GetValueOrDefault(shopId, 0m)
|
|
});
|
|
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Get device tokens registered for this merchant's staff.
|
|
/// VI: Lấy danh sách device token đã đăng ký cho nhân viên của merchant.
|
|
/// </summary>
|
|
[HttpGet("devices")]
|
|
public async Task<IActionResult> GetDevices()
|
|
{
|
|
await using var conn = _dbFactory.CreateConnection("merchant_service");
|
|
var devices = await conn.QueryAsync<dynamic>(
|
|
@"SELECT dt.id, dt.device_token, dt.platform, dt.is_active, dt.created_at,
|
|
ms.employee_code as staff_code
|
|
FROM device_tokens dt
|
|
LEFT JOIN merchant_staff ms ON dt.staff_id = ms.id
|
|
ORDER BY dt.created_at DESC");
|
|
return Ok(devices);
|
|
}
|
|
}
|