1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-24 12:35:25 +01:00

[AC-1843] Automate PM discount for SM Trial (#3661)

* Added appliesTo to customer discount. Added productId to subscription item

* Added IsFromSecretsManagerTrial flag to add discount for SM trials

* Fixed broken tests

---------

Co-authored-by: Alex Morask <amorask@bitwarden.com>
This commit is contained in:
Conner Turnbull 2024-01-29 11:10:27 -05:00 committed by GitHub
parent 693f0566a6
commit d7de5cbf28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 55 additions and 25 deletions

View File

@ -46,6 +46,7 @@ public class OrganizationCreateRequestModel : IValidatableObject
public int? AdditionalServiceAccounts { get; set; } public int? AdditionalServiceAccounts { get; set; }
[Required] [Required]
public bool UseSecretsManager { get; set; } public bool UseSecretsManager { get; set; }
public bool IsFromSecretsManagerTrial { get; set; }
public virtual OrganizationSignup ToOrganizationSignup(User user) public virtual OrganizationSignup ToOrganizationSignup(User user)
{ {
@ -67,6 +68,7 @@ public class OrganizationCreateRequestModel : IValidatableObject
AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(), AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(),
AdditionalServiceAccounts = AdditionalServiceAccounts.GetValueOrDefault(), AdditionalServiceAccounts = AdditionalServiceAccounts.GetValueOrDefault(),
UseSecretsManager = UseSecretsManager, UseSecretsManager = UseSecretsManager,
IsFromSecretsManagerTrial = IsFromSecretsManagerTrial,
TaxInfo = new TaxInfo TaxInfo = new TaxInfo
{ {
TaxIdNumber = TaxIdNumber, TaxIdNumber = TaxIdNumber,

View File

@ -50,11 +50,13 @@ public class BillingCustomerDiscount
Id = discount.Id; Id = discount.Id;
Active = discount.Active; Active = discount.Active;
PercentOff = discount.PercentOff; PercentOff = discount.PercentOff;
AppliesTo = discount.AppliesTo;
} }
public string Id { get; } public string Id { get; }
public bool Active { get; } public bool Active { get; }
public decimal? PercentOff { get; } public decimal? PercentOff { get; }
public List<string> AppliesTo { get; }
} }
public class BillingSubscription public class BillingSubscription
@ -89,6 +91,7 @@ public class BillingSubscription
{ {
public BillingSubscriptionItem(SubscriptionInfo.BillingSubscription.BillingSubscriptionItem item) public BillingSubscriptionItem(SubscriptionInfo.BillingSubscription.BillingSubscriptionItem item)
{ {
ProductId = item.ProductId;
Name = item.Name; Name = item.Name;
Amount = item.Amount; Amount = item.Amount;
Interval = item.Interval; Interval = item.Interval;
@ -97,6 +100,7 @@ public class BillingSubscription
AddonSubscriptionItem = item.AddonSubscriptionItem; AddonSubscriptionItem = item.AddonSubscriptionItem;
} }
public string ProductId { get; set; }
public string Name { get; set; } public string Name { get; set; }
public decimal Amount { get; set; } public decimal Amount { get; set; }
public int Quantity { get; set; } public int Quantity { get; set; }

View File

@ -517,7 +517,7 @@ public class OrganizationService : IOrganizationService
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value, await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats, signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
signup.PremiumAccessAddon, signup.TaxInfo, provider, signup.AdditionalSmSeats.GetValueOrDefault(), signup.PremiumAccessAddon, signup.TaxInfo, provider, signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault()); signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
} }
var ownerId = provider ? default : signup.Owner.Id; var ownerId = provider ? default : signup.Owner.Id;

View File

@ -15,4 +15,5 @@ public class OrganizationUpgrade
public int? AdditionalSmSeats { get; set; } public int? AdditionalSmSeats { get; set; }
public int? AdditionalServiceAccounts { get; set; } public int? AdditionalServiceAccounts { get; set; }
public bool UseSecretsManager { get; set; } public bool UseSecretsManager { get; set; }
public bool IsFromSecretsManagerTrial { get; set; }
} }

View File

@ -17,11 +17,13 @@ public class SubscriptionInfo
Id = discount.Id; Id = discount.Id;
Active = discount.Start != null && discount.End == null; Active = discount.Start != null && discount.End == null;
PercentOff = discount.Coupon?.PercentOff; PercentOff = discount.Coupon?.PercentOff;
AppliesTo = discount.Coupon?.AppliesTo?.Products ?? new List<string>();
} }
public string Id { get; } public string Id { get; }
public bool Active { get; } public bool Active { get; }
public decimal? PercentOff { get; } public decimal? PercentOff { get; }
public List<string> AppliesTo { get; }
} }
public class BillingSubscription public class BillingSubscription
@ -59,6 +61,7 @@ public class SubscriptionInfo
{ {
if (item.Plan != null) if (item.Plan != null)
{ {
ProductId = item.Plan.ProductId;
Name = item.Plan.Nickname; Name = item.Plan.Nickname;
Amount = item.Plan.Amount.GetValueOrDefault() / 100M; Amount = item.Plan.Amount.GetValueOrDefault() / 100M;
Interval = item.Plan.Interval; Interval = item.Plan.Interval;
@ -72,6 +75,7 @@ public class SubscriptionInfo
public bool AddonSubscriptionItem { get; set; } public bool AddonSubscriptionItem { get; set; }
public string ProductId { get; set; }
public string Name { get; set; } public string Name { get; set; }
public decimal Amount { get; set; } public decimal Amount { get; set; }
public int Quantity { get; set; } public int Quantity { get; set; }

View File

@ -12,7 +12,7 @@ public interface IPaymentService
Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats, string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats,
bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, int additionalSmSeats = 0, 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 SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship);
Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship); Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship);
Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade); Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade);

View File

@ -7,6 +7,7 @@ using Bit.Core.Models.Business;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Stripe;
using StaticStore = Bit.Core.Models.StaticStore; using StaticStore = Bit.Core.Models.StaticStore;
using TaxRate = Bit.Core.Entities.TaxRate; using TaxRate = Bit.Core.Entities.TaxRate;
@ -17,6 +18,7 @@ public class StripePaymentService : IPaymentService
private const string PremiumPlanId = "premium-annually"; private const string PremiumPlanId = "premium-annually";
private const string StoragePlanId = "storage-gb-annually"; private const string StoragePlanId = "storage-gb-annually";
private const string ProviderDiscountId = "msp-discount-35"; private const string ProviderDiscountId = "msp-discount-35";
private const string SecretsManagerStandaloneDiscountId = "sm-standalone";
private readonly ITransactionRepository _transactionRepository; private readonly ITransactionRepository _transactionRepository;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
@ -47,7 +49,7 @@ public class StripePaymentService : IPaymentService
public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
string paymentToken, StaticStore.Plan plan, short additionalStorageGb, string paymentToken, StaticStore.Plan plan, short additionalStorageGb,
int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, 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; Braintree.Customer braintreeCustomer = null;
string stipeCustomerSourceToken = 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 Address = new Stripe.AddressOptions
{ {
Country = taxInfo.BillingAddressCountry, Country = taxInfo.BillingAddressCountry,
@ -1410,7 +1416,9 @@ public class StripePaymentService : IPaymentService
if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) 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) if (customer.Discount != null)
{ {
@ -1418,28 +1426,35 @@ public class StripePaymentService : IPaymentService
} }
} }
if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) if (string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
{ {
return subscriptionInfo;
}
var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId); var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId);
if (sub != null) if (sub != null)
{ {
subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub); subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub);
} }
if (!sub.CanceledAt.HasValue && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) if (sub is { CanceledAt: not null } || string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{ {
return subscriptionInfo;
}
try try
{ {
var upcomingInvoice = await _stripeAdapter.InvoiceUpcomingAsync( var upcomingInvoiceOptions = new UpcomingInvoiceOptions { Customer = subscriber.GatewayCustomerId };
new Stripe.UpcomingInvoiceOptions { Customer = subscriber.GatewayCustomerId }); var upcomingInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions);
if (upcomingInvoice != null) if (upcomingInvoice != null)
{ {
subscriptionInfo.UpcomingInvoice = subscriptionInfo.UpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice(upcomingInvoice);
new SubscriptionInfo.BillingUpcomingInvoice(upcomingInvoice);
} }
} }
catch (Stripe.StripeException) { } catch (StripeException ex)
} {
_logger.LogWarning(ex, "Encountered an unexpected Stripe error");
} }
return subscriptionInfo; return subscriptionInfo;

View File

@ -209,6 +209,7 @@ public class OrganizationServiceTests
signup.PaymentMethodType = PaymentMethodType.Card; signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false; signup.PremiumAccessAddon = false;
signup.UseSecretsManager = false; signup.UseSecretsManager = false;
signup.IsFromSecretsManagerTrial = false;
var purchaseOrganizationPlan = StaticStore.GetPlan(signup.Plan); var purchaseOrganizationPlan = StaticStore.GetPlan(signup.Plan);
@ -247,7 +248,8 @@ public class OrganizationServiceTests
signup.TaxInfo, signup.TaxInfo,
false, false,
signup.AdditionalSmSeats.GetValueOrDefault(), signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault() signup.AdditionalServiceAccounts.GetValueOrDefault(),
signup.UseSecretsManager
); );
} }
@ -326,6 +328,7 @@ public class OrganizationServiceTests
signup.AdditionalServiceAccounts = 20; signup.AdditionalServiceAccounts = 20;
signup.PaymentMethodType = PaymentMethodType.Card; signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false; signup.PremiumAccessAddon = false;
signup.IsFromSecretsManagerTrial = false;
var result = await sutProvider.Sut.SignUpAsync(signup); var result = await sutProvider.Sut.SignUpAsync(signup);
@ -362,7 +365,8 @@ public class OrganizationServiceTests
signup.TaxInfo, signup.TaxInfo,
false, false,
signup.AdditionalSmSeats.GetValueOrDefault(), signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault() signup.AdditionalServiceAccounts.GetValueOrDefault(),
signup.IsFromSecretsManagerTrial
); );
} }