1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

[PM-21638] Stripe .NET v48 (#6202)

* Upgrade Stripe.net to v48.4.0

* Update PreviewTaxAmountCommand

* Remove unused UpcomingInvoiceOptionExtensions

* Added SubscriptionExtensions with GetCurrentPeriodEnd

* Update PremiumUserBillingService

* Update OrganizationBillingService

* Update GetOrganizationWarningsQuery

* Update BillingHistoryInfo

* Update SubscriptionInfo

* Remove unused Sql Billing folder

* Update StripeAdapter

* Update StripePaymentService

* Update InvoiceCreatedHandler

* Update PaymentFailedHandler

* Update PaymentSucceededHandler

* Update ProviderEventService

* Update StripeEventUtilityService

* Update SubscriptionDeletedHandler

* Update SubscriptionUpdatedHandler

* Update UpcomingInvoiceHandler

* Update ProviderSubscriptionResponse

* Remove unused Stripe Subscriptions Admin Tool

* Update RemoveOrganizationFromProviderCommand

* Update ProviderBillingService

* Update RemoveOrganizatinoFromProviderCommandTests

* Update PreviewTaxAmountCommandTests

* Update GetCloudOrganizationLicenseQueryTests

* Update GetOrganizationWarningsQueryTests

* Update StripePaymentServiceTests

* Update ProviderBillingControllerTests

* Update ProviderEventServiceTests

* Update SubscriptionDeletedHandlerTests

* Update SubscriptionUpdatedHandlerTests

* Resolve Billing test failures

I completely removed tests for the StripeEventService as they were using a system I setup a while back that read JSON files of the Stripe event structure. I did not anticipate how frequently these structures would change with each API version and the cost of trying to update these specific JSON files to test a very static data retrieval service far outweigh the benefit.

* Resolve Core test failures

* Run dotnet format

* Remove unused provider migration

* Fixed failing tests

* Run dotnet format

* Replace the old webhook secret key with new one (#6223)

* Fix compilation failures in additions

* Run dotnet format

* Bump Stripe API version

* Fix recent addition: CreatePremiumCloudHostedSubscriptionCommand

* Fix new code in main according to Stripe update

* Fix InvoiceExtensions

* Bump SDK version to match API Version

* Fix provider invoice generation validation

* More QA fixes

* Fix tests

* QA defect resolutions

* QA defect resolutions

* Run dotnet format

* Fix tests

---------

Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
This commit is contained in:
Alex Morask
2025-10-21 14:07:55 -05:00
committed by GitHub
parent 6324f692b8
commit 9c51c9971b
81 changed files with 1273 additions and 3959 deletions

View File

@@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Repositories;
@@ -270,7 +271,6 @@ public class ProviderBillingControllerTests
var subscription = new Subscription
{
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,
CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth),
Customer = new Customer
{
Address = new Address
@@ -291,20 +291,23 @@ public class ProviderBillingControllerTests
Data = [
new SubscriptionItem
{
CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth),
Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise }
},
new SubscriptionItem
{
CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth),
Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams }
}
]
},
Status = "unpaid",
Status = "unpaid"
};
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(
options =>
options.Expand.Contains("customer.tax_ids") &&
options.Expand.Contains("discounts") &&
options.Expand.Contains("test_clock"))).Returns(subscription);
var daysInLastMonth = DateTime.DaysInMonth(oneMonthAgo.Year, oneMonthAgo.Month);
@@ -365,7 +368,7 @@ public class ProviderBillingControllerTests
var response = ((Ok<ProviderSubscriptionResponse>)result).Value;
Assert.Equal(subscription.Status, response.Status);
Assert.Equal(subscription.CurrentPeriodEnd, response.CurrentPeriodEndDate);
Assert.Equal(subscription.GetCurrentPeriodEnd(), response.CurrentPeriodEndDate);
Assert.Equal(subscription.Customer!.Discount!.Coupon!.PercentOff, response.DiscountPercentage);
Assert.Equal(subscription.CollectionMethod, response.CollectionMethod);
@@ -405,6 +408,118 @@ public class ProviderBillingControllerTests
Assert.Equal(14, response.Suspension.GracePeriod);
}
[Theory, BitAutoData]
public async Task GetSubscriptionAsync_SubscriptionLevelDiscount_Ok(
Provider provider,
SutProvider<ProviderBillingController> sutProvider)
{
ConfigureStableProviderServiceUserInputs(provider, sutProvider);
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var now = DateTime.UtcNow;
var oneMonthAgo = now.AddMonths(-1);
var daysInThisMonth = DateTime.DaysInMonth(now.Year, now.Month);
var subscription = new Subscription
{
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,
Customer = new Customer
{
Address = new Address
{
Country = "US",
PostalCode = "12345",
Line1 = "123 Example St.",
Line2 = "Unit 1",
City = "Example Town",
State = "NY"
},
Balance = -100000,
Discount = null, // No customer-level discount
TaxIds = new StripeList<TaxId> { Data = [new TaxId { Value = "123456789" }] }
},
Discounts =
[
new Discount { Coupon = new Coupon { PercentOff = 15 } } // Subscription-level discount
],
Items = new StripeList<SubscriptionItem>
{
Data = [
new SubscriptionItem
{
CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth),
Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise }
},
new SubscriptionItem
{
CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth),
Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams }
}
]
},
Status = "active"
};
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(
options =>
options.Expand.Contains("customer.tax_ids") &&
options.Expand.Contains("discounts") &&
options.Expand.Contains("test_clock"))).Returns(subscription);
stripeAdapter.InvoiceSearchAsync(Arg.Is<InvoiceSearchOptions>(
options => options.Query == $"subscription:'{subscription.Id}' status:'open'"))
.Returns([]);
var providerPlans = new List<ProviderPlan>
{
new ()
{
Id = Guid.NewGuid(),
ProviderId = provider.Id,
PlanType = PlanType.TeamsMonthly,
SeatMinimum = 50,
PurchasedSeats = 10,
AllocatedSeats = 60
},
new ()
{
Id = Guid.NewGuid(),
ProviderId = provider.Id,
PlanType = PlanType.EnterpriseMonthly,
SeatMinimum = 100,
PurchasedSeats = 0,
AllocatedSeats = 90
}
};
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
foreach (var providerPlan in providerPlans)
{
var plan = StaticStore.GetPlan(providerPlan.PlanType);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(plan);
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType);
sutProvider.GetDependency<IStripeAdapter>().PriceGetAsync(priceId)
.Returns(new Price
{
UnitAmountDecimal = plan.PasswordManager.ProviderPortalSeatPrice * 100
});
}
var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);
Assert.IsType<Ok<ProviderSubscriptionResponse>>(result);
var response = ((Ok<ProviderSubscriptionResponse>)result).Value;
Assert.Equal(subscription.Status, response.Status);
Assert.Equal(subscription.GetCurrentPeriodEnd(), response.CurrentPeriodEndDate);
Assert.Equal(15, response.DiscountPercentage); // Verify subscription-level discount is used
Assert.Equal(subscription.CollectionMethod, response.CollectionMethod);
}
#endregion
#region UpdateTaxInformationAsync

View File

@@ -27,24 +27,6 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\Events\charge.succeeded.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Resources\Events\customer.subscription.updated.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Resources\Events\customer.updated.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Resources\Events\invoice.created.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Resources\Events\invoice.upcoming.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Resources\Events\payment_method.attached.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Resources\IPN\echeck-payment.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
@@ -73,9 +55,6 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<None Remove="Resources\Events\invoice.finalized.json" />
<EmbeddedResource Include="Resources\Events\invoice.finalized.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -1,130 +0,0 @@
{
"id": "evt_3NvKgBIGBnsLynRr0pJJqudS",
"object": "event",
"api_version": "2024-06-20",
"created": 1695909300,
"data": {
"object": {
"id": "ch_3NvKgBIGBnsLynRr0ZyvP9AN",
"object": "charge",
"amount": 7200,
"amount_captured": 7200,
"amount_refunded": 0,
"application": null,
"application_fee": null,
"application_fee_amount": null,
"balance_transaction": "txn_3NvKgBIGBnsLynRr0KbYEz76",
"billing_details": {
"address": {
"city": null,
"country": null,
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": null,
"name": null,
"phone": null
},
"calculated_statement_descriptor": "BITWARDEN",
"captured": true,
"created": 1695909299,
"currency": "usd",
"customer": "cus_OimAwOzQmThNXx",
"description": "Subscription update",
"destination": null,
"dispute": null,
"disputed": false,
"failure_balance_transaction": null,
"failure_code": null,
"failure_message": null,
"fraud_details": {
},
"invoice": "in_1NvKgBIGBnsLynRrmRFHAcoV",
"livemode": false,
"metadata": {
},
"on_behalf_of": null,
"order": null,
"outcome": {
"network_status": "approved_by_network",
"reason": null,
"risk_level": "normal",
"risk_score": 37,
"seller_message": "Payment complete.",
"type": "authorized"
},
"paid": true,
"payment_intent": "pi_3NvKgBIGBnsLynRr09Ny3Heu",
"payment_method": "pm_1NvKbpIGBnsLynRrcOwez4A1",
"payment_method_details": {
"card": {
"amount_authorized": 7200,
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": "pass"
},
"country": "US",
"exp_month": 6,
"exp_year": 2033,
"extended_authorization": {
"status": "disabled"
},
"fingerprint": "0VgUBpvqcUUnuSmK",
"funding": "credit",
"incremental_authorization": {
"status": "unavailable"
},
"installments": null,
"last4": "4242",
"mandate": null,
"multicapture": {
"status": "unavailable"
},
"network": "visa",
"network_token": {
"used": false
},
"overcapture": {
"maximum_amount_capturable": 7200,
"status": "unavailable"
},
"three_d_secure": null,
"wallet": null
},
"type": "card"
},
"receipt_email": "cturnbull@bitwarden.com",
"receipt_number": null,
"receipt_url": "https://pay.stripe.com/receipts/invoices/CAcaFwoVYWNjdF8xOXNtSVhJR0Juc0x5blJyKLSL1qgGMgYTnk_JOUA6LBY_SDEZNtuae1guQ6Dlcuev1TUHwn712t-UNnZdIc383zS15bXv_1dby8e4?s=ap",
"refunded": false,
"refunds": {
"object": "list",
"data": [
],
"has_more": false,
"total_count": 0,
"url": "/v1/charges/ch_3NvKgBIGBnsLynRr0ZyvP9AN/refunds"
},
"review": null,
"shipping": null,
"source": null,
"source_transfer": null,
"statement_descriptor": null,
"statement_descriptor_suffix": null,
"status": "succeeded",
"transfer_data": null,
"transfer_group": null
}
},
"livemode": false,
"pending_webhooks": 9,
"request": {
"id": "req_rig8N5Ca8EXYRy",
"idempotency_key": "db75068d-5d90-4c65-a410-4e2ed8347509"
},
"type": "charge.succeeded"
}

View File

@@ -1,177 +0,0 @@
{
"id": "evt_1NvLMDIGBnsLynRr6oBxebrE",
"object": "event",
"api_version": "2024-06-20",
"created": 1695911902,
"data": {
"object": {
"id": "sub_1NvKoKIGBnsLynRrcLIAUWGf",
"object": "subscription",
"application": null,
"application_fee_percent": null,
"automatic_tax": {
"enabled": false
},
"billing_cycle_anchor": 1695911900,
"billing_thresholds": null,
"cancel_at": null,
"cancel_at_period_end": false,
"canceled_at": null,
"cancellation_details": {
"comment": null,
"feedback": null,
"reason": null
},
"collection_method": "charge_automatically",
"created": 1695909804,
"currency": "usd",
"current_period_end": 1727534300,
"current_period_start": 1695911900,
"customer": "cus_OimNNCC3RiI2HQ",
"days_until_due": null,
"default_payment_method": null,
"default_source": null,
"default_tax_rates": [
],
"description": null,
"discount": null,
"ended_at": null,
"items": {
"object": "list",
"data": [
{
"id": "si_OimNgVtrESpqus",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1695909805,
"metadata": {
},
"plan": {
"id": "enterprise-org-seat-annually",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 3600,
"amount_decimal": "3600",
"billing_scheme": "per_unit",
"created": 1494268677,
"currency": "usd",
"interval": "year",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"nickname": "2019 Enterprise Seat (Annually)",
"product": "prod_BUtogGemxnTi9z",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"price": {
"id": "enterprise-org-seat-annually",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1494268677,
"currency": "usd",
"custom_unit_amount": null,
"livemode": false,
"lookup_key": null,
"metadata": {
},
"nickname": "2019 Enterprise Seat (Annually)",
"product": "prod_BUtogGemxnTi9z",
"recurring": {
"aggregate_usage": null,
"interval": "year",
"interval_count": 1,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "unspecified",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 3600,
"unit_amount_decimal": "3600"
},
"quantity": 1,
"subscription": "sub_1NvKoKIGBnsLynRrcLIAUWGf",
"tax_rates": [
]
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_1NvKoKIGBnsLynRrcLIAUWGf"
},
"latest_invoice": "in_1NvLM9IGBnsLynRrOysII07d",
"livemode": false,
"metadata": {
"organizationId": "84a569ea-4643-474a-83a9-b08b00e7a20d"
},
"next_pending_invoice_item_invoice": null,
"on_behalf_of": null,
"pause_collection": null,
"payment_settings": {
"payment_method_options": null,
"payment_method_types": null,
"save_default_payment_method": "off"
},
"pending_invoice_item_interval": null,
"pending_setup_intent": null,
"pending_update": null,
"plan": {
"id": "enterprise-org-seat-annually",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 3600,
"amount_decimal": "3600",
"billing_scheme": "per_unit",
"created": 1494268677,
"currency": "usd",
"interval": "year",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"nickname": "2019 Enterprise Seat (Annually)",
"product": "prod_BUtogGemxnTi9z",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"schedule": null,
"start_date": 1695909804,
"status": "active",
"test_clock": null,
"transfer_data": null,
"trial_end": 1695911899,
"trial_settings": {
"end_behavior": {
"missing_payment_method": "create_invoice"
}
},
"trial_start": 1695909804
},
"previous_attributes": {
"billing_cycle_anchor": 1696514604,
"current_period_end": 1696514604,
"current_period_start": 1695909804,
"latest_invoice": "in_1NvKoKIGBnsLynRrSNRC6oYI",
"status": "trialing",
"trial_end": 1696514604
}
},
"livemode": false,
"pending_webhooks": 8,
"request": {
"id": "req_DMZPUU3BI66zAx",
"idempotency_key": "3fd8b4a5-6a20-46ab-9f45-b37b02a8017f"
},
"type": "customer.subscription.updated"
}

View File

@@ -1,311 +0,0 @@
{
"id": "evt_1NvKjSIGBnsLynRrS3MTK4DZ",
"object": "event",
"account": "acct_19smIXIGBnsLynRr",
"api_version": "2024-06-20",
"created": 1695909502,
"data": {
"object": {
"id": "cus_Of54kUr3gV88lM",
"object": "customer",
"address": {
"city": null,
"country": "US",
"line1": "",
"line2": null,
"postal_code": "33701",
"state": null
},
"balance": 0,
"created": 1695056798,
"currency": "usd",
"default_source": "src_1NtAfeIGBnsLynRrYDrceax7",
"delinquent": false,
"description": "Premium User",
"discount": null,
"email": "premium@bitwarden.com",
"invoice_prefix": "C506E8CE",
"invoice_settings": {
"custom_fields": [
{
"name": "Subscriber",
"value": "Premium User"
}
],
"default_payment_method": "pm_1Nrku9IGBnsLynRrcsQ3hy6C",
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"region": "US"
},
"name": null,
"next_invoice_sequence": 2,
"phone": null,
"preferred_locales": [
],
"shipping": null,
"tax_exempt": "none",
"test_clock": null,
"account_balance": 0,
"cards": {
"object": "list",
"data": [
],
"has_more": false,
"total_count": 0,
"url": "/v1/customers/cus_Of54kUr3gV88lM/cards"
},
"default_card": null,
"default_currency": "usd",
"sources": {
"object": "list",
"data": [
{
"id": "src_1NtAfeIGBnsLynRrYDrceax7",
"object": "source",
"ach_credit_transfer": {
"account_number": "test_b2d1c6415f6f",
"routing_number": "110000000",
"fingerprint": "ePO4hBQanSft3gvU",
"swift_code": "TSTEZ122",
"bank_name": "TEST BANK",
"refund_routing_number": null,
"refund_account_holder_type": null,
"refund_account_holder_name": null
},
"amount": null,
"client_secret": "src_client_secret_bUAP2uDRw6Pwj0xYk32LmJ3K",
"created": 1695394170,
"currency": "usd",
"customer": "cus_Of54kUr3gV88lM",
"flow": "receiver",
"livemode": false,
"metadata": {
},
"owner": {
"address": null,
"email": "amount_0@stripe.com",
"name": null,
"phone": null,
"verified_address": null,
"verified_email": null,
"verified_name": null,
"verified_phone": null
},
"receiver": {
"address": "110000000-test_b2d1c6415f6f",
"amount_charged": 0,
"amount_received": 0,
"amount_returned": 0,
"refund_attributes_method": "email",
"refund_attributes_status": "missing"
},
"statement_descriptor": null,
"status": "pending",
"type": "ach_credit_transfer",
"usage": "reusable"
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/customers/cus_Of54kUr3gV88lM/sources"
},
"subscriptions": {
"object": "list",
"data": [
{
"id": "sub_1NrkuBIGBnsLynRrzjFGIjEw",
"object": "subscription",
"application": null,
"application_fee_percent": null,
"automatic_tax": {
"enabled": false
},
"billing": "charge_automatically",
"billing_cycle_anchor": 1695056799,
"billing_thresholds": null,
"cancel_at": null,
"cancel_at_period_end": false,
"canceled_at": null,
"cancellation_details": {
"comment": null,
"feedback": null,
"reason": null
},
"collection_method": "charge_automatically",
"created": 1695056799,
"currency": "usd",
"current_period_end": 1726679199,
"current_period_start": 1695056799,
"customer": "cus_Of54kUr3gV88lM",
"days_until_due": null,
"default_payment_method": null,
"default_source": null,
"default_tax_rates": [
],
"description": null,
"discount": null,
"ended_at": null,
"invoice_customer_balance_settings": {
"consume_applied_balance_on_void": true
},
"items": {
"object": "list",
"data": [
{
"id": "si_Of54i3aK9I5Wro",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1695056800,
"metadata": {
},
"plan": {
"id": "premium-annually",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 1000,
"amount_decimal": "1000",
"billing_scheme": "per_unit",
"created": 1499289328,
"currency": "usd",
"interval": "year",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"name": "Premium (Annually)",
"nickname": "Premium (Annually)",
"product": "prod_BUqgYr48VzDuCg",
"statement_description": null,
"statement_descriptor": null,
"tiers": null,
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"price": {
"id": "premium-annually",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1499289328,
"currency": "usd",
"custom_unit_amount": null,
"livemode": false,
"lookup_key": null,
"metadata": {
},
"nickname": "Premium (Annually)",
"product": "prod_BUqgYr48VzDuCg",
"recurring": {
"aggregate_usage": null,
"interval": "year",
"interval_count": 1,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "unspecified",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 1000,
"unit_amount_decimal": "1000"
},
"quantity": 1,
"subscription": "sub_1NrkuBIGBnsLynRrzjFGIjEw",
"tax_rates": [
]
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_1NrkuBIGBnsLynRrzjFGIjEw"
},
"latest_invoice": "in_1NrkuBIGBnsLynRr40gyJTVU",
"livemode": false,
"metadata": {
"userId": "91f40b6d-ac3b-4348-804b-b0810119ac6a"
},
"next_pending_invoice_item_invoice": null,
"on_behalf_of": null,
"pause_collection": null,
"payment_settings": {
"payment_method_options": null,
"payment_method_types": null,
"save_default_payment_method": "off"
},
"pending_invoice_item_interval": null,
"pending_setup_intent": null,
"pending_update": null,
"plan": {
"id": "premium-annually",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 1000,
"amount_decimal": "1000",
"billing_scheme": "per_unit",
"created": 1499289328,
"currency": "usd",
"interval": "year",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"name": "Premium (Annually)",
"nickname": "Premium (Annually)",
"product": "prod_BUqgYr48VzDuCg",
"statement_description": null,
"statement_descriptor": null,
"tiers": null,
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"schedule": null,
"start": 1695056799,
"start_date": 1695056799,
"status": "active",
"tax_percent": null,
"test_clock": null,
"transfer_data": null,
"trial_end": null,
"trial_settings": {
"end_behavior": {
"missing_payment_method": "create_invoice"
}
},
"trial_start": null
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/customers/cus_Of54kUr3gV88lM/subscriptions"
},
"tax_ids": {
"object": "list",
"data": [
],
"has_more": false,
"total_count": 0,
"url": "/v1/customers/cus_Of54kUr3gV88lM/tax_ids"
},
"tax_info": null,
"tax_info_verification": null
},
"previous_attributes": {
"email": "premium-new@bitwarden.com"
}
},
"livemode": false,
"pending_webhooks": 5,
"request": "req_2RtGdXCfiicFLx",
"type": "customer.updated",
"user_id": "acct_19smIXIGBnsLynRr"
}

View File

@@ -1,222 +0,0 @@
{
"id": "evt_1NvKzfIGBnsLynRr0SkwrlkE",
"object": "event",
"api_version": "2024-06-20",
"created": 1695910506,
"data": {
"object": {
"id": "in_1NvKzdIGBnsLynRr8fE8cpbg",
"object": "invoice",
"account_country": "US",
"account_name": "Bitwarden Inc.",
"account_tax_ids": null,
"amount_due": 0,
"amount_paid": 0,
"amount_remaining": 0,
"amount_shipping": 0,
"application": null,
"application_fee_amount": null,
"attempt_count": 0,
"attempted": true,
"auto_advance": false,
"automatic_tax": {
"enabled": false,
"status": null
},
"billing_reason": "subscription_create",
"charge": null,
"collection_method": "charge_automatically",
"created": 1695910505,
"currency": "usd",
"custom_fields": [
{
"name": "Organization",
"value": "teams 2023 monthly - 2"
}
],
"customer": "cus_OimYrxnMTMMK1E",
"customer_address": {
"city": null,
"country": "US",
"line1": "",
"line2": null,
"postal_code": "12345",
"state": null
},
"customer_email": "cturnbull@bitwarden.com",
"customer_name": null,
"customer_phone": null,
"customer_shipping": null,
"customer_tax_exempt": "none",
"customer_tax_ids": [
],
"default_payment_method": null,
"default_source": null,
"default_tax_rates": [
],
"description": null,
"discount": null,
"discounts": [
],
"due_date": null,
"effective_at": 1695910505,
"ending_balance": 0,
"footer": null,
"from_invoice": null,
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_19smIXIGBnsLynRr/test_YWNjdF8xOXNtSVhJR0Juc0x5blJyLF9PaW1ZVlo4dFRtbkNQQVY5aHNpckQxN1QzRHBPcVBOLDg2NDUxMzA30200etYRHca2?s=ap",
"invoice_pdf": "https://pay.stripe.com/invoice/acct_19smIXIGBnsLynRr/test_YWNjdF8xOXNtSVhJR0Juc0x5blJyLF9PaW1ZVlo4dFRtbkNQQVY5aHNpckQxN1QzRHBPcVBOLDg2NDUxMzA30200etYRHca2/pdf?s=ap",
"last_finalization_error": null,
"latest_revision": null,
"lines": {
"object": "list",
"data": [
{
"id": "il_1NvKzdIGBnsLynRr2pS4ZA8e",
"object": "line_item",
"amount": 0,
"amount_excluding_tax": 0,
"currency": "usd",
"description": "Trial period for Teams Organization Seat",
"discount_amounts": [
],
"discountable": true,
"discounts": [
],
"livemode": false,
"metadata": {
"organizationId": "3fbc84ce-102d-4919-b89b-b08b00ead71a"
},
"period": {
"end": 1696515305,
"start": 1695910505
},
"plan": {
"id": "2020-teams-org-seat-monthly",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 400,
"amount_decimal": "400",
"billing_scheme": "per_unit",
"created": 1595263113,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"nickname": "Teams Organization Seat (Monthly) 2023",
"product": "prod_HgOooYXDr2DDAA",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"price": {
"id": "2020-teams-org-seat-monthly",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1595263113,
"currency": "usd",
"custom_unit_amount": null,
"livemode": false,
"lookup_key": null,
"metadata": {
},
"nickname": "Teams Organization Seat (Monthly) 2023",
"product": "prod_HgOooYXDr2DDAA",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "unspecified",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 400,
"unit_amount_decimal": "400"
},
"proration": false,
"proration_details": {
"credited_items": null
},
"quantity": 1,
"subscription": "sub_1NvKzdIGBnsLynRrKIHQamZc",
"subscription_item": "si_OimYNSbvuqdtTr",
"tax_amounts": [
],
"tax_rates": [
],
"type": "subscription",
"unit_amount_excluding_tax": "0"
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/invoices/in_1NvKzdIGBnsLynRr8fE8cpbg/lines"
},
"livemode": false,
"metadata": {
},
"next_payment_attempt": null,
"number": "3E96D078-0001",
"on_behalf_of": null,
"paid": true,
"paid_out_of_band": false,
"payment_intent": null,
"payment_settings": {
"default_mandate": null,
"payment_method_options": null,
"payment_method_types": null
},
"period_end": 1695910505,
"period_start": 1695910505,
"post_payment_credit_notes_amount": 0,
"pre_payment_credit_notes_amount": 0,
"quote": null,
"receipt_number": null,
"rendering": null,
"rendering_options": null,
"shipping_cost": null,
"shipping_details": null,
"starting_balance": 0,
"statement_descriptor": null,
"status": "paid",
"status_transitions": {
"finalized_at": 1695910505,
"marked_uncollectible_at": null,
"paid_at": 1695910505,
"voided_at": null
},
"subscription": "sub_1NvKzdIGBnsLynRrKIHQamZc",
"subscription_details": {
"metadata": {
"organizationId": "3fbc84ce-102d-4919-b89b-b08b00ead71a"
}
},
"subtotal": 0,
"subtotal_excluding_tax": 0,
"tax": null,
"test_clock": null,
"total": 0,
"total_discount_amounts": [
],
"total_excluding_tax": 0,
"total_tax_amounts": [
],
"transfer_data": null,
"webhooks_delivered_at": null
}
},
"livemode": false,
"pending_webhooks": 8,
"request": {
"id": "req_roIwONfgyfZdr4",
"idempotency_key": "dd2a171b-b9c7-4d2d-89d5-1ceae3c0595d"
},
"type": "invoice.created"
}

View File

@@ -1,400 +0,0 @@
{
"id": "evt_1PQaABIGBnsLynRrhoJjGnyz",
"object": "event",
"account": "acct_19smIXIGBnsLynRr",
"api_version": "2024-06-20",
"created": 1718133319,
"data": {
"object": {
"id": "in_1PQa9fIGBnsLynRraYIqTdBs",
"object": "invoice",
"account_country": "US",
"account_name": "Bitwarden Inc.",
"account_tax_ids": null,
"amount_due": 84240,
"amount_paid": 0,
"amount_remaining": 84240,
"amount_shipping": 0,
"application": null,
"attempt_count": 0,
"attempted": false,
"auto_advance": true,
"automatic_tax": {
"enabled": true,
"liability": {
"type": "self"
},
"status": "complete"
},
"billing_reason": "subscription_update",
"charge": null,
"collection_method": "send_invoice",
"created": 1718133291,
"currency": "usd",
"custom_fields": [
{
"name": "Provider",
"value": "MSP"
}
],
"customer": "cus_QH8QVKyTh2lfcG",
"customer_address": {
"city": null,
"country": "US",
"line1": null,
"line2": null,
"postal_code": "12345",
"state": null
},
"customer_email": "billing@msp.com",
"customer_name": null,
"customer_phone": null,
"customer_shipping": null,
"customer_tax_exempt": "none",
"customer_tax_ids": [
],
"default_payment_method": null,
"default_source": null,
"default_tax_rates": [
],
"description": null,
"discount": {
"id": "di_1PQa9eIGBnsLynRrwwYr2bGD",
"object": "discount",
"checkout_session": null,
"coupon": {
"id": "msp-discount-35",
"object": "coupon",
"amount_off": null,
"created": 1678805729,
"currency": null,
"duration": "forever",
"duration_in_months": null,
"livemode": false,
"max_redemptions": null,
"metadata": {
},
"name": "MSP Discount - 35%",
"percent_off": 35,
"redeem_by": null,
"times_redeemed": 515,
"valid": true,
"percent_off_precise": 35
},
"customer": "cus_QH8QVKyTh2lfcG",
"end": null,
"invoice": null,
"invoice_item": null,
"promotion_code": null,
"start": 1718133290,
"subscription": null,
"subscription_item": null
},
"discounts": [
"di_1PQa9eIGBnsLynRrwwYr2bGD"
],
"due_date": 1720725291,
"effective_at": 1718136893,
"ending_balance": 0,
"footer": null,
"from_invoice": null,
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_19smIXIGBnsLynRr/test_YWNjdF8xOXNtSVhJR0Juc0x5blJyLF9RSDhRYVNIejNDMXBMVXAzM0M3S2RwaUt1Z3NuVHVzLDEwODY3NDEyMg0200RT8cC2nw?s=ap",
"invoice_pdf": "https://pay.stripe.com/invoice/acct_19smIXIGBnsLynRr/test_YWNjdF8xOXNtSVhJR0Juc0x5blJyLF9RSDhRYVNIejNDMXBMVXAzM0M3S2RwaUt1Z3NuVHVzLDEwODY3NDEyMg0200RT8cC2nw/pdf?s=ap",
"issuer": {
"type": "self"
},
"last_finalization_error": null,
"latest_revision": null,
"lines": {
"object": "list",
"data": [
{
"id": "sub_1PQa9fIGBnsLynRr83lNrFHa",
"object": "line_item",
"amount": 50000,
"amount_excluding_tax": 50000,
"currency": "usd",
"description": null,
"discount_amounts": [
{
"amount": 17500,
"discount": "di_1PQa9eIGBnsLynRrwwYr2bGD"
}
],
"discountable": true,
"discounts": [
],
"invoice": "in_1PQa9fIGBnsLynRraYIqTdBs",
"livemode": false,
"metadata": {
},
"period": {
"end": 1720725291,
"start": 1718133291
},
"plan": {
"id": "2023-teams-org-seat-monthly",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 500,
"amount_decimal": "500",
"billing_scheme": "per_unit",
"created": 1695839010,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"meter": null,
"nickname": "Teams Organization Seat (Monthly)",
"product": "prod_HgOooYXDr2DDAA",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed",
"name": "Password Manager - Teams Plan",
"statement_description": null,
"statement_descriptor": null,
"tiers": null
},
"price": {
"id": "2023-teams-org-seat-monthly",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1695839010,
"currency": "usd",
"custom_unit_amount": null,
"livemode": false,
"lookup_key": null,
"metadata": {
},
"nickname": "Teams Organization Seat (Monthly)",
"product": "prod_HgOooYXDr2DDAA",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"meter": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "exclusive",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 500,
"unit_amount_decimal": "500"
},
"proration": false,
"proration_details": {
"credited_items": null
},
"quantity": 100,
"subscription": null,
"subscription_item": "si_QH8Qo4WEJxOVwx",
"tax_amounts": [
{
"amount": 2600,
"inclusive": false,
"tax_rate": "txr_1OZyBuIGBnsLynRrX0PJLuMC",
"taxability_reason": "standard_rated",
"taxable_amount": 32500
}
],
"tax_rates": [
],
"type": "subscription",
"unit_amount_excluding_tax": "500",
"unique_id": "il_1PQa9fIGBnsLynRrSJ3cxrdU",
"unique_line_item_id": "sli_1acb3eIGBnsLynRr4b9c2f48"
},
{
"id": "sub_1PQa9fIGBnsLynRr83lNrFHa",
"object": "line_item",
"amount": 70000,
"amount_excluding_tax": 70000,
"currency": "usd",
"description": null,
"discount_amounts": [
{
"amount": 24500,
"discount": "di_1PQa9eIGBnsLynRrwwYr2bGD"
}
],
"discountable": true,
"discounts": [
],
"invoice": "in_1PQa9fIGBnsLynRraYIqTdBs",
"livemode": false,
"metadata": {
},
"period": {
"end": 1720725291,
"start": 1718133291
},
"plan": {
"id": "2023-enterprise-seat-monthly",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 700,
"amount_decimal": "700",
"billing_scheme": "per_unit",
"created": 1695152194,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"meter": null,
"nickname": "Enterprise Organization (Monthly)",
"product": "prod_HgSOgzUlYDFOzf",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed",
"name": "Password Manager - Enterprise Plan",
"statement_description": null,
"statement_descriptor": null,
"tiers": null
},
"price": {
"id": "2023-enterprise-seat-monthly",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1695152194,
"currency": "usd",
"custom_unit_amount": null,
"livemode": false,
"lookup_key": null,
"metadata": {
},
"nickname": "Enterprise Organization (Monthly)",
"product": "prod_HgSOgzUlYDFOzf",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"meter": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "exclusive",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 700,
"unit_amount_decimal": "700"
},
"proration": false,
"proration_details": {
"credited_items": null
},
"quantity": 100,
"subscription": null,
"subscription_item": "si_QH8QUjtceXvcis",
"tax_amounts": [
{
"amount": 3640,
"inclusive": false,
"tax_rate": "txr_1OZyBuIGBnsLynRrX0PJLuMC",
"taxability_reason": "standard_rated",
"taxable_amount": 45500
}
],
"tax_rates": [
],
"type": "subscription",
"unit_amount_excluding_tax": "700",
"unique_id": "il_1PQa9fIGBnsLynRrVviet37m",
"unique_line_item_id": "sli_11b229IGBnsLynRr837b79d0"
}
],
"has_more": false,
"total_count": 2,
"url": "/v1/invoices/in_1PQa9fIGBnsLynRraYIqTdBs/lines"
},
"livemode": false,
"metadata": {
},
"next_payment_attempt": null,
"number": "525EB050-0001",
"on_behalf_of": null,
"paid": false,
"paid_out_of_band": false,
"payment_intent": "pi_3PQaA7IGBnsLynRr1swr9XJE",
"payment_settings": {
"default_mandate": null,
"payment_method_options": null,
"payment_method_types": null
},
"period_end": 1718133291,
"period_start": 1718133291,
"post_payment_credit_notes_amount": 0,
"pre_payment_credit_notes_amount": 0,
"quote": null,
"receipt_number": null,
"rendering": null,
"rendering_options": null,
"shipping_cost": null,
"shipping_details": null,
"starting_balance": 0,
"statement_descriptor": null,
"status": "open",
"status_transitions": {
"finalized_at": 1718136893,
"marked_uncollectible_at": null,
"paid_at": null,
"voided_at": null
},
"subscription": "sub_1PQa9fIGBnsLynRr83lNrFHa",
"subscription_details": {
"metadata": {
"providerId": "655bc5a3-2332-4201-a9a6-b18c013d0572"
}
},
"subtotal": 120000,
"subtotal_excluding_tax": 120000,
"tax": 6240,
"test_clock": "clock_1PQaA4IGBnsLynRrptkZjgxc",
"total": 84240,
"total_discount_amounts": [
{
"amount": 42000,
"discount": "di_1PQa9eIGBnsLynRrwwYr2bGD"
}
],
"total_excluding_tax": 78000,
"total_tax_amounts": [
{
"amount": 6240,
"inclusive": false,
"tax_rate": "txr_1OZyBuIGBnsLynRrX0PJLuMC",
"taxability_reason": "standard_rated",
"taxable_amount": 78000
}
],
"transfer_data": null,
"webhooks_delivered_at": 1718133293,
"application_fee": null,
"billing": "send_invoice",
"closed": false,
"date": 1718133291,
"finalized_at": 1718136893,
"forgiven": false,
"payment": null,
"statement_description": null,
"tax_percent": 8
}
},
"livemode": false,
"pending_webhooks": 5,
"request": null,
"type": "invoice.finalized",
"user_id": "acct_19smIXIGBnsLynRr"
}

View File

@@ -1,225 +0,0 @@
{
"id": "evt_1Nv0w8IGBnsLynRrZoDVI44u",
"object": "event",
"api_version": "2024-06-20",
"created": 1695833408,
"data": {
"object": {
"object": "invoice",
"account_country": "US",
"account_name": "Bitwarden Inc.",
"account_tax_ids": null,
"amount_due": 0,
"amount_paid": 0,
"amount_remaining": 0,
"amount_shipping": 0,
"application": null,
"application_fee_amount": null,
"attempt_count": 0,
"attempted": false,
"automatic_tax": {
"enabled": true,
"status": "complete"
},
"billing_reason": "upcoming",
"charge": null,
"collection_method": "charge_automatically",
"created": 1697128681,
"currency": "usd",
"custom_fields": null,
"customer": "cus_M8DV9wiyNa2JxQ",
"customer_address": {
"city": null,
"country": "US",
"line1": "",
"line2": null,
"postal_code": "90019",
"state": null
},
"customer_email": "vphan@bitwarden.com",
"customer_name": null,
"customer_phone": null,
"customer_shipping": null,
"customer_tax_exempt": "none",
"customer_tax_ids": [
],
"default_payment_method": null,
"default_source": null,
"default_tax_rates": [
],
"description": null,
"discount": null,
"discounts": [
],
"due_date": null,
"effective_at": null,
"ending_balance": -6779,
"footer": null,
"from_invoice": null,
"last_finalization_error": null,
"latest_revision": null,
"lines": {
"object": "list",
"data": [
{
"id": "il_tmp_12b5e8IGBnsLynRr1996ac3a",
"object": "line_item",
"amount": 2000,
"amount_excluding_tax": 2000,
"currency": "usd",
"description": "5 × 2019 Enterprise Seat (Monthly) (at $4.00 / month)",
"discount_amounts": [
],
"discountable": true,
"discounts": [
],
"livemode": false,
"metadata": {
},
"period": {
"end": 1699807081,
"start": 1697128681
},
"plan": {
"id": "enterprise-org-seat-monthly",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 400,
"amount_decimal": "400",
"billing_scheme": "per_unit",
"created": 1494268635,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"nickname": "2019 Enterprise Seat (Monthly)",
"product": "prod_BVButYytPSlgs6",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"price": {
"id": "enterprise-org-seat-monthly",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1494268635,
"currency": "usd",
"custom_unit_amount": null,
"livemode": false,
"lookup_key": null,
"metadata": {
},
"nickname": "2019 Enterprise Seat (Monthly)",
"product": "prod_BVButYytPSlgs6",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "unspecified",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 400,
"unit_amount_decimal": "400"
},
"proration": false,
"proration_details": {
"credited_items": null
},
"quantity": 5,
"subscription": "sub_1NQxz4IGBnsLynRr1KbitG7v",
"subscription_item": "si_ODOmLnPDHBuMxX",
"tax_amounts": [
{
"amount": 0,
"inclusive": false,
"tax_rate": "txr_1N6XCyIGBnsLynRr0LHs4AUD",
"taxability_reason": "product_exempt",
"taxable_amount": 0
}
],
"tax_rates": [
],
"type": "subscription",
"unit_amount_excluding_tax": "400"
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/invoices/upcoming/lines?customer=cus_M8DV9wiyNa2JxQ&subscription=sub_1NQxz4IGBnsLynRr1KbitG7v"
},
"livemode": false,
"metadata": {
},
"next_payment_attempt": 1697132281,
"number": null,
"on_behalf_of": null,
"paid": false,
"paid_out_of_band": false,
"payment_intent": null,
"payment_settings": {
"default_mandate": null,
"payment_method_options": null,
"payment_method_types": null
},
"period_end": 1697128681,
"period_start": 1694536681,
"post_payment_credit_notes_amount": 0,
"pre_payment_credit_notes_amount": 0,
"quote": null,
"receipt_number": null,
"rendering": null,
"rendering_options": null,
"shipping_cost": null,
"shipping_details": null,
"starting_balance": -8779,
"statement_descriptor": null,
"status": "draft",
"status_transitions": {
"finalized_at": null,
"marked_uncollectible_at": null,
"paid_at": null,
"voided_at": null
},
"subscription": "sub_1NQxz4IGBnsLynRr1KbitG7v",
"subscription_details": {
"metadata": {
}
},
"subtotal": 2000,
"subtotal_excluding_tax": 2000,
"tax": 0,
"test_clock": null,
"total": 2000,
"total_discount_amounts": [
],
"total_excluding_tax": 2000,
"total_tax_amounts": [
{
"amount": 0,
"inclusive": false,
"tax_rate": "txr_1N6XCyIGBnsLynRr0LHs4AUD",
"taxability_reason": "product_exempt",
"taxable_amount": 0
}
],
"transfer_data": null,
"webhooks_delivered_at": null
}
},
"livemode": false,
"pending_webhooks": 5,
"request": {
"id": null,
"idempotency_key": null
},
"type": "invoice.upcoming"
}

View File

@@ -1,63 +0,0 @@
{
"id": "evt_1NvKzcIGBnsLynRrPJ3hybkd",
"object": "event",
"api_version": "2024-06-20",
"created": 1695910504,
"data": {
"object": {
"id": "pm_1NvKzbIGBnsLynRry6x7Buvc",
"object": "payment_method",
"billing_details": {
"address": {
"city": null,
"country": null,
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": null,
"name": null,
"phone": null
},
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": "pass"
},
"country": "US",
"exp_month": 6,
"exp_year": 2033,
"fingerprint": "0VgUBpvqcUUnuSmK",
"funding": "credit",
"generated_from": null,
"last4": "4242",
"networks": {
"available": [
"visa"
],
"preferred": null
},
"three_d_secure_usage": {
"supported": true
},
"wallet": null
},
"created": 1695910503,
"customer": "cus_OimYrxnMTMMK1E",
"livemode": false,
"metadata": {
},
"type": "card"
}
},
"livemode": false,
"pending_webhooks": 7,
"request": {
"id": "req_2WslNSBD9wAV5v",
"idempotency_key": "db1a648a-3445-47b3-a403-9f3d1303a880"
},
"type": "payment_method.attached"
}

View File

@@ -1,6 +1,5 @@
using Bit.Billing.Services;
using Bit.Billing.Services.Implementations;
using Bit.Billing.Test.Utilities;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories;
@@ -59,29 +58,69 @@ public class ProviderEventServiceTests
public async Task TryRecordInvoiceLineItems_EventTypeNotInvoiceCreatedOrInvoiceFinalized_NoOp()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached);
var stripeEvent = new Event { Type = "payment_method.attached" };
// Act
await _providerEventService.TryRecordInvoiceLineItems(stripeEvent);
// Assert
await _stripeEventService.DidNotReceiveWithAnyArgs().GetInvoice(Arg.Any<Event>());
await _stripeEventService.DidNotReceiveWithAnyArgs().GetInvoice(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>?>());
}
[Fact]
public async Task TryRecordInvoiceLineItems_InvoiceParentTypeNotSubscriptionDetails_NoOp()
{
// Arrange
var stripeEvent = new Event
{
Type = "invoice.created"
};
var invoice = new Invoice
{
Parent = new InvoiceParent
{
Type = "credit_note",
SubscriptionDetails = new InvoiceParentSubscriptionDetails
{
SubscriptionId = "sub_1"
}
}
};
_stripeEventService.GetInvoice(stripeEvent, true, Arg.Any<List<string>?>()).Returns(invoice);
// Act
await _providerEventService.TryRecordInvoiceLineItems(stripeEvent);
// Assert
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(Arg.Any<string>());
}
[Fact]
public async Task TryRecordInvoiceLineItems_EventNotProviderRelated_NoOp()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
var stripeEvent = new Event
{
Type = "invoice.created"
};
const string subscriptionId = "sub_1";
var invoice = new Invoice
{
SubscriptionId = subscriptionId
Parent = new InvoiceParent
{
Type = "subscription_details",
SubscriptionDetails = new InvoiceParentSubscriptionDetails
{
SubscriptionId = subscriptionId
}
}
};
_stripeEventService.GetInvoice(stripeEvent).Returns(invoice);
_stripeEventService.GetInvoice(stripeEvent, true, Arg.Any<List<string>?>()).Returns(invoice);
var subscription = new Subscription
{
@@ -101,7 +140,10 @@ public class ProviderEventServiceTests
public async Task TryRecordInvoiceLineItems_InvoiceCreated_Succeeds()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
var stripeEvent = new Event
{
Type = "invoice.created"
};
const string subscriptionId = "sub_1";
var providerId = Guid.NewGuid();
@@ -110,17 +152,26 @@ public class ProviderEventServiceTests
{
Id = "invoice_1",
Number = "A",
SubscriptionId = subscriptionId,
Discount = new Discount
Parent = new InvoiceParent
{
Coupon = new Coupon
Type = "subscription_details",
SubscriptionDetails = new InvoiceParentSubscriptionDetails
{
PercentOff = 35
SubscriptionId = subscriptionId
}
}
},
Discounts = [
new Discount
{
Coupon = new Coupon
{
PercentOff = 35
}
}
]
};
_stripeEventService.GetInvoice(stripeEvent).Returns(invoice);
_stripeEventService.GetInvoice(stripeEvent, true, Arg.Any<List<string>?>()).Returns(invoice);
var subscription = new Subscription
{
@@ -249,7 +300,10 @@ public class ProviderEventServiceTests
public async Task TryRecordInvoiceLineItems_InvoiceFinalized_Succeeds()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceFinalized);
var stripeEvent = new Event
{
Type = "invoice.finalized"
};
const string subscriptionId = "sub_1";
var providerId = Guid.NewGuid();
@@ -258,10 +312,17 @@ public class ProviderEventServiceTests
{
Id = "invoice_1",
Number = "A",
SubscriptionId = subscriptionId
Parent = new InvoiceParent
{
Type = "subscription_details",
SubscriptionDetails = new InvoiceParentSubscriptionDetails
{
SubscriptionId = subscriptionId
}
},
};
_stripeEventService.GetInvoice(stripeEvent).Returns(invoice);
_stripeEventService.GetInvoice(stripeEvent, true, Arg.Any<List<string>?>()).Returns(invoice);
var subscription = new Subscription
{

View File

@@ -2,6 +2,7 @@
using Bit.Billing.Services;
using Bit.Billing.Services.Implementations;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Billing.Extensions;
using Bit.Core.Services;
using NSubstitute;
using Stripe;
@@ -38,7 +39,13 @@ public class SubscriptionDeletedHandlerTests
var subscription = new Subscription
{
Status = "active",
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30),
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }
]
},
Metadata = new Dictionary<string, string>()
};
@@ -63,11 +70,14 @@ public class SubscriptionDeletedHandlerTests
var subscription = new Subscription
{
Status = StripeSubscriptionStatus.Canceled,
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30),
Metadata = new Dictionary<string, string>
Items = new StripeList<SubscriptionItem>
{
{ "organizationId", organizationId.ToString() }
}
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }
]
},
Metadata = new Dictionary<string, string> { { "organizationId", organizationId.ToString() } }
};
_stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription);
@@ -79,7 +89,7 @@ public class SubscriptionDeletedHandlerTests
// Assert
await _organizationDisableCommand.Received(1)
.DisableAsync(organizationId, subscription.CurrentPeriodEnd);
.DisableAsync(organizationId, subscription.GetCurrentPeriodEnd());
}
[Fact]
@@ -91,11 +101,14 @@ public class SubscriptionDeletedHandlerTests
var subscription = new Subscription
{
Status = StripeSubscriptionStatus.Canceled,
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30),
Metadata = new Dictionary<string, string>
Items = new StripeList<SubscriptionItem>
{
{ "userId", userId.ToString() }
}
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }
]
},
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } }
};
_stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription);
@@ -107,7 +120,7 @@ public class SubscriptionDeletedHandlerTests
// Assert
await _userService.Received(1)
.DisablePremiumAsync(userId, subscription.CurrentPeriodEnd);
.DisablePremiumAsync(userId, subscription.GetCurrentPeriodEnd());
}
[Fact]
@@ -119,11 +132,14 @@ public class SubscriptionDeletedHandlerTests
var subscription = new Subscription
{
Status = StripeSubscriptionStatus.Canceled,
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30),
Metadata = new Dictionary<string, string>
Items = new StripeList<SubscriptionItem>
{
{ "organizationId", organizationId.ToString() }
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }
]
},
Metadata = new Dictionary<string, string> { { "organizationId", organizationId.ToString() } },
CancellationDetails = new SubscriptionCancellationDetails
{
Comment = "Cancelled as part of provider migration to Consolidated Billing"
@@ -151,11 +167,14 @@ public class SubscriptionDeletedHandlerTests
var subscription = new Subscription
{
Status = StripeSubscriptionStatus.Canceled,
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30),
Metadata = new Dictionary<string, string>
Items = new StripeList<SubscriptionItem>
{
{ "organizationId", organizationId.ToString() }
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }
]
},
Metadata = new Dictionary<string, string> { { "organizationId", organizationId.ToString() } },
CancellationDetails = new SubscriptionCancellationDetails
{
Comment = "Organization was added to Provider"

View File

@@ -96,7 +96,13 @@ public class SubscriptionUpdatedHandlerTests
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Unpaid,
CurrentPeriodEnd = currentPeriodEnd,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
},
Metadata = new Dictionary<string, string> { { "organizationId", organizationId.ToString() } },
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }
};
@@ -142,7 +148,13 @@ public class SubscriptionUpdatedHandlerTests
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Unpaid,
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30),
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }
]
},
Metadata = new Dictionary<string, string>
{
["providerId"] = providerId.ToString(),
@@ -206,7 +218,13 @@ public class SubscriptionUpdatedHandlerTests
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Unpaid,
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30),
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }
]
},
Metadata = new Dictionary<string, string> { ["providerId"] = providerId.ToString() },
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" },
TestClock = null
@@ -257,6 +275,13 @@ public class SubscriptionUpdatedHandlerTests
var subscription = new Subscription
{
Id = subscriptionId,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }
]
},
Status = StripeSubscriptionStatus.Unpaid,
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } },
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }
@@ -306,6 +331,13 @@ public class SubscriptionUpdatedHandlerTests
var subscription = new Subscription
{
Id = subscriptionId,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }
]
},
Status = StripeSubscriptionStatus.Unpaid,
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } },
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }
@@ -348,7 +380,13 @@ public class SubscriptionUpdatedHandlerTests
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.IncompleteExpired,
CurrentPeriodEnd = currentPeriodEnd,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
},
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } },
LatestInvoice = new Invoice { BillingReason = "renewal" }
};
@@ -390,7 +428,13 @@ public class SubscriptionUpdatedHandlerTests
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Unpaid,
CurrentPeriodEnd = currentPeriodEnd,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
},
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } },
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }
};
@@ -426,7 +470,13 @@ public class SubscriptionUpdatedHandlerTests
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Unpaid,
CurrentPeriodEnd = currentPeriodEnd,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
},
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } },
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }
};
@@ -464,13 +514,16 @@ public class SubscriptionUpdatedHandlerTests
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Unpaid,
CurrentPeriodEnd = currentPeriodEnd,
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } },
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId } }
new SubscriptionItem
{
CurrentPeriodEnd = currentPeriodEnd,
Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId }
}
]
}
};
@@ -508,7 +561,13 @@ public class SubscriptionUpdatedHandlerTests
var subscription = new Subscription
{
Status = StripeSubscriptionStatus.Active,
CurrentPeriodEnd = currentPeriodEnd,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
},
Metadata = new Dictionary<string, string> { { "organizationId", organizationId.ToString() } }
};
@@ -552,7 +611,13 @@ public class SubscriptionUpdatedHandlerTests
var subscription = new Subscription
{
Status = StripeSubscriptionStatus.Active,
CurrentPeriodEnd = currentPeriodEnd,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
},
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } }
};
@@ -583,7 +648,13 @@ public class SubscriptionUpdatedHandlerTests
var subscription = new Subscription
{
Status = StripeSubscriptionStatus.Active,
CurrentPeriodEnd = currentPeriodEnd,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
},
Metadata = new Dictionary<string, string> { { "organizationId", organizationId.ToString() } }
};
@@ -616,18 +687,24 @@ public class SubscriptionUpdatedHandlerTests
{
Id = "sub_123",
Status = StripeSubscriptionStatus.Active,
CurrentPeriodEnd = DateTime.UtcNow.AddDays(10),
CustomerId = "cus_123",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { Plan = new Plan { Id = "2023-enterprise-org-seat-annually" } }]
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(10),
Plan = new Plan { Id = "2023-enterprise-org-seat-annually" }
}
]
},
Customer = new Customer
{
Balance = 0,
Discount = new Discount { Coupon = new Coupon { Id = "sm-standalone" } }
},
Discount = new Discount { Coupon = new Coupon { Id = "sm-standalone" } },
Discounts = [new Discount { Coupon = new Coupon { Id = "sm-standalone" } }],
Metadata = new Dictionary<string, string> { { "organizationId", organizationId.ToString() } }
};
@@ -728,7 +805,6 @@ public class SubscriptionUpdatedHandlerTests
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
[Fact]
public async Task
HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasCanceled_EnableProvider()
@@ -998,6 +1074,13 @@ public class SubscriptionUpdatedHandlerTests
var newSubscription = new Subscription
{
Id = previousSubscription?.Id ?? "sub_123",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }
]
},
Status = StripeSubscriptionStatus.Active,
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } }
};
@@ -1021,7 +1104,10 @@ public class SubscriptionUpdatedHandlerTests
{
new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Unpaid } },
new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Incomplete } },
new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.IncompleteExpired } },
new object[]
{
new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.IncompleteExpired }
},
new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Paused } }
};
}

View File

@@ -1,35 +0,0 @@
using Stripe;
namespace Bit.Billing.Test.Utilities;
public enum StripeEventType
{
ChargeSucceeded,
CustomerSubscriptionUpdated,
CustomerUpdated,
InvoiceCreated,
InvoiceFinalized,
InvoiceUpcoming,
PaymentMethodAttached
}
public static class StripeTestEvents
{
public static async Task<Event> GetAsync(StripeEventType eventType)
{
var fileName = eventType switch
{
StripeEventType.ChargeSucceeded => "charge.succeeded.json",
StripeEventType.CustomerSubscriptionUpdated => "customer.subscription.updated.json",
StripeEventType.CustomerUpdated => "customer.updated.json",
StripeEventType.InvoiceCreated => "invoice.created.json",
StripeEventType.InvoiceFinalized => "invoice.finalized.json",
StripeEventType.InvoiceUpcoming => "invoice.upcoming.json",
StripeEventType.PaymentMethodAttached => "payment_method.attached.json"
};
var resource = await EmbeddedResourceReader.ReadAsync("Events", fileName);
return EventUtility.ParseEvent(resource);
}
}

View File

@@ -294,7 +294,8 @@ public class InvoiceExtensionsTests
Amount = 600
}
);
invoice.Tax = 120; // $1.20 in cents
invoice.TotalTaxes = [new InvoiceTotalTax { Amount = 120 }]; // $1.20 in cents
var subscription = new Subscription();
// Act
@@ -318,7 +319,7 @@ public class InvoiceExtensionsTests
Amount = 600
}
);
invoice.Tax = null;
invoice.TotalTaxes = [];
var subscription = new Subscription();
// Act
@@ -341,7 +342,7 @@ public class InvoiceExtensionsTests
Amount = 600
}
);
invoice.Tax = 0;
invoice.TotalTaxes = [new InvoiceTotalTax { Amount = 0 }];
var subscription = new Subscription();
// Act
@@ -374,7 +375,7 @@ public class InvoiceExtensionsTests
var invoice = new Invoice
{
Lines = lineItems,
Tax = 200 // Additional $2.00 tax
TotalTaxes = [new InvoiceTotalTax { Amount = 200 }] // Additional $2.00 tax
};
var subscription = new Subscription();

View File

@@ -227,8 +227,16 @@ If you believe you need to change the version for a valid reason, please discuss
Status = "active",
TrialStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
TrialEnd = new DateTime(2024, 2, 1, 0, 0, 0, DateTimeKind.Utc),
CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)
Items = new StripeList<SubscriptionItem>
{
Data = [
new SubscriptionItem
{
CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)
}
]
}
};
return new SubscriptionInfo

View File

@@ -141,8 +141,16 @@ If you believe you need to change the version for a valid reason, please discuss
Status = "active",
TrialStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
TrialEnd = new DateTime(2024, 2, 1, 0, 0, 0, DateTimeKind.Utc),
CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)
Items = new StripeList<SubscriptionItem>
{
Data = [
new SubscriptionItem
{
CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)
}
]
}
};
return new SubscriptionInfo

View File

@@ -54,7 +54,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 500,
TotalTaxes = [new InvoiceTotalTax { Amount = 500 }],
Total = 5500
};
@@ -77,7 +77,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2021-family-for-enterprise-annually" &&
options.SubscriptionDetails.Items[0].Quantity == 1 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -112,7 +112,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 750,
TotalTaxes = [new InvoiceTotalTax { Amount = 750 }],
Total = 8250
};
@@ -137,7 +137,9 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "2023-teams-org-seat-monthly" && item.Quantity == 5) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "secrets-manager-teams-seat-monthly" && item.Quantity == 3) &&
options.Coupon == CouponIDs.SecretsManagerStandalone));
options.Discounts != null &&
options.Discounts.Count == 1 &&
options.Discounts[0].Coupon == CouponIDs.SecretsManagerStandalone));
}
[Fact]
@@ -173,7 +175,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 1200,
TotalTaxes = [new InvoiceTotalTax { Amount = 1200 }],
Total = 12200
};
@@ -205,7 +207,7 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "secrets-manager-enterprise-seat-annually" && item.Quantity == 8) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "secrets-manager-service-account-2024-annually" && item.Quantity == 3) &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -234,7 +236,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 300,
TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],
Total = 3300
};
@@ -257,7 +259,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2020-families-org-annually" &&
options.SubscriptionDetails.Items[0].Quantity == 6 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -286,7 +288,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 0,
TotalTaxes = [new InvoiceTotalTax { Amount = 0 }],
Total = 2700
};
@@ -309,7 +311,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" &&
options.SubscriptionDetails.Items[0].Quantity == 3 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -339,7 +341,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 2100,
TotalTaxes = [new InvoiceTotalTax { Amount = 2100 }],
Total = 12100
};
@@ -365,7 +367,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-enterprise-seat-monthly" &&
options.SubscriptionDetails.Items[0].Quantity == 15 &&
options.Coupon == null));
options.Discounts == null));
}
#endregion
@@ -399,7 +401,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 120,
TotalTaxes = [new InvoiceTotalTax { Amount = 120 }],
Total = 1320
};
@@ -422,7 +424,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" &&
options.SubscriptionDetails.Items[0].Quantity == 2 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -452,7 +454,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 400,
TotalTaxes = [new InvoiceTotalTax { Amount = 400 }],
Total = 4400
};
@@ -475,7 +477,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2020-families-org-annually" &&
options.SubscriptionDetails.Items[0].Quantity == 1 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -524,7 +526,11 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 900,
TotalTaxes = [new InvoiceTotalTax
{
Amount = 900
}
],
Total = 9900
};
@@ -546,7 +552,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-annually" &&
options.SubscriptionDetails.Items[0].Quantity == 6 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -595,7 +601,11 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 1200,
TotalTaxes = [new InvoiceTotalTax
{
Amount = 1200
}
],
Total = 13200
};
@@ -617,7 +627,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-enterprise-org-seat-annually" &&
options.SubscriptionDetails.Items[0].Quantity == 6 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -647,7 +657,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 800,
TotalTaxes = [new InvoiceTotalTax { Amount = 800 }],
Total = 8800
};
@@ -672,7 +682,7 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "2023-enterprise-org-seat-annually" && item.Quantity == 2) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "secrets-manager-enterprise-seat-annually" && item.Quantity == 2) &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -724,7 +734,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 1500,
TotalTaxes = [new InvoiceTotalTax { Amount = 1500 }],
Total = 16500
};
@@ -753,7 +763,7 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "secrets-manager-enterprise-seat-annually" && item.Quantity == 5) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "secrets-manager-service-account-2024-annually" && item.Quantity == 10) &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -808,7 +818,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 600,
TotalTaxes = [new InvoiceTotalTax { Amount = 600 }],
Total = 6600
};
@@ -831,7 +841,9 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-enterprise-org-seat-annually" &&
options.SubscriptionDetails.Items[0].Quantity == 5 &&
options.Coupon == "EXISTING_DISCOUNT_50"));
options.Discounts != null &&
options.Discounts.Count == 1 &&
options.Discounts[0].Coupon == "EXISTING_DISCOUNT_50"));
}
[Fact]
@@ -911,7 +923,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 600,
TotalTaxes = [new InvoiceTotalTax { Amount = 600 }],
Total = 6600
};
@@ -934,7 +946,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" &&
options.SubscriptionDetails.Items[0].Quantity == 10 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -976,7 +988,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 1200,
TotalTaxes = [new InvoiceTotalTax { Amount = 1200 }],
Total = 13200
};
@@ -1001,7 +1013,7 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "2023-enterprise-org-seat-annually" && item.Quantity == 15) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "storage-gb-annually" && item.Quantity == 5) &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -1043,7 +1055,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 800,
TotalTaxes = [new InvoiceTotalTax { Amount = 800 }],
Total = 8800
};
@@ -1066,7 +1078,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "secrets-manager-teams-seat-annually" &&
options.SubscriptionDetails.Items[0].Quantity == 8 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -1111,7 +1123,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 1500,
TotalTaxes = [new InvoiceTotalTax { Amount = 1500 }],
Total = 16500
};
@@ -1139,7 +1151,7 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "secrets-manager-enterprise-seat-monthly" && item.Quantity == 12) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "secrets-manager-service-account-2024-monthly" && item.Quantity == 20) &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -1192,7 +1204,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 2500,
TotalTaxes = [new InvoiceTotalTax { Amount = 2500 }],
Total = 27500
};
@@ -1224,7 +1236,9 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "secrets-manager-enterprise-seat-annually" && item.Quantity == 15) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "secrets-manager-service-account-2024-annually" && item.Quantity == 30) &&
options.Coupon == "ENTERPRISE_DISCOUNT_20"));
options.Discounts != null &&
options.Discounts.Count == 1 &&
options.Discounts[0].Coupon == "ENTERPRISE_DISCOUNT_20"));
}
[Fact]
@@ -1266,7 +1280,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 500,
TotalTaxes = [new InvoiceTotalTax { Amount = 500 }],
Total = 5500
};
@@ -1291,7 +1305,7 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "2020-families-org-annually" && item.Quantity == 6) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "personal-storage-gb-annually" && item.Quantity == 2) &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -1368,7 +1382,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 300,
TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],
Total = 3300
};
@@ -1391,7 +1405,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" &&
options.SubscriptionDetails.Items[0].Quantity == 5 &&
options.Coupon == null));
options.Discounts == null));
}
#endregion

View File

@@ -27,25 +27,27 @@ public class GetCloudOrganizationLicenseQueryTests
{
[Theory]
[BitAutoData]
public async Task GetLicenseAsync_InvalidInstallationId_Throws(SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
public async Task GetLicenseAsync_InvalidInstallationId_Throws(
SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
Organization organization, Guid installationId, int version)
{
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).ReturnsNull();
var exception = await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.GetLicenseAsync(organization, installationId, version));
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.GetLicenseAsync(organization, installationId, version));
Assert.Contains("Invalid installation id", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetLicenseAsync_DisabledOrganization_Throws(SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
public async Task GetLicenseAsync_DisabledOrganization_Throws(
SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
Organization organization, Guid installationId, Installation installation)
{
installation.Enabled = false;
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);
var exception = await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.GetLicenseAsync(organization, installationId));
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.GetLicenseAsync(organization, installationId));
Assert.Contains("Invalid installation id", exception.Message);
}
@@ -71,7 +73,8 @@ public class GetCloudOrganizationLicenseQueryTests
[Theory]
[BitAutoData]
public async Task GetLicenseAsync_WhenFeatureFlagEnabled_CreatesToken(SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
public async Task GetLicenseAsync_WhenFeatureFlagEnabled_CreatesToken(
SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo,
byte[] licenseSignature, string token)
{
@@ -90,7 +93,8 @@ public class GetCloudOrganizationLicenseQueryTests
[Theory]
[BitAutoData]
public async Task GetLicenseAsync_MSPManagedOrganization_UsesProviderSubscription(SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
public async Task GetLicenseAsync_MSPManagedOrganization_UsesProviderSubscription(
SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo,
byte[] licenseSignature, Provider provider)
{
@@ -99,8 +103,17 @@ public class GetCloudOrganizationLicenseQueryTests
subInfo.Subscription = new SubscriptionInfo.BillingSubscription(new Subscription
{
CurrentPeriodStart = DateTime.UtcNow,
CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1)
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodStart = DateTime.UtcNow,
CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1)
}
]
}
});
installation.Enabled = true;

View File

@@ -272,7 +272,16 @@ public class GetOrganizationWarningsQueryTests
CollectionMethod = CollectionMethod.SendInvoice,
Customer = new Customer(),
Status = SubscriptionStatus.Active,
CurrentPeriodEnd = now.AddDays(10),
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = now.AddDays(10)
}
]
},
TestClock = new TestClock
{
FrozenTime = now

View File

@@ -1,4 +1,5 @@
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Premium.Commands;
using Bit.Core.Billing.Services;
@@ -105,6 +106,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();
@@ -152,6 +163,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();
@@ -241,7 +262,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();
@@ -286,6 +316,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();
@@ -326,7 +366,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "incomplete";
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();
@@ -342,7 +391,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
// Assert
Assert.True(result.IsT0);
Assert.True(user.Premium);
Assert.Equal(mockSubscription.CurrentPeriodEnd, user.PremiumExpirationDate);
Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate);
}
[Theory, BitAutoData]
@@ -368,7 +417,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();
@@ -384,7 +442,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
// Assert
Assert.True(result.IsT0);
Assert.True(user.Premium);
Assert.Equal(mockSubscription.CurrentPeriodEnd, user.PremiumExpirationDate);
Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate);
}
[Theory, BitAutoData]
@@ -411,7 +469,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active"; // PayPal + active doesn't match pattern
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();
@@ -453,7 +520,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "incomplete";
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();

View File

@@ -31,7 +31,7 @@ public class PreviewPremiumTaxCommandTests
var invoice = new Invoice
{
Tax = 300,
TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],
Total = 3300
};
@@ -65,7 +65,7 @@ public class PreviewPremiumTaxCommandTests
var invoice = new Invoice
{
Tax = 500,
TotalTaxes = [new InvoiceTotalTax { Amount = 500 }],
Total = 5500
};
@@ -101,7 +101,7 @@ public class PreviewPremiumTaxCommandTests
var invoice = new Invoice
{
Tax = 250,
TotalTaxes = [new InvoiceTotalTax { Amount = 250 }],
Total = 2750
};
@@ -135,7 +135,7 @@ public class PreviewPremiumTaxCommandTests
var invoice = new Invoice
{
Tax = 800,
TotalTaxes = [new InvoiceTotalTax { Amount = 800 }],
Total = 8800
};
@@ -171,7 +171,7 @@ public class PreviewPremiumTaxCommandTests
var invoice = new Invoice
{
Tax = 450,
TotalTaxes = [new InvoiceTotalTax { Amount = 450 }],
Total = 4950
};
@@ -207,7 +207,7 @@ public class PreviewPremiumTaxCommandTests
var invoice = new Invoice
{
Tax = 0,
TotalTaxes = [new InvoiceTotalTax { Amount = 0 }],
Total = 3000
};
@@ -241,7 +241,7 @@ public class PreviewPremiumTaxCommandTests
var invoice = new Invoice
{
Tax = 600,
TotalTaxes = [new InvoiceTotalTax { Amount = 600 }],
Total = 6600
};
@@ -276,7 +276,7 @@ public class PreviewPremiumTaxCommandTests
// Stripe amounts are in cents
var invoice = new Invoice
{
Tax = 123, // $1.23
TotalTaxes = [new InvoiceTotalTax { Amount = 123 }], // $1.23
Total = 3123 // $31.23
};

View File

@@ -1,10 +1,15 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Organizations.Services;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -118,4 +123,232 @@ public class OrganizationBillingServiceTests
}
#endregion
#region Finalize - Trial Settings
[Theory, BitAutoData]
public async Task NoPaymentMethodAndTrialPeriod_SetsMissingPaymentMethodCancelBehavior(
Organization organization,
SutProvider<OrganizationBillingService> sutProvider)
{
// Arrange
var plan = StaticStore.GetPlan(PlanType.TeamsAnnually);
organization.PlanType = PlanType.TeamsAnnually;
organization.GatewayCustomerId = "cus_test123";
organization.GatewaySubscriptionId = null;
var subscriptionSetup = new SubscriptionSetup
{
PlanType = PlanType.TeamsAnnually,
PasswordManagerOptions = new SubscriptionSetup.PasswordManager
{
Seats = 5,
Storage = null,
PremiumAccess = false
},
SecretsManagerOptions = null,
SkipTrial = false
};
var sale = new OrganizationSale
{
Organization = organization,
SubscriptionSetup = subscriptionSetup
};
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(PlanType.TeamsAnnually)
.Returns(plan);
sutProvider.GetDependency<IHasPaymentMethodQuery>()
.Run(organization)
.Returns(false);
var customer = new Customer
{
Id = "cus_test123",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())
.Returns(customer);
SubscriptionCreateOptions capturedOptions = null;
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionCreateAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))
.Returns(new Subscription
{
Id = "sub_test123",
Status = StripeConstants.SubscriptionStatus.Trialing
});
sutProvider.GetDependency<IOrganizationRepository>()
.ReplaceAsync(organization)
.Returns(Task.CompletedTask);
// Act
await sutProvider.Sut.Finalize(sale);
// Assert
await sutProvider.GetDependency<IStripeAdapter>()
.Received(1)
.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
Assert.NotNull(capturedOptions);
Assert.Equal(7, capturedOptions.TrialPeriodDays);
Assert.NotNull(capturedOptions.TrialSettings);
Assert.NotNull(capturedOptions.TrialSettings.EndBehavior);
Assert.Equal("cancel", capturedOptions.TrialSettings.EndBehavior.MissingPaymentMethod);
}
[Theory, BitAutoData]
public async Task NoPaymentMethodButNoTrial_DoesNotSetMissingPaymentMethodBehavior(
Organization organization,
SutProvider<OrganizationBillingService> sutProvider)
{
// Arrange
var plan = StaticStore.GetPlan(PlanType.TeamsAnnually);
organization.PlanType = PlanType.TeamsAnnually;
organization.GatewayCustomerId = "cus_test123";
organization.GatewaySubscriptionId = null;
var subscriptionSetup = new SubscriptionSetup
{
PlanType = PlanType.TeamsAnnually,
PasswordManagerOptions = new SubscriptionSetup.PasswordManager
{
Seats = 5,
Storage = null,
PremiumAccess = false
},
SecretsManagerOptions = null,
SkipTrial = true // This will result in TrialPeriodDays = 0
};
var sale = new OrganizationSale
{
Organization = organization,
SubscriptionSetup = subscriptionSetup
};
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(PlanType.TeamsAnnually)
.Returns(plan);
sutProvider.GetDependency<IHasPaymentMethodQuery>()
.Run(organization)
.Returns(false);
var customer = new Customer
{
Id = "cus_test123",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())
.Returns(customer);
SubscriptionCreateOptions capturedOptions = null;
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionCreateAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))
.Returns(new Subscription
{
Id = "sub_test123",
Status = StripeConstants.SubscriptionStatus.Active
});
sutProvider.GetDependency<IOrganizationRepository>()
.ReplaceAsync(organization)
.Returns(Task.CompletedTask);
// Act
await sutProvider.Sut.Finalize(sale);
// Assert
await sutProvider.GetDependency<IStripeAdapter>()
.Received(1)
.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
Assert.NotNull(capturedOptions);
Assert.Equal(0, capturedOptions.TrialPeriodDays);
Assert.Null(capturedOptions.TrialSettings);
}
[Theory, BitAutoData]
public async Task HasPaymentMethodAndTrialPeriod_DoesNotSetMissingPaymentMethodBehavior(
Organization organization,
SutProvider<OrganizationBillingService> sutProvider)
{
// Arrange
var plan = StaticStore.GetPlan(PlanType.TeamsAnnually);
organization.PlanType = PlanType.TeamsAnnually;
organization.GatewayCustomerId = "cus_test123";
organization.GatewaySubscriptionId = null;
var subscriptionSetup = new SubscriptionSetup
{
PlanType = PlanType.TeamsAnnually,
PasswordManagerOptions = new SubscriptionSetup.PasswordManager
{
Seats = 5,
Storage = null,
PremiumAccess = false
},
SecretsManagerOptions = null,
SkipTrial = false
};
var sale = new OrganizationSale
{
Organization = organization,
SubscriptionSetup = subscriptionSetup
};
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(PlanType.TeamsAnnually)
.Returns(plan);
sutProvider.GetDependency<IHasPaymentMethodQuery>()
.Run(organization)
.Returns(true); // Has payment method
var customer = new Customer
{
Id = "cus_test123",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())
.Returns(customer);
SubscriptionCreateOptions capturedOptions = null;
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionCreateAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))
.Returns(new Subscription
{
Id = "sub_test123",
Status = StripeConstants.SubscriptionStatus.Trialing
});
sutProvider.GetDependency<IOrganizationRepository>()
.ReplaceAsync(organization)
.Returns(Task.CompletedTask);
// Act
await sutProvider.Sut.Finalize(sale);
// Assert
await sutProvider.GetDependency<IStripeAdapter>()
.Received(1)
.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
Assert.NotNull(capturedOptions);
Assert.Equal(7, capturedOptions.TrialPeriodDays);
Assert.Null(capturedOptions.TrialSettings);
}
#endregion
}

View File

@@ -88,7 +88,13 @@ public class RestartSubscriptionCommandTests
var newSubscription = new Subscription
{
Id = "sub_new",
CurrentPeriodEnd = currentPeriodEnd
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
}
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
@@ -138,7 +144,13 @@ public class RestartSubscriptionCommandTests
var newSubscription = new Subscription
{
Id = "sub_new",
CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1)
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }
]
}
};
_subscriberService.GetSubscription(provider).Returns(existingSubscription);
@@ -177,7 +189,13 @@ public class RestartSubscriptionCommandTests
var newSubscription = new Subscription
{
Id = "sub_new",
CurrentPeriodEnd = currentPeriodEnd
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
}
};
_subscriberService.GetSubscription(user).Returns(existingSubscription);

View File

@@ -18,8 +18,9 @@ public class StripePaymentServiceTests
{
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
public async Task
PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
{
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
@@ -28,16 +29,13 @@ public class StripePaymentServiceTests
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually,
AdditionalStorage = 0
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
PasswordManager =
new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually,
AdditionalStorage = 0
},
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
};
sutProvider.GetDependency<IStripeAdapter>()
@@ -52,7 +50,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 4000,
Tax = 800,
TotalTaxes = [new InvoiceTotalTax { Amount = 800 }],
Total = 4800
});
@@ -75,16 +73,13 @@ public class StripePaymentServiceTests
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually,
AdditionalStorage = 1
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
PasswordManager =
new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually,
AdditionalStorage = 1
},
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
};
sutProvider.GetDependency<IStripeAdapter>()
@@ -96,12 +91,7 @@ public class StripePaymentServiceTests
p.SubscriptionDetails.Items.Any(x =>
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
x.Quantity == 1)))
.Returns(new Invoice
{
TotalExcludingTax = 4000,
Tax = 800,
Total = 4800
});
.Returns(new Invoice { TotalExcludingTax = 4000, TotalTaxes = [new InvoiceTotalTax { Amount = 800 }], Total = 4800 });
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
@@ -112,8 +102,9 @@ public class StripePaymentServiceTests
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
public async Task
PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
{
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
@@ -128,11 +119,7 @@ public class StripePaymentServiceTests
SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise,
AdditionalStorage = 0
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
};
sutProvider.GetDependency<IStripeAdapter>()
@@ -144,12 +131,7 @@ public class StripePaymentServiceTests
p.SubscriptionDetails.Items.Any(x =>
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
x.Quantity == 0)))
.Returns(new Invoice
{
TotalExcludingTax = 0,
Tax = 0,
Total = 0
});
.Returns(new Invoice { TotalExcludingTax = 0, TotalTaxes = [new InvoiceTotalTax { Amount = 0 }], Total = 0 });
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
@@ -160,8 +142,9 @@ public class StripePaymentServiceTests
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
public async Task
PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
{
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
@@ -176,11 +159,7 @@ public class StripePaymentServiceTests
SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise,
AdditionalStorage = 1
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
};
sutProvider.GetDependency<IStripeAdapter>()
@@ -192,12 +171,7 @@ public class StripePaymentServiceTests
p.SubscriptionDetails.Items.Any(x =>
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
x.Quantity == 1)))
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
.Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 });
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
@@ -235,7 +209,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});
@@ -277,7 +251,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});
@@ -319,7 +293,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});
@@ -361,7 +335,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});
@@ -403,7 +377,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});
@@ -445,7 +419,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});
@@ -487,7 +461,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});
@@ -529,7 +503,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});