using api.Data; using api.Models; using Microsoft.EntityFrameworkCore; namespace api.Features.Plans.Services; public interface IPlanLimitsService { PlanLimits GetLimits(WorkspacePlan plan); Task GetUsageAsync(Guid userId, CancellationToken ct = default); Task GetWorkspaceUsageAsync(Guid workspaceId, CancellationToken ct = default); Task CanCreateWorkspaceAsync(Guid userId, CancellationToken ct = default); Task CanCreateLinkAsync(Guid workspaceId, CancellationToken ct = default); Task CanCreateQRCodeAsync(Guid workspaceId, CancellationToken ct = default); Task CanCreateDomainAsync(Guid workspaceId, CancellationToken ct = default); Task CanTrackEventAsync(Guid workspaceId, CancellationToken ct = default); } public record PlanLimits( int MaxWorkspaces, int MaxLinksPerWorkspace, int MaxQRCodesPerWorkspace, int MaxDomainsPerWorkspace, int MaxEventsPerMonth, bool HasCustomDomains, bool HasPasswordProtection, bool HasAnalytics ); public record UsageStats( int TotalWorkspaces, int TotalLinks, int TotalQRCodes, int TotalDomains, int EventsThisMonth, WorkspacePlan HighestPlan ); public record WorkspaceUsageStats( Guid WorkspaceId, WorkspacePlan Plan, int Links, int QRCodes, int Domains, int EventsThisMonth, PlanLimits Limits ); public class PlanLimitsService(IServiceScopeFactory scopeFactory) : IPlanLimitsService { private static readonly Dictionary PlanConfigs = new() { [WorkspacePlan.Free] = new PlanLimits( MaxWorkspaces: 1, MaxLinksPerWorkspace: 50, MaxQRCodesPerWorkspace: 25, MaxDomainsPerWorkspace: 0, MaxEventsPerMonth: 10_000, HasCustomDomains: false, HasPasswordProtection: false, HasAnalytics: true ), [WorkspacePlan.Pro] = new PlanLimits( MaxWorkspaces: 5, MaxLinksPerWorkspace: 5_000, MaxQRCodesPerWorkspace: 1_000, MaxDomainsPerWorkspace: 3, MaxEventsPerMonth: 100_000, HasCustomDomains: true, HasPasswordProtection: true, HasAnalytics: true ), [WorkspacePlan.Business] = new PlanLimits( MaxWorkspaces: int.MaxValue, MaxLinksPerWorkspace: int.MaxValue, MaxQRCodesPerWorkspace: int.MaxValue, MaxDomainsPerWorkspace: int.MaxValue, MaxEventsPerMonth: int.MaxValue, HasCustomDomains: true, HasPasswordProtection: true, HasAnalytics: true ) }; public PlanLimits GetLimits(WorkspacePlan plan) => PlanConfigs[plan]; public async Task GetUsageAsync(Guid userId, CancellationToken ct = default) { using var scope = scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var workspaces = await db.Workspaces .Where(w => w.OwnerUserId == userId) .Select(w => new { w.Id, w.Plan }) .ToListAsync(ct); var workspaceIds = workspaces.Select(w => w.Id).ToList(); var totalLinks = await db.ShortLinks .CountAsync(l => workspaceIds.Contains(l.WorkspaceId), ct); var totalQRCodes = await db.QrCodeDesigns .CountAsync(q => workspaceIds.Contains(q.WorkspaceId), ct); var totalDomains = await db.Domains .CountAsync(d => workspaceIds.Contains(d.WorkspaceId), ct); var monthStart = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); var eventsThisMonth = await db.Events .CountAsync(e => workspaceIds.Contains(e.WorkspaceId) && e.Timestamp >= monthStart, ct); var highestPlan = workspaces.Any() ? workspaces.Max(w => w.Plan) : WorkspacePlan.Free; return new UsageStats( TotalWorkspaces: workspaces.Count, TotalLinks: totalLinks, TotalQRCodes: totalQRCodes, TotalDomains: totalDomains, EventsThisMonth: eventsThisMonth, HighestPlan: highestPlan ); } public async Task GetWorkspaceUsageAsync(Guid workspaceId, CancellationToken ct = default) { using var scope = scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var workspace = await db.Workspaces .Where(w => w.Id == workspaceId) .Select(w => new { w.Id, w.Plan }) .FirstOrDefaultAsync(ct); if (workspace == null) throw new KeyNotFoundException("Workspace not found"); var links = await db.ShortLinks.CountAsync(l => l.WorkspaceId == workspaceId, ct); var qrCodes = await db.QrCodeDesigns.CountAsync(q => q.WorkspaceId == workspaceId, ct); var domains = await db.Domains.CountAsync(d => d.WorkspaceId == workspaceId, ct); var monthStart = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); var eventsThisMonth = await db.Events .CountAsync(e => e.WorkspaceId == workspaceId && e.Timestamp >= monthStart, ct); var limits = GetLimits(workspace.Plan); return new WorkspaceUsageStats( WorkspaceId: workspaceId, Plan: workspace.Plan, Links: links, QRCodes: qrCodes, Domains: domains, EventsThisMonth: eventsThisMonth, Limits: limits ); } public async Task CanCreateWorkspaceAsync(Guid userId, CancellationToken ct = default) { var usage = await GetUsageAsync(userId, ct); var limits = GetLimits(usage.HighestPlan); return usage.TotalWorkspaces < limits.MaxWorkspaces; } public async Task CanCreateLinkAsync(Guid workspaceId, CancellationToken ct = default) { var usage = await GetWorkspaceUsageAsync(workspaceId, ct); return usage.Links < usage.Limits.MaxLinksPerWorkspace; } public async Task CanCreateQRCodeAsync(Guid workspaceId, CancellationToken ct = default) { var usage = await GetWorkspaceUsageAsync(workspaceId, ct); return usage.QRCodes < usage.Limits.MaxQRCodesPerWorkspace; } public async Task CanCreateDomainAsync(Guid workspaceId, CancellationToken ct = default) { var usage = await GetWorkspaceUsageAsync(workspaceId, ct); return usage.Domains < usage.Limits.MaxDomainsPerWorkspace; } public async Task CanTrackEventAsync(Guid workspaceId, CancellationToken ct = default) { var usage = await GetWorkspaceUsageAsync(workspaceId, ct); return usage.EventsThisMonth < usage.Limits.MaxEventsPerMonth; } }