Implement the detail Subscription Discount Database Infrastructure

This commit is contained in:
Cy Okeke
2026-01-28 14:20:41 +01:00
parent 2a458807a5
commit 6d060776b0
12 changed files with 615 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
namespace Bit.Core.Billing.Enums;
/// <summary>
/// Defines the target audience for subscription discounts using an extensible strategy pattern.
/// Each audience type maps to specific eligibility rules implemented via IDiscountAudienceFilter.
/// </summary>
public enum DiscountAudienceType
{
/// <summary>
/// Discount applies to users who have never had a subscription before.
/// </summary>
UserHasNoPreviousSubscriptions = 0
}

View File

@@ -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<Guid>, 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<ValidationResult> Validate(ValidationContext validationContext)
{
if (EndDate < StartDate)
{
yield return new ValidationResult(
"EndDate must be greater than or equal to StartDate.",
new[] { nameof(EndDate) });
}
}
}

View File

@@ -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<SubscriptionDiscount, Guid>
{
/// <summary>
/// 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).
/// </summary>
/// <returns>A collection of active subscription discounts.</returns>
Task<ICollection<SubscriptionDiscount>> GetActiveDiscountsAsync();
/// <summary>
/// Retrieves a subscription discount by its Stripe coupon ID.
/// </summary>
/// <param name="stripeCouponId">The Stripe coupon ID to search for.</param>
/// <returns>The subscription discount if found; otherwise, null.</returns>
Task<SubscriptionDiscount?> GetByStripeCouponIdAsync(string stripeCouponId);
}

View File

@@ -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<SubscriptionDiscount, Guid>(
globalSettings.SqlServer.ConnectionString,
globalSettings.SqlServer.ReadOnlyConnectionString), ISubscriptionDiscountRepository
{
public async Task<ICollection<SubscriptionDiscount>> GetActiveDiscountsAsync()
{
using var sqlConnection = new SqlConnection(ReadOnlyConnectionString);
var results = await sqlConnection.QueryAsync<SubscriptionDiscount>(
"[dbo].[SubscriptionDiscount_ReadActive]",
commandType: CommandType.StoredProcedure);
return results.ToArray();
}
public async Task<SubscriptionDiscount?> GetByStripeCouponIdAsync(string stripeCouponId)
{
using var sqlConnection = new SqlConnection(ReadOnlyConnectionString);
var result = await sqlConnection.QueryFirstOrDefaultAsync<SubscriptionDiscount>(
"[dbo].[SubscriptionDiscount_ReadByStripeCouponId]",
new { StripeCouponId = stripeCouponId },
commandType: CommandType.StoredProcedure);
return result;
}
}

View File

@@ -2,6 +2,7 @@
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Organizations.Repositories; using Bit.Core.Billing.Organizations.Repositories;
using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Subscriptions.Repositories;
using Bit.Core.Dirt.Reports.Repositories; using Bit.Core.Dirt.Reports.Repositories;
using Bit.Core.Dirt.Repositories; using Bit.Core.Dirt.Repositories;
using Bit.Core.KeyManagement.Repositories; using Bit.Core.KeyManagement.Repositories;
@@ -65,6 +66,7 @@ public static class DapperServiceCollectionExtensions
services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>(); services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();
services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>(); services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>();
services.AddSingleton<IProviderInvoiceItemRepository, ProviderInvoiceItemRepository>(); services.AddSingleton<IProviderInvoiceItemRepository, ProviderInvoiceItemRepository>();
services.AddSingleton<ISubscriptionDiscountRepository, SubscriptionDiscountRepository>();
services.AddSingleton<INotificationRepository, NotificationRepository>(); services.AddSingleton<INotificationRepository, NotificationRepository>();
services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>(); services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>();
services services

View File

@@ -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<SubscriptionDiscount>
{
public void Configure(EntityTypeBuilder<SubscriptionDiscount> 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));
}
}

View File

@@ -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<Core.Billing.Subscriptions.Entities.SubscriptionDiscount, SubscriptionDiscount>().ReverseMap();
}
}

View File

@@ -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<SubscriptionDiscount, EFSubscriptionDiscount, Guid>(
serviceScopeFactory,
mapper,
context => context.SubscriptionDiscounts), ISubscriptionDiscountRepository
{
public async Task<ICollection<SubscriptionDiscount>> 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<List<SubscriptionDiscount>>(results);
}
public async Task<SubscriptionDiscount?> 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<SubscriptionDiscount>(result);
}
}

View File

@@ -2,6 +2,7 @@
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Organizations.Repositories; using Bit.Core.Billing.Organizations.Repositories;
using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Subscriptions.Repositories;
using Bit.Core.Dirt.Reports.Repositories; using Bit.Core.Dirt.Reports.Repositories;
using Bit.Core.Dirt.Repositories; using Bit.Core.Dirt.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
@@ -102,6 +103,7 @@ public static class EntityFrameworkServiceCollectionExtensions
services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>(); services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();
services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>(); services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>();
services.AddSingleton<IProviderInvoiceItemRepository, ProviderInvoiceItemRepository>(); services.AddSingleton<IProviderInvoiceItemRepository, ProviderInvoiceItemRepository>();
services.AddSingleton<ISubscriptionDiscountRepository, SubscriptionDiscountRepository>();
services.AddSingleton<INotificationRepository, NotificationRepository>(); services.AddSingleton<INotificationRepository, NotificationRepository>();
services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>(); services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>();
services services

View File

@@ -79,6 +79,7 @@ public class DatabaseContext : DbContext
public DbSet<WebAuthnCredential> WebAuthnCredentials { get; set; } public DbSet<WebAuthnCredential> WebAuthnCredentials { get; set; }
public DbSet<ProviderPlan> ProviderPlans { get; set; } public DbSet<ProviderPlan> ProviderPlans { get; set; }
public DbSet<ProviderInvoiceItem> ProviderInvoiceItems { get; set; } public DbSet<ProviderInvoiceItem> ProviderInvoiceItems { get; set; }
public DbSet<SubscriptionDiscount> SubscriptionDiscounts { get; set; }
public DbSet<Notification> Notifications { get; set; } public DbSet<Notification> Notifications { get; set; }
public DbSet<NotificationStatus> NotificationStatuses { get; set; } public DbSet<NotificationStatus> NotificationStatuses { get; set; }
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; } public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }

View File

@@ -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);
}
}

View File

@@ -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