Phase 5J - Subscription Level Pricing

This commit is contained in:
Nick Beckley 2026-06-07 18:32:31 +01:00
parent e16a5cdb52
commit 55973d3e07
3 changed files with 146 additions and 0 deletions

View File

@ -20,6 +20,9 @@ public sealed class SubscriptionLevel
public string? StripeAnnualPriceId { get; set; }
public bool RequiresStripeSubscription { get; set; }
public string? BillingInterval { get; set; }
public int? MonthlyPricePence { get; set; }
public int? AnnualPricePence { get; set; }
public string CurrencyCode { get; set; } = "GBP";
}
public class UserSubscription
@ -60,6 +63,9 @@ public sealed class UserSubscriptionDetail : UserSubscription
public int MaxCollaborators { get; set; }
public bool SubscriptionLevelIsActive { get; set; }
public int SortOrder { get; set; }
public int? MonthlyPricePence { get; set; }
public int? AnnualPricePence { get; set; }
public string CurrencyCode { get; set; } = "GBP";
}
public sealed class SubscriptionLimitResult

View File

@ -0,0 +1,112 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'dbo.SubscriptionLevel') AND name = N'MonthlyPricePence')
ALTER TABLE dbo.SubscriptionLevel ADD MonthlyPricePence int NULL;
GO
IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'dbo.SubscriptionLevel') AND name = N'AnnualPricePence')
ALTER TABLE dbo.SubscriptionLevel ADD AnnualPricePence int NULL;
GO
IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'dbo.SubscriptionLevel') AND name = N'CurrencyCode')
ALTER TABLE dbo.SubscriptionLevel ADD CurrencyCode nvarchar(3) NOT NULL CONSTRAINT DF_SubscriptionLevel_CurrencyCode DEFAULT N'GBP';
GO
UPDATE dbo.SubscriptionLevel
SET MonthlyPricePence = CASE Name
WHEN N'DraftDesk' THEN NULL
WHEN N'WritingRoom' THEN 499
WHEN N'StoryForge' THEN 799
WHEN N'AuthorStudio' THEN 1299
ELSE MonthlyPricePence
END,
AnnualPricePence = CASE Name
WHEN N'DraftDesk' THEN NULL
WHEN N'WritingRoom' THEN 4999
WHEN N'StoryForge' THEN 7999
WHEN N'AuthorStudio' THEN 12999
ELSE AnnualPricePence
END,
CurrencyCode = N'GBP',
ModifiedDateUTC = SYSUTCDATETIME()
WHERE Name IN (N'DraftDesk', N'WritingRoom', N'StoryForge', N'AuthorStudio');
GO
CREATE OR ALTER PROCEDURE dbo.SubscriptionLevel_ListActive
AS
BEGIN
SET NOCOUNT ON;
SELECT SubscriptionLevelID, Name, DisplayName, Description, MaxBooks, MaxChaptersPerBook,
MaxScenesPerBook, MaxCharactersPerProject, MaxStorageMB, MaxCollaborators, IsActive,
SortOrder, CreatedDateUTC, ModifiedDateUTC, StripeMonthlyPriceId, StripeAnnualPriceId,
RequiresStripeSubscription, MonthlyPricePence, AnnualPricePence, CurrencyCode
FROM dbo.SubscriptionLevel
WHERE IsActive = 1
ORDER BY SortOrder, DisplayName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SubscriptionLevel_Get
@SubscriptionLevelID int
AS
BEGIN
SET NOCOUNT ON;
SELECT SubscriptionLevelID, Name, DisplayName, Description, MaxBooks, MaxChaptersPerBook,
MaxScenesPerBook, MaxCharactersPerProject, MaxStorageMB, MaxCollaborators, IsActive,
SortOrder, CreatedDateUTC, ModifiedDateUTC, StripeMonthlyPriceId, StripeAnnualPriceId,
RequiresStripeSubscription, MonthlyPricePence, AnnualPricePence, CurrencyCode
FROM dbo.SubscriptionLevel
WHERE SubscriptionLevelID = @SubscriptionLevelID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SubscriptionLevel_GetByStripePrice
@StripePriceId nvarchar(100)
AS
BEGIN
SET NOCOUNT ON;
SELECT SubscriptionLevelID, Name, DisplayName, Description, MaxBooks, MaxChaptersPerBook,
MaxScenesPerBook, MaxCharactersPerProject, MaxStorageMB, MaxCollaborators, IsActive,
SortOrder, CreatedDateUTC, ModifiedDateUTC, StripeMonthlyPriceId, StripeAnnualPriceId,
RequiresStripeSubscription, MonthlyPricePence, AnnualPricePence, CurrencyCode,
CASE
WHEN StripeMonthlyPriceId = @StripePriceId THEN N'Monthly'
WHEN StripeAnnualPriceId = @StripePriceId THEN N'Annual'
ELSE NULL
END AS BillingInterval
FROM dbo.SubscriptionLevel
WHERE IsActive = 1
AND RequiresStripeSubscription = 1
AND (StripeMonthlyPriceId = @StripePriceId OR StripeAnnualPriceId = @StripePriceId);
END;
GO
CREATE OR ALTER PROCEDURE dbo.UserSubscription_GetCurrent
@UserID int
AS
BEGIN
SET NOCOUNT ON;
SELECT us.UserSubscriptionID, us.UserID, us.SubscriptionLevelID, us.StartDateUTC, us.EndDateUTC,
us.IsActive, us.IsTrial, us.TrialExpiryDateUTC, us.CreatedDateUTC, us.ModifiedDateUTC,
us.StripeCustomerId, us.StripeSubscriptionId, us.StripePriceId, us.BillingInterval, us.StripeStatus,
us.CurrentPeriodStartUTC, us.CurrentPeriodEndUTC, us.CancelAtPeriodEnd, us.CancelledDateUTC,
us.LastPaymentFailureDateUTC, us.LastStripeEventId,
sl.Name, sl.DisplayName, sl.Description, sl.MaxBooks, sl.MaxChaptersPerBook,
sl.MaxScenesPerBook, sl.MaxCharactersPerProject, sl.MaxStorageMB, sl.MaxCollaborators,
sl.IsActive AS SubscriptionLevelIsActive, sl.SortOrder, sl.StripeMonthlyPriceId,
sl.StripeAnnualPriceId, sl.RequiresStripeSubscription, sl.MonthlyPricePence,
sl.AnnualPricePence, sl.CurrencyCode
FROM dbo.UserSubscription us
INNER JOIN dbo.SubscriptionLevel sl ON sl.SubscriptionLevelID = us.SubscriptionLevelID
WHERE us.UserID = @UserID
AND us.IsActive = 1
AND (us.EndDateUTC IS NULL OR us.EndDateUTC > SYSUTCDATETIME());
END;
GO

View File

@ -158,6 +158,29 @@
<h3>@level.DisplayName</h3>
<p class="eyebrow">@(isCurrent ? "Current plan" : "Available plan")</p>
<p>@(string.IsNullOrWhiteSpace(level.Description) ? "No description available." : level.Description)</p>
@if (!level.RequiresStripeSubscription)
{
<p class="lead-text">Free / trial / application managed</p>
}
else
{
<div class="row g-2 mb-3">
@if (level.MonthlyPricePence.HasValue)
{
<div class="col-sm-6">
<p class="eyebrow">Monthly</p>
<p>@FormatMoney(level.MonthlyPricePence.Value, level.CurrencyCode)</p>
</div>
}
@if (level.AnnualPricePence.HasValue)
{
<div class="col-sm-6">
<p class="eyebrow">Annual</p>
<p>@FormatMoney(level.AnnualPricePence.Value, level.CurrencyCode)</p>
</div>
}
</div>
}
<dl class="row small">
<dt class="col-7">Books</dt>
<dd class="col-5">@FormatLimit(level.MaxBooks)</dd>
@ -226,4 +249,9 @@
maximumBytes >= PracticalUnlimited * 1024L * 1024L
? $"{StorageUsage.FormatBytes(usedBytes)} / Unlimited storage"
: $"{StorageUsage.FormatBytes(usedBytes)} / {StorageUsage.FormatBytes(maximumBytes)} storage";
private static string FormatMoney(int pence, string? currencyCode) =>
string.Equals(currencyCode, "GBP", StringComparison.OrdinalIgnoreCase)
? $"£{pence / 100m:0.00}"
: $"{pence / 100m:0.00} {(string.IsNullOrWhiteSpace(currencyCode) ? "GBP" : currencyCode.ToUpperInvariant())}";
}