mirror of
https://github.com/bitwarden/server
synced 2026-02-11 05:53:47 +00:00
[PM 30100][Server] Subscription Discount Database Infrastructure (#6936)
* Implement the detail Subscription Discount Database Infrastructure * Change string to string list * fix lint error * Create all missing database object definition files * Regenerate EF migrations with Designer files The previous migrations were missing .Designer.cs files. This commit: - Removes the incomplete migration files - Regenerates all three provider migrations (MySQL, Postgres, SQLite) with proper Designer files - Updates DatabaseContextModelSnapshot.cs for each provider 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix failing database * Resolve the lint warnings * Resolve the database failure * Fix the build Lint * resolve the dbops reviews * Add the default value --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
13
src/Core/Billing/Enums/DiscountAudienceType.cs
Normal file
13
src/Core/Billing/Enums/DiscountAudienceType.cs
Normal 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
|
||||
}
|
||||
@@ -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 ICollection<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) });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();
|
||||
services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>();
|
||||
services.AddSingleton<IProviderInvoiceItemRepository, ProviderInvoiceItemRepository>();
|
||||
services.AddSingleton<ISubscriptionDiscountRepository, SubscriptionDiscountRepository>();
|
||||
services.AddSingleton<INotificationRepository, NotificationRepository>();
|
||||
services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>();
|
||||
services
|
||||
|
||||
@@ -9,6 +9,7 @@ public abstract class BaseRepository
|
||||
static BaseRepository()
|
||||
{
|
||||
SqlMapper.AddTypeHandler(new DateTimeHandler());
|
||||
SqlMapper.AddTypeHandler(new JsonCollectionTypeHandler());
|
||||
}
|
||||
|
||||
public BaseRepository(string connectionString, string readOnlyConnectionString)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Data;
|
||||
using System.Text.Json;
|
||||
using Dapper;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Infrastructure.Dapper.Repositories;
|
||||
|
||||
public class JsonCollectionTypeHandler : SqlMapper.TypeHandler<ICollection<string>?>
|
||||
{
|
||||
public override void SetValue(IDbDataParameter parameter, ICollection<string>? value)
|
||||
{
|
||||
parameter.Value = value == null ? (object)DBNull.Value : JsonSerializer.Serialize(value);
|
||||
}
|
||||
|
||||
public override ICollection<string>? Parse(object value)
|
||||
{
|
||||
if (value == null || value is DBNull)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var json = value.ToString();
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<List<string>>(json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Infrastructure.EntityFramework.Billing.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
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();
|
||||
|
||||
builder
|
||||
.Property(sd => sd.StripeProductIds)
|
||||
.HasConversion(
|
||||
v => v == null ? null : JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
|
||||
v => v == null ? null : JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions?)null),
|
||||
new ValueComparer<ICollection<string>?>(
|
||||
(c1, c2) => (c1 == null && c2 == null) || (c1 != null && c2 != null && c1.SequenceEqual(c2)),
|
||||
c => c == null ? 0 : c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
|
||||
c => c == null ? null : c.ToList()));
|
||||
|
||||
builder
|
||||
.Property(sd => sd.PercentOff)
|
||||
.HasPrecision(5, 2);
|
||||
|
||||
builder
|
||||
.HasIndex(sd => new { sd.StartDate, sd.EndDate })
|
||||
.IsClustered(false)
|
||||
.HasDatabaseName("IX_SubscriptionDiscount_DateRange");
|
||||
|
||||
builder.ToTable(nameof(SubscriptionDiscount));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();
|
||||
services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>();
|
||||
services.AddSingleton<IProviderInvoiceItemRepository, ProviderInvoiceItemRepository>();
|
||||
services.AddSingleton<ISubscriptionDiscountRepository, SubscriptionDiscountRepository>();
|
||||
services.AddSingleton<INotificationRepository, NotificationRepository>();
|
||||
services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>();
|
||||
services
|
||||
|
||||
@@ -79,6 +79,7 @@ public class DatabaseContext : DbContext
|
||||
public DbSet<WebAuthnCredential> WebAuthnCredentials { get; set; }
|
||||
public DbSet<ProviderPlan> ProviderPlans { get; set; }
|
||||
public DbSet<ProviderInvoiceItem> ProviderInvoiceItems { get; set; }
|
||||
public DbSet<SubscriptionDiscount> SubscriptionDiscounts { get; set; }
|
||||
public DbSet<Notification> Notifications { get; set; }
|
||||
public DbSet<NotificationStatus> NotificationStatuses { get; set; }
|
||||
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
CREATE 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
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE PROCEDURE [dbo].[SubscriptionDiscount_DeleteById]
|
||||
@Id UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[SubscriptionDiscount]
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE PROCEDURE [dbo].[SubscriptionDiscount_ReadActive]
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[SubscriptionDiscountView]
|
||||
WHERE
|
||||
[StartDate] <= GETUTCDATE()
|
||||
AND [EndDate] >= GETUTCDATE()
|
||||
END
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE PROCEDURE [dbo].[SubscriptionDiscount_ReadById]
|
||||
@Id UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[SubscriptionDiscountView]
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE PROCEDURE [dbo].[SubscriptionDiscount_ReadByStripeCouponId]
|
||||
@StripeCouponId VARCHAR(50)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[SubscriptionDiscountView]
|
||||
WHERE
|
||||
[StripeCouponId] = @StripeCouponId
|
||||
END
|
||||
@@ -0,0 +1,38 @@
|
||||
CREATE 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
|
||||
22
src/Sql/dbo/Tables/SubscriptionDiscount.sql
Normal file
22
src/Sql/dbo/Tables/SubscriptionDiscount.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
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 CONSTRAINT [DF_SubscriptionDiscount_AudienceType] 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 NONCLUSTERED ([StripeCouponId] ASC)
|
||||
);
|
||||
|
||||
GO
|
||||
CREATE NONCLUSTERED INDEX [IX_SubscriptionDiscount_DateRange]
|
||||
ON [dbo].[SubscriptionDiscount]([StartDate] ASC, [EndDate] ASC);
|
||||
5
src/Sql/dbo/Views/SubscriptionDiscountView.sql
Normal file
5
src/Sql/dbo/Views/SubscriptionDiscountView.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
CREATE VIEW [dbo].[SubscriptionDiscountView]
|
||||
AS
|
||||
SELECT *
|
||||
FROM
|
||||
[dbo].[SubscriptionDiscount]
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Subscriptions.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Subscriptions.Entities;
|
||||
|
||||
public class SubscriptionDiscountTests
|
||||
{
|
||||
[Fact]
|
||||
public void StripeProductIds_CanSerializeToJson()
|
||||
{
|
||||
// Arrange
|
||||
var discount = new SubscriptionDiscount
|
||||
{
|
||||
StripeCouponId = "test-coupon",
|
||||
StripeProductIds = new List<string> { "prod_123", "prod_456" },
|
||||
Duration = "once",
|
||||
StartDate = DateTime.UtcNow,
|
||||
EndDate = DateTime.UtcNow.AddDays(30),
|
||||
AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(discount.StripeProductIds);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("[\"prod_123\",\"prod_456\"]", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripeProductIds_CanDeserializeFromJson()
|
||||
{
|
||||
// Arrange
|
||||
var json = "[\"prod_123\",\"prod_456\"]";
|
||||
|
||||
// Act
|
||||
var result = JsonSerializer.Deserialize<List<string>>(json);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains("prod_123", result);
|
||||
Assert.Contains("prod_456", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripeProductIds_HandlesNull()
|
||||
{
|
||||
// Arrange
|
||||
var discount = new SubscriptionDiscount
|
||||
{
|
||||
StripeCouponId = "test-coupon",
|
||||
StripeProductIds = null,
|
||||
Duration = "once",
|
||||
StartDate = DateTime.UtcNow,
|
||||
EndDate = DateTime.UtcNow.AddDays(30),
|
||||
AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(discount.StripeProductIds);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("null", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripeProductIds_HandlesEmptyCollection()
|
||||
{
|
||||
// Arrange
|
||||
var discount = new SubscriptionDiscount
|
||||
{
|
||||
StripeCouponId = "test-coupon",
|
||||
StripeProductIds = new List<string>(),
|
||||
Duration = "once",
|
||||
StartDate = DateTime.UtcNow,
|
||||
EndDate = DateTime.UtcNow.AddDays(30),
|
||||
AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(discount.StripeProductIds);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("[]", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_RejectsEndDateBeforeStartDate()
|
||||
{
|
||||
// Arrange
|
||||
var discount = new SubscriptionDiscount
|
||||
{
|
||||
StripeCouponId = "test-coupon",
|
||||
Duration = "once",
|
||||
StartDate = DateTime.UtcNow.AddDays(30),
|
||||
EndDate = DateTime.UtcNow, // EndDate before StartDate
|
||||
AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions
|
||||
};
|
||||
|
||||
// Act
|
||||
var validationResults = discount.Validate(new System.ComponentModel.DataAnnotations.ValidationContext(discount)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(validationResults);
|
||||
Assert.Contains("EndDate", validationResults[0].MemberNames);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
ICollection<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: new List<string> { "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: new List<string> { "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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
-- 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 CONSTRAINT [DF_SubscriptionDiscount_AudienceType] 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]);
|
||||
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
|
||||
3570
util/MySqlMigrations/Migrations/20260203083629_AddSubscriptionDiscountTable.Designer.cs
generated
Normal file
3570
util/MySqlMigrations/Migrations/20260203083629_AddSubscriptionDiscountTable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.MySqlMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddSubscriptionDiscountTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SubscriptionDiscount",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
StripeCouponId = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
StripeProductIds = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
PercentOff = table.Column<decimal>(type: "decimal(5,2)", nullable: true),
|
||||
AmountOff = table.Column<long>(type: "bigint", nullable: true),
|
||||
Currency = table.Column<string>(type: "varchar(10)", maxLength: 10, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Duration = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
DurationInMonths = table.Column<int>(type: "int", nullable: true),
|
||||
Name = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
StartDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
EndDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
AudienceType = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
CreationDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
RevisionDate = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SubscriptionDiscount", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SubscriptionDiscount_DateRange",
|
||||
table: "SubscriptionDiscount",
|
||||
columns: new[] { "StartDate", "EndDate" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SubscriptionDiscount_StripeCouponId",
|
||||
table: "SubscriptionDiscount",
|
||||
column: "StripeCouponId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SubscriptionDiscount");
|
||||
}
|
||||
}
|
||||
@@ -917,6 +917,70 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
b.ToTable("ProviderPlan", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.SubscriptionDiscount", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<long?>("AmountOff")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("AudienceType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("Duration")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<int?>("DurationInMonths")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("EndDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<decimal?>("PercentOff")
|
||||
.HasPrecision(5, 2)
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
b.Property<DateTime>("RevisionDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("StripeCouponId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("StripeProductIds")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StripeCouponId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("StartDate", "EndDate")
|
||||
.HasDatabaseName("IX_SubscriptionDiscount_DateRange")
|
||||
.HasAnnotation("SqlServer:Clustered", false)
|
||||
.HasAnnotation("SqlServer:Include", new[] { "StripeProductIds", "AudienceType" });
|
||||
|
||||
b.ToTable("SubscriptionDiscount", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
||||
3576
util/PostgresMigrations/Migrations/20260203083641_AddSubscriptionDiscountTable.Designer.cs
generated
Normal file
3576
util/PostgresMigrations/Migrations/20260203083641_AddSubscriptionDiscountTable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.PostgresMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddSubscriptionDiscountTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SubscriptionDiscount",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
StripeCouponId = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
StripeProductIds = table.Column<string>(type: "text", nullable: true),
|
||||
PercentOff = table.Column<decimal>(type: "numeric(5,2)", nullable: true),
|
||||
AmountOff = table.Column<long>(type: "bigint", nullable: true),
|
||||
Currency = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: true),
|
||||
Duration = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
DurationInMonths = table.Column<int>(type: "integer", nullable: true),
|
||||
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
StartDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
EndDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
AudienceType = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
||||
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
RevisionDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SubscriptionDiscount", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SubscriptionDiscount_DateRange",
|
||||
table: "SubscriptionDiscount",
|
||||
columns: new[] { "StartDate", "EndDate" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SubscriptionDiscount_StripeCouponId",
|
||||
table: "SubscriptionDiscount",
|
||||
column: "StripeCouponId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SubscriptionDiscount");
|
||||
}
|
||||
}
|
||||
@@ -922,6 +922,70 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
b.ToTable("ProviderPlan", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.SubscriptionDiscount", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<long?>("AmountOff")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("AudienceType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<string>("Duration")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<int?>("DurationInMonths")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("EndDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<decimal?>("PercentOff")
|
||||
.HasPrecision(5, 2)
|
||||
.HasColumnType("numeric(5,2)");
|
||||
|
||||
b.Property<DateTime>("RevisionDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("StripeCouponId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("StripeProductIds")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StripeCouponId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("StartDate", "EndDate")
|
||||
.HasDatabaseName("IX_SubscriptionDiscount_DateRange")
|
||||
.HasAnnotation("SqlServer:Clustered", false)
|
||||
.HasAnnotation("SqlServer:Include", new[] { "StripeProductIds", "AudienceType" });
|
||||
|
||||
b.ToTable("SubscriptionDiscount", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
||||
3558
util/SqliteMigrations/Migrations/20260203083654_AddSubscriptionDiscountTable.Designer.cs
generated
Normal file
3558
util/SqliteMigrations/Migrations/20260203083654_AddSubscriptionDiscountTable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.SqliteMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddSubscriptionDiscountTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SubscriptionDiscount",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
StripeCouponId = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
|
||||
StripeProductIds = table.Column<string>(type: "TEXT", nullable: true),
|
||||
PercentOff = table.Column<decimal>(type: "TEXT", nullable: true),
|
||||
AmountOff = table.Column<long>(type: "INTEGER", nullable: true),
|
||||
Currency = table.Column<string>(type: "TEXT", maxLength: 10, nullable: true),
|
||||
Duration = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
|
||||
DurationInMonths = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
StartDate = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
EndDate = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
AudienceType = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
|
||||
CreationDate = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
RevisionDate = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SubscriptionDiscount", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SubscriptionDiscount_DateRange",
|
||||
table: "SubscriptionDiscount",
|
||||
columns: new[] { "StartDate", "EndDate" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SubscriptionDiscount_StripeCouponId",
|
||||
table: "SubscriptionDiscount",
|
||||
column: "StripeCouponId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SubscriptionDiscount");
|
||||
}
|
||||
}
|
||||
@@ -906,6 +906,69 @@ namespace Bit.SqliteMigrations.Migrations
|
||||
b.ToTable("ProviderPlan", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.SubscriptionDiscount", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("AmountOff")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AudienceType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Duration")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("DurationInMonths")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("EndDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal?>("PercentOff")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("RevisionDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StripeCouponId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StripeProductIds")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StripeCouponId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("StartDate", "EndDate")
|
||||
.HasDatabaseName("IX_SubscriptionDiscount_DateRange")
|
||||
.HasAnnotation("SqlServer:Clustered", false)
|
||||
.HasAnnotation("SqlServer:Include", new[] { "StripeProductIds", "AudienceType" });
|
||||
|
||||
b.ToTable("SubscriptionDiscount", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
||||
Reference in New Issue
Block a user