mirror of
https://github.com/bitwarden/server.git
synced 2025-04-21 13:05:11 -05:00
upgrade org api
This commit is contained in:
parent
81788b3eb0
commit
5bfed59f9c
@ -223,7 +223,7 @@ namespace Bit.Api.Controllers
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _organizationService.UpgradePlanAsync(orgIdGuid, model.PlanType, model.AdditionalSeats);
|
await _organizationService.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/seat")]
|
[HttpPost("{id}/seat")]
|
||||||
|
@ -1,12 +1,30 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Api
|
namespace Bit.Core.Models.Api
|
||||||
{
|
{
|
||||||
public class OrganizationUpgradeRequestModel
|
public class OrganizationUpgradeRequestModel
|
||||||
{
|
{
|
||||||
|
[StringLength(50)]
|
||||||
|
public string BusinessName { get; set; }
|
||||||
public PlanType PlanType { get; set; }
|
public PlanType PlanType { get; set; }
|
||||||
[Range(0, double.MaxValue)]
|
[Range(0, double.MaxValue)]
|
||||||
public short AdditionalSeats { get; set; }
|
public short AdditionalSeats { get; set; }
|
||||||
|
[Range(0, 99)]
|
||||||
|
public short? AdditionalStorageGb { get; set; }
|
||||||
|
public bool PremiumAccessAddon { get; set; }
|
||||||
|
|
||||||
|
public OrganizationUpgrade ToOrganizationUpgrade()
|
||||||
|
{
|
||||||
|
return new OrganizationUpgrade
|
||||||
|
{
|
||||||
|
AdditionalSeats = AdditionalSeats,
|
||||||
|
AdditionalStorageGb = AdditionalStorageGb.GetValueOrDefault(),
|
||||||
|
BusinessName = BusinessName,
|
||||||
|
Plan = PlanType,
|
||||||
|
PremiumAccessAddon = PremiumAccessAddon
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,19 +3,14 @@ using Bit.Core.Models.Table;
|
|||||||
|
|
||||||
namespace Bit.Core.Models.Business
|
namespace Bit.Core.Models.Business
|
||||||
{
|
{
|
||||||
public class OrganizationSignup
|
public class OrganizationSignup : OrganizationUpgrade
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string BusinessName { get; set; }
|
|
||||||
public string BillingEmail { get; set; }
|
public string BillingEmail { get; set; }
|
||||||
public User Owner { get; set; }
|
public User Owner { get; set; }
|
||||||
public string OwnerKey { get; set; }
|
public string OwnerKey { get; set; }
|
||||||
public PlanType Plan { get; set; }
|
public string CollectionName { get; set; }
|
||||||
public short AdditionalSeats { get; set; }
|
|
||||||
public short AdditionalStorageGb { get; set; }
|
|
||||||
public bool PremiumAccessAddon { get; set; }
|
|
||||||
public PaymentMethodType? PaymentMethodType { get; set; }
|
public PaymentMethodType? PaymentMethodType { get; set; }
|
||||||
public string PaymentToken { get; set; }
|
public string PaymentToken { get; set; }
|
||||||
public string CollectionName { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
src/Core/Models/Business/OrganizationUpgrade.cs
Normal file
13
src/Core/Models/Business/OrganizationUpgrade.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Business
|
||||||
|
{
|
||||||
|
public class OrganizationUpgrade
|
||||||
|
{
|
||||||
|
public string BusinessName { get; set; }
|
||||||
|
public PlanType Plan { get; set; }
|
||||||
|
public short AdditionalSeats { get; set; }
|
||||||
|
public short AdditionalStorageGb { get; set; }
|
||||||
|
public bool PremiumAccessAddon { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@ namespace Bit.Core.Services
|
|||||||
Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, PaymentMethodType paymentMethodType);
|
Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, PaymentMethodType paymentMethodType);
|
||||||
Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null);
|
Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null);
|
||||||
Task ReinstateSubscriptionAsync(Guid organizationId);
|
Task ReinstateSubscriptionAsync(Guid organizationId);
|
||||||
Task UpgradePlanAsync(Guid organizationId, PlanType plan, int additionalSeats);
|
Task UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade);
|
||||||
Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb);
|
Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb);
|
||||||
Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment);
|
Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment);
|
||||||
Task VerifyBankAsync(Guid organizationId, int amount1, int amount2);
|
Task VerifyBankAsync(Guid organizationId, int amount1, int amount2);
|
||||||
|
@ -10,6 +10,8 @@ namespace Bit.Core.Services
|
|||||||
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
|
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
|
||||||
Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, string paymentToken,
|
Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, string paymentToken,
|
||||||
Models.StaticStore.Plan plan, short additionalStorageGb, short additionalSeats, bool premiumAccessAddon);
|
Models.StaticStore.Plan plan, short additionalStorageGb, short additionalSeats, bool premiumAccessAddon);
|
||||||
|
Task UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan,
|
||||||
|
short additionalStorageGb, short additionalSeats, bool premiumAccessAddon);
|
||||||
Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
|
Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
|
||||||
short additionalStorageGb);
|
short additionalStorageGb);
|
||||||
Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId);
|
Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId);
|
||||||
|
@ -117,7 +117,7 @@ namespace Bit.Core.Services
|
|||||||
await _paymentService.ReinstateSubscriptionAsync(organization);
|
await _paymentService.ReinstateSubscriptionAsync(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpgradePlanAsync(Guid organizationId, PlanType plan, int additionalSeats)
|
public async Task UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade)
|
||||||
{
|
{
|
||||||
var organization = await GetOrgById(organizationId);
|
var organization = await GetOrgById(organizationId);
|
||||||
if(organization == null)
|
if(organization == null)
|
||||||
@ -127,7 +127,7 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
if(string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("No payment method found.");
|
throw new BadRequestException("Your account has no payment method available.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
var existingPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||||
@ -136,7 +136,7 @@ namespace Bit.Core.Services
|
|||||||
throw new BadRequestException("Existing plan not found.");
|
throw new BadRequestException("Existing plan not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var newPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == plan && !p.Disabled);
|
var newPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled);
|
||||||
if(newPlan == null)
|
if(newPlan == null)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Plan not found.");
|
throw new BadRequestException("Plan not found.");
|
||||||
@ -152,31 +152,27 @@ namespace Bit.Core.Services
|
|||||||
throw new BadRequestException("You cannot upgrade to this plan.");
|
throw new BadRequestException("You cannot upgrade to this plan.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!newPlan.CanBuyAdditionalSeats && additionalSeats > 0)
|
if(existingPlan.Type != PlanType.Free)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Plan does not allow additional seats.");
|
throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(newPlan.CanBuyAdditionalSeats && newPlan.MaxAdditionalSeats.HasValue &&
|
ValidateOrganizationUpgradeParameters(newPlan, upgrade);
|
||||||
additionalSeats > newPlan.MaxAdditionalSeats.Value)
|
|
||||||
{
|
|
||||||
throw new BadRequestException($"Selected plan allows a maximum of " +
|
|
||||||
$"{newPlan.MaxAdditionalSeats.Value} additional seats.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var newPlanSeats = (short)(newPlan.BaseSeats + (newPlan.CanBuyAdditionalSeats ? additionalSeats : 0));
|
var newPlanSeats = (short)(newPlan.BaseSeats +
|
||||||
|
(newPlan.CanBuyAdditionalSeats ? upgrade.AdditionalSeats : 0));
|
||||||
if(!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats)
|
if(!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats)
|
||||||
{
|
{
|
||||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
|
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
|
||||||
if(userCount > newPlanSeats)
|
if(userCount > newPlanSeats)
|
||||||
{
|
{
|
||||||
throw new BadRequestException($"Your organization currently has {userCount} seats filled. Your new plan " +
|
throw new BadRequestException($"Your organization currently has {userCount} seats filled. " +
|
||||||
$"only has ({newPlanSeats}) seats. Remove some users.");
|
$"Your new plan only has ({newPlanSeats}) seats. Remove some users.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(newPlan.MaxCollections.HasValue &&
|
if(newPlan.MaxCollections.HasValue && (!organization.MaxCollections.HasValue ||
|
||||||
(!organization.MaxCollections.HasValue || organization.MaxCollections.Value > newPlan.MaxCollections.Value))
|
organization.MaxCollections.Value > newPlan.MaxCollections.Value))
|
||||||
{
|
{
|
||||||
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id);
|
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id);
|
||||||
if(collectionCount > newPlan.MaxCollections.Value)
|
if(collectionCount > newPlan.MaxCollections.Value)
|
||||||
@ -187,72 +183,47 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Groups?
|
if(!newPlan.UseGroups && organization.UseGroups)
|
||||||
|
{
|
||||||
|
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);
|
||||||
|
if(groups.Any())
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Your new plan does not allow the groups feature. " +
|
||||||
|
$"Remove your groups.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check storage?
|
||||||
|
|
||||||
var subscriptionService = new Stripe.SubscriptionService();
|
|
||||||
if(string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
if(string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||||
{
|
{
|
||||||
// They must have been on a free plan. Create new sub.
|
await _paymentService.UpgradeFreeOrganizationAsync(organization, newPlan,
|
||||||
var subCreateOptions = new SubscriptionCreateOptions
|
upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon);
|
||||||
{
|
|
||||||
CustomerId = organization.GatewayCustomerId,
|
|
||||||
TrialPeriodDays = newPlan.TrialPeriodDays,
|
|
||||||
Items = new List<SubscriptionItemOption>(),
|
|
||||||
Metadata = new Dictionary<string, string> {
|
|
||||||
{ "organizationId", organization.Id.ToString() }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if(newPlan.StripePlanId != null)
|
|
||||||
{
|
|
||||||
subCreateOptions.Items.Add(new SubscriptionItemOption
|
|
||||||
{
|
|
||||||
PlanId = newPlan.StripePlanId,
|
|
||||||
Quantity = 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if(additionalSeats > 0 && newPlan.StripeSeatPlanId != null)
|
|
||||||
{
|
|
||||||
subCreateOptions.Items.Add(new SubscriptionItemOption
|
|
||||||
{
|
|
||||||
PlanId = newPlan.StripeSeatPlanId,
|
|
||||||
Quantity = additionalSeats
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await subscriptionService.CreateAsync(subCreateOptions);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Update existing sub.
|
// TODO: Update existing sub
|
||||||
var subUpdateOptions = new SubscriptionUpdateOptions
|
throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
|
||||||
{
|
|
||||||
Items = new List<SubscriptionItemUpdateOption>()
|
|
||||||
};
|
|
||||||
|
|
||||||
if(newPlan.StripePlanId != null)
|
|
||||||
{
|
|
||||||
subUpdateOptions.Items.Add(new SubscriptionItemUpdateOption
|
|
||||||
{
|
|
||||||
PlanId = newPlan.StripePlanId,
|
|
||||||
Quantity = 1
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(additionalSeats > 0 && newPlan.StripeSeatPlanId != null)
|
organization.BusinessName = upgrade.BusinessName;
|
||||||
{
|
organization.PlanType = newPlan.Type;
|
||||||
subUpdateOptions.Items.Add(new SubscriptionItemUpdateOption
|
organization.Seats = (short)(newPlan.BaseSeats + upgrade.AdditionalSeats);
|
||||||
{
|
organization.MaxCollections = newPlan.MaxCollections;
|
||||||
PlanId = newPlan.StripeSeatPlanId,
|
organization.MaxStorageGb = !newPlan.MaxStorageGb.HasValue ?
|
||||||
Quantity = additionalSeats
|
(short?)null : (short)(newPlan.MaxStorageGb.Value + upgrade.AdditionalStorageGb);
|
||||||
});
|
organization.UseGroups = newPlan.UseGroups;
|
||||||
}
|
organization.UseDirectory = newPlan.UseDirectory;
|
||||||
|
organization.UseEvents = newPlan.UseEvents;
|
||||||
await subscriptionService.UpdateAsync(organization.GatewaySubscriptionId, subUpdateOptions);
|
organization.UseTotp = newPlan.UseTotp;
|
||||||
}
|
organization.Use2fa = newPlan.Use2fa;
|
||||||
|
organization.UseApi = newPlan.UseApi;
|
||||||
// TODO: Update organization
|
organization.SelfHost = newPlan.SelfHost;
|
||||||
|
organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon;
|
||||||
|
organization.Plan = newPlan.Name;
|
||||||
|
organization.Enabled = true;
|
||||||
|
organization.RevisionDate = DateTime.UtcNow;
|
||||||
|
await ReplaceAndUpdateCache(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
|
public async Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
|
||||||
@ -459,42 +430,7 @@ namespace Bit.Core.Services
|
|||||||
throw new BadRequestException("Plan not found.");
|
throw new BadRequestException("Plan not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!plan.MaxStorageGb.HasValue && signup.AdditionalStorageGb > 0)
|
ValidateOrganizationUpgradeParameters(plan, signup);
|
||||||
{
|
|
||||||
throw new BadRequestException("Plan does not allow additional storage.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(signup.AdditionalStorageGb < 0)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("You can't subtract storage!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!plan.CanBuyPremiumAccessAddon && signup.PremiumAccessAddon)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("This plan does not allow you to buy the premium access addon.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(plan.BaseSeats + signup.AdditionalSeats <= 0)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("You do not have any seats!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(signup.AdditionalSeats < 0)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("You can't subtract seats!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!plan.CanBuyAdditionalSeats && signup.AdditionalSeats > 0)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Plan does not allow additional users.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(plan.CanBuyAdditionalSeats && plan.MaxAdditionalSeats.HasValue &&
|
|
||||||
signup.AdditionalSeats > plan.MaxAdditionalSeats.Value)
|
|
||||||
{
|
|
||||||
throw new BadRequestException($"Selected plan allows a maximum of " +
|
|
||||||
$"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = new Organization
|
var organization = new Organization
|
||||||
{
|
{
|
||||||
@ -644,7 +580,8 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
// push
|
// push
|
||||||
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
|
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
|
||||||
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds, organization.Id.ToString());
|
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds,
|
||||||
|
organization.Id.ToString());
|
||||||
await _pushNotificationService.PushSyncOrgKeysAsync(ownerId);
|
await _pushNotificationService.PushSyncOrgKeysAsync(ownerId);
|
||||||
|
|
||||||
return new Tuple<Organization, OrganizationUser>(organization, orgUser);
|
return new Tuple<Organization, OrganizationUser>(organization, orgUser);
|
||||||
@ -1395,5 +1332,45 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
return await _organizationRepository.GetByIdAsync(id);
|
return await _organizationRepository.GetByIdAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ValidateOrganizationUpgradeParameters(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
|
||||||
|
{
|
||||||
|
if(!plan.MaxStorageGb.HasValue && upgrade.AdditionalStorageGb > 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Plan does not allow additional storage.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(upgrade.AdditionalStorageGb < 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You can't subtract storage!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!plan.CanBuyPremiumAccessAddon && upgrade.PremiumAccessAddon)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("This plan does not allow you to buy the premium access addon.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(plan.BaseSeats + upgrade.AdditionalSeats <= 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You do not have any seats!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(upgrade.AdditionalSeats < 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You can't subtract seats!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!plan.CanBuyAdditionalSeats && upgrade.AdditionalSeats > 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Plan does not allow additional users.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(plan.CanBuyAdditionalSeats && plan.MaxAdditionalSeats.HasValue &&
|
||||||
|
upgrade.AdditionalSeats > plan.MaxAdditionalSeats.Value)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Selected plan allows a maximum of " +
|
||||||
|
$"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,6 +162,95 @@ namespace Bit.Core.Services
|
|||||||
org.ExpirationDate = subscription.CurrentPeriodEnd;
|
org.ExpirationDate = subscription.CurrentPeriodEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan,
|
||||||
|
short additionalStorageGb, short additionalSeats, bool premiumAccessAddon)
|
||||||
|
{
|
||||||
|
if(!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Organization already has a subscription.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var customerService = new CustomerService();
|
||||||
|
customerService.ExpandDefaultSource = true;
|
||||||
|
var customer = await customerService.GetAsync(org.GatewayCustomerId);
|
||||||
|
if(customer == null)
|
||||||
|
{
|
||||||
|
throw new GatewayException("Could not find customer payment profile.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var subCreateOptions = new SubscriptionCreateOptions
|
||||||
|
{
|
||||||
|
CustomerId = customer.Id,
|
||||||
|
Items = new List<SubscriptionItemOption>(),
|
||||||
|
Metadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
[org.GatewayIdField()] = org.Id.ToString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if(plan.StripePlanId != null)
|
||||||
|
{
|
||||||
|
subCreateOptions.Items.Add(new SubscriptionItemOption
|
||||||
|
{
|
||||||
|
PlanId = plan.StripePlanId,
|
||||||
|
Quantity = 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(additionalSeats > 0 && plan.StripeSeatPlanId != null)
|
||||||
|
{
|
||||||
|
subCreateOptions.Items.Add(new SubscriptionItemOption
|
||||||
|
{
|
||||||
|
PlanId = plan.StripeSeatPlanId,
|
||||||
|
Quantity = additionalSeats
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(additionalStorageGb > 0)
|
||||||
|
{
|
||||||
|
subCreateOptions.Items.Add(new SubscriptionItemOption
|
||||||
|
{
|
||||||
|
PlanId = plan.StripeStoragePlanId,
|
||||||
|
Quantity = additionalStorageGb
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(premiumAccessAddon && plan.StripePremiumAccessPlanId != null)
|
||||||
|
{
|
||||||
|
subCreateOptions.Items.Add(new SubscriptionItemOption
|
||||||
|
{
|
||||||
|
PlanId = plan.StripePremiumAccessPlanId,
|
||||||
|
Quantity = 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var stripePaymentMethod = false;
|
||||||
|
var paymentMethodType = PaymentMethodType.Credit;
|
||||||
|
var hasBtCustomerId = customer.Metadata.ContainsKey("btCustomerId");
|
||||||
|
if(hasBtCustomerId)
|
||||||
|
{
|
||||||
|
paymentMethodType = PaymentMethodType.PayPal;
|
||||||
|
}
|
||||||
|
if(!hasBtCustomerId && customer.DefaultSource != null)
|
||||||
|
{
|
||||||
|
if(customer.DefaultSource is Card || customer.DefaultSource is SourceCard)
|
||||||
|
{
|
||||||
|
paymentMethodType = PaymentMethodType.Card;
|
||||||
|
stripePaymentMethod = true;
|
||||||
|
}
|
||||||
|
else if(customer.DefaultSource is BankAccount || customer.DefaultSource is SourceAchDebit)
|
||||||
|
{
|
||||||
|
paymentMethodType = PaymentMethodType.BankAccount;
|
||||||
|
stripePaymentMethod = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
|
||||||
|
stripePaymentMethod, paymentMethodType, subCreateOptions, null);
|
||||||
|
org.GatewaySubscriptionId = subscription.Id;
|
||||||
|
org.ExpirationDate = subscription.CurrentPeriodEnd;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
|
public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
|
||||||
short additionalStorageGb)
|
short additionalStorageGb)
|
||||||
{
|
{
|
||||||
@ -179,13 +268,9 @@ namespace Bit.Core.Services
|
|||||||
throw new GatewayException("Bank account payment method is not supported at this time.");
|
throw new GatewayException("Bank account payment method is not supported at this time.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var invoiceService = new InvoiceService();
|
|
||||||
var customerService = new CustomerService();
|
var customerService = new CustomerService();
|
||||||
|
|
||||||
var createdStripeCustomer = false;
|
var createdStripeCustomer = false;
|
||||||
var addedCreditToStripeCustomer = false;
|
|
||||||
Customer customer = null;
|
Customer customer = null;
|
||||||
Braintree.Transaction braintreeTransaction = null;
|
|
||||||
Braintree.Customer braintreeCustomer = null;
|
Braintree.Customer braintreeCustomer = null;
|
||||||
var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card ||
|
var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card ||
|
||||||
paymentMethodType == PaymentMethodType.BankAccount || paymentMethodType == PaymentMethodType.Credit;
|
paymentMethodType == PaymentMethodType.BankAccount || paymentMethodType == PaymentMethodType.Credit;
|
||||||
@ -283,6 +368,25 @@ namespace Bit.Core.Services
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var subscription = await ChargeForNewSubscriptionAsync(user, customer, createdStripeCustomer,
|
||||||
|
stripePaymentMethod, paymentMethodType, subCreateOptions, braintreeCustomer);
|
||||||
|
|
||||||
|
user.Gateway = GatewayType.Stripe;
|
||||||
|
user.GatewayCustomerId = customer.Id;
|
||||||
|
user.GatewaySubscriptionId = subscription.Id;
|
||||||
|
user.Premium = true;
|
||||||
|
user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Subscription> ChargeForNewSubscriptionAsync(ISubscriber subcriber, Customer customer,
|
||||||
|
bool createdStripeCustomer, bool stripePaymentMethod, PaymentMethodType paymentMethodType,
|
||||||
|
SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer)
|
||||||
|
{
|
||||||
|
var addedCreditToStripeCustomer = false;
|
||||||
|
Braintree.Transaction braintreeTransaction = null;
|
||||||
|
var invoiceService = new InvoiceService();
|
||||||
|
var customerService = new CustomerService();
|
||||||
|
|
||||||
var subInvoiceMetadata = new Dictionary<string, string>();
|
var subInvoiceMetadata = new Dictionary<string, string>();
|
||||||
Subscription subscription = null;
|
Subscription subscription = null;
|
||||||
try
|
try
|
||||||
@ -312,12 +416,12 @@ namespace Bit.Core.Services
|
|||||||
SubmitForSettlement = true,
|
SubmitForSettlement = true,
|
||||||
PayPal = new Braintree.TransactionOptionsPayPalRequest
|
PayPal = new Braintree.TransactionOptionsPayPalRequest
|
||||||
{
|
{
|
||||||
CustomField = $"{user.BraintreeIdField()}:{user.Id}"
|
CustomField = $"{subcriber.BraintreeIdField()}:{subcriber.Id}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
CustomFields = new Dictionary<string, string>
|
CustomFields = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
[user.BraintreeIdField()] = user.Id.ToString()
|
[subcriber.BraintreeIdField()] = subcriber.Id.ToString()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -377,6 +481,8 @@ namespace Bit.Core.Services
|
|||||||
Metadata = subInvoiceMetadata
|
Metadata = subInvoiceMetadata
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return subscription;
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
@ -386,7 +492,7 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
await customerService.DeleteAsync(customer.Id);
|
await customerService.DeleteAsync(customer.Id);
|
||||||
}
|
}
|
||||||
else if(addedCreditToStripeCustomer)
|
else if(addedCreditToStripeCustomer || customer.AccountBalance < 0)
|
||||||
{
|
{
|
||||||
await customerService.UpdateAsync(customer.Id, new CustomerUpdateOptions
|
await customerService.UpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||||
{
|
{
|
||||||
@ -402,14 +508,15 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
|
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
|
||||||
}
|
}
|
||||||
throw e;
|
|
||||||
|
if(e is StripeException strEx &&
|
||||||
|
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
|
||||||
|
{
|
||||||
|
throw new GatewayException("Bank account is not yet verified.");
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Gateway = GatewayType.Stripe;
|
throw e;
|
||||||
user.GatewayCustomerId = customer.Id;
|
}
|
||||||
user.GatewaySubscriptionId = subscription.Id;
|
|
||||||
user.Premium = true;
|
|
||||||
user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions(
|
private List<InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions(
|
||||||
@ -721,6 +828,13 @@ namespace Bit.Core.Services
|
|||||||
await invoiceItemService.DeleteAsync(ii.Id);
|
await invoiceItemService.DeleteAsync(ii.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(e is 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 e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user