1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-04 01:22:50 -05:00

Families for enterprise/stripe integrations (#1699)

* Add PlanSponsorshipType to static store

* Add sponsorship type to token and creates sponsorship

* PascalCase properties

* Require sponsorship for remove

* Create subscription sponsorship helper class

* Handle Sponsored subscription changes

* Add sponsorship id to subscription metadata

* Make sponsoring references nullable

This state indicates that a sponsorship has lapsed, but was not able to
be reverted for billing reasons

* WIP: Validate and remove subscriptions

* Update sponsorships on organization and org user delete

* Add friendly name to organization sponsorship
This commit is contained in:
Matt Gibson
2021-11-08 17:01:09 -06:00
committed by Justin Baur
parent 143be4273b
commit 45f6ec1781
42 changed files with 1060 additions and 188 deletions

View File

@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Microsoft.AspNetCore.DataProtection;
@ -13,12 +14,18 @@ namespace Bit.Core.Services
private const string TokenClearTextPrefix = "BWOrganizationSponsorship_";
private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IPaymentService _paymentService;
private readonly IDataProtector _dataProtector;
public OrganizationSponsorshipService(IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IOrganizationRepository organizationRepository,
IPaymentService paymentService,
IDataProtector dataProtector)
{
_organizationSponsorshipRepository = organizationSponsorshipRepository;
_organizationRepository = organizationRepository;
_paymentService = paymentService;
_dataProtector = dataProtector;
}
@ -63,13 +70,16 @@ namespace Bit.Core.Services
_dataProtector.Protect($"{FamiliesForEnterpriseTokenName} {sponsorshipId} {sponsorshipType}")
);
public async Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, PlanSponsorshipType sponsorshipType, string sponsoredEmail)
public async Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,
PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName)
{
var sponsorship = new OrganizationSponsorship
{
SponsoringOrganizationId = sponsoringOrg.Id,
SponsoringOrganizationUserId = sponsoringOrgUser.Id,
FriendlyName = friendlyName,
OfferedToEmail = sponsoredEmail,
PlanSponsorshipType = sponsorshipType,
CloudSponsor = true,
};
@ -78,6 +88,7 @@ namespace Bit.Core.Services
sponsorship = await _organizationSponsorshipRepository.CreateAsync(sponsorship);
// TODO: send email to sponsoredEmail w/ redemption token link
var _ = RedemptionToken(sponsorship.Id, sponsorshipType);
}
catch
{
@ -91,14 +102,117 @@ namespace Bit.Core.Services
public async Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization)
{
// TODO: set up sponsorship, remember remove offeredToEmail from sponsorship
throw new NotImplementedException();
if (sponsorship.PlanSponsorshipType == null)
{
throw new BadRequestException("Cannot set up sponsorship without a known sponsorship type.");
}
// TODO: rollback?
await _paymentService.SponsorOrganizationAsync(sponsoredOrganization, sponsorship);
await _organizationRepository.UpsertAsync(sponsoredOrganization);
sponsorship.SponsoredOrganizationId = sponsoredOrganization.Id;
sponsorship.OfferedToEmail = null;
await _organizationSponsorshipRepository.UpsertAsync(sponsorship);
}
public async Task RemoveSponsorshipAsync(OrganizationSponsorship sponsorship)
public async Task<bool> ValidateSponsorshipAsync(Guid sponsoredOrganizationId)
{
// TODO: remove sponsorship
throw new NotImplementedException();
var sponsoredOrganization = await _organizationRepository.GetByIdAsync(sponsoredOrganizationId);
var existingSponsorship = await _organizationSponsorshipRepository
.GetBySponsoredOrganizationIdAsync(sponsoredOrganizationId);
if (existingSponsorship == null)
{
await RemoveSponsorshipAsync(sponsoredOrganization);
// TODO on fail, mark org as disabled.
return false;
}
var validated = true;
if (existingSponsorship.SponsoringOrganizationId == null || existingSponsorship.SponsoringOrganizationUserId == null)
{
await RemoveSponsorshipAsync(sponsoredOrganization);
validated = false;
}
var sponsoringOrganization = await _organizationRepository
.GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value);
if (!sponsoringOrganization.Enabled)
{
await RemoveSponsorshipAsync(sponsoredOrganization);
validated = false;
}
if (!validated && existingSponsorship.SponsoredOrganizationId != null)
{
existingSponsorship.TimesRenewedWithoutValidation += 1;
existingSponsorship.SponsorshipLapsedDate ??= DateTime.UtcNow;
await _organizationSponsorshipRepository.UpsertAsync(existingSponsorship);
if (existingSponsorship.TimesRenewedWithoutValidation >= 6)
{
sponsoredOrganization.Enabled = false;
await _organizationRepository.UpsertAsync(sponsoredOrganization);
}
}
return true;
}
public async Task RemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null)
{
var success = await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization);
await _organizationRepository.UpsertAsync(sponsoredOrganization);
if (sponsorship == null)
{
return;
}
if (success)
{
// Initialize the record as available
sponsorship.SponsoredOrganizationId = null;
sponsorship.FriendlyName = null;
sponsorship.OfferedToEmail = null;
sponsorship.PlanSponsorshipType = null;
sponsorship.TimesRenewedWithoutValidation = 0;
sponsorship.SponsorshipLapsedDate = null;
if (sponsorship.CloudSponsor || sponsorship.SponsorshipLapsedDate.HasValue)
{
await _organizationSponsorshipRepository.DeleteAsync(sponsorship);
}
else
{
await _organizationSponsorshipRepository.UpsertAsync(sponsorship);
}
}
else
{
sponsorship.SponsoringOrganizationId = null;
sponsorship.SponsoringOrganizationUserId = null;
if (!sponsorship.CloudSponsor)
{
// Sef-hosted sponsorship record
// we need to make the existing sponsorship available, and add
// a new sponsorship record to record the lapsed sponsorship
var cleanSponsorship = new OrganizationSponsorship
{
InstallationId = sponsorship.InstallationId,
SponsoringOrganizationId = sponsorship.SponsoringOrganizationId,
SponsoringOrganizationUserId = sponsorship.SponsoringOrganizationUserId,
CloudSponsor = sponsorship.CloudSponsor,
};
await _organizationSponsorshipRepository.UpsertAsync(cleanSponsorship);
}
sponsorship.SponsorshipLapsedDate ??= DateTime.UtcNow;
await _organizationSponsorshipRepository.UpsertAsync(sponsorship);
}
}
}

View File

@ -192,6 +192,44 @@ namespace Bit.Core.Services
}
}
public async Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship)
{
var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId);
var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId);
var sponsoredSubscription = new SponsoredOrganizationSubscription(org, sub);
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
false, PaymentMethodType.None, sponsoredSubscription.GetSponsorSubscriptionOptions(sponsorship), null);
org.GatewaySubscriptionId = subscription.Id;
org.ExpirationDate = subscription.CurrentPeriodEnd;
}
public async Task<bool> RemoveOrganizationSponsorshipAsync(Organization org)
{
var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId);
var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId);
var sponsoredSubscription = new SponsoredOrganizationSubscription(org, sub);
var subCreateOptions = sponsoredSubscription.RemoveOrganizationSubscriptionOptions();
var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions);
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
stripePaymentMethod, paymentMethodType, subCreateOptions, null);
if (subscription.Status == "incomplete")
{
// TODO: revert
return false;
}
org.GatewaySubscriptionId = subscription.Id;
org.Enabled = true;
org.ExpirationDate = subscription.CurrentPeriodEnd;
return true;
}
public async Task<string> UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan,
short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo)
{
@ -227,6 +265,29 @@ namespace Bit.Core.Services
}
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon);
var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions);
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
stripePaymentMethod, paymentMethodType, subCreateOptions, null);
org.GatewaySubscriptionId = subscription.Id;
if (subscription.Status == "incomplete" &&
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
{
org.Enabled = false;
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
}
else
{
org.Enabled = true;
org.ExpirationDate = subscription.CurrentPeriodEnd;
return null;
}
}
private (bool stripePaymentMethod, PaymentMethodType PaymentMethodType) IdentifyPaymentMethod(
Stripe.Customer customer, Stripe.SubscriptionCreateOptions subCreateOptions)
{
var stripePaymentMethod = false;
var paymentMethodType = PaymentMethodType.Credit;
var hasBtCustomerId = customer.Metadata.ContainsKey("btCustomerId");
@ -265,23 +326,7 @@ namespace Bit.Core.Services
}
}
}
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
stripePaymentMethod, paymentMethodType, subCreateOptions, null);
org.GatewaySubscriptionId = subscription.Id;
if (subscription.Status == "incomplete" &&
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
{
org.Enabled = false;
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
}
else
{
org.Enabled = true;
org.ExpirationDate = subscription.CurrentPeriodEnd;
return null;
}
return (stripePaymentMethod, paymentMethodType);
}
public async Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType,