1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-06 05:28:15 -05:00

Add additional return properties ti providerSubscriptionResponse (#4159)

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>
This commit is contained in:
cyprain-okeke 2024-06-06 15:54:08 +01:00 committed by GitHub
parent 97b3f3e7ee
commit fef34d845f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 84 additions and 48 deletions

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
@ -30,7 +31,8 @@ public class ProviderBillingService(
IProviderPlanRepository providerPlanRepository, IProviderPlanRepository providerPlanRepository,
IProviderRepository providerRepository, IProviderRepository providerRepository,
IStripeAdapter stripeAdapter, IStripeAdapter stripeAdapter,
ISubscriberService subscriberService) : IProviderBillingService ISubscriberService subscriberService,
IFeatureService featureService) : IProviderBillingService
{ {
public async Task AssignSeatsToClientOrganization( public async Task AssignSeatsToClientOrganization(
Provider provider, Provider provider,
@ -248,6 +250,18 @@ public class ProviderBillingService(
return null; return null;
} }
DateTime? subscriptionSuspensionDate = null;
DateTime? subscriptionUnpaidPeriodEndDate = null;
if (featureService.IsEnabled(FeatureFlagKeys.AC1795_UpdatedSubscriptionStatusSection))
{
var (suspensionDate, unpaidPeriodEndDate) = await paymentService.GetSuspensionDateAsync(subscription);
if (suspensionDate.HasValue && unpaidPeriodEndDate.HasValue)
{
subscriptionSuspensionDate = suspensionDate;
subscriptionUnpaidPeriodEndDate = unpaidPeriodEndDate;
}
}
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
var configuredProviderPlans = providerPlans var configuredProviderPlans = providerPlans
@ -257,7 +271,9 @@ public class ProviderBillingService(
return new ConsolidatedBillingSubscriptionDTO( return new ConsolidatedBillingSubscriptionDTO(
configuredProviderPlans, configuredProviderPlans,
subscription); subscription,
subscriptionSuspensionDate,
subscriptionUnpaidPeriodEndDate);
} }
public async Task ScaleSeats( public async Task ScaleSeats(

View File

@ -7,6 +7,10 @@ public record ConsolidatedBillingSubscriptionResponse(
string Status, string Status,
DateTime CurrentPeriodEndDate, DateTime CurrentPeriodEndDate,
decimal? DiscountPercentage, decimal? DiscountPercentage,
string CollectionMethod,
DateTime? UnpaidPeriodEndDate,
int? GracePeriod,
DateTime? SuspensionDate,
IEnumerable<ProviderPlanResponse> Plans) IEnumerable<ProviderPlanResponse> Plans)
{ {
private const string _annualCadence = "Annual"; private const string _annualCadence = "Annual";
@ -15,7 +19,7 @@ public record ConsolidatedBillingSubscriptionResponse(
public static ConsolidatedBillingSubscriptionResponse From( public static ConsolidatedBillingSubscriptionResponse From(
ConsolidatedBillingSubscriptionDTO consolidatedBillingSubscription) ConsolidatedBillingSubscriptionDTO consolidatedBillingSubscription)
{ {
var (providerPlans, subscription) = consolidatedBillingSubscription; var (providerPlans, subscription, suspensionDate, unpaidPeriodEndDate) = consolidatedBillingSubscription;
var providerPlansDTO = providerPlans var providerPlansDTO = providerPlans
.Select(providerPlan => .Select(providerPlan =>
@ -31,11 +35,15 @@ public record ConsolidatedBillingSubscriptionResponse(
cost, cost,
cadence); cadence);
}); });
var gracePeriod = subscription.CollectionMethod == "charge_automatically" ? 14 : 30;
return new ConsolidatedBillingSubscriptionResponse( return new ConsolidatedBillingSubscriptionResponse(
subscription.Status, subscription.Status,
subscription.CurrentPeriodEnd, subscription.CurrentPeriodEnd,
subscription.Customer?.Discount?.Coupon?.PercentOff, subscription.Customer?.Discount?.Coupon?.PercentOff,
subscription.CollectionMethod,
unpaidPeriodEndDate,
gracePeriod,
suspensionDate,
providerPlansDTO); providerPlansDTO);
} }
} }

View File

@ -4,4 +4,6 @@ namespace Bit.Core.Billing.Models;
public record ConsolidatedBillingSubscriptionDTO( public record ConsolidatedBillingSubscriptionDTO(
List<ConfiguredProviderPlanDTO> ProviderPlans, List<ConfiguredProviderPlanDTO> ProviderPlans,
Subscription Subscription); Subscription Subscription,
DateTime? SuspensionDate,
DateTime? UnpaidPeriodEndDate);

View File

@ -55,4 +55,5 @@ public interface IPaymentService
int additionalServiceAccount); int additionalServiceAccount);
Task<bool> RisksSubscriptionFailure(Organization organization); Task<bool> RisksSubscriptionFailure(Organization organization);
Task<bool> HasSecretsManagerStandalone(Organization organization); Task<bool> HasSecretsManagerStandalone(Organization organization);
Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Stripe.Subscription subscription);
} }

View File

@ -1820,6 +1820,47 @@ public class StripePaymentService : IPaymentService
return customer?.Discount?.Coupon?.Id == SecretsManagerStandaloneDiscountId; return customer?.Discount?.Coupon?.Id == SecretsManagerStandaloneDiscountId;
} }
public async Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Subscription subscription)
{
if (subscription.Status is not "past_due" && subscription.Status is not "unpaid")
{
return (null, null);
}
var openInvoices = await _stripeAdapter.InvoiceSearchAsync(new InvoiceSearchOptions
{
Query = $"subscription:'{subscription.Id}' status:'open'"
});
if (openInvoices.Count == 0)
{
return (null, null);
}
var currentDate = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;
switch (subscription.CollectionMethod)
{
case "charge_automatically":
{
var firstOverdueInvoice = openInvoices
.Where(invoice => invoice.PeriodEnd < currentDate && invoice.Attempted)
.MinBy(invoice => invoice.Created);
return (firstOverdueInvoice?.Created.AddDays(14), firstOverdueInvoice?.PeriodEnd);
}
case "send_invoice":
{
var firstOverdueInvoice = openInvoices
.Where(invoice => invoice.DueDate < currentDate)
.MinBy(invoice => invoice.Created);
return (firstOverdueInvoice?.DueDate?.AddDays(30), firstOverdueInvoice?.PeriodEnd);
}
default: return (null, null);
}
}
private PaymentMethod GetLatestCardPaymentMethod(string customerId) private PaymentMethod GetLatestCardPaymentMethod(string customerId)
{ {
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging( var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(
@ -1962,45 +2003,4 @@ public class StripePaymentService : IPaymentService
? subscriberName ? subscriberName
: subscriberName[..30]; : subscriberName[..30];
} }
private async Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Subscription subscription)
{
if (subscription.Status is not "past_due" && subscription.Status is not "unpaid")
{
return (null, null);
}
var openInvoices = await _stripeAdapter.InvoiceSearchAsync(new InvoiceSearchOptions
{
Query = $"subscription:'{subscription.Id}' status:'open'"
});
if (openInvoices.Count == 0)
{
return (null, null);
}
var currentDate = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;
switch (subscription.CollectionMethod)
{
case "charge_automatically":
{
var firstOverdueInvoice = openInvoices
.Where(invoice => invoice.PeriodEnd < currentDate && invoice.Attempted)
.MinBy(invoice => invoice.Created);
return (firstOverdueInvoice?.Created.AddDays(14), firstOverdueInvoice?.PeriodEnd);
}
case "send_invoice":
{
var firstOverdueInvoice = openInvoices
.Where(invoice => invoice.DueDate < currentDate)
.MinBy(invoice => invoice.Created);
return (firstOverdueInvoice?.DueDate?.AddDays(30), firstOverdueInvoice?.PeriodEnd);
}
default: return (null, null);
}
}
} }

View File

@ -282,9 +282,14 @@ public class ProviderBillingControllerTests
Customer = new Customer { Discount = new Discount { Coupon = new Coupon { PercentOff = 10 } } } Customer = new Customer { Discount = new Discount { Coupon = new Coupon { PercentOff = 10 } } }
}; };
DateTime? SuspensionDate = new DateTime();
DateTime? UnpaidPeriodEndDate = new DateTime();
var gracePeriod = 30;
var consolidatedBillingSubscription = new ConsolidatedBillingSubscriptionDTO( var consolidatedBillingSubscription = new ConsolidatedBillingSubscriptionDTO(
configuredProviderPlans, configuredProviderPlans,
subscription); subscription,
SuspensionDate,
UnpaidPeriodEndDate);
sutProvider.GetDependency<IProviderBillingService>().GetConsolidatedBillingSubscription(provider) sutProvider.GetDependency<IProviderBillingService>().GetConsolidatedBillingSubscription(provider)
.Returns(consolidatedBillingSubscription); .Returns(consolidatedBillingSubscription);
@ -298,6 +303,10 @@ public class ProviderBillingControllerTests
Assert.Equal(response.Status, subscription.Status); Assert.Equal(response.Status, subscription.Status);
Assert.Equal(response.CurrentPeriodEndDate, subscription.CurrentPeriodEnd); Assert.Equal(response.CurrentPeriodEndDate, subscription.CurrentPeriodEnd);
Assert.Equal(response.DiscountPercentage, subscription.Customer!.Discount!.Coupon!.PercentOff); Assert.Equal(response.DiscountPercentage, subscription.Customer!.Discount!.Coupon!.PercentOff);
Assert.Equal(response.CollectionMethod, subscription.CollectionMethod);
Assert.Equal(response.UnpaidPeriodEndDate, UnpaidPeriodEndDate);
Assert.Equal(response.GracePeriod, gracePeriod);
Assert.Equal(response.SuspensionDate, SuspensionDate);
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
var providerTeamsPlan = response.Plans.FirstOrDefault(plan => plan.PlanName == teamsPlan.Name); var providerTeamsPlan = response.Plans.FirstOrDefault(plan => plan.PlanName == teamsPlan.Name);