mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 08:02:49 -05:00
[AC 1451] Refactor staticstore plans and consuming logic (#3164)
* refactor the plan and create new objects * initial commit * Add new plan types * continue the refactoring by adding new plantypes * changes for plans * Refactoring continues * making changes for plan * Fixing the failing test * Fixing whitespace * Fix some in correct values * Resolve the plan data * rearranging the plan * Make the plan more immutable * Resolve the lint errors * Fix the failing test * Add custom plan * Fix the failing test * Fix the failing test * resolve the failing addons after refactoring * Refactoring * Merge branch 'master' into ac-1451/refactor-staticstore-plans-and-consuming-logic * merge from master * Merge branch 'master' into ac-1451/refactor-staticstore-plans-and-consuming-logic * format whitespace * resolve the conflict * Fix some pr comments * Fixing some of the pr comments * fixing some of the pr comments * Resolve some pr comments * Resolve pr comments * Resolves some pr comments * Resolving some or comments * Resolve a failing test * fix the failing test * Resolving some pr comments * Fix the failing test * resolve pr comment * add a using statement fir a failing test --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
@ -9,12 +9,12 @@ public interface IPaymentService
|
||||
{
|
||||
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
|
||||
Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
||||
string paymentToken, List<Plan> plans, short additionalStorageGb, int additionalSeats,
|
||||
string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats,
|
||||
bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, int additionalSmSeats = 0,
|
||||
int additionalServiceAccount = 0);
|
||||
Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship);
|
||||
Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship);
|
||||
Task<string> UpgradeFreeOrganizationAsync(Organization org, List<Plan> plans, OrganizationUpgrade upgrade);
|
||||
Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade);
|
||||
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
|
||||
short additionalStorageGb, TaxInfo taxInfo);
|
||||
Task<string> AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
||||
|
@ -175,19 +175,19 @@ public class OrganizationService : IOrganizationService
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var plan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BadRequestException("Existing plan not found.");
|
||||
}
|
||||
|
||||
if (!plan.HasAdditionalStorageOption)
|
||||
if (!plan.PasswordManager.HasAdditionalStorageOption)
|
||||
{
|
||||
throw new BadRequestException("Plan does not allow additional storage.");
|
||||
}
|
||||
|
||||
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb,
|
||||
plan.StripeStoragePlanId);
|
||||
plan.PasswordManager.StripeStoragePlanId);
|
||||
await _referenceEventService.RaiseEventAsync(
|
||||
new ReferenceEvent(ReferenceEventType.AdjustStorage, organization, _currentContext)
|
||||
{
|
||||
@ -233,21 +233,21 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException($"Cannot set max seat autoscaling below current seat count.");
|
||||
}
|
||||
|
||||
var plan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BadRequestException("Existing plan not found.");
|
||||
}
|
||||
|
||||
if (!plan.AllowSeatAutoscale)
|
||||
if (!plan.PasswordManager.AllowSeatAutoscale)
|
||||
{
|
||||
throw new BadRequestException("Your plan does not allow seat autoscaling.");
|
||||
}
|
||||
|
||||
if (plan.MaxUsers.HasValue && maxAutoscaleSeats.HasValue &&
|
||||
maxAutoscaleSeats > plan.MaxUsers)
|
||||
if (plan.PasswordManager.MaxSeats.HasValue && maxAutoscaleSeats.HasValue &&
|
||||
maxAutoscaleSeats > plan.PasswordManager.MaxSeats)
|
||||
{
|
||||
throw new BadRequestException(string.Concat($"Your plan has a seat limit of {plan.MaxUsers}, ",
|
||||
throw new BadRequestException(string.Concat($"Your plan has a seat limit of {plan.PasswordManager.MaxSeats}, ",
|
||||
$"but you have specified a max autoscale count of {maxAutoscaleSeats}.",
|
||||
"Reduce your max autoscale seat count."));
|
||||
}
|
||||
@ -285,21 +285,21 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException("No subscription found.");
|
||||
}
|
||||
|
||||
var plan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BadRequestException("Existing plan not found.");
|
||||
}
|
||||
|
||||
if (!plan.HasAdditionalSeatsOption)
|
||||
if (!plan.PasswordManager.HasAdditionalSeatsOption)
|
||||
{
|
||||
throw new BadRequestException("Plan does not allow additional seats.");
|
||||
}
|
||||
|
||||
var newSeatTotal = organization.Seats.Value + seatAdjustment;
|
||||
if (plan.BaseSeats > newSeatTotal)
|
||||
if (plan.PasswordManager.BaseSeats > newSeatTotal)
|
||||
{
|
||||
throw new BadRequestException($"Plan has a minimum of {plan.BaseSeats} seats.");
|
||||
throw new BadRequestException($"Plan has a minimum of {plan.PasswordManager.BaseSeats} seats.");
|
||||
}
|
||||
|
||||
if (newSeatTotal <= 0)
|
||||
@ -307,11 +307,11 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException("You must have at least 1 seat.");
|
||||
}
|
||||
|
||||
var additionalSeats = newSeatTotal - plan.BaseSeats;
|
||||
if (plan.MaxAdditionalSeats.HasValue && additionalSeats > plan.MaxAdditionalSeats.Value)
|
||||
var additionalSeats = newSeatTotal - plan.PasswordManager.BaseSeats;
|
||||
if (plan.PasswordManager.MaxAdditionalSeats.HasValue && additionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value)
|
||||
{
|
||||
throw new BadRequestException($"Organization plan allows a maximum of " +
|
||||
$"{plan.MaxAdditionalSeats.Value} additional seats.");
|
||||
$"{plan.PasswordManager.MaxAdditionalSeats.Value} additional seats.");
|
||||
}
|
||||
|
||||
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
|
||||
@ -403,11 +403,10 @@ public class OrganizationService : IOrganizationService
|
||||
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup,
|
||||
bool provider = false)
|
||||
{
|
||||
var passwordManagerPlan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == signup.Plan);
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
|
||||
ValidatePasswordManagerPlan(passwordManagerPlan, signup);
|
||||
ValidatePasswordManagerPlan(plan, signup);
|
||||
|
||||
var secretsManagerPlan = StaticStore.SecretManagerPlans.FirstOrDefault(p => p.Type == signup.Plan);
|
||||
if (signup.UseSecretsManager)
|
||||
{
|
||||
if (provider)
|
||||
@ -415,7 +414,7 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException(
|
||||
"Organizations with a Managed Service Provider do not support Secrets Manager.");
|
||||
}
|
||||
ValidateSecretsManagerPlan(secretsManagerPlan, signup);
|
||||
ValidateSecretsManagerPlan(plan, signup);
|
||||
}
|
||||
|
||||
if (!provider)
|
||||
@ -430,25 +429,25 @@ public class OrganizationService : IOrganizationService
|
||||
Name = signup.Name,
|
||||
BillingEmail = signup.BillingEmail,
|
||||
BusinessName = signup.BusinessName,
|
||||
PlanType = passwordManagerPlan.Type,
|
||||
Seats = (short)(passwordManagerPlan.BaseSeats + signup.AdditionalSeats),
|
||||
MaxCollections = passwordManagerPlan.MaxCollections,
|
||||
MaxStorageGb = !passwordManagerPlan.BaseStorageGb.HasValue ?
|
||||
(short?)null : (short)(passwordManagerPlan.BaseStorageGb.Value + signup.AdditionalStorageGb),
|
||||
UsePolicies = passwordManagerPlan.HasPolicies,
|
||||
UseSso = passwordManagerPlan.HasSso,
|
||||
UseGroups = passwordManagerPlan.HasGroups,
|
||||
UseEvents = passwordManagerPlan.HasEvents,
|
||||
UseDirectory = passwordManagerPlan.HasDirectory,
|
||||
UseTotp = passwordManagerPlan.HasTotp,
|
||||
Use2fa = passwordManagerPlan.Has2fa,
|
||||
UseApi = passwordManagerPlan.HasApi,
|
||||
UseResetPassword = passwordManagerPlan.HasResetPassword,
|
||||
SelfHost = passwordManagerPlan.HasSelfHost,
|
||||
UsersGetPremium = passwordManagerPlan.UsersGetPremium || signup.PremiumAccessAddon,
|
||||
UseCustomPermissions = passwordManagerPlan.HasCustomPermissions,
|
||||
UseScim = passwordManagerPlan.HasScim,
|
||||
Plan = passwordManagerPlan.Name,
|
||||
PlanType = plan!.Type,
|
||||
Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats),
|
||||
MaxCollections = plan.PasswordManager.MaxCollections,
|
||||
MaxStorageGb = !plan.PasswordManager.BaseStorageGb.HasValue ?
|
||||
(short?)null : (short)(plan.PasswordManager.BaseStorageGb.Value + signup.AdditionalStorageGb),
|
||||
UsePolicies = plan.HasPolicies,
|
||||
UseSso = plan.HasSso,
|
||||
UseGroups = plan.HasGroups,
|
||||
UseEvents = plan.HasEvents,
|
||||
UseDirectory = plan.HasDirectory,
|
||||
UseTotp = plan.HasTotp,
|
||||
Use2fa = plan.Has2fa,
|
||||
UseApi = plan.HasApi,
|
||||
UseResetPassword = plan.HasResetPassword,
|
||||
SelfHost = plan.HasSelfHost,
|
||||
UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon,
|
||||
UseCustomPermissions = plan.HasCustomPermissions,
|
||||
UseScim = plan.HasScim,
|
||||
Plan = plan.Name,
|
||||
Gateway = null,
|
||||
ReferenceData = signup.Owner.ReferenceData,
|
||||
Enabled = true,
|
||||
@ -464,12 +463,12 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
if (signup.UseSecretsManager)
|
||||
{
|
||||
organization.SmSeats = secretsManagerPlan.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault();
|
||||
organization.SmServiceAccounts = secretsManagerPlan.BaseServiceAccount.GetValueOrDefault() +
|
||||
organization.SmSeats = plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault();
|
||||
organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount +
|
||||
signup.AdditionalServiceAccounts.GetValueOrDefault();
|
||||
}
|
||||
|
||||
if (passwordManagerPlan.Type == PlanType.Free && !provider)
|
||||
if (plan.Type == PlanType.Free && !provider)
|
||||
{
|
||||
var adminCount =
|
||||
await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
|
||||
@ -478,14 +477,10 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException("You can only be an admin of one free organization.");
|
||||
}
|
||||
}
|
||||
else if (passwordManagerPlan.Type != PlanType.Free)
|
||||
else if (plan.Type != PlanType.Free)
|
||||
{
|
||||
var purchaseOrganizationPlan = signup.UseSecretsManager
|
||||
? StaticStore.Plans.Where(p => p.Type == signup.Plan).ToList()
|
||||
: StaticStore.PasswordManagerPlans.Where(p => p.Type == signup.Plan).Take(1).ToList();
|
||||
|
||||
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
|
||||
signup.PaymentToken, purchaseOrganizationPlan, signup.AdditionalStorageGb, signup.AdditionalSeats,
|
||||
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
|
||||
signup.PremiumAccessAddon, signup.TaxInfo, provider, signup.AdditionalSmSeats.GetValueOrDefault(),
|
||||
signup.AdditionalServiceAccounts.GetValueOrDefault());
|
||||
}
|
||||
@ -495,8 +490,8 @@ public class OrganizationService : IOrganizationService
|
||||
await _referenceEventService.RaiseEventAsync(
|
||||
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
|
||||
{
|
||||
PlanName = passwordManagerPlan.Name,
|
||||
PlanType = passwordManagerPlan.Type,
|
||||
PlanName = plan.Name,
|
||||
PlanType = plan.Type,
|
||||
Seats = returnValue.Item1.Seats,
|
||||
Storage = returnValue.Item1.MaxStorageGb,
|
||||
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
|
||||
@ -525,7 +520,7 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
|
||||
if (license.PlanType != PlanType.Custom &&
|
||||
StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == license.PlanType && !p.Disabled) == null)
|
||||
StaticStore.Plans.FirstOrDefault(p => p.Type == license.PlanType && !p.Disabled) == null)
|
||||
{
|
||||
throw new BadRequestException("Plan not found.");
|
||||
}
|
||||
@ -1955,11 +1950,6 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException($"{productType} Plan not found.");
|
||||
}
|
||||
|
||||
if (plan.BaseSeats + additionalSeats <= 0)
|
||||
{
|
||||
throw new BadRequestException($"You do not have any {productType} seats!");
|
||||
}
|
||||
|
||||
if (additionalSeats < 0)
|
||||
{
|
||||
throw new BadRequestException($"You can't subtract {productType} seats!");
|
||||
@ -1970,7 +1960,7 @@ public class OrganizationService : IOrganizationService
|
||||
{
|
||||
ValidatePlan(plan, upgrade.AdditionalSeats, "Password Manager");
|
||||
|
||||
if (plan.BaseSeats + upgrade.AdditionalSeats <= 0)
|
||||
if (plan.PasswordManager.BaseSeats + upgrade.AdditionalSeats <= 0)
|
||||
{
|
||||
throw new BadRequestException($"You do not have any Password Manager seats!");
|
||||
}
|
||||
@ -1980,7 +1970,7 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException($"You can't subtract Password Manager seats!");
|
||||
}
|
||||
|
||||
if (!plan.HasAdditionalStorageOption && upgrade.AdditionalStorageGb > 0)
|
||||
if (!plan.PasswordManager.HasAdditionalStorageOption && upgrade.AdditionalStorageGb > 0)
|
||||
{
|
||||
throw new BadRequestException("Plan does not allow additional storage.");
|
||||
}
|
||||
@ -1990,29 +1980,39 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException("You can't subtract storage!");
|
||||
}
|
||||
|
||||
if (!plan.HasPremiumAccessOption && upgrade.PremiumAccessAddon)
|
||||
if (!plan.PasswordManager.HasPremiumAccessOption && upgrade.PremiumAccessAddon)
|
||||
{
|
||||
throw new BadRequestException("This plan does not allow you to buy the premium access addon.");
|
||||
}
|
||||
|
||||
if (!plan.HasAdditionalSeatsOption && upgrade.AdditionalSeats > 0)
|
||||
if (!plan.PasswordManager.HasAdditionalSeatsOption && upgrade.AdditionalSeats > 0)
|
||||
{
|
||||
throw new BadRequestException("Plan does not allow additional users.");
|
||||
}
|
||||
|
||||
if (plan.HasAdditionalSeatsOption && plan.MaxAdditionalSeats.HasValue &&
|
||||
upgrade.AdditionalSeats > plan.MaxAdditionalSeats.Value)
|
||||
if (plan.PasswordManager.HasAdditionalSeatsOption && plan.PasswordManager.MaxAdditionalSeats.HasValue &&
|
||||
upgrade.AdditionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value)
|
||||
{
|
||||
throw new BadRequestException($"Selected plan allows a maximum of " +
|
||||
$"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
|
||||
$"{plan.PasswordManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
|
||||
}
|
||||
}
|
||||
|
||||
public void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
|
||||
{
|
||||
if (plan.SupportsSecretsManager == false)
|
||||
{
|
||||
throw new BadRequestException("Invalid Secrets Manager plan selected.");
|
||||
}
|
||||
|
||||
ValidatePlan(plan, upgrade.AdditionalSmSeats.GetValueOrDefault(), "Secrets Manager");
|
||||
|
||||
if (!plan.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0)
|
||||
if (plan.SecretsManager.BaseSeats + upgrade.AdditionalSmSeats <= 0)
|
||||
{
|
||||
throw new BadRequestException($"You do not have any Secrets Manager seats!");
|
||||
}
|
||||
|
||||
if (!plan.SecretsManager.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0)
|
||||
{
|
||||
throw new BadRequestException("Plan does not allow additional Service Accounts.");
|
||||
}
|
||||
@ -2027,14 +2027,14 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException("You can't subtract Service Accounts!");
|
||||
}
|
||||
|
||||
switch (plan.HasAdditionalSeatsOption)
|
||||
switch (plan.SecretsManager.HasAdditionalSeatsOption)
|
||||
{
|
||||
case false when upgrade.AdditionalSmSeats > 0:
|
||||
throw new BadRequestException("Plan does not allow additional users.");
|
||||
case true when plan.MaxAdditionalSeats.HasValue &&
|
||||
upgrade.AdditionalSmSeats > plan.MaxAdditionalSeats.Value:
|
||||
case true when plan.SecretsManager.MaxAdditionalSeats.HasValue &&
|
||||
upgrade.AdditionalSmSeats > plan.SecretsManager.MaxAdditionalSeats.Value:
|
||||
throw new BadRequestException($"Selected plan allows a maximum of " +
|
||||
$"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
|
||||
$"{plan.SecretsManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -2457,7 +2457,7 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
public async Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted)
|
||||
{
|
||||
var plan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||
if (plan is not { LegacyYear: null })
|
||||
{
|
||||
throw new BadRequestException("Invalid plan selected.");
|
||||
|
@ -49,7 +49,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
||||
string paymentToken, List<StaticStore.Plan> plans, short additionalStorageGb,
|
||||
string paymentToken, StaticStore.Plan plan, short additionalStorageGb,
|
||||
int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false,
|
||||
int additionalSmSeats = 0, int additionalServiceAccount = 0)
|
||||
{
|
||||
@ -119,7 +119,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
}
|
||||
|
||||
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plans, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon
|
||||
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon
|
||||
, additionalSmSeats, additionalServiceAccount);
|
||||
|
||||
Stripe.Customer customer = null;
|
||||
@ -211,7 +211,7 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
private async Task ChangeOrganizationSponsorship(Organization org, OrganizationSponsorship sponsorship, bool applySponsorship)
|
||||
{
|
||||
var existingPlan = Utilities.StaticStore.GetPasswordManagerPlan(org.PlanType);
|
||||
var existingPlan = Utilities.StaticStore.GetPlan(org.PlanType);
|
||||
var sponsoredPlan = sponsorship != null ?
|
||||
Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) :
|
||||
null;
|
||||
@ -231,7 +231,7 @@ public class StripePaymentService : IPaymentService
|
||||
public Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship) =>
|
||||
ChangeOrganizationSponsorship(org, sponsorship, false);
|
||||
|
||||
public async Task<string> UpgradeFreeOrganizationAsync(Organization org, List<StaticStore.Plan> plans,
|
||||
public async Task<string> UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan,
|
||||
OrganizationUpgrade upgrade)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
|
||||
@ -266,7 +266,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
}
|
||||
|
||||
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plans, upgrade);
|
||||
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade);
|
||||
var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions);
|
||||
|
||||
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
|
||||
|
Reference in New Issue
Block a user