mirror of
https://github.com/bitwarden/server.git
synced 2025-12-11 13:53:48 -06:00
[PM-28423] Add latest_invoice expansion / logging to SubscriptionCancellationJob (#6603)
* Added latest_invoice expansion / logging to cancellation job * Run dotnet format * Claude feedback * Run dotnet format
This commit is contained in:
parent
d88fff4262
commit
101ff9d6ed
@ -1,16 +1,17 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
using Bit.Billing.Services;
|
||||||
#nullable disable
|
using Bit.Core.Billing.Constants;
|
||||||
|
|
||||||
using Bit.Billing.Services;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Billing.Jobs;
|
namespace Bit.Billing.Jobs;
|
||||||
|
|
||||||
|
using static StripeConstants;
|
||||||
|
|
||||||
public class SubscriptionCancellationJob(
|
public class SubscriptionCancellationJob(
|
||||||
IStripeFacade stripeFacade,
|
IStripeFacade stripeFacade,
|
||||||
IOrganizationRepository organizationRepository)
|
IOrganizationRepository organizationRepository,
|
||||||
|
ILogger<SubscriptionCancellationJob> logger)
|
||||||
: IJob
|
: IJob
|
||||||
{
|
{
|
||||||
public async Task Execute(IJobExecutionContext context)
|
public async Task Execute(IJobExecutionContext context)
|
||||||
@ -21,20 +22,31 @@ public class SubscriptionCancellationJob(
|
|||||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||||
if (organization == null || organization.Enabled)
|
if (organization == null || organization.Enabled)
|
||||||
{
|
{
|
||||||
|
logger.LogWarning("{Job} skipped for subscription ({SubscriptionID}) because organization is either null or enabled", nameof(SubscriptionCancellationJob), subscriptionId);
|
||||||
// Organization was deleted or re-enabled by CS, skip cancellation
|
// Organization was deleted or re-enabled by CS, skip cancellation
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscription = await stripeFacade.GetSubscription(subscriptionId);
|
var subscription = await stripeFacade.GetSubscription(subscriptionId, new SubscriptionGetOptions
|
||||||
if (subscription?.Status != "unpaid" ||
|
|
||||||
subscription.LatestInvoice?.BillingReason is not ("subscription_cycle" or "subscription_create"))
|
|
||||||
{
|
{
|
||||||
|
Expand = ["latest_invoice"]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (subscription is not
|
||||||
|
{
|
||||||
|
Status: SubscriptionStatus.Unpaid,
|
||||||
|
LatestInvoice: { BillingReason: BillingReasons.SubscriptionCreate or BillingReasons.SubscriptionCycle }
|
||||||
|
})
|
||||||
|
{
|
||||||
|
logger.LogWarning("{Job} skipped for subscription ({SubscriptionID}) because subscription is not unpaid or does not have a cancellable billing reason", nameof(SubscriptionCancellationJob), subscriptionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel the subscription
|
// Cancel the subscription
|
||||||
await stripeFacade.CancelSubscription(subscriptionId, new SubscriptionCancelOptions());
|
await stripeFacade.CancelSubscription(subscriptionId, new SubscriptionCancelOptions());
|
||||||
|
|
||||||
|
logger.LogInformation("{Job} cancelled subscription ({SubscriptionID})", nameof(SubscriptionCancellationJob), subscriptionId);
|
||||||
|
|
||||||
// Void any open invoices
|
// Void any open invoices
|
||||||
var options = new InvoiceListOptions
|
var options = new InvoiceListOptions
|
||||||
{
|
{
|
||||||
@ -46,6 +58,7 @@ public class SubscriptionCancellationJob(
|
|||||||
foreach (var invoice in invoices)
|
foreach (var invoice in invoices)
|
||||||
{
|
{
|
||||||
await stripeFacade.VoidInvoice(invoice.Id);
|
await stripeFacade.VoidInvoice(invoice.Id);
|
||||||
|
logger.LogInformation("{Job} voided invoice ({InvoiceID}) for subscription ({SubscriptionID})", nameof(SubscriptionCancellationJob), invoice.Id, subscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (invoices.HasMore)
|
while (invoices.HasMore)
|
||||||
@ -55,6 +68,7 @@ public class SubscriptionCancellationJob(
|
|||||||
foreach (var invoice in invoices)
|
foreach (var invoice in invoices)
|
||||||
{
|
{
|
||||||
await stripeFacade.VoidInvoice(invoice.Id);
|
await stripeFacade.VoidInvoice(invoice.Id);
|
||||||
|
logger.LogInformation("{Job} voided invoice ({InvoiceID}) for subscription ({SubscriptionID})", nameof(SubscriptionCancellationJob), invoice.Id, subscriptionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,12 @@ public static class StripeConstants
|
|||||||
public const string UnrecognizedLocation = "unrecognized_location";
|
public const string UnrecognizedLocation = "unrecognized_location";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class BillingReasons
|
||||||
|
{
|
||||||
|
public const string SubscriptionCreate = "subscription_create";
|
||||||
|
public const string SubscriptionCycle = "subscription_cycle";
|
||||||
|
}
|
||||||
|
|
||||||
public static class CollectionMethod
|
public static class CollectionMethod
|
||||||
{
|
{
|
||||||
public const string ChargeAutomatically = "charge_automatically";
|
public const string ChargeAutomatically = "charge_automatically";
|
||||||
|
|||||||
388
test/Billing.Test/Jobs/SubscriptionCancellationJobTests.cs
Normal file
388
test/Billing.Test/Jobs/SubscriptionCancellationJobTests.cs
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
using Bit.Billing.Constants;
|
||||||
|
using Bit.Billing.Jobs;
|
||||||
|
using Bit.Billing.Services;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NSubstitute;
|
||||||
|
using Quartz;
|
||||||
|
using Stripe;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Billing.Test.Jobs;
|
||||||
|
|
||||||
|
public class SubscriptionCancellationJobTests
|
||||||
|
{
|
||||||
|
private readonly IStripeFacade _stripeFacade;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly SubscriptionCancellationJob _sut;
|
||||||
|
|
||||||
|
public SubscriptionCancellationJobTests()
|
||||||
|
{
|
||||||
|
_stripeFacade = Substitute.For<IStripeFacade>();
|
||||||
|
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||||
|
_sut = new SubscriptionCancellationJob(_stripeFacade, _organizationRepository, Substitute.For<ILogger<SubscriptionCancellationJob>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_OrganizationIsNull_SkipsCancellation()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string subscriptionId = "sub_123";
|
||||||
|
var organizationId = Guid.NewGuid();
|
||||||
|
var context = CreateJobExecutionContext(subscriptionId, organizationId);
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns((Organization)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.Execute(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());
|
||||||
|
await _stripeFacade.DidNotReceiveWithAnyArgs().CancelSubscription(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_OrganizationIsEnabled_SkipsCancellation()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string subscriptionId = "sub_123";
|
||||||
|
var organizationId = Guid.NewGuid();
|
||||||
|
var context = CreateJobExecutionContext(subscriptionId, organizationId);
|
||||||
|
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
Enabled = true
|
||||||
|
};
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.Execute(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());
|
||||||
|
await _stripeFacade.DidNotReceiveWithAnyArgs().CancelSubscription(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_SubscriptionStatusIsNotUnpaid_SkipsCancellation()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string subscriptionId = "sub_123";
|
||||||
|
var organizationId = Guid.NewGuid();
|
||||||
|
var context = CreateJobExecutionContext(subscriptionId, organizationId);
|
||||||
|
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
Enabled = false
|
||||||
|
};
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = subscriptionId,
|
||||||
|
Status = StripeSubscriptionStatus.Active,
|
||||||
|
LatestInvoice = new Invoice
|
||||||
|
{
|
||||||
|
BillingReason = "subscription_cycle"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_stripeFacade.GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains("latest_invoice")))
|
||||||
|
.Returns(subscription);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.Execute(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _stripeFacade.DidNotReceive().CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_BillingReasonIsInvalid_SkipsCancellation()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string subscriptionId = "sub_123";
|
||||||
|
var organizationId = Guid.NewGuid();
|
||||||
|
var context = CreateJobExecutionContext(subscriptionId, organizationId);
|
||||||
|
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
Enabled = false
|
||||||
|
};
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = subscriptionId,
|
||||||
|
Status = StripeSubscriptionStatus.Unpaid,
|
||||||
|
LatestInvoice = new Invoice
|
||||||
|
{
|
||||||
|
BillingReason = "manual"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_stripeFacade.GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains("latest_invoice")))
|
||||||
|
.Returns(subscription);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.Execute(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _stripeFacade.DidNotReceive().CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_ValidConditions_CancelsSubscriptionAndVoidsInvoices()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string subscriptionId = "sub_123";
|
||||||
|
var organizationId = Guid.NewGuid();
|
||||||
|
var context = CreateJobExecutionContext(subscriptionId, organizationId);
|
||||||
|
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
Enabled = false
|
||||||
|
};
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = subscriptionId,
|
||||||
|
Status = StripeSubscriptionStatus.Unpaid,
|
||||||
|
LatestInvoice = new Invoice
|
||||||
|
{
|
||||||
|
BillingReason = "subscription_cycle"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_stripeFacade.GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains("latest_invoice")))
|
||||||
|
.Returns(subscription);
|
||||||
|
|
||||||
|
var invoices = new StripeList<Invoice>
|
||||||
|
{
|
||||||
|
Data =
|
||||||
|
[
|
||||||
|
new Invoice { Id = "inv_1" },
|
||||||
|
new Invoice { Id = "inv_2" }
|
||||||
|
],
|
||||||
|
HasMore = false
|
||||||
|
};
|
||||||
|
_stripeFacade.ListInvoices(Arg.Any<InvoiceListOptions>()).Returns(invoices);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.Execute(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _stripeFacade.Received(1).CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());
|
||||||
|
await _stripeFacade.Received(1).VoidInvoice("inv_1");
|
||||||
|
await _stripeFacade.Received(1).VoidInvoice("inv_2");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_WithSubscriptionCreateBillingReason_CancelsSubscription()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string subscriptionId = "sub_123";
|
||||||
|
var organizationId = Guid.NewGuid();
|
||||||
|
var context = CreateJobExecutionContext(subscriptionId, organizationId);
|
||||||
|
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
Enabled = false
|
||||||
|
};
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = subscriptionId,
|
||||||
|
Status = StripeSubscriptionStatus.Unpaid,
|
||||||
|
LatestInvoice = new Invoice
|
||||||
|
{
|
||||||
|
BillingReason = "subscription_create"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_stripeFacade.GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains("latest_invoice")))
|
||||||
|
.Returns(subscription);
|
||||||
|
|
||||||
|
var invoices = new StripeList<Invoice>
|
||||||
|
{
|
||||||
|
Data = [],
|
||||||
|
HasMore = false
|
||||||
|
};
|
||||||
|
_stripeFacade.ListInvoices(Arg.Any<InvoiceListOptions>()).Returns(invoices);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.Execute(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _stripeFacade.Received(1).CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_NoOpenInvoices_CancelsSubscriptionOnly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string subscriptionId = "sub_123";
|
||||||
|
var organizationId = Guid.NewGuid();
|
||||||
|
var context = CreateJobExecutionContext(subscriptionId, organizationId);
|
||||||
|
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
Enabled = false
|
||||||
|
};
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = subscriptionId,
|
||||||
|
Status = StripeSubscriptionStatus.Unpaid,
|
||||||
|
LatestInvoice = new Invoice
|
||||||
|
{
|
||||||
|
BillingReason = "subscription_cycle"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_stripeFacade.GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains("latest_invoice")))
|
||||||
|
.Returns(subscription);
|
||||||
|
|
||||||
|
var invoices = new StripeList<Invoice>
|
||||||
|
{
|
||||||
|
Data = [],
|
||||||
|
HasMore = false
|
||||||
|
};
|
||||||
|
_stripeFacade.ListInvoices(Arg.Any<InvoiceListOptions>()).Returns(invoices);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.Execute(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _stripeFacade.Received(1).CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());
|
||||||
|
await _stripeFacade.DidNotReceiveWithAnyArgs().VoidInvoice(Arg.Any<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_WithPagination_VoidsAllInvoices()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string subscriptionId = "sub_123";
|
||||||
|
var organizationId = Guid.NewGuid();
|
||||||
|
var context = CreateJobExecutionContext(subscriptionId, organizationId);
|
||||||
|
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
Enabled = false
|
||||||
|
};
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = subscriptionId,
|
||||||
|
Status = StripeSubscriptionStatus.Unpaid,
|
||||||
|
LatestInvoice = new Invoice
|
||||||
|
{
|
||||||
|
BillingReason = "subscription_cycle"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_stripeFacade.GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains("latest_invoice")))
|
||||||
|
.Returns(subscription);
|
||||||
|
|
||||||
|
// First page of invoices
|
||||||
|
var firstPage = new StripeList<Invoice>
|
||||||
|
{
|
||||||
|
Data =
|
||||||
|
[
|
||||||
|
new Invoice { Id = "inv_1" },
|
||||||
|
new Invoice { Id = "inv_2" }
|
||||||
|
],
|
||||||
|
HasMore = true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Second page of invoices
|
||||||
|
var secondPage = new StripeList<Invoice>
|
||||||
|
{
|
||||||
|
Data =
|
||||||
|
[
|
||||||
|
new Invoice { Id = "inv_3" },
|
||||||
|
new Invoice { Id = "inv_4" }
|
||||||
|
],
|
||||||
|
HasMore = false
|
||||||
|
};
|
||||||
|
|
||||||
|
_stripeFacade.ListInvoices(Arg.Is<InvoiceListOptions>(o => o.StartingAfter == null))
|
||||||
|
.Returns(firstPage);
|
||||||
|
_stripeFacade.ListInvoices(Arg.Is<InvoiceListOptions>(o => o.StartingAfter == "inv_2"))
|
||||||
|
.Returns(secondPage);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.Execute(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _stripeFacade.Received(1).CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());
|
||||||
|
await _stripeFacade.Received(1).VoidInvoice("inv_1");
|
||||||
|
await _stripeFacade.Received(1).VoidInvoice("inv_2");
|
||||||
|
await _stripeFacade.Received(1).VoidInvoice("inv_3");
|
||||||
|
await _stripeFacade.Received(1).VoidInvoice("inv_4");
|
||||||
|
await _stripeFacade.Received(2).ListInvoices(Arg.Any<InvoiceListOptions>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_ListInvoicesCalledWithCorrectOptions()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string subscriptionId = "sub_123";
|
||||||
|
var organizationId = Guid.NewGuid();
|
||||||
|
var context = CreateJobExecutionContext(subscriptionId, organizationId);
|
||||||
|
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
Enabled = false
|
||||||
|
};
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Id = subscriptionId,
|
||||||
|
Status = StripeSubscriptionStatus.Unpaid,
|
||||||
|
LatestInvoice = new Invoice
|
||||||
|
{
|
||||||
|
BillingReason = "subscription_cycle"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_stripeFacade.GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains("latest_invoice")))
|
||||||
|
.Returns(subscription);
|
||||||
|
|
||||||
|
var invoices = new StripeList<Invoice>
|
||||||
|
{
|
||||||
|
Data = [],
|
||||||
|
HasMore = false
|
||||||
|
};
|
||||||
|
_stripeFacade.ListInvoices(Arg.Any<InvoiceListOptions>()).Returns(invoices);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.Execute(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _stripeFacade.Received(1).GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains("latest_invoice")));
|
||||||
|
await _stripeFacade.Received(1).ListInvoices(Arg.Is<InvoiceListOptions>(o =>
|
||||||
|
o.Status == "open" &&
|
||||||
|
o.Subscription == subscriptionId &&
|
||||||
|
o.Limit == 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IJobExecutionContext CreateJobExecutionContext(string subscriptionId, Guid organizationId)
|
||||||
|
{
|
||||||
|
var context = Substitute.For<IJobExecutionContext>();
|
||||||
|
var jobDataMap = new JobDataMap
|
||||||
|
{
|
||||||
|
{ "subscriptionId", subscriptionId },
|
||||||
|
{ "organizationId", organizationId.ToString() }
|
||||||
|
};
|
||||||
|
context.MergedJobDataMap.Returns(jobDataMap);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user