using Microsoft.AspNetCore.Mvc;
using Dapper;
using WebClientTpos.Server.Infrastructure;
using WebClientTpos.Server.Models;
namespace WebClientTpos.Server.Controllers;
///
/// 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.
///
[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;
}
///
/// 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.
///
[HttpGet("shops")]
public async Task GetShops()
{
var merchantId = await _tenant.GetMerchantIdAsync();
if (merchantId == null)
return Ok(Array.Empty());
await using var conn = _dbFactory.CreateConnection("merchant_service");
var shops = await conn.QueryAsync(
@"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);
}
///
/// 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.
///
[HttpGet("shops/{shopId:guid}")]
public async Task 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(
@"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);
}
///
/// 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.
///
[HttpPut("shops/{shopId:guid}")]
public async Task 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();
}
///
/// 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).
///
[HttpGet("shops/{shopId:guid}/settings")]
public async Task 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(
@"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);
}
///
/// 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.
///
[HttpPut("shops/{shopId:guid}/settings")]
public async Task 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 }); }
}
///
/// 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.
///
[HttpGet("shops/stats")]
public async Task GetShopStats()
{
var merchantId = await _tenant.GetMerchantIdAsync();
if (merchantId == null)
return Ok(Array.Empty());
var myShopIds = await _tenant.GetShopIdsAsync();
if (!myShopIds.Any())
return Ok(Array.Empty());
var shopIdsArray = myShopIds.ToArray();
// EN: Products per shop (scoped) / VI: Số sản phẩm mỗi shop
Dictionary productCounts = new();
try
{
await using var catConn = _dbFactory.CreateConnection("catalog_service");
var prodStats = await catConn.QueryAsync(
"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 orderCounts = new();
Dictionary revenues = new();
try
{
await using var orderConn = _dbFactory.CreateConnection("order_service");
var orderStats = await orderConn.QueryAsync(
"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 staffCounts = new();
try
{
await using var mConn = _dbFactory.CreateConnection("merchant_service");
var staffStats = await mConn.QueryAsync(
@"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);
}
///
/// 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.
///
[HttpGet("devices")]
public async Task GetDevices()
{
await using var conn = _dbFactory.CreateConnection("merchant_service");
var devices = await conn.QueryAsync(
@"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);
}
}