1
0
mirror of https://github.com/bitwarden/server.git synced 2025-03-11 13:19:40 +01:00

Merge branch 'main' into PM-17732

This commit is contained in:
cd-bitwarden 2025-03-06 10:07:25 -05:00 committed by GitHub
commit 2702a8a66a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 9370 additions and 49 deletions

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2025.2.2</Version>
<Version>2025.2.3</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>

View File

@ -475,20 +475,17 @@ public class ProviderBillingService(
Provider provider,
TaxInfo taxInfo)
{
ArgumentNullException.ThrowIfNull(provider);
ArgumentNullException.ThrowIfNull(taxInfo);
if (string.IsNullOrEmpty(taxInfo.BillingAddressCountry) ||
string.IsNullOrEmpty(taxInfo.BillingAddressPostalCode))
if (taxInfo is not
{
BillingAddressCountry: not null and not "",
BillingAddressPostalCode: not null and not ""
})
{
logger.LogError("Cannot create customer for provider ({ProviderID}) without both a country and postal code", provider.Id);
throw new BillingException();
}
var providerDisplayName = provider.DisplayName();
var customerCreateOptions = new CustomerCreateOptions
var options = new CustomerCreateOptions
{
Address = new AddressOptions
{
@ -508,9 +505,9 @@ public class ProviderBillingService(
new CustomerInvoiceSettingsCustomFieldOptions
{
Name = provider.SubscriberType(),
Value = providerDisplayName?.Length <= 30
? providerDisplayName
: providerDisplayName?[..30]
Value = provider.DisplayName()?.Length <= 30
? provider.DisplayName()
: provider.DisplayName()?[..30]
}
]
},
@ -522,7 +519,8 @@ public class ProviderBillingService(
if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber))
{
var taxIdType = taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry,
var taxIdType = taxService.GetStripeTaxCode(
taxInfo.BillingAddressCountry,
taxInfo.TaxIdNumber);
if (taxIdType == null)
@ -533,15 +531,20 @@ public class ProviderBillingService(
throw new BadRequestException("billingTaxIdTypeInferenceError");
}
customerCreateOptions.TaxIdData =
options.TaxIdData =
[
new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber }
];
}
if (!string.IsNullOrEmpty(provider.DiscountId))
{
options.Coupon = provider.DiscountId;
}
try
{
return await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
return await stripeAdapter.CustomerCreateAsync(options);
}
catch (StripeException stripeException) when (stripeException.StripeError?.Code == StripeConstants.ErrorCodes.TaxIdInvalid)
{

View File

@ -731,18 +731,6 @@ public class ProviderBillingServiceTests
#region SetupCustomer
[Theory, BitAutoData]
public async Task SetupCustomer_NullProvider_ThrowsArgumentNullException(
SutProvider<ProviderBillingService> sutProvider,
TaxInfo taxInfo) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.SetupCustomer(null, taxInfo));
[Theory, BitAutoData]
public async Task SetupCustomer_NullTaxInfo_ThrowsArgumentNullException(
SutProvider<ProviderBillingService> sutProvider,
Provider provider) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.SetupCustomer(provider, null));
[Theory, BitAutoData]
public async Task SetupCustomer_MissingCountry_ContactSupport(
SutProvider<ProviderBillingService> sutProvider,

View File

@ -10,6 +10,9 @@ public class CreateMspProviderModel : IValidatableObject
[Display(Name = "Owner Email")]
public string OwnerEmail { get; set; }
[Display(Name = "Subscription Discount")]
public string DiscountId { get; set; }
[Display(Name = "Teams (Monthly) Seat Minimum")]
public int TeamsMonthlySeatMinimum { get; set; }
@ -20,7 +23,8 @@ public class CreateMspProviderModel : IValidatableObject
{
return new Provider
{
Type = ProviderType.Msp
Type = ProviderType.Msp,
DiscountId = DiscountId
};
}

View File

@ -1,3 +1,4 @@
@using Bit.Core.Billing.Constants
@model CreateMspProviderModel
@{
@ -12,6 +13,19 @@
<label asp-for="OwnerEmail" class="form-label"></label>
<input type="text" class="form-control" asp-for="OwnerEmail">
</div>
<div class="mb-3">
@{
var selectList = new List<SelectListItem>
{
new ("No discount", string.Empty, true),
new ("20% - Open", StripeConstants.CouponIDs.MSPDiscounts.Open),
new ("35% - Silver", StripeConstants.CouponIDs.MSPDiscounts.Silver),
new ("50% - Gold", StripeConstants.CouponIDs.MSPDiscounts.Gold)
};
}
<label asp-for="DiscountId" class="form-label"></label>
<select class="form-select" asp-for="DiscountId" asp-items="selectList"></select>
</div>
<div class="row">
<div class="col-sm">
<div class="mb-3">

View File

@ -41,10 +41,15 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler
return;
}
if (organizationId.HasValue &&
subscription.CancellationDetails.Comment != providerMigrationCancellationComment &&
!subscription.CancellationDetails.Comment.Contains(addedToProviderCancellationComment))
if (organizationId.HasValue)
{
if (!string.IsNullOrEmpty(subscription.CancellationDetails?.Comment) &&
(subscription.CancellationDetails.Comment == providerMigrationCancellationComment ||
subscription.CancellationDetails.Comment.Contains(addedToProviderCancellationComment)))
{
return;
}
await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
}
else if (userId.HasValue)

View File

@ -35,6 +35,7 @@ public class Provider : ITableObject<Guid>, ISubscriber
public GatewayType? Gateway { get; set; }
public string? GatewayCustomerId { get; set; }
public string? GatewaySubscriptionId { get; set; }
public string? DiscountId { get; set; }
public string? BillingEmailAddress() => BillingEmail?.ToLowerInvariant().Trim();

View File

@ -18,8 +18,15 @@ public static class StripeConstants
public static class CouponIDs
{
public const string MSPDiscount35 = "msp-discount-35";
public const string LegacyMSPDiscount = "msp-discount-35";
public const string SecretsManagerStandalone = "sm-standalone";
public static class MSPDiscounts
{
public const string Open = "msp-open-discount";
public const string Silver = "msp-silver-discount";
public const string Gold = "msp-gold-discount";
}
}
public static class ErrorCodes

View File

@ -254,7 +254,7 @@ public class ProviderMigrator(
await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
{
Coupon = StripeConstants.CouponIDs.MSPDiscount35
Coupon = StripeConstants.CouponIDs.LegacyMSPDiscount
});
provider.GatewayCustomerId = customer.Id;

View File

@ -46,7 +46,8 @@ public class OrganizationSale
var customerSetup = new CustomerSetup
{
Coupon = signup.IsFromProvider
? StripeConstants.CouponIDs.MSPDiscount35
// TODO: Remove when last of the legacy providers has been migrated.
? StripeConstants.CouponIDs.LegacyMSPDiscount
: signup.IsFromSecretsManagerTrial
? StripeConstants.CouponIDs.SecretsManagerStandalone
: null

View File

@ -119,7 +119,6 @@ public static class FeatureFlagKeys
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
public const string DuoRedirect = "duo-redirect";
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
public const string EmailVerification = "email-verification";
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
public const string ExtensionRefresh = "extension-refresh";

View File

@ -1609,15 +1609,12 @@ public class StripePaymentService : IPaymentService
{
subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub);
if (_featureService.IsEnabled(FeatureFlagKeys.AC1795_UpdatedSubscriptionStatusSection))
{
var (suspensionDate, unpaidPeriodEndDate) = await GetSuspensionDateAsync(sub);
var (suspensionDate, unpaidPeriodEndDate) = await GetSuspensionDateAsync(sub);
if (suspensionDate.HasValue && unpaidPeriodEndDate.HasValue)
{
subscriptionInfo.Subscription.SuspensionDate = suspensionDate;
subscriptionInfo.Subscription.UnpaidPeriodEndDate = unpaidPeriodEndDate;
}
if (suspensionDate.HasValue && unpaidPeriodEndDate.HasValue)
{
subscriptionInfo.Subscription.SuspensionDate = suspensionDate;
subscriptionInfo.Subscription.UnpaidPeriodEndDate = unpaidPeriodEndDate;
}
}

View File

@ -17,7 +17,8 @@
@RevisionDate DATETIME2(7),
@Gateway TINYINT = 0,
@GatewayCustomerId VARCHAR(50) = NULL,
@GatewaySubscriptionId VARCHAR(50) = NULL
@GatewaySubscriptionId VARCHAR(50) = NULL,
@DiscountId VARCHAR(50) = NULL
AS
BEGIN
SET NOCOUNT ON
@ -42,7 +43,8 @@ BEGIN
[RevisionDate],
[Gateway],
[GatewayCustomerId],
[GatewaySubscriptionId]
[GatewaySubscriptionId],
[DiscountId]
)
VALUES
(
@ -64,6 +66,7 @@ BEGIN
@RevisionDate,
@Gateway,
@GatewayCustomerId,
@GatewaySubscriptionId
@GatewaySubscriptionId,
@DiscountId
)
END

View File

@ -17,7 +17,8 @@
@RevisionDate DATETIME2(7),
@Gateway TINYINT = 0,
@GatewayCustomerId VARCHAR(50) = NULL,
@GatewaySubscriptionId VARCHAR(50) = NULL
@GatewaySubscriptionId VARCHAR(50) = NULL,
@DiscountId VARCHAR(50) = NULL
AS
BEGIN
SET NOCOUNT ON
@ -42,7 +43,8 @@ BEGIN
[RevisionDate] = @RevisionDate,
[Gateway] = @Gateway,
[GatewayCustomerId] = @GatewayCustomerId,
[GatewaySubscriptionId] = @GatewaySubscriptionId
[GatewaySubscriptionId] = @GatewaySubscriptionId,
[DiscountId] = @DiscountId
WHERE
[Id] = @Id
END

View File

@ -18,5 +18,6 @@
[Gateway] TINYINT NULL,
[GatewayCustomerId] VARCHAR (50) NULL,
[GatewaySubscriptionId] VARCHAR (50) NULL,
[DiscountId] VARCHAR (50) NULL,
CONSTRAINT [PK_Provider] PRIMARY KEY CLUSTERED ([Id] ASC)
);

View File

@ -0,0 +1,171 @@
-- Add 'DiscountId' column to 'Provider' table.
IF COL_LENGTH('[dbo].[Provider]', 'DiscountId') IS NULL
BEGIN
ALTER TABLE
[dbo].[Provider]
ADD
[DiscountId] VARCHAR(50) NULL;
END
GO
-- Recreate 'ProviderView' so that it includes the 'DiscountId' column.
CREATE OR ALTER VIEW [dbo].[ProviderView]
AS
SELECT
*
FROM
[dbo].[Provider]
GO
-- Alter 'Provider_Create' SPROC to add 'DiscountId' column.
CREATE OR ALTER PROCEDURE [dbo].[Provider_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@Name NVARCHAR(50),
@BusinessName NVARCHAR(50),
@BusinessAddress1 NVARCHAR(50),
@BusinessAddress2 NVARCHAR(50),
@BusinessAddress3 NVARCHAR(50),
@BusinessCountry VARCHAR(2),
@BusinessTaxNumber NVARCHAR(30),
@BillingEmail NVARCHAR(256),
@BillingPhone NVARCHAR(50) = NULL,
@Status TINYINT,
@Type TINYINT = 0,
@UseEvents BIT,
@Enabled BIT,
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@Gateway TINYINT = 0,
@GatewayCustomerId VARCHAR(50) = NULL,
@GatewaySubscriptionId VARCHAR(50) = NULL,
@DiscountId VARCHAR(50) = NULL
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[Provider]
(
[Id],
[Name],
[BusinessName],
[BusinessAddress1],
[BusinessAddress2],
[BusinessAddress3],
[BusinessCountry],
[BusinessTaxNumber],
[BillingEmail],
[BillingPhone],
[Status],
[Type],
[UseEvents],
[Enabled],
[CreationDate],
[RevisionDate],
[Gateway],
[GatewayCustomerId],
[GatewaySubscriptionId],
[DiscountId]
)
VALUES
(
@Id,
@Name,
@BusinessName,
@BusinessAddress1,
@BusinessAddress2,
@BusinessAddress3,
@BusinessCountry,
@BusinessTaxNumber,
@BillingEmail,
@BillingPhone,
@Status,
@Type,
@UseEvents,
@Enabled,
@CreationDate,
@RevisionDate,
@Gateway,
@GatewayCustomerId,
@GatewaySubscriptionId,
@DiscountId
)
END
GO
-- Alter 'Provider_Update' SPROC to add 'DiscountId' column.
CREATE OR ALTER PROCEDURE [dbo].[Provider_Update]
@Id UNIQUEIDENTIFIER,
@Name NVARCHAR(50),
@BusinessName NVARCHAR(50),
@BusinessAddress1 NVARCHAR(50),
@BusinessAddress2 NVARCHAR(50),
@BusinessAddress3 NVARCHAR(50),
@BusinessCountry VARCHAR(2),
@BusinessTaxNumber NVARCHAR(30),
@BillingEmail NVARCHAR(256),
@BillingPhone NVARCHAR(50) = NULL,
@Status TINYINT,
@Type TINYINT = 0,
@UseEvents BIT,
@Enabled BIT,
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@Gateway TINYINT = 0,
@GatewayCustomerId VARCHAR(50) = NULL,
@GatewaySubscriptionId VARCHAR(50) = NULL,
@DiscountId VARCHAR(50) = NULL
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[Provider]
SET
[Name] = @Name,
[BusinessName] = @BusinessName,
[BusinessAddress1] = @BusinessAddress1,
[BusinessAddress2] = @BusinessAddress2,
[BusinessAddress3] = @BusinessAddress3,
[BusinessCountry] = @BusinessCountry,
[BusinessTaxNumber] = @BusinessTaxNumber,
[BillingEmail] = @BillingEmail,
[BillingPhone] = @BillingPhone,
[Status] = @Status,
[Type] = @Type,
[UseEvents] = @UseEvents,
[Enabled] = @Enabled,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[Gateway] = @Gateway,
[GatewayCustomerId] = @GatewayCustomerId,
[GatewaySubscriptionId] = @GatewaySubscriptionId,
[DiscountId] = @DiscountId
WHERE
[Id] = @Id
END
GO
-- Refresh modules for SPROCs reliant on 'Provider' table/view.
IF OBJECT_ID('[dbo].[Provider_ReadAbilities]') IS NOT NULL
BEGIN
EXECUTE sp_refreshsqlmodule N'[dbo].[Provider_ReadAbilities]';
END
GO
IF OBJECT_ID('[dbo].[Provider_ReadById]') IS NOT NULL
BEGIN
EXECUTE sp_refreshsqlmodule N'[dbo].[Provider_ReadById]';
END
GO
IF OBJECT_ID('[dbo].[Provider_ReadByOrganizationId]') IS NOT NULL
BEGIN
EXECUTE sp_refreshsqlmodule N'[dbo].[Provider_ReadByOrganizationId]';
END
GO
IF OBJECT_ID('[dbo].[Provider_Search]') IS NOT NULL
BEGIN
EXECUTE sp_refreshsqlmodule N'[dbo].[Provider_Search]';
END
GO

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.MySqlMigrations.Migrations;
/// <inheritdoc />
public partial class AddColumn_ProviderDiscountId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "DiscountId",
table: "Provider",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DiscountId",
table: "Provider");
}
}

View File

@ -284,6 +284,9 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<DateTime>("CreationDate")
.HasColumnType("datetime(6)");
b.Property<string>("DiscountId")
.HasColumnType("longtext");
b.Property<bool>("Enabled")
.HasColumnType("tinyint(1)");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.PostgresMigrations.Migrations;
/// <inheritdoc />
public partial class AddColumn_ProviderDiscountId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "DiscountId",
table: "Provider",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DiscountId",
table: "Provider");
}
}

View File

@ -287,6 +287,9 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<DateTime>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("DiscountId")
.HasColumnType("text");
b.Property<bool>("Enabled")
.HasColumnType("boolean");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.SqliteMigrations.Migrations;
/// <inheritdoc />
public partial class AddColumn_ProviderDiscountId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "DiscountId",
table: "Provider",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DiscountId",
table: "Provider");
}
}

View File

@ -279,6 +279,9 @@ namespace Bit.SqliteMigrations.Migrations
b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");
b.Property<string>("DiscountId")
.HasColumnType("TEXT");
b.Property<bool>("Enabled")
.HasColumnType("INTEGER");