From 6d060776b013395737e68ccb8f54d07eaf0f4311 Mon Sep 17 00:00:00 2001 From: Cy Okeke Date: Wed, 28 Jan 2026 14:20:41 +0100 Subject: [PATCH] Implement the detail Subscription Discount Database Infrastructure --- .../Billing/Enums/DiscountAudienceType.cs | 13 ++ .../Entities/SubscriptionDiscount.cs | 48 +++++ .../ISubscriptionDiscountRepository.cs | 23 ++ .../SubscriptionDiscountRepository.cs | 39 ++++ .../DapperServiceCollectionExtensions.cs | 2 + ...criptionDiscountEntityTypeConfiguration.cs | 30 +++ .../Billing/Models/SubscriptionDiscount.cs | 18 ++ .../SubscriptionDiscountRepository.cs | 51 +++++ ...ityFrameworkServiceCollectionExtensions.cs | 2 + .../Repositories/DatabaseContext.cs | 1 + .../SubscriptionDiscountRepositoryTests.cs | 187 ++++++++++++++++ ...-01-27_00_AddSubscriptionDiscountTable.sql | 201 ++++++++++++++++++ 12 files changed, 615 insertions(+) create mode 100644 src/Core/Billing/Enums/DiscountAudienceType.cs create mode 100644 src/Core/Billing/Subscriptions/Entities/SubscriptionDiscount.cs create mode 100644 src/Core/Billing/Subscriptions/Repositories/ISubscriptionDiscountRepository.cs create mode 100644 src/Infrastructure.Dapper/Billing/Repositories/SubscriptionDiscountRepository.cs create mode 100644 src/Infrastructure.EntityFramework/Billing/Configurations/SubscriptionDiscountEntityTypeConfiguration.cs create mode 100644 src/Infrastructure.EntityFramework/Billing/Models/SubscriptionDiscount.cs create mode 100644 src/Infrastructure.EntityFramework/Billing/Repositories/SubscriptionDiscountRepository.cs create mode 100644 test/Infrastructure.IntegrationTest/Billing/Repositories/SubscriptionDiscountRepositoryTests.cs create mode 100644 util/Migrator/DbScripts/2026-01-27_00_AddSubscriptionDiscountTable.sql diff --git a/src/Core/Billing/Enums/DiscountAudienceType.cs b/src/Core/Billing/Enums/DiscountAudienceType.cs new file mode 100644 index 0000000000..6ab4646622 --- /dev/null +++ b/src/Core/Billing/Enums/DiscountAudienceType.cs @@ -0,0 +1,13 @@ +namespace Bit.Core.Billing.Enums; + +/// +/// Defines the target audience for subscription discounts using an extensible strategy pattern. +/// Each audience type maps to specific eligibility rules implemented via IDiscountAudienceFilter. +/// +public enum DiscountAudienceType +{ + /// + /// Discount applies to users who have never had a subscription before. + /// + UserHasNoPreviousSubscriptions = 0 +} diff --git a/src/Core/Billing/Subscriptions/Entities/SubscriptionDiscount.cs b/src/Core/Billing/Subscriptions/Entities/SubscriptionDiscount.cs new file mode 100644 index 0000000000..ec8fa5f090 --- /dev/null +++ b/src/Core/Billing/Subscriptions/Entities/SubscriptionDiscount.cs @@ -0,0 +1,48 @@ +#nullable enable + +using System.ComponentModel.DataAnnotations; +using Bit.Core.Billing.Enums; +using Bit.Core.Entities; +using Bit.Core.Utilities; + +namespace Bit.Core.Billing.Subscriptions.Entities; + +public class SubscriptionDiscount : ITableObject, IRevisable, IValidatableObject +{ + public Guid Id { get; set; } + [MaxLength(50)] + public string StripeCouponId { get; set; } = null!; + public string? StripeProductIds { get; set; } + public decimal? PercentOff { get; set; } + public long? AmountOff { get; set; } + [MaxLength(10)] + public string? Currency { get; set; } + [MaxLength(20)] + public string Duration { get; set; } = null!; + public int? DurationInMonths { get; set; } + [MaxLength(100)] + public string? Name { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public DiscountAudienceType AudienceType { get; set; } + public DateTime CreationDate { get; set; } = DateTime.UtcNow; + public DateTime RevisionDate { get; set; } = DateTime.UtcNow; + + public void SetNewId() + { + if (Id == default) + { + Id = CoreHelpers.GenerateComb(); + } + } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (EndDate < StartDate) + { + yield return new ValidationResult( + "EndDate must be greater than or equal to StartDate.", + new[] { nameof(EndDate) }); + } + } +} diff --git a/src/Core/Billing/Subscriptions/Repositories/ISubscriptionDiscountRepository.cs b/src/Core/Billing/Subscriptions/Repositories/ISubscriptionDiscountRepository.cs new file mode 100644 index 0000000000..9a72334688 --- /dev/null +++ b/src/Core/Billing/Subscriptions/Repositories/ISubscriptionDiscountRepository.cs @@ -0,0 +1,23 @@ +#nullable enable + +using Bit.Core.Billing.Subscriptions.Entities; +using Bit.Core.Repositories; + +namespace Bit.Core.Billing.Subscriptions.Repositories; + +public interface ISubscriptionDiscountRepository : IRepository +{ + /// + /// Retrieves all active subscription discounts that are currently within their valid date range. + /// A discount is considered active if the current UTC date falls between StartDate (inclusive) and EndDate (inclusive). + /// + /// A collection of active subscription discounts. + Task> GetActiveDiscountsAsync(); + + /// + /// Retrieves a subscription discount by its Stripe coupon ID. + /// + /// The Stripe coupon ID to search for. + /// The subscription discount if found; otherwise, null. + Task GetByStripeCouponIdAsync(string stripeCouponId); +} diff --git a/src/Infrastructure.Dapper/Billing/Repositories/SubscriptionDiscountRepository.cs b/src/Infrastructure.Dapper/Billing/Repositories/SubscriptionDiscountRepository.cs new file mode 100644 index 0000000000..e7fd6c662b --- /dev/null +++ b/src/Infrastructure.Dapper/Billing/Repositories/SubscriptionDiscountRepository.cs @@ -0,0 +1,39 @@ +using System.Data; +using Bit.Core.Billing.Subscriptions.Entities; +using Bit.Core.Billing.Subscriptions.Repositories; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +namespace Bit.Infrastructure.Dapper.Billing.Repositories; + +public class SubscriptionDiscountRepository( + GlobalSettings globalSettings) + : Repository( + globalSettings.SqlServer.ConnectionString, + globalSettings.SqlServer.ReadOnlyConnectionString), ISubscriptionDiscountRepository +{ + public async Task> GetActiveDiscountsAsync() + { + using var sqlConnection = new SqlConnection(ReadOnlyConnectionString); + + var results = await sqlConnection.QueryAsync( + "[dbo].[SubscriptionDiscount_ReadActive]", + commandType: CommandType.StoredProcedure); + + return results.ToArray(); + } + + public async Task GetByStripeCouponIdAsync(string stripeCouponId) + { + using var sqlConnection = new SqlConnection(ReadOnlyConnectionString); + + var result = await sqlConnection.QueryFirstOrDefaultAsync( + "[dbo].[SubscriptionDiscount_ReadByStripeCouponId]", + new { StripeCouponId = stripeCouponId }, + commandType: CommandType.StoredProcedure); + + return result; + } +} diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index dcb0dc1306..4055281352 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Organizations.Repositories; using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Subscriptions.Repositories; using Bit.Core.Dirt.Reports.Repositories; using Bit.Core.Dirt.Repositories; using Bit.Core.KeyManagement.Repositories; @@ -65,6 +66,7 @@ public static class DapperServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services diff --git a/src/Infrastructure.EntityFramework/Billing/Configurations/SubscriptionDiscountEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/Billing/Configurations/SubscriptionDiscountEntityTypeConfiguration.cs new file mode 100644 index 0000000000..512c5506a3 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Billing/Configurations/SubscriptionDiscountEntityTypeConfiguration.cs @@ -0,0 +1,30 @@ +using Bit.Infrastructure.EntityFramework.Billing.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Bit.Infrastructure.EntityFramework.Billing.Configurations; + +public class SubscriptionDiscountEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(t => t.Id) + .ValueGeneratedNever(); + + builder + .HasIndex(sd => sd.StripeCouponId) + .IsUnique(); + + var dateRangeIndex = builder + .HasIndex(sd => new { sd.StartDate, sd.EndDate }) + .IsClustered(false) + .HasDatabaseName("IX_SubscriptionDiscount_DateRange"); + + SqlServerIndexBuilderExtensions.IncludeProperties( + dateRangeIndex, + sd => new { sd.StripeProductIds, sd.AudienceType }); + + builder.ToTable(nameof(SubscriptionDiscount)); + } +} diff --git a/src/Infrastructure.EntityFramework/Billing/Models/SubscriptionDiscount.cs b/src/Infrastructure.EntityFramework/Billing/Models/SubscriptionDiscount.cs new file mode 100644 index 0000000000..b73f2c06fc --- /dev/null +++ b/src/Infrastructure.EntityFramework/Billing/Models/SubscriptionDiscount.cs @@ -0,0 +1,18 @@ +#nullable enable + +using AutoMapper; + +namespace Bit.Infrastructure.EntityFramework.Billing.Models; + +// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global +public class SubscriptionDiscount : Core.Billing.Subscriptions.Entities.SubscriptionDiscount +{ +} + +public class SubscriptionDiscountMapperProfile : Profile +{ + public SubscriptionDiscountMapperProfile() + { + CreateMap().ReverseMap(); + } +} diff --git a/src/Infrastructure.EntityFramework/Billing/Repositories/SubscriptionDiscountRepository.cs b/src/Infrastructure.EntityFramework/Billing/Repositories/SubscriptionDiscountRepository.cs new file mode 100644 index 0000000000..e2659afd54 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Billing/Repositories/SubscriptionDiscountRepository.cs @@ -0,0 +1,51 @@ +using AutoMapper; +using Bit.Core.Billing.Subscriptions.Entities; +using Bit.Core.Billing.Subscriptions.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using EFSubscriptionDiscount = Bit.Infrastructure.EntityFramework.Billing.Models.SubscriptionDiscount; + +namespace Bit.Infrastructure.EntityFramework.Billing.Repositories; + +public class SubscriptionDiscountRepository( + IMapper mapper, + IServiceScopeFactory serviceScopeFactory) + : Repository( + serviceScopeFactory, + mapper, + context => context.SubscriptionDiscounts), ISubscriptionDiscountRepository +{ + public async Task> GetActiveDiscountsAsync() + { + using var serviceScope = ServiceScopeFactory.CreateScope(); + + var databaseContext = GetDatabaseContext(serviceScope); + + var query = + from subscriptionDiscount in databaseContext.SubscriptionDiscounts + where subscriptionDiscount.StartDate <= DateTime.UtcNow + && subscriptionDiscount.EndDate >= DateTime.UtcNow + select subscriptionDiscount; + + var results = await query.ToArrayAsync(); + + return Mapper.Map>(results); + } + + public async Task GetByStripeCouponIdAsync(string stripeCouponId) + { + using var serviceScope = ServiceScopeFactory.CreateScope(); + + var databaseContext = GetDatabaseContext(serviceScope); + + var query = + from subscriptionDiscount in databaseContext.SubscriptionDiscounts + where subscriptionDiscount.StripeCouponId == stripeCouponId + select subscriptionDiscount; + + var result = await query.FirstOrDefaultAsync(); + + return result == null ? null : Mapper.Map(result); + } +} diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index 320cb9436d..84a370b723 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Organizations.Repositories; using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Subscriptions.Repositories; using Bit.Core.Dirt.Reports.Repositories; using Bit.Core.Dirt.Repositories; using Bit.Core.Enums; @@ -102,6 +103,7 @@ public static class EntityFrameworkServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index a0ee0376c0..503cad6895 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -79,6 +79,7 @@ public class DatabaseContext : DbContext public DbSet WebAuthnCredentials { get; set; } public DbSet ProviderPlans { get; set; } public DbSet ProviderInvoiceItems { get; set; } + public DbSet SubscriptionDiscounts { get; set; } public DbSet Notifications { get; set; } public DbSet NotificationStatuses { get; set; } public DbSet ClientOrganizationMigrationRecords { get; set; } diff --git a/test/Infrastructure.IntegrationTest/Billing/Repositories/SubscriptionDiscountRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Billing/Repositories/SubscriptionDiscountRepositoryTests.cs new file mode 100644 index 0000000000..ecde98191c --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Billing/Repositories/SubscriptionDiscountRepositoryTests.cs @@ -0,0 +1,187 @@ +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Subscriptions.Entities; +using Bit.Core.Billing.Subscriptions.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.Billing.Repositories; + +public class SubscriptionDiscountRepositoryTests +{ + private static SubscriptionDiscount CreateTestDiscount( + string? stripeCouponId = null, + string? stripeProductIds = null, + decimal? percentOff = null, + long? amountOff = null, + string? currency = null, + string duration = "once", + int? durationInMonths = null, + string? name = null, + DateTime? startDate = null, + DateTime? endDate = null, + DiscountAudienceType audienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions, + DateTime? creationDate = null, + DateTime? revisionDate = null) + { + var now = DateTime.UtcNow; + return new SubscriptionDiscount + { + StripeCouponId = stripeCouponId ?? $"test-{Guid.NewGuid()}", + StripeProductIds = stripeProductIds, + PercentOff = percentOff, + AmountOff = amountOff, + Currency = currency, + Duration = duration, + DurationInMonths = durationInMonths, + Name = name, + StartDate = startDate ?? now, + EndDate = endDate ?? now.AddDays(30), + AudienceType = audienceType, + CreationDate = creationDate ?? now, + RevisionDate = revisionDate ?? now + }; + } + + [Theory, DatabaseData] + public async Task GetActiveDiscountsAsync_ReturnsDiscountsWithinDateRange( + ISubscriptionDiscountRepository subscriptionDiscountRepository) + { + // Create a discount that is currently active + var activeDiscount = await subscriptionDiscountRepository.CreateAsync( + CreateTestDiscount( + stripeCouponId: $"test-active-{Guid.NewGuid()}", + percentOff: 25.00m, + name: "Active Discount", + startDate: DateTime.UtcNow.AddDays(-1), + endDate: DateTime.UtcNow.AddDays(30))); + + // Create a discount that has expired + var expiredDiscount = await subscriptionDiscountRepository.CreateAsync( + CreateTestDiscount( + stripeCouponId: $"test-expired-{Guid.NewGuid()}", + percentOff: 50.00m, + name: "Expired Discount", + startDate: DateTime.UtcNow.AddDays(-60), + endDate: DateTime.UtcNow.AddDays(-30))); + + // Create a discount that starts in the future + var futureDiscount = await subscriptionDiscountRepository.CreateAsync( + CreateTestDiscount( + stripeCouponId: $"test-future-{Guid.NewGuid()}", + percentOff: 15.00m, + name: "Future Discount", + startDate: DateTime.UtcNow.AddDays(30), + endDate: DateTime.UtcNow.AddDays(60))); + + // Act + var activeDiscounts = await subscriptionDiscountRepository.GetActiveDiscountsAsync(); + + // Assert + Assert.Contains(activeDiscounts, d => d.Id == activeDiscount.Id); + Assert.DoesNotContain(activeDiscounts, d => d.Id == expiredDiscount.Id); + Assert.DoesNotContain(activeDiscounts, d => d.Id == futureDiscount.Id); + } + + [Theory, DatabaseData] + public async Task GetByStripeCouponIdAsync_ReturnsCorrectDiscount( + ISubscriptionDiscountRepository subscriptionDiscountRepository) + { + // Arrange + var couponId = $"test-coupon-{Guid.NewGuid()}"; + var discount = await subscriptionDiscountRepository.CreateAsync( + CreateTestDiscount( + stripeCouponId: couponId, + stripeProductIds: "[\"prod_123\", \"prod_456\"]", + percentOff: 20.00m, + duration: "repeating", + durationInMonths: 3, + name: "Test Discount", + endDate: DateTime.UtcNow.AddDays(90))); + + // Act + var result = await subscriptionDiscountRepository.GetByStripeCouponIdAsync(couponId); + + // Assert + Assert.NotNull(result); + Assert.Equal(discount.Id, result.Id); + Assert.Equal(couponId, result.StripeCouponId); + Assert.Equal(20.00m, result.PercentOff); + Assert.Equal(3, result.DurationInMonths); + } + + [Theory, DatabaseData] + public async Task GetByStripeCouponIdAsync_ReturnsNull_WhenCouponDoesNotExist( + ISubscriptionDiscountRepository subscriptionDiscountRepository) + { + // Act + var result = await subscriptionDiscountRepository.GetByStripeCouponIdAsync("non-existent-coupon"); + + // Assert + Assert.Null(result); + } + + [Theory, DatabaseData] + public async Task CreateAsync_CreatesDiscountSuccessfully( + ISubscriptionDiscountRepository subscriptionDiscountRepository) + { + // Arrange + var discount = CreateTestDiscount( + stripeCouponId: $"test-create-{Guid.NewGuid()}", + stripeProductIds: "[\"prod_789\"]", + amountOff: 500, + currency: "usd", + name: "Fixed Amount Discount"); + + // Act + var createdDiscount = await subscriptionDiscountRepository.CreateAsync(discount); + + // Assert + Assert.NotNull(createdDiscount); + Assert.NotEqual(Guid.Empty, createdDiscount.Id); + Assert.Equal(discount.StripeCouponId, createdDiscount.StripeCouponId); + Assert.Equal(500, createdDiscount.AmountOff); + Assert.Equal("usd", createdDiscount.Currency); + } + + [Theory, DatabaseData] + public async Task ReplaceAsync_UpdatesDiscountSuccessfully( + ISubscriptionDiscountRepository subscriptionDiscountRepository) + { + // Arrange + var discount = await subscriptionDiscountRepository.CreateAsync( + CreateTestDiscount( + stripeCouponId: $"test-update-{Guid.NewGuid()}", + percentOff: 10.00m, + name: "Original Name")); + + // Act + discount.Name = "Updated Name"; + discount.PercentOff = 15.00m; + discount.RevisionDate = DateTime.UtcNow; + await subscriptionDiscountRepository.ReplaceAsync(discount); + + // Assert + var updatedDiscount = await subscriptionDiscountRepository.GetByIdAsync(discount.Id); + Assert.NotNull(updatedDiscount); + Assert.Equal("Updated Name", updatedDiscount.Name); + Assert.Equal(15.00m, updatedDiscount.PercentOff); + } + + [Theory, DatabaseData] + public async Task DeleteAsync_RemovesDiscountSuccessfully( + ISubscriptionDiscountRepository subscriptionDiscountRepository) + { + // Arrange + var discount = await subscriptionDiscountRepository.CreateAsync( + CreateTestDiscount( + stripeCouponId: $"test-delete-{Guid.NewGuid()}", + percentOff: 25.00m, + name: "To Be Deleted")); + + // Act + await subscriptionDiscountRepository.DeleteAsync(discount); + + // Assert + var deletedDiscount = await subscriptionDiscountRepository.GetByIdAsync(discount.Id); + Assert.Null(deletedDiscount); + } +} diff --git a/util/Migrator/DbScripts/2026-01-27_00_AddSubscriptionDiscountTable.sql b/util/Migrator/DbScripts/2026-01-27_00_AddSubscriptionDiscountTable.sql new file mode 100644 index 0000000000..7cd292b5a1 --- /dev/null +++ b/util/Migrator/DbScripts/2026-01-27_00_AddSubscriptionDiscountTable.sql @@ -0,0 +1,201 @@ +-- Table +IF OBJECT_ID('[dbo].[SubscriptionDiscount]') IS NULL +BEGIN + CREATE TABLE [dbo].[SubscriptionDiscount] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [StripeCouponId] VARCHAR(50) NOT NULL, + [StripeProductIds] NVARCHAR(MAX) NULL, + [PercentOff] DECIMAL(5,2) NULL, + [AmountOff] BIGINT NULL, + [Currency] VARCHAR(10) NULL, + [Duration] VARCHAR(20) NOT NULL, + [DurationInMonths] INT NULL, + [Name] NVARCHAR(100) NULL, + [StartDate] DATETIME2(7) NOT NULL, + [EndDate] DATETIME2(7) NOT NULL, + [AudienceType] INT NOT NULL DEFAULT 0, + [CreationDate] DATETIME2(7) NOT NULL, + [RevisionDate] DATETIME2(7) NOT NULL, + CONSTRAINT [PK_SubscriptionDiscount] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [IX_SubscriptionDiscount_StripeCouponId] UNIQUE ([StripeCouponId]) + ); +END +GO + +-- Index for date range queries +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_SubscriptionDiscount_DateRange' AND object_id = OBJECT_ID('[dbo].[SubscriptionDiscount]')) +BEGIN + CREATE INDEX [IX_SubscriptionDiscount_DateRange] ON [dbo].[SubscriptionDiscount] + ([StartDate], [EndDate]) INCLUDE ([StripeProductIds], [AudienceType]); +END +GO + +-- View +CREATE OR ALTER VIEW [dbo].[SubscriptionDiscountView] +AS +SELECT + * +FROM + [dbo].[SubscriptionDiscount] +GO + +-- Stored Procedures: Create +CREATE OR ALTER PROCEDURE [dbo].[SubscriptionDiscount_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @StripeCouponId VARCHAR(50), + @StripeProductIds NVARCHAR(MAX), + @PercentOff DECIMAL(5,2), + @AmountOff BIGINT, + @Currency VARCHAR(10), + @Duration VARCHAR(20), + @DurationInMonths INT, + @Name NVARCHAR(100), + @StartDate DATETIME2(7), + @EndDate DATETIME2(7), + @AudienceType INT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[SubscriptionDiscount] + ( + [Id], + [StripeCouponId], + [StripeProductIds], + [PercentOff], + [AmountOff], + [Currency], + [Duration], + [DurationInMonths], + [Name], + [StartDate], + [EndDate], + [AudienceType], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @StripeCouponId, + @StripeProductIds, + @PercentOff, + @AmountOff, + @Currency, + @Duration, + @DurationInMonths, + @Name, + @StartDate, + @EndDate, + @AudienceType, + @CreationDate, + @RevisionDate + ) +END +GO + +-- Stored Procedures: DeleteById +CREATE OR ALTER PROCEDURE [dbo].[SubscriptionDiscount_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE + FROM + [dbo].[SubscriptionDiscount] + WHERE + [Id] = @Id +END +GO + +-- Stored Procedures: ReadById +CREATE OR ALTER PROCEDURE [dbo].[SubscriptionDiscount_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[SubscriptionDiscountView] + WHERE + [Id] = @Id +END +GO + +-- Stored Procedures: Update +CREATE OR ALTER PROCEDURE [dbo].[SubscriptionDiscount_Update] + @Id UNIQUEIDENTIFIER OUTPUT, + @StripeCouponId VARCHAR(50), + @StripeProductIds NVARCHAR(MAX), + @PercentOff DECIMAL(5,2), + @AmountOff BIGINT, + @Currency VARCHAR(10), + @Duration VARCHAR(20), + @DurationInMonths INT, + @Name NVARCHAR(100), + @StartDate DATETIME2(7), + @EndDate DATETIME2(7), + @AudienceType INT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[SubscriptionDiscount] + SET + [StripeCouponId] = @StripeCouponId, + [StripeProductIds] = @StripeProductIds, + [PercentOff] = @PercentOff, + [AmountOff] = @AmountOff, + [Currency] = @Currency, + [Duration] = @Duration, + [DurationInMonths] = @DurationInMonths, + [Name] = @Name, + [StartDate] = @StartDate, + [EndDate] = @EndDate, + [AudienceType] = @AudienceType, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END +GO + +-- Stored Procedures: ReadActive (returns discounts within date range) +CREATE OR ALTER PROCEDURE [dbo].[SubscriptionDiscount_ReadActive] +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[SubscriptionDiscountView] + WHERE + [StartDate] <= GETUTCDATE() + AND [EndDate] >= GETUTCDATE() +END +GO + +-- Stored Procedures: ReadByStripeCouponId +CREATE OR ALTER PROCEDURE [dbo].[SubscriptionDiscount_ReadByStripeCouponId] + @StripeCouponId VARCHAR(50) +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[SubscriptionDiscountView] + WHERE + [StripeCouponId] = @StripeCouponId +END +GO