2026-05-27 21:33:48 +03:30
|
|
|
using Meezi.Infrastructure.Services.Platform;
|
|
|
|
|
using Meezi.Core.Enums;
|
|
|
|
|
using Meezi.Core.Interfaces;
|
|
|
|
|
using Meezi.Infrastructure.Data;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using StackExchange.Redis;
|
|
|
|
|
|
|
|
|
|
namespace Meezi.API.Services;
|
|
|
|
|
|
|
|
|
|
public interface IPlanLimitChecker
|
|
|
|
|
{
|
|
|
|
|
Task<(bool Allowed, string? ErrorCode, string? Message)> CheckAsync(
|
|
|
|
|
HttpContext context,
|
|
|
|
|
ITenantContext tenant,
|
|
|
|
|
CancellationToken cancellationToken = default);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class PlanLimitChecker : IPlanLimitChecker
|
|
|
|
|
{
|
|
|
|
|
private readonly AppDbContext _db;
|
|
|
|
|
private readonly IConnectionMultiplexer _redis;
|
|
|
|
|
private readonly IPlatformCatalogService _platformCatalog;
|
|
|
|
|
|
|
|
|
|
public PlanLimitChecker(
|
|
|
|
|
AppDbContext db,
|
|
|
|
|
IConnectionMultiplexer redis,
|
|
|
|
|
IPlatformCatalogService platformCatalog)
|
|
|
|
|
{
|
|
|
|
|
_db = db;
|
|
|
|
|
_redis = redis;
|
|
|
|
|
_platformCatalog = platformCatalog;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<(bool Allowed, string? ErrorCode, string? Message)> CheckAsync(
|
|
|
|
|
HttpContext context,
|
|
|
|
|
ITenantContext tenant,
|
|
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
if (tenant.IsSystemAdmin || !tenant.IsAuthenticated || tenant.PlanTier is null || string.IsNullOrEmpty(tenant.CafeId))
|
|
|
|
|
return (true, null, null);
|
|
|
|
|
|
|
|
|
|
var method = context.Request.Method;
|
|
|
|
|
var path = context.Request.Path.Value ?? string.Empty;
|
|
|
|
|
|
|
|
|
|
if (method != HttpMethods.Post)
|
|
|
|
|
return (true, null, null);
|
|
|
|
|
|
|
|
|
|
var cafeId = tenant.CafeId;
|
|
|
|
|
var tier = tenant.PlanTier.Value;
|
|
|
|
|
|
|
|
|
|
var ordersPath = $"/api/cafes/{cafeId}/orders";
|
|
|
|
|
if (method == HttpMethods.Post &&
|
|
|
|
|
path.StartsWith(ordersPath, StringComparison.OrdinalIgnoreCase) &&
|
|
|
|
|
(path.Equals(ordersPath, StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
path.Equals($"{ordersPath}/", StringComparison.OrdinalIgnoreCase)))
|
|
|
|
|
{
|
|
|
|
|
var limits = await _platformCatalog.GetLimitsAsync(tier, cancellationToken);
|
|
|
|
|
var maxOrders = limits.MaxOrdersPerDay;
|
|
|
|
|
if (maxOrders == int.MaxValue)
|
|
|
|
|
return (true, null, null);
|
|
|
|
|
|
|
|
|
|
var todayStart = DateTime.UtcNow.Date;
|
|
|
|
|
var count = await _db.Orders
|
|
|
|
|
.CountAsync(o => o.CafeId == cafeId && o.CreatedAt >= todayStart, cancellationToken);
|
|
|
|
|
|
|
|
|
|
if (count >= maxOrders)
|
|
|
|
|
return (false, "PLAN_LIMIT_REACHED", "Daily order limit reached for your plan. Please upgrade.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var customersPath = $"/api/cafes/{cafeId}/customers";
|
|
|
|
|
if (path.StartsWith(customersPath, StringComparison.OrdinalIgnoreCase) &&
|
|
|
|
|
(path.Equals(customersPath, StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
path.Equals($"{customersPath}/", StringComparison.OrdinalIgnoreCase)))
|
|
|
|
|
{
|
|
|
|
|
var limitsCustomers = await _platformCatalog.GetLimitsAsync(tier, cancellationToken);
|
|
|
|
|
var maxCustomers = limitsCustomers.MaxCustomers;
|
|
|
|
|
if (maxCustomers == int.MaxValue)
|
|
|
|
|
return (true, null, null);
|
|
|
|
|
|
|
|
|
|
var count = await _db.Customers
|
|
|
|
|
.CountAsync(c => c.CafeId == cafeId, cancellationToken);
|
|
|
|
|
|
|
|
|
|
if (count >= maxCustomers)
|
|
|
|
|
return (false, "PLAN_LIMIT_REACHED", "Customer limit reached for your plan. Please upgrade.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var branchesPath = $"/api/cafes/{cafeId}/branches";
|
|
|
|
|
if (path.StartsWith(branchesPath, StringComparison.OrdinalIgnoreCase) &&
|
|
|
|
|
(path.Equals(branchesPath, StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
path.Equals($"{branchesPath}/", StringComparison.OrdinalIgnoreCase)))
|
|
|
|
|
{
|
|
|
|
|
var limitsBranches = await _platformCatalog.GetLimitsAsync(tier, cancellationToken);
|
|
|
|
|
var maxBranches = limitsBranches.MaxBranches;
|
|
|
|
|
if (maxBranches == int.MaxValue)
|
|
|
|
|
return (true, null, null);
|
|
|
|
|
|
|
|
|
|
var branchCount = await _db.Branches.CountAsync(b => b.CafeId == cafeId, cancellationToken);
|
|
|
|
|
if (branchCount >= maxBranches)
|
|
|
|
|
return (false, "PLAN_LIMIT_REACHED", "Branch limit reached for your plan. Please upgrade.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 00:58:49 +03:30
|
|
|
var tablesPath = $"/api/cafes/{cafeId}/tables";
|
|
|
|
|
if (path.StartsWith(tablesPath, StringComparison.OrdinalIgnoreCase) &&
|
|
|
|
|
(path.Equals(tablesPath, StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
path.Equals($"{tablesPath}/", StringComparison.OrdinalIgnoreCase)))
|
|
|
|
|
{
|
|
|
|
|
var limitsTables = await _platformCatalog.GetLimitsAsync(tier, cancellationToken);
|
|
|
|
|
var maxTables = limitsTables.MaxTables;
|
|
|
|
|
if (maxTables != int.MaxValue)
|
|
|
|
|
{
|
|
|
|
|
var tableCount = await _db.Tables.CountAsync(t => t.CafeId == cafeId, cancellationToken);
|
|
|
|
|
if (tableCount >= maxTables)
|
|
|
|
|
return (false, "PLAN_LIMIT_REACHED", "Table limit reached for your plan. Please upgrade.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 21:33:48 +03:30
|
|
|
var smsCampaignPath = $"/api/cafes/{cafeId}/sms/campaign";
|
|
|
|
|
if (path.Equals(smsCampaignPath, StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
path.Equals($"{smsCampaignPath}/", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
var limitsSms = await _platformCatalog.GetLimitsAsync(tier, cancellationToken);
|
|
|
|
|
var maxSms = limitsSms.MaxSmsPerMonth;
|
|
|
|
|
if (maxSms == 0)
|
|
|
|
|
return (false, "PLAN_LIMIT_REACHED", "SMS is not available on the Free plan. Please upgrade.");
|
|
|
|
|
|
|
|
|
|
if (maxSms == int.MaxValue)
|
|
|
|
|
return (true, null, null);
|
|
|
|
|
|
|
|
|
|
var monthKey = $"sms:usage:{cafeId}:{DateTime.UtcNow:yyyy-MM}";
|
|
|
|
|
var redis = _redis.GetDatabase();
|
|
|
|
|
var used = await redis.StringGetAsync(monthKey);
|
|
|
|
|
var usedCount = used.HasValue ? (int)used : 0;
|
|
|
|
|
|
|
|
|
|
if (usedCount >= maxSms)
|
|
|
|
|
return (false, "PLAN_LIMIT_REACHED", "Monthly SMS limit reached for your plan. Please upgrade.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (true, null, null);
|
|
|
|
|
}
|
|
|
|
|
}
|