mirror of
https://github.com/bitwarden/server.git
synced 2025-05-16 17:15:40 -05:00
[PM-18955] Implement OrganizationWarningsQuery
(#5713)
* Add GetWarnings endpoint to OrganizationBillingController * Add OrganizationWarningsQueryTests
This commit is contained in:
parent
41001fefae
commit
2d4ec530c5
@ -2,6 +2,7 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.Billing.Models.Requests;
|
using Bit.Api.Billing.Models.Requests;
|
||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
|
using Bit.Api.Billing.Queries.Organizations;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Models.Sales;
|
using Bit.Core.Billing.Models.Sales;
|
||||||
@ -24,6 +25,7 @@ public class OrganizationBillingController(
|
|||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IOrganizationBillingService organizationBillingService,
|
IOrganizationBillingService organizationBillingService,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationWarningsQuery organizationWarningsQuery,
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
IPricingClient pricingClient,
|
IPricingClient pricingClient,
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService,
|
||||||
@ -335,4 +337,28 @@ public class OrganizationBillingController(
|
|||||||
|
|
||||||
return TypedResults.Ok(providerId);
|
return TypedResults.Ok(providerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("warnings")]
|
||||||
|
public async Task<IResult> GetWarningsAsync([FromRoute] Guid organizationId)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* We'll keep these available at the User level, because we're hiding any pertinent information and
|
||||||
|
* we want to throw as few errors as possible since these are not core features.
|
||||||
|
*/
|
||||||
|
if (!await currentContext.OrganizationUser(organizationId))
|
||||||
|
{
|
||||||
|
return Error.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
return Error.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await organizationWarningsQuery.Run(organization);
|
||||||
|
|
||||||
|
return TypedResults.Ok(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
#nullable enable
|
||||||
|
namespace Bit.Api.Billing.Models.Responses.Organizations;
|
||||||
|
|
||||||
|
public record OrganizationWarningsResponse
|
||||||
|
{
|
||||||
|
public FreeTrialWarning? FreeTrial { get; set; }
|
||||||
|
public InactiveSubscriptionWarning? InactiveSubscription { get; set; }
|
||||||
|
public ResellerRenewalWarning? ResellerRenewal { get; set; }
|
||||||
|
|
||||||
|
public record FreeTrialWarning
|
||||||
|
{
|
||||||
|
public int RemainingTrialDays { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record InactiveSubscriptionWarning
|
||||||
|
{
|
||||||
|
public required string Resolution { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ResellerRenewalWarning
|
||||||
|
{
|
||||||
|
public required string Type { get; set; }
|
||||||
|
public UpcomingRenewal? Upcoming { get; set; }
|
||||||
|
public IssuedRenewal? Issued { get; set; }
|
||||||
|
public PastDueRenewal? PastDue { get; set; }
|
||||||
|
|
||||||
|
public record UpcomingRenewal
|
||||||
|
{
|
||||||
|
public required DateTime RenewalDate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record IssuedRenewal
|
||||||
|
{
|
||||||
|
public required DateTime IssuedDate { get; set; }
|
||||||
|
public required DateTime DueDate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PastDueRenewal
|
||||||
|
{
|
||||||
|
public required DateTime SuspensionDate { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,214 @@
|
|||||||
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Api.Billing.Models.Responses.Organizations;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Stripe;
|
||||||
|
using FreeTrialWarning = Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.FreeTrialWarning;
|
||||||
|
using InactiveSubscriptionWarning =
|
||||||
|
Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.InactiveSubscriptionWarning;
|
||||||
|
using ResellerRenewalWarning =
|
||||||
|
Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.ResellerRenewalWarning;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Queries.Organizations;
|
||||||
|
|
||||||
|
public interface IOrganizationWarningsQuery
|
||||||
|
{
|
||||||
|
Task<OrganizationWarningsResponse> Run(
|
||||||
|
Organization organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrganizationWarningsQuery(
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IProviderRepository providerRepository,
|
||||||
|
IStripeAdapter stripeAdapter,
|
||||||
|
ISubscriberService subscriberService) : IOrganizationWarningsQuery
|
||||||
|
{
|
||||||
|
public async Task<OrganizationWarningsResponse> Run(
|
||||||
|
Organization organization)
|
||||||
|
{
|
||||||
|
var response = new OrganizationWarningsResponse();
|
||||||
|
|
||||||
|
var subscription =
|
||||||
|
await subscriberService.GetSubscription(organization,
|
||||||
|
new SubscriptionGetOptions { Expand = ["customer", "latest_invoice", "test_clock"] });
|
||||||
|
|
||||||
|
if (subscription == null)
|
||||||
|
{
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.FreeTrial = await GetFreeTrialWarning(organization, subscription);
|
||||||
|
|
||||||
|
var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||||
|
|
||||||
|
response.InactiveSubscription = await GetInactiveSubscriptionWarning(organization, provider, subscription);
|
||||||
|
|
||||||
|
response.ResellerRenewal = await GetResellerRenewalWarning(provider, subscription);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<FreeTrialWarning?> GetFreeTrialWarning(
|
||||||
|
Organization organization,
|
||||||
|
Subscription subscription)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditSubscription(organization.Id))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscription is not
|
||||||
|
{
|
||||||
|
Status: StripeConstants.SubscriptionStatus.Trialing,
|
||||||
|
TrialEnd: not null,
|
||||||
|
Customer: not null
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer = subscription.Customer;
|
||||||
|
|
||||||
|
var hasPaymentMethod =
|
||||||
|
!string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) ||
|
||||||
|
!string.IsNullOrEmpty(customer.DefaultSourceId) ||
|
||||||
|
customer.Metadata.ContainsKey(StripeConstants.MetadataKeys.BraintreeCustomerId);
|
||||||
|
|
||||||
|
if (hasPaymentMethod)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;
|
||||||
|
|
||||||
|
var remainingTrialDays = (int)Math.Ceiling((subscription.TrialEnd.Value - now).TotalDays);
|
||||||
|
|
||||||
|
return new FreeTrialWarning { RemainingTrialDays = remainingTrialDays };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<InactiveSubscriptionWarning?> GetInactiveSubscriptionWarning(
|
||||||
|
Organization organization,
|
||||||
|
Provider? provider,
|
||||||
|
Subscription subscription)
|
||||||
|
{
|
||||||
|
if (organization.Enabled ||
|
||||||
|
subscription.Status is not StripeConstants.SubscriptionStatus.Unpaid
|
||||||
|
and not StripeConstants.SubscriptionStatus.Canceled)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider != null)
|
||||||
|
{
|
||||||
|
return new InactiveSubscriptionWarning { Resolution = "contact_provider" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await currentContext.OrganizationOwner(organization.Id))
|
||||||
|
{
|
||||||
|
return subscription.Status switch
|
||||||
|
{
|
||||||
|
StripeConstants.SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning
|
||||||
|
{
|
||||||
|
Resolution = "add_payment_method"
|
||||||
|
},
|
||||||
|
StripeConstants.SubscriptionStatus.Canceled => new InactiveSubscriptionWarning
|
||||||
|
{
|
||||||
|
Resolution = "resubscribe"
|
||||||
|
},
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InactiveSubscriptionWarning { Resolution = "contact_owner" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ResellerRenewalWarning?> GetResellerRenewalWarning(
|
||||||
|
Provider? provider,
|
||||||
|
Subscription subscription)
|
||||||
|
{
|
||||||
|
if (provider is not
|
||||||
|
{
|
||||||
|
Type: ProviderType.Reseller
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscription.CollectionMethod != StripeConstants.CollectionMethod.SendInvoice)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;
|
||||||
|
|
||||||
|
// ReSharper disable once ConvertIfStatementToSwitchStatement
|
||||||
|
if (subscription is
|
||||||
|
{
|
||||||
|
Status: StripeConstants.SubscriptionStatus.Trialing or StripeConstants.SubscriptionStatus.Active,
|
||||||
|
LatestInvoice: null or { Status: StripeConstants.InvoiceStatus.Paid }
|
||||||
|
} && (subscription.CurrentPeriodEnd - now).TotalDays <= 14)
|
||||||
|
{
|
||||||
|
return new ResellerRenewalWarning
|
||||||
|
{
|
||||||
|
Type = "upcoming",
|
||||||
|
Upcoming = new ResellerRenewalWarning.UpcomingRenewal
|
||||||
|
{
|
||||||
|
RenewalDate = subscription.CurrentPeriodEnd
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscription is
|
||||||
|
{
|
||||||
|
Status: StripeConstants.SubscriptionStatus.Active,
|
||||||
|
LatestInvoice: { Status: StripeConstants.InvoiceStatus.Open, DueDate: not null }
|
||||||
|
} && subscription.LatestInvoice.DueDate > now)
|
||||||
|
{
|
||||||
|
return new ResellerRenewalWarning
|
||||||
|
{
|
||||||
|
Type = "issued",
|
||||||
|
Issued = new ResellerRenewalWarning.IssuedRenewal
|
||||||
|
{
|
||||||
|
IssuedDate = subscription.LatestInvoice.Created,
|
||||||
|
DueDate = subscription.LatestInvoice.DueDate.Value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once InvertIf
|
||||||
|
if (subscription.Status == StripeConstants.SubscriptionStatus.PastDue)
|
||||||
|
{
|
||||||
|
var openInvoices = await stripeAdapter.InvoiceSearchAsync(new InvoiceSearchOptions
|
||||||
|
{
|
||||||
|
Query = $"subscription:'{subscription.Id}' status:'open'"
|
||||||
|
});
|
||||||
|
|
||||||
|
var earliestOverdueInvoice = openInvoices
|
||||||
|
.Where(invoice => invoice.DueDate != null && invoice.DueDate < now)
|
||||||
|
.MinBy(invoice => invoice.Created);
|
||||||
|
|
||||||
|
if (earliestOverdueInvoice != null)
|
||||||
|
{
|
||||||
|
return new ResellerRenewalWarning
|
||||||
|
{
|
||||||
|
Type = "past_due",
|
||||||
|
PastDue = new ResellerRenewalWarning.PastDueRenewal
|
||||||
|
{
|
||||||
|
SuspensionDate = earliestOverdueInvoice.DueDate!.Value.AddDays(30)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
11
src/Api/Billing/Registrations.cs
Normal file
11
src/Api/Billing/Registrations.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using Bit.Api.Billing.Queries.Organizations;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing;
|
||||||
|
|
||||||
|
public static class Registrations
|
||||||
|
{
|
||||||
|
public static void AddBillingQueries(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddTransient<IOrganizationWarningsQuery, OrganizationWarningsQuery>();
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,7 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
|||||||
using Bit.Core.Tools.Entities;
|
using Bit.Core.Tools.Entities;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||||
|
using Bit.Api.Billing;
|
||||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Auth.Identity.TokenProviders;
|
using Bit.Core.Auth.Identity.TokenProviders;
|
||||||
@ -184,6 +185,8 @@ public class Startup
|
|||||||
services.AddImportServices();
|
services.AddImportServices();
|
||||||
services.AddPhishingDomainServices(globalSettings);
|
services.AddPhishingDomainServices(globalSettings);
|
||||||
|
|
||||||
|
services.AddBillingQueries();
|
||||||
|
|
||||||
// Authorization Handlers
|
// Authorization Handlers
|
||||||
services.AddAuthorizationHandlers();
|
services.AddAuthorizationHandlers();
|
||||||
|
|
||||||
|
@ -46,10 +46,12 @@ public static class StripeConstants
|
|||||||
{
|
{
|
||||||
public const string Draft = "draft";
|
public const string Draft = "draft";
|
||||||
public const string Open = "open";
|
public const string Open = "open";
|
||||||
|
public const string Paid = "paid";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class MetadataKeys
|
public static class MetadataKeys
|
||||||
{
|
{
|
||||||
|
public const string BraintreeCustomerId = "btCustomerId";
|
||||||
public const string InvoiceApproved = "invoice_approved";
|
public const string InvoiceApproved = "invoice_approved";
|
||||||
public const string OrganizationId = "organizationId";
|
public const string OrganizationId = "organizationId";
|
||||||
public const string ProviderId = "providerId";
|
public const string ProviderId = "providerId";
|
||||||
|
@ -150,6 +150,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion";
|
public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion";
|
||||||
public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically";
|
public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically";
|
||||||
public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup";
|
public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup";
|
||||||
|
public const string UseOrganizationWarningsService = "use-organization-warnings-service";
|
||||||
|
|
||||||
/* Data Insights and Reporting Team */
|
/* Data Insights and Reporting Team */
|
||||||
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
||||||
|
@ -0,0 +1,315 @@
|
|||||||
|
using Bit.Api.Billing.Queries.Organizations;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using NSubstitute.ReturnsExtensions;
|
||||||
|
using Stripe;
|
||||||
|
using Stripe.TestHelpers;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Api.Test.Billing.Queries.Organizations;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class OrganizationWarningsQueryTests
|
||||||
|
{
|
||||||
|
private static readonly string[] _requiredExpansions = ["customer", "latest_invoice", "test_clock"];
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_NoSubscription_NoWarnings(
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<OrganizationWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.ReturnsNull();
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(organization);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
FreeTrial: null,
|
||||||
|
InactiveSubscription: null,
|
||||||
|
ResellerRenewal: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_FreeTrialWarning(
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<OrganizationWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = StripeConstants.SubscriptionStatus.Trialing,
|
||||||
|
TrialEnd = now.AddDays(7),
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||||
|
Metadata = new Dictionary<string, string>()
|
||||||
|
},
|
||||||
|
TestClock = new TestClock
|
||||||
|
{
|
||||||
|
FrozenTime = now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(organization);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
FreeTrial.RemainingTrialDays: 7
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_InactiveSubscriptionWarning_ContactProvider(
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<OrganizationWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
organization.Enabled = false;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = StripeConstants.SubscriptionStatus.Unpaid
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id)
|
||||||
|
.Returns(new Provider());
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(organization);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
InactiveSubscription.Resolution: "contact_provider"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_InactiveSubscriptionWarning_AddPaymentMethod(
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<OrganizationWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
organization.Enabled = false;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = StripeConstants.SubscriptionStatus.Unpaid
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(organization);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
InactiveSubscription.Resolution: "add_payment_method"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_InactiveSubscriptionWarning_Resubscribe(
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<OrganizationWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
organization.Enabled = false;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = StripeConstants.SubscriptionStatus.Canceled
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(organization);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
InactiveSubscription.Resolution: "resubscribe"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_InactiveSubscriptionWarning_ContactOwner(
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<OrganizationWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
organization.Enabled = false;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = StripeConstants.SubscriptionStatus.Unpaid
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(false);
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(organization);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
InactiveSubscription.Resolution: "contact_owner"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_ResellerRenewalWarning_Upcoming(
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<OrganizationWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
|
||||||
|
Status = StripeConstants.SubscriptionStatus.Active,
|
||||||
|
CurrentPeriodEnd = now.AddDays(10),
|
||||||
|
TestClock = new TestClock
|
||||||
|
{
|
||||||
|
FrozenTime = now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id)
|
||||||
|
.Returns(new Provider
|
||||||
|
{
|
||||||
|
Type = ProviderType.Reseller
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(organization);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
ResellerRenewal.Type: "upcoming"
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(now.AddDays(10), response.ResellerRenewal.Upcoming!.RenewalDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_ResellerRenewalWarning_Issued(
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<OrganizationWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
|
||||||
|
Status = StripeConstants.SubscriptionStatus.Active,
|
||||||
|
LatestInvoice = new Invoice
|
||||||
|
{
|
||||||
|
Status = StripeConstants.InvoiceStatus.Open,
|
||||||
|
DueDate = now.AddDays(30),
|
||||||
|
Created = now
|
||||||
|
},
|
||||||
|
TestClock = new TestClock
|
||||||
|
{
|
||||||
|
FrozenTime = now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id)
|
||||||
|
.Returns(new Provider
|
||||||
|
{
|
||||||
|
Type = ProviderType.Reseller
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(organization);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
ResellerRenewal.Type: "issued"
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(now, response.ResellerRenewal.Issued!.IssuedDate);
|
||||||
|
Assert.Equal(now.AddDays(30), response.ResellerRenewal.Issued!.DueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_ResellerRenewalWarning_PastDue(
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<OrganizationWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
const string subscriptionId = "subscription_id";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Id = subscriptionId,
|
||||||
|
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
|
||||||
|
Status = StripeConstants.SubscriptionStatus.PastDue,
|
||||||
|
TestClock = new TestClock
|
||||||
|
{
|
||||||
|
FrozenTime = now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id)
|
||||||
|
.Returns(new Provider
|
||||||
|
{
|
||||||
|
Type = ProviderType.Reseller
|
||||||
|
});
|
||||||
|
|
||||||
|
var dueDate = now.AddDays(-10);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().InvoiceSearchAsync(Arg.Is<InvoiceSearchOptions>(options =>
|
||||||
|
options.Query == $"subscription:'{subscriptionId}' status:'open'")).Returns([
|
||||||
|
new Invoice { DueDate = dueDate, Created = dueDate.AddDays(-30) }
|
||||||
|
]);
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(organization);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
ResellerRenewal.Type: "past_due"
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(dueDate.AddDays(30), response.ResellerRenewal.PastDue!.SuspensionDate);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user