164 lines
7.0 KiB
C#
164 lines
7.0 KiB
C#
using PlotLine.Data;
|
|
using PlotLine.Models;
|
|
|
|
namespace PlotLine.Services;
|
|
|
|
public interface ISubscriptionService
|
|
{
|
|
Task<UserSubscriptionDetail?> GetCurrentSubscriptionAsync(int userId);
|
|
Task<bool> HasPaidBillingRiskAsync(int userId);
|
|
Task<SubscriptionLevel?> GetSubscriptionLevelAsync(int subscriptionLevelId);
|
|
Task<IReadOnlyList<SubscriptionLevel>> GetActiveSubscriptionLevelsAsync();
|
|
Task<SubscriptionLimitResult> CanCreateBookAsync(int userId);
|
|
Task<SubscriptionLimitResult> CanCreateChapterAsync(int bookId, int userId);
|
|
Task<SubscriptionLimitResult> CanCreateSceneAsync(int bookId, int userId);
|
|
Task<SubscriptionLimitResult> CanCreateCharacterAsync(int projectId, int userId);
|
|
Task<SubscriptionLimitResult> CanInviteCollaboratorAsync(int userId);
|
|
Task<StorageUsage> GetStorageUsageAsync(int userId);
|
|
Task<SubscriptionUsageSummary> GetUsageSummaryAsync(int userId);
|
|
Task<StorageLimitResult> CanUploadFileAsync(int userId, long fileSizeBytes);
|
|
}
|
|
|
|
public sealed class SubscriptionService(ISubscriptionRepository subscriptions, IUserFileRepository userFiles) : ISubscriptionService
|
|
{
|
|
private const int PracticalUnlimited = 999999;
|
|
|
|
public Task<UserSubscriptionDetail?> GetCurrentSubscriptionAsync(int userId) =>
|
|
subscriptions.GetCurrentSubscriptionAsync(userId);
|
|
|
|
public async Task<bool> HasPaidBillingRiskAsync(int userId)
|
|
{
|
|
var subscription = await subscriptions.GetCurrentSubscriptionAsync(userId);
|
|
return HasPaidBillingRisk(subscription, DateTime.UtcNow);
|
|
}
|
|
|
|
public Task<SubscriptionLevel?> GetSubscriptionLevelAsync(int subscriptionLevelId) =>
|
|
subscriptions.GetSubscriptionLevelAsync(subscriptionLevelId);
|
|
|
|
public Task<IReadOnlyList<SubscriptionLevel>> GetActiveSubscriptionLevelsAsync() =>
|
|
subscriptions.GetActiveSubscriptionLevelsAsync();
|
|
|
|
public async Task<SubscriptionLimitResult> CanCreateBookAsync(int userId)
|
|
{
|
|
var subscription = await GetRequiredSubscriptionAsync(userId);
|
|
var currentUsage = await subscriptions.GetOwnedBookCountAsync(userId);
|
|
return Evaluate(subscription, currentUsage, subscription.MaxBooks, "books");
|
|
}
|
|
|
|
public async Task<SubscriptionLimitResult> CanCreateChapterAsync(int bookId, int userId)
|
|
{
|
|
var subscription = await GetRequiredSubscriptionAsync(userId);
|
|
var currentUsage = await subscriptions.GetChapterCountForBookAsync(bookId);
|
|
return Evaluate(subscription, currentUsage, subscription.MaxChaptersPerBook, "chapters per book");
|
|
}
|
|
|
|
public async Task<SubscriptionLimitResult> CanCreateSceneAsync(int bookId, int userId)
|
|
{
|
|
var subscription = await GetRequiredSubscriptionAsync(userId);
|
|
var currentUsage = await subscriptions.GetSceneCountForBookAsync(bookId);
|
|
return Evaluate(subscription, currentUsage, subscription.MaxScenesPerBook, "scenes per book");
|
|
}
|
|
|
|
public async Task<SubscriptionLimitResult> CanCreateCharacterAsync(int projectId, int userId)
|
|
{
|
|
var subscription = await GetRequiredSubscriptionAsync(userId);
|
|
var currentUsage = await subscriptions.GetCharacterCountForProjectAsync(projectId);
|
|
return Evaluate(subscription, currentUsage, subscription.MaxCharactersPerProject, "characters per project");
|
|
}
|
|
|
|
public async Task<SubscriptionLimitResult> CanInviteCollaboratorAsync(int userId)
|
|
{
|
|
var subscription = await GetRequiredSubscriptionAsync(userId);
|
|
var currentUsage = await subscriptions.GetCollaboratorCountForOwnerAsync(userId);
|
|
return Evaluate(subscription, currentUsage, subscription.MaxCollaborators, "collaborators");
|
|
}
|
|
|
|
public async Task<StorageUsage> GetStorageUsageAsync(int userId)
|
|
{
|
|
var subscription = await GetRequiredSubscriptionAsync(userId);
|
|
return new StorageUsage
|
|
{
|
|
UsedBytes = await userFiles.GetStorageUsageForOwnerAsync(userId),
|
|
MaximumBytes = ToBytes(subscription.MaxStorageMB)
|
|
};
|
|
}
|
|
|
|
public async Task<SubscriptionUsageSummary> GetUsageSummaryAsync(int userId)
|
|
{
|
|
var subscription = await GetRequiredSubscriptionAsync(userId);
|
|
var usage = await subscriptions.GetUsageSummaryForOwnerAsync(userId);
|
|
usage.StorageUsedBytes = await userFiles.GetStorageUsageForOwnerAsync(userId);
|
|
usage.StorageMaximumBytes = ToBytes(subscription.MaxStorageMB);
|
|
return usage;
|
|
}
|
|
|
|
public async Task<StorageLimitResult> CanUploadFileAsync(int userId, long fileSizeBytes)
|
|
{
|
|
var usage = await GetStorageUsageAsync(userId);
|
|
if (usage.MaximumBytes >= ToBytes(PracticalUnlimited) || usage.UsedBytes + fileSizeBytes <= usage.MaximumBytes)
|
|
{
|
|
return StorageLimitResult.AllowedResult(usage.UsedBytes, usage.MaximumBytes);
|
|
}
|
|
|
|
return StorageLimitResult.Blocked(usage.UsedBytes, usage.MaximumBytes, fileSizeBytes);
|
|
}
|
|
|
|
private async Task<UserSubscriptionDetail> GetRequiredSubscriptionAsync(int userId)
|
|
{
|
|
var subscription = await subscriptions.GetCurrentSubscriptionAsync(userId);
|
|
if (subscription is null)
|
|
{
|
|
throw new SubscriptionLimitException("No active subscription is assigned to your account.");
|
|
}
|
|
|
|
return subscription;
|
|
}
|
|
|
|
private static SubscriptionLimitResult Evaluate(UserSubscriptionDetail subscription, int currentUsage, int maximumAllowed, string limitName)
|
|
{
|
|
if (maximumAllowed >= PracticalUnlimited || currentUsage < maximumAllowed)
|
|
{
|
|
return SubscriptionLimitResult.AllowedResult(currentUsage, maximumAllowed);
|
|
}
|
|
|
|
return SubscriptionLimitResult.Blocked(
|
|
currentUsage,
|
|
maximumAllowed,
|
|
$"You have reached the maximum number of {limitName} allowed by your {subscription.DisplayName} subscription ({maximumAllowed:N0}). Upgrade your subscription to add more.");
|
|
}
|
|
|
|
private static long ToBytes(int megabytes) => megabytes * 1024L * 1024L;
|
|
|
|
internal static bool HasPaidBillingRisk(UserSubscriptionDetail? subscription, DateTime utcNow)
|
|
{
|
|
if (subscription is null || string.IsNullOrWhiteSpace(subscription.StripeSubscriptionId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var stripeStatus = (subscription.StripeStatus ?? string.Empty).Trim().ToLowerInvariant();
|
|
if (stripeStatus is "canceled" or "cancelled" or "incomplete_expired")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (subscription.CancelAtPeriodEnd
|
|
&& (!subscription.CurrentPeriodEndUTC.HasValue || subscription.CurrentPeriodEndUTC.Value > utcNow))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (subscription.CurrentPeriodEndUTC.HasValue
|
|
&& subscription.CurrentPeriodEndUTC.Value <= utcNow
|
|
&& stripeStatus is "ended" or "expired" or "inactive")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return stripeStatus is "active" or "trialing" or "past_due" or "unpaid" or "incomplete" or "paused"
|
|
|| string.IsNullOrWhiteSpace(stripeStatus);
|
|
}
|
|
}
|
|
|
|
public sealed class SubscriptionLimitException(string message) : InvalidOperationException(message);
|