Phase 5C Subscription Limit Usage
This commit is contained in:
parent
49f239b301
commit
b94ca66918
@ -35,7 +35,26 @@ public sealed class BooksController(IBookService books) : Controller
|
||||
return View("Edit", model);
|
||||
}
|
||||
|
||||
var bookId = await books.SaveAsync(model);
|
||||
int bookId;
|
||||
try
|
||||
{
|
||||
bookId = await books.SaveAsync(model);
|
||||
}
|
||||
catch (SubscriptionLimitException ex) when (model.BookID == 0)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, ex.Message);
|
||||
var createModel = await books.GetCreateAsync(model.ProjectID);
|
||||
if (createModel is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
createModel.BookTitle = model.BookTitle;
|
||||
createModel.BookNumber = model.BookNumber;
|
||||
createModel.Description = model.Description;
|
||||
return View("Edit", createModel);
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Details), new { id = bookId });
|
||||
}
|
||||
|
||||
|
||||
@ -35,7 +35,27 @@ public sealed class ChaptersController(IChapterService chapters) : Controller
|
||||
return View("Edit", model);
|
||||
}
|
||||
|
||||
var chapterId = await chapters.SaveAsync(model);
|
||||
int chapterId;
|
||||
try
|
||||
{
|
||||
chapterId = await chapters.SaveAsync(model);
|
||||
}
|
||||
catch (SubscriptionLimitException ex) when (model.ChapterID == 0)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, ex.Message);
|
||||
var createModel = await chapters.GetCreateAsync(model.BookID);
|
||||
if (createModel is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
createModel.ChapterNumber = model.ChapterNumber;
|
||||
createModel.ChapterTitle = model.ChapterTitle;
|
||||
createModel.Summary = model.Summary;
|
||||
createModel.RevisionStatusID = model.RevisionStatusID;
|
||||
return View("Edit", createModel);
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Details), new { id = chapterId });
|
||||
}
|
||||
|
||||
|
||||
@ -41,7 +41,33 @@ public sealed class CharactersController(ICharacterService characters) : Control
|
||||
return View("Edit", model);
|
||||
}
|
||||
|
||||
var characterId = await characters.SaveCharacterAsync(model);
|
||||
int characterId;
|
||||
try
|
||||
{
|
||||
characterId = await characters.SaveCharacterAsync(model);
|
||||
}
|
||||
catch (SubscriptionLimitException ex) when (model.CharacterID == 0)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, ex.Message);
|
||||
var createModel = await characters.GetCreateCharacterAsync(model.ProjectID);
|
||||
if (createModel is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
createModel.CharacterName = model.CharacterName;
|
||||
createModel.ShortName = model.ShortName;
|
||||
createModel.Sex = model.Sex;
|
||||
createModel.BirthDate = model.BirthDate;
|
||||
createModel.AgeAtSeriesStart = model.AgeAtSeriesStart;
|
||||
createModel.Height = model.Height;
|
||||
createModel.EyeColour = model.EyeColour;
|
||||
createModel.CharacterImportance = model.CharacterImportance;
|
||||
createModel.ShowInQuickAddBar = model.ShowInQuickAddBar;
|
||||
createModel.DefaultDescription = model.DefaultDescription;
|
||||
return View("Edit", createModel);
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Details), new { id = characterId });
|
||||
}
|
||||
|
||||
|
||||
@ -38,7 +38,24 @@ public sealed class ScenesController(
|
||||
return View("Edit", model);
|
||||
}
|
||||
|
||||
var sceneId = await scenes.SaveAsync(model);
|
||||
int sceneId;
|
||||
try
|
||||
{
|
||||
sceneId = await scenes.SaveAsync(model);
|
||||
}
|
||||
catch (SubscriptionLimitException ex) when (model.SceneID == 0)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, ex.Message);
|
||||
var createModel = await scenes.GetCreateAsync(model.ChapterID);
|
||||
if (createModel is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
CopyPostedSceneFields(model, createModel);
|
||||
return View("Edit", createModel);
|
||||
}
|
||||
|
||||
if (model.ReturnToTimeline && model.ReturnProjectID.HasValue)
|
||||
{
|
||||
return RedirectToAction("Index", "Timeline", new
|
||||
@ -402,6 +419,39 @@ public sealed class ScenesController(
|
||||
return RedirectToAction(nameof(Edit), new { id = sceneId });
|
||||
}
|
||||
|
||||
private static void CopyPostedSceneFields(SceneEditViewModel source, SceneEditViewModel target)
|
||||
{
|
||||
target.SceneTitle = source.SceneTitle;
|
||||
target.SceneNumber = source.SceneNumber;
|
||||
target.Summary = source.Summary;
|
||||
target.TimeModeID = source.TimeModeID;
|
||||
target.StartDateTime = source.StartDateTime;
|
||||
target.EndDateTime = source.EndDateTime;
|
||||
target.DurationAmount = source.DurationAmount;
|
||||
target.DurationUnitID = source.DurationUnitID;
|
||||
target.RelativeTimeText = source.RelativeTimeText;
|
||||
target.TimeConfidenceID = source.TimeConfidenceID;
|
||||
target.ScenePurposeNotes = source.ScenePurposeNotes;
|
||||
target.SceneOutcomeNotes = source.SceneOutcomeNotes;
|
||||
target.RevisionStatusID = source.RevisionStatusID;
|
||||
target.PrimaryLocationID = source.PrimaryLocationID;
|
||||
target.SelectedPurposeTypeIds = source.SelectedPurposeTypeIds;
|
||||
target.ReturnToTimeline = source.ReturnToTimeline;
|
||||
target.ReturnProjectID = source.ReturnProjectID;
|
||||
target.ReturnBookID = source.ReturnBookID;
|
||||
target.Metrics = target.Metrics.Select(metric =>
|
||||
{
|
||||
var postedMetric = source.Metrics.FirstOrDefault(x => x.MetricTypeID == metric.MetricTypeID);
|
||||
if (postedMetric is not null)
|
||||
{
|
||||
metric.Value = postedMetric.Value;
|
||||
metric.Notes = postedMetric.Notes;
|
||||
}
|
||||
|
||||
return metric;
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private async Task<string> SaveUploadedAttachmentAsync(int sceneId, IFormFile uploadedFile)
|
||||
{
|
||||
var uploadRoot = Path.Combine(environment.WebRootPath, "uploads", "scenes", sceneId.ToString());
|
||||
|
||||
@ -9,6 +9,11 @@ public interface ISubscriptionRepository
|
||||
Task<UserSubscriptionDetail?> GetCurrentSubscriptionAsync(int userId);
|
||||
Task<SubscriptionLevel?> GetSubscriptionLevelAsync(int subscriptionLevelId);
|
||||
Task<IReadOnlyList<SubscriptionLevel>> GetActiveSubscriptionLevelsAsync();
|
||||
Task<int> GetOwnedBookCountAsync(int userId);
|
||||
Task<int> GetChapterCountForBookAsync(int bookId);
|
||||
Task<int> GetSceneCountForBookAsync(int bookId);
|
||||
Task<int> GetCharacterCountForProjectAsync(int projectId);
|
||||
Task<int> GetCollaboratorCountForOwnerAsync(int userId);
|
||||
}
|
||||
|
||||
public sealed class SubscriptionRepository(ISqlConnectionFactory connectionFactory) : ISubscriptionRepository
|
||||
@ -39,4 +44,49 @@ public sealed class SubscriptionRepository(ISqlConnectionFactory connectionFacto
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<int> GetOwnedBookCountAsync(int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
"dbo.SubscriptionUsage_BookCountForOwner",
|
||||
new { UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<int> GetChapterCountForBookAsync(int bookId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
"dbo.SubscriptionUsage_ChapterCountForBook",
|
||||
new { BookID = bookId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<int> GetSceneCountForBookAsync(int bookId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
"dbo.SubscriptionUsage_SceneCountForBook",
|
||||
new { BookID = bookId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<int> GetCharacterCountForProjectAsync(int projectId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
"dbo.SubscriptionUsage_CharacterCountForProject",
|
||||
new { ProjectID = projectId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<int> GetCollaboratorCountForOwnerAsync(int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
"dbo.SubscriptionUsage_CollaboratorCountForOwner",
|
||||
new { UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,3 +46,26 @@ public sealed class UserSubscriptionDetail : UserSubscription
|
||||
public bool SubscriptionLevelIsActive { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SubscriptionLimitResult
|
||||
{
|
||||
public bool Allowed { get; init; }
|
||||
public int CurrentUsage { get; init; }
|
||||
public int MaximumAllowed { get; init; }
|
||||
public string Message { get; init; } = string.Empty;
|
||||
|
||||
public static SubscriptionLimitResult AllowedResult(int currentUsage, int maximumAllowed) => new()
|
||||
{
|
||||
Allowed = true,
|
||||
CurrentUsage = currentUsage,
|
||||
MaximumAllowed = maximumAllowed
|
||||
};
|
||||
|
||||
public static SubscriptionLimitResult Blocked(int currentUsage, int maximumAllowed, string message) => new()
|
||||
{
|
||||
Allowed = false,
|
||||
CurrentUsage = currentUsage,
|
||||
MaximumAllowed = maximumAllowed,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
@ -303,7 +303,29 @@ public sealed class ProjectService(IProjectRepository projects, IBookRepository
|
||||
=> currentUser.UserId.HasValue ? projects.ArchiveForUserAsync(projectId, currentUser.UserId.Value) : Task.CompletedTask;
|
||||
}
|
||||
|
||||
public sealed class BookService(IProjectRepository projects, IBookRepository books, IChapterRepository chapters) : IBookService
|
||||
internal static class SubscriptionLimitGuards
|
||||
{
|
||||
public static async Task EnsureAllowedAsync(Func<int, Task<SubscriptionLimitResult>> validateAsync, int? userId)
|
||||
{
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new SubscriptionLimitException("Sign in before creating new content.");
|
||||
}
|
||||
|
||||
var result = await validateAsync(userId.Value);
|
||||
if (!result.Allowed)
|
||||
{
|
||||
throw new SubscriptionLimitException(result.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class BookService(
|
||||
IProjectRepository projects,
|
||||
IBookRepository books,
|
||||
IChapterRepository chapters,
|
||||
ISubscriptionService subscriptions,
|
||||
ICurrentUserService currentUser) : IBookService
|
||||
{
|
||||
public async Task<BookDetailViewModel?> GetDetailAsync(int bookId)
|
||||
{
|
||||
@ -363,14 +385,22 @@ public sealed class BookService(IProjectRepository projects, IBookRepository boo
|
||||
};
|
||||
}
|
||||
|
||||
public Task<int> SaveAsync(BookEditViewModel model) => books.SaveAsync(new Book
|
||||
public async Task<int> SaveAsync(BookEditViewModel model)
|
||||
{
|
||||
BookID = model.BookID,
|
||||
ProjectID = model.ProjectID,
|
||||
BookTitle = model.BookTitle,
|
||||
BookNumber = model.BookNumber,
|
||||
Description = model.Description
|
||||
});
|
||||
if (model.BookID == 0)
|
||||
{
|
||||
await SubscriptionLimitGuards.EnsureAllowedAsync(subscriptions.CanCreateBookAsync, currentUser.UserId);
|
||||
}
|
||||
|
||||
return await books.SaveAsync(new Book
|
||||
{
|
||||
BookID = model.BookID,
|
||||
ProjectID = model.ProjectID,
|
||||
BookTitle = model.BookTitle,
|
||||
BookNumber = model.BookNumber,
|
||||
Description = model.Description
|
||||
});
|
||||
}
|
||||
|
||||
public Task ArchiveAsync(int bookId) => books.ArchiveAsync(bookId);
|
||||
}
|
||||
@ -380,7 +410,9 @@ public sealed class ChapterService(
|
||||
IBookRepository books,
|
||||
IChapterRepository chapters,
|
||||
ISceneRepository scenes,
|
||||
ILookupRepository lookups) : IChapterService
|
||||
ILookupRepository lookups,
|
||||
ISubscriptionService subscriptions,
|
||||
ICurrentUserService currentUser) : IChapterService
|
||||
{
|
||||
public async Task<ChapterDetailViewModel?> GetDetailAsync(int chapterId)
|
||||
{
|
||||
@ -458,15 +490,23 @@ public sealed class ChapterService(
|
||||
};
|
||||
}
|
||||
|
||||
public Task<int> SaveAsync(ChapterEditViewModel model) => chapters.SaveAsync(new Chapter
|
||||
public async Task<int> SaveAsync(ChapterEditViewModel model)
|
||||
{
|
||||
ChapterID = model.ChapterID,
|
||||
BookID = model.BookID,
|
||||
ChapterNumber = model.ChapterNumber,
|
||||
ChapterTitle = model.ChapterTitle,
|
||||
Summary = model.Summary,
|
||||
RevisionStatusID = model.RevisionStatusID
|
||||
});
|
||||
if (model.ChapterID == 0)
|
||||
{
|
||||
await SubscriptionLimitGuards.EnsureAllowedAsync(userId => subscriptions.CanCreateChapterAsync(model.BookID, userId), currentUser.UserId);
|
||||
}
|
||||
|
||||
return await chapters.SaveAsync(new Chapter
|
||||
{
|
||||
ChapterID = model.ChapterID,
|
||||
BookID = model.BookID,
|
||||
ChapterNumber = model.ChapterNumber,
|
||||
ChapterTitle = model.ChapterTitle,
|
||||
Summary = model.Summary,
|
||||
RevisionStatusID = model.RevisionStatusID
|
||||
});
|
||||
}
|
||||
|
||||
public Task ArchiveAsync(int chapterId) => chapters.ArchiveAsync(chapterId);
|
||||
|
||||
@ -490,7 +530,9 @@ public sealed class SceneService(
|
||||
ILocationRepository locations,
|
||||
IWarningRepository warnings,
|
||||
ISceneDependencyRepository sceneDependencies,
|
||||
IWriterWorkspaceRepository writerWorkspace) : ISceneService
|
||||
IWriterWorkspaceRepository writerWorkspace,
|
||||
ISubscriptionService subscriptions,
|
||||
ICurrentUserService currentUser) : ISceneService
|
||||
{
|
||||
public async Task<SceneEditViewModel?> GetCreateAsync(int chapterId)
|
||||
{
|
||||
@ -567,6 +609,17 @@ public sealed class SceneService(
|
||||
|
||||
public async Task<int> SaveAsync(SceneEditViewModel model)
|
||||
{
|
||||
if (model.SceneID == 0)
|
||||
{
|
||||
var chapter = await chapters.GetAsync(model.ChapterID);
|
||||
if (chapter is null)
|
||||
{
|
||||
throw new SubscriptionLimitException("The chapter could not be found.");
|
||||
}
|
||||
|
||||
await SubscriptionLimitGuards.EnsureAllowedAsync(userId => subscriptions.CanCreateSceneAsync(chapter.BookID, userId), currentUser.UserId);
|
||||
}
|
||||
|
||||
var sceneId = await scenes.SaveAsync(new Scene
|
||||
{
|
||||
SceneID = model.SceneID,
|
||||
@ -3406,7 +3459,12 @@ public sealed class LocationService(IProjectRepository projects, ILocationReposi
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CharacterService(IProjectRepository projects, IBookRepository books, ICharacterRepository characters) : ICharacterService
|
||||
public sealed class CharacterService(
|
||||
IProjectRepository projects,
|
||||
IBookRepository books,
|
||||
ICharacterRepository characters,
|
||||
ISubscriptionService subscriptions,
|
||||
ICurrentUserService currentUser) : ICharacterService
|
||||
{
|
||||
public async Task<CharacterListViewModel?> GetCharactersAsync(int projectId)
|
||||
{
|
||||
@ -3519,21 +3577,29 @@ public sealed class CharacterService(IProjectRepository projects, IBookRepositor
|
||||
};
|
||||
}
|
||||
|
||||
public Task<int> SaveCharacterAsync(CharacterEditViewModel model) => characters.SaveCharacterAsync(new Character
|
||||
public async Task<int> SaveCharacterAsync(CharacterEditViewModel model)
|
||||
{
|
||||
CharacterID = model.CharacterID,
|
||||
ProjectID = model.ProjectID,
|
||||
CharacterName = model.CharacterName,
|
||||
ShortName = model.ShortName,
|
||||
Sex = model.Sex,
|
||||
BirthDate = model.BirthDate,
|
||||
AgeAtSeriesStart = model.AgeAtSeriesStart,
|
||||
Height = model.Height,
|
||||
EyeColour = model.EyeColour,
|
||||
CharacterImportance = model.CharacterImportance,
|
||||
ShowInQuickAddBar = model.ShowInQuickAddBar,
|
||||
DefaultDescription = model.DefaultDescription
|
||||
});
|
||||
if (model.CharacterID == 0)
|
||||
{
|
||||
await SubscriptionLimitGuards.EnsureAllowedAsync(userId => subscriptions.CanCreateCharacterAsync(model.ProjectID, userId), currentUser.UserId);
|
||||
}
|
||||
|
||||
return await characters.SaveCharacterAsync(new Character
|
||||
{
|
||||
CharacterID = model.CharacterID,
|
||||
ProjectID = model.ProjectID,
|
||||
CharacterName = model.CharacterName,
|
||||
ShortName = model.ShortName,
|
||||
Sex = model.Sex,
|
||||
BirthDate = model.BirthDate,
|
||||
AgeAtSeriesStart = model.AgeAtSeriesStart,
|
||||
Height = model.Height,
|
||||
EyeColour = model.EyeColour,
|
||||
CharacterImportance = model.CharacterImportance,
|
||||
ShowInQuickAddBar = model.ShowInQuickAddBar,
|
||||
DefaultDescription = model.DefaultDescription
|
||||
});
|
||||
}
|
||||
|
||||
public Task ArchiveCharacterAsync(int characterId) => characters.ArchiveCharacterAsync(characterId);
|
||||
|
||||
|
||||
@ -8,10 +8,17 @@ public interface ISubscriptionService
|
||||
Task<UserSubscriptionDetail?> GetCurrentSubscriptionAsync(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);
|
||||
}
|
||||
|
||||
public sealed class SubscriptionService(ISubscriptionRepository subscriptions) : ISubscriptionService
|
||||
{
|
||||
private const int PracticalUnlimited = 999999;
|
||||
|
||||
public Task<UserSubscriptionDetail?> GetCurrentSubscriptionAsync(int userId) =>
|
||||
subscriptions.GetCurrentSubscriptionAsync(userId);
|
||||
|
||||
@ -20,4 +27,65 @@ public sealed class SubscriptionService(ISubscriptionRepository subscriptions) :
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SubscriptionLimitException(string message) : InvalidOperationException(message);
|
||||
|
||||
82
PlotLine/Sql/044_Phase5C_SubscriptionLimitUsage.sql
Normal file
82
PlotLine/Sql/044_Phase5C_SubscriptionLimitUsage.sql
Normal file
@ -0,0 +1,82 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SubscriptionUsage_BookCountForOwner
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT COUNT(DISTINCT b.BookID)
|
||||
FROM dbo.Books b
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||
WHERE pua.UserID = @UserID
|
||||
AND pua.AccessRole = N'Owner'
|
||||
AND pua.IsActive = 1
|
||||
AND p.IsArchived = 0
|
||||
AND b.IsArchived = 0;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SubscriptionUsage_ChapterCountForBook
|
||||
@BookID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT COUNT(1)
|
||||
FROM dbo.Chapters
|
||||
WHERE BookID = @BookID
|
||||
AND IsArchived = 0;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SubscriptionUsage_SceneCountForBook
|
||||
@BookID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT COUNT(1)
|
||||
FROM dbo.Scenes s
|
||||
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||
WHERE c.BookID = @BookID
|
||||
AND c.IsArchived = 0
|
||||
AND s.IsArchived = 0;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SubscriptionUsage_CharacterCountForProject
|
||||
@ProjectID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT COUNT(1)
|
||||
FROM dbo.Characters
|
||||
WHERE ProjectID = @ProjectID
|
||||
AND IsArchived = 0;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SubscriptionUsage_CollaboratorCountForOwner
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT COUNT(DISTINCT collaborator.UserID)
|
||||
FROM dbo.ProjectUserAccess ownerAccess
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = ownerAccess.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess collaborator ON collaborator.ProjectID = ownerAccess.ProjectID
|
||||
WHERE ownerAccess.UserID = @UserID
|
||||
AND ownerAccess.AccessRole = N'Owner'
|
||||
AND ownerAccess.IsActive = 1
|
||||
AND collaborator.AccessRole = N'Collaborator'
|
||||
AND collaborator.IsActive = 1
|
||||
AND p.IsArchived = 0;
|
||||
END;
|
||||
GO
|
||||
@ -13,6 +13,7 @@
|
||||
<form asp-action="Save" method="post" class="edit-panel">
|
||||
<input asp-for="CharacterID" type="hidden" />
|
||||
<input asp-for="ProjectID" type="hidden" />
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user