1
0
mirror of https://github.com/bitwarden/server.git synced 2025-01-13 20:21:22 +01:00

[AC-1795] Provide extra subscription info when past due (#3950)

* Provide past due data on subscription

* Add feature flag
This commit is contained in:
Alex Morask 2024-04-02 13:21:40 -04:00 committed by GitHub
parent 48da6eba1c
commit a048d6d9e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 78 additions and 1 deletions

View File

@ -75,6 +75,10 @@ public class BillingSubscription
{
Items = sub.Items.Select(i => new BillingSubscriptionItem(i));
}
CollectionMethod = sub.CollectionMethod;
SuspensionDate = sub.SuspensionDate;
UnpaidPeriodEndDate = sub.UnpaidPeriodEndDate;
GracePeriod = sub.GracePeriod;
}
public DateTime? TrialStartDate { get; set; }
@ -86,6 +90,10 @@ public class BillingSubscription
public string Status { get; set; }
public bool Cancelled { get; set; }
public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>();
public string CollectionMethod { get; set; }
public DateTime? SuspensionDate { get; set; }
public DateTime? UnpaidPeriodEndDate { get; set; }
public int? GracePeriod { get; set; }
public class BillingSubscriptionItem
{

View File

@ -131,6 +131,7 @@ public static class FeatureFlagKeys
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
public const string ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners";
public const string EnableConsolidatedBilling = "enable-consolidated-billing";
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
public static List<string> GetAllKeys()
{

View File

@ -43,6 +43,9 @@ public class SubscriptionInfo
Items = sub.Items.Data.Select(i => new BillingSubscriptionItem(i));
}
CollectionMethod = sub.CollectionMethod;
GracePeriod = sub.CollectionMethod == "charge_automatically"
? 14
: 30;
}
public DateTime? TrialStartDate { get; set; }
@ -56,6 +59,9 @@ public class SubscriptionInfo
public bool Cancelled { get; set; }
public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>();
public string CollectionMethod { get; set; }
public DateTime? SuspensionDate { get; set; }
public DateTime? UnpaidPeriodEndDate { get; set; }
public int GracePeriod { get; set; }
public class BillingSubscriptionItem
{

View File

@ -1,4 +1,5 @@
using Bit.Core.Models.BitStripe;
using Stripe;
namespace Bit.Core.Services;
@ -16,6 +17,7 @@ public interface IStripeAdapter
Task<Stripe.Invoice> InvoiceUpcomingAsync(Stripe.UpcomingInvoiceOptions options);
Task<Stripe.Invoice> InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options);
Task<List<Stripe.Invoice>> InvoiceListAsync(StripeInvoiceListOptions options);
Task<List<Stripe.Invoice>> InvoiceSearchAsync(InvoiceSearchOptions options);
Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options);
Task<Stripe.Invoice> InvoiceFinalizeInvoiceAsync(string id, Stripe.InvoiceFinalizeOptions options);
Task<Stripe.Invoice> InvoiceSendInvoiceAsync(string id, Stripe.InvoiceSendOptions options);

View File

@ -1,4 +1,5 @@
using Bit.Core.Models.BitStripe;
using Stripe;
namespace Bit.Core.Services;
@ -103,6 +104,9 @@ public class StripeAdapter : IStripeAdapter
return invoices;
}
public async Task<List<Stripe.Invoice>> InvoiceSearchAsync(InvoiceSearchOptions options)
=> (await _invoiceService.SearchAsync(options)).Data;
public Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options)
{
return _invoiceService.UpdateAsync(id, options);

View File

@ -1603,10 +1603,25 @@ public class StripePaymentService : IPaymentService
return subscriptionInfo;
}
var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId);
var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, new SubscriptionGetOptions
{
Expand = ["test_clock"]
});
if (sub != null)
{
subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub);
if (_featureService.IsEnabled(FeatureFlagKeys.AC1795_UpdatedSubscriptionStatusSection))
{
var (suspensionDate, unpaidPeriodEndDate) = await GetSuspensionDateAsync(sub);
if (suspensionDate.HasValue && unpaidPeriodEndDate.HasValue)
{
subscriptionInfo.Subscription.SuspensionDate = suspensionDate;
subscriptionInfo.Subscription.UnpaidPeriodEndDate = unpaidPeriodEndDate;
}
}
}
if (sub is { CanceledAt: not null } || string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
@ -1923,4 +1938,45 @@ public class StripePaymentService : IPaymentService
? subscriberName
: 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);
}
}
}