diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs index 2ea4e8b84..304978eb1 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs @@ -46,6 +46,7 @@ public class OrganizationCreateRequestModel : IValidatableObject public int? AdditionalServiceAccounts { get; set; } [Required] public bool UseSecretsManager { get; set; } + public bool IsFromSecretsManagerTrial { get; set; } public virtual OrganizationSignup ToOrganizationSignup(User user) { @@ -67,6 +68,7 @@ public class OrganizationCreateRequestModel : IValidatableObject AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(), AdditionalServiceAccounts = AdditionalServiceAccounts.GetValueOrDefault(), UseSecretsManager = UseSecretsManager, + IsFromSecretsManagerTrial = IsFromSecretsManagerTrial, TaxInfo = new TaxInfo { TaxIdNumber = TaxIdNumber, diff --git a/src/Api/Models/Response/SubscriptionResponseModel.cs b/src/Api/Models/Response/SubscriptionResponseModel.cs index bac1cf3f9..7ba2b857e 100644 --- a/src/Api/Models/Response/SubscriptionResponseModel.cs +++ b/src/Api/Models/Response/SubscriptionResponseModel.cs @@ -50,11 +50,13 @@ public class BillingCustomerDiscount Id = discount.Id; Active = discount.Active; PercentOff = discount.PercentOff; + AppliesTo = discount.AppliesTo; } public string Id { get; } public bool Active { get; } public decimal? PercentOff { get; } + public List AppliesTo { get; } } public class BillingSubscription @@ -89,6 +91,7 @@ public class BillingSubscription { public BillingSubscriptionItem(SubscriptionInfo.BillingSubscription.BillingSubscriptionItem item) { + ProductId = item.ProductId; Name = item.Name; Amount = item.Amount; Interval = item.Interval; @@ -97,6 +100,7 @@ public class BillingSubscription AddonSubscriptionItem = item.AddonSubscriptionItem; } + public string ProductId { get; set; } public string Name { get; set; } public decimal Amount { get; set; } public int Quantity { get; set; } diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index f97bd7c07..8ab5293e9 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -517,7 +517,7 @@ public class OrganizationService : IOrganizationService await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value, signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats, signup.PremiumAccessAddon, signup.TaxInfo, provider, signup.AdditionalSmSeats.GetValueOrDefault(), - signup.AdditionalServiceAccounts.GetValueOrDefault()); + signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial); } var ownerId = provider ? default : signup.Owner.Id; diff --git a/src/Core/Models/Business/OrganizationUpgrade.cs b/src/Core/Models/Business/OrganizationUpgrade.cs index 173502a24..6992f492a 100644 --- a/src/Core/Models/Business/OrganizationUpgrade.cs +++ b/src/Core/Models/Business/OrganizationUpgrade.cs @@ -15,4 +15,5 @@ public class OrganizationUpgrade public int? AdditionalSmSeats { get; set; } public int? AdditionalServiceAccounts { get; set; } public bool UseSecretsManager { get; set; } + public bool IsFromSecretsManagerTrial { get; set; } } diff --git a/src/Core/Models/Business/SubscriptionInfo.cs b/src/Core/Models/Business/SubscriptionInfo.cs index e23108123..e2a689f61 100644 --- a/src/Core/Models/Business/SubscriptionInfo.cs +++ b/src/Core/Models/Business/SubscriptionInfo.cs @@ -17,11 +17,13 @@ public class SubscriptionInfo Id = discount.Id; Active = discount.Start != null && discount.End == null; PercentOff = discount.Coupon?.PercentOff; + AppliesTo = discount.Coupon?.AppliesTo?.Products ?? new List(); } public string Id { get; } public bool Active { get; } public decimal? PercentOff { get; } + public List AppliesTo { get; } } public class BillingSubscription @@ -59,6 +61,7 @@ public class SubscriptionInfo { if (item.Plan != null) { + ProductId = item.Plan.ProductId; Name = item.Plan.Nickname; Amount = item.Plan.Amount.GetValueOrDefault() / 100M; Interval = item.Plan.Interval; @@ -72,6 +75,7 @@ public class SubscriptionInfo public bool AddonSubscriptionItem { get; set; } + public string ProductId { get; set; } public string Name { get; set; } public decimal Amount { get; set; } public int Quantity { get; set; } diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 70cc88c20..f8f24cfbd 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -12,7 +12,7 @@ public interface IPaymentService Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, int additionalSmSeats = 0, - int additionalServiceAccount = 0); + int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false); Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship); Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship); Task UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade); diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index cc960083a..f3a939650 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -7,6 +7,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; @@ -17,6 +18,7 @@ public class StripePaymentService : IPaymentService private const string PremiumPlanId = "premium-annually"; private const string StoragePlanId = "storage-gb-annually"; private const string ProviderDiscountId = "msp-discount-35"; + private const string SecretsManagerStandaloneDiscountId = "sm-standalone"; private readonly ITransactionRepository _transactionRepository; private readonly IUserRepository _userRepository; @@ -47,7 +49,7 @@ public class StripePaymentService : IPaymentService public async Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, string paymentToken, StaticStore.Plan plan, short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, - int additionalSmSeats = 0, int additionalServiceAccount = 0) + int additionalSmSeats = 0, int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false) { Braintree.Customer braintreeCustomer = null; string stipeCustomerSourceToken = null; @@ -124,7 +126,11 @@ public class StripePaymentService : IPaymentService }, }, }, - Coupon = provider ? ProviderDiscountId : null, + Coupon = signupIsFromSecretsManagerTrial + ? SecretsManagerStandaloneDiscountId + : provider + ? ProviderDiscountId + : null, Address = new Stripe.AddressOptions { Country = taxInfo.BillingAddressCountry, @@ -1410,7 +1416,9 @@ public class StripePaymentService : IPaymentService if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) { - var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId); + var customerGetOptions = new CustomerGetOptions(); + customerGetOptions.AddExpand("discount.coupon.applies_to"); + var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerGetOptions); if (customer.Discount != null) { @@ -1418,29 +1426,36 @@ public class StripePaymentService : IPaymentService } } - if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) + if (string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) { - var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId); - if (sub != null) - { - subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub); - } + return subscriptionInfo; + } - if (!sub.CanceledAt.HasValue && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) + var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId); + if (sub != null) + { + subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub); + } + + if (sub is { CanceledAt: not null } || string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) + { + return subscriptionInfo; + } + + try + { + var upcomingInvoiceOptions = new UpcomingInvoiceOptions { Customer = subscriber.GatewayCustomerId }; + var upcomingInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions); + + if (upcomingInvoice != null) { - try - { - var upcomingInvoice = await _stripeAdapter.InvoiceUpcomingAsync( - new Stripe.UpcomingInvoiceOptions { Customer = subscriber.GatewayCustomerId }); - if (upcomingInvoice != null) - { - subscriptionInfo.UpcomingInvoice = - new SubscriptionInfo.BillingUpcomingInvoice(upcomingInvoice); - } - } - catch (Stripe.StripeException) { } + subscriptionInfo.UpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice(upcomingInvoice); } } + catch (StripeException ex) + { + _logger.LogWarning(ex, "Encountered an unexpected Stripe error"); + } return subscriptionInfo; } diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index 52dce5802..5f6aa0866 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -209,6 +209,7 @@ public class OrganizationServiceTests signup.PaymentMethodType = PaymentMethodType.Card; signup.PremiumAccessAddon = false; signup.UseSecretsManager = false; + signup.IsFromSecretsManagerTrial = false; var purchaseOrganizationPlan = StaticStore.GetPlan(signup.Plan); @@ -247,7 +248,8 @@ public class OrganizationServiceTests signup.TaxInfo, false, signup.AdditionalSmSeats.GetValueOrDefault(), - signup.AdditionalServiceAccounts.GetValueOrDefault() + signup.AdditionalServiceAccounts.GetValueOrDefault(), + signup.UseSecretsManager ); } @@ -326,6 +328,7 @@ public class OrganizationServiceTests signup.AdditionalServiceAccounts = 20; signup.PaymentMethodType = PaymentMethodType.Card; signup.PremiumAccessAddon = false; + signup.IsFromSecretsManagerTrial = false; var result = await sutProvider.Sut.SignUpAsync(signup); @@ -362,7 +365,8 @@ public class OrganizationServiceTests signup.TaxInfo, false, signup.AdditionalSmSeats.GetValueOrDefault(), - signup.AdditionalServiceAccounts.GetValueOrDefault() + signup.AdditionalServiceAccounts.GetValueOrDefault(), + signup.IsFromSecretsManagerTrial ); }