1
0
mirror of https://github.com/bitwarden/server synced 2026-01-09 03:53:42 +00:00

Merge branch 'master' into flexible-collections/deprecate-custom-collection-perm

This commit is contained in:
Rui Tome
2023-11-02 14:52:14 +00:00
45 changed files with 1481 additions and 178 deletions

View File

@@ -24,6 +24,20 @@ public static class Constants
public const string Fido2KeyCipherMinimumVersion = "2023.10.0";
public const string CipherKeyEncryptionMinimumVersion = "2023.9.2";
/// <summary>
/// When you set the ProrationBehavior to create_prorations,
/// Stripe will automatically create prorations for any changes made to the subscription,
/// such as changing the plan, adding or removing quantities, or applying discounts.
/// </summary>
public const string CreateProrations = "create_prorations";
/// <summary>
/// When you set the ProrationBehavior to always_invoice,
/// Stripe will always generate an invoice when a subscription update occurs,
/// regardless of whether there is a proration or not.
/// </summary>
public const string AlwaysInvoice = "always_invoice";
}
public static class TokenPurposes
@@ -49,6 +63,7 @@ public static class FeatureFlagKeys
public const string BulkCollectionAccess = "bulk-collection-access";
public const string AutofillOverlay = "autofill-overlay";
public const string ItemShare = "item-share";
public const string BillingPlansUpgrade = "billing-plans-upgrade";
public static List<string> GetAllKeys()
{

View File

@@ -20,12 +20,20 @@ public enum PlanType : byte
Custom = 6,
[Display(Name = "Families")]
FamiliesAnnually = 7,
[Display(Name = "Teams (Monthly) 2020")]
TeamsMonthly2020 = 8,
[Display(Name = "Teams (Annually) 2020")]
TeamsAnnually2020 = 9,
[Display(Name = "Enterprise (Monthly) 2020")]
EnterpriseMonthly2020 = 10,
[Display(Name = "Enterprise (Annually) 2020")]
EnterpriseAnnually2020 = 11,
[Display(Name = "Teams (Monthly)")]
TeamsMonthly = 8,
TeamsMonthly = 12,
[Display(Name = "Teams (Annually)")]
TeamsAnnually = 9,
TeamsAnnually = 13,
[Display(Name = "Enterprise (Monthly)")]
EnterpriseMonthly = 10,
EnterpriseMonthly = 14,
[Display(Name = "Enterprise (Annually)")]
EnterpriseAnnually = 11,
EnterpriseAnnually = 15,
}

View File

@@ -0,0 +1,7 @@
namespace Bit.Core.Models.Business;
public class InvoicePreviewResult
{
public bool IsInvoicedNow { get; set; }
public string PaymentIntentClientSecret { get; set; }
}

View File

@@ -184,8 +184,11 @@ public class OrganizationLicense : ILicense
(Version >= 11 || !p.Name.Equals(nameof(UseCustomPermissions))) &&
// ExpirationWithoutGracePeriod was added in Version 12
(Version >= 12 || !p.Name.Equals(nameof(ExpirationWithoutGracePeriod))) &&
// UseSecretsManager was added in Version 13
// UseSecretsManager, UsePasswordManager, SmSeats, and SmServiceAccounts were added in Version 13
(Version >= 13 || !p.Name.Equals(nameof(UseSecretsManager))) &&
(Version >= 13 || !p.Name.Equals(nameof(UsePasswordManager))) &&
(Version >= 13 || !p.Name.Equals(nameof(SmSeats))) &&
(Version >= 13 || !p.Name.Equals(nameof(SmServiceAccounts))) &&
(
!forHash ||
(

View File

@@ -0,0 +1,9 @@
using Stripe;
namespace Bit.Core.Models.Business;
public class PendingInoviceItems
{
public IEnumerable<InvoiceItem> PendingInvoiceItems { get; set; }
public IDictionary<string, InvoiceItem> PendingInvoiceItemsDict { get; set; }
}

View File

@@ -44,7 +44,7 @@ public class SecretsManagerSubscribeUpdate : SubscriptionUpdate
{
updatedItems.Add(new SubscriptionItemOptions
{
Price = _plan.SecretsManager.StripeSeatPlanId,
Plan = _plan.SecretsManager.StripeSeatPlanId,
Quantity = _additionalSeats
});
}
@@ -53,7 +53,7 @@ public class SecretsManagerSubscribeUpdate : SubscriptionUpdate
{
updatedItems.Add(new SubscriptionItemOptions
{
Price = _plan.SecretsManager.StripeServiceAccountPlanId,
Plan = _plan.SecretsManager.StripeServiceAccountPlanId,
Quantity = _additionalServiceAccounts
});
}
@@ -63,14 +63,14 @@ public class SecretsManagerSubscribeUpdate : SubscriptionUpdate
{
updatedItems.Add(new SubscriptionItemOptions
{
Price = _plan.SecretsManager.StripeSeatPlanId,
Plan = _plan.SecretsManager.StripeSeatPlanId,
Quantity = _previousSeats,
Deleted = _previousSeats == 0 ? true : (bool?)null,
});
updatedItems.Add(new SubscriptionItemOptions
{
Price = _plan.SecretsManager.StripeServiceAccountPlanId,
Plan = _plan.SecretsManager.StripeServiceAccountPlanId,
Quantity = _previousServiceAccounts,
Deleted = _previousServiceAccounts == 0 ? true : (bool?)null,
});

View File

@@ -28,7 +28,7 @@ public abstract record Plan
public bool HasCustomPermissions { get; protected init; }
public int UpgradeSortOrder { get; protected init; }
public int DisplaySortOrder { get; protected init; }
public int? LegacyYear { get; protected init; }
public int? LegacyYear { get; set; }
public bool Disabled { get; protected init; }
public PasswordManagerPlanFeatures PasswordManager { get; protected init; }
public SecretsManagerPlanFeatures SecretsManager { get; protected init; }

View File

@@ -24,6 +24,10 @@ public record Enterprise2019Plan : Models.StaticStore.Plan
HasTotp = true;
Has2fa = true;
HasApi = true;
HasSso = true;
HasKeyConnector = true;
HasScim = true;
HasResetPassword = true;
UsersGetPremium = true;
HasCustomPermissions = true;
@@ -31,9 +35,41 @@ public record Enterprise2019Plan : Models.StaticStore.Plan
DisplaySortOrder = 3;
LegacyYear = 2020;
SecretsManager = new Enterprise2019SecretsManagerFeatures(isAnnual);
PasswordManager = new Enterprise2019PasswordManagerFeatures(isAnnual);
}
private record Enterprise2019SecretsManagerFeatures : SecretsManagerPlanFeatures
{
public Enterprise2019SecretsManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BasePrice = 0;
BaseServiceAccount = 200;
HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true;
AllowSeatAutoscale = true;
AllowServiceAccountsAutoscale = true;
if (isAnnual)
{
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
SeatPrice = 144;
AdditionalPricePerServiceAccount = 6;
}
else
{
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
SeatPrice = 13;
AdditionalPricePerServiceAccount = 0.5M;
}
}
}
private record Enterprise2019PasswordManagerFeatures : PasswordManagerPlanFeatures
{
public Enterprise2019PasswordManagerFeatures(bool isAnnual)

View File

@@ -0,0 +1,101 @@
using Bit.Core.Enums;
namespace Bit.Core.Models.StaticStore.Plans;
public record Enterprise2020Plan : Models.StaticStore.Plan
{
public Enterprise2020Plan(bool isAnnual)
{
Type = isAnnual ? PlanType.EnterpriseAnnually2020 : PlanType.EnterpriseMonthly2020;
Product = ProductType.Enterprise;
Name = isAnnual ? "Enterprise (Annually) 2020" : "Enterprise (Monthly) 2020";
IsAnnual = isAnnual;
NameLocalizationKey = "planNameEnterprise";
DescriptionLocalizationKey = "planDescEnterprise";
CanBeUsedByBusiness = true;
TrialPeriodDays = 7;
HasPolicies = true;
HasSelfHost = true;
HasGroups = true;
HasDirectory = true;
HasEvents = true;
HasTotp = true;
Has2fa = true;
HasApi = true;
HasSso = true;
HasKeyConnector = true;
HasScim = true;
HasResetPassword = true;
UsersGetPremium = true;
HasCustomPermissions = true;
UpgradeSortOrder = 3;
DisplaySortOrder = 3;
LegacyYear = 2023;
PasswordManager = new Enterprise2020PasswordManagerFeatures(isAnnual);
SecretsManager = new Enterprise2020SecretsManagerFeatures(isAnnual);
}
private record Enterprise2020SecretsManagerFeatures : SecretsManagerPlanFeatures
{
public Enterprise2020SecretsManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BasePrice = 0;
BaseServiceAccount = 200;
HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true;
AllowSeatAutoscale = true;
AllowServiceAccountsAutoscale = true;
if (isAnnual)
{
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
SeatPrice = 144;
AdditionalPricePerServiceAccount = 6;
}
else
{
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
SeatPrice = 13;
AdditionalPricePerServiceAccount = 0.5M;
}
}
}
private record Enterprise2020PasswordManagerFeatures : PasswordManagerPlanFeatures
{
public Enterprise2020PasswordManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BaseStorageGb = 1;
HasAdditionalStorageOption = true;
HasAdditionalSeatsOption = true;
AllowSeatAutoscale = true;
if (isAnnual)
{
AdditionalStoragePricePerGb = 4;
StripeStoragePlanId = "storage-gb-annually";
StripeSeatPlanId = "2020-enterprise-org-seat-annually";
SeatPrice = 60;
}
else
{
StripeSeatPlanId = "2020-enterprise-seat-monthly";
StripeStoragePlanId = "storage-gb-monthly";
SeatPrice = 6;
AdditionalStoragePricePerGb = 0.5M;
}
}
}
}

View File

@@ -85,14 +85,14 @@ public record EnterprisePlan : Models.StaticStore.Plan
{
AdditionalStoragePricePerGb = 4;
StripeStoragePlanId = "storage-gb-annually";
StripeSeatPlanId = "2020-enterprise-org-seat-annually";
SeatPrice = 60;
StripeSeatPlanId = "2023-enterprise-org-seat-annually";
SeatPrice = 72;
}
else
{
StripeSeatPlanId = "2020-enterprise-seat-monthly";
StripeSeatPlanId = "2023-enterprise-seat-monthly";
StripeStoragePlanId = "storage-gb-monthly";
SeatPrice = 6;
SeatPrice = 7;
AdditionalStoragePricePerGb = 0.5M;
}
}

View File

@@ -17,6 +17,7 @@ public record Families2019Plan : Models.StaticStore.Plan
HasSelfHost = true;
HasTotp = true;
UsersGetPremium = true;
UpgradeSortOrder = 1;
DisplaySortOrder = 1;

View File

@@ -16,15 +16,53 @@ public record Teams2019Plan : Models.StaticStore.Plan
TrialPeriodDays = 7;
HasGroups = true;
HasDirectory = true;
HasEvents = true;
HasTotp = true;
Has2fa = true;
HasApi = true;
UsersGetPremium = true;
UpgradeSortOrder = 2;
DisplaySortOrder = 2;
LegacyYear = 2020;
SecretsManager = new Teams2019SecretsManagerFeatures(isAnnual);
PasswordManager = new Teams2019PasswordManagerFeatures(isAnnual);
}
private record Teams2019SecretsManagerFeatures : SecretsManagerPlanFeatures
{
public Teams2019SecretsManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BasePrice = 0;
BaseServiceAccount = 50;
HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true;
AllowSeatAutoscale = true;
AllowServiceAccountsAutoscale = true;
if (isAnnual)
{
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
SeatPrice = 72;
AdditionalPricePerServiceAccount = 6;
}
else
{
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
SeatPrice = 7;
AdditionalPricePerServiceAccount = 0.5M;
}
}
}
private record Teams2019PasswordManagerFeatures : PasswordManagerPlanFeatures
{
public Teams2019PasswordManagerFeatures(bool isAnnual)

View File

@@ -0,0 +1,95 @@
using Bit.Core.Enums;
namespace Bit.Core.Models.StaticStore.Plans;
public record Teams2020Plan : Models.StaticStore.Plan
{
public Teams2020Plan(bool isAnnual)
{
Type = isAnnual ? PlanType.TeamsAnnually2020 : PlanType.TeamsMonthly2020;
Product = ProductType.Teams;
Name = isAnnual ? "Teams (Annually) 2020" : "Teams (Monthly) 2020";
IsAnnual = isAnnual;
NameLocalizationKey = "planNameTeams";
DescriptionLocalizationKey = "planDescTeams";
CanBeUsedByBusiness = true;
TrialPeriodDays = 7;
HasGroups = true;
HasDirectory = true;
HasEvents = true;
HasTotp = true;
Has2fa = true;
HasApi = true;
UsersGetPremium = true;
UpgradeSortOrder = 2;
DisplaySortOrder = 2;
LegacyYear = 2023;
PasswordManager = new Teams2020PasswordManagerFeatures(isAnnual);
SecretsManager = new Teams2020SecretsManagerFeatures(isAnnual);
}
private record Teams2020SecretsManagerFeatures : SecretsManagerPlanFeatures
{
public Teams2020SecretsManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BasePrice = 0;
BaseServiceAccount = 50;
HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true;
AllowSeatAutoscale = true;
AllowServiceAccountsAutoscale = true;
if (isAnnual)
{
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
SeatPrice = 72;
AdditionalPricePerServiceAccount = 6;
}
else
{
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
SeatPrice = 7;
AdditionalPricePerServiceAccount = 0.5M;
}
}
}
private record Teams2020PasswordManagerFeatures : PasswordManagerPlanFeatures
{
public Teams2020PasswordManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BaseStorageGb = 1;
BasePrice = 0;
HasAdditionalStorageOption = true;
HasAdditionalSeatsOption = true;
AllowSeatAutoscale = true;
if (isAnnual)
{
StripeStoragePlanId = "storage-gb-annually";
StripeSeatPlanId = "2020-teams-org-seat-annually";
SeatPrice = 36;
AdditionalStoragePricePerGb = 4;
}
else
{
StripeSeatPlanId = "2020-teams-org-seat-monthly";
StripeStoragePlanId = "storage-gb-monthly";
SeatPrice = 4;
AdditionalStoragePricePerGb = 0.5M;
}
}
}
}

View File

@@ -78,15 +78,15 @@ public record TeamsPlan : Models.StaticStore.Plan
if (isAnnual)
{
StripeStoragePlanId = "storage-gb-annually";
StripeSeatPlanId = "2020-teams-org-seat-annually";
SeatPrice = 36;
StripeSeatPlanId = "2023-teams-org-seat-annually";
SeatPrice = 48;
AdditionalStoragePricePerGb = 4;
}
else
{
StripeSeatPlanId = "2020-teams-org-seat-monthly";
StripeSeatPlanId = "2023-teams-org-seat-monthly";
StripeStoragePlanId = "storage-gb-monthly";
SeatPrice = 4;
SeatPrice = 5;
AdditionalStoragePricePerGb = 0.5M;
}
}

View File

@@ -1,4 +1,5 @@
using Bit.Core.Models.BitStripe;
using Stripe;
namespace Bit.Core.Services;
@@ -14,8 +15,11 @@ public interface IStripeAdapter
Task<Stripe.Subscription> SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null);
Task<Stripe.Subscription> SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null);
Task<Stripe.Invoice> InvoiceUpcomingAsync(Stripe.UpcomingInvoiceOptions options);
Task<Stripe.Invoice> InvoiceCreateAsync(Stripe.InvoiceCreateOptions options);
Task<Stripe.InvoiceItem> InvoiceItemCreateAsync(Stripe.InvoiceItemCreateOptions options);
Task<Stripe.Invoice> InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options);
Task<Stripe.StripeList<Stripe.Invoice>> InvoiceListAsync(Stripe.InvoiceListOptions options);
IEnumerable<InvoiceItem> InvoiceItemListAsync(InvoiceItemListOptions options);
Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options);
Task<Stripe.Invoice> InvoiceFinalizeInvoiceAsync(string id, Stripe.InvoiceFinalizeOptions options);
Task<Stripe.Invoice> InvoiceSendInvoiceAsync(string id, Stripe.InvoiceSendOptions options);

View File

@@ -98,14 +98,6 @@ public class PolicyService : IPolicyService
await DependsOnSingleOrgAsync(org);
}
break;
// Activate Autofill is only available to Enterprise 2020-current plans
case PolicyType.ActivateAutofill:
if (policy.Enabled)
{
LockedTo2020Plan(org);
}
break;
}
var now = DateTime.UtcNow;
@@ -274,14 +266,6 @@ public class PolicyService : IPolicyService
}
}
private void LockedTo2020Plan(Organization org)
{
if (org.PlanType != PlanType.EnterpriseAnnually && org.PlanType != PlanType.EnterpriseMonthly)
{
throw new BadRequestException("This policy is only available to 2020 Enterprise plans.");
}
}
private async Task RequiredBySsoTrustedDeviceEncryptionAsync(Organization org)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);

View File

@@ -1,4 +1,5 @@
using Bit.Core.Models.BitStripe;
using Stripe;
namespace Bit.Core.Services;
@@ -16,6 +17,7 @@ public class StripeAdapter : IStripeAdapter
private readonly Stripe.BankAccountService _bankAccountService;
private readonly Stripe.PriceService _priceService;
private readonly Stripe.TestHelpers.TestClockService _testClockService;
private readonly Stripe.InvoiceItemService _invoiceItemService;
public StripeAdapter()
{
@@ -31,6 +33,7 @@ public class StripeAdapter : IStripeAdapter
_bankAccountService = new Stripe.BankAccountService();
_priceService = new Stripe.PriceService();
_testClockService = new Stripe.TestHelpers.TestClockService();
_invoiceItemService = new Stripe.InvoiceItemService();
}
public Task<Stripe.Customer> CustomerCreateAsync(Stripe.CustomerCreateOptions options)
@@ -79,6 +82,16 @@ public class StripeAdapter : IStripeAdapter
return _invoiceService.UpcomingAsync(options);
}
public Task<Stripe.Invoice> InvoiceCreateAsync(Stripe.InvoiceCreateOptions options)
{
return _invoiceService.CreateAsync(options);
}
public Task<Stripe.InvoiceItem> InvoiceItemCreateAsync(Stripe.InvoiceItemCreateOptions options)
{
return _invoiceItemService.CreateAsync(options);
}
public Task<Stripe.Invoice> InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options)
{
return _invoiceService.GetAsync(id, options);
@@ -89,6 +102,11 @@ public class StripeAdapter : IStripeAdapter
return _invoiceService.ListAsync(options);
}
public IEnumerable<InvoiceItem> InvoiceItemListAsync(InvoiceItemListOptions options)
{
return _invoiceItemService.ListAutoPaging(options);
}
public Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options)
{
return _invoiceService.UpdateAsync(id, options);

View File

@@ -6,6 +6,7 @@ using Bit.Core.Models.Business;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
using Stripe;
using StaticStore = Bit.Core.Models.StaticStore;
using TaxRate = Bit.Core.Entities.TaxRate;
@@ -749,16 +750,14 @@ public class StripePaymentService : IPaymentService
prorationDate ??= DateTime.UtcNow;
var collectionMethod = sub.CollectionMethod;
var daysUntilDue = sub.DaysUntilDue;
var chargeNow = collectionMethod == "charge_automatically";
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
var subUpdateOptions = new Stripe.SubscriptionUpdateOptions
{
Items = updatedItemOptions,
ProrationBehavior = "always_invoice",
ProrationBehavior = Constants.CreateProrations,
DaysUntilDue = daysUntilDue ?? 1,
CollectionMethod = "send_invoice",
ProrationDate = prorationDate,
CollectionMethod = "send_invoice"
};
if (!subscriptionUpdate.UpdateNeeded(sub))
@@ -792,66 +791,50 @@ public class StripePaymentService : IPaymentService
string paymentIntentClientSecret = null;
try
{
var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions);
var subItemOptions = updatedItemOptions.Select(itemOption =>
new Stripe.InvoiceSubscriptionItemOptions
{
Id = itemOption.Id,
Plan = itemOption.Plan,
Quantity = itemOption.Quantity,
}).ToList();
var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions());
var reviewInvoiceResponse = await PreviewUpcomingInvoiceAndPayAsync(storableSubscriber, subItemOptions);
paymentIntentClientSecret = reviewInvoiceResponse.PaymentIntentClientSecret;
var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions);
var invoice =
await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions());
if (invoice == null)
{
throw new BadRequestException("Unable to locate draft invoice for subscription update.");
}
if (invoice.AmountDue > 0 && updatedItemOptions.Any(i => i.Quantity > 0))
}
catch (Exception e)
{
// Need to revert the subscription
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
{
try
{
if (chargeNow)
{
paymentIntentClientSecret = await PayInvoiceAfterSubscriptionChangeAsync(
storableSubscriber, invoice);
}
else
{
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new Stripe.InvoiceFinalizeOptions
{
AutoAdvance = false,
});
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new Stripe.InvoiceSendOptions());
paymentIntentClientSecret = null;
}
}
catch
{
// Need to revert the subscription
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
{
Items = subscriptionUpdate.RevertItemsOptions(sub),
// This proration behavior prevents a false "credit" from
// being applied forward to the next month's invoice
ProrationBehavior = "none",
CollectionMethod = collectionMethod,
DaysUntilDue = daysUntilDue,
});
throw;
}
}
else if (!invoice.Paid)
{
// Pay invoice with no charge to customer this completes the invoice immediately without waiting the scheduled 1h
invoice = await _stripeAdapter.InvoicePayAsync(subResponse.LatestInvoiceId);
paymentIntentClientSecret = null;
}
Items = subscriptionUpdate.RevertItemsOptions(sub),
// This proration behavior prevents a false "credit" from
// being applied forward to the next month's invoice
ProrationBehavior = "none",
CollectionMethod = collectionMethod,
DaysUntilDue = daysUntilDue,
});
throw;
}
finally
{
// Change back the subscription collection method and/or days until due
if (collectionMethod != "send_invoice" || daysUntilDue == null)
{
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
{
CollectionMethod = collectionMethod,
DaysUntilDue = daysUntilDue,
});
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
new Stripe.SubscriptionUpdateOptions
{
CollectionMethod = collectionMethod,
DaysUntilDue = daysUntilDue,
});
}
}
@@ -934,6 +917,7 @@ public class StripePaymentService : IPaymentService
await _stripeAdapter.CustomerDeleteAsync(subscriber.GatewayCustomerId);
}
//This method is no-longer is use because we return the dollar threshold feature on invoice will be generated. but we dont want to lose this implementation.
public async Task<string> PayInvoiceAfterSubscriptionChangeAsync(ISubscriber subscriber, Stripe.Invoice invoice)
{
var customerOptions = new Stripe.CustomerGetOptions();
@@ -1103,6 +1087,310 @@ public class StripePaymentService : IPaymentService
return paymentIntentClientSecret;
}
internal async Task<InvoicePreviewResult> PreviewUpcomingInvoiceAndPayAsync(ISubscriber subscriber,
List<Stripe.InvoiceSubscriptionItemOptions> subItemOptions, int prorateThreshold = 50000)
{
var customer = await CheckInAppPurchaseMethod(subscriber);
string paymentIntentClientSecret = null;
var pendingInvoiceItems = GetPendingInvoiceItems(subscriber);
var upcomingPreview = await GetUpcomingInvoiceAsync(subscriber, subItemOptions);
var itemsForInvoice = GetItemsForInvoice(subItemOptions, upcomingPreview, pendingInvoiceItems);
var invoiceAmount = itemsForInvoice?.Sum(i => i.Amount) ?? 0;
var invoiceNow = invoiceAmount >= prorateThreshold;
if (invoiceNow)
{
await ProcessImmediateInvoiceAsync(subscriber, upcomingPreview, invoiceAmount, customer, itemsForInvoice, pendingInvoiceItems, paymentIntentClientSecret);
}
return new InvoicePreviewResult { IsInvoicedNow = invoiceNow, PaymentIntentClientSecret = paymentIntentClientSecret };
}
private async Task<InvoicePreviewResult> ProcessImmediateInvoiceAsync(ISubscriber subscriber, Invoice upcomingPreview, long invoiceAmount,
Customer customer, IEnumerable<InvoiceLineItem> itemsForInvoice, PendingInoviceItems pendingInvoiceItems,
string paymentIntentClientSecret)
{
// Owes more than prorateThreshold on the next invoice.
// Invoice them and pay now instead of waiting until the next billing cycle.
string cardPaymentMethodId = null;
var invoiceAmountDue = upcomingPreview.StartingBalance + invoiceAmount;
cardPaymentMethodId = GetCardPaymentMethodId(invoiceAmountDue, customer, cardPaymentMethodId);
Stripe.Invoice invoice = null;
var createdInvoiceItems = new List<Stripe.InvoiceItem>();
Braintree.Transaction braintreeTransaction = null;
try
{
await CreateInvoiceItemsAsync(subscriber, itemsForInvoice, pendingInvoiceItems, createdInvoiceItems);
invoice = await CreateInvoiceAsync(subscriber, cardPaymentMethodId);
var invoicePayOptions = new Stripe.InvoicePayOptions();
await CreateBrainTreeTransactionRequestAsync(subscriber, invoice, customer, invoicePayOptions,
cardPaymentMethodId, braintreeTransaction);
await InvoicePayAsync(invoicePayOptions, invoice, paymentIntentClientSecret);
}
catch (Exception e)
{
if (braintreeTransaction != null)
{
await _btGateway.Transaction.RefundAsync(braintreeTransaction.Id);
}
if (invoice != null)
{
if (invoice.Status == "paid")
{
// It's apparently paid, so we return without throwing an exception
return new InvoicePreviewResult
{
IsInvoicedNow = false,
PaymentIntentClientSecret = paymentIntentClientSecret
};
}
await RestoreInvoiceItemsAsync(invoice, customer, pendingInvoiceItems.PendingInvoiceItems);
}
else
{
foreach (var ii in createdInvoiceItems)
{
await _stripeAdapter.InvoiceDeleteAsync(ii.Id);
}
}
if (e is Stripe.StripeException strEx &&
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
{
throw new GatewayException("Bank account is not yet verified.");
}
throw;
}
return new InvoicePreviewResult
{
IsInvoicedNow = false,
PaymentIntentClientSecret = paymentIntentClientSecret
};
}
private static IEnumerable<InvoiceLineItem> GetItemsForInvoice(List<InvoiceSubscriptionItemOptions> subItemOptions, Invoice upcomingPreview,
PendingInoviceItems pendingInvoiceItems)
{
var itemsForInvoice = upcomingPreview.Lines?.Data?
.Where(i => pendingInvoiceItems.PendingInvoiceItemsDict.ContainsKey(i.Id) ||
(i.Plan.Id == subItemOptions[0]?.Plan && i.Proration));
return itemsForInvoice;
}
private PendingInoviceItems GetPendingInvoiceItems(ISubscriber subscriber)
{
var pendingInvoiceItems = new PendingInoviceItems();
var invoiceItems = _stripeAdapter.InvoiceItemListAsync(new Stripe.InvoiceItemListOptions
{
Customer = subscriber.GatewayCustomerId
}).ToList().Where(i => i.InvoiceId == null);
pendingInvoiceItems.PendingInvoiceItemsDict = invoiceItems.ToDictionary(pii => pii.Id);
return pendingInvoiceItems;
}
private async Task<Customer> CheckInAppPurchaseMethod(ISubscriber subscriber)
{
var customerOptions = GetCustomerPaymentOptions();
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions);
var usingInAppPaymentMethod = customer.Metadata.ContainsKey("appleReceipt");
if (usingInAppPaymentMethod)
{
throw new BadRequestException("Cannot perform this action with in-app purchase payment method. " +
"Contact support.");
}
return customer;
}
private string GetCardPaymentMethodId(long invoiceAmountDue, Customer customer, string cardPaymentMethodId)
{
try
{
if (invoiceAmountDue <= 0 || customer.Metadata.ContainsKey("btCustomerId")) return cardPaymentMethodId;
var hasDefaultCardPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card";
var hasDefaultValidSource = customer.DefaultSource != null &&
(customer.DefaultSource is Stripe.Card ||
customer.DefaultSource is Stripe.BankAccount);
if (hasDefaultCardPaymentMethod || hasDefaultValidSource) return cardPaymentMethodId;
cardPaymentMethodId = GetLatestCardPaymentMethod(customer.Id)?.Id;
if (cardPaymentMethodId == null)
{
throw new BadRequestException("No payment method is available.");
}
}
catch (Exception e)
{
throw new BadRequestException("No payment method is available.");
}
return cardPaymentMethodId;
}
private async Task<Invoice> GetUpcomingInvoiceAsync(ISubscriber subscriber, List<InvoiceSubscriptionItemOptions> subItemOptions)
{
var upcomingPreview = await _stripeAdapter.InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions
{
Customer = subscriber.GatewayCustomerId,
Subscription = subscriber.GatewaySubscriptionId,
SubscriptionItems = subItemOptions
});
return upcomingPreview;
}
private async Task RestoreInvoiceItemsAsync(Invoice invoice, Customer customer, IEnumerable<InvoiceItem> pendingInvoiceItems)
{
invoice = await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id, new Stripe.InvoiceVoidOptions());
if (invoice.StartingBalance != 0)
{
await _stripeAdapter.CustomerUpdateAsync(customer.Id,
new Stripe.CustomerUpdateOptions { Balance = customer.Balance });
}
// Restore invoice items that were brought in
foreach (var item in pendingInvoiceItems)
{
var i = new Stripe.InvoiceItemCreateOptions
{
Currency = item.Currency,
Description = item.Description,
Customer = item.CustomerId,
Subscription = item.SubscriptionId,
Discountable = item.Discountable,
Metadata = item.Metadata,
Quantity = item.Proration ? 1 : item.Quantity,
UnitAmount = item.UnitAmount
};
await _stripeAdapter.InvoiceItemCreateAsync(i);
}
}
private async Task InvoicePayAsync(InvoicePayOptions invoicePayOptions, Invoice invoice, string paymentIntentClientSecret)
{
try
{
await _stripeAdapter.InvoicePayAsync(invoice.Id, invoicePayOptions);
}
catch (Stripe.StripeException e)
{
if (e.HttpStatusCode == System.Net.HttpStatusCode.PaymentRequired &&
e.StripeError?.Code == "invoice_payment_intent_requires_action")
{
// SCA required, get intent client secret
var invoiceGetOptions = new Stripe.InvoiceGetOptions();
invoiceGetOptions.AddExpand("payment_intent");
invoice = await _stripeAdapter.InvoiceGetAsync(invoice.Id, invoiceGetOptions);
paymentIntentClientSecret = invoice?.PaymentIntent?.ClientSecret;
}
else
{
throw new GatewayException("Unable to pay invoice.");
}
}
}
private async Task CreateBrainTreeTransactionRequestAsync(ISubscriber subscriber, Invoice invoice, Customer customer,
InvoicePayOptions invoicePayOptions, string cardPaymentMethodId, Braintree.Transaction braintreeTransaction)
{
if (invoice.AmountDue > 0)
{
if (customer?.Metadata?.ContainsKey("btCustomerId") ?? false)
{
invoicePayOptions.PaidOutOfBand = true;
var btInvoiceAmount = (invoice.AmountDue / 100M);
var transactionResult = await _btGateway.Transaction.SaleAsync(
new Braintree.TransactionRequest
{
Amount = btInvoiceAmount,
CustomerId = customer.Metadata["btCustomerId"],
Options = new Braintree.TransactionOptionsRequest
{
SubmitForSettlement = true,
PayPal = new Braintree.TransactionOptionsPayPalRequest
{
CustomField = $"{subscriber.BraintreeIdField()}:{subscriber.Id}"
}
},
CustomFields = new Dictionary<string, string>
{
[subscriber.BraintreeIdField()] = subscriber.Id.ToString()
}
});
if (!transactionResult.IsSuccess())
{
throw new GatewayException("Failed to charge PayPal customer.");
}
braintreeTransaction = transactionResult.Target;
await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new Stripe.InvoiceUpdateOptions
{
Metadata = new Dictionary<string, string>
{
["btTransactionId"] = braintreeTransaction.Id,
["btPayPalTransactionId"] =
braintreeTransaction.PayPalDetails.AuthorizationId
}
});
}
else
{
invoicePayOptions.OffSession = true;
invoicePayOptions.PaymentMethod = cardPaymentMethodId;
}
}
}
private async Task<Invoice> CreateInvoiceAsync(ISubscriber subscriber, string cardPaymentMethodId)
{
Invoice invoice;
invoice = await _stripeAdapter.InvoiceCreateAsync(new Stripe.InvoiceCreateOptions
{
CollectionMethod = "send_invoice",
DaysUntilDue = 1,
Customer = subscriber.GatewayCustomerId,
Subscription = subscriber.GatewaySubscriptionId,
DefaultPaymentMethod = cardPaymentMethodId
});
return invoice;
}
private async Task CreateInvoiceItemsAsync(ISubscriber subscriber, IEnumerable<InvoiceLineItem> itemsForInvoice,
PendingInoviceItems pendingInvoiceItems, List<InvoiceItem> createdInvoiceItems)
{
foreach (var invoiceLineItem in itemsForInvoice)
{
if (pendingInvoiceItems.PendingInvoiceItemsDict.ContainsKey(invoiceLineItem.Id))
{
continue;
}
var invoiceItem = await _stripeAdapter.InvoiceItemCreateAsync(new Stripe.InvoiceItemCreateOptions
{
Currency = invoiceLineItem.Currency,
Description = invoiceLineItem.Description,
Customer = subscriber.GatewayCustomerId,
Subscription = invoiceLineItem.Subscription,
Discountable = invoiceLineItem.Discountable,
Amount = invoiceLineItem.Amount
});
createdInvoiceItems.Add(invoiceItem);
}
}
public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false,
bool skipInAppPurchaseCheck = false)
{

View File

@@ -6,7 +6,7 @@ using Bit.Core.Models.StaticStore.Plans;
namespace Bit.Core.Utilities;
public class StaticStore
public static class StaticStore
{
static StaticStore()
{
@@ -112,6 +112,11 @@ public class StaticStore
new EnterprisePlan(false),
new TeamsPlan(true),
new TeamsPlan(false),
new Enterprise2020Plan(true),
new Enterprise2020Plan(false),
new Teams2020Plan(true),
new Teams2020Plan(false),
new FamiliesPlan(),
new FreePlan(),
new CustomPlan(),
@@ -139,8 +144,7 @@ public class StaticStore
}
};
public static Models.StaticStore.Plan GetPlan(PlanType planType) =>
Plans.SingleOrDefault(p => p.Type == planType);
public static Plan GetPlan(PlanType planType) => Plans.SingleOrDefault(p => p.Type == planType);
public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) =>